From 02079f1f5ceede785b537073807bc4c67b68dbd0 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Wed, 27 Apr 2016 22:49:20 +0200 Subject: [PATCH 001/200] #432 Create custom dependency injector --- pom.xml | 7 + src/main/java/fr/xephi/authme/AntiBot.java | 2 + src/main/java/fr/xephi/authme/AuthMe.java | 154 ++++------ .../java/fr/xephi/authme/DataManager.java | 40 ++- src/main/java/fr/xephi/authme/api/API.java | 19 +- src/main/java/fr/xephi/authme/api/NewAPI.java | 3 + .../xephi/authme/command/CommandHandler.java | 9 +- .../xephi/authme/command/CommandMapper.java | 5 +- .../xephi/authme/command/CommandService.java | 58 ++-- .../authme/command/help/HelpProvider.java | 8 +- .../fr/xephi/authme/hooks/PluginHooks.java | 14 +- .../AuthMeServiceInitializer.java | 286 ++++++++++++++++++ .../authme/initialization/BaseCommands.java | 14 + .../initialization/ConstructorInjection.java | 77 +++++ .../authme/initialization/DataFolder.java | 14 + .../authme/initialization/FieldInjection.java | 112 +++++++ .../authme/initialization/Injection.java | 36 +++ .../{ => initialization}/MetricsStarter.java | 4 +- .../authme/listener/AuthMePlayerListener.java | 40 +-- .../authme/listener/AuthMeServerListener.java | 21 +- .../AuthMeTabCompletePacketAdapter.java | 3 + .../listener/AuthMeTablistPacketAdapter.java | 2 + .../authme/{ => mail}/ImageGenerator.java | 7 +- .../fr/xephi/authme/mail/SendMailSSL.java | 1 - .../authme/permission/PermissionsManager.java | 55 ++-- .../fr/xephi/authme/process/Management.java | 30 +- .../xephi/authme/process/ProcessService.java | 42 ++- .../authme/process/join/AsynchronousJoin.java | 7 +- .../authme/security/PasswordSecurity.java | 2 + .../fr/xephi/authme/settings/SpawnLoader.java | 5 +- .../fr/xephi/authme/util/BukkitService.java | 2 + src/main/java/fr/xephi/authme/util/Utils.java | 3 +- .../xephi/authme/util/ValidationService.java | 2 + .../authme/command/CommandServiceTest.java | 9 +- .../authme/command/help/HelpProviderTest.java | 6 +- .../AuthMeServiceInitializerTest.java | 194 ++++++++++++ .../initialization/samples/AlphaService.java | 20 ++ .../samples/BadFieldInjection.java | 16 + .../initialization/samples/BetaManager.java | 20 ++ .../samples/CircularClasses.java | 30 ++ .../samples/ClassWithAbstractDependency.java | 32 ++ .../samples/ClassWithAnnotations.java | 29 ++ .../initialization/samples/Duration.java | 14 + .../FieldInjectionWithAnnotations.java | 40 +++ .../initialization/samples/GammaService.java | 20 ++ .../initialization/samples/InvalidClass.java | 14 + .../samples/InvalidPostConstruct.java | 19 ++ .../samples/PostConstructTestClass.java | 37 +++ .../initialization/samples/ProvidedClass.java | 18 ++ .../authme/initialization/samples/Size.java | 14 + .../authme/process/ProcessServiceTest.java | 9 +- 51 files changed, 1347 insertions(+), 278 deletions(-) create mode 100644 src/main/java/fr/xephi/authme/initialization/AuthMeServiceInitializer.java create mode 100644 src/main/java/fr/xephi/authme/initialization/BaseCommands.java create mode 100644 src/main/java/fr/xephi/authme/initialization/ConstructorInjection.java create mode 100644 src/main/java/fr/xephi/authme/initialization/DataFolder.java create mode 100644 src/main/java/fr/xephi/authme/initialization/FieldInjection.java create mode 100644 src/main/java/fr/xephi/authme/initialization/Injection.java rename src/main/java/fr/xephi/authme/{ => initialization}/MetricsStarter.java (93%) rename src/main/java/fr/xephi/authme/{ => mail}/ImageGenerator.java (88%) create mode 100644 src/test/java/fr/xephi/authme/initialization/AuthMeServiceInitializerTest.java create mode 100644 src/test/java/fr/xephi/authme/initialization/samples/AlphaService.java create mode 100644 src/test/java/fr/xephi/authme/initialization/samples/BadFieldInjection.java create mode 100644 src/test/java/fr/xephi/authme/initialization/samples/BetaManager.java create mode 100644 src/test/java/fr/xephi/authme/initialization/samples/CircularClasses.java create mode 100644 src/test/java/fr/xephi/authme/initialization/samples/ClassWithAbstractDependency.java create mode 100644 src/test/java/fr/xephi/authme/initialization/samples/ClassWithAnnotations.java create mode 100644 src/test/java/fr/xephi/authme/initialization/samples/Duration.java create mode 100644 src/test/java/fr/xephi/authme/initialization/samples/FieldInjectionWithAnnotations.java create mode 100644 src/test/java/fr/xephi/authme/initialization/samples/GammaService.java create mode 100644 src/test/java/fr/xephi/authme/initialization/samples/InvalidClass.java create mode 100644 src/test/java/fr/xephi/authme/initialization/samples/InvalidPostConstruct.java create mode 100644 src/test/java/fr/xephi/authme/initialization/samples/PostConstructTestClass.java create mode 100644 src/test/java/fr/xephi/authme/initialization/samples/ProvidedClass.java create mode 100644 src/test/java/fr/xephi/authme/initialization/samples/Size.java diff --git a/pom.xml b/pom.xml index ec273c53..a9398738 100644 --- a/pom.xml +++ b/pom.xml @@ -405,6 +405,13 @@ true + + + javax.inject + javax.inject + 1 + + com.maxmind.geoip diff --git a/src/main/java/fr/xephi/authme/AntiBot.java b/src/main/java/fr/xephi/authme/AntiBot.java index 75a2a3b5..fcdb2206 100644 --- a/src/main/java/fr/xephi/authme/AntiBot.java +++ b/src/main/java/fr/xephi/authme/AntiBot.java @@ -9,6 +9,7 @@ import fr.xephi.authme.settings.properties.ProtectionSettings; import fr.xephi.authme.util.BukkitService; import org.bukkit.entity.Player; +import javax.inject.Inject; import java.util.ArrayList; import java.util.List; @@ -27,6 +28,7 @@ public class AntiBot { private final List antibotPlayers = new ArrayList<>(); private AntiBotStatus antiBotStatus = AntiBotStatus.DISABLED; + @Inject public AntiBot(NewSetting settings, Messages messages, PermissionsManager permissionsManager, BukkitService bukkitService) { this.settings = settings; diff --git a/src/main/java/fr/xephi/authme/AuthMe.java b/src/main/java/fr/xephi/authme/AuthMe.java index 08a1945c..812e88e9 100644 --- a/src/main/java/fr/xephi/authme/AuthMe.java +++ b/src/main/java/fr/xephi/authme/AuthMe.java @@ -7,12 +7,8 @@ import fr.xephi.authme.cache.auth.PlayerCache; import fr.xephi.authme.cache.backup.JsonCache; import fr.xephi.authme.cache.limbo.LimboCache; import fr.xephi.authme.cache.limbo.LimboPlayer; -import fr.xephi.authme.command.CommandDescription; import fr.xephi.authme.command.CommandHandler; import fr.xephi.authme.command.CommandInitializer; -import fr.xephi.authme.command.CommandMapper; -import fr.xephi.authme.command.CommandService; -import fr.xephi.authme.command.help.HelpProvider; import fr.xephi.authme.datasource.CacheDataSource; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.datasource.DataSourceType; @@ -21,8 +17,11 @@ import fr.xephi.authme.datasource.MySQL; import fr.xephi.authme.datasource.SQLite; import fr.xephi.authme.hooks.BungeeCordMessage; import fr.xephi.authme.hooks.PluginHooks; +import fr.xephi.authme.initialization.AuthMeServiceInitializer; +import fr.xephi.authme.initialization.BaseCommands; +import fr.xephi.authme.initialization.DataFolder; +import fr.xephi.authme.initialization.MetricsStarter; import fr.xephi.authme.listener.AuthMeBlockListener; -import fr.xephi.authme.listener.AuthMeEntityListener; import fr.xephi.authme.listener.AuthMeInventoryPacketAdapter; import fr.xephi.authme.listener.AuthMePlayerListener; import fr.xephi.authme.listener.AuthMePlayerListener16; @@ -38,7 +37,6 @@ import fr.xephi.authme.output.Messages; import fr.xephi.authme.permission.PermissionsManager; import fr.xephi.authme.permission.PlayerStatePermission; import fr.xephi.authme.process.Management; -import fr.xephi.authme.process.ProcessService; import fr.xephi.authme.security.PasswordSecurity; import fr.xephi.authme.security.crypts.SHA256; import fr.xephi.authme.settings.NewSetting; @@ -61,7 +59,6 @@ import fr.xephi.authme.util.GeoLiteAPI; import fr.xephi.authme.util.MigrationService; import fr.xephi.authme.util.StringUtils; import fr.xephi.authme.util.Utils; -import fr.xephi.authme.util.ValidationService; import org.apache.logging.log4j.LogManager; import org.bukkit.Bukkit; import org.bukkit.Location; @@ -71,6 +68,7 @@ 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.BukkitScheduler; import org.bukkit.scheduler.BukkitTask; import java.io.File; @@ -80,14 +78,12 @@ 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 static fr.xephi.authme.settings.properties.EmailSettings.MAIL_ACCOUNT; import static fr.xephi.authme.settings.properties.EmailSettings.MAIL_PASSWORD; import static fr.xephi.authme.settings.properties.EmailSettings.RECALL_PLAYERS; -import static fr.xephi.authme.settings.properties.PluginSettings.HELP_HEADER; /** * The AuthMe main class. @@ -103,7 +99,6 @@ public class AuthMe extends JavaPlugin { // Private Instances private static AuthMe plugin; - private static Server server; /* * Maps and stuff */ @@ -138,7 +133,6 @@ public class AuthMe extends JavaPlugin { private DataSource database; private PluginHooks pluginHooks; private SpawnLoader spawnLoader; - private AntiBot antiBot; private boolean autoPurging; private BukkitService bukkitService; @@ -215,17 +209,15 @@ public class AuthMe extends JavaPlugin { @Override public void onEnable() { // Set various instances - server = getServer(); plugin = this; ConsoleLogger.setLogger(getLogger()); - setPluginInfos(); // Load settings and custom configurations, if it fails, stop the server due to security reasons. newSettings = createNewSetting(); if (newSettings == null) { ConsoleLogger.showError("Could not load configuration. Aborting."); - server.shutdown(); + getServer().shutdown(); return; } ConsoleLogger.setLoggingOptions(newSettings.getProperty(SecuritySettings.USE_LOGGING), @@ -233,7 +225,7 @@ public class AuthMe extends JavaPlugin { // Old settings manager if (!loadSettings()) { - server.shutdown(); + getServer().shutdown(); setEnabled(false); return; } @@ -249,23 +241,40 @@ public class AuthMe extends JavaPlugin { stopOrUnload(); return; } - - bukkitService = new BukkitService(this); - pluginHooks = new PluginHooks(server.getPluginManager()); - MigrationService.changePlainTextToSha256(newSettings, database, new SHA256()); - passwordSecurity = new PasswordSecurity(getDataSource(), newSettings, Bukkit.getPluginManager()); - // Initialize spawn loader - spawnLoader = new SpawnLoader(getDataFolder(), newSettings, pluginHooks); - permsMan = initializePermissionsManager(); - antiBot = new AntiBot(newSettings, messages, permsMan, bukkitService); - ValidationService validationService = new ValidationService(newSettings, database, permsMan); - commandHandler = initializeCommandHandler(permsMan, messages, passwordSecurity, newSettings, - pluginHooks, spawnLoader, antiBot, validationService, bukkitService); + + AuthMeServiceInitializer initializer = new AuthMeServiceInitializer("fr.xephi.authme"); + // Register elements of the Bukkit / JavaPlugin environment + initializer.register(AuthMe.class, this); + initializer.register(Server.class, getServer()); + initializer.register(PluginManager.class, getServer().getPluginManager()); + initializer.register(BukkitScheduler.class, getServer().getScheduler()); + initializer.provide(DataFolder.class, getDataFolder()); + + // Register elements we instantiate manually + initializer.register(newSettings); + initializer.register(messages); + initializer.register(DataSource.class, database); + initializer.provide(BaseCommands.class, CommandInitializer.buildCommands()); + + // Some statically injected things + initializer.register(PlayerCache.class, PlayerCache.getInstance()); + initializer.register(LimboCache.class, LimboCache.getInstance()); + + permsMan = initializer.get(PermissionsManager.class); + bukkitService = initializer.get(BukkitService.class); + pluginHooks = initializer.get(PluginHooks.class); + passwordSecurity = initializer.get(PasswordSecurity.class); + spawnLoader = initializer.get(SpawnLoader.class); + commandHandler = initializer.get(CommandHandler.class); + api = initializer.get(NewAPI.class); + management = initializer.get(Management.class); + dataManager = initializer.get(DataManager.class); + initializer.get(API.class); // Set up Metrics - MetricsStarter.setupMetrics(plugin, newSettings); + MetricsStarter.setupMetrics(this, newSettings); // Set console filter setupConsoleFilter(); @@ -282,22 +291,12 @@ public class AuthMe extends JavaPlugin { // End of Hooks // Do a backup on start - new PerformBackup(plugin, newSettings).doBackup(PerformBackup.BackupCause.START); + new PerformBackup(this, newSettings).doBackup(PerformBackup.BackupCause.START); // Setup the inventory backup playerBackup = new JsonCache(); - // Set the DataManager - dataManager = new DataManager(this, pluginHooks, bukkitService); - - // Set up the new API - setupApi(); - - // Set up the management - ProcessService processService = new ProcessService(newSettings, messages, this, database, - passwordSecurity, pluginHooks, spawnLoader, validationService, bukkitService); - management = new Management(this, processService, database, PlayerCache.getInstance()); // Set up the BungeeCord hook setupBungeeCordHook(newSettings); @@ -306,7 +305,7 @@ public class AuthMe extends JavaPlugin { reloadSupportHook(); // Register event listeners - registerEventListeners(messages, database, management, pluginHooks, spawnLoader, antiBot); + registerEventListeners(initializer); // Start Email recall task if needed scheduleRecallEmailTask(); @@ -370,29 +369,27 @@ public class AuthMe extends JavaPlugin { /** * Register all event listeners. */ - private void registerEventListeners(Messages messages, DataSource dataSource, Management management, - PluginHooks pluginHooks, SpawnLoader spawnLoader, AntiBot antiBot) { + private void registerEventListeners(AuthMeServiceInitializer initializer) { // Get the plugin manager instance - PluginManager pluginManager = server.getPluginManager(); + PluginManager pluginManager = getServer().getPluginManager(); // Register event listeners - pluginManager.registerEvents(new AuthMePlayerListener( - this, newSettings, messages, dataSource, antiBot, management, bukkitService), this); - pluginManager.registerEvents(new AuthMeBlockListener(), this); - pluginManager.registerEvents(new AuthMeEntityListener(), this); - pluginManager.registerEvents(new AuthMeServerListener(this, messages, pluginHooks, spawnLoader), this); + pluginManager.registerEvents(initializer.get(AuthMePlayerListener.class), this); + pluginManager.registerEvents(initializer.get(AuthMeBlockListener.class), this); + pluginManager.registerEvents(initializer.get(AuthMePlayerListener.class), this); + pluginManager.registerEvents(initializer.get(AuthMeServerListener.class), this); // Try to register 1.6 player listeners try { Class.forName("org.bukkit.event.player.PlayerEditBookEvent"); - pluginManager.registerEvents(new AuthMePlayerListener16(), this); + pluginManager.registerEvents(initializer.get(AuthMePlayerListener16.class), this); } catch (ClassNotFoundException ignore) { } // Try to register 1.8 player listeners try { Class.forName("org.bukkit.event.player.PlayerInteractAtEntityEvent"); - pluginManager.registerEvents(new AuthMePlayerListener18(), this); + pluginManager.registerEvents(initializer.get(AuthMePlayerListener18.class), this); } catch (ClassNotFoundException ignore) { } } @@ -426,30 +423,6 @@ public class AuthMe extends JavaPlugin { } } - private CommandHandler initializeCommandHandler(PermissionsManager permissionsManager, Messages messages, - PasswordSecurity passwordSecurity, NewSetting settings, - PluginHooks pluginHooks, SpawnLoader spawnLoader, AntiBot antiBot, - ValidationService validationService, BukkitService bukkitService) { - HelpProvider helpProvider = new HelpProvider(permissionsManager, settings.getProperty(HELP_HEADER)); - Set baseCommands = CommandInitializer.buildCommands(); - CommandMapper mapper = new CommandMapper(baseCommands, permissionsManager); - CommandService commandService = new CommandService(this, mapper, helpProvider, messages, passwordSecurity, - permissionsManager, settings, pluginHooks, spawnLoader, antiBot, validationService, bukkitService); - return new CommandHandler(commandService); - } - - /** - * Set up the API. This sets up the new and the old API. - */ - @SuppressWarnings("deprecation") - private void setupApi() { - // Set up the API - api = new NewAPI(this); - - // Set up the deprecated API - new API(this); - } - /** * Load the plugin's settings. * @@ -462,7 +435,7 @@ public class AuthMe extends JavaPlugin { } catch (Exception e) { ConsoleLogger.logException("Can't load the configuration file... Something went wrong. " + "To avoid security issues the server will shut down!", e); - server.shutdown(); + getServer().shutdown(); } return false; } @@ -507,14 +480,15 @@ public class AuthMe extends JavaPlugin { // Do backup on stop if enabled if (newSettings != null) { - new PerformBackup(plugin, newSettings).doBackup(PerformBackup.BackupCause.STOP); + new PerformBackup(this, newSettings).doBackup(PerformBackup.BackupCause.STOP); } + final AuthMe pluginInstance = this; new Thread(new Runnable() { @Override public void run() { List pendingTasks = new ArrayList<>(); for (BukkitTask pendingTask : getServer().getScheduler().getPendingTasks()) { - if (pendingTask.getOwner().equals(plugin) && !pendingTask.isSync()) { + if (pendingTask.getOwner().equals(pluginInstance) && !pendingTask.isSync()) { pendingTasks.add(pendingTask.getTaskId()); } } @@ -553,9 +527,9 @@ public class AuthMe extends JavaPlugin { public void stopOrUnload() { if (Settings.isStopEnabled) { ConsoleLogger.showError("THE SERVER IS GOING TO SHUT DOWN AS DEFINED IN THE CONFIGURATION!"); - server.shutdown(); + getServer().shutdown(); } else { - server.getPluginManager().disablePlugin(AuthMe.getInstance()); + getServer().getPluginManager().disablePlugin(AuthMe.getInstance()); } } @@ -598,7 +572,7 @@ public class AuthMe extends JavaPlugin { database = dataSource; if (DataSourceType.SQLITE == dataSourceType) { - server.getScheduler().runTaskAsynchronously(this, new Runnable() { + getServer().getScheduler().runTaskAsynchronously(this, new Runnable() { @Override public void run() { int accounts = database.getAccountsRegistered(); @@ -611,15 +585,6 @@ public class AuthMe extends JavaPlugin { } } - /** - * Set up the permissions manager. - */ - private PermissionsManager initializePermissionsManager() { - PermissionsManager manager = new PermissionsManager(Bukkit.getServer(), getLogger()); - manager.setup(); - return manager; - } - // Set the console filter to remove the passwords private void setLog4JFilter() { Bukkit.getScheduler().scheduleSyncDelayedTask(this, new Runnable() { @@ -636,7 +601,7 @@ public class AuthMe extends JavaPlugin { // Check the presence of the ProtocolLib plugin public void checkProtocolLib() { - if (!server.getPluginManager().isPluginEnabled("ProtocolLib")) { + if (!getServer().getPluginManager().isPluginEnabled("ProtocolLib")) { if (newSettings.getProperty(RestrictionSettings.PROTECT_INVENTORY_BEFORE_LOGIN)) { ConsoleLogger.showError("WARNING! The protectInventory feature requires ProtocolLib! Disabling it..."); Settings.protectInventoryBeforeLogInEnabled = false; @@ -671,7 +636,7 @@ public class AuthMe extends JavaPlugin { // Save Player Data private void savePlayer(Player player) { - if (Utils.isNPC(player) || Utils.isUnrestricted(player)) { + if (safeIsNpc(player) || Utils.isUnrestricted(player)) { return; } String name = player.getName().toLowerCase(); @@ -699,6 +664,10 @@ public class AuthMe extends JavaPlugin { PlayerCache.getInstance().removePlayer(name); } + private boolean safeIsNpc(Player player) { + return pluginHooks != null && pluginHooks.isNpc(player) || player.hasMetadata("NPC"); + } + // Select the player to kick when a vip player joins the server when full public Player generateKickPlayer(Collection collection) { for (Player player : collection) { @@ -715,7 +684,7 @@ public class AuthMe extends JavaPlugin { return; } autoPurging = true; - server.getScheduler().runTaskAsynchronously(this, new Runnable() { + getServer().getScheduler().runTaskAsynchronously(this, new Runnable() { @Override public void run() { ConsoleLogger.info("AutoPurging the Database..."); @@ -772,6 +741,7 @@ public class AuthMe extends JavaPlugin { public String replaceAllInfo(String message, Player player) { String playersOnline = Integer.toString(bukkitService.getOnlinePlayers().size()); String ipAddress = Utils.getPlayerIp(player); + Server server = getServer(); return message .replace("&", "\u00a7") .replace("{PLAYER}", player.getName()) diff --git a/src/main/java/fr/xephi/authme/DataManager.java b/src/main/java/fr/xephi/authme/DataManager.java index 061683fa..f5751d42 100644 --- a/src/main/java/fr/xephi/authme/DataManager.java +++ b/src/main/java/fr/xephi/authme/DataManager.java @@ -2,32 +2,37 @@ package fr.xephi.authme; import fr.xephi.authme.hooks.PluginHooks; import fr.xephi.authme.permission.PermissionsManager; +import fr.xephi.authme.settings.NewSetting; import fr.xephi.authme.settings.properties.PurgeSettings; import fr.xephi.authme.util.BukkitService; import fr.xephi.authme.util.Utils; import org.bukkit.Bukkit; import org.bukkit.OfflinePlayer; +import org.bukkit.Server; +import javax.inject.Inject; import java.io.File; import java.util.ArrayList; import java.util.List; +import static fr.xephi.authme.util.StringUtils.makePath; + /** */ public class DataManager { - private final AuthMe plugin; - private final PluginHooks pluginHooks; - private final BukkitService bukkitService; + @Inject + private Server server; + @Inject + private PluginHooks pluginHooks; + @Inject + private BukkitService bukkitService; + @Inject + private NewSetting settings; + @Inject + private PermissionsManager permissionsManager; - /* - * Constructor. - */ - public DataManager(AuthMe plugin, PluginHooks pluginHooks, BukkitService bukkitService) { - this.plugin = plugin; - this.pluginHooks = pluginHooks; - this.bukkitService = bukkitService; - } + DataManager() { } private List getOfflinePlayers(List names) { List result = new ArrayList<>(); @@ -98,9 +103,8 @@ public class DataManager { public synchronized void purgeDat(List cleared) { int i = 0; - File dataFolder = new File(plugin.getServer().getWorldContainer() - + File.separator + plugin.getSettings().getProperty(PurgeSettings.DEFAULT_WORLD) - + File.separator + "players"); + File dataFolder = new File(server.getWorldContainer(), + makePath(settings.getProperty(PurgeSettings.DEFAULT_WORLD), "players")); List offlinePlayers = getOfflinePlayers(cleared); for (OfflinePlayer player : offlinePlayers) { File playerFile = new File(dataFolder, Utils.getUUIDorName(player) + ".dat"); @@ -142,14 +146,8 @@ public class DataManager { // TODO: What is this method for? Is it correct? // TODO: Make it work with OfflinePlayers group data. public synchronized void purgePermissions(List cleared) { - // Get the permissions manager, and make sure it's valid - PermissionsManager permsMan = plugin.getPermissionsManager(); - if (permsMan == null) { - ConsoleLogger.showError("Unable to access permissions manager instance!"); - return; - } for (String name : cleared) { - permsMan.removeAllGroups(bukkitService.getPlayerExact(name)); + permissionsManager.removeAllGroups(bukkitService.getPlayerExact(name)); } ConsoleLogger.info("AutoPurge: Removed permissions from " + cleared.size() + " player(s)."); } diff --git a/src/main/java/fr/xephi/authme/api/API.java b/src/main/java/fr/xephi/authme/api/API.java index 58960f9d..26f5ea8f 100644 --- a/src/main/java/fr/xephi/authme/api/API.java +++ b/src/main/java/fr/xephi/authme/api/API.java @@ -3,6 +3,8 @@ package fr.xephi.authme.api; import fr.xephi.authme.AuthMe; import fr.xephi.authme.cache.auth.PlayerAuth; import fr.xephi.authme.cache.auth.PlayerCache; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.process.Management; import fr.xephi.authme.security.PasswordSecurity; import fr.xephi.authme.security.crypts.HashedPassword; import fr.xephi.authme.util.Utils; @@ -12,6 +14,8 @@ import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; import org.bukkit.plugin.Plugin; +import javax.inject.Inject; + /** * Deprecated API of AuthMe. Please use {@link NewAPI} instead. */ @@ -20,7 +24,9 @@ public class API { public static final String newline = System.getProperty("line.separator"); public static AuthMe instance; + private static DataSource dataSource; private static PasswordSecurity passwordSecurity; + private static Management management; /** * Constructor for the deprecated API. @@ -28,9 +34,12 @@ public class API { * @param instance AuthMe */ @Deprecated - public API(AuthMe instance) { + @Inject + API(AuthMe instance, DataSource dataSource, PasswordSecurity passwordSecurity, Management management) { API.instance = instance; - passwordSecurity = instance.getPasswordSecurity(); + API.dataSource = dataSource; + API.passwordSecurity = passwordSecurity; + API.management = management; } /** @@ -109,7 +118,7 @@ public class API { @Deprecated public static boolean isRegistered(String playerName) { String player = playerName.toLowerCase(); - return instance.getDataSource().isAuthAvailable(player); + return dataSource.isAuthAvailable(player); } /** @@ -144,7 +153,7 @@ public class API { .lastLogin(0) .realName(playerName) .build(); - return instance.getDataSource().saveAuth(auth); + return dataSource.saveAuth(auth); } /** @@ -154,7 +163,7 @@ public class API { */ @Deprecated public static void forceLogin(Player player) { - instance.getManagement().performLogin(player, "dontneed", true); + management.performLogin(player, "dontneed", true); } @Deprecated diff --git a/src/main/java/fr/xephi/authme/api/NewAPI.java b/src/main/java/fr/xephi/authme/api/NewAPI.java index 0f543762..d3a0b7d8 100644 --- a/src/main/java/fr/xephi/authme/api/NewAPI.java +++ b/src/main/java/fr/xephi/authme/api/NewAPI.java @@ -12,6 +12,8 @@ import fr.xephi.authme.cache.auth.PlayerCache; import fr.xephi.authme.security.crypts.HashedPassword; import fr.xephi.authme.util.Utils; +import javax.inject.Inject; + /** * The current API of AuthMe. Recommended method of retrieving the API object: * @@ -28,6 +30,7 @@ public class NewAPI { * * @param plugin The AuthMe plugin instance */ + @Inject public NewAPI(AuthMe plugin) { this.plugin = plugin; } diff --git a/src/main/java/fr/xephi/authme/command/CommandHandler.java b/src/main/java/fr/xephi/authme/command/CommandHandler.java index 37c1a2a1..5f7b32af 100644 --- a/src/main/java/fr/xephi/authme/command/CommandHandler.java +++ b/src/main/java/fr/xephi/authme/command/CommandHandler.java @@ -1,14 +1,14 @@ 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 fr.xephi.authme.util.StringUtils; import org.bukkit.ChatColor; import org.bukkit.command.CommandSender; -import fr.xephi.authme.util.StringUtils; +import javax.inject.Inject; +import java.util.ArrayList; +import java.util.List; /** * The AuthMe command handler, responsible for mapping incoming commands to the correct {@link CommandDescription} @@ -29,6 +29,7 @@ public class CommandHandler { * * @param commandService The CommandService instance */ + @Inject public CommandHandler(CommandService commandService) { this.commandService = commandService; } diff --git a/src/main/java/fr/xephi/authme/command/CommandMapper.java b/src/main/java/fr/xephi/authme/command/CommandMapper.java index e43ee4bb..18473b28 100644 --- a/src/main/java/fr/xephi/authme/command/CommandMapper.java +++ b/src/main/java/fr/xephi/authme/command/CommandMapper.java @@ -1,11 +1,13 @@ package fr.xephi.authme.command; import fr.xephi.authme.command.executable.HelpCommand; +import fr.xephi.authme.initialization.BaseCommands; import fr.xephi.authme.permission.PermissionsManager; import fr.xephi.authme.util.CollectionUtils; import fr.xephi.authme.util.StringUtils; import org.bukkit.command.CommandSender; +import javax.inject.Inject; import java.util.ArrayList; import java.util.List; import java.util.Set; @@ -28,7 +30,8 @@ public class CommandMapper { private final Set baseCommands; private final PermissionsManager permissionsManager; - public CommandMapper(Set baseCommands, PermissionsManager permissionsManager) { + @Inject + public CommandMapper(@BaseCommands Set baseCommands, PermissionsManager permissionsManager) { this.baseCommands = baseCommands; this.permissionsManager = permissionsManager; } diff --git a/src/main/java/fr/xephi/authme/command/CommandService.java b/src/main/java/fr/xephi/authme/command/CommandService.java index 5c5abfcc..d4262944 100644 --- a/src/main/java/fr/xephi/authme/command/CommandService.java +++ b/src/main/java/fr/xephi/authme/command/CommandService.java @@ -19,6 +19,7 @@ import fr.xephi.authme.util.ValidationService; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; +import javax.inject.Inject; import java.util.Collection; import java.util.List; @@ -28,39 +29,30 @@ import java.util.List; */ public class CommandService { - private final AuthMe authMe; - private final Messages messages; - private final HelpProvider helpProvider; - private final CommandMapper commandMapper; - private final PasswordSecurity passwordSecurity; - private final PermissionsManager permissionsManager; - private final NewSetting settings; - private final PluginHooks pluginHooks; - private final SpawnLoader spawnLoader; - private final AntiBot antiBot; - private final ValidationService validationService; - private final BukkitService bukkitService; - - /* - * Constructor. - */ - public CommandService(AuthMe authMe, CommandMapper commandMapper, HelpProvider helpProvider, Messages messages, - PasswordSecurity passwordSecurity, PermissionsManager permissionsManager, NewSetting settings, - PluginHooks pluginHooks, SpawnLoader spawnLoader, AntiBot antiBot, - ValidationService validationService, BukkitService bukkitService) { - this.authMe = authMe; - this.messages = messages; - this.helpProvider = helpProvider; - this.commandMapper = commandMapper; - this.passwordSecurity = passwordSecurity; - this.permissionsManager = permissionsManager; - this.settings = settings; - this.pluginHooks = pluginHooks; - this.spawnLoader = spawnLoader; - this.antiBot = antiBot; - this.validationService = validationService; - this.bukkitService = bukkitService; - } + @Inject + private AuthMe authMe; + @Inject + private Messages messages; + @Inject + private HelpProvider helpProvider; + @Inject + private CommandMapper commandMapper; + @Inject + private PasswordSecurity passwordSecurity; + @Inject + private PermissionsManager permissionsManager; + @Inject + private NewSetting settings; + @Inject + private PluginHooks pluginHooks; + @Inject + private SpawnLoader spawnLoader; + @Inject + private AntiBot antiBot; + @Inject + private ValidationService validationService; + @Inject + private BukkitService bukkitService; /** * Send a message to a player. diff --git a/src/main/java/fr/xephi/authme/command/help/HelpProvider.java b/src/main/java/fr/xephi/authme/command/help/HelpProvider.java index 7bc06f03..c5a3ec5f 100644 --- a/src/main/java/fr/xephi/authme/command/help/HelpProvider.java +++ b/src/main/java/fr/xephi/authme/command/help/HelpProvider.java @@ -10,10 +10,13 @@ import fr.xephi.authme.command.FoundCommandResult; import fr.xephi.authme.permission.DefaultPermission; import fr.xephi.authme.permission.PermissionNode; import fr.xephi.authme.permission.PermissionsManager; +import fr.xephi.authme.settings.NewSetting; +import fr.xephi.authme.settings.properties.PluginSettings; import fr.xephi.authme.util.CollectionUtils; import org.bukkit.ChatColor; import org.bukkit.command.CommandSender; +import javax.inject.Inject; import java.util.ArrayList; import java.util.List; @@ -45,9 +48,10 @@ public class HelpProvider { private final PermissionsManager permissionsManager; private final String helpHeader; - public HelpProvider(PermissionsManager permissionsManager, String helpHeader) { + @Inject + public HelpProvider(PermissionsManager permissionsManager, NewSetting settings) { this.permissionsManager = permissionsManager; - this.helpHeader = helpHeader; + this.helpHeader = settings.getProperty(PluginSettings.HELP_HEADER); } public List printHelp(CommandSender sender, FoundCommandResult result, int options) { diff --git a/src/main/java/fr/xephi/authme/hooks/PluginHooks.java b/src/main/java/fr/xephi/authme/hooks/PluginHooks.java index 3a54709a..76f8e034 100644 --- a/src/main/java/fr/xephi/authme/hooks/PluginHooks.java +++ b/src/main/java/fr/xephi/authme/hooks/PluginHooks.java @@ -11,6 +11,7 @@ import org.bukkit.entity.Player; import org.bukkit.plugin.Plugin; import org.bukkit.plugin.PluginManager; +import javax.inject.Inject; import java.io.File; /** @@ -28,6 +29,7 @@ public class PluginHooks { * * @param pluginManager The server's plugin manager */ + @Inject public PluginHooks(PluginManager pluginManager) { this.pluginManager = pluginManager; tryHookToCombatPlus(); @@ -75,13 +77,23 @@ public class PluginHooks { return null; } + /** + * Checks whether the player is an NPC. + * + * @param player The player to process + * @return True if player is NPC, false otherwise + */ + public boolean isNpc(Player player) { + return player.hasMetadata("NPC") || isNpcInCombatTagPlus(player); + } + /** * Query the CombatTagPlus plugin whether the given player is an NPC. * * @param player The player to verify * @return True if the player is an NPC according to CombatTagPlus, false if not or if the plugin is unavailable */ - public boolean isNpcInCombatTagPlus(Player player) { + private boolean isNpcInCombatTagPlus(Player player) { return combatTagPlus != null && combatTagPlus.getNpcPlayerHelper().isNpc(player); } diff --git a/src/main/java/fr/xephi/authme/initialization/AuthMeServiceInitializer.java b/src/main/java/fr/xephi/authme/initialization/AuthMeServiceInitializer.java new file mode 100644 index 00000000..b751b32e --- /dev/null +++ b/src/main/java/fr/xephi/authme/initialization/AuthMeServiceInitializer.java @@ -0,0 +1,286 @@ +package fr.xephi.authme.initialization; + +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableSet; + +import javax.annotation.PostConstruct; +import javax.inject.Provider; +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * Dependency injector of AuthMe: initializes and injects services and tasks. + *

+ * Only constructor and field injection are supported, indicated with the JSR330 + * {@link javax.inject.Inject @Inject} annotation. + *

+ * {@link PostConstruct @PostConstruct} methods are recognized and invoked upon + * instantiation. Note that the parent classes are not scanned for such methods. + */ +public class AuthMeServiceInitializer { + + private final Set ALLOWED_PACKAGES; + private final Map, Object> objects; + + /** + * Constructor. + * + * @param allowedPackages list of allowed packages. Only classes whose package + * starts with any of the given entries will be instantiated + */ + public AuthMeServiceInitializer(String... allowedPackages) { + ALLOWED_PACKAGES = ImmutableSet.copyOf(allowedPackages); + objects = new HashMap<>(); + } + + /** + * Retrieves or instantiates an object of the given type. + * + * @param clazz the class to retrieve the value for + * @param the class' type + * @return object of the class' type + */ + public T get(Class clazz) { + return get(clazz, new HashSet>()); + } + + /** + * Registers an instantiation by its type. + * + * @param object the object to register + * @throws IllegalStateException if an object of the same type has already been registered + */ + public void register(Object object) { + if (object instanceof Type) { + throw new IllegalStateException("You tried to register a Type object: '" + object + + "'. This likely indicates an error. Please use register(Class, T) if really desired."); + } + storeObject(object); + } + + /** + * Register an object with a custom class (supertype). Use this for example to specify a + * concrete implementation of an interface or an abstract class. + * + * @param clazz the class to register the object for + * @param object the object + * @param the class' type + */ + public void register(Class clazz, T object) { + if (objects.containsKey(clazz)) { + throw new IllegalStateException("There is already an object present for " + clazz); + } + Preconditions.checkNotNull(object); + objects.put(clazz, object); + } + + /** + * Associate an annotation with a value. + * + * @param annotation the annotation + * @param value the value + */ + public void provide(Class annotation, Object value) { + if (objects.containsKey(annotation)) { + throw new IllegalStateException("Annotation @" + annotation.getClass().getSimpleName() + + " already registered"); + } + Preconditions.checkNotNull(value); + objects.put(annotation, value); + } + + /** + * Returns an instance of the given class or the value associated with an annotation, + * by retrieving it or by instantiating it if not yet present. + * + * @param clazz the class to retrieve a value for + * @param traversedClasses the list of traversed classes + * @param the class' type + * @return instance or associated value (for annotations) + */ + private T get(Class clazz, Set> traversedClasses) { + if (Annotation.class.isAssignableFrom(clazz)) { + throw new UnsupportedOperationException("Cannot retrieve annotated elements in this way!"); + } else if (objects.containsKey(clazz)) { + return getObject(clazz); + } + + // First time we come across clazz, need to instantiate it. Add the clazz to the list of traversed + // classes in a new list, so each path we need to take has its own Set. + validatePackage(clazz); + validateInstantiable(clazz); + + traversedClasses = new HashSet<>(traversedClasses); + traversedClasses.add(clazz); + System.out.println(Strings.repeat(" ", traversedClasses.size() * 2) + "- Instantiating " + clazz); + return instantiate(clazz, traversedClasses); + } + + /** + * Instantiates the given class by locating an @Inject constructor and retrieving + * or instantiating its parameters. + * + * @param clazz the class to instantiate + * @param traversedClasses collection of classes already traversed + * @param the class' type + * @return the instantiated object + */ + private T instantiate(Class clazz, Set> traversedClasses) { + Injection injection = firstNotNull(ConstructorInjection.provide(clazz), FieldInjection.provide(clazz)); + if (injection == null) { + throw new IllegalStateException("Did not find injection method for " + clazz + ". Make sure you have " + + "a constructor with @Inject or fields with @Inject. Fields with @Inject require " + + "the default constructor"); + } + + validateInjectionHasNoCircularDependencies(injection.getDependencies(), traversedClasses); + Object[] dependencies = resolveDependencies(injection, traversedClasses); + T object = injection.instantiateWith(dependencies); + storeObject(object); + executePostConstructMethods(object); + return object; + } + + /** + * Resolves the dependencies for the given constructor, i.e. returns a collection that satisfy + * the constructor's parameter types by retrieving elements or instantiating them where necessary. + * + * @param injection the injection parameters + * @param traversedClasses collection of traversed classes + * @return array with the parameters to use in the constructor + */ + private Object[] resolveDependencies(Injection injection, Set> traversedClasses) { + Class[] dependencies = injection.getDependencies(); + Class[] annotations = injection.getDependencyAnnotations(); + Object[] values = new Object[dependencies.length]; + for (int i = 0; i < dependencies.length; ++i) { + if (annotations[i] != null) { + Object value = objects.get(annotations[i]); + if (value == null) { + throw new IllegalStateException("Value for field with @" + annotations[i].getSimpleName() + + " must be registered beforehand"); + } + values[i] = value; + } else { + values[i] = get(dependencies[i], traversedClasses); + } + } + return values; + } + + + /** + * Internal method to retrieve an object from the objects map for non-annotation classes. + * In such cases, the type of the entry always corresponds to the key, i.e. the entry of key + * {@code Class} is guaranteed to be of type {@code T}. + *

+ * To retrieve values identified with an annotation, use {@code objects.get(clazz)} directly. + * We do not know or control the type of the value of keys of annotation classes. + * + * @param clazz the class to retrieve the implementation of + * @param the type + * @return the implementation + */ + private T getObject(Class clazz) { + Object o = objects.get(clazz); + if (o == null) { + throw new NullPointerException("No instance of " + clazz + " available"); + } + return clazz.cast(o); + } + + /** + * Stores the given object with its class as key. Throws an exception if the key already has + * a value associated to it. + * + * @param object the object to store + */ + private void storeObject(Object object) { + if (objects.containsKey(object.getClass())) { + throw new IllegalStateException("There is already an object present for " + object.getClass()); + } + Preconditions.checkNotNull(object); + objects.put(object.getClass(), object); + } + + /** + * Validates that none of the dependencies' types are present in the given collection + * of traversed classes. This prevents circular dependencies. + * + * @param dependencies the dependencies of the class + * @param traversedClasses the collection of traversed classes + */ + private static void validateInjectionHasNoCircularDependencies(Class[] dependencies, + Set> traversedClasses) { + for (Class clazz : dependencies) { + if (traversedClasses.contains(clazz)) { + throw new IllegalStateException("Found cyclic dependency - already traversed '" + clazz + + "' (full traversal list: " + traversedClasses + ")"); + } + } + } + + /** + * Validates the package of a parameter type to ensure that it is part of the allowed packages. + * This ensures that we don't try to instantiate things that are beyond our reach in case some + * external parameter type has not been registered. + * + * @param clazz the class to validate + */ + private void validatePackage(Class clazz) { + if (clazz.getPackage() == null) { + throw new IllegalStateException("Primitive types must be provided explicitly (or use an annotation)."); + } + String packageName = clazz.getPackage().getName(); + for (String allowedPackage : ALLOWED_PACKAGES) { + if (packageName.startsWith(allowedPackage)) { + return; + } + } + throw new IllegalStateException("Class " + clazz + " with package " + packageName + " is outside of the " + + "allowed packages. It must be provided explicitly or the package must be passed to the constructor."); + } + + private static void executePostConstructMethods(Object object) { + for (Method method : object.getClass().getDeclaredMethods()) { + if (method.isAnnotationPresent(PostConstruct.class)) { + if (method.getParameterCount() == 0) { + try { + method.setAccessible(true); + method.invoke(object); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new UnsupportedOperationException(e); + } + } else { + throw new IllegalStateException("@PostConstruct methods must have an empty parameter list. " + + "Found parameters in " + method + " belonging to " + object.getClass()); + } + } + } + } + + private static void validateInstantiable(Class clazz) { + if (clazz.isEnum() || clazz.isInterface() || Modifier.isAbstract(clazz.getModifiers())) { + throw new IllegalStateException("Class " + clazz.getSimpleName() + " cannot be instantiated"); + } + } + + @SafeVarargs + private static Injection firstNotNull(Provider>... providers) { + for (Provider> provider : providers) { + Injection object = provider.get(); + if (object != null) { + return object; + } + } + return null; + } +} diff --git a/src/main/java/fr/xephi/authme/initialization/BaseCommands.java b/src/main/java/fr/xephi/authme/initialization/BaseCommands.java new file mode 100644 index 00000000..156cb947 --- /dev/null +++ b/src/main/java/fr/xephi/authme/initialization/BaseCommands.java @@ -0,0 +1,14 @@ +package fr.xephi.authme.initialization; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to denote the collection of AuthMe commands. + */ +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface BaseCommands { +} diff --git a/src/main/java/fr/xephi/authme/initialization/ConstructorInjection.java b/src/main/java/fr/xephi/authme/initialization/ConstructorInjection.java new file mode 100644 index 00000000..f77ae58c --- /dev/null +++ b/src/main/java/fr/xephi/authme/initialization/ConstructorInjection.java @@ -0,0 +1,77 @@ +package fr.xephi.authme.initialization; + +import javax.inject.Inject; +import javax.inject.Provider; +import java.lang.annotation.Annotation; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; + +/** + * Functionality for constructor injection. + */ +class ConstructorInjection implements Injection { + + private final Constructor constructor; + + private ConstructorInjection(Constructor constructor) { + this.constructor = constructor; + } + + @Override + public Class[] getDependencies() { + return constructor.getParameterTypes(); + } + + @Override + public Class[] getDependencyAnnotations() { + Annotation[][] parameterAnnotations = constructor.getParameterAnnotations(); + Class[] annotations = new Class[parameterAnnotations.length]; + for (int i = 0; i < parameterAnnotations.length; ++i) { + annotations[i] = parameterAnnotations[i].length > 0 + ? parameterAnnotations[i][0].annotationType() + : null; + } + return annotations; + } + + @Override + public T instantiateWith(Object... values) { + try { + return constructor.newInstance(values); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { + throw new UnsupportedOperationException(e); + } + } + + public static Provider> provide(final Class clazz) { + return new Provider>() { + @Override + public ConstructorInjection get() { + Constructor constructor = getInjectionConstructor(clazz); + return constructor == null ? null : new ConstructorInjection<>(constructor); + } + }; + } + + + /** + * Gets the first found constructor annotated with {@link Inject} of the given class + * and marks it as accessible. + * + * @param clazz the class to process + * @param the class' type + * @return injection constructor for the class + */ + @SuppressWarnings("unchecked") + private static Constructor getInjectionConstructor(Class clazz) { + Constructor[] constructors = clazz.getDeclaredConstructors(); + for (Constructor constructor : constructors) { + if (constructor.isAnnotationPresent(Inject.class)) { + constructor.setAccessible(true); + return (Constructor) constructor; + } + } + return null; + } + +} diff --git a/src/main/java/fr/xephi/authme/initialization/DataFolder.java b/src/main/java/fr/xephi/authme/initialization/DataFolder.java new file mode 100644 index 00000000..33b87997 --- /dev/null +++ b/src/main/java/fr/xephi/authme/initialization/DataFolder.java @@ -0,0 +1,14 @@ +package fr.xephi.authme.initialization; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation for specifying the plugin's data folder. + */ +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface DataFolder { +} diff --git a/src/main/java/fr/xephi/authme/initialization/FieldInjection.java b/src/main/java/fr/xephi/authme/initialization/FieldInjection.java new file mode 100644 index 00000000..aafb0bde --- /dev/null +++ b/src/main/java/fr/xephi/authme/initialization/FieldInjection.java @@ -0,0 +1,112 @@ +package fr.xephi.authme.initialization; + +import com.google.common.base.Preconditions; + +import javax.inject.Inject; +import javax.inject.Provider; +import java.lang.annotation.Annotation; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * Functionality for field injection. + */ +class FieldInjection implements Injection { + + private final Field[] fields; + private final Constructor defaultConstructor; + + private FieldInjection(Constructor defaultConstructor, Collection fields) { + this.fields = fields.toArray(new Field[fields.size()]); + this.defaultConstructor = defaultConstructor; + } + + @Override + public Class[] getDependencies() { + Class[] types = new Class[fields.length]; + for (int i = 0; i < fields.length; ++i) { + types[i] = fields[i].getType(); + } + return types; + } + + @Override + public Class[] getDependencyAnnotations() { + Class[] annotations = new Class[fields.length]; + for (int i = 0; i < fields.length; ++i) { + annotations[i] = getFirstNonInjectAnnotation(fields[i]); + } + return annotations; + } + + @Override + public T instantiateWith(Object... values) { + Preconditions.checkArgument(values.length == fields.length, + "The number of values must be equal to the number of fields"); + + T instance; + try { + instance = defaultConstructor.newInstance(); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { + throw new UnsupportedOperationException(e); + } + + for (int i = 0; i < fields.length; ++i) { + try { + fields[i].set(instance, values[i]); + } catch (IllegalAccessException e) { + throw new UnsupportedOperationException(e); + } + } + return instance; + } + + private static Class getFirstNonInjectAnnotation(Field field) { + for (Annotation annotation : field.getAnnotations()) { + if (annotation.annotationType() != Inject.class) { + return annotation.annotationType(); + } + } + return null; + } + + public static Provider> provide(final Class clazz) { + return new Provider>() { + @Override + public FieldInjection get() { + Constructor constructor = getDefaultConstructor(clazz); + if (constructor == null) { + return null; + } + List fields = getInjectionFields(clazz); + return fields == null ? null : new FieldInjection<>(constructor, fields); + } + }; + } + + private static List getInjectionFields(Class clazz) { + List fields = new ArrayList<>(); + for (Field field : clazz.getDeclaredFields()) { + if (field.isAnnotationPresent(Inject.class)) { + field.setAccessible(true); + fields.add(field); + } + } + return fields; + } + + private static Constructor getDefaultConstructor(Class clazz) { + try { + Constructor defaultConstructor = clazz.getDeclaredConstructor(); + defaultConstructor.setAccessible(true); + return (Constructor) defaultConstructor; + } catch (NoSuchMethodException ignore) { + // no default constructor available + } + return null; + } +} diff --git a/src/main/java/fr/xephi/authme/initialization/Injection.java b/src/main/java/fr/xephi/authme/initialization/Injection.java new file mode 100644 index 00000000..6e85b4ce --- /dev/null +++ b/src/main/java/fr/xephi/authme/initialization/Injection.java @@ -0,0 +1,36 @@ +package fr.xephi.authme.initialization; + +/** + * Common interface for all injection methods. + * + * @param the type of the concerned object + */ +interface Injection { + + /** + * Returns the dependencies that must be provided to instantiate the given item. + * + * @return list of dependencies + * @see #instantiateWith + */ + Class[] getDependencies(); + + /** + * Returns the annotation on each dependency if available. The indices of this + * array correspond to the ones of {@link #getDependencies()}. If no annotation + * is available, {@code null} is stored. If multiple annotations are present, only + * one is stored (no guarantee on which one). + * + * @return annotation for each dependency + */ + Class[] getDependencyAnnotations(); + + /** + * Creates a new instance with the given values as dependencies. The given values + * must correspond to {@link #getDependencies()} in size, order and type. + * + * @param values the values to set for the dependencies + * @return resulting object + */ + T instantiateWith(Object... values); +} diff --git a/src/main/java/fr/xephi/authme/MetricsStarter.java b/src/main/java/fr/xephi/authme/initialization/MetricsStarter.java similarity index 93% rename from src/main/java/fr/xephi/authme/MetricsStarter.java rename to src/main/java/fr/xephi/authme/initialization/MetricsStarter.java index eac9ebaf..4a163f98 100644 --- a/src/main/java/fr/xephi/authme/MetricsStarter.java +++ b/src/main/java/fr/xephi/authme/initialization/MetricsStarter.java @@ -1,5 +1,7 @@ -package fr.xephi.authme; +package fr.xephi.authme.initialization; +import fr.xephi.authme.AuthMe; +import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.settings.NewSetting; import fr.xephi.authme.settings.properties.DatabaseSettings; import fr.xephi.authme.settings.properties.PluginSettings; diff --git a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java index a17eacd5..97383c53 100644 --- a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java +++ b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java @@ -18,6 +18,7 @@ import fr.xephi.authme.permission.PlayerStatePermission; import fr.xephi.authme.process.Management; import fr.xephi.authme.settings.NewSetting; import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.SpawnLoader; import fr.xephi.authme.settings.properties.HooksSettings; import fr.xephi.authme.settings.properties.RegistrationSettings; import fr.xephi.authme.settings.properties.RestrictionSettings; @@ -53,6 +54,7 @@ import org.bukkit.event.player.PlayerQuitEvent; import org.bukkit.event.player.PlayerRespawnEvent; import org.bukkit.event.player.PlayerShearEntityEvent; +import javax.inject.Inject; import java.util.concurrent.ConcurrentHashMap; import static fr.xephi.authme.listener.ListenerService.shouldCancelEvent; @@ -67,24 +69,22 @@ public class AuthMePlayerListener implements Listener { public static final ConcurrentHashMap joinMessage = new ConcurrentHashMap<>(); public static final ConcurrentHashMap causeByAuthMe = new ConcurrentHashMap<>(); - private final AuthMe plugin; - private final NewSetting settings; - private final Messages m; - private final DataSource dataSource; - private final AntiBot antiBot; - private final Management management; - private final BukkitService bukkitService; - - public AuthMePlayerListener(AuthMe plugin, NewSetting settings, Messages messages, DataSource dataSource, - AntiBot antiBot, Management management, BukkitService bukkitService) { - this.plugin = plugin; - this.settings = settings; - this.m = messages; - this.dataSource = dataSource; - this.antiBot = antiBot; - this.management = management; - this.bukkitService = bukkitService; - } + @Inject + private AuthMe plugin; + @Inject + private NewSetting settings; + @Inject + private Messages m; + @Inject + private DataSource dataSource; + @Inject + private AntiBot antiBot; + @Inject + private Management management; + @Inject + private BukkitService bukkitService; + @Inject + private SpawnLoader spawnLoader; private void handleChat(AsyncPlayerChatEvent event) { if (settings.getProperty(RestrictionSettings.ALLOW_CHAT)) { @@ -201,7 +201,7 @@ public class AuthMePlayerListener implements Listener { return; } - Location spawn = plugin.getSpawnLocation(player); + Location spawn = spawnLoader.getSpawnLocation(player); if (spawn != null && spawn.getWorld() != null) { if (!player.getWorld().equals(spawn.getWorld())) { player.teleport(spawn); @@ -517,7 +517,7 @@ public class AuthMePlayerListener implements Listener { Player player = event.getPlayer(); String name = player.getName().toLowerCase(); - Location spawn = plugin.getSpawnLocation(player); + Location spawn = spawnLoader.getSpawnLocation(player); if (Settings.isSaveQuitLocationEnabled && dataSource.isAuthAvailable(name)) { PlayerAuth auth = PlayerAuth.builder() .name(name) diff --git a/src/main/java/fr/xephi/authme/listener/AuthMeServerListener.java b/src/main/java/fr/xephi/authme/listener/AuthMeServerListener.java index ec8326b5..54df067e 100644 --- a/src/main/java/fr/xephi/authme/listener/AuthMeServerListener.java +++ b/src/main/java/fr/xephi/authme/listener/AuthMeServerListener.java @@ -15,21 +15,20 @@ import org.bukkit.event.server.PluginDisableEvent; import org.bukkit.event.server.PluginEnableEvent; import org.bukkit.event.server.ServerListPingEvent; +import javax.inject.Inject; + /** */ public class AuthMeServerListener implements Listener { - private final AuthMe plugin; - private final Messages messages; - private final PluginHooks pluginHooks; - private final SpawnLoader spawnLoader; - - public AuthMeServerListener(AuthMe plugin, Messages messages, PluginHooks pluginHooks, SpawnLoader spawnLoader) { - this.plugin = plugin; - this.messages = messages; - this.pluginHooks = pluginHooks; - this.spawnLoader = spawnLoader; - } + @Inject + private AuthMe plugin; + @Inject + private Messages messages; + @Inject + private PluginHooks pluginHooks; + @Inject + private SpawnLoader spawnLoader; @EventHandler(priority = EventPriority.HIGHEST) public void onServerPing(ServerListPingEvent event) { diff --git a/src/main/java/fr/xephi/authme/listener/AuthMeTabCompletePacketAdapter.java b/src/main/java/fr/xephi/authme/listener/AuthMeTabCompletePacketAdapter.java index d8266985..ae6c47bc 100644 --- a/src/main/java/fr/xephi/authme/listener/AuthMeTabCompletePacketAdapter.java +++ b/src/main/java/fr/xephi/authme/listener/AuthMeTabCompletePacketAdapter.java @@ -11,8 +11,11 @@ import fr.xephi.authme.AuthMe; import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.cache.auth.PlayerCache; +import javax.inject.Inject; + public class AuthMeTabCompletePacketAdapter extends PacketAdapter { + @Inject public AuthMeTabCompletePacketAdapter(AuthMe plugin) { super(plugin, ListenerPriority.NORMAL, PacketType.Play.Client.TAB_COMPLETE); } diff --git a/src/main/java/fr/xephi/authme/listener/AuthMeTablistPacketAdapter.java b/src/main/java/fr/xephi/authme/listener/AuthMeTablistPacketAdapter.java index a23ce5c1..d13668d5 100644 --- a/src/main/java/fr/xephi/authme/listener/AuthMeTablistPacketAdapter.java +++ b/src/main/java/fr/xephi/authme/listener/AuthMeTablistPacketAdapter.java @@ -19,6 +19,7 @@ import fr.xephi.authme.cache.auth.PlayerCache; import fr.xephi.authme.util.BukkitService; import org.bukkit.entity.Player; +import javax.inject.Inject; import java.lang.reflect.InvocationTargetException; import java.util.Arrays; import java.util.logging.Level; @@ -27,6 +28,7 @@ public class AuthMeTablistPacketAdapter extends PacketAdapter { private final BukkitService bukkitService; + @Inject public AuthMeTablistPacketAdapter(AuthMe plugin, BukkitService bukkitService) { super(plugin, ListenerPriority.NORMAL, PacketType.Play.Server.PLAYER_INFO); this.bukkitService = bukkitService; diff --git a/src/main/java/fr/xephi/authme/ImageGenerator.java b/src/main/java/fr/xephi/authme/mail/ImageGenerator.java similarity index 88% rename from src/main/java/fr/xephi/authme/ImageGenerator.java rename to src/main/java/fr/xephi/authme/mail/ImageGenerator.java index e529efab..77d98901 100644 --- a/src/main/java/fr/xephi/authme/ImageGenerator.java +++ b/src/main/java/fr/xephi/authme/mail/ImageGenerator.java @@ -1,6 +1,9 @@ -package fr.xephi.authme; +package fr.xephi.authme.mail; -import java.awt.*; +import java.awt.Color; +import java.awt.Font; +import java.awt.GradientPaint; +import java.awt.Graphics2D; import java.awt.image.BufferedImage; /** diff --git a/src/main/java/fr/xephi/authme/mail/SendMailSSL.java b/src/main/java/fr/xephi/authme/mail/SendMailSSL.java index c80001c3..d8332602 100644 --- a/src/main/java/fr/xephi/authme/mail/SendMailSSL.java +++ b/src/main/java/fr/xephi/authme/mail/SendMailSSL.java @@ -2,7 +2,6 @@ package fr.xephi.authme.mail; import fr.xephi.authme.AuthMe; import fr.xephi.authme.ConsoleLogger; -import fr.xephi.authme.ImageGenerator; import fr.xephi.authme.cache.auth.PlayerAuth; import fr.xephi.authme.settings.NewSetting; import fr.xephi.authme.settings.properties.EmailSettings; diff --git a/src/main/java/fr/xephi/authme/permission/PermissionsManager.java b/src/main/java/fr/xephi/authme/permission/PermissionsManager.java index ee27f82c..7dfcd40c 100644 --- a/src/main/java/fr/xephi/authme/permission/PermissionsManager.java +++ b/src/main/java/fr/xephi/authme/permission/PermissionsManager.java @@ -2,6 +2,7 @@ package fr.xephi.authme.permission; import de.bananaco.bpermissions.api.ApiLayer; import de.bananaco.bpermissions.api.CalculableType; +import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.command.CommandDescription; import fr.xephi.authme.util.CollectionUtils; import net.milkbowl.vault.permission.Permission; @@ -20,11 +21,12 @@ import org.tyrannyofheaven.bukkit.zPermissions.ZPermissionsService; import ru.tehkode.permissions.PermissionUser; import ru.tehkode.permissions.bukkit.PermissionsEx; +import javax.annotation.PostConstruct; +import javax.inject.Inject; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; -import java.util.logging.Logger; /** *

@@ -48,10 +50,7 @@ public class PermissionsManager implements PermissionsService { * Server instance. */ private final Server server; - /** - * Logger instance. - */ - private Logger log; + private final PluginManager pluginManager; /** * Type of permissions system that is currently used. * Null if no permissions system is hooked and/or used. @@ -70,11 +69,11 @@ public class PermissionsManager implements PermissionsService { * Constructor. * * @param server Server instance - * @param log Logger */ - public PermissionsManager(Server server, Logger log) { + @Inject + public PermissionsManager(Server server, PluginManager pluginManager) { this.server = server; - this.log = log; + this.pluginManager = pluginManager; } /** @@ -100,39 +99,37 @@ public class PermissionsManager implements PermissionsService { * * @return The detected permissions system. */ + @PostConstruct public PermissionsSystemType setup() { // Force-unhook from current hooked permissions systems unhook(); - // Define the plugin manager - final PluginManager pluginManager = this.server.getPluginManager(); - // Reset used permissions system type flag permsType = null; // Loop through all the available permissions system types - for(PermissionsSystemType type : PermissionsSystemType.values()) { + for (PermissionsSystemType type : PermissionsSystemType.values()) { // Try to find and hook the current plugin if available, print an error if failed try { // Try to find the plugin for the current permissions system Plugin plugin = pluginManager.getPlugin(type.getPluginName()); // Make sure a plugin with this name was found - if(plugin == null) + if (plugin == null) continue; // Make sure the plugin is enabled before hooking - if(!plugin.isEnabled()) { - this.log.info("Not hooking into " + type.getName() + " because it's disabled!"); + if (!plugin.isEnabled()) { + ConsoleLogger.info("Not hooking into " + type.getName() + " because it's disabled!"); continue; } // Use the proper method to hook this plugin - switch(type) { + switch (type) { case PERMISSIONS_EX: // Get the permissions manager for PermissionsEx and make sure it isn't null - if(PermissionsEx.getPermissionManager() == null) { - this.log.info("Failed to hook into " + type.getName() + "!"); + if (PermissionsEx.getPermissionManager() == null) { + ConsoleLogger.info("Failed to hook into " + type.getName() + "!"); continue; } @@ -146,8 +143,8 @@ public class PermissionsManager implements PermissionsService { case Z_PERMISSIONS: // Set the zPermissions service and make sure it's valid zPermissionsService = Bukkit.getServicesManager().load(ZPermissionsService.class); - if(zPermissionsService == null) { - this.log.info("Failed to hook into " + type.getName() + "!"); + if (zPermissionsService == null) { + ConsoleLogger.info("Failed to hook into " + type.getName() + "!"); continue; } @@ -157,14 +154,14 @@ public class PermissionsManager implements PermissionsService { // Get the permissions provider service RegisteredServiceProvider permissionProvider = this.server.getServicesManager().getRegistration(Permission.class); if (permissionProvider == null) { - this.log.info("Failed to hook into " + type.getName() + "!"); + ConsoleLogger.info("Failed to hook into " + type.getName() + "!"); continue; } // Get the Vault provider and make sure it's valid vaultPerms = permissionProvider.getProvider(); - if(vaultPerms == null) { - this.log.info("Not using " + type.getName() + " because it's disabled!"); + if (vaultPerms == null) { + ConsoleLogger.info("Not using " + type.getName() + " because it's disabled!"); continue; } @@ -177,19 +174,19 @@ public class PermissionsManager implements PermissionsService { this.permsType = type; // Show a success message - this.log.info("Hooked into " + type.getName() + "!"); + ConsoleLogger.info("Hooked into " + type.getName() + "!"); // Return the used permissions system type return type; } catch (Exception ex) { // An error occurred, show a warning message - this.log.info("Error while hooking into " + type.getName() + "!"); + ConsoleLogger.logException("Error while hooking into " + type.getName(), ex); } } // No recognized permissions system found, show a message and return - this.log.info("No supported permissions system found! Permissions are disabled!"); + ConsoleLogger.info("No supported permissions system found! Permissions are disabled!"); return null; } @@ -201,7 +198,7 @@ public class PermissionsManager implements PermissionsService { this.permsType = null; // Print a status message to the console - this.log.info("Unhooked from Permissions!"); + ConsoleLogger.info("Unhooked from Permissions!"); } /** @@ -232,7 +229,7 @@ public class PermissionsManager implements PermissionsService { if (pluginName.equals("PermissionsEx") || pluginName.equals("PermissionsBukkit") || pluginName.equals("bPermissions") || pluginName.equals("GroupManager") || pluginName.equals("zPermissions") || pluginName.equals("Vault")) { - this.log.info(pluginName + " plugin enabled, dynamically updating permissions hooks!"); + ConsoleLogger.info(pluginName + " plugin enabled, dynamically updating permissions hooks!"); setup(); } } @@ -251,7 +248,7 @@ public class PermissionsManager implements PermissionsService { if (pluginName.equals("PermissionsEx") || pluginName.equals("PermissionsBukkit") || pluginName.equals("bPermissions") || pluginName.equals("GroupManager") || pluginName.equals("zPermissions") || pluginName.equals("Vault")) { - this.log.info(pluginName + " plugin disabled, updating hooks!"); + ConsoleLogger.info(pluginName + " plugin disabled, updating hooks!"); setup(); } } diff --git a/src/main/java/fr/xephi/authme/process/Management.java b/src/main/java/fr/xephi/authme/process/Management.java index 4b279c9a..04a0f414 100644 --- a/src/main/java/fr/xephi/authme/process/Management.java +++ b/src/main/java/fr/xephi/authme/process/Management.java @@ -3,6 +3,7 @@ package fr.xephi.authme.process; import fr.xephi.authme.AuthMe; import fr.xephi.authme.cache.auth.PlayerCache; import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.hooks.PluginHooks; import fr.xephi.authme.process.email.AsyncAddEmail; import fr.xephi.authme.process.email.AsyncChangeEmail; import fr.xephi.authme.process.join.AsynchronousJoin; @@ -14,23 +15,26 @@ import fr.xephi.authme.process.unregister.AsynchronousUnregister; import org.bukkit.entity.Player; import org.bukkit.scheduler.BukkitScheduler; +import javax.inject.Inject; + /** */ public class Management { - private final AuthMe plugin; - private final BukkitScheduler sched; - private final ProcessService processService; - private final DataSource dataSource; - private final PlayerCache playerCache; + @Inject + private AuthMe plugin; + @Inject + private BukkitScheduler sched; + @Inject + private ProcessService processService; + @Inject + private DataSource dataSource; + @Inject + private PlayerCache playerCache; + @Inject + private PluginHooks pluginHooks; - public Management(AuthMe plugin, ProcessService processService, DataSource dataSource, PlayerCache playerCache) { - this.plugin = plugin; - this.sched = this.plugin.getServer().getScheduler(); - this.processService = processService; - this.dataSource = dataSource; - this.playerCache = playerCache; - } + Management() { } public void performLogin(final Player player, final String password, final boolean forceLogin) { runTask(new AsynchronousLogin(player, password, forceLogin, plugin, dataSource, processService)); @@ -49,7 +53,7 @@ public class Management { } public void performJoin(final Player player) { - runTask(new AsynchronousJoin(player, plugin, dataSource, playerCache, processService)); + runTask(new AsynchronousJoin(player, plugin, dataSource, playerCache, pluginHooks, processService)); } public void performQuit(final Player player, final boolean isKick) { diff --git a/src/main/java/fr/xephi/authme/process/ProcessService.java b/src/main/java/fr/xephi/authme/process/ProcessService.java index 653be156..a338cad4 100644 --- a/src/main/java/fr/xephi/authme/process/ProcessService.java +++ b/src/main/java/fr/xephi/authme/process/ProcessService.java @@ -17,6 +17,7 @@ import org.bukkit.entity.Player; import org.bukkit.event.Event; import org.bukkit.scheduler.BukkitTask; +import javax.inject.Inject; import java.util.Collection; /** @@ -24,29 +25,24 @@ import java.util.Collection; */ public class ProcessService { - private final NewSetting settings; - private final Messages messages; - private final AuthMe authMe; - private final DataSource dataSource; - private final PasswordSecurity passwordSecurity; - private final PluginHooks pluginHooks; - private final SpawnLoader spawnLoader; - private final ValidationService validationService; - private final BukkitService bukkitService; - - public ProcessService(NewSetting settings, Messages messages, AuthMe authMe, DataSource dataSource, - PasswordSecurity passwordSecurity, PluginHooks pluginHooks, SpawnLoader spawnLoader, - ValidationService validationService, BukkitService bukkitService) { - this.settings = settings; - this.messages = messages; - this.authMe = authMe; - this.dataSource = dataSource; - this.passwordSecurity = passwordSecurity; - this.pluginHooks = pluginHooks; - this.spawnLoader = spawnLoader; - this.validationService = validationService; - this.bukkitService = bukkitService; - } + @Inject + private NewSetting settings; + @Inject + private Messages messages; + @Inject + private AuthMe authMe; + @Inject + private DataSource dataSource; + @Inject + private PasswordSecurity passwordSecurity; + @Inject + private PluginHooks pluginHooks; + @Inject + private SpawnLoader spawnLoader; + @Inject + private ValidationService validationService; + @Inject + private BukkitService bukkitService; /** * Retrieve a property's value. diff --git a/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java b/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java index 37b0dfbd..c9ab7159 100644 --- a/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java +++ b/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java @@ -9,6 +9,7 @@ import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.events.FirstSpawnTeleportEvent; import fr.xephi.authme.events.ProtectInventoryEvent; import fr.xephi.authme.events.SpawnTeleportEvent; +import fr.xephi.authme.hooks.PluginHooks; import fr.xephi.authme.listener.AuthMePlayerListener; import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.permission.PlayerStatePermission; @@ -45,18 +46,20 @@ public class AsynchronousJoin implements Process { private final String name; private final ProcessService service; private final PlayerCache playerCache; + private final PluginHooks pluginHooks; private final boolean disableCollisions = MethodUtils .getAccessibleMethod(LivingEntity.class, "setCollidable", new Class[]{}) != null; public AsynchronousJoin(Player player, AuthMe plugin, DataSource database, PlayerCache playerCache, - ProcessService service) { + PluginHooks pluginHooks, ProcessService service) { this.player = player; this.plugin = plugin; this.database = database; this.name = player.getName().toLowerCase(); this.service = service; this.playerCache = playerCache; + this.pluginHooks = pluginHooks; } @Override @@ -190,7 +193,7 @@ public class AsynchronousJoin implements Process { player.setWalkSpeed(0.0f); } player.setNoDamageTicks(registrationTimeout); - if (plugin.getPluginHooks().isEssentialsAvailable() && service.getProperty(HooksSettings.USE_ESSENTIALS_MOTD)) { + if (pluginHooks.isEssentialsAvailable() && service.getProperty(HooksSettings.USE_ESSENTIALS_MOTD)) { player.performCommand("motd"); } if (service.getProperty(RegistrationSettings.APPLY_BLIND_EFFECT)) { diff --git a/src/main/java/fr/xephi/authme/security/PasswordSecurity.java b/src/main/java/fr/xephi/authme/security/PasswordSecurity.java index 94a8abeb..458c9969 100644 --- a/src/main/java/fr/xephi/authme/security/PasswordSecurity.java +++ b/src/main/java/fr/xephi/authme/security/PasswordSecurity.java @@ -8,6 +8,7 @@ import fr.xephi.authme.settings.NewSetting; import fr.xephi.authme.settings.properties.SecuritySettings; import org.bukkit.plugin.PluginManager; +import javax.inject.Inject; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; @@ -22,6 +23,7 @@ public class PasswordSecurity { private final DataSource dataSource; private final PluginManager pluginManager; + @Inject public PasswordSecurity(DataSource dataSource, NewSetting settings, PluginManager pluginManager) { this.settings = settings; this.algorithm = settings.getProperty(SecuritySettings.PASSWORD_HASH); diff --git a/src/main/java/fr/xephi/authme/settings/SpawnLoader.java b/src/main/java/fr/xephi/authme/settings/SpawnLoader.java index 59693a98..a046d813 100644 --- a/src/main/java/fr/xephi/authme/settings/SpawnLoader.java +++ b/src/main/java/fr/xephi/authme/settings/SpawnLoader.java @@ -4,6 +4,7 @@ import fr.xephi.authme.AuthMe; import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.cache.auth.PlayerCache; import fr.xephi.authme.hooks.PluginHooks; +import fr.xephi.authme.initialization.DataFolder; import fr.xephi.authme.settings.properties.RestrictionSettings; import fr.xephi.authme.util.FileUtils; import fr.xephi.authme.util.StringUtils; @@ -14,6 +15,7 @@ import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.entity.Player; +import javax.inject.Inject; import java.io.File; import java.io.IOException; @@ -40,7 +42,8 @@ public class SpawnLoader { * @param settings The setting instance * @param pluginHooks The plugin hooks instance */ - public SpawnLoader(File pluginFolder, NewSetting settings, PluginHooks pluginHooks) { + @Inject + public SpawnLoader(@DataFolder File pluginFolder, NewSetting settings, PluginHooks pluginHooks) { File spawnFile = new File(pluginFolder, "spawn.yml"); // TODO ljacqu 20160312: Check if resource could be copied and handle the case if not FileUtils.copyFileFromResource(spawnFile, "spawn.yml"); diff --git a/src/main/java/fr/xephi/authme/util/BukkitService.java b/src/main/java/fr/xephi/authme/util/BukkitService.java index ea405d80..592caa22 100644 --- a/src/main/java/fr/xephi/authme/util/BukkitService.java +++ b/src/main/java/fr/xephi/authme/util/BukkitService.java @@ -7,6 +7,7 @@ import org.bukkit.OfflinePlayer; import org.bukkit.entity.Player; import org.bukkit.scheduler.BukkitTask; +import javax.inject.Inject; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Arrays; @@ -28,6 +29,7 @@ public class BukkitService { private final boolean getOnlinePlayersIsCollection; private Method getOnlinePlayers; + @Inject public BukkitService(AuthMe authMe) { this.authMe = authMe; getOnlinePlayersIsCollection = initializeOnlinePlayersIsCollectionField(); diff --git a/src/main/java/fr/xephi/authme/util/Utils.java b/src/main/java/fr/xephi/authme/util/Utils.java index 5850645b..5d8c08ee 100644 --- a/src/main/java/fr/xephi/authme/util/Utils.java +++ b/src/main/java/fr/xephi/authme/util/Utils.java @@ -165,8 +165,9 @@ public final class Utils { }); } + @Deprecated public static boolean isNPC(Player player) { - return player.hasMetadata("NPC") || plugin.getPluginHooks().isNpcInCombatTagPlus(player); + return plugin.getPluginHooks().isNpc(player); } public static void teleportToSpawn(Player player) { diff --git a/src/main/java/fr/xephi/authme/util/ValidationService.java b/src/main/java/fr/xephi/authme/util/ValidationService.java index 7f8b78b7..815f493f 100644 --- a/src/main/java/fr/xephi/authme/util/ValidationService.java +++ b/src/main/java/fr/xephi/authme/util/ValidationService.java @@ -10,6 +10,7 @@ import fr.xephi.authme.settings.properties.RestrictionSettings; import fr.xephi.authme.settings.properties.SecuritySettings; import org.bukkit.command.CommandSender; +import javax.inject.Inject; import java.util.Collection; import java.util.List; @@ -22,6 +23,7 @@ public class ValidationService { private final DataSource dataSource; private final PermissionsManager permissionsManager; + @Inject public ValidationService(NewSetting settings, DataSource dataSource, PermissionsManager permissionsManager) { this.settings = settings; this.dataSource = dataSource; diff --git a/src/test/java/fr/xephi/authme/command/CommandServiceTest.java b/src/test/java/fr/xephi/authme/command/CommandServiceTest.java index 89a35571..a1ff5e22 100644 --- a/src/test/java/fr/xephi/authme/command/CommandServiceTest.java +++ b/src/test/java/fr/xephi/authme/command/CommandServiceTest.java @@ -18,10 +18,10 @@ import fr.xephi.authme.util.BukkitService; import fr.xephi.authme.util.ValidationService; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; -import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; @@ -41,6 +41,7 @@ import static org.mockito.Mockito.verify; @RunWith(MockitoJUnitRunner.class) public class CommandServiceTest { + @InjectMocks private CommandService commandService; @Mock private AuthMe authMe; @@ -67,12 +68,6 @@ public class CommandServiceTest { @Mock private BukkitService bukkitService; - @Before - public void setUpService() { - commandService = new CommandService(authMe, commandMapper, helpProvider, messages, passwordSecurity, - permissionsManager, settings, pluginHooks, spawnLoader, antiBot, validationService, bukkitService); - } - @Test public void shouldSendMessage() { // given diff --git a/src/test/java/fr/xephi/authme/command/help/HelpProviderTest.java b/src/test/java/fr/xephi/authme/command/help/HelpProviderTest.java index 3ec47a4f..e7d85f14 100644 --- a/src/test/java/fr/xephi/authme/command/help/HelpProviderTest.java +++ b/src/test/java/fr/xephi/authme/command/help/HelpProviderTest.java @@ -6,6 +6,8 @@ import fr.xephi.authme.command.FoundResultStatus; import fr.xephi.authme.command.TestCommandsUtil; import fr.xephi.authme.permission.PermissionsManager; import fr.xephi.authme.permission.PlayerPermission; +import fr.xephi.authme.settings.NewSetting; +import fr.xephi.authme.settings.properties.PluginSettings; import org.bukkit.ChatColor; import org.bukkit.command.CommandSender; import org.junit.Before; @@ -52,7 +54,9 @@ public class HelpProviderTest { @Before public void setUpHelpProvider() { permissionsManager = mock(PermissionsManager.class); - helpProvider = new HelpProvider(permissionsManager, HELP_HEADER); + NewSetting settings = mock(NewSetting.class); + given(settings.getProperty(PluginSettings.HELP_HEADER)).willReturn(HELP_HEADER); + helpProvider = new HelpProvider(permissionsManager, settings); sender = mock(CommandSender.class); } diff --git a/src/test/java/fr/xephi/authme/initialization/AuthMeServiceInitializerTest.java b/src/test/java/fr/xephi/authme/initialization/AuthMeServiceInitializerTest.java new file mode 100644 index 00000000..61322291 --- /dev/null +++ b/src/test/java/fr/xephi/authme/initialization/AuthMeServiceInitializerTest.java @@ -0,0 +1,194 @@ +package fr.xephi.authme.initialization; + +import fr.xephi.authme.initialization.samples.BadFieldInjection; +import fr.xephi.authme.initialization.samples.BetaManager; +import fr.xephi.authme.initialization.samples.CircularClasses; +import fr.xephi.authme.initialization.samples.ClassWithAbstractDependency; +import fr.xephi.authme.initialization.samples.ClassWithAnnotations; +import fr.xephi.authme.initialization.samples.Duration; +import fr.xephi.authme.initialization.samples.FieldInjectionWithAnnotations; +import fr.xephi.authme.initialization.samples.InvalidClass; +import fr.xephi.authme.initialization.samples.InvalidPostConstruct; +import fr.xephi.authme.initialization.samples.PostConstructTestClass; +import fr.xephi.authme.initialization.samples.ProvidedClass; +import fr.xephi.authme.initialization.samples.Size; +import org.junit.Before; +import org.junit.Test; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertThat; + +/** + * Test for {@link AuthMeServiceInitializer}. + */ +public class AuthMeServiceInitializerTest { + + private static final String ALLOWED_PACKAGE = "fr.xephi.authme.initialization"; + + private AuthMeServiceInitializer initializer; + + @Before + public void setInitializer() { + initializer = new AuthMeServiceInitializer(ALLOWED_PACKAGE); + initializer.register(new ProvidedClass("")); + } + + @Test + public void shouldInitializeElements() { + // given / when + BetaManager betaManager = initializer.get(BetaManager.class); + + // then + assertThat(betaManager, not(nullValue())); + for (Object o : betaManager.getDependencies()) { + assertThat(o, not(nullValue())); + } + } + + @Test(expected = IllegalStateException.class) + public void shouldThrowForInvalidPackage() { + // given / when / then + initializer.get(InvalidClass.class); + } + + @Test + public void shouldPassValueByAnnotation() { + // given + int size = 12; + long duration = -15482L; + initializer.provide(Size.class, size); + initializer.provide(Duration.class, duration); + + // when + ClassWithAnnotations object = initializer.get(ClassWithAnnotations.class); + + // then + assertThat(object, not(nullValue())); + assertThat(object.getSize(), equalTo(size)); + assertThat(object.getDuration(), equalTo(duration)); + // some sample check to make sure we only have one instance of GammaService + assertThat(object.getGammaService(), equalTo(initializer.get(BetaManager.class).getDependencies()[1])); + } + + @Test(expected = RuntimeException.class) + public void shouldRecognizeCircularReferences() { + // given / when / then + initializer.get(CircularClasses.Circular3.class); + } + + @Test(expected = RuntimeException.class) + public void shouldThrowForUnregisteredAnnotation() { + // given + initializer.provide(Size.class, 4523); + + // when / then + initializer.get(ClassWithAnnotations.class); + } + + @Test(expected = RuntimeException.class) + public void shouldThrowForFieldInjectionWithNoDefaultConstructor() { + // given / when / then + initializer.get(BadFieldInjection.class); + } + + @Test + public void shouldInjectFieldsWithAnnotationsProperly() { + // given + initializer.provide(Size.class, 2809375); + initializer.provide(Duration.class, 13095L); + + // when + FieldInjectionWithAnnotations result = initializer.get(FieldInjectionWithAnnotations.class); + + // then + assertThat(result.getSize(), equalTo(2809375)); + assertThat(result.getDuration(), equalTo(13095L)); + assertThat(result.getBetaManager(), not(nullValue())); + assertThat(result.getClassWithAnnotations(), not(nullValue())); + assertThat(result.getClassWithAnnotations().getGammaService(), + equalTo(result.getBetaManager().getDependencies()[1])); + } + + @Test(expected = RuntimeException.class) + public void shouldThrowForAnnotationAsKey() { + // given / when / then + initializer.get(Size.class); + } + + @Test(expected = RuntimeException.class) + public void shouldThrowForSecondRegistration() { + // given / when / then + initializer.register(new ProvidedClass("")); + } + + @Test(expected = RuntimeException.class) + public void shouldThrowForSecondAnnotationRegistration() { + // given + initializer.provide(Size.class, 12); + + // when / then + initializer.provide(Size.class, -8); + } + + @Test(expected = NullPointerException.class) + public void shouldThrowForNullValueAssociatedToAnnotation() { + // given / when / then + initializer.provide(Duration.class, null); + } + + @Test(expected = NullPointerException.class) + public void shouldThrowForRegisterWithNull() { + // given / when / then + initializer.register(String.class, null); + } + + @Test(expected = RuntimeException.class) + public void shouldThrowForRegisterOfType() { + // given / when / then + // this most likely means that the second argument was forgotten, so throw an error and force + // the API user to use the explicit register(Class.class, String.class) if really, really desired + // (Though for such generic types, an annotation would be a lot better) + initializer.register(String.class); + } + + @Test + public void shouldExecutePostConstructMethod() { + // given + initializer.provide(Size.class, 15123); + + // when + PostConstructTestClass testClass = initializer.get(PostConstructTestClass.class); + + // then + assertThat(testClass.werePostConstructsCalled(), equalTo(true)); + assertThat(testClass.getBetaManager(), not(nullValue())); + } + + @Test(expected = RuntimeException.class) + public void shouldThrowForInvalidPostConstructMethod() { + // given / when / then + initializer.get(InvalidPostConstruct.class); + } + + @Test(expected = RuntimeException.class) + public void shouldThrowForAbstractNonRegisteredDependency() { + // given / when / then + initializer.get(ClassWithAbstractDependency.class); + } + + @Test + public void shouldInstantiateWithImplementationOfAbstractDependency() { + // given + ClassWithAbstractDependency.ConcreteDependency concrete = new ClassWithAbstractDependency.ConcreteDependency(); + initializer.register(ClassWithAbstractDependency.AbstractDependency.class, concrete); + + // when + ClassWithAbstractDependency cwad = initializer.get(ClassWithAbstractDependency.class); + + // then + assertThat(cwad.getAbstractDependency() == concrete, equalTo(true)); + assertThat(cwad.getAlphaService(), not(nullValue())); + } +} diff --git a/src/test/java/fr/xephi/authme/initialization/samples/AlphaService.java b/src/test/java/fr/xephi/authme/initialization/samples/AlphaService.java new file mode 100644 index 00000000..2a93a43c --- /dev/null +++ b/src/test/java/fr/xephi/authme/initialization/samples/AlphaService.java @@ -0,0 +1,20 @@ +package fr.xephi.authme.initialization.samples; + +import javax.inject.Inject; + +/** + * Sample - class with dependency to ProvidedClass. + */ +public class AlphaService { + + private ProvidedClass providedClass; + + @Inject + AlphaService(ProvidedClass providedClass) { + this.providedClass = providedClass; + } + + public ProvidedClass getProvidedClass() { + return providedClass; + } +} diff --git a/src/test/java/fr/xephi/authme/initialization/samples/BadFieldInjection.java b/src/test/java/fr/xephi/authme/initialization/samples/BadFieldInjection.java new file mode 100644 index 00000000..9dc41548 --- /dev/null +++ b/src/test/java/fr/xephi/authme/initialization/samples/BadFieldInjection.java @@ -0,0 +1,16 @@ +package fr.xephi.authme.initialization.samples; + +import javax.inject.Inject; + +/** + * Sample class with invalid field injection (requires default constructor). + */ +public class BadFieldInjection { + + @Inject + private AlphaService alphaService; + + public BadFieldInjection(BetaManager betaManager) { + throw new IllegalStateException("Should never be called"); + } +} diff --git a/src/test/java/fr/xephi/authme/initialization/samples/BetaManager.java b/src/test/java/fr/xephi/authme/initialization/samples/BetaManager.java new file mode 100644 index 00000000..8c2fe923 --- /dev/null +++ b/src/test/java/fr/xephi/authme/initialization/samples/BetaManager.java @@ -0,0 +1,20 @@ +package fr.xephi.authme.initialization.samples; + +import javax.inject.Inject; + +/** + * Sample - depends on Provided, alpha and gamma. + */ +public class BetaManager { + + @Inject + private ProvidedClass providedClass; + @Inject + private GammaService gammaService; + @Inject + private AlphaService alphaService; + + public Object[] getDependencies() { + return new Object[]{providedClass, gammaService, alphaService}; + } +} diff --git a/src/test/java/fr/xephi/authme/initialization/samples/CircularClasses.java b/src/test/java/fr/xephi/authme/initialization/samples/CircularClasses.java new file mode 100644 index 00000000..e9a1460e --- /dev/null +++ b/src/test/java/fr/xephi/authme/initialization/samples/CircularClasses.java @@ -0,0 +1,30 @@ +package fr.xephi.authme.initialization.samples; + +import javax.inject.Inject; + +/** + * Classes with circular dependencies. + */ +public class CircularClasses { + + public static final class Circular1 { + @Inject + public Circular1(AlphaService alphaService, Circular3 circular3) { + // -- + } + } + + public static final class Circular2 { + @Inject + public Circular2(Circular1 circular1) { + // -- + } + } + + public static final class Circular3 { + @Inject + public Circular3(Circular2 circular2, BetaManager betaManager) { + // -- + } + } +} diff --git a/src/test/java/fr/xephi/authme/initialization/samples/ClassWithAbstractDependency.java b/src/test/java/fr/xephi/authme/initialization/samples/ClassWithAbstractDependency.java new file mode 100644 index 00000000..25b651b3 --- /dev/null +++ b/src/test/java/fr/xephi/authme/initialization/samples/ClassWithAbstractDependency.java @@ -0,0 +1,32 @@ +package fr.xephi.authme.initialization.samples; + +import javax.inject.Inject; + +/** + * Test with an abstract class declared as dependency. + */ +public class ClassWithAbstractDependency { + + private final AlphaService alphaService; + private final AbstractDependency abstractDependency; + + @Inject + public ClassWithAbstractDependency(AlphaService as, AbstractDependency ad) { + this.alphaService = as; + this.abstractDependency = ad; + } + + public AlphaService getAlphaService() { + return alphaService; + } + + public AbstractDependency getAbstractDependency() { + return abstractDependency; + } + + public static abstract class AbstractDependency { + } + + public static final class ConcreteDependency extends AbstractDependency { + } +} diff --git a/src/test/java/fr/xephi/authme/initialization/samples/ClassWithAnnotations.java b/src/test/java/fr/xephi/authme/initialization/samples/ClassWithAnnotations.java new file mode 100644 index 00000000..61dca71b --- /dev/null +++ b/src/test/java/fr/xephi/authme/initialization/samples/ClassWithAnnotations.java @@ -0,0 +1,29 @@ +package fr.xephi.authme.initialization.samples; + +import javax.inject.Inject; + +public class ClassWithAnnotations { + + private int size; + private GammaService gammaService; + private long duration; + + @Inject + ClassWithAnnotations(@Size int size, GammaService gammaService, @Duration long duration) { + this.size = size; + this.gammaService = gammaService; + this.duration = duration; + } + + public int getSize() { + return size; + } + + public GammaService getGammaService() { + return gammaService; + } + + public long getDuration() { + return duration; + } +} diff --git a/src/test/java/fr/xephi/authme/initialization/samples/Duration.java b/src/test/java/fr/xephi/authme/initialization/samples/Duration.java new file mode 100644 index 00000000..e61c6d28 --- /dev/null +++ b/src/test/java/fr/xephi/authme/initialization/samples/Duration.java @@ -0,0 +1,14 @@ +package fr.xephi.authme.initialization.samples; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Sample annotation. + */ +@Target({ElementType.PARAMETER, ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface Duration { +} diff --git a/src/test/java/fr/xephi/authme/initialization/samples/FieldInjectionWithAnnotations.java b/src/test/java/fr/xephi/authme/initialization/samples/FieldInjectionWithAnnotations.java new file mode 100644 index 00000000..65bdf537 --- /dev/null +++ b/src/test/java/fr/xephi/authme/initialization/samples/FieldInjectionWithAnnotations.java @@ -0,0 +1,40 @@ +package fr.xephi.authme.initialization.samples; + +import javax.inject.Inject; + +/** + * Sample - field injection, including custom annotations. + */ +public class FieldInjectionWithAnnotations { + + @Inject + private BetaManager betaManager; + @Inject + @Size + private int size; + @Duration + @Inject + private long duration; + @Inject + protected ClassWithAnnotations classWithAnnotations; + + FieldInjectionWithAnnotations() { + } + + public BetaManager getBetaManager() { + return betaManager; + } + + public int getSize() { + return size; + } + + public long getDuration() { + return duration; + } + + public ClassWithAnnotations getClassWithAnnotations() { + return classWithAnnotations; + } + +} diff --git a/src/test/java/fr/xephi/authme/initialization/samples/GammaService.java b/src/test/java/fr/xephi/authme/initialization/samples/GammaService.java new file mode 100644 index 00000000..8ee97032 --- /dev/null +++ b/src/test/java/fr/xephi/authme/initialization/samples/GammaService.java @@ -0,0 +1,20 @@ +package fr.xephi.authme.initialization.samples; + +import javax.inject.Inject; + +/** + * Sample - class dependent on alpha and provided. + */ +public class GammaService { + + private AlphaService alphaService; + + @Inject + GammaService(AlphaService alphaService) { + this.alphaService = alphaService; + } + + public AlphaService getAlphaService() { + return alphaService; + } +} diff --git a/src/test/java/fr/xephi/authme/initialization/samples/InvalidClass.java b/src/test/java/fr/xephi/authme/initialization/samples/InvalidClass.java new file mode 100644 index 00000000..b896af9d --- /dev/null +++ b/src/test/java/fr/xephi/authme/initialization/samples/InvalidClass.java @@ -0,0 +1,14 @@ +package fr.xephi.authme.initialization.samples; + +import javax.inject.Inject; + +/** + * Sample - invalid class, since Integer parameter type is outside of the allowed package and not annotated. + */ +public class InvalidClass { + + @Inject + public InvalidClass(AlphaService alphaService, Integer i) { + throw new IllegalStateException("Should never be called"); + } +} diff --git a/src/test/java/fr/xephi/authme/initialization/samples/InvalidPostConstruct.java b/src/test/java/fr/xephi/authme/initialization/samples/InvalidPostConstruct.java new file mode 100644 index 00000000..85c7f4c4 --- /dev/null +++ b/src/test/java/fr/xephi/authme/initialization/samples/InvalidPostConstruct.java @@ -0,0 +1,19 @@ +package fr.xephi.authme.initialization.samples; + +import javax.annotation.PostConstruct; +import javax.inject.Inject; + +/** + * Class with invalid @PostConstruct method. + */ +public class InvalidPostConstruct { + + @Inject + private AlphaService alphaService; + @Inject + private ProvidedClass providedClass; + + @PostConstruct + public void invalidPostConstr(BetaManager betaManager) { + } +} diff --git a/src/test/java/fr/xephi/authme/initialization/samples/PostConstructTestClass.java b/src/test/java/fr/xephi/authme/initialization/samples/PostConstructTestClass.java new file mode 100644 index 00000000..a4e57032 --- /dev/null +++ b/src/test/java/fr/xephi/authme/initialization/samples/PostConstructTestClass.java @@ -0,0 +1,37 @@ +package fr.xephi.authme.initialization.samples; + +import javax.annotation.PostConstruct; +import javax.inject.Inject; + +/** + * Sample class for testing the execution of the @PostConstruct method. + */ +public class PostConstructTestClass { + + @Inject + @Size + private int size; + @Inject + private BetaManager betaManager; + private boolean wasPostConstructCalled = false; + private boolean wasSecondPostConstructCalled = false; + + @PostConstruct + protected void setFieldToTrue() { + wasPostConstructCalled = true; + } + + @PostConstruct + public int otherPostConstructMethod() { + wasSecondPostConstructCalled = true; + return 42; + } + + public boolean werePostConstructsCalled() { + return wasPostConstructCalled && wasSecondPostConstructCalled; + } + + public BetaManager getBetaManager() { + return betaManager; + } +} diff --git a/src/test/java/fr/xephi/authme/initialization/samples/ProvidedClass.java b/src/test/java/fr/xephi/authme/initialization/samples/ProvidedClass.java new file mode 100644 index 00000000..7ce81bd3 --- /dev/null +++ b/src/test/java/fr/xephi/authme/initialization/samples/ProvidedClass.java @@ -0,0 +1,18 @@ +package fr.xephi.authme.initialization.samples; + +import javax.inject.Inject; + +/** + * Sample - class that is always provided to the initializer beforehand. + */ +public class ProvidedClass { + + @Inject + public ProvidedClass() { + throw new IllegalStateException("Should never be called (tests always provide this class)"); + } + + public ProvidedClass(String manualConstructor) { + } + +} diff --git a/src/test/java/fr/xephi/authme/initialization/samples/Size.java b/src/test/java/fr/xephi/authme/initialization/samples/Size.java new file mode 100644 index 00000000..320a7b7c --- /dev/null +++ b/src/test/java/fr/xephi/authme/initialization/samples/Size.java @@ -0,0 +1,14 @@ +package fr.xephi.authme.initialization.samples; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Sample annotation. + */ +@Target({ElementType.PARAMETER, ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface Size { +} diff --git a/src/test/java/fr/xephi/authme/process/ProcessServiceTest.java b/src/test/java/fr/xephi/authme/process/ProcessServiceTest.java index ab935d02..f641b1f0 100644 --- a/src/test/java/fr/xephi/authme/process/ProcessServiceTest.java +++ b/src/test/java/fr/xephi/authme/process/ProcessServiceTest.java @@ -13,9 +13,9 @@ import fr.xephi.authme.settings.properties.SecuritySettings; import fr.xephi.authme.util.BukkitService; import fr.xephi.authme.util.ValidationService; import org.bukkit.command.CommandSender; -import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; @@ -31,6 +31,7 @@ import static org.mockito.Mockito.verify; @RunWith(MockitoJUnitRunner.class) public class ProcessServiceTest { + @InjectMocks private ProcessService processService; @Mock private ValidationService validationService; @@ -51,12 +52,6 @@ public class ProcessServiceTest { @Mock private BukkitService bukkitService; - @Before - public void setUpService() { - processService = new ProcessService(settings, messages, authMe, dataSource, passwordSecurity, - pluginHooks, spawnLoader, validationService, bukkitService); - } - @Test public void shouldGetProperty() { // given From 5963628fa618b1d5b90417a4c59580d24c4edb06 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Wed, 27 Apr 2016 22:59:44 +0200 Subject: [PATCH 002/200] #432 Add field injection to AccountsCommand --- src/main/java/fr/xephi/authme/AuthMe.java | 2 +- .../authme/command/CommandInitializer.java | 73 ++++++++++--------- .../executable/authme/AccountsCommand.java | 18 +++-- .../AuthMeServiceInitializer.java | 13 +++- .../command/CommandInitializerTest.java | 17 ++++- .../authme/AccountsCommandTest.java | 27 +++---- src/tools/commands/CommandPageCreater.java | 5 +- 7 files changed, 97 insertions(+), 58 deletions(-) diff --git a/src/main/java/fr/xephi/authme/AuthMe.java b/src/main/java/fr/xephi/authme/AuthMe.java index 812e88e9..b69cd299 100644 --- a/src/main/java/fr/xephi/authme/AuthMe.java +++ b/src/main/java/fr/xephi/authme/AuthMe.java @@ -256,7 +256,7 @@ public class AuthMe extends JavaPlugin { initializer.register(newSettings); initializer.register(messages); initializer.register(DataSource.class, database); - initializer.provide(BaseCommands.class, CommandInitializer.buildCommands()); + initializer.provide(BaseCommands.class, CommandInitializer.buildCommands(initializer)); // Some statically injected things initializer.register(PlayerCache.class, PlayerCache.getInstance()); diff --git a/src/main/java/fr/xephi/authme/command/CommandInitializer.java b/src/main/java/fr/xephi/authme/command/CommandInitializer.java index 6635545f..14d72f4a 100644 --- a/src/main/java/fr/xephi/authme/command/CommandInitializer.java +++ b/src/main/java/fr/xephi/authme/command/CommandInitializer.java @@ -33,6 +33,7 @@ import fr.xephi.authme.command.executable.login.LoginCommand; import fr.xephi.authme.command.executable.logout.LogoutCommand; import fr.xephi.authme.command.executable.register.RegisterCommand; import fr.xephi.authme.command.executable.unregister.UnregisterCommand; +import fr.xephi.authme.initialization.AuthMeServiceInitializer; import fr.xephi.authme.permission.AdminPermission; import fr.xephi.authme.permission.PlayerPermission; @@ -53,13 +54,13 @@ public final class CommandInitializer { // Helper class } - public static Set buildCommands() { + public static Set buildCommands(AuthMeServiceInitializer initializer) { // Register the base AuthMe Reloaded command final CommandDescription AUTHME_BASE = CommandDescription.builder() .labels("authme") .description("Main command") .detailedDescription("The main AuthMeReloaded command. The root for all admin commands.") - .executableCommand(new AuthMeCommand()) + .executableCommand(initializer.newInstance(AuthMeCommand.class)) .build(); // Register the register command @@ -71,7 +72,7 @@ public final class CommandInitializer { .withArgument("player", "Player name", false) .withArgument("password", "Password", false) .permissions(OP_ONLY, AdminPermission.REGISTER) - .executableCommand(new RegisterAdminCommand()) + .executableCommand(initializer.newInstance(RegisterAdminCommand.class)) .build(); // Register the unregister command @@ -82,7 +83,7 @@ public final class CommandInitializer { .detailedDescription("Unregister the specified player.") .withArgument("player", "Player name", false) .permissions(OP_ONLY, AdminPermission.UNREGISTER) - .executableCommand(new UnregisterAdminCommand()) + .executableCommand(initializer.newInstance(UnregisterAdminCommand.class)) .build(); // Register the forcelogin command @@ -93,7 +94,7 @@ public final class CommandInitializer { .detailedDescription("Enforce the specified player to login.") .withArgument("player", "Online player name", true) .permissions(OP_ONLY, AdminPermission.FORCE_LOGIN) - .executableCommand(new ForceLoginCommand()) + .executableCommand(initializer.newInstance(ForceLoginCommand.class)) .build(); // Register the changepassword command @@ -105,7 +106,7 @@ public final class CommandInitializer { .withArgument("player", "Player name", false) .withArgument("pwd", "New password", false) .permissions(OP_ONLY, AdminPermission.CHANGE_PASSWORD) - .executableCommand(new ChangePasswordAdminCommand()) + .executableCommand(initializer.newInstance(ChangePasswordAdminCommand.class)) .build(); // Register the last login command @@ -116,7 +117,7 @@ public final class CommandInitializer { .detailedDescription("View the date of the specified players last login.") .withArgument("player", "Player name", true) .permissions(OP_ONLY, AdminPermission.LAST_LOGIN) - .executableCommand(new LastLoginCommand()) + .executableCommand(initializer.newInstance(LastLoginCommand.class)) .build(); // Register the accounts command @@ -127,7 +128,7 @@ public final class CommandInitializer { .detailedDescription("Display all accounts of a player by his player name or IP.") .withArgument("player", "Player name or IP", true) .permissions(OP_ONLY, AdminPermission.ACCOUNTS) - .executableCommand(new AccountsCommand()) + .executableCommand(initializer.newInstance(AccountsCommand.class)) .build(); // Register the getemail command @@ -138,7 +139,7 @@ public final class CommandInitializer { .detailedDescription("Display the email address of the specified player if set.") .withArgument("player", "Player name", true) .permissions(OP_ONLY, AdminPermission.GET_EMAIL) - .executableCommand(new GetEmailCommand()) + .executableCommand(initializer.newInstance(GetEmailCommand.class)) .build(); // Register the setemail command @@ -150,7 +151,7 @@ public final class CommandInitializer { .withArgument("player", "Player name", false) .withArgument("email", "Player email", false) .permissions(OP_ONLY, AdminPermission.CHANGE_EMAIL) - .executableCommand(new SetEmailCommand()) + .executableCommand(initializer.newInstance(SetEmailCommand.class)) .build(); // Register the getip command @@ -161,7 +162,7 @@ public final class CommandInitializer { .detailedDescription("Get the IP address of the specified online player.") .withArgument("player", "Player name", false) .permissions(OP_ONLY, AdminPermission.GET_IP) - .executableCommand(new GetIpCommand()) + .executableCommand(initializer.newInstance(GetIpCommand.class)) .build(); // Register the spawn command @@ -171,7 +172,7 @@ public final class CommandInitializer { .description("Teleport to spawn") .detailedDescription("Teleport to the spawn.") .permissions(OP_ONLY, AdminPermission.SPAWN) - .executableCommand(new SpawnCommand()) + .executableCommand(initializer.newInstance(SpawnCommand.class)) .build(); // Register the setspawn command @@ -181,7 +182,7 @@ public final class CommandInitializer { .description("Change the spawn") .detailedDescription("Change the player's spawn to your current position.") .permissions(OP_ONLY, AdminPermission.SET_SPAWN) - .executableCommand(new SetSpawnCommand()) + .executableCommand(initializer.newInstance(SetSpawnCommand.class)) .build(); // Register the firstspawn command @@ -191,7 +192,7 @@ public final class CommandInitializer { .description("Teleport to first spawn") .detailedDescription("Teleport to the first spawn.") .permissions(OP_ONLY, AdminPermission.FIRST_SPAWN) - .executableCommand(new FirstSpawnCommand()) + .executableCommand(initializer.newInstance(FirstSpawnCommand.class)) .build(); // Register the setfirstspawn command @@ -201,7 +202,7 @@ public final class CommandInitializer { .description("Change the first spawn") .detailedDescription("Change the first player's spawn to your current position.") .permissions(OP_ONLY, AdminPermission.SET_FIRST_SPAWN) - .executableCommand(new SetFirstSpawnCommand()) + .executableCommand(initializer.newInstance(SetFirstSpawnCommand.class)) .build(); // Register the purge command @@ -212,7 +213,7 @@ public final class CommandInitializer { .detailedDescription("Purge old AuthMeReloaded data longer than the specified amount of days ago.") .withArgument("days", "Number of days", false) .permissions(OP_ONLY, AdminPermission.PURGE) - .executableCommand(new PurgeCommand()) + .executableCommand(initializer.newInstance(PurgeCommand.class)) .build(); // Register the purgelastposition command @@ -224,7 +225,7 @@ public final class CommandInitializer { .detailedDescription("Purge the last know position of the specified player or all of them.") .withArgument("player/*", "Player name or * for all players", false) .permissions(OP_ONLY, AdminPermission.PURGE_LAST_POSITION) - .executableCommand(new PurgeLastPositionCommand()) + .executableCommand(initializer.newInstance(PurgeLastPositionCommand.class)) .build(); // Register the purgebannedplayers command @@ -234,7 +235,7 @@ public final class CommandInitializer { .description("Purge banned players data") .detailedDescription("Purge all AuthMeReloaded data for banned players.") .permissions(OP_ONLY, AdminPermission.PURGE_BANNED_PLAYERS) - .executableCommand(new PurgeBannedPlayersCommand()) + .executableCommand(initializer.newInstance(PurgeBannedPlayersCommand.class)) .build(); // Register the switchantibot command @@ -245,7 +246,7 @@ public final class CommandInitializer { .detailedDescription("Switch or toggle the AntiBot mode to the specified state.") .withArgument("mode", "ON / OFF", true) .permissions(OP_ONLY, AdminPermission.SWITCH_ANTIBOT) - .executableCommand(new SwitchAntiBotCommand()) + .executableCommand(initializer.newInstance(SwitchAntiBotCommand.class)) .build(); // Register the reload command @@ -255,7 +256,7 @@ public final class CommandInitializer { .description("Reload plugin") .detailedDescription("Reload the AuthMeReloaded plugin.") .permissions(OP_ONLY, AdminPermission.RELOAD) - .executableCommand(new ReloadCommand()) + .executableCommand(initializer.newInstance(ReloadCommand.class)) .build(); // Register the version command @@ -265,7 +266,7 @@ public final class CommandInitializer { .description("Version info") .detailedDescription("Show detailed information about the installed AuthMeReloaded version, the " + "developers, contributors, and license.") - .executableCommand(new VersionCommand()) + .executableCommand(initializer.newInstance(VersionCommand.class)) .build(); CommandDescription.builder() @@ -276,7 +277,7 @@ public final class CommandInitializer { .withArgument("job", "Conversion job: xauth / crazylogin / rakamak / " + "royalauth / vauth / sqlitetosql", false) .permissions(OP_ONLY, AdminPermission.CONVERTER) - .executableCommand(new ConverterCommand()) + .executableCommand(initializer.newInstance(ConverterCommand.class)) .build(); // Register the base login command @@ -287,7 +288,7 @@ public final class CommandInitializer { .detailedDescription("Command to log in using AuthMeReloaded.") .withArgument("password", "Login password", false) .permissions(ALLOWED, PlayerPermission.LOGIN) - .executableCommand(new LoginCommand()) + .executableCommand(initializer.newInstance(LoginCommand.class)) .build(); // Register the base logout command @@ -297,7 +298,7 @@ public final class CommandInitializer { .description("Logout command") .detailedDescription("Command to logout using AuthMeReloaded.") .permissions(ALLOWED, PlayerPermission.LOGOUT) - .executableCommand(new LogoutCommand()) + .executableCommand(initializer.newInstance(LogoutCommand.class)) .build(); // Register the base register command @@ -309,7 +310,7 @@ public final class CommandInitializer { .withArgument("password", "Password", true) .withArgument("verifyPassword", "Verify password", true) .permissions(ALLOWED, PlayerPermission.REGISTER) - .executableCommand(new RegisterCommand()) + .executableCommand(initializer.newInstance(RegisterCommand.class)) .build(); // Register the base unregister command @@ -320,7 +321,7 @@ public final class CommandInitializer { .detailedDescription("Command to unregister using AuthMeReloaded.") .withArgument("password", "Password", false) .permissions(ALLOWED, PlayerPermission.UNREGISTER) - .executableCommand(new UnregisterCommand()) + .executableCommand(initializer.newInstance(UnregisterCommand.class)) .build(); // Register the base changepassword command @@ -332,7 +333,7 @@ public final class CommandInitializer { .withArgument("oldPassword", "Old Password", false) .withArgument("newPassword", "New Password.", false) .permissions(ALLOWED, PlayerPermission.CHANGE_PASSWORD) - .executableCommand(new ChangePasswordCommand()) + .executableCommand(initializer.newInstance(ChangePasswordCommand.class)) .build(); // Register the base Email command @@ -341,7 +342,7 @@ public final class CommandInitializer { .labels("email", "mail") .description("Email command") .detailedDescription("The AuthMeReloaded Email command base.") - .executableCommand(new EmailBaseCommand()) + .executableCommand(initializer.newInstance(EmailBaseCommand.class)) .build(); // Register the add command @@ -353,7 +354,7 @@ public final class CommandInitializer { .withArgument("email", "Email address", false) .withArgument("verifyEmail", "Email address verification", false) .permissions(ALLOWED, PlayerPermission.ADD_EMAIL) - .executableCommand(new AddEmailCommand()) + .executableCommand(initializer.newInstance(AddEmailCommand.class)) .build(); // Register the change command @@ -365,7 +366,7 @@ public final class CommandInitializer { .withArgument("oldEmail", "Old email address", false) .withArgument("newEmail", "New email address", false) .permissions(ALLOWED, PlayerPermission.CHANGE_EMAIL) - .executableCommand(new ChangeEmailCommand()) + .executableCommand(initializer.newInstance(ChangeEmailCommand.class)) .build(); // Register the recover command @@ -377,7 +378,7 @@ public final class CommandInitializer { "a new password.") .withArgument("email", "Email address", false) .permissions(ALLOWED, PlayerPermission.RECOVER_EMAIL) - .executableCommand(new RecoverEmailCommand()) + .executableCommand(initializer.newInstance(RecoverEmailCommand.class)) .build(); // Register the base captcha command @@ -388,7 +389,7 @@ public final class CommandInitializer { .detailedDescription("Captcha command for AuthMeReloaded.") .withArgument("captcha", "The Captcha", false) .permissions(ALLOWED, PlayerPermission.CAPTCHA) - .executableCommand(new CaptchaCommand()) + .executableCommand(initializer.newInstance(CaptchaCommand.class)) .build(); Set baseCommands = ImmutableSet.of( @@ -401,7 +402,7 @@ public final class CommandInitializer { EMAIL_BASE, CAPTCHA_BASE); - setHelpOnAllBases(baseCommands); + setHelpOnAllBases(baseCommands, initializer); return baseCommands; } @@ -409,9 +410,11 @@ public final class CommandInitializer { * Set the help command on all base commands, e.g. to register /authme help or /register help. * * @param commands The list of base commands to register a help child command on + * @param initializer The service initializer */ - private static void setHelpOnAllBases(Collection commands) { - final HelpCommand helpCommandExecutable = new HelpCommand(); + private static void setHelpOnAllBases(Collection commands, + AuthMeServiceInitializer initializer) { + final HelpCommand helpCommandExecutable = initializer.newInstance(HelpCommand.class); final List helpCommandLabels = Arrays.asList("help", "hlp", "h", "sos", "?"); for (CommandDescription base : commands) { diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/AccountsCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/AccountsCommand.java index d3698cfd..ad5bf9b2 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/AccountsCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/AccountsCommand.java @@ -3,10 +3,13 @@ package fr.xephi.authme.command.executable.authme; import fr.xephi.authme.cache.auth.PlayerAuth; import fr.xephi.authme.command.CommandService; import fr.xephi.authme.command.ExecutableCommand; +import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.output.MessageKey; +import fr.xephi.authme.output.Messages; import fr.xephi.authme.util.StringUtils; import org.bukkit.command.CommandSender; +import javax.inject.Inject; import java.util.List; /** @@ -14,6 +17,11 @@ import java.util.List; */ public class AccountsCommand implements ExecutableCommand { + @Inject + private DataSource dataSource; + @Inject + private Messages messages; + @Override public void executeCommand(final CommandSender sender, List arguments, final CommandService commandService) { @@ -24,15 +32,15 @@ public class AccountsCommand implements ExecutableCommand { commandService.runTaskAsynchronously(new Runnable() { @Override public void run() { - PlayerAuth auth = commandService.getDataSource().getAuth(playerName.toLowerCase()); + PlayerAuth auth = dataSource.getAuth(playerName.toLowerCase()); if (auth == null) { - commandService.send(sender, MessageKey.UNKNOWN_USER); + messages.send(sender, MessageKey.UNKNOWN_USER); return; } - List accountList = commandService.getDataSource().getAllAuthsByIp(auth.getIp()); + List accountList = dataSource.getAllAuthsByIp(auth.getIp()); if (accountList.isEmpty()) { - commandService.send(sender, MessageKey.USER_NOT_REGISTERED); + messages.send(sender, MessageKey.USER_NOT_REGISTERED); } else if (accountList.size() == 1) { sender.sendMessage("[AuthMe] " + playerName + " is a single account player"); } else { @@ -44,7 +52,7 @@ public class AccountsCommand implements ExecutableCommand { commandService.runTaskAsynchronously(new Runnable() { @Override public void run() { - List accountList = commandService.getDataSource().getAllAuthsByIp(playerName); + List accountList = dataSource.getAllAuthsByIp(playerName); if (accountList.isEmpty()) { sender.sendMessage("[AuthMe] This IP does not exist in the database."); } else if (accountList.size() == 1) { diff --git a/src/main/java/fr/xephi/authme/initialization/AuthMeServiceInitializer.java b/src/main/java/fr/xephi/authme/initialization/AuthMeServiceInitializer.java index b751b32e..e5dddd04 100644 --- a/src/main/java/fr/xephi/authme/initialization/AuthMeServiceInitializer.java +++ b/src/main/java/fr/xephi/authme/initialization/AuthMeServiceInitializer.java @@ -97,6 +97,18 @@ public class AuthMeServiceInitializer { objects.put(annotation, value); } + /** + * Creates a new instance of the given class and does not keep track of it afterwards, + * unlike {@link #get(Class)} (singleton scope). + * + * @param clazz the class to instantiate + * @param the class' type + * @return new instance of class T + */ + public T newInstance(Class clazz) { + return instantiate(clazz, new HashSet>()); + } + /** * Returns an instance of the given class or the value associated with an annotation, * by retrieving it or by instantiating it if not yet present. @@ -120,7 +132,6 @@ public class AuthMeServiceInitializer { traversedClasses = new HashSet<>(traversedClasses); traversedClasses.add(clazz); - System.out.println(Strings.repeat(" ", traversedClasses.size() * 2) + "- Instantiating " + clazz); return instantiate(clazz, traversedClasses); } diff --git a/src/test/java/fr/xephi/authme/command/CommandInitializerTest.java b/src/test/java/fr/xephi/authme/command/CommandInitializerTest.java index 6e5418d5..7c62377f 100644 --- a/src/test/java/fr/xephi/authme/command/CommandInitializerTest.java +++ b/src/test/java/fr/xephi/authme/command/CommandInitializerTest.java @@ -1,10 +1,13 @@ package fr.xephi.authme.command; +import fr.xephi.authme.initialization.AuthMeServiceInitializer; import fr.xephi.authme.permission.AdminPermission; import fr.xephi.authme.permission.PermissionNode; import fr.xephi.authme.util.StringUtils; import org.junit.BeforeClass; import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; import java.util.ArrayList; import java.util.Collection; @@ -21,6 +24,9 @@ import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.nullValue; import static org.junit.Assert.assertThat; import static org.junit.Assert.fail; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; /** * Test for {@link CommandInitializer} to guarantee the integrity of the defined commands. @@ -37,7 +43,16 @@ public class CommandInitializerTest { @BeforeClass public static void initializeCommandManager() { - commands = CommandInitializer.buildCommands(); + AuthMeServiceInitializer initializer = mock(AuthMeServiceInitializer.class); + when(initializer.newInstance(any(Class.class))).thenAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) { + Class clazz = (Class) invocation.getArguments()[0]; + return mock(clazz); + } + }); + + commands = CommandInitializer.buildCommands(initializer); } @Test diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/AccountsCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/AccountsCommandTest.java index 5a837280..95124658 100644 --- a/src/test/java/fr/xephi/authme/command/executable/authme/AccountsCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/authme/AccountsCommandTest.java @@ -4,10 +4,14 @@ import fr.xephi.authme.cache.auth.PlayerAuth; import fr.xephi.authme.command.CommandService; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.output.MessageKey; +import fr.xephi.authme.output.Messages; import org.bukkit.command.CommandSender; -import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; import java.util.Arrays; import java.util.Collections; @@ -22,26 +26,23 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; /** * Test for {@link AccountsCommand}. */ +@RunWith(MockitoJUnitRunner.class) public class AccountsCommandTest { + @InjectMocks private AccountsCommand command; + @Mock private CommandSender sender; + @Mock private CommandService service; + @Mock private DataSource dataSource; - - @Before - public void setUpMocks() { - command = new AccountsCommand(); - sender = mock(CommandSender.class); - dataSource = mock(DataSource.class); - service = mock(CommandService.class); - when(service.getDataSource()).thenReturn(dataSource); - } + @Mock + private Messages messages; @Test public void shouldGetAccountsOfCurrentUser() { @@ -72,7 +73,7 @@ public class AccountsCommandTest { runInnerRunnable(service); // then - verify(service).send(sender, MessageKey.UNKNOWN_USER); + verify(messages).send(sender, MessageKey.UNKNOWN_USER); verify(sender, never()).sendMessage(anyString()); } @@ -88,7 +89,7 @@ public class AccountsCommandTest { runInnerRunnable(service); // then - verify(service).send(sender, MessageKey.USER_NOT_REGISTERED); + verify(messages).send(sender, MessageKey.USER_NOT_REGISTERED); verify(sender, never()).sendMessage(anyString()); } diff --git a/src/tools/commands/CommandPageCreater.java b/src/tools/commands/CommandPageCreater.java index 8d92dbfb..3369b906 100644 --- a/src/tools/commands/CommandPageCreater.java +++ b/src/tools/commands/CommandPageCreater.java @@ -2,7 +2,6 @@ package commands; import fr.xephi.authme.command.CommandArgumentDescription; import fr.xephi.authme.command.CommandDescription; -import fr.xephi.authme.command.CommandInitializer; import fr.xephi.authme.command.CommandPermissions; import fr.xephi.authme.command.CommandUtils; import fr.xephi.authme.permission.PermissionNode; @@ -13,6 +12,7 @@ import utils.ToolTask; import utils.ToolsConstants; import java.util.Collection; +import java.util.HashSet; import java.util.Scanner; import java.util.Set; @@ -27,7 +27,8 @@ public class CommandPageCreater implements ToolTask { @Override public void execute(Scanner scanner) { - final Set baseCommands = CommandInitializer.buildCommands(); + // TODO ljacqu 20160427: Fix initialization of commands + final Set baseCommands = new HashSet<>();//CommandInitializer.buildCommands(); NestedTagValue commandTags = new NestedTagValue(); addCommandsInfo(commandTags, baseCommands); From ee08eb9efb839d45943d34e0647f9cbea6125ceb Mon Sep 17 00:00:00 2001 From: ljacqu Date: Wed, 27 Apr 2016 23:15:32 +0200 Subject: [PATCH 003/200] Replace Java 1.8 method with 1.7 --- .../xephi/authme/initialization/AuthMeServiceInitializer.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/fr/xephi/authme/initialization/AuthMeServiceInitializer.java b/src/main/java/fr/xephi/authme/initialization/AuthMeServiceInitializer.java index e5dddd04..dff7a76f 100644 --- a/src/main/java/fr/xephi/authme/initialization/AuthMeServiceInitializer.java +++ b/src/main/java/fr/xephi/authme/initialization/AuthMeServiceInitializer.java @@ -1,7 +1,6 @@ package fr.xephi.authme.initialization; import com.google.common.base.Preconditions; -import com.google.common.base.Strings; import com.google.common.collect.ImmutableSet; import javax.annotation.PostConstruct; @@ -263,7 +262,7 @@ public class AuthMeServiceInitializer { private static void executePostConstructMethods(Object object) { for (Method method : object.getClass().getDeclaredMethods()) { if (method.isAnnotationPresent(PostConstruct.class)) { - if (method.getParameterCount() == 0) { + if (method.getParameterTypes().length == 0) { try { method.setAccessible(true); method.invoke(object); From 2c491803d3f79f1ae8b86fbe5655977d595b89e0 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Fri, 29 Apr 2016 23:49:03 +0200 Subject: [PATCH 004/200] Injector - disallow static PostConstruct methods, add more tests --- .../AuthMeServiceInitializer.java | 39 ++++---------- .../AuthMeServiceInitializerTest.java | 46 +++++++++++++++- .../initialization/FieldInjectionTest.java | 54 +++++++++++++++++++ .../initialization/samples/AlphaService.java | 10 ++++ .../initialization/samples/GammaService.java | 2 +- .../samples/InvalidPostConstruct.java | 35 +++++++++--- 6 files changed, 148 insertions(+), 38 deletions(-) create mode 100644 src/test/java/fr/xephi/authme/initialization/FieldInjectionTest.java diff --git a/src/main/java/fr/xephi/authme/initialization/AuthMeServiceInitializer.java b/src/main/java/fr/xephi/authme/initialization/AuthMeServiceInitializer.java index dff7a76f..560596b7 100644 --- a/src/main/java/fr/xephi/authme/initialization/AuthMeServiceInitializer.java +++ b/src/main/java/fr/xephi/authme/initialization/AuthMeServiceInitializer.java @@ -121,17 +121,19 @@ public class AuthMeServiceInitializer { if (Annotation.class.isAssignableFrom(clazz)) { throw new UnsupportedOperationException("Cannot retrieve annotated elements in this way!"); } else if (objects.containsKey(clazz)) { - return getObject(clazz); + return clazz.cast(objects.get(clazz)); } - // First time we come across clazz, need to instantiate it. Add the clazz to the list of traversed - // classes in a new list, so each path we need to take has its own Set. + // First time we come across clazz, need to instantiate it. Validate that we can do so validatePackage(clazz); validateInstantiable(clazz); + // Add the clazz to the list of traversed classes in a new Set, so each path we take has its own Set. traversedClasses = new HashSet<>(traversedClasses); traversedClasses.add(clazz); - return instantiate(clazz, traversedClasses); + T object = instantiate(clazz, traversedClasses); + storeObject(object); + return object; } /** @@ -154,7 +156,6 @@ public class AuthMeServiceInitializer { validateInjectionHasNoCircularDependencies(injection.getDependencies(), traversedClasses); Object[] dependencies = resolveDependencies(injection, traversedClasses); T object = injection.instantiateWith(dependencies); - storeObject(object); executePostConstructMethods(object); return object; } @@ -186,27 +187,6 @@ public class AuthMeServiceInitializer { return values; } - - /** - * Internal method to retrieve an object from the objects map for non-annotation classes. - * In such cases, the type of the entry always corresponds to the key, i.e. the entry of key - * {@code Class} is guaranteed to be of type {@code T}. - *

- * To retrieve values identified with an annotation, use {@code objects.get(clazz)} directly. - * We do not know or control the type of the value of keys of annotation classes. - * - * @param clazz the class to retrieve the implementation of - * @param the type - * @return the implementation - */ - private T getObject(Class clazz) { - Object o = objects.get(clazz); - if (o == null) { - throw new NullPointerException("No instance of " + clazz + " available"); - } - return clazz.cast(o); - } - /** * Stores the given object with its class as key. Throws an exception if the key already has * a value associated to it. @@ -262,7 +242,7 @@ public class AuthMeServiceInitializer { private static void executePostConstructMethods(Object object) { for (Method method : object.getClass().getDeclaredMethods()) { if (method.isAnnotationPresent(PostConstruct.class)) { - if (method.getParameterTypes().length == 0) { + if (method.getParameterTypes().length == 0 && !Modifier.isStatic(method.getModifiers())) { try { method.setAccessible(true); method.invoke(object); @@ -270,8 +250,9 @@ public class AuthMeServiceInitializer { throw new UnsupportedOperationException(e); } } else { - throw new IllegalStateException("@PostConstruct methods must have an empty parameter list. " + - "Found parameters in " + method + " belonging to " + object.getClass()); + throw new IllegalStateException(String.format("@PostConstruct methods may not be static or have " + + " any parameters. Method '%s' of class '%s' is either static or has parameters", + method.getName(), object.getClass().getSimpleName())); } } } diff --git a/src/test/java/fr/xephi/authme/initialization/AuthMeServiceInitializerTest.java b/src/test/java/fr/xephi/authme/initialization/AuthMeServiceInitializerTest.java index 61322291..54789ef0 100644 --- a/src/test/java/fr/xephi/authme/initialization/AuthMeServiceInitializerTest.java +++ b/src/test/java/fr/xephi/authme/initialization/AuthMeServiceInitializerTest.java @@ -1,5 +1,6 @@ package fr.xephi.authme.initialization; +import fr.xephi.authme.initialization.samples.AlphaService; import fr.xephi.authme.initialization.samples.BadFieldInjection; import fr.xephi.authme.initialization.samples.BetaManager; import fr.xephi.authme.initialization.samples.CircularClasses; @@ -18,6 +19,7 @@ import org.junit.Test; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.Matchers.sameInstance; import static org.junit.Assert.assertThat; /** @@ -47,12 +49,18 @@ public class AuthMeServiceInitializerTest { } } - @Test(expected = IllegalStateException.class) + @Test(expected = RuntimeException.class) public void shouldThrowForInvalidPackage() { // given / when / then initializer.get(InvalidClass.class); } + @Test(expected = RuntimeException.class) + public void shouldThrowForUnregisteredPrimitiveType() { + // given / when / then + initializer.get(int.class); + } + @Test public void shouldPassValueByAnnotation() { // given @@ -169,7 +177,19 @@ public class AuthMeServiceInitializerTest { @Test(expected = RuntimeException.class) public void shouldThrowForInvalidPostConstructMethod() { // given / when / then - initializer.get(InvalidPostConstruct.class); + initializer.get(InvalidPostConstruct.WithParams.class); + } + + @Test(expected = RuntimeException.class) + public void shouldThrowForStaticPostConstructMethod() { + // given / when / then + initializer.get(InvalidPostConstruct.Static.class); + } + + @Test(expected = RuntimeException.class) + public void shouldForwardExceptionFromPostConstruct() { + // given / when / then + initializer.get(InvalidPostConstruct.ThrowsException.class); } @Test(expected = RuntimeException.class) @@ -191,4 +211,26 @@ public class AuthMeServiceInitializerTest { assertThat(cwad.getAbstractDependency() == concrete, equalTo(true)); assertThat(cwad.getAlphaService(), not(nullValue())); } + + @Test(expected = RuntimeException.class) + public void shouldThrowForAlreadyRegisteredClass() { + // given + initializer.register(new BetaManager()); + + // when / then + initializer.register(BetaManager.class, new BetaManager()); + } + + @Test + public void shouldCreateNewUntrackedInstance() { + // given / when + AlphaService singletonScoped = initializer.get(AlphaService.class); + AlphaService requestScoped = initializer.newInstance(AlphaService.class); + + // then + assertThat(singletonScoped.getProvidedClass(), not(nullValue())); + assertThat(singletonScoped.getProvidedClass(), equalTo(requestScoped.getProvidedClass())); + assertThat(singletonScoped, not(sameInstance(requestScoped))); + } + } diff --git a/src/test/java/fr/xephi/authme/initialization/FieldInjectionTest.java b/src/test/java/fr/xephi/authme/initialization/FieldInjectionTest.java new file mode 100644 index 00000000..5f0b45c2 --- /dev/null +++ b/src/test/java/fr/xephi/authme/initialization/FieldInjectionTest.java @@ -0,0 +1,54 @@ +package fr.xephi.authme.initialization; + +import fr.xephi.authme.initialization.samples.AlphaService; +import fr.xephi.authme.initialization.samples.BetaManager; +import fr.xephi.authme.initialization.samples.ClassWithAnnotations; +import fr.xephi.authme.initialization.samples.Duration; +import fr.xephi.authme.initialization.samples.FieldInjectionWithAnnotations; +import fr.xephi.authme.initialization.samples.GammaService; +import fr.xephi.authme.initialization.samples.ProvidedClass; +import fr.xephi.authme.initialization.samples.Size; +import org.junit.Test; + +import static org.hamcrest.Matchers.arrayContaining; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertThat; + +/** + * Test for {@link FieldInjection}. + */ +public class FieldInjectionTest { + + @Test + public void shouldReturnDependencies() { + // given + FieldInjection injection = + FieldInjection.provide(FieldInjectionWithAnnotations.class).get(); + + // when + Class[] dependencies = injection.getDependencies(); + Class[] annotations = injection.getDependencyAnnotations(); + + // then + assertThat(dependencies, arrayContaining(BetaManager.class, int.class, long.class, ClassWithAnnotations.class)); + assertThat(annotations, arrayContaining((Class) null, Size.class, Duration.class, null)); + } + + @Test + public void shouldInstantiateClass() { + // given + FieldInjection injection = FieldInjection.provide(BetaManager.class).get(); + ProvidedClass providedClass = new ProvidedClass(""); + AlphaService alphaService = AlphaService.newInstance(providedClass); + GammaService gammaService = new GammaService(alphaService); + + // when + BetaManager betaManager = injection.instantiateWith(providedClass, gammaService, alphaService); + + // then + assertThat(betaManager, not(nullValue())); + assertThat(betaManager.getDependencies(), arrayContaining(providedClass, gammaService, alphaService)); + } + +} diff --git a/src/test/java/fr/xephi/authme/initialization/samples/AlphaService.java b/src/test/java/fr/xephi/authme/initialization/samples/AlphaService.java index 2a93a43c..21dd3b32 100644 --- a/src/test/java/fr/xephi/authme/initialization/samples/AlphaService.java +++ b/src/test/java/fr/xephi/authme/initialization/samples/AlphaService.java @@ -17,4 +17,14 @@ public class AlphaService { public ProvidedClass getProvidedClass() { return providedClass; } + + /** + * Creates a new instance (for instantiations in tests). + * + * @param providedClass . + * @return created instance + */ + public static AlphaService newInstance(ProvidedClass providedClass) { + return new AlphaService(providedClass); + } } diff --git a/src/test/java/fr/xephi/authme/initialization/samples/GammaService.java b/src/test/java/fr/xephi/authme/initialization/samples/GammaService.java index 8ee97032..9f9dd229 100644 --- a/src/test/java/fr/xephi/authme/initialization/samples/GammaService.java +++ b/src/test/java/fr/xephi/authme/initialization/samples/GammaService.java @@ -10,7 +10,7 @@ public class GammaService { private AlphaService alphaService; @Inject - GammaService(AlphaService alphaService) { + public GammaService(AlphaService alphaService) { this.alphaService = alphaService; } diff --git a/src/test/java/fr/xephi/authme/initialization/samples/InvalidPostConstruct.java b/src/test/java/fr/xephi/authme/initialization/samples/InvalidPostConstruct.java index 85c7f4c4..c80a0c6c 100644 --- a/src/test/java/fr/xephi/authme/initialization/samples/InvalidPostConstruct.java +++ b/src/test/java/fr/xephi/authme/initialization/samples/InvalidPostConstruct.java @@ -8,12 +8,35 @@ import javax.inject.Inject; */ public class InvalidPostConstruct { - @Inject - private AlphaService alphaService; - @Inject - private ProvidedClass providedClass; + public static final class WithParams { + @Inject + private AlphaService alphaService; + @Inject + private ProvidedClass providedClass; - @PostConstruct - public void invalidPostConstr(BetaManager betaManager) { + WithParams() { } + + @PostConstruct + public void invalidPostConstr(BetaManager betaManager) { + } + } + + public static final class Static { + @Inject + Static(BetaManager betaManager) { + // -- + } + + @PostConstruct + public static void invalidMethod() { + // -- + } + } + + public static final class ThrowsException { + @PostConstruct + public void throwingPostConstruct() { + throw new IllegalStateException("Exception in post construct"); + } } } From 908399e2716a5041932f70993a8005a1553f8630 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Sat, 30 Apr 2016 10:44:32 +0200 Subject: [PATCH 005/200] #432 Injector - prevent static field injection, add more tests --- src/main/java/fr/xephi/authme/AuthMe.java | 4 +- .../AuthMeServiceInitializer.java | 15 ---- .../authme/initialization/BaseCommands.java | 2 +- .../initialization/ConstructorInjection.java | 11 ++- .../authme/initialization/DataFolder.java | 2 +- .../authme/initialization/FieldInjection.java | 33 +++++--- .../AuthMeServiceInitializerTest.java | 22 +++-- .../ConstructorInjectionTest.java | 81 +++++++++++++++++++ .../initialization/FieldInjectionTest.java | 61 ++++++++++++++ .../samples/CircularClasses.java | 2 +- .../initialization/samples/GammaService.java | 2 +- .../samples/InvalidPostConstruct.java | 2 +- .../samples/InvalidStaticFieldInjection.java | 17 ++++ .../samples/PostConstructTestClass.java | 2 +- 14 files changed, 211 insertions(+), 45 deletions(-) create mode 100644 src/test/java/fr/xephi/authme/initialization/ConstructorInjectionTest.java create mode 100644 src/test/java/fr/xephi/authme/initialization/samples/InvalidStaticFieldInjection.java diff --git a/src/main/java/fr/xephi/authme/AuthMe.java b/src/main/java/fr/xephi/authme/AuthMe.java index b69cd299..b240edae 100644 --- a/src/main/java/fr/xephi/authme/AuthMe.java +++ b/src/main/java/fr/xephi/authme/AuthMe.java @@ -253,8 +253,8 @@ public class AuthMe extends JavaPlugin { initializer.provide(DataFolder.class, getDataFolder()); // Register elements we instantiate manually - initializer.register(newSettings); - initializer.register(messages); + initializer.register(NewSetting.class, newSettings); + initializer.register(Messages.class, messages); initializer.register(DataSource.class, database); initializer.provide(BaseCommands.class, CommandInitializer.buildCommands(initializer)); diff --git a/src/main/java/fr/xephi/authme/initialization/AuthMeServiceInitializer.java b/src/main/java/fr/xephi/authme/initialization/AuthMeServiceInitializer.java index 560596b7..576ea227 100644 --- a/src/main/java/fr/xephi/authme/initialization/AuthMeServiceInitializer.java +++ b/src/main/java/fr/xephi/authme/initialization/AuthMeServiceInitializer.java @@ -9,7 +9,6 @@ import java.lang.annotation.Annotation; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; -import java.lang.reflect.Type; import java.util.HashMap; import java.util.HashSet; import java.util.Map; @@ -51,20 +50,6 @@ public class AuthMeServiceInitializer { return get(clazz, new HashSet>()); } - /** - * Registers an instantiation by its type. - * - * @param object the object to register - * @throws IllegalStateException if an object of the same type has already been registered - */ - public void register(Object object) { - if (object instanceof Type) { - throw new IllegalStateException("You tried to register a Type object: '" + object - + "'. This likely indicates an error. Please use register(Class, T) if really desired."); - } - storeObject(object); - } - /** * Register an object with a custom class (supertype). Use this for example to specify a * concrete implementation of an interface or an abstract class. diff --git a/src/main/java/fr/xephi/authme/initialization/BaseCommands.java b/src/main/java/fr/xephi/authme/initialization/BaseCommands.java index 156cb947..8ff263ab 100644 --- a/src/main/java/fr/xephi/authme/initialization/BaseCommands.java +++ b/src/main/java/fr/xephi/authme/initialization/BaseCommands.java @@ -8,7 +8,7 @@ import java.lang.annotation.Target; /** * Annotation to denote the collection of AuthMe commands. */ -@Target(ElementType.PARAMETER) +@Target({ElementType.PARAMETER, ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) public @interface BaseCommands { } diff --git a/src/main/java/fr/xephi/authme/initialization/ConstructorInjection.java b/src/main/java/fr/xephi/authme/initialization/ConstructorInjection.java index f77ae58c..e80ea128 100644 --- a/src/main/java/fr/xephi/authme/initialization/ConstructorInjection.java +++ b/src/main/java/fr/xephi/authme/initialization/ConstructorInjection.java @@ -1,5 +1,7 @@ package fr.xephi.authme.initialization; +import com.google.common.base.Preconditions; + import javax.inject.Inject; import javax.inject.Provider; import java.lang.annotation.Annotation; @@ -36,6 +38,7 @@ class ConstructorInjection implements Injection { @Override public T instantiateWith(Object... values) { + validateNoNullValues(values); try { return constructor.newInstance(values); } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { @@ -60,7 +63,7 @@ class ConstructorInjection implements Injection { * * @param clazz the class to process * @param the class' type - * @return injection constructor for the class + * @return injection constructor for the class, null if not applicable */ @SuppressWarnings("unchecked") private static Constructor getInjectionConstructor(Class clazz) { @@ -74,4 +77,10 @@ class ConstructorInjection implements Injection { return null; } + private static void validateNoNullValues(Object[] array) { + for (Object entry : array) { + Preconditions.checkNotNull(entry); + } + } + } diff --git a/src/main/java/fr/xephi/authme/initialization/DataFolder.java b/src/main/java/fr/xephi/authme/initialization/DataFolder.java index 33b87997..0288f45a 100644 --- a/src/main/java/fr/xephi/authme/initialization/DataFolder.java +++ b/src/main/java/fr/xephi/authme/initialization/DataFolder.java @@ -8,7 +8,7 @@ import java.lang.annotation.Target; /** * Annotation for specifying the plugin's data folder. */ -@Target(ElementType.PARAMETER) +@Target({ElementType.PARAMETER, ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) public @interface DataFolder { } diff --git a/src/main/java/fr/xephi/authme/initialization/FieldInjection.java b/src/main/java/fr/xephi/authme/initialization/FieldInjection.java index aafb0bde..1e7973e2 100644 --- a/src/main/java/fr/xephi/authme/initialization/FieldInjection.java +++ b/src/main/java/fr/xephi/authme/initialization/FieldInjection.java @@ -8,6 +8,7 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -57,6 +58,7 @@ class FieldInjection implements Injection { for (int i = 0; i < fields.length; ++i) { try { + Preconditions.checkNotNull(values[i]); fields[i].set(instance, values[i]); } catch (IllegalAccessException e) { throw new UnsupportedOperationException(e); @@ -65,15 +67,15 @@ class FieldInjection implements Injection { return instance; } - private static Class getFirstNonInjectAnnotation(Field field) { - for (Annotation annotation : field.getAnnotations()) { - if (annotation.annotationType() != Inject.class) { - return annotation.annotationType(); - } - } - return null; - } - + /** + * Returns a provider for a {@code FieldInjection} instance, i.e. a provides an object + * with which field injection can be performed on the given class if applicable. The provided + * value is {@code null} if field injection cannot be applied to the class. + * + * @param clazz the class to provide field injection for + * @param the class' type + * @return field injection provider for the given class + */ public static Provider> provide(final Class clazz) { return new Provider>() { @Override @@ -92,6 +94,10 @@ class FieldInjection implements Injection { List fields = new ArrayList<>(); for (Field field : clazz.getDeclaredFields()) { if (field.isAnnotationPresent(Inject.class)) { + if (Modifier.isStatic(field.getModifiers())) { + throw new IllegalStateException(String.format("Field '%s' in class '%s' is static but " + + "annotated with @Inject", field.getName(), clazz.getSimpleName())); + } field.setAccessible(true); fields.add(field); } @@ -99,6 +105,15 @@ class FieldInjection implements Injection { return fields; } + private static Class getFirstNonInjectAnnotation(Field field) { + for (Annotation annotation : field.getAnnotations()) { + if (annotation.annotationType() != Inject.class) { + return annotation.annotationType(); + } + } + return null; + } + private static Constructor getDefaultConstructor(Class clazz) { try { Constructor defaultConstructor = clazz.getDeclaredConstructor(); diff --git a/src/test/java/fr/xephi/authme/initialization/AuthMeServiceInitializerTest.java b/src/test/java/fr/xephi/authme/initialization/AuthMeServiceInitializerTest.java index 54789ef0..0541c704 100644 --- a/src/test/java/fr/xephi/authme/initialization/AuthMeServiceInitializerTest.java +++ b/src/test/java/fr/xephi/authme/initialization/AuthMeServiceInitializerTest.java @@ -10,6 +10,7 @@ import fr.xephi.authme.initialization.samples.Duration; import fr.xephi.authme.initialization.samples.FieldInjectionWithAnnotations; import fr.xephi.authme.initialization.samples.InvalidClass; import fr.xephi.authme.initialization.samples.InvalidPostConstruct; +import fr.xephi.authme.initialization.samples.InvalidStaticFieldInjection; import fr.xephi.authme.initialization.samples.PostConstructTestClass; import fr.xephi.authme.initialization.samples.ProvidedClass; import fr.xephi.authme.initialization.samples.Size; @@ -34,7 +35,7 @@ public class AuthMeServiceInitializerTest { @Before public void setInitializer() { initializer = new AuthMeServiceInitializer(ALLOWED_PACKAGE); - initializer.register(new ProvidedClass("")); + initializer.register(ProvidedClass.class, new ProvidedClass("")); } @Test @@ -128,7 +129,7 @@ public class AuthMeServiceInitializerTest { @Test(expected = RuntimeException.class) public void shouldThrowForSecondRegistration() { // given / when / then - initializer.register(new ProvidedClass("")); + initializer.register(ProvidedClass.class, new ProvidedClass("")); } @Test(expected = RuntimeException.class) @@ -152,15 +153,6 @@ public class AuthMeServiceInitializerTest { initializer.register(String.class, null); } - @Test(expected = RuntimeException.class) - public void shouldThrowForRegisterOfType() { - // given / when / then - // this most likely means that the second argument was forgotten, so throw an error and force - // the API user to use the explicit register(Class.class, String.class) if really, really desired - // (Though for such generic types, an annotation would be a lot better) - initializer.register(String.class); - } - @Test public void shouldExecutePostConstructMethod() { // given @@ -215,7 +207,7 @@ public class AuthMeServiceInitializerTest { @Test(expected = RuntimeException.class) public void shouldThrowForAlreadyRegisteredClass() { // given - initializer.register(new BetaManager()); + initializer.register(BetaManager.class, new BetaManager()); // when / then initializer.register(BetaManager.class, new BetaManager()); @@ -233,4 +225,10 @@ public class AuthMeServiceInitializerTest { assertThat(singletonScoped, not(sameInstance(requestScoped))); } + @Test(expected = RuntimeException.class) + public void shouldThrowForStaticFieldInjection() { + // given / when / then + initializer.newInstance(InvalidStaticFieldInjection.class); + } + } diff --git a/src/test/java/fr/xephi/authme/initialization/ConstructorInjectionTest.java b/src/test/java/fr/xephi/authme/initialization/ConstructorInjectionTest.java new file mode 100644 index 00000000..dd21a590 --- /dev/null +++ b/src/test/java/fr/xephi/authme/initialization/ConstructorInjectionTest.java @@ -0,0 +1,81 @@ +package fr.xephi.authme.initialization; + +import fr.xephi.authme.initialization.samples.AlphaService; +import fr.xephi.authme.initialization.samples.ClassWithAnnotations; +import fr.xephi.authme.initialization.samples.Duration; +import fr.xephi.authme.initialization.samples.GammaService; +import fr.xephi.authme.initialization.samples.InvalidClass; +import fr.xephi.authme.initialization.samples.ProvidedClass; +import fr.xephi.authme.initialization.samples.Size; +import org.junit.Test; + +import static org.hamcrest.Matchers.arrayContaining; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertThat; + +/** + * Test for {@link ConstructorInjection}. + */ +public class ConstructorInjectionTest { + + @Test + public void shouldReturnDependencies() { + // given + Injection injection = ConstructorInjection.provide(ClassWithAnnotations.class).get(); + + // when + Class[] dependencies = injection.getDependencies(); + Class[] annotations = injection.getDependencyAnnotations(); + + // then + assertThat(dependencies, arrayContaining(int.class, GammaService.class, long.class)); + assertThat(annotations, arrayContaining((Class) Size.class, null, Duration.class)); + } + + @Test + public void shouldInstantiate() { + // given + GammaService gammaService = new GammaService( + AlphaService.newInstance(new ProvidedClass(""))); + Injection injection = ConstructorInjection.provide(ClassWithAnnotations.class).get(); + + // when + ClassWithAnnotations instance = injection.instantiateWith(-112, gammaService, 19L); + + // then + assertThat(instance, not(nullValue())); + assertThat(instance.getSize(), equalTo(-112)); + assertThat(instance.getGammaService(), equalTo(gammaService)); + assertThat(instance.getDuration(), equalTo(19L)); + } + + @Test(expected = NullPointerException.class) + public void shouldThrowForNullValue() { + // given + Injection injection = ConstructorInjection.provide(ClassWithAnnotations.class).get(); + + // when / then + injection.instantiateWith(-112, null, 12L); + } + + @Test(expected = RuntimeException.class) + public void shouldThrowUponInstantiationError() { + // given + AlphaService alphaService = AlphaService.newInstance(new ProvidedClass("")); + Injection injection = ConstructorInjection.provide(InvalidClass.class).get(); + + // when + injection.instantiateWith(alphaService, 5); + } + + @Test + public void shouldReturnNullForNoConstructorInjection() { + // given / when + Injection injection = ConstructorInjection.provide(FieldInjection.class).get(); + + // then + assertThat(injection, nullValue()); + } +} diff --git a/src/test/java/fr/xephi/authme/initialization/FieldInjectionTest.java b/src/test/java/fr/xephi/authme/initialization/FieldInjectionTest.java index 5f0b45c2..9d2294ae 100644 --- a/src/test/java/fr/xephi/authme/initialization/FieldInjectionTest.java +++ b/src/test/java/fr/xephi/authme/initialization/FieldInjectionTest.java @@ -1,15 +1,19 @@ package fr.xephi.authme.initialization; import fr.xephi.authme.initialization.samples.AlphaService; +import fr.xephi.authme.initialization.samples.BadFieldInjection; import fr.xephi.authme.initialization.samples.BetaManager; import fr.xephi.authme.initialization.samples.ClassWithAnnotations; import fr.xephi.authme.initialization.samples.Duration; import fr.xephi.authme.initialization.samples.FieldInjectionWithAnnotations; import fr.xephi.authme.initialization.samples.GammaService; +import fr.xephi.authme.initialization.samples.InvalidStaticFieldInjection; import fr.xephi.authme.initialization.samples.ProvidedClass; import fr.xephi.authme.initialization.samples.Size; import org.junit.Test; +import javax.inject.Inject; + import static org.hamcrest.Matchers.arrayContaining; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.nullValue; @@ -51,4 +55,61 @@ public class FieldInjectionTest { assertThat(betaManager.getDependencies(), arrayContaining(providedClass, gammaService, alphaService)); } + @Test + public void shouldProvideNullForImpossibleFieldInjection() { + // given / when + FieldInjection injection = FieldInjection.provide(BadFieldInjection.class).get(); + + // then + assertThat(injection, nullValue()); + } + + @Test(expected = RuntimeException.class) + public void shouldForwardExceptionDuringInstantiation() { + // given + FieldInjection injection = FieldInjection.provide(ThrowingConstructor.class).get(); + + // when / when + injection.instantiateWith(new ProvidedClass("")); + } + + @Test(expected = RuntimeException.class) + public void shouldThrowForInvalidFieldValue() { + // given + ProvidedClass providedClass = new ProvidedClass(""); + AlphaService alphaService = AlphaService.newInstance(providedClass); + GammaService gammaService = new GammaService(alphaService); + FieldInjection injection = FieldInjection.provide(BetaManager.class).get(); + + // when / then + // Correct order is provided, gamma, alpha + injection.instantiateWith(providedClass, alphaService, gammaService); + } + + @Test(expected = NullPointerException.class) + public void shouldThrowForNullValue() { + // given + ProvidedClass providedClass = new ProvidedClass(""); + AlphaService alphaService = AlphaService.newInstance(providedClass); + FieldInjection injection = FieldInjection.provide(BetaManager.class).get(); + + // when / then + // Correct order is provided, gamma, alpha + injection.instantiateWith(providedClass, null, alphaService); + } + + @Test(expected = RuntimeException.class) + public void shouldThrowForStaticFieldInjection() { + // given / when / then + FieldInjection.provide(InvalidStaticFieldInjection.class).get(); + } + + private static class ThrowingConstructor { + @Inject + private ProvidedClass providedClass; + + public ThrowingConstructor() { + throw new UnsupportedOperationException("Exception in constructor"); + } + } } diff --git a/src/test/java/fr/xephi/authme/initialization/samples/CircularClasses.java b/src/test/java/fr/xephi/authme/initialization/samples/CircularClasses.java index e9a1460e..dc2313e3 100644 --- a/src/test/java/fr/xephi/authme/initialization/samples/CircularClasses.java +++ b/src/test/java/fr/xephi/authme/initialization/samples/CircularClasses.java @@ -5,7 +5,7 @@ import javax.inject.Inject; /** * Classes with circular dependencies. */ -public class CircularClasses { +public abstract class CircularClasses { public static final class Circular1 { @Inject diff --git a/src/test/java/fr/xephi/authme/initialization/samples/GammaService.java b/src/test/java/fr/xephi/authme/initialization/samples/GammaService.java index 9f9dd229..9d6f9bfa 100644 --- a/src/test/java/fr/xephi/authme/initialization/samples/GammaService.java +++ b/src/test/java/fr/xephi/authme/initialization/samples/GammaService.java @@ -3,7 +3,7 @@ package fr.xephi.authme.initialization.samples; import javax.inject.Inject; /** - * Sample - class dependent on alpha and provided. + * Sample - class dependent on alpha service. */ public class GammaService { diff --git a/src/test/java/fr/xephi/authme/initialization/samples/InvalidPostConstruct.java b/src/test/java/fr/xephi/authme/initialization/samples/InvalidPostConstruct.java index c80a0c6c..e97c4f98 100644 --- a/src/test/java/fr/xephi/authme/initialization/samples/InvalidPostConstruct.java +++ b/src/test/java/fr/xephi/authme/initialization/samples/InvalidPostConstruct.java @@ -6,7 +6,7 @@ import javax.inject.Inject; /** * Class with invalid @PostConstruct method. */ -public class InvalidPostConstruct { +public abstract class InvalidPostConstruct { public static final class WithParams { @Inject diff --git a/src/test/java/fr/xephi/authme/initialization/samples/InvalidStaticFieldInjection.java b/src/test/java/fr/xephi/authme/initialization/samples/InvalidStaticFieldInjection.java new file mode 100644 index 00000000..199b2f4a --- /dev/null +++ b/src/test/java/fr/xephi/authme/initialization/samples/InvalidStaticFieldInjection.java @@ -0,0 +1,17 @@ +package fr.xephi.authme.initialization.samples; + +import javax.inject.Inject; + +/** + * Sample class - attempted field injection on a static member. + */ +public class InvalidStaticFieldInjection { + + @Inject + private ProvidedClass providedClass; + @Inject + protected static AlphaService alphaService; + + InvalidStaticFieldInjection() { } + +} diff --git a/src/test/java/fr/xephi/authme/initialization/samples/PostConstructTestClass.java b/src/test/java/fr/xephi/authme/initialization/samples/PostConstructTestClass.java index a4e57032..0bec84ee 100644 --- a/src/test/java/fr/xephi/authme/initialization/samples/PostConstructTestClass.java +++ b/src/test/java/fr/xephi/authme/initialization/samples/PostConstructTestClass.java @@ -4,7 +4,7 @@ import javax.annotation.PostConstruct; import javax.inject.Inject; /** - * Sample class for testing the execution of the @PostConstruct method. + * Sample class for testing the execution of @PostConstruct methods. */ public class PostConstructTestClass { From 3c6415a6a45d426ef66b30ec22fe3fa434c0f42c Mon Sep 17 00:00:00 2001 From: ljacqu Date: Sat, 30 Apr 2016 12:17:18 +0200 Subject: [PATCH 006/200] #432 Use injector instantiate hash algorithms --- .../AuthMeServiceInitializer.java | 1 + .../authme/security/PasswordSecurity.java | 67 +++++++------------ .../xephi/authme/security/crypts/BCRYPT.java | 4 +- .../authme/security/crypts/SALTED2MD5.java | 5 +- .../HashAlgorithmIntegrationTest.java | 13 ++-- .../xephi/authme/security/HashUtilsTest.java | 2 +- .../authme/security/PasswordSecurityTest.java | 50 ++++++++------ 7 files changed, 71 insertions(+), 71 deletions(-) diff --git a/src/main/java/fr/xephi/authme/initialization/AuthMeServiceInitializer.java b/src/main/java/fr/xephi/authme/initialization/AuthMeServiceInitializer.java index 576ea227..c2241c11 100644 --- a/src/main/java/fr/xephi/authme/initialization/AuthMeServiceInitializer.java +++ b/src/main/java/fr/xephi/authme/initialization/AuthMeServiceInitializer.java @@ -37,6 +37,7 @@ public class AuthMeServiceInitializer { public AuthMeServiceInitializer(String... allowedPackages) { ALLOWED_PACKAGES = ImmutableSet.copyOf(allowedPackages); objects = new HashMap<>(); + objects.put(getClass(), this); } /** diff --git a/src/main/java/fr/xephi/authme/security/PasswordSecurity.java b/src/main/java/fr/xephi/authme/security/PasswordSecurity.java index 458c9969..4a525531 100644 --- a/src/main/java/fr/xephi/authme/security/PasswordSecurity.java +++ b/src/main/java/fr/xephi/authme/security/PasswordSecurity.java @@ -2,34 +2,43 @@ package fr.xephi.authme.security; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.events.PasswordEncryptionEvent; +import fr.xephi.authme.initialization.AuthMeServiceInitializer; import fr.xephi.authme.security.crypts.EncryptionMethod; import fr.xephi.authme.security.crypts.HashedPassword; import fr.xephi.authme.settings.NewSetting; import fr.xephi.authme.settings.properties.SecuritySettings; import org.bukkit.plugin.PluginManager; +import javax.annotation.PostConstruct; import javax.inject.Inject; -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; /** * Manager class for password-related operations. */ public class PasswordSecurity { - private final NewSetting settings; - private HashAlgorithm algorithm; - private boolean supportOldAlgorithm; - private final DataSource dataSource; - private final PluginManager pluginManager; + @Inject + private NewSetting settings; @Inject - public PasswordSecurity(DataSource dataSource, NewSetting settings, PluginManager pluginManager) { - this.settings = settings; + private DataSource dataSource; + + @Inject + private PluginManager pluginManager; + + @Inject + private AuthMeServiceInitializer initializer; + + private HashAlgorithm algorithm; + private boolean supportOldAlgorithm; + + /** + * Load or reload the configuration. + */ + @PostConstruct + public void reload() { this.algorithm = settings.getProperty(SecuritySettings.PASSWORD_HASH); this.supportOldAlgorithm = settings.getProperty(SecuritySettings.SUPPORT_OLD_PASSWORD_HASH); - this.dataSource = dataSource; - this.pluginManager = pluginManager; } /** @@ -75,14 +84,6 @@ public class PasswordSecurity { || supportOldAlgorithm && compareWithAllEncryptionMethods(password, hashedPassword, playerLowerCase); } - /** - * Reload the configuration. - */ - public void reload() { - this.algorithm = settings.getProperty(SecuritySettings.PASSWORD_HASH); - this.supportOldAlgorithm = settings.getProperty(SecuritySettings.SUPPORT_OLD_PASSWORD_HASH); - } - /** * Compare the given hash with all available encryption methods to support * the migration to a new encryption method. Upon a successful match, the password @@ -97,7 +98,7 @@ public class PasswordSecurity { private boolean compareWithAllEncryptionMethods(String password, HashedPassword hashedPassword, String playerName) { for (HashAlgorithm algorithm : HashAlgorithm.values()) { if (!HashAlgorithm.CUSTOM.equals(algorithm)) { - EncryptionMethod method = initializeEncryptionMethod(algorithm, settings); + EncryptionMethod method = initializeEncryptionMethod(algorithm); if (methodMatches(method, password, hashedPassword, playerName)) { hashPasswordForNewAlgorithm(password, playerName); return true; @@ -135,7 +136,7 @@ public class PasswordSecurity { * @return The encryption method */ private EncryptionMethod initializeEncryptionMethodWithEvent(HashAlgorithm algorithm, String playerName) { - EncryptionMethod method = initializeEncryptionMethod(algorithm, settings); + EncryptionMethod method = initializeEncryptionMethod(algorithm); PasswordEncryptionEvent event = new PasswordEncryptionEvent(method, playerName); pluginManager.callEvent(event); return event.getMethod(); @@ -145,30 +146,14 @@ public class PasswordSecurity { * Initialize the encryption method associated with the given hash algorithm. * * @param algorithm The algorithm to retrieve the encryption method for - * @param settings The settings instance to pass to the constructor if required * * @return The associated encryption method, or null if CUSTOM / deprecated */ - public static EncryptionMethod initializeEncryptionMethod(HashAlgorithm algorithm, - NewSetting settings) { - try { - if (HashAlgorithm.CUSTOM.equals(algorithm) || HashAlgorithm.PLAINTEXT.equals(algorithm)) { - return null; - } - Constructor constructor = algorithm.getClazz().getConstructors()[0]; - Class[] parameters = constructor.getParameterTypes(); - if (parameters.length == 0) { - return (EncryptionMethod) constructor.newInstance(); - } else if (parameters.length == 1 && parameters[0] == NewSetting.class) { - return (EncryptionMethod) constructor.newInstance(settings); - } else { - throw new UnsupportedOperationException("Did not find default constructor or constructor with settings " - + "parameter in class " + algorithm.getClazz().getSimpleName()); - } - } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { - throw new UnsupportedOperationException("Constructor for '" + algorithm.getClazz().getSimpleName() - + "' could not be invoked. (Is there no default constructor?)", e); + public EncryptionMethod initializeEncryptionMethod(HashAlgorithm algorithm) { + if (HashAlgorithm.CUSTOM.equals(algorithm) || HashAlgorithm.PLAINTEXT.equals(algorithm)) { + return null; } + return initializer.newInstance(algorithm.getClazz()); } private void hashPasswordForNewAlgorithm(String password, String playerName) { diff --git a/src/main/java/fr/xephi/authme/security/crypts/BCRYPT.java b/src/main/java/fr/xephi/authme/security/crypts/BCRYPT.java index a114adc0..67d8c794 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/BCRYPT.java +++ b/src/main/java/fr/xephi/authme/security/crypts/BCRYPT.java @@ -9,13 +9,15 @@ import fr.xephi.authme.settings.NewSetting; import fr.xephi.authme.settings.properties.HooksSettings; import fr.xephi.authme.util.StringUtils; +import javax.inject.Inject; @Recommendation(Usage.RECOMMENDED) // provided the salt length is >= 8 -@HasSalt(value = SaltType.TEXT) // length depends on Settings.bCryptLog2Rounds +@HasSalt(value = SaltType.TEXT) // length depends on the bcryptLog2Rounds setting public class BCRYPT implements EncryptionMethod { private final int bCryptLog2Rounds; + @Inject public BCRYPT(NewSetting settings) { this.bCryptLog2Rounds = settings.getProperty(HooksSettings.BCRYPT_LOG2_ROUND); } diff --git a/src/main/java/fr/xephi/authme/security/crypts/SALTED2MD5.java b/src/main/java/fr/xephi/authme/security/crypts/SALTED2MD5.java index 27066f7a..bf67432b 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/SALTED2MD5.java +++ b/src/main/java/fr/xephi/authme/security/crypts/SALTED2MD5.java @@ -8,14 +8,17 @@ import fr.xephi.authme.security.crypts.description.Usage; import fr.xephi.authme.settings.NewSetting; import fr.xephi.authme.settings.properties.SecuritySettings; +import javax.inject.Inject; + import static fr.xephi.authme.security.HashUtils.md5; @Recommendation(Usage.ACCEPTABLE) // presuming that length is something sensible (>= 8) -@HasSalt(value = SaltType.TEXT) // length defined by Settings.saltLength +@HasSalt(value = SaltType.TEXT) // length defined by the doubleMd5SaltLength setting public class SALTED2MD5 extends SeparateSaltMethod { private final int saltLength; + @Inject public SALTED2MD5(NewSetting settings) { saltLength = settings.getProperty(SecuritySettings.DOUBLE_MD5_SALT_LENGTH); } diff --git a/src/test/java/fr/xephi/authme/security/HashAlgorithmIntegrationTest.java b/src/test/java/fr/xephi/authme/security/HashAlgorithmIntegrationTest.java index 976a7a44..30940c34 100644 --- a/src/test/java/fr/xephi/authme/security/HashAlgorithmIntegrationTest.java +++ b/src/test/java/fr/xephi/authme/security/HashAlgorithmIntegrationTest.java @@ -1,5 +1,6 @@ package fr.xephi.authme.security; +import fr.xephi.authme.initialization.AuthMeServiceInitializer; import fr.xephi.authme.security.crypts.EncryptionMethod; import fr.xephi.authme.security.crypts.HashedPassword; import fr.xephi.authme.settings.NewSetting; @@ -13,8 +14,6 @@ import java.util.HashSet; import java.util.Set; import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.not; -import static org.hamcrest.Matchers.nullValue; import static org.junit.Assert.assertThat; import static org.junit.Assert.fail; import static org.mockito.BDDMockito.given; @@ -25,13 +24,15 @@ import static org.mockito.Mockito.mock; */ public class HashAlgorithmIntegrationTest { - private static NewSetting settings; + private static AuthMeServiceInitializer initializer; @BeforeClass public static void setUpWrapper() { - settings = mock(NewSetting.class); + NewSetting settings = mock(NewSetting.class); given(settings.getProperty(HooksSettings.BCRYPT_LOG2_ROUND)).willReturn(8); given(settings.getProperty(SecuritySettings.DOUBLE_MD5_SALT_LENGTH)).willReturn(16); + initializer = new AuthMeServiceInitializer(); + initializer.register(NewSetting.class, settings); } @Test @@ -55,9 +56,7 @@ public class HashAlgorithmIntegrationTest { // given / when / then for (HashAlgorithm algorithm : HashAlgorithm.values()) { if (!HashAlgorithm.CUSTOM.equals(algorithm) && !HashAlgorithm.PLAINTEXT.equals(algorithm)) { - EncryptionMethod method = PasswordSecurity.initializeEncryptionMethod(algorithm, settings); - assertThat("Encryption method for algorithm '" + algorithm + "' is not null", - method, not(nullValue())); + EncryptionMethod method = initializer.newInstance(algorithm.getClazz()); HashedPassword hashedPassword = method.computeHash("pwd", "name"); assertThat("Salt should not be null if method.hasSeparateSalt(), and vice versa. Method: '" + method + "'", StringUtils.isEmpty(hashedPassword.getSalt()), equalTo(!method.hasSeparateSalt())); diff --git a/src/test/java/fr/xephi/authme/security/HashUtilsTest.java b/src/test/java/fr/xephi/authme/security/HashUtilsTest.java index e515fdbb..c7ef1ccd 100644 --- a/src/test/java/fr/xephi/authme/security/HashUtilsTest.java +++ b/src/test/java/fr/xephi/authme/security/HashUtilsTest.java @@ -18,7 +18,7 @@ public class HashUtilsTest { /** * List of passwords whose hash is provided to the class to test against. */ - public static final String[] GIVEN_PASSWORDS = {"", "password", "PassWord1", "&^%te$t?Pw@_"}; + private static final String[] GIVEN_PASSWORDS = {"", "password", "PassWord1", "&^%te$t?Pw@_"}; @Test public void shouldHashMd5() { diff --git a/src/test/java/fr/xephi/authme/security/PasswordSecurityTest.java b/src/test/java/fr/xephi/authme/security/PasswordSecurityTest.java index 470b8a2d..f3d577dc 100644 --- a/src/test/java/fr/xephi/authme/security/PasswordSecurityTest.java +++ b/src/test/java/fr/xephi/authme/security/PasswordSecurityTest.java @@ -4,6 +4,7 @@ import fr.xephi.authme.ReflectionTestUtils; import fr.xephi.authme.TestHelper; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.events.PasswordEncryptionEvent; +import fr.xephi.authme.initialization.AuthMeServiceInitializer; import fr.xephi.authme.security.crypts.EncryptionMethod; import fr.xephi.authme.security.crypts.HashedPassword; import fr.xephi.authme.security.crypts.JOOMLA; @@ -30,7 +31,6 @@ import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.argThat; import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -41,12 +41,20 @@ import static org.mockito.Mockito.verify; @RunWith(MockitoJUnitRunner.class) public class PasswordSecurityTest { + private AuthMeServiceInitializer initializer; + + @Mock + private NewSetting settings; + @Mock private PluginManager pluginManager; + @Mock private DataSource dataSource; + @Mock private EncryptionMethod method; + private Class caughtClassInEvent; @BeforeClass @@ -71,6 +79,10 @@ public class PasswordSecurityTest { return null; } }).when(pluginManager).callEvent(any(Event.class)); + initializer = new AuthMeServiceInitializer(new String[]{}); + initializer.register(NewSetting.class, settings); + initializer.register(DataSource.class, dataSource); + initializer.register(PluginManager.class, pluginManager); } @Test @@ -84,8 +96,8 @@ public class PasswordSecurityTest { given(dataSource.getPassword(playerName)).willReturn(password); given(method.comparePassword(clearTextPass, password, playerLowerCase)).willReturn(true); - PasswordSecurity security = - new PasswordSecurity(dataSource, mockSettings(HashAlgorithm.BCRYPT, false), pluginManager); + initSettings(HashAlgorithm.BCRYPT, false); + PasswordSecurity security = initializer.newInstance(PasswordSecurity.class); // when boolean result = security.comparePassword(clearTextPass, playerName); @@ -107,8 +119,8 @@ public class PasswordSecurityTest { given(dataSource.getPassword(playerName)).willReturn(password); given(method.comparePassword(clearTextPass, password, playerLowerCase)).willReturn(false); - PasswordSecurity security = - new PasswordSecurity(dataSource, mockSettings(HashAlgorithm.CUSTOM, false), pluginManager); + initSettings(HashAlgorithm.CUSTOM, false); + PasswordSecurity security = initializer.newInstance(PasswordSecurity.class); // when boolean result = security.comparePassword(clearTextPass, playerName); @@ -127,8 +139,8 @@ public class PasswordSecurityTest { String clearTextPass = "tables"; given(dataSource.getPassword(playerName)).willReturn(null); - PasswordSecurity security = - new PasswordSecurity(dataSource, mockSettings(HashAlgorithm.MD5, false), pluginManager); + initSettings(HashAlgorithm.MD5, false); + PasswordSecurity security = initializer.newInstance(PasswordSecurity.class); // when boolean result = security.comparePassword(clearTextPass, playerName); @@ -155,8 +167,8 @@ public class PasswordSecurityTest { given(dataSource.getPassword(argThat(equalToIgnoringCase(playerName)))).willReturn(password); given(method.comparePassword(clearTextPass, password, playerLowerCase)).willReturn(false); given(method.computeHash(clearTextPass, playerLowerCase)).willReturn(newPassword); - PasswordSecurity security = - new PasswordSecurity(dataSource, mockSettings(HashAlgorithm.MD5, true), pluginManager); + initSettings(HashAlgorithm.MD5, true); + PasswordSecurity security = initializer.newInstance(PasswordSecurity.class); // when boolean result = security.comparePassword(clearTextPass, playerName); @@ -180,8 +192,8 @@ public class PasswordSecurityTest { String clearTextPass = "someInvalidPassword"; given(dataSource.getPassword(playerName)).willReturn(password); given(method.comparePassword(clearTextPass, password, playerName)).willReturn(false); - PasswordSecurity security = - new PasswordSecurity(dataSource, mockSettings(HashAlgorithm.MD5, true), pluginManager); + initSettings(HashAlgorithm.MD5, true); + PasswordSecurity security = initializer.newInstance(PasswordSecurity.class); // when boolean result = security.comparePassword(clearTextPass, playerName); @@ -199,8 +211,8 @@ public class PasswordSecurityTest { String usernameLowerCase = username.toLowerCase(); HashedPassword hashedPassword = new HashedPassword("$T$est#Hash", "__someSalt__"); given(method.computeHash(password, usernameLowerCase)).willReturn(hashedPassword); - PasswordSecurity security = - new PasswordSecurity(dataSource, mockSettings(HashAlgorithm.JOOMLA, true), pluginManager); + initSettings(HashAlgorithm.JOOMLA, true); + PasswordSecurity security = initializer.newInstance(PasswordSecurity.class); // when HashedPassword result = security.computeHash(password, username); @@ -222,8 +234,8 @@ public class PasswordSecurityTest { HashedPassword hashedPassword = new HashedPassword("~T!est#Hash"); given(method.computeHash(password, username)).willReturn(hashedPassword); given(method.hasSeparateSalt()).willReturn(true); - PasswordSecurity security = - new PasswordSecurity(dataSource, mockSettings(HashAlgorithm.XAUTH, false), pluginManager); + initSettings(HashAlgorithm.XAUTH, false); + PasswordSecurity security = initializer.newInstance(PasswordSecurity.class); // when boolean result = security.comparePassword(password, hashedPassword, username); @@ -238,8 +250,8 @@ public class PasswordSecurityTest { @Test public void shouldReloadSettings() { // given - NewSetting settings = mockSettings(HashAlgorithm.BCRYPT, false); - PasswordSecurity passwordSecurity = new PasswordSecurity(dataSource, settings, pluginManager); + initSettings(HashAlgorithm.BCRYPT, false); + PasswordSecurity passwordSecurity = initializer.newInstance(PasswordSecurity.class); given(settings.getProperty(SecuritySettings.PASSWORD_HASH)).willReturn(HashAlgorithm.MD5); given(settings.getProperty(SecuritySettings.SUPPORT_OLD_PASSWORD_HASH)).willReturn(true); @@ -253,13 +265,11 @@ public class PasswordSecurityTest { equalTo((Object) Boolean.TRUE)); } - private static NewSetting mockSettings(HashAlgorithm algorithm, boolean supportOldPassword) { - NewSetting settings = mock(NewSetting.class); + private void initSettings(HashAlgorithm algorithm, boolean supportOldPassword) { given(settings.getProperty(SecuritySettings.PASSWORD_HASH)).willReturn(algorithm); given(settings.getProperty(SecuritySettings.SUPPORT_OLD_PASSWORD_HASH)).willReturn(supportOldPassword); given(settings.getProperty(HooksSettings.BCRYPT_LOG2_ROUND)).willReturn(8); given(settings.getProperty(SecuritySettings.DOUBLE_MD5_SALT_LENGTH)).willReturn(16); - return settings; } } From 9af596327a88ddb5241b27664754eea5658b1a05 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Mon, 2 May 2016 18:52:34 +0200 Subject: [PATCH 007/200] #432 Inject in commands: DataSource / AntiBot / PasswordSecurity / PlayerCache - Inject the services instead of passing them through the command service --- src/main/java/fr/xephi/authme/AuthMe.java | 2 +- .../xephi/authme/command/CommandService.java | 31 ----------- .../authme/ChangePasswordAdminCommand.java | 20 +++++-- .../executable/authme/GetEmailCommand.java | 7 ++- .../executable/authme/LastLoginCommand.java | 7 ++- .../authme/PurgeBannedPlayersCommand.java | 9 +++- .../executable/authme/PurgeCommand.java | 7 ++- .../authme/PurgeLastPositionCommand.java | 13 +++-- .../authme/RegisterAdminCommand.java | 18 +++++-- .../executable/authme/SetEmailCommand.java | 12 +++-- .../authme/SwitchAntiBotCommand.java | 5 +- .../authme/UnregisterAdminCommand.java | 14 +++-- .../changepassword/ChangePasswordCommand.java | 5 +- .../executable/email/RecoverEmailCommand.java | 18 +++++-- .../authme/command/CommandServiceTest.java | 26 --------- .../ChangePasswordAdminCommandTest.java | 54 ++++++------------- .../authme/GetEmailCommandTest.java | 31 ++++++----- .../authme/LastLoginCommandTest.java | 40 ++++++-------- .../authme/PurgeLastPositionCommandTest.java | 44 ++++++--------- .../authme/RegisterAdminCommandTest.java | 31 ++++------- .../authme/SwitchAntiBotCommandTest.java | 33 +++++------- .../ChangePasswordCommandTest.java | 24 +++++---- .../EncryptionMethodInfoGatherer.java | 3 +- 23 files changed, 211 insertions(+), 243 deletions(-) diff --git a/src/main/java/fr/xephi/authme/AuthMe.java b/src/main/java/fr/xephi/authme/AuthMe.java index b240edae..626ed2a6 100644 --- a/src/main/java/fr/xephi/authme/AuthMe.java +++ b/src/main/java/fr/xephi/authme/AuthMe.java @@ -256,11 +256,11 @@ public class AuthMe extends JavaPlugin { initializer.register(NewSetting.class, newSettings); initializer.register(Messages.class, messages); initializer.register(DataSource.class, database); - initializer.provide(BaseCommands.class, CommandInitializer.buildCommands(initializer)); // Some statically injected things initializer.register(PlayerCache.class, PlayerCache.getInstance()); initializer.register(LimboCache.class, LimboCache.getInstance()); + initializer.provide(BaseCommands.class, CommandInitializer.buildCommands(initializer)); permsMan = initializer.get(PermissionsManager.class); bukkitService = initializer.get(BukkitService.class); diff --git a/src/main/java/fr/xephi/authme/command/CommandService.java b/src/main/java/fr/xephi/authme/command/CommandService.java index d4262944..95ddb563 100644 --- a/src/main/java/fr/xephi/authme/command/CommandService.java +++ b/src/main/java/fr/xephi/authme/command/CommandService.java @@ -1,10 +1,7 @@ package fr.xephi.authme.command; -import fr.xephi.authme.AntiBot; import fr.xephi.authme.AuthMe; -import fr.xephi.authme.cache.auth.PlayerCache; import fr.xephi.authme.command.help.HelpProvider; -import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.hooks.PluginHooks; import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.output.Messages; @@ -48,8 +45,6 @@ public class CommandService { @Inject private SpawnLoader spawnLoader; @Inject - private AntiBot antiBot; - @Inject private ValidationService validationService; @Inject private BukkitService bukkitService; @@ -95,15 +90,6 @@ public class CommandService { authMe.getServer().getScheduler().runTaskAsynchronously(authMe, task); } - /** - * Return the AuthMe data source. - * - * @return The used data source - */ - public DataSource getDataSource() { - return authMe.getDataSource(); - } - /** * Return the AuthMe instance for further manipulation. Use only if other methods from * the command service cannot be used. @@ -114,15 +100,6 @@ public class CommandService { return authMe; } - /** - * Return the PasswordSecurity instance. - * - * @return The password security instance - */ - public PasswordSecurity getPasswordSecurity() { - return passwordSecurity; - } - /** * Output the help for a given command. * @@ -185,10 +162,6 @@ public class CommandService { return settings; } - public PlayerCache getPlayerCache() { - return PlayerCache.getInstance(); - } - public PluginHooks getPluginHooks() { return pluginHooks; } @@ -197,10 +170,6 @@ public class CommandService { return spawnLoader; } - public AntiBot getAntiBot() { - return antiBot; - } - /** * Verifies whether a password is valid according to the plugin settings. * 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 867443ed..f81ac07f 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 @@ -2,13 +2,16 @@ package fr.xephi.authme.command.executable.authme; import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.cache.auth.PlayerAuth; +import fr.xephi.authme.cache.auth.PlayerCache; import fr.xephi.authme.command.CommandService; import fr.xephi.authme.command.ExecutableCommand; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.output.MessageKey; +import fr.xephi.authme.security.PasswordSecurity; import fr.xephi.authme.security.crypts.HashedPassword; import org.bukkit.command.CommandSender; +import javax.inject.Inject; import java.util.List; /** @@ -16,6 +19,15 @@ import java.util.List; */ public class ChangePasswordAdminCommand implements ExecutableCommand { + @Inject + private PasswordSecurity passwordSecurity; + + @Inject + private PlayerCache playerCache; + + @Inject + private DataSource dataSource; + @Override public void executeCommand(final CommandSender sender, List arguments, final CommandService commandService) { @@ -36,10 +48,9 @@ public class ChangePasswordAdminCommand implements ExecutableCommand { @Override public void run() { - DataSource dataSource = commandService.getDataSource(); PlayerAuth auth = null; - if (commandService.getPlayerCache().isAuthenticated(playerNameLowerCase)) { - auth = commandService.getPlayerCache().getAuth(playerNameLowerCase); + if (playerCache.isAuthenticated(playerNameLowerCase)) { + auth = playerCache.getAuth(playerNameLowerCase); } else if (dataSource.isAuthAvailable(playerNameLowerCase)) { auth = dataSource.getAuth(playerNameLowerCase); } @@ -48,8 +59,7 @@ public class ChangePasswordAdminCommand implements ExecutableCommand { return; } - HashedPassword hashedPassword = commandService.getPasswordSecurity() - .computeHash(playerPass, playerNameLowerCase); + HashedPassword hashedPassword = passwordSecurity.computeHash(playerPass, playerNameLowerCase); auth.setPassword(hashedPassword); if (!dataSource.updatePassword(auth)) { diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/GetEmailCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/GetEmailCommand.java index 53a3051c..ede9a424 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/GetEmailCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/GetEmailCommand.java @@ -3,9 +3,11 @@ package fr.xephi.authme.command.executable.authme; import fr.xephi.authme.cache.auth.PlayerAuth; import fr.xephi.authme.command.CommandService; import fr.xephi.authme.command.ExecutableCommand; +import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.output.MessageKey; import org.bukkit.command.CommandSender; +import javax.inject.Inject; import java.util.List; /** @@ -13,11 +15,14 @@ import java.util.List; */ public class GetEmailCommand implements ExecutableCommand { + @Inject + private DataSource dataSource; + @Override public void executeCommand(CommandSender sender, List arguments, CommandService commandService) { String playerName = arguments.isEmpty() ? sender.getName() : arguments.get(0); - PlayerAuth auth = commandService.getDataSource().getAuth(playerName); + PlayerAuth auth = dataSource.getAuth(playerName); if (auth == null) { commandService.send(sender, MessageKey.UNKNOWN_USER); } else { diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/LastLoginCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/LastLoginCommand.java index 05fdab2b..4f0135a2 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/LastLoginCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/LastLoginCommand.java @@ -3,9 +3,11 @@ package fr.xephi.authme.command.executable.authme; import fr.xephi.authme.cache.auth.PlayerAuth; import fr.xephi.authme.command.CommandService; import fr.xephi.authme.command.ExecutableCommand; +import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.output.MessageKey; import org.bukkit.command.CommandSender; +import javax.inject.Inject; import java.util.Date; import java.util.List; @@ -14,12 +16,15 @@ import java.util.List; */ public class LastLoginCommand implements ExecutableCommand { + @Inject + private DataSource dataSource; + @Override public void executeCommand(CommandSender sender, List arguments, CommandService commandService) { // Get the player String playerName = (arguments.size() >= 1) ? arguments.get(0) : sender.getName(); - PlayerAuth auth = commandService.getDataSource().getAuth(playerName); + PlayerAuth auth = dataSource.getAuth(playerName); if (auth == null) { commandService.send(sender, MessageKey.USER_NOT_REGISTERED); return; diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/PurgeBannedPlayersCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/PurgeBannedPlayersCommand.java index 0ace9424..99535027 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/PurgeBannedPlayersCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/PurgeBannedPlayersCommand.java @@ -3,10 +3,12 @@ package fr.xephi.authme.command.executable.authme; import fr.xephi.authme.AuthMe; import fr.xephi.authme.command.CommandService; import fr.xephi.authme.command.ExecutableCommand; +import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.settings.properties.PurgeSettings; import org.bukkit.OfflinePlayer; import org.bukkit.command.CommandSender; +import javax.inject.Inject; import java.util.ArrayList; import java.util.List; @@ -16,6 +18,9 @@ import java.util.List; */ public class PurgeBannedPlayersCommand implements ExecutableCommand { + @Inject + private DataSource dataSource; + @Override public void executeCommand(CommandSender sender, List arguments, CommandService commandService) { // AuthMe plugin instance @@ -23,12 +28,12 @@ public class PurgeBannedPlayersCommand implements ExecutableCommand { // Get the list of banned players List bannedPlayers = new ArrayList<>(); - for (OfflinePlayer offlinePlayer : plugin.getServer().getBannedPlayers()) { + for (OfflinePlayer offlinePlayer : commandService.getBukkitService().getBannedPlayers()) { bannedPlayers.add(offlinePlayer.getName().toLowerCase()); } // Purge the banned players - commandService.getDataSource().purgeBanned(bannedPlayers); + dataSource.purgeBanned(bannedPlayers); if (commandService.getProperty(PurgeSettings.REMOVE_ESSENTIALS_FILES) && commandService.getPluginHooks().isEssentialsAvailable()) plugin.dataManager.purgeEssentials(bannedPlayers); diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/PurgeCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/PurgeCommand.java index 43e1daa5..51ca6df1 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/PurgeCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/PurgeCommand.java @@ -3,10 +3,12 @@ package fr.xephi.authme.command.executable.authme; import fr.xephi.authme.AuthMe; import fr.xephi.authme.command.CommandService; import fr.xephi.authme.command.ExecutableCommand; +import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.settings.properties.PurgeSettings; import org.bukkit.ChatColor; import org.bukkit.command.CommandSender; +import javax.inject.Inject; import java.util.Calendar; import java.util.List; @@ -18,6 +20,9 @@ public class PurgeCommand implements ExecutableCommand { private static final int MINIMUM_LAST_SEEN_DAYS = 30; + @Inject + private DataSource dataSource; + @Override public void executeCommand(CommandSender sender, List arguments, CommandService commandService) { // Get the days parameter @@ -45,7 +50,7 @@ public class PurgeCommand implements ExecutableCommand { long until = calendar.getTimeInMillis(); // Purge the data, get the purged values - List purged = commandService.getDataSource().autoPurgeDatabase(until); + List purged = dataSource.autoPurgeDatabase(until); // Show a status message sender.sendMessage(ChatColor.GOLD + "Deleted " + purged.size() + " user accounts"); diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/PurgeLastPositionCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/PurgeLastPositionCommand.java index 118f8460..7c88df86 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/PurgeLastPositionCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/PurgeLastPositionCommand.java @@ -3,9 +3,11 @@ package fr.xephi.authme.command.executable.authme; import fr.xephi.authme.cache.auth.PlayerAuth; import fr.xephi.authme.command.CommandService; import fr.xephi.authme.command.ExecutableCommand; +import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.output.MessageKey; import org.bukkit.command.CommandSender; +import javax.inject.Inject; import java.util.List; /** @@ -13,26 +15,29 @@ import java.util.List; */ public class PurgeLastPositionCommand implements ExecutableCommand { + @Inject + private DataSource dataSource; + @Override public void executeCommand(final CommandSender sender, List arguments, CommandService commandService) { String playerName = arguments.isEmpty() ? sender.getName() : arguments.get(0); if ("*".equals(playerName)) { - for (PlayerAuth auth : commandService.getDataSource().getAllAuths()) { + for (PlayerAuth auth : dataSource.getAllAuths()) { resetLastPosition(auth); - commandService.getDataSource().updateQuitLoc(auth); + dataSource.updateQuitLoc(auth); } sender.sendMessage("All players last position locations are now reset"); } else { // Get the user auth and make sure the user exists - PlayerAuth auth = commandService.getDataSource().getAuth(playerName); + PlayerAuth auth = dataSource.getAuth(playerName); if (auth == null) { commandService.send(sender, MessageKey.UNKNOWN_USER); return; } resetLastPosition(auth); - commandService.getDataSource().updateQuitLoc(auth); + dataSource.updateQuitLoc(auth); sender.sendMessage(playerName + "'s last position location is now reset"); } } 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 2caf7a3c..d61ea002 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 @@ -4,11 +4,14 @@ import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.cache.auth.PlayerAuth; import fr.xephi.authme.command.CommandService; import fr.xephi.authme.command.ExecutableCommand; +import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.output.MessageKey; +import fr.xephi.authme.security.PasswordSecurity; import fr.xephi.authme.security.crypts.HashedPassword; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; +import javax.inject.Inject; import java.util.List; /** @@ -16,6 +19,12 @@ import java.util.List; */ public class RegisterAdminCommand implements ExecutableCommand { + @Inject + private PasswordSecurity passwordSecurity; + + @Inject + private DataSource dataSource; + @Override public void executeCommand(final CommandSender sender, List arguments, final CommandService commandService) { @@ -35,23 +44,22 @@ public class RegisterAdminCommand implements ExecutableCommand { @Override public void run() { - if (commandService.getDataSource().isAuthAvailable(playerNameLowerCase)) { + if (dataSource.isAuthAvailable(playerNameLowerCase)) { commandService.send(sender, MessageKey.NAME_ALREADY_REGISTERED); return; } - HashedPassword hashedPassword = commandService.getPasswordSecurity() - .computeHash(playerPass, playerNameLowerCase); + HashedPassword hashedPassword = passwordSecurity.computeHash(playerPass, playerNameLowerCase); PlayerAuth auth = PlayerAuth.builder() .name(playerNameLowerCase) .realName(playerName) .password(hashedPassword) .build(); - if (!commandService.getDataSource().saveAuth(auth)) { + if (!dataSource.saveAuth(auth)) { commandService.send(sender, MessageKey.ERROR); return; } - commandService.getDataSource().setUnlogged(playerNameLowerCase); + dataSource.setUnlogged(playerNameLowerCase); commandService.send(sender, MessageKey.REGISTER_SUCCESS); ConsoleLogger.info(sender.getName() + " registered " + playerName); diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/SetEmailCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/SetEmailCommand.java index 0e7bcf35..989c7210 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/SetEmailCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/SetEmailCommand.java @@ -8,6 +8,7 @@ import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.output.MessageKey; import org.bukkit.command.CommandSender; +import javax.inject.Inject; import java.util.List; /** @@ -15,6 +16,12 @@ import java.util.List; */ public class SetEmailCommand implements ExecutableCommand { + @Inject + private DataSource dataSource; + + @Inject + private PlayerCache playerCache; + @Override public void executeCommand(final CommandSender sender, List arguments, final CommandService commandService) { @@ -32,7 +39,6 @@ public class SetEmailCommand implements ExecutableCommand { @Override public void run() { // Validate the user - DataSource dataSource = commandService.getDataSource(); PlayerAuth auth = dataSource.getAuth(playerName); if (auth == null) { commandService.send(sender, MessageKey.UNKNOWN_USER); @@ -50,8 +56,8 @@ public class SetEmailCommand implements ExecutableCommand { } // Update the player cache - if (PlayerCache.getInstance().getAuth(playerName) != null) { - PlayerCache.getInstance().updatePlayer(auth); + if (playerCache.getAuth(playerName) != null) { + playerCache.updatePlayer(auth); } // Show a status message diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/SwitchAntiBotCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/SwitchAntiBotCommand.java index 3c36c169..e996e6f0 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/SwitchAntiBotCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/SwitchAntiBotCommand.java @@ -8,6 +8,7 @@ import fr.xephi.authme.command.help.HelpProvider; import org.bukkit.ChatColor; import org.bukkit.command.CommandSender; +import javax.inject.Inject; import java.util.Arrays; import java.util.List; @@ -16,9 +17,11 @@ import java.util.List; */ public class SwitchAntiBotCommand implements ExecutableCommand { + @Inject + private AntiBot antiBot; + @Override public void executeCommand(final CommandSender sender, List arguments, CommandService commandService) { - AntiBot antiBot = commandService.getAntiBot(); if (arguments.isEmpty()) { sender.sendMessage("[AuthMe] AntiBot status: " + antiBot.getAntiBotStatus().name()); return; diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/UnregisterAdminCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/UnregisterAdminCommand.java index 5c8196dd..c69f9aff 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/UnregisterAdminCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/UnregisterAdminCommand.java @@ -6,6 +6,7 @@ import fr.xephi.authme.cache.auth.PlayerCache; import fr.xephi.authme.cache.limbo.LimboCache; import fr.xephi.authme.command.CommandService; import fr.xephi.authme.command.ExecutableCommand; +import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.settings.properties.RegistrationSettings; import fr.xephi.authme.settings.properties.RestrictionSettings; @@ -19,6 +20,7 @@ import org.bukkit.potion.PotionEffect; import org.bukkit.potion.PotionEffectType; import org.bukkit.scheduler.BukkitTask; +import javax.inject.Inject; import java.util.List; import static fr.xephi.authme.util.BukkitService.TICKS_PER_SECOND; @@ -28,6 +30,12 @@ import static fr.xephi.authme.util.BukkitService.TICKS_PER_SECOND; */ public class UnregisterAdminCommand implements ExecutableCommand { + @Inject + private DataSource dataSource; + + @Inject + private PlayerCache playerCache; + @Override public void executeCommand(final CommandSender sender, List arguments, CommandService commandService) { // Get the player name @@ -35,20 +43,20 @@ public class UnregisterAdminCommand implements ExecutableCommand { String playerNameLowerCase = playerName.toLowerCase(); // Make sure the user is valid - if (!commandService.getDataSource().isAuthAvailable(playerNameLowerCase)) { + if (!dataSource.isAuthAvailable(playerNameLowerCase)) { commandService.send(sender, MessageKey.UNKNOWN_USER); return; } // Remove the player - if (!commandService.getDataSource().removeAuth(playerNameLowerCase)) { + if (!dataSource.removeAuth(playerNameLowerCase)) { commandService.send(sender, MessageKey.ERROR); return; } // Unregister the player Player target = commandService.getPlayer(playerNameLowerCase); - PlayerCache.getInstance().removePlayer(playerNameLowerCase); + playerCache.removePlayer(playerNameLowerCase); Utils.setGroup(target, Utils.GroupType.UNREGISTERED); if (target != null && target.isOnline()) { if (commandService.getProperty(RegistrationSettings.FORCE)) { 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 7d378ca8..67df2d86 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 @@ -8,6 +8,7 @@ import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.task.ChangePasswordTask; import org.bukkit.entity.Player; +import javax.inject.Inject; import java.util.List; /** @@ -15,13 +16,15 @@ import java.util.List; */ public class ChangePasswordCommand extends PlayerCommand { + @Inject + private PlayerCache playerCache; + @Override public void runCommand(Player player, List arguments, CommandService commandService) { String oldPassword = arguments.get(0); String newPassword = arguments.get(1); String name = player.getName().toLowerCase(); - final PlayerCache playerCache = commandService.getPlayerCache(); if (!playerCache.isAuthenticated(name)) { commandService.send(player, MessageKey.NOT_LOGGED_IN); return; diff --git a/src/main/java/fr/xephi/authme/command/executable/email/RecoverEmailCommand.java b/src/main/java/fr/xephi/authme/command/executable/email/RecoverEmailCommand.java index f4a319a5..56fb5e5c 100644 --- a/src/main/java/fr/xephi/authme/command/executable/email/RecoverEmailCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/email/RecoverEmailCommand.java @@ -8,16 +8,27 @@ import fr.xephi.authme.command.CommandService; import fr.xephi.authme.command.PlayerCommand; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.output.MessageKey; +import fr.xephi.authme.security.PasswordSecurity; import fr.xephi.authme.security.RandomString; import fr.xephi.authme.security.crypts.HashedPassword; import fr.xephi.authme.settings.properties.EmailSettings; import fr.xephi.authme.util.StringUtils; import org.bukkit.entity.Player; +import javax.inject.Inject; import java.util.List; public class RecoverEmailCommand extends PlayerCommand { + @Inject + private PasswordSecurity passwordSecurity; + + @Inject + private DataSource dataSource; + + @Inject + private PlayerCache playerCache; + @Override public void runCommand(Player player, List arguments, CommandService commandService) { final String playerMail = arguments.get(0); @@ -29,7 +40,6 @@ public class RecoverEmailCommand extends PlayerCommand { commandService.send(player, MessageKey.ERROR); return; } - DataSource dataSource = commandService.getDataSource(); if (dataSource.isAuthAvailable(playerName)) { if (PlayerCache.getInstance().isAuthenticated(playerName)) { commandService.send(player, MessageKey.ALREADY_LOGGED_IN_ERROR); @@ -37,10 +47,10 @@ public class RecoverEmailCommand extends PlayerCommand { } String thePass = RandomString.generate(commandService.getProperty(EmailSettings.RECOVERY_PASSWORD_LENGTH)); - HashedPassword hashNew = commandService.getPasswordSecurity().computeHash(thePass, playerName); + HashedPassword hashNew = passwordSecurity.computeHash(thePass, playerName); PlayerAuth auth; - if (PlayerCache.getInstance().isAuthenticated(playerName)) { - auth = PlayerCache.getInstance().getAuth(playerName); + if (playerCache.isAuthenticated(playerName)) { + auth = playerCache.getAuth(playerName); } else if (dataSource.isAuthAvailable(playerName)) { auth = dataSource.getAuth(playerName); } else { diff --git a/src/test/java/fr/xephi/authme/command/CommandServiceTest.java b/src/test/java/fr/xephi/authme/command/CommandServiceTest.java index a1ff5e22..76685fce 100644 --- a/src/test/java/fr/xephi/authme/command/CommandServiceTest.java +++ b/src/test/java/fr/xephi/authme/command/CommandServiceTest.java @@ -1,9 +1,7 @@ package fr.xephi.authme.command; -import fr.xephi.authme.AntiBot; import fr.xephi.authme.AuthMe; import fr.xephi.authme.command.help.HelpProvider; -import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.hooks.PluginHooks; import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.output.Messages; @@ -62,8 +60,6 @@ public class CommandServiceTest { @Mock private SpawnLoader spawnLoader; @Mock - private AntiBot antiBot; - @Mock private ValidationService validationService; @Mock private BukkitService bukkitService; @@ -108,28 +104,6 @@ public class CommandServiceTest { verify(commandMapper).mapPartsToCommand(sender, commandParts); } - @Test - public void shouldGetDataSource() { - // given - DataSource dataSource = mock(DataSource.class); - given(authMe.getDataSource()).willReturn(dataSource); - - // when - DataSource result = commandService.getDataSource(); - - // then - assertThat(result, equalTo(dataSource)); - } - - @Test - public void shouldGetPasswordSecurity() { - // given/when - PasswordSecurity passwordSecurity = commandService.getPasswordSecurity(); - - // then - assertThat(passwordSecurity, equalTo(this.passwordSecurity)); - } - @Test public void shouldOutputHelp() { // given diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/ChangePasswordAdminCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/ChangePasswordAdminCommandTest.java index b4f42886..163bee15 100644 --- a/src/test/java/fr/xephi/authme/command/executable/authme/ChangePasswordAdminCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/authme/ChangePasswordAdminCommandTest.java @@ -4,7 +4,6 @@ import fr.xephi.authme.TestHelper; import fr.xephi.authme.cache.auth.PlayerAuth; import fr.xephi.authme.cache.auth.PlayerCache; import fr.xephi.authme.command.CommandService; -import fr.xephi.authme.command.ExecutableCommand; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.security.PasswordSecurity; @@ -13,6 +12,7 @@ import org.bukkit.command.CommandSender; import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; @@ -24,6 +24,7 @@ import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; /** * Test for {@link ChangePasswordAdminCommand}. @@ -31,9 +32,21 @@ import static org.mockito.Mockito.verify; @RunWith(MockitoJUnitRunner.class) public class ChangePasswordAdminCommandTest { + @InjectMocks + private ChangePasswordAdminCommand command; + @Mock private CommandService service; + @Mock + private PasswordSecurity passwordSecurity; + + @Mock + private DataSource dataSource; + + @Mock + private PlayerCache playerCache; + @BeforeClass public static void setUpLogger() { TestHelper.setupLogger(); @@ -42,7 +55,6 @@ public class ChangePasswordAdminCommandTest { @Test public void shouldRejectInvalidPassword() { // given - ExecutableCommand command = new ChangePasswordAdminCommand(); CommandSender sender = mock(CommandSender.class); given(service.validatePassword("Bobby", "bobby")).willReturn(MessageKey.PASSWORD_IS_USERNAME_ERROR); @@ -52,23 +64,16 @@ public class ChangePasswordAdminCommandTest { // then verify(service).validatePassword("Bobby", "bobby"); verify(service).send(sender, MessageKey.PASSWORD_IS_USERNAME_ERROR); - verify(service, never()).getDataSource(); + verifyZeroInteractions(dataSource); } @Test public void shouldRejectCommandForUnknownUser() { // given - ExecutableCommand command = new ChangePasswordAdminCommand(); CommandSender sender = mock(CommandSender.class); String player = "player"; - - PlayerCache playerCache = mock(PlayerCache.class); given(playerCache.isAuthenticated(player)).willReturn(false); - given(service.getPlayerCache()).willReturn(playerCache); - - DataSource dataSource = mock(DataSource.class); given(dataSource.getAuth(player)).willReturn(null); - given(service.getDataSource()).willReturn(dataSource); // when command.executeCommand(sender, Arrays.asList(player, "password"), service); @@ -82,26 +87,17 @@ public class ChangePasswordAdminCommandTest { @Test public void shouldUpdatePasswordOfLoggedInUser() { // given - ExecutableCommand command = new ChangePasswordAdminCommand(); CommandSender sender = mock(CommandSender.class); - String player = "my_user12"; String password = "passPass"; PlayerAuth auth = mock(PlayerAuth.class); - PlayerCache playerCache = mock(PlayerCache.class); given(playerCache.isAuthenticated(player)).willReturn(true); given(playerCache.getAuth(player)).willReturn(auth); - given(service.getPlayerCache()).willReturn(playerCache); - PasswordSecurity passwordSecurity = mock(PasswordSecurity.class); HashedPassword hashedPassword = mock(HashedPassword.class); given(passwordSecurity.computeHash(password, player)).willReturn(hashedPassword); - given(service.getPasswordSecurity()).willReturn(passwordSecurity); - - DataSource dataSource = mock(DataSource.class); given(dataSource.updatePassword(auth)).willReturn(true); - given(service.getDataSource()).willReturn(dataSource); // when command.executeCommand(sender, Arrays.asList(player, password), service); @@ -118,27 +114,17 @@ public class ChangePasswordAdminCommandTest { @Test public void shouldUpdatePasswordOfOfflineUser() { // given - ExecutableCommand command = new ChangePasswordAdminCommand(); CommandSender sender = mock(CommandSender.class); - String player = "my_user12"; String password = "passPass"; PlayerAuth auth = mock(PlayerAuth.class); - - PlayerCache playerCache = mock(PlayerCache.class); given(playerCache.isAuthenticated(player)).willReturn(false); - given(service.getPlayerCache()).willReturn(playerCache); - - DataSource dataSource = mock(DataSource.class); given(dataSource.isAuthAvailable(player)).willReturn(true); given(dataSource.getAuth(player)).willReturn(auth); given(dataSource.updatePassword(auth)).willReturn(true); - given(service.getDataSource()).willReturn(dataSource); - PasswordSecurity passwordSecurity = mock(PasswordSecurity.class); HashedPassword hashedPassword = mock(HashedPassword.class); given(passwordSecurity.computeHash(password, player)).willReturn(hashedPassword); - given(service.getPasswordSecurity()).willReturn(passwordSecurity); // when command.executeCommand(sender, Arrays.asList(player, password), service); @@ -155,26 +141,16 @@ public class ChangePasswordAdminCommandTest { @Test public void shouldReportWhenSaveFailed() { // given - ExecutableCommand command = new ChangePasswordAdminCommand(); CommandSender sender = mock(CommandSender.class); - String player = "my_user12"; String password = "passPass"; PlayerAuth auth = mock(PlayerAuth.class); - - PlayerCache playerCache = mock(PlayerCache.class); given(playerCache.isAuthenticated(player)).willReturn(true); given(playerCache.getAuth(player)).willReturn(auth); - given(service.getPlayerCache()).willReturn(playerCache); - PasswordSecurity passwordSecurity = mock(PasswordSecurity.class); HashedPassword hashedPassword = mock(HashedPassword.class); given(passwordSecurity.computeHash(password, player)).willReturn(hashedPassword); - given(service.getPasswordSecurity()).willReturn(passwordSecurity); - - DataSource dataSource = mock(DataSource.class); given(dataSource.updatePassword(auth)).willReturn(false); - given(service.getDataSource()).willReturn(dataSource); // when command.executeCommand(sender, Arrays.asList(player, password), service); diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/GetEmailCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/GetEmailCommandTest.java index 6d232037..f03ff426 100644 --- a/src/test/java/fr/xephi/authme/command/executable/authme/GetEmailCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/authme/GetEmailCommandTest.java @@ -2,11 +2,14 @@ package fr.xephi.authme.command.executable.authme; import fr.xephi.authme.cache.auth.PlayerAuth; import fr.xephi.authme.command.CommandService; -import fr.xephi.authme.command.ExecutableCommand; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.output.MessageKey; import org.bukkit.command.CommandSender; import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; import java.util.Collections; @@ -19,19 +22,26 @@ import static org.mockito.Mockito.verify; /** * Test for {@link GetEmailCommand}. */ +@RunWith(MockitoJUnitRunner.class) public class GetEmailCommandTest { + @InjectMocks + private GetEmailCommand command; + + @Mock + private DataSource dataSource; + + @Mock + private CommandSender sender; + + @Mock + private CommandService service; + @Test public void shouldReportUnknownUser() { // given String user = "myTestUser"; - DataSource dataSource = mock(DataSource.class); given(dataSource.getAuth(user)).willReturn(null); - CommandService service = mock(CommandService.class); - given(service.getDataSource()).willReturn(dataSource); - - CommandSender sender = mock(CommandSender.class); - ExecutableCommand command = new GetEmailCommand(); // when command.executeCommand(sender, Collections.singletonList(user), service); @@ -47,14 +57,7 @@ public class GetEmailCommandTest { String email = "user.email@example.org"; PlayerAuth auth = mock(PlayerAuth.class); given(auth.getEmail()).willReturn(email); - - DataSource dataSource = mock(DataSource.class); given(dataSource.getAuth(user)).willReturn(auth); - CommandService service = mock(CommandService.class); - given(service.getDataSource()).willReturn(dataSource); - - CommandSender sender = mock(CommandSender.class); - ExecutableCommand command = new GetEmailCommand(); // when command.executeCommand(sender, Collections.singletonList(user), service); diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/LastLoginCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/LastLoginCommandTest.java index d1e58e92..04ffabef 100644 --- a/src/test/java/fr/xephi/authme/command/executable/authme/LastLoginCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/authme/LastLoginCommandTest.java @@ -2,12 +2,15 @@ package fr.xephi.authme.command.executable.authme; import fr.xephi.authme.cache.auth.PlayerAuth; import fr.xephi.authme.command.CommandService; -import fr.xephi.authme.command.ExecutableCommand; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.output.MessageKey; import org.bukkit.command.CommandSender; import org.junit.Test; +import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; import java.util.Collections; import java.util.Date; @@ -23,8 +26,21 @@ import static org.mockito.Mockito.verify; /** * Test for {@link LastLoginCommand}. */ +@RunWith(MockitoJUnitRunner.class) public class LastLoginCommandTest { + @InjectMocks + private LastLoginCommand command; + + @Mock + private DataSource dataSource; + + @Mock + private CommandService service; + + @Mock + private CommandSender sender; + private static final long HOUR_IN_MSEC = 3600 * 1000; private static final long DAY_IN_MSEC = 24 * HOUR_IN_MSEC; @@ -32,15 +48,8 @@ public class LastLoginCommandTest { public void shouldRejectNonExistentUser() { // given String player = "tester"; - DataSource dataSource = mock(DataSource.class); given(dataSource.getAuth(player)).willReturn(null); - CommandService service = mock(CommandService.class); - given(service.getDataSource()).willReturn(dataSource); - - CommandSender sender = mock(CommandSender.class); - ExecutableCommand command = new LastLoginCommand(); - // when command.executeCommand(sender, Collections.singletonList(player), service); @@ -58,14 +67,7 @@ public class LastLoginCommandTest { PlayerAuth auth = mock(PlayerAuth.class); given(auth.getLastLogin()).willReturn(lastLogin); given(auth.getIp()).willReturn("123.45.66.77"); - - DataSource dataSource = mock(DataSource.class); given(dataSource.getAuth(player)).willReturn(auth); - CommandService service = mock(CommandService.class); - given(service.getDataSource()).willReturn(dataSource); - - CommandSender sender = mock(CommandSender.class); - ExecutableCommand command = new LastLoginCommand(); // when command.executeCommand(sender, Collections.singletonList(player), service); @@ -85,7 +87,6 @@ public class LastLoginCommandTest { public void shouldDisplayLastLoginOfCommandSender() { // given String name = "CommandSender"; - CommandSender sender = mock(CommandSender.class); given(sender.getName()).willReturn(name); long lastLogin = System.currentTimeMillis() - @@ -93,14 +94,7 @@ public class LastLoginCommandTest { PlayerAuth auth = mock(PlayerAuth.class); given(auth.getLastLogin()).willReturn(lastLogin); given(auth.getIp()).willReturn("123.45.66.77"); - - DataSource dataSource = mock(DataSource.class); given(dataSource.getAuth(name)).willReturn(auth); - CommandService service = mock(CommandService.class); - given(service.getDataSource()).willReturn(dataSource); - - - ExecutableCommand command = new LastLoginCommand(); // when command.executeCommand(sender, Collections.emptyList(), service); diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/PurgeLastPositionCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/PurgeLastPositionCommandTest.java index bcce0342..2c80a19d 100644 --- a/src/test/java/fr/xephi/authme/command/executable/authme/PurgeLastPositionCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/authme/PurgeLastPositionCommandTest.java @@ -2,11 +2,14 @@ package fr.xephi.authme.command.executable.authme; import fr.xephi.authme.cache.auth.PlayerAuth; import fr.xephi.authme.command.CommandService; -import fr.xephi.authme.command.ExecutableCommand; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.output.MessageKey; import org.bukkit.command.CommandSender; import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; import java.util.Arrays; import java.util.Collections; @@ -20,21 +23,27 @@ import static org.mockito.Mockito.verify; /** * Test for {@link PurgeLastPositionCommand}. */ +@RunWith(MockitoJUnitRunner.class) public class PurgeLastPositionCommandTest { + @InjectMocks + private PurgeLastPositionCommand command; + + @Mock + private DataSource dataSource; + + @Mock + private CommandService service; + + @Mock + private CommandSender sender; + @Test public void shouldPurgeLastPosOfUser() { // given String player = "_Bobby"; PlayerAuth auth = mock(PlayerAuth.class); - - DataSource dataSource = mock(DataSource.class); given(dataSource.getAuth(player)).willReturn(auth); - CommandService service = mock(CommandService.class); - given(service.getDataSource()).willReturn(dataSource); - - CommandSender sender = mock(CommandSender.class); - ExecutableCommand command = new PurgeLastPositionCommand(); // when command.executeCommand(sender, Collections.singletonList(player), service); @@ -51,14 +60,8 @@ public class PurgeLastPositionCommandTest { String player = "_Bobby"; CommandSender sender = mock(CommandSender.class); given(sender.getName()).willReturn(player); - PlayerAuth auth = mock(PlayerAuth.class); - DataSource dataSource = mock(DataSource.class); given(dataSource.getAuth(player)).willReturn(auth); - CommandService service = mock(CommandService.class); - given(service.getDataSource()).willReturn(dataSource); - - ExecutableCommand command = new PurgeLastPositionCommand(); // when command.executeCommand(sender, Collections.emptyList(), service); @@ -72,12 +75,6 @@ public class PurgeLastPositionCommandTest { @Test public void shouldHandleNonExistentUser() { // given - DataSource dataSource = mock(DataSource.class); - CommandService service = mock(CommandService.class); - given(service.getDataSource()).willReturn(dataSource); - - ExecutableCommand command = new PurgeLastPositionCommand(); - CommandSender sender = mock(CommandSender.class); String name = "invalidPlayer"; // when @@ -94,14 +91,7 @@ public class PurgeLastPositionCommandTest { PlayerAuth auth1 = mock(PlayerAuth.class); PlayerAuth auth2 = mock(PlayerAuth.class); PlayerAuth auth3 = mock(PlayerAuth.class); - - DataSource dataSource = mock(DataSource.class); given(dataSource.getAllAuths()).willReturn(Arrays.asList(auth1, auth2, auth3)); - CommandService service = mock(CommandService.class); - given(service.getDataSource()).willReturn(dataSource); - - ExecutableCommand command = new PurgeLastPositionCommand(); - CommandSender sender = mock(CommandSender.class); // when command.executeCommand(sender, Collections.singletonList("*"), service); diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/RegisterAdminCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/RegisterAdminCommandTest.java index 66b7d6c4..3ce93fcf 100644 --- a/src/test/java/fr/xephi/authme/command/executable/authme/RegisterAdminCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/authme/RegisterAdminCommandTest.java @@ -3,7 +3,6 @@ package fr.xephi.authme.command.executable.authme; import fr.xephi.authme.TestHelper; import fr.xephi.authme.cache.auth.PlayerAuth; import fr.xephi.authme.command.CommandService; -import fr.xephi.authme.command.ExecutableCommand; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.security.PasswordSecurity; @@ -14,6 +13,7 @@ import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; @@ -35,11 +35,21 @@ import static org.mockito.Mockito.verify; @RunWith(MockitoJUnitRunner.class) public class RegisterAdminCommandTest { + @InjectMocks + private RegisterAdminCommand command; + + @Mock + private PasswordSecurity passwordSecurity; + @Mock private CommandSender sender; + @Mock private CommandService commandService; + @Mock + private DataSource dataSource; + @BeforeClass public static void setUpLogger() { TestHelper.setupLogger(); @@ -51,7 +61,6 @@ public class RegisterAdminCommandTest { String user = "tester"; String password = "myPassword"; given(commandService.validatePassword(password, user)).willReturn(MessageKey.INVALID_PASSWORD_LENGTH); - ExecutableCommand command = new RegisterAdminCommand(); // when command.executeCommand(sender, Arrays.asList(user, password), commandService); @@ -68,10 +77,7 @@ public class RegisterAdminCommandTest { String user = "my_name55"; String password = "@some-pass@"; given(commandService.validatePassword(password, user)).willReturn(null); - DataSource dataSource = mock(DataSource.class); given(dataSource.isAuthAvailable(user)).willReturn(true); - given(commandService.getDataSource()).willReturn(dataSource); - ExecutableCommand command = new RegisterAdminCommand(); // when command.executeCommand(sender, Arrays.asList(user, password), commandService); @@ -89,15 +95,10 @@ public class RegisterAdminCommandTest { String user = "test-test"; String password = "afdjhfkt"; given(commandService.validatePassword(password, user)).willReturn(null); - DataSource dataSource = mock(DataSource.class); given(dataSource.isAuthAvailable(user)).willReturn(false); given(dataSource.saveAuth(any(PlayerAuth.class))).willReturn(false); - given(commandService.getDataSource()).willReturn(dataSource); - PasswordSecurity passwordSecurity = mock(PasswordSecurity.class); HashedPassword hashedPassword = new HashedPassword("235sdf4w5udsgf"); given(passwordSecurity.computeHash(password, user)).willReturn(hashedPassword); - given(commandService.getPasswordSecurity()).willReturn(passwordSecurity); - ExecutableCommand command = new RegisterAdminCommand(); // when command.executeCommand(sender, Arrays.asList(user, password), commandService); @@ -117,16 +118,11 @@ public class RegisterAdminCommandTest { String user = "someone"; String password = "Al1O3P49S5%"; given(commandService.validatePassword(password, user)).willReturn(null); - DataSource dataSource = mock(DataSource.class); given(dataSource.isAuthAvailable(user)).willReturn(false); given(dataSource.saveAuth(any(PlayerAuth.class))).willReturn(true); - given(commandService.getDataSource()).willReturn(dataSource); - PasswordSecurity passwordSecurity = mock(PasswordSecurity.class); HashedPassword hashedPassword = new HashedPassword("$aea2345EW235dfsa@#R%987048"); given(passwordSecurity.computeHash(password, user)).willReturn(hashedPassword); - given(commandService.getPasswordSecurity()).willReturn(passwordSecurity); given(commandService.getPlayer(user)).willReturn(null); - ExecutableCommand command = new RegisterAdminCommand(); // when command.executeCommand(sender, Arrays.asList(user, password), commandService); @@ -147,17 +143,12 @@ public class RegisterAdminCommandTest { String user = "someone"; String password = "Al1O3P49S5%"; given(commandService.validatePassword(password, user)).willReturn(null); - DataSource dataSource = mock(DataSource.class); given(dataSource.isAuthAvailable(user)).willReturn(false); given(dataSource.saveAuth(any(PlayerAuth.class))).willReturn(true); - given(commandService.getDataSource()).willReturn(dataSource); - PasswordSecurity passwordSecurity = mock(PasswordSecurity.class); HashedPassword hashedPassword = new HashedPassword("$aea2345EW235dfsa@#R%987048"); given(passwordSecurity.computeHash(password, user)).willReturn(hashedPassword); - given(commandService.getPasswordSecurity()).willReturn(passwordSecurity); Player player = mock(Player.class); given(commandService.getPlayer(user)).willReturn(player); - ExecutableCommand command = new RegisterAdminCommand(); // when command.executeCommand(sender, Arrays.asList(user, password), commandService); diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/SwitchAntiBotCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/SwitchAntiBotCommandTest.java index 3f9767f9..f8945a1c 100644 --- a/src/test/java/fr/xephi/authme/command/executable/authme/SwitchAntiBotCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/authme/SwitchAntiBotCommandTest.java @@ -2,11 +2,14 @@ package fr.xephi.authme.command.executable.authme; import fr.xephi.authme.AntiBot; import fr.xephi.authme.command.CommandService; -import fr.xephi.authme.command.ExecutableCommand; import fr.xephi.authme.command.FoundCommandResult; import fr.xephi.authme.command.help.HelpProvider; import org.bukkit.command.CommandSender; import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; import java.util.Collections; @@ -22,17 +25,23 @@ import static org.mockito.Mockito.verify; /** * Test for {@link SwitchAntiBotCommand}. */ +@RunWith(MockitoJUnitRunner.class) public class SwitchAntiBotCommandTest { + @InjectMocks + private SwitchAntiBotCommand command; + + @Mock + private AntiBot antiBot; + + @Mock + private CommandService service; + @Test public void shouldReturnAntiBotState() { // given - AntiBot antiBot = mock(AntiBot.class); given(antiBot.getAntiBotStatus()).willReturn(AntiBot.AntiBotStatus.ACTIVE); - CommandService service = mock(CommandService.class); - given(service.getAntiBot()).willReturn(antiBot); CommandSender sender = mock(CommandSender.class); - ExecutableCommand command = new SwitchAntiBotCommand(); // when command.executeCommand(sender, Collections.emptyList(), service); @@ -44,11 +53,7 @@ public class SwitchAntiBotCommandTest { @Test public void shouldActivateAntiBot() { // given - AntiBot antiBot = mock(AntiBot.class); - CommandService service = mock(CommandService.class); - given(service.getAntiBot()).willReturn(antiBot); CommandSender sender = mock(CommandSender.class); - ExecutableCommand command = new SwitchAntiBotCommand(); // when command.executeCommand(sender, Collections.singletonList("on"), service); @@ -61,11 +66,7 @@ public class SwitchAntiBotCommandTest { @Test public void shouldDeactivateAntiBot() { // given - AntiBot antiBot = mock(AntiBot.class); - CommandService service = mock(CommandService.class); - given(service.getAntiBot()).willReturn(antiBot); CommandSender sender = mock(CommandSender.class); - ExecutableCommand command = new SwitchAntiBotCommand(); // when command.executeCommand(sender, Collections.singletonList("Off"), service); @@ -79,15 +80,9 @@ public class SwitchAntiBotCommandTest { public void shouldShowHelpForUnknownState() { // given CommandSender sender = mock(CommandSender.class); - - AntiBot antiBot = mock(AntiBot.class); FoundCommandResult foundCommandResult = mock(FoundCommandResult.class); - CommandService service = mock(CommandService.class); - given(service.getAntiBot()).willReturn(antiBot); given(service.mapPartsToCommand(sender, asList("authme", "antibot"))).willReturn(foundCommandResult); - ExecutableCommand command = new SwitchAntiBotCommand(); - // when command.executeCommand(sender, Collections.singletonList("wrong"), service); 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 bc8e0876..1dc4dbb1 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 @@ -12,7 +12,11 @@ import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; import java.util.ArrayList; import java.util.Arrays; @@ -33,26 +37,31 @@ import static org.mockito.Mockito.when; /** * Test for {@link ChangePasswordCommand}. */ +@RunWith(MockitoJUnitRunner.class) public class ChangePasswordCommandTest { + @InjectMocks + private ChangePasswordCommand command; + + @Mock + private PlayerCache playerCache; + + @Mock private CommandService commandService; @Before - public void setUpMocks() { - commandService = mock(CommandService.class); - + public void setSettings() { 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 when(commandService.getProperty(RestrictionSettings.ALLOWED_PASSWORD_REGEX)).thenReturn("[a-zA-Z0-9]+"); - when(commandService.getProperty(SecuritySettings.UNSAFE_PASSWORDS)).thenReturn(Collections. emptyList()); + when(commandService.getProperty(SecuritySettings.UNSAFE_PASSWORDS)).thenReturn(Collections.emptyList()); } @Test public void shouldRejectNonPlayerSender() { // given CommandSender sender = mock(BlockCommandSender.class); - ChangePasswordCommand command = new ChangePasswordCommand(); // when command.executeCommand(sender, new ArrayList(), commandService); @@ -65,7 +74,6 @@ public class ChangePasswordCommandTest { public void shouldRejectNotLoggedInPlayer() { // given CommandSender sender = initPlayerWithName("name", false); - ChangePasswordCommand command = new ChangePasswordCommand(); // when command.executeCommand(sender, Arrays.asList("pass", "pass"), commandService); @@ -78,7 +86,6 @@ public class ChangePasswordCommandTest { public void shouldRejectInvalidPassword() { // given CommandSender sender = initPlayerWithName("abc12", true); - ChangePasswordCommand command = new ChangePasswordCommand(); String password = "newPW"; given(commandService.validatePassword(password, "abc12")).willReturn(MessageKey.INVALID_PASSWORD_LENGTH); @@ -94,7 +101,6 @@ public class ChangePasswordCommandTest { public void shouldForwardTheDataForValidPassword() { // given CommandSender sender = initPlayerWithName("parker", true); - ChangePasswordCommand command = new ChangePasswordCommand(); // when command.executeCommand(sender, Arrays.asList("abc123", "abc123"), commandService); @@ -112,9 +118,7 @@ public class ChangePasswordCommandTest { private Player initPlayerWithName(String name, boolean loggedIn) { Player player = mock(Player.class); when(player.getName()).thenReturn(name); - PlayerCache playerCache = mock(PlayerCache.class); when(playerCache.isAuthenticated(name)).thenReturn(loggedIn); - when(commandService.getPlayerCache()).thenReturn(playerCache); return player; } diff --git a/src/tools/hashmethods/EncryptionMethodInfoGatherer.java b/src/tools/hashmethods/EncryptionMethodInfoGatherer.java index 4ade88ce..d56e370d 100644 --- a/src/tools/hashmethods/EncryptionMethodInfoGatherer.java +++ b/src/tools/hashmethods/EncryptionMethodInfoGatherer.java @@ -1,7 +1,6 @@ package hashmethods; import fr.xephi.authme.security.HashAlgorithm; -import fr.xephi.authme.security.PasswordSecurity; import fr.xephi.authme.security.crypts.EncryptionMethod; import fr.xephi.authme.security.crypts.HexSaltedMethod; import fr.xephi.authme.security.crypts.description.AsciiRestricted; @@ -55,7 +54,7 @@ public class EncryptionMethodInfoGatherer { private static MethodDescription createDescription(HashAlgorithm algorithm) { Class clazz = algorithm.getClazz(); - EncryptionMethod method = PasswordSecurity.initializeEncryptionMethod(algorithm, settings); + EncryptionMethod method = null; // TODO ljacqu PasswordSecurity.initializeEncryptionMethod(algorithm, settings); if (method == null) { throw new NullPointerException("Method for '" + algorithm + "' is null"); } From 491dc06de4184234f17d0d072fbc5c92209e77ff Mon Sep 17 00:00:00 2001 From: ljacqu Date: Mon, 2 May 2016 19:57:54 +0200 Subject: [PATCH 008/200] Fix RegisterAdminCommand test verifying that online player is kicked --- .../authme/RegisterAdminCommand.java | 7 +++---- .../authme/RegisterAdminCommandTest.java | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 4 deletions(-) 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 d57581ff..5ddd0b95 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 @@ -63,13 +63,12 @@ public class RegisterAdminCommand implements ExecutableCommand { commandService.send(sender, MessageKey.REGISTER_SUCCESS); ConsoleLogger.info(sender.getName() + " registered " + playerName); - Player player = commandService.getPlayer(playerName); + final Player player = commandService.getPlayer(playerName); if (player != null) { - final Player p = player; - p.getServer().getScheduler().scheduleSyncDelayedTask(commandService.getAuthMe(), new Runnable() { + commandService.getBukkitService().scheduleSyncDelayedTask(new Runnable() { @Override public void run() { - p.kickPlayer("An admin just registered you, please log in again"); + player.kickPlayer("An admin just registered you, please log in again"); } }); } diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/RegisterAdminCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/RegisterAdminCommandTest.java index 89fc3da6..47f4de1b 100644 --- a/src/test/java/fr/xephi/authme/command/executable/authme/RegisterAdminCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/authme/RegisterAdminCommandTest.java @@ -7,7 +7,9 @@ import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.security.PasswordSecurity; import fr.xephi.authme.security.crypts.HashedPassword; +import fr.xephi.authme.util.BukkitService; import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; @@ -18,10 +20,13 @@ import org.mockito.runners.MockitoJUnitRunner; import java.util.Arrays; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.junit.Assert.assertThat; import static org.mockito.BDDMockito.given; import static org.mockito.Matchers.any; +import static org.mockito.Matchers.argThat; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -143,10 +148,15 @@ public class RegisterAdminCommandTest { given(dataSource.saveAuth(any(PlayerAuth.class))).willReturn(true); HashedPassword hashedPassword = new HashedPassword("$aea2345EW235dfsa@#R%987048"); given(passwordSecurity.computeHash(password, user)).willReturn(hashedPassword); + Player player = mock(Player.class); + given(commandService.getPlayer(user)).willReturn(player); + BukkitService bukkitService = mock(BukkitService.class); + given(commandService.getBukkitService()).willReturn(bukkitService); // when command.executeCommand(sender, Arrays.asList(user, password), commandService); TestHelper.runInnerRunnable(commandService); + runSyncDelayedTask(bukkitService); // then verify(commandService).validatePassword(password, user); @@ -155,6 +165,7 @@ public class RegisterAdminCommandTest { verify(dataSource).saveAuth(captor.capture()); assertAuthHasInfo(captor.getValue(), user, hashedPassword); verify(dataSource).setUnlogged(user); + verify(player).kickPlayer(argThat(containsString("please log in again"))); } private void assertAuthHasInfo(PlayerAuth auth, String name, HashedPassword hashedPassword) { @@ -162,4 +173,11 @@ public class RegisterAdminCommandTest { assertThat(auth.getNickname(), equalTo(name.toLowerCase())); assertThat(auth.getPassword(), equalTo(hashedPassword)); } + + private static void runSyncDelayedTask(BukkitService bukkitService) { + ArgumentCaptor captor = ArgumentCaptor.forClass(Runnable.class); + verify(bukkitService).scheduleSyncDelayedTask(captor.capture()); + Runnable runnable = captor.getValue(); + runnable.run(); + } } From 2a423e6f2f78ec5a8b3db65c842530e6a05a6ae1 Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Tue, 3 May 2016 12:45:58 +0200 Subject: [PATCH 009/200] Continue SNAPSHOT development --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index ffb244e9..5b130946 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ fr.xephi authme - 5.2-BETA2 + 5.2-SNAPSHOT jar AuthMeReloaded From 0ea95fb93c5b8408d6cb34c8de381dbce06060d5 Mon Sep 17 00:00:00 2001 From: NoChanceSD Date: Tue, 3 May 2016 16:36:05 +0100 Subject: [PATCH 010/200] Simplify force spawn location settings by grouping them Also, seems to work fine. Closes https://github.com/Xephi/AuthMeReloaded/issues/256 --- .../fr/xephi/authme/settings/Settings.java | 2 +- .../properties/RestrictionSettings.java | 6 ++--- src/main/resources/config.yml | 25 ++++++++++--------- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/main/java/fr/xephi/authme/settings/Settings.java b/src/main/java/fr/xephi/authme/settings/Settings.java index 944f675d..0b6d719f 100644 --- a/src/main/java/fr/xephi/authme/settings/Settings.java +++ b/src/main/java/fr/xephi/authme/settings/Settings.java @@ -102,7 +102,7 @@ public final class Settings { captchaLength = configFile.getInt("Security.captcha.captchaLength", 5); multiverse = load(HooksSettings.MULTIVERSE); bungee = load(HooksSettings.BUNGEECORD); - getForcedWorlds = configFile.getStringList("settings.restrictions.ForceSpawnOnTheseWorlds"); + getForcedWorlds = load(RestrictionSettings.FORCE_SPAWN_ON_WORLDS); defaultWorld = configFile.getString("Purge.defaultWorld", "world"); enableProtection = configFile.getBoolean("Protection.enableProtection", false); countries = configFile.getStringList("Protection.countries"); diff --git a/src/main/java/fr/xephi/authme/settings/properties/RestrictionSettings.java b/src/main/java/fr/xephi/authme/settings/properties/RestrictionSettings.java index 134f9e17..592e244c 100644 --- a/src/main/java/fr/xephi/authme/settings/properties/RestrictionSettings.java +++ b/src/main/java/fr/xephi/authme/settings/properties/RestrictionSettings.java @@ -61,7 +61,7 @@ public class RestrictionSettings implements SettingsClass { "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); + newProperty("settings.restrictions.ForceSpawnLocOnJoin.enabled", false); @Comment("This option will save the quit location of the players.") public static final Property SAVE_QUIT_LOCATION = @@ -150,10 +150,10 @@ public class RestrictionSettings implements SettingsClass { newProperty("settings.restrictions.displayOtherAccounts", true); @Comment({ - "WorldNames where we need to force the spawn location for ForceSpawnLocOnJoinEnabled", + "WorldNames where we need to force the spawn location", "Case-sensitive!"}) public static final Property> FORCE_SPAWN_ON_WORLDS = - newListProperty("settings.restrictions.ForceSpawnOnTheseWorlds", + newListProperty("settings.restrictions.ForceSpawnLocOnJoin.worlds", "world", "world_nether", "world_the_end"); @Comment("Ban ip when the ip is not the ip registered in database") diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 17b62b37..b7640108 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -88,12 +88,19 @@ settings: # due to "Logged in from another Location" # This setting will prevent potetial security exploits. ForceSingleSession: true - # 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. - ForceSpawnLocOnJoinEnabled: false + ForceSpawnLocOnJoin: + # 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. + enabled: false + # WorldNames where we need to force the spawn location + # Case-sensitive! + worlds: + - 'world' + - 'world_nether' + - 'world_the_end' # This option will save the quit location of the players. SaveQuitLocation: false # To activate the restricted user feature you need @@ -143,12 +150,6 @@ settings: # Should we display all other accounts from a player when he joins? # permission: /authme.admin.accounts displayOtherAccounts: true - # WorldNames where we need to force the spawn location for ForceSpawnLocOnJoinEnabled - # CASE SENSITIVE - ForceSpawnOnTheseWorlds: - - world - - world_nether - - world_the_end # Ban ip when the ip is not the ip registered in database banUnsafedIP: false # Spawn Priority, Values : authme, essentials, multiverse, default From 3645806edc41915a0411b9c9a62fa14f39383d7c Mon Sep 17 00:00:00 2001 From: ljacqu Date: Tue, 3 May 2016 20:24:34 +0200 Subject: [PATCH 011/200] Move tools folder into test folder - Classes still won't be present in JAR but classes will be automatically compiled by Maven inside of the test scope, facilitating the execution of tool tasks --- src/test/java/tools/README.md | 6 ++++++ src/{ => test/java}/tools/ToolsRunner.java | 8 +++++--- src/{ => test/java}/tools/bathelpers/README.md | 0 .../java}/tools/bathelpers/analyze_project.bat | 0 .../java}/tools/bathelpers/build_project.bat | 0 src/{ => test/java}/tools/bathelpers/list_files.bat | 0 src/{ => test/java}/tools/bathelpers/move_plugin.bat | 0 src/{ => test/java}/tools/bathelpers/quick_build.bat | 0 src/{ => test/java}/tools/bathelpers/run_server.bat | 0 src/{ => test/java}/tools/bathelpers/setvars.bat | 0 .../java}/tools/commands/CommandPageCreater.java | 12 ++++++------ src/{ => test/java}/tools/commands/commands.tpl.md | 0 src/{ => test/java}/tools/docs/UpdateDocsTask.java | 10 +++++----- .../hashmethods/EncryptionMethodInfoGatherer.java | 2 +- .../hashmethods/HashAlgorithmsDescriptionTask.java | 12 ++++++------ .../java}/tools/hashmethods/MethodDescription.java | 2 +- .../java}/tools/hashmethods/hash_algorithms.tpl.md | 0 .../java}/tools/messages/MessageFileVerifier.java | 4 ++-- src/{ => test/java}/tools/messages/README.md | 0 .../java}/tools/messages/VerifyMessagesTask.java | 6 +++--- .../translation/AuthMeYamlConfiguration.java | 2 +- .../messages/translation/ExportMessagesTask.java | 6 +++--- .../messages/translation/ImportMessagesTask.java | 12 ++++++------ .../tools/messages/translation/LanguageExport.java | 2 +- .../tools/messages/translation/MessageExport.java | 2 +- .../messages/translation/WriteAllExportsTask.java | 6 +++--- .../translation/export/export-is-written-here | 0 .../tools/permissions/PermissionNodesGatherer.java | 6 +++--- .../tools/permissions/PermissionsListWriter.java | 12 ++++++------ src/{ => test/java}/tools/permissions/README.md | 0 .../java}/tools/permissions/permission_nodes.tpl.md | 0 .../java}/tools/shhelpers/analyze_project.sh | 0 src/{ => test/java}/tools/shhelpers/build_project.sh | 0 src/{ => test/java}/tools/shhelpers/list_files.sh | 0 src/{ => test/java}/tools/shhelpers/move_plugin.sh | 0 src/{ => test/java}/tools/shhelpers/quick_build.sh | 0 src/{ => test/java}/tools/shhelpers/run_server.sh | 0 src/{ => test/java}/tools/shhelpers/setvars.sh | 0 .../java}/tools/shhelpers/sort_file_content.sh | 0 src/{ => test/java}/tools/utils/FileUtils.java | 2 +- src/{ => test/java}/tools/utils/TagReplacer.java | 6 +++--- src/{ => test/java}/tools/utils/TagValue.java | 2 +- src/{ => test/java}/tools/utils/TagValueHolder.java | 4 ++-- src/{ => test/java}/tools/utils/ToolTask.java | 2 +- src/{ => test/java}/tools/utils/ToolsConstants.java | 4 ++-- src/tools/README.md | 3 --- 46 files changed, 69 insertions(+), 64 deletions(-) create mode 100644 src/test/java/tools/README.md rename src/{ => test/java}/tools/ToolsRunner.java (97%) rename src/{ => test/java}/tools/bathelpers/README.md (100%) rename src/{ => test/java}/tools/bathelpers/analyze_project.bat (100%) rename src/{ => test/java}/tools/bathelpers/build_project.bat (100%) rename src/{ => test/java}/tools/bathelpers/list_files.bat (100%) rename src/{ => test/java}/tools/bathelpers/move_plugin.bat (100%) rename src/{ => test/java}/tools/bathelpers/quick_build.bat (100%) rename src/{ => test/java}/tools/bathelpers/run_server.bat (100%) rename src/{ => test/java}/tools/bathelpers/setvars.bat (100%) rename src/{ => test/java}/tools/commands/CommandPageCreater.java (93%) rename src/{ => test/java}/tools/commands/commands.tpl.md (100%) rename src/{ => test/java}/tools/docs/UpdateDocsTask.java (86%) rename src/{ => test/java}/tools/hashmethods/EncryptionMethodInfoGatherer.java (99%) rename src/{ => test/java}/tools/hashmethods/HashAlgorithmsDescriptionTask.java (93%) rename src/{ => test/java}/tools/hashmethods/MethodDescription.java (98%) rename src/{ => test/java}/tools/hashmethods/hash_algorithms.tpl.md (100%) rename src/{ => test/java}/tools/messages/MessageFileVerifier.java (99%) rename src/{ => test/java}/tools/messages/README.md (100%) rename src/{ => test/java}/tools/messages/VerifyMessagesTask.java (98%) rename src/{ => test/java}/tools/messages/translation/AuthMeYamlConfiguration.java (98%) rename src/{ => test/java}/tools/messages/translation/ExportMessagesTask.java (97%) rename src/{ => test/java}/tools/messages/translation/ImportMessagesTask.java (95%) rename src/{ => test/java}/tools/messages/translation/LanguageExport.java (91%) rename src/{ => test/java}/tools/messages/translation/MessageExport.java (93%) rename src/{ => test/java}/tools/messages/translation/WriteAllExportsTask.java (92%) rename src/{ => test/java}/tools/messages/translation/export/export-is-written-here (100%) rename src/{ => test/java}/tools/permissions/PermissionNodesGatherer.java (97%) rename src/{ => test/java}/tools/permissions/PermissionsListWriter.java (93%) rename src/{ => test/java}/tools/permissions/README.md (100%) rename src/{ => test/java}/tools/permissions/permission_nodes.tpl.md (100%) rename src/{ => test/java}/tools/shhelpers/analyze_project.sh (100%) rename src/{ => test/java}/tools/shhelpers/build_project.sh (100%) rename src/{ => test/java}/tools/shhelpers/list_files.sh (100%) rename src/{ => test/java}/tools/shhelpers/move_plugin.sh (100%) rename src/{ => test/java}/tools/shhelpers/quick_build.sh (100%) rename src/{ => test/java}/tools/shhelpers/run_server.sh (100%) rename src/{ => test/java}/tools/shhelpers/setvars.sh (100%) rename src/{ => test/java}/tools/shhelpers/sort_file_content.sh (100%) rename src/{ => test/java}/tools/utils/FileUtils.java (98%) rename src/{ => test/java}/tools/utils/TagReplacer.java (97%) rename src/{ => test/java}/tools/utils/TagValue.java (97%) rename src/{ => test/java}/tools/utils/TagValueHolder.java (90%) rename src/{ => test/java}/tools/utils/ToolTask.java (96%) rename src/{ => test/java}/tools/utils/ToolsConstants.java (82%) delete mode 100644 src/tools/README.md diff --git a/src/test/java/tools/README.md b/src/test/java/tools/README.md new file mode 100644 index 00000000..012405cf --- /dev/null +++ b/src/test/java/tools/README.md @@ -0,0 +1,6 @@ +# About the _tools_ Folder + +This _tools_ folder provides helpers and extended tests useful during the development of AuthMe. +This folder is not included during the build of AuthMe and does not contain unit tests. + +Run the `ToolsRunner` class to perform a task. diff --git a/src/tools/ToolsRunner.java b/src/test/java/tools/ToolsRunner.java similarity index 97% rename from src/tools/ToolsRunner.java rename to src/test/java/tools/ToolsRunner.java index d920b23f..54e66b19 100644 --- a/src/tools/ToolsRunner.java +++ b/src/test/java/tools/ToolsRunner.java @@ -1,5 +1,7 @@ -import utils.ToolTask; -import utils.ToolsConstants; +package tools; + +import tools.utils.ToolTask; +import tools.utils.ToolsConstants; import java.io.File; import java.lang.reflect.Constructor; @@ -108,7 +110,7 @@ public final class ToolsRunner { } String filePath = file.getPath(); - String className = filePath + String className = "tools." + filePath .substring(ToolsConstants.TOOLS_SOURCE_ROOT.length(), filePath.length() - 5) .replace(File.separator, "."); try { diff --git a/src/tools/bathelpers/README.md b/src/test/java/tools/bathelpers/README.md similarity index 100% rename from src/tools/bathelpers/README.md rename to src/test/java/tools/bathelpers/README.md diff --git a/src/tools/bathelpers/analyze_project.bat b/src/test/java/tools/bathelpers/analyze_project.bat similarity index 100% rename from src/tools/bathelpers/analyze_project.bat rename to src/test/java/tools/bathelpers/analyze_project.bat diff --git a/src/tools/bathelpers/build_project.bat b/src/test/java/tools/bathelpers/build_project.bat similarity index 100% rename from src/tools/bathelpers/build_project.bat rename to src/test/java/tools/bathelpers/build_project.bat diff --git a/src/tools/bathelpers/list_files.bat b/src/test/java/tools/bathelpers/list_files.bat similarity index 100% rename from src/tools/bathelpers/list_files.bat rename to src/test/java/tools/bathelpers/list_files.bat diff --git a/src/tools/bathelpers/move_plugin.bat b/src/test/java/tools/bathelpers/move_plugin.bat similarity index 100% rename from src/tools/bathelpers/move_plugin.bat rename to src/test/java/tools/bathelpers/move_plugin.bat diff --git a/src/tools/bathelpers/quick_build.bat b/src/test/java/tools/bathelpers/quick_build.bat similarity index 100% rename from src/tools/bathelpers/quick_build.bat rename to src/test/java/tools/bathelpers/quick_build.bat diff --git a/src/tools/bathelpers/run_server.bat b/src/test/java/tools/bathelpers/run_server.bat similarity index 100% rename from src/tools/bathelpers/run_server.bat rename to src/test/java/tools/bathelpers/run_server.bat diff --git a/src/tools/bathelpers/setvars.bat b/src/test/java/tools/bathelpers/setvars.bat similarity index 100% rename from src/tools/bathelpers/setvars.bat rename to src/test/java/tools/bathelpers/setvars.bat diff --git a/src/tools/commands/CommandPageCreater.java b/src/test/java/tools/commands/CommandPageCreater.java similarity index 93% rename from src/tools/commands/CommandPageCreater.java rename to src/test/java/tools/commands/CommandPageCreater.java index 3369b906..f7409065 100644 --- a/src/tools/commands/CommandPageCreater.java +++ b/src/test/java/tools/commands/CommandPageCreater.java @@ -1,15 +1,15 @@ -package commands; +package tools.commands; import fr.xephi.authme.command.CommandArgumentDescription; import fr.xephi.authme.command.CommandDescription; import fr.xephi.authme.command.CommandPermissions; import fr.xephi.authme.command.CommandUtils; import fr.xephi.authme.permission.PermissionNode; -import utils.FileUtils; -import utils.TagValue.NestedTagValue; -import utils.TagValueHolder; -import utils.ToolTask; -import utils.ToolsConstants; +import tools.utils.FileUtils; +import tools.utils.TagValue.NestedTagValue; +import tools.utils.TagValueHolder; +import tools.utils.ToolTask; +import tools.utils.ToolsConstants; import java.util.Collection; import java.util.HashSet; diff --git a/src/tools/commands/commands.tpl.md b/src/test/java/tools/commands/commands.tpl.md similarity index 100% rename from src/tools/commands/commands.tpl.md rename to src/test/java/tools/commands/commands.tpl.md diff --git a/src/tools/docs/UpdateDocsTask.java b/src/test/java/tools/docs/UpdateDocsTask.java similarity index 86% rename from src/tools/docs/UpdateDocsTask.java rename to src/test/java/tools/docs/UpdateDocsTask.java index ccd0ed1b..df03b921 100644 --- a/src/tools/docs/UpdateDocsTask.java +++ b/src/test/java/tools/docs/UpdateDocsTask.java @@ -1,10 +1,10 @@ -package docs; +package tools.docs; import com.google.common.collect.ImmutableSet; -import commands.CommandPageCreater; -import hashmethods.HashAlgorithmsDescriptionTask; -import permissions.PermissionsListWriter; -import utils.ToolTask; +import tools.commands.CommandPageCreater; +import tools.hashmethods.HashAlgorithmsDescriptionTask; +import tools.permissions.PermissionsListWriter; +import tools.utils.ToolTask; import java.util.Scanner; import java.util.Set; diff --git a/src/tools/hashmethods/EncryptionMethodInfoGatherer.java b/src/test/java/tools/hashmethods/EncryptionMethodInfoGatherer.java similarity index 99% rename from src/tools/hashmethods/EncryptionMethodInfoGatherer.java rename to src/test/java/tools/hashmethods/EncryptionMethodInfoGatherer.java index d56e370d..47bb8bd6 100644 --- a/src/tools/hashmethods/EncryptionMethodInfoGatherer.java +++ b/src/test/java/tools/hashmethods/EncryptionMethodInfoGatherer.java @@ -1,4 +1,4 @@ -package hashmethods; +package tools.hashmethods; import fr.xephi.authme.security.HashAlgorithm; import fr.xephi.authme.security.crypts.EncryptionMethod; diff --git a/src/tools/hashmethods/HashAlgorithmsDescriptionTask.java b/src/test/java/tools/hashmethods/HashAlgorithmsDescriptionTask.java similarity index 93% rename from src/tools/hashmethods/HashAlgorithmsDescriptionTask.java rename to src/test/java/tools/hashmethods/HashAlgorithmsDescriptionTask.java index 6d1a57ed..e62f5b3d 100644 --- a/src/tools/hashmethods/HashAlgorithmsDescriptionTask.java +++ b/src/test/java/tools/hashmethods/HashAlgorithmsDescriptionTask.java @@ -1,11 +1,11 @@ -package hashmethods; +package tools.hashmethods; import fr.xephi.authme.security.HashAlgorithm; -import utils.FileUtils; -import utils.TagValue.NestedTagValue; -import utils.TagValueHolder; -import utils.ToolTask; -import utils.ToolsConstants; +import tools.utils.FileUtils; +import tools.utils.TagValue.NestedTagValue; +import tools.utils.TagValueHolder; +import tools.utils.ToolTask; +import tools.utils.ToolsConstants; import java.util.Map; import java.util.Scanner; diff --git a/src/tools/hashmethods/MethodDescription.java b/src/test/java/tools/hashmethods/MethodDescription.java similarity index 98% rename from src/tools/hashmethods/MethodDescription.java rename to src/test/java/tools/hashmethods/MethodDescription.java index 45aaf448..58765467 100644 --- a/src/tools/hashmethods/MethodDescription.java +++ b/src/test/java/tools/hashmethods/MethodDescription.java @@ -1,4 +1,4 @@ -package hashmethods; +package tools.hashmethods; import fr.xephi.authme.security.crypts.EncryptionMethod; import fr.xephi.authme.security.crypts.description.SaltType; diff --git a/src/tools/hashmethods/hash_algorithms.tpl.md b/src/test/java/tools/hashmethods/hash_algorithms.tpl.md similarity index 100% rename from src/tools/hashmethods/hash_algorithms.tpl.md rename to src/test/java/tools/hashmethods/hash_algorithms.tpl.md diff --git a/src/tools/messages/MessageFileVerifier.java b/src/test/java/tools/messages/MessageFileVerifier.java similarity index 99% rename from src/tools/messages/MessageFileVerifier.java rename to src/test/java/tools/messages/MessageFileVerifier.java index 68e07ea4..1157284f 100644 --- a/src/tools/messages/MessageFileVerifier.java +++ b/src/test/java/tools/messages/MessageFileVerifier.java @@ -1,4 +1,4 @@ -package messages; +package tools.messages; import com.google.common.base.Predicate; import com.google.common.collect.HashMultimap; @@ -8,7 +8,7 @@ import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.util.StringUtils; import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.configuration.file.YamlConfiguration; -import utils.FileUtils; +import tools.utils.FileUtils; import java.io.File; import java.util.ArrayList; diff --git a/src/tools/messages/README.md b/src/test/java/tools/messages/README.md similarity index 100% rename from src/tools/messages/README.md rename to src/test/java/tools/messages/README.md diff --git a/src/tools/messages/VerifyMessagesTask.java b/src/test/java/tools/messages/VerifyMessagesTask.java similarity index 98% rename from src/tools/messages/VerifyMessagesTask.java rename to src/test/java/tools/messages/VerifyMessagesTask.java index 28b42b71..c4415e03 100644 --- a/src/tools/messages/VerifyMessagesTask.java +++ b/src/test/java/tools/messages/VerifyMessagesTask.java @@ -1,11 +1,11 @@ -package messages; +package tools.messages; import com.google.common.collect.Multimap; import fr.xephi.authme.util.StringUtils; import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.configuration.file.YamlConfiguration; -import utils.ToolTask; -import utils.ToolsConstants; +import tools.utils.ToolTask; +import tools.utils.ToolsConstants; import java.io.File; import java.util.ArrayList; diff --git a/src/tools/messages/translation/AuthMeYamlConfiguration.java b/src/test/java/tools/messages/translation/AuthMeYamlConfiguration.java similarity index 98% rename from src/tools/messages/translation/AuthMeYamlConfiguration.java rename to src/test/java/tools/messages/translation/AuthMeYamlConfiguration.java index f8724dff..21564b57 100644 --- a/src/tools/messages/translation/AuthMeYamlConfiguration.java +++ b/src/test/java/tools/messages/translation/AuthMeYamlConfiguration.java @@ -1,4 +1,4 @@ -package messages.translation; +package tools.messages.translation; import org.bukkit.configuration.InvalidConfigurationException; import org.bukkit.configuration.file.YamlConfiguration; diff --git a/src/tools/messages/translation/ExportMessagesTask.java b/src/test/java/tools/messages/translation/ExportMessagesTask.java similarity index 97% rename from src/tools/messages/translation/ExportMessagesTask.java rename to src/test/java/tools/messages/translation/ExportMessagesTask.java index c281696b..672dac39 100644 --- a/src/tools/messages/translation/ExportMessagesTask.java +++ b/src/test/java/tools/messages/translation/ExportMessagesTask.java @@ -1,4 +1,4 @@ -package messages.translation; +package tools.messages.translation; import com.google.common.io.CharStreams; import com.google.gson.Gson; @@ -6,8 +6,8 @@ import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.util.StringUtils; import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.configuration.file.YamlConfiguration; -import utils.ToolTask; -import utils.ToolsConstants; +import tools.utils.ToolTask; +import tools.utils.ToolsConstants; import java.io.File; import java.io.IOException; diff --git a/src/tools/messages/translation/ImportMessagesTask.java b/src/test/java/tools/messages/translation/ImportMessagesTask.java similarity index 95% rename from src/tools/messages/translation/ImportMessagesTask.java rename to src/test/java/tools/messages/translation/ImportMessagesTask.java index 1edcdf04..320edb01 100644 --- a/src/tools/messages/translation/ImportMessagesTask.java +++ b/src/test/java/tools/messages/translation/ImportMessagesTask.java @@ -1,15 +1,15 @@ -package messages.translation; +package tools.messages.translation; import com.google.common.io.Resources; import com.google.gson.Gson; import fr.xephi.authme.output.MessageKey; -import messages.MessageFileVerifier; -import messages.VerifyMessagesTask; import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.configuration.file.YamlConfiguration; -import utils.FileUtils; -import utils.ToolTask; -import utils.ToolsConstants; +import tools.messages.MessageFileVerifier; +import tools.messages.VerifyMessagesTask; +import tools.utils.FileUtils; +import tools.utils.ToolTask; +import tools.utils.ToolsConstants; import java.io.File; import java.io.IOException; diff --git a/src/tools/messages/translation/LanguageExport.java b/src/test/java/tools/messages/translation/LanguageExport.java similarity index 91% rename from src/tools/messages/translation/LanguageExport.java rename to src/test/java/tools/messages/translation/LanguageExport.java index 3be0fa44..c15a54bd 100644 --- a/src/tools/messages/translation/LanguageExport.java +++ b/src/test/java/tools/messages/translation/LanguageExport.java @@ -1,4 +1,4 @@ -package messages.translation; +package tools.messages.translation; import java.util.Collections; import java.util.List; diff --git a/src/tools/messages/translation/MessageExport.java b/src/test/java/tools/messages/translation/MessageExport.java similarity index 93% rename from src/tools/messages/translation/MessageExport.java rename to src/test/java/tools/messages/translation/MessageExport.java index da2c0874..270c460d 100644 --- a/src/tools/messages/translation/MessageExport.java +++ b/src/test/java/tools/messages/translation/MessageExport.java @@ -1,4 +1,4 @@ -package messages.translation; +package tools.messages.translation; import fr.xephi.authme.util.StringUtils; diff --git a/src/tools/messages/translation/WriteAllExportsTask.java b/src/test/java/tools/messages/translation/WriteAllExportsTask.java similarity index 92% rename from src/tools/messages/translation/WriteAllExportsTask.java rename to src/test/java/tools/messages/translation/WriteAllExportsTask.java index 413943d4..f5f3825b 100644 --- a/src/tools/messages/translation/WriteAllExportsTask.java +++ b/src/test/java/tools/messages/translation/WriteAllExportsTask.java @@ -1,9 +1,9 @@ -package messages.translation; +package tools.messages.translation; import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.configuration.file.YamlConfiguration; -import utils.FileUtils; -import utils.ToolsConstants; +import tools.utils.FileUtils; +import tools.utils.ToolsConstants; import java.io.File; import java.util.Scanner; diff --git a/src/tools/messages/translation/export/export-is-written-here b/src/test/java/tools/messages/translation/export/export-is-written-here similarity index 100% rename from src/tools/messages/translation/export/export-is-written-here rename to src/test/java/tools/messages/translation/export/export-is-written-here diff --git a/src/tools/permissions/PermissionNodesGatherer.java b/src/test/java/tools/permissions/PermissionNodesGatherer.java similarity index 97% rename from src/tools/permissions/PermissionNodesGatherer.java rename to src/test/java/tools/permissions/PermissionNodesGatherer.java index bfde6c87..58b5df4e 100644 --- a/src/tools/permissions/PermissionNodesGatherer.java +++ b/src/test/java/tools/permissions/PermissionNodesGatherer.java @@ -1,11 +1,11 @@ -package permissions; +package tools.permissions; import fr.xephi.authme.permission.AdminPermission; import fr.xephi.authme.permission.PermissionNode; import fr.xephi.authme.permission.PlayerPermission; import fr.xephi.authme.permission.PlayerStatePermission; -import utils.FileUtils; -import utils.ToolsConstants; +import tools.utils.FileUtils; +import tools.utils.ToolsConstants; import java.util.EnumSet; import java.util.HashMap; diff --git a/src/tools/permissions/PermissionsListWriter.java b/src/test/java/tools/permissions/PermissionsListWriter.java similarity index 93% rename from src/tools/permissions/PermissionsListWriter.java rename to src/test/java/tools/permissions/PermissionsListWriter.java index dc16cea2..79522d9d 100644 --- a/src/tools/permissions/PermissionsListWriter.java +++ b/src/test/java/tools/permissions/PermissionsListWriter.java @@ -1,10 +1,10 @@ -package permissions; +package tools.permissions; -import utils.FileUtils; -import utils.TagValue.NestedTagValue; -import utils.TagValueHolder; -import utils.ToolTask; -import utils.ToolsConstants; +import tools.utils.FileUtils; +import tools.utils.TagValue.NestedTagValue; +import tools.utils.TagValueHolder; +import tools.utils.ToolTask; +import tools.utils.ToolsConstants; import java.util.Map; import java.util.Scanner; diff --git a/src/tools/permissions/README.md b/src/test/java/tools/permissions/README.md similarity index 100% rename from src/tools/permissions/README.md rename to src/test/java/tools/permissions/README.md diff --git a/src/tools/permissions/permission_nodes.tpl.md b/src/test/java/tools/permissions/permission_nodes.tpl.md similarity index 100% rename from src/tools/permissions/permission_nodes.tpl.md rename to src/test/java/tools/permissions/permission_nodes.tpl.md diff --git a/src/tools/shhelpers/analyze_project.sh b/src/test/java/tools/shhelpers/analyze_project.sh similarity index 100% rename from src/tools/shhelpers/analyze_project.sh rename to src/test/java/tools/shhelpers/analyze_project.sh diff --git a/src/tools/shhelpers/build_project.sh b/src/test/java/tools/shhelpers/build_project.sh similarity index 100% rename from src/tools/shhelpers/build_project.sh rename to src/test/java/tools/shhelpers/build_project.sh diff --git a/src/tools/shhelpers/list_files.sh b/src/test/java/tools/shhelpers/list_files.sh similarity index 100% rename from src/tools/shhelpers/list_files.sh rename to src/test/java/tools/shhelpers/list_files.sh diff --git a/src/tools/shhelpers/move_plugin.sh b/src/test/java/tools/shhelpers/move_plugin.sh similarity index 100% rename from src/tools/shhelpers/move_plugin.sh rename to src/test/java/tools/shhelpers/move_plugin.sh diff --git a/src/tools/shhelpers/quick_build.sh b/src/test/java/tools/shhelpers/quick_build.sh similarity index 100% rename from src/tools/shhelpers/quick_build.sh rename to src/test/java/tools/shhelpers/quick_build.sh diff --git a/src/tools/shhelpers/run_server.sh b/src/test/java/tools/shhelpers/run_server.sh similarity index 100% rename from src/tools/shhelpers/run_server.sh rename to src/test/java/tools/shhelpers/run_server.sh diff --git a/src/tools/shhelpers/setvars.sh b/src/test/java/tools/shhelpers/setvars.sh similarity index 100% rename from src/tools/shhelpers/setvars.sh rename to src/test/java/tools/shhelpers/setvars.sh diff --git a/src/tools/shhelpers/sort_file_content.sh b/src/test/java/tools/shhelpers/sort_file_content.sh similarity index 100% rename from src/tools/shhelpers/sort_file_content.sh rename to src/test/java/tools/shhelpers/sort_file_content.sh diff --git a/src/tools/utils/FileUtils.java b/src/test/java/tools/utils/FileUtils.java similarity index 98% rename from src/tools/utils/FileUtils.java rename to src/test/java/tools/utils/FileUtils.java index 853965e9..dbf40037 100644 --- a/src/tools/utils/FileUtils.java +++ b/src/test/java/tools/utils/FileUtils.java @@ -1,4 +1,4 @@ -package utils; +package tools.utils; import java.io.IOException; import java.nio.charset.Charset; diff --git a/src/tools/utils/TagReplacer.java b/src/test/java/tools/utils/TagReplacer.java similarity index 97% rename from src/tools/utils/TagReplacer.java rename to src/test/java/tools/utils/TagReplacer.java index 5efe4c8b..9b5a74bb 100644 --- a/src/tools/utils/TagReplacer.java +++ b/src/test/java/tools/utils/TagReplacer.java @@ -1,7 +1,7 @@ -package utils; +package tools.utils; -import utils.TagValue.NestedTagValue; -import utils.TagValue.TextTagValue; +import tools.utils.TagValue.NestedTagValue; +import tools.utils.TagValue.TextTagValue; import java.util.Date; import java.util.Map; diff --git a/src/tools/utils/TagValue.java b/src/test/java/tools/utils/TagValue.java similarity index 97% rename from src/tools/utils/TagValue.java rename to src/test/java/tools/utils/TagValue.java index 7ca66b3d..ad73a816 100644 --- a/src/tools/utils/TagValue.java +++ b/src/test/java/tools/utils/TagValue.java @@ -1,4 +1,4 @@ -package utils; +package tools.utils; import java.util.ArrayList; import java.util.List; diff --git a/src/tools/utils/TagValueHolder.java b/src/test/java/tools/utils/TagValueHolder.java similarity index 90% rename from src/tools/utils/TagValueHolder.java rename to src/test/java/tools/utils/TagValueHolder.java index e2bd09b0..83862830 100644 --- a/src/tools/utils/TagValueHolder.java +++ b/src/test/java/tools/utils/TagValueHolder.java @@ -1,6 +1,6 @@ -package utils; +package tools.utils; -import utils.TagValue.TextTagValue; +import tools.utils.TagValue.TextTagValue; import java.util.HashMap; import java.util.Map; diff --git a/src/tools/utils/ToolTask.java b/src/test/java/tools/utils/ToolTask.java similarity index 96% rename from src/tools/utils/ToolTask.java rename to src/test/java/tools/utils/ToolTask.java index 56a2a788..d5cec152 100644 --- a/src/tools/utils/ToolTask.java +++ b/src/test/java/tools/utils/ToolTask.java @@ -1,4 +1,4 @@ -package utils; +package tools.utils; import java.util.Scanner; diff --git a/src/tools/utils/ToolsConstants.java b/src/test/java/tools/utils/ToolsConstants.java similarity index 82% rename from src/tools/utils/ToolsConstants.java rename to src/test/java/tools/utils/ToolsConstants.java index 504a1be7..84ad7fad 100644 --- a/src/tools/utils/ToolsConstants.java +++ b/src/test/java/tools/utils/ToolsConstants.java @@ -1,4 +1,4 @@ -package utils; +package tools.utils; /** * Constants for the src/tools folder. @@ -12,7 +12,7 @@ public final class ToolsConstants { public static final String MAIN_RESOURCES_ROOT = "src/main/resources/"; - public static final String TOOLS_SOURCE_ROOT = "src/tools/"; + public static final String TOOLS_SOURCE_ROOT = "src/test/java/tools/"; public static final String DOCS_FOLDER = "docs/"; diff --git a/src/tools/README.md b/src/tools/README.md deleted file mode 100644 index b61dabea..00000000 --- a/src/tools/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# About src/tools -This _tools_ folder provides helpers and extended tests useful during the development of AuthMe. -This folder is not included during the build of AuthMe. From 67aea654cc8dbbf0f93535fce0213447cb3c1b6c Mon Sep 17 00:00:00 2001 From: ljacqu Date: Tue, 3 May 2016 20:44:01 +0200 Subject: [PATCH 012/200] #432 Fix broken tool tasks --- .../tools/commands/CommandPageCreater.java | 33 ++++++++++++++-- .../EncryptionMethodInfoGatherer.java | 38 +++++++++++-------- 2 files changed, 52 insertions(+), 19 deletions(-) diff --git a/src/test/java/tools/commands/CommandPageCreater.java b/src/test/java/tools/commands/CommandPageCreater.java index f7409065..4ba48006 100644 --- a/src/test/java/tools/commands/CommandPageCreater.java +++ b/src/test/java/tools/commands/CommandPageCreater.java @@ -2,9 +2,14 @@ package tools.commands; import fr.xephi.authme.command.CommandArgumentDescription; import fr.xephi.authme.command.CommandDescription; +import fr.xephi.authme.command.CommandInitializer; import fr.xephi.authme.command.CommandPermissions; import fr.xephi.authme.command.CommandUtils; +import fr.xephi.authme.command.ExecutableCommand; +import fr.xephi.authme.initialization.AuthMeServiceInitializer; import fr.xephi.authme.permission.PermissionNode; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; import tools.utils.FileUtils; import tools.utils.TagValue.NestedTagValue; import tools.utils.TagValueHolder; @@ -12,10 +17,13 @@ import tools.utils.ToolTask; import tools.utils.ToolsConstants; import java.util.Collection; -import java.util.HashSet; import java.util.Scanner; import java.util.Set; +import static org.mockito.Matchers.isA; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + public class CommandPageCreater implements ToolTask { private static final String OUTPUT_FILE = ToolsConstants.DOCS_FOLDER + "commands.md"; @@ -27,8 +35,7 @@ public class CommandPageCreater implements ToolTask { @Override public void execute(Scanner scanner) { - // TODO ljacqu 20160427: Fix initialization of commands - final Set baseCommands = new HashSet<>();//CommandInitializer.buildCommands(); + final Set baseCommands = CommandInitializer.buildCommands(getMockInitializer()); NestedTagValue commandTags = new NestedTagValue(); addCommandsInfo(commandTags, baseCommands); @@ -75,4 +82,24 @@ public class CommandPageCreater implements ToolTask { } return result.toString(); } + + /** + * Creates an initializer mock that returns mocks of any {@link ExecutableCommand} subclasses passed to it. + * + * @return the initializer mock + */ + private static AuthMeServiceInitializer getMockInitializer() { + AuthMeServiceInitializer initializer = mock(AuthMeServiceInitializer.class); + when(initializer.newInstance(isA(Class.class))).thenAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + Class clazz = (Class) invocation.getArguments()[0]; + if (ExecutableCommand.class.isAssignableFrom(clazz)) { + return mock(clazz); + } + throw new IllegalStateException("Unexpected request to instantiate class of type " + clazz.getName()); + } + }); + return initializer; + } } diff --git a/src/test/java/tools/hashmethods/EncryptionMethodInfoGatherer.java b/src/test/java/tools/hashmethods/EncryptionMethodInfoGatherer.java index 47bb8bd6..ad2ecba4 100644 --- a/src/test/java/tools/hashmethods/EncryptionMethodInfoGatherer.java +++ b/src/test/java/tools/hashmethods/EncryptionMethodInfoGatherer.java @@ -1,5 +1,6 @@ package tools.hashmethods; +import fr.xephi.authme.initialization.AuthMeServiceInitializer; import fr.xephi.authme.security.HashAlgorithm; import fr.xephi.authme.security.crypts.EncryptionMethod; import fr.xephi.authme.security.crypts.HexSaltedMethod; @@ -7,9 +8,9 @@ import fr.xephi.authme.security.crypts.description.AsciiRestricted; import fr.xephi.authme.security.crypts.description.HasSalt; import fr.xephi.authme.security.crypts.description.Recommendation; import fr.xephi.authme.settings.NewSetting; -import fr.xephi.authme.settings.properties.HooksSettings; -import fr.xephi.authme.settings.properties.SecuritySettings; -import org.mockito.BDDMockito; +import fr.xephi.authme.settings.domain.Property; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; import java.lang.annotation.Annotation; import java.util.HashMap; @@ -18,7 +19,9 @@ import java.util.Map; import java.util.Set; import static com.google.common.collect.Sets.newHashSet; +import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; /** * Gathers information on {@link EncryptionMethod} implementations based on @@ -30,7 +33,7 @@ public class EncryptionMethodInfoGatherer { private final static Set> RELEVANT_ANNOTATIONS = newHashSet(HasSalt.class, Recommendation.class, AsciiRestricted.class); - private static NewSetting settings = createSettings(); + private static AuthMeServiceInitializer initializer = createInitializer(); private Map descriptions; @@ -54,7 +57,7 @@ public class EncryptionMethodInfoGatherer { private static MethodDescription createDescription(HashAlgorithm algorithm) { Class clazz = algorithm.getClazz(); - EncryptionMethod method = null; // TODO ljacqu PasswordSecurity.initializeEncryptionMethod(algorithm, settings); + EncryptionMethod method = initializer.newInstance(clazz); if (method == null) { throw new NullPointerException("Method for '" + algorithm + "' is null"); } @@ -134,19 +137,22 @@ public class EncryptionMethodInfoGatherer { return key.cast(map.get(key)); } - private static NewSetting createSettings() { - // TODO #672 Don't mock settings but instantiate a NewSetting object without any validation / migration + private static AuthMeServiceInitializer createInitializer() { NewSetting settings = mock(NewSetting.class); - BDDMockito.given(settings.getProperty(HooksSettings.BCRYPT_LOG2_ROUND)).willReturn(8); - BDDMockito.given(settings.getProperty(SecuritySettings.DOUBLE_MD5_SALT_LENGTH)).willReturn(8); - return settings; + // Return the default value for any property + when(settings.getProperty(any(Property.class))).thenAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + Property property = (Property) invocation.getArguments()[0]; + return property.getDefaultValue(); + } + }); - /*try (InputStreamReader isr = new InputStreamReader(getClass().getResourceAsStream("config.yml"))) { - FileConfiguration configuration = YamlConfiguration.loadConfiguration(isr); - return new NewSetting(configuration, null, null, null); - } catch (IOException e) { - throw new UnsupportedOperationException(e); - }*/ + // By not passing any "allowed package" to the constructor, the initializer will throw if it needs to + // instantiate any dependency other than what we provide. + AuthMeServiceInitializer initializer = new AuthMeServiceInitializer(); + initializer.register(NewSetting.class, settings); + return initializer; } } From 2f76e063731d9536a1a207fccfde4e2597e7012d Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Wed, 4 May 2016 16:59:02 +0200 Subject: [PATCH 013/200] Remove spigot-api --- pom.xml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/pom.xml b/pom.xml index 094b2e19..2972cebb 100644 --- a/pom.xml +++ b/pom.xml @@ -436,14 +436,6 @@ true - - - org.spigotmc - spigot-api - 1.9.2-R0.1-SNAPSHOT - provided - - org.bukkit From 39aaef8f8ac8c6f63d6fdb7a50b225fbfff7a6c1 Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Wed, 4 May 2016 17:12:14 +0200 Subject: [PATCH 014/200] Pom enhancements --- pom.xml | 173 +++++++++++++++------------------- src/main/resources/plugin.yml | 6 +- 2 files changed, 81 insertions(+), 98 deletions(-) diff --git a/pom.xml b/pom.xml index 2972cebb..145d9372 100644 --- a/pom.xml +++ b/pom.xml @@ -21,9 +21,9 @@ - scm:git:https://github.com/Xephi/AuthMeReloaded.git - scm:git:git@github.com:Xephi/AuthMeReloaded.git - https://github.com/Xephi/AuthMeReloaded + scm:git:https://github.com/AuthMe-Team/AuthMeReloaded.git + scm:git:git@github.com:AuthMe-Team/AuthMeReloaded.git + https://github.com/AuthMe-Team/AuthMeReloaded @@ -49,22 +49,21 @@ - - UTF-8 - ${projectEncoding} - ${projectEncoding} - 1.7 - 1.7 - + + UTF-8 + 1.7 + - AuthMe - ${pluginName}-${project.version} - ${project.groupId}.${project.artifactId}.${pluginName} - Xephi, sgdc3, DNx5, timvisee, games647, ljacqu - Unknown + CUSTOM + ${pluginName}-${project.version} + + + AuthMe + ${project.groupId}.${project.artifactId}.${pluginName} + Xephi, sgdc3, DNx5, timvisee, games647, ljacqu - 1.9.2-R0.1-SNAPSHOT + 1.9.2-R0.1-SNAPSHOT @@ -77,47 +76,32 @@ - ${env.BUILD_NUMBER} + ${env.BUILD_NUMBER} - ${jarName}-noshade - src/main/java - src/test/java + ${project.buildNumber}-noshade - . - false . + false LICENSE - . - true src/main/resources/ - - * - + true + src/main/resources/messages/ ./messages/ false - src/main/resources/messages/ - - *.yml - - - - src/test/resources - - @@ -126,10 +110,8 @@ maven-compiler-plugin 3.5.1 - ${jdkVersion} - ${jdkVersion} - ${testJreVersion} - ${testJreVersion} + ${project.jdkVersion} + ${project.jdkVersion} @@ -138,21 +120,27 @@ maven-surefire-plugin 2.19.1 - -Dfile.encoding=${projectEncoding} ${argLine} + -Dfile.encoding=${project.build.sourceEncoding} ${argLine} - + org.apache.maven.plugins maven-shade-plugin 2.4.3 false + + spigot-shade package @@ -160,6 +148,7 @@ shade + com.google.guava:guava @@ -196,6 +185,7 @@ target/${jarName}-spigot.jar + legacy-shade package @@ -354,19 +344,6 @@ compile true - - - org.xerial - sqlite-jdbc - 3.8.11.2 - test - - - com.h2database - h2 - 1.4.191 - test - @@ -375,7 +352,6 @@ 2.0-beta9 provided - true @@ -396,7 +372,7 @@ true - + com.google.guava guava @@ -405,11 +381,15 @@ true + + javax.inject javax.inject 1 + compile + true @@ -440,9 +420,8 @@ org.bukkit bukkit - ${bukkitVersion} + ${bukkit.version} provided - true junit @@ -473,7 +452,6 @@ ProtocolLib 3.6.5-SNAPSHOT provided - true cglib-nodep @@ -590,7 +568,6 @@ craftbukkit - true @@ -646,7 +623,6 @@ junit - true @@ -665,7 +641,6 @@ craftbukkit - true @@ -740,7 +715,6 @@ org.mcstats.bukkit - true @@ -783,36 +757,6 @@ org.apache.logging.log4j - true - - - - - junit - junit - test - 4.12 - true - - - org.hamcrest - java-hamcrest - test - 2.0.0.0 - true - - - org.mockito - mockito-core - test - 2.0.5-beta - true - - - hamcrest-core - org.hamcrest - - @@ -823,5 +767,44 @@ compile true + + + + junit + junit + test + 4.12 + + + org.hamcrest + java-hamcrest + test + 2.0.0.0 + + + org.mockito + mockito-core + test + 2.0.5-beta + + + hamcrest-core + org.hamcrest + + + + + + org.xerial + sqlite-jdbc + 3.8.11.2 + test + + + com.h2database + h2 + 1.4.191 + test + diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index bf63b364..0a50c74c 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,8 +1,8 @@ -name: ${pluginName} -authors: [${pluginAuthors}] +name: ${bukkitplugin.name} +authors: [${bukkitplugin.authors}] website: ${project.url} description: ${project.description} -main: ${mainClass} +main: ${bukkitplugin.main} version: ${project.version}-b${buildNumber} softdepend: - Vault From 5ceabe2146727cb94ba62fc3f55984e44cbc7586 Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Wed, 4 May 2016 17:26:58 +0200 Subject: [PATCH 015/200] Cleanup --- .../xephi/authme/command/CommandService.java | 1 + .../fr/xephi/authme/datasource/MySQL.java | 2 + .../authme/listener/AuthMePlayerListener.java | 6 -- .../fr/xephi/authme/mail/SendMailSSL.java | 3 +- .../authme/permission/PermissionsManager.java | 2 - .../authme/security/pbkdf2/BinTools.java | 90 +++++++++---------- .../xephi/authme/util/MigrationService.java | 2 - .../command/CommandInitializerTest.java | 1 + .../authme/events/EventsConsistencyTest.java | 1 + .../ConstructorInjectionTest.java | 2 + .../initialization/FieldInjectionTest.java | 3 + .../samples/BadFieldInjection.java | 1 + .../samples/InvalidPostConstruct.java | 2 + .../samples/InvalidStaticFieldInjection.java | 1 + .../tools/commands/CommandPageCreater.java | 1 + .../EncryptionMethodInfoGatherer.java | 1 + 16 files changed, 57 insertions(+), 62 deletions(-) diff --git a/src/main/java/fr/xephi/authme/command/CommandService.java b/src/main/java/fr/xephi/authme/command/CommandService.java index 95ddb563..f88196cc 100644 --- a/src/main/java/fr/xephi/authme/command/CommandService.java +++ b/src/main/java/fr/xephi/authme/command/CommandService.java @@ -34,6 +34,7 @@ public class CommandService { private HelpProvider helpProvider; @Inject private CommandMapper commandMapper; + @SuppressWarnings("unused") @Inject private PasswordSecurity passwordSecurity; @Inject diff --git a/src/main/java/fr/xephi/authme/datasource/MySQL.java b/src/main/java/fr/xephi/authme/datasource/MySQL.java index 5182b859..4f6317bc 100644 --- a/src/main/java/fr/xephi/authme/datasource/MySQL.java +++ b/src/main/java/fr/xephi/authme/datasource/MySQL.java @@ -424,6 +424,7 @@ public class MySQL implements DataSource { rs.close(); pst.close(); } else if (hashAlgorithm == HashAlgorithm.WORDPRESS) { + // NOTE: Eclipse says pst should be closed HERE, but it's a bug, we already close it above. -sgdc3 pst = con.prepareStatement("SELECT " + col.ID + " FROM " + tableName + " WHERE " + col.NAME + "=?;"); pst.setString(1, auth.getNickname()); rs = pst.executeQuery(); @@ -500,6 +501,7 @@ public class MySQL implements DataSource { rs.close(); pst.close(); } else if (hashAlgorithm == HashAlgorithm.XFBCRYPT) { + // NOTE: Eclipse says pst should be closed HERE, but it's a bug, we already close it above. -sgdc3 pst = con.prepareStatement("SELECT " + col.ID + " FROM " + tableName + " WHERE " + col.NAME + "=?;"); pst.setString(1, auth.getNickname()); rs = pst.executeQuery(); diff --git a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java index a0fa4011..fb44b4c3 100644 --- a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java +++ b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java @@ -407,12 +407,6 @@ public class AuthMePlayerListener implements Listener { plugin.getManagement().performQuit(player, true); } - /* - * <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< - * Note #360: npc status can be used to bypass security!!! - * <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< - */ - @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) public void onPlayerPickupItem(PlayerPickupItemEvent event) { if (shouldCancelEvent(event)) { diff --git a/src/main/java/fr/xephi/authme/mail/SendMailSSL.java b/src/main/java/fr/xephi/authme/mail/SendMailSSL.java index d8332602..88d57d9a 100644 --- a/src/main/java/fr/xephi/authme/mail/SendMailSSL.java +++ b/src/main/java/fr/xephi/authme/mail/SendMailSSL.java @@ -152,7 +152,6 @@ public class SendMailSSL { } else { email.setStartTLSEnabled(true); email.setStartTLSRequired(true); - email.setTLS(true); } break; case 25: @@ -161,7 +160,7 @@ public class SendMailSSL { break; case 465: email.setSslSmtpPort(Integer.toString(port)); - email.setSSL(true); + email.setSSLOnConnect(true); break; default: email.setStartTLSEnabled(true); diff --git a/src/main/java/fr/xephi/authme/permission/PermissionsManager.java b/src/main/java/fr/xephi/authme/permission/PermissionsManager.java index 7dfcd40c..388eed9e 100644 --- a/src/main/java/fr/xephi/authme/permission/PermissionsManager.java +++ b/src/main/java/fr/xephi/authme/permission/PermissionsManager.java @@ -346,7 +346,6 @@ public class PermissionsManager implements PermissionsService { case Z_PERMISSIONS: // zPermissions - @SuppressWarnings("deprecation") Map perms = zPermissionsService.getPlayerPermissions(player.getWorld().getName(), null, player.getName()); if (perms.containsKey(permsNode)) return perms.get(permsNode); @@ -448,7 +447,6 @@ public class PermissionsManager implements PermissionsService { * * @return The name of the primary permission group. Or null. */ - @SuppressWarnings("deprecation") public String getPrimaryGroup(Player player) { // If no permissions system is used, return an empty list if (!isEnabled()) diff --git a/src/main/java/fr/xephi/authme/security/pbkdf2/BinTools.java b/src/main/java/fr/xephi/authme/security/pbkdf2/BinTools.java index daadcae8..34bf8aac 100644 --- a/src/main/java/fr/xephi/authme/security/pbkdf2/BinTools.java +++ b/src/main/java/fr/xephi/authme/security/pbkdf2/BinTools.java @@ -1,71 +1,62 @@ package fr.xephi.authme.security.pbkdf2; +/* + * Free auxiliary functions. Copyright 2007, 2014, Matthias Gärtner + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ + /** - *

- * Free auxiliary functions. Copyright (c) 2007 Matthias Gärtner - *

- *

- * This library is free software; you can redistribute it and/or modify it under - * the terms of the GNU Lesser General Public License as published by the Free - * Software Foundation; either version 2.1 of the License, or (at your option) - * any later version. - *

- *

- * This library is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS - * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more - * details. - *

- *

- * You should have received a copy of the GNU Lesser General Public License - * along with this library; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - *

- *

- * For Details, see http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. - *

+ * Free auxiliary functions * * @author Matthias Gärtner - * @version 1.0 */ public class BinTools { - - private static final String HEX_CHARS = "0123456789ABCDEF"; - - private BinTools() { - } + public static final String hex = "0123456789ABCDEF"; /** * Simple binary-to-hexadecimal conversion. * - * @param b Input bytes. May be null. - * + * @param b + * Input bytes. May be null. * @return Hexadecimal representation of b. Uppercase A-F, two characters - * per byte. Empty string on null input. + * per byte. Empty string on null input. */ public static String bin2hex(final byte[] b) { if (b == null) { return ""; } - StringBuffer stringBuffer = new StringBuffer(2 * b.length); - for (byte aB : b) { - int v = (256 + aB) % 256; - stringBuffer.append(HEX_CHARS.charAt((v / 16) & 15)); - stringBuffer.append(HEX_CHARS.charAt((v % 16) & 15)); + StringBuffer sb = new StringBuffer(2 * b.length); + for (int i = 0; i < b.length; i++) { + int v = (256 + b[i]) % 256; + sb.append(hex.charAt((v / 16) & 15)); + sb.append(hex.charAt((v % 16) & 15)); } - return stringBuffer.toString(); + return sb.toString(); } /** * Convert hex string to array of bytes. * - * @param s String containing hexadecimal digits. May be null. - * On odd length leading zero will be assumed. - * + * @param s + * String containing hexadecimal digits. May be null + * . On odd length leading zero will be assumed. * @return Array on bytes, non-null. - * @throws IllegalArgumentException when string contains non-hex character + * @throws IllegalArgumentException + * when string contains non-hex character */ public static byte[] hex2bin(final String s) { String m = s; @@ -88,10 +79,11 @@ public class BinTools { /** * Convert hex digit to numerical value. * - * @param c 0-9, a-f, A-F allowed. - * + * @param c + * 0-9, a-f, A-F allowd. * @return 0-15 - * @throws IllegalArgumentException on non-hex character + * @throws IllegalArgumentException + * on non-hex character */ public static int hex2bin(char c) { if (c >= '0' && c <= '9') { @@ -106,9 +98,7 @@ public class BinTools { throw new IllegalArgumentException("Input string may only contain hex digits, but found '" + c + "'"); } - // Note ljacqu 20160313: This appears to be a test method that was present in the third-party source. - // We can keep it for troubleshooting in the future. - private static void testUtils() { + public static void main(String[] args) { byte b[] = new byte[256]; byte bb = 0; for (int i = 0; i < 256; i++) { diff --git a/src/main/java/fr/xephi/authme/util/MigrationService.java b/src/main/java/fr/xephi/authme/util/MigrationService.java index 7bf028f6..cd9bb382 100644 --- a/src/main/java/fr/xephi/authme/util/MigrationService.java +++ b/src/main/java/fr/xephi/authme/util/MigrationService.java @@ -31,7 +31,6 @@ public final class MigrationService { * @param dataSource The data source * @param authmeSha256 Instance to the AuthMe SHA256 encryption method implementation */ - @SuppressWarnings("deprecation") public static void changePlainTextToSha256(NewSetting settings, DataSource dataSource, SHA256 authmeSha256) { if (HashAlgorithm.PLAINTEXT == settings.getProperty(SecuritySettings.PASSWORD_HASH)) { @@ -62,7 +61,6 @@ public final class MigrationService { * @param dataSource The data source * @return The converted datasource (SQLite), or null if no migration was necessary */ - @SuppressWarnings("deprecation") public static DataSource convertFlatfileToSqlite(NewSetting settings, DataSource dataSource) { if (DataSourceType.FILE == settings.getProperty(DatabaseSettings.BACKEND)) { ConsoleLogger.showError("FlatFile backend has been detected and is now deprecated; it will be changed " diff --git a/src/test/java/fr/xephi/authme/command/CommandInitializerTest.java b/src/test/java/fr/xephi/authme/command/CommandInitializerTest.java index 7c62377f..9eac4f40 100644 --- a/src/test/java/fr/xephi/authme/command/CommandInitializerTest.java +++ b/src/test/java/fr/xephi/authme/command/CommandInitializerTest.java @@ -41,6 +41,7 @@ public class CommandInitializerTest { private static Set commands; + @SuppressWarnings("unchecked") @BeforeClass public static void initializeCommandManager() { AuthMeServiceInitializer initializer = mock(AuthMeServiceInitializer.class); diff --git a/src/test/java/fr/xephi/authme/events/EventsConsistencyTest.java b/src/test/java/fr/xephi/authme/events/EventsConsistencyTest.java index d7ba5f74..0f8340d8 100644 --- a/src/test/java/fr/xephi/authme/events/EventsConsistencyTest.java +++ b/src/test/java/fr/xephi/authme/events/EventsConsistencyTest.java @@ -75,6 +75,7 @@ public class EventsConsistencyTest { return !clazz.isInterface() && !clazz.isEnum() && !Modifier.isAbstract(clazz.getModifiers()); } + @SuppressWarnings("unchecked") private static Class getEventClassFromFile(File file) { String fileName = file.getPath(); String className = fileName diff --git a/src/test/java/fr/xephi/authme/initialization/ConstructorInjectionTest.java b/src/test/java/fr/xephi/authme/initialization/ConstructorInjectionTest.java index dd21a590..df5493e7 100644 --- a/src/test/java/fr/xephi/authme/initialization/ConstructorInjectionTest.java +++ b/src/test/java/fr/xephi/authme/initialization/ConstructorInjectionTest.java @@ -20,6 +20,7 @@ import static org.junit.Assert.assertThat; */ public class ConstructorInjectionTest { + @SuppressWarnings("unchecked") @Test public void shouldReturnDependencies() { // given @@ -73,6 +74,7 @@ public class ConstructorInjectionTest { @Test public void shouldReturnNullForNoConstructorInjection() { // given / when + @SuppressWarnings("rawtypes") Injection injection = ConstructorInjection.provide(FieldInjection.class).get(); // then diff --git a/src/test/java/fr/xephi/authme/initialization/FieldInjectionTest.java b/src/test/java/fr/xephi/authme/initialization/FieldInjectionTest.java index 9d2294ae..77035e4d 100644 --- a/src/test/java/fr/xephi/authme/initialization/FieldInjectionTest.java +++ b/src/test/java/fr/xephi/authme/initialization/FieldInjectionTest.java @@ -24,6 +24,7 @@ import static org.junit.Assert.assertThat; */ public class FieldInjectionTest { + @SuppressWarnings("unchecked") @Test public void shouldReturnDependencies() { // given @@ -105,9 +106,11 @@ public class FieldInjectionTest { } private static class ThrowingConstructor { + @SuppressWarnings("unused") @Inject private ProvidedClass providedClass; + @SuppressWarnings("unused") public ThrowingConstructor() { throw new UnsupportedOperationException("Exception in constructor"); } diff --git a/src/test/java/fr/xephi/authme/initialization/samples/BadFieldInjection.java b/src/test/java/fr/xephi/authme/initialization/samples/BadFieldInjection.java index 9dc41548..7e218c4c 100644 --- a/src/test/java/fr/xephi/authme/initialization/samples/BadFieldInjection.java +++ b/src/test/java/fr/xephi/authme/initialization/samples/BadFieldInjection.java @@ -7,6 +7,7 @@ import javax.inject.Inject; */ public class BadFieldInjection { + @SuppressWarnings("unused") @Inject private AlphaService alphaService; diff --git a/src/test/java/fr/xephi/authme/initialization/samples/InvalidPostConstruct.java b/src/test/java/fr/xephi/authme/initialization/samples/InvalidPostConstruct.java index e97c4f98..23130e5e 100644 --- a/src/test/java/fr/xephi/authme/initialization/samples/InvalidPostConstruct.java +++ b/src/test/java/fr/xephi/authme/initialization/samples/InvalidPostConstruct.java @@ -9,8 +9,10 @@ import javax.inject.Inject; public abstract class InvalidPostConstruct { public static final class WithParams { + @SuppressWarnings("unused") @Inject private AlphaService alphaService; + @SuppressWarnings("unused") @Inject private ProvidedClass providedClass; diff --git a/src/test/java/fr/xephi/authme/initialization/samples/InvalidStaticFieldInjection.java b/src/test/java/fr/xephi/authme/initialization/samples/InvalidStaticFieldInjection.java index 199b2f4a..83d88b0c 100644 --- a/src/test/java/fr/xephi/authme/initialization/samples/InvalidStaticFieldInjection.java +++ b/src/test/java/fr/xephi/authme/initialization/samples/InvalidStaticFieldInjection.java @@ -7,6 +7,7 @@ import javax.inject.Inject; */ public class InvalidStaticFieldInjection { + @SuppressWarnings("unused") @Inject private ProvidedClass providedClass; @Inject diff --git a/src/test/java/tools/commands/CommandPageCreater.java b/src/test/java/tools/commands/CommandPageCreater.java index 4ba48006..ddfaf3e3 100644 --- a/src/test/java/tools/commands/CommandPageCreater.java +++ b/src/test/java/tools/commands/CommandPageCreater.java @@ -88,6 +88,7 @@ public class CommandPageCreater implements ToolTask { * * @return the initializer mock */ + @SuppressWarnings("unchecked") private static AuthMeServiceInitializer getMockInitializer() { AuthMeServiceInitializer initializer = mock(AuthMeServiceInitializer.class); when(initializer.newInstance(isA(Class.class))).thenAnswer(new Answer() { diff --git a/src/test/java/tools/hashmethods/EncryptionMethodInfoGatherer.java b/src/test/java/tools/hashmethods/EncryptionMethodInfoGatherer.java index ad2ecba4..26af6f82 100644 --- a/src/test/java/tools/hashmethods/EncryptionMethodInfoGatherer.java +++ b/src/test/java/tools/hashmethods/EncryptionMethodInfoGatherer.java @@ -137,6 +137,7 @@ public class EncryptionMethodInfoGatherer { return key.cast(map.get(key)); } + @SuppressWarnings("unchecked") private static AuthMeServiceInitializer createInitializer() { NewSetting settings = mock(NewSetting.class); // Return the default value for any property From f38d3b4545bd1da92489841a86604643daa0e7aa Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Wed, 4 May 2016 17:35:04 +0200 Subject: [PATCH 016/200] Remove deprecated method usage --- src/main/java/fr/xephi/authme/api/NewAPI.java | 2 +- .../xephi/authme/listener/AuthMePlayerListener.java | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/main/java/fr/xephi/authme/api/NewAPI.java b/src/main/java/fr/xephi/authme/api/NewAPI.java index d3a0b7d8..b359b270 100644 --- a/src/main/java/fr/xephi/authme/api/NewAPI.java +++ b/src/main/java/fr/xephi/authme/api/NewAPI.java @@ -99,7 +99,7 @@ public class NewAPI { * @return true if the player is an npc */ public boolean isNPC(Player player) { - return Utils.isNPC(player); + return plugin.getPluginHooks().isNpc(player); } /** diff --git a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java index fb44b4c3..68f72d60 100644 --- a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java +++ b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java @@ -451,14 +451,19 @@ public class AuthMePlayerListener implements Listener { @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) public void onPlayerInventoryClick(InventoryClickEvent event) { - if (event.getWhoClicked() == null) + if (event.getWhoClicked() == null) { return; - if (!(event.getWhoClicked() instanceof Player)) + } + if (!(event.getWhoClicked() instanceof Player)) { return; - if (Utils.checkAuth((Player) event.getWhoClicked())) + } + Player player = (Player) event.getWhoClicked(); + if (Utils.checkAuth(player)) { return; - if (Utils.isNPC((Player) event.getWhoClicked())) + } + if (plugin.getPluginHooks().isNpc(player)) { return; + } event.setCancelled(true); } From 1ea2f987273268334f01cd37ddf563253ef2ba07 Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Wed, 4 May 2016 18:01:47 +0200 Subject: [PATCH 017/200] Start working on #687 Next step: Future return of the async registration --- src/main/java/fr/xephi/authme/api/NewAPI.java | 18 +++++++++++++++--- .../executable/register/RegisterCommand.java | 6 +++--- .../fr/xephi/authme/process/Management.java | 4 ++-- .../authme/process/register/AsyncRegister.java | 6 ++++-- .../register/RegisterCommandTest.java | 6 +++--- 5 files changed, 27 insertions(+), 13 deletions(-) diff --git a/src/main/java/fr/xephi/authme/api/NewAPI.java b/src/main/java/fr/xephi/authme/api/NewAPI.java index 0f543762..aa9b209b 100644 --- a/src/main/java/fr/xephi/authme/api/NewAPI.java +++ b/src/main/java/fr/xephi/authme/api/NewAPI.java @@ -148,10 +148,11 @@ public class NewAPI { } /** - * Register a player with the given password. + * Register an OFFLINE/ONLINE player with the given password. * * @param playerName The player to register * @param password The password to register the player with + * * @return true if the player was registered successfully */ public boolean registerPlayer(String playerName, String password) { @@ -187,13 +188,24 @@ public class NewAPI { } /** - * Register a player with the given password. + * Force an ONLINE player to register. + * + * @param player The player to register + * @param password The password to use + * @param autoLogin Should the player be authenticated automatically after the registration? + */ + public void forceRegister(Player player, String password, boolean autoLogin) { + plugin.getManagement().performRegister(player, password, null, autoLogin); + } + + /** + * Register an ONLINE player with the given password. * * @param player The player to register * @param password The password to use */ public void forceRegister(Player player, String password) { - plugin.getManagement().performRegister(player, password, null); + forceRegister(player, password, true); } /** 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 d060c56b..37e2100a 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 @@ -23,7 +23,7 @@ public class RegisterCommand extends PlayerCommand { public void runCommand(Player player, List arguments, CommandService commandService) { if (commandService.getProperty(SecuritySettings.PASSWORD_HASH) == HashAlgorithm.TWO_FACTOR) { //for two factor auth we don't need to check the usage - commandService.getManagement().performRegister(player, "", ""); + commandService.getManagement().performRegister(player, "", "", true); return; } @@ -50,7 +50,7 @@ public class RegisterCommand extends PlayerCommand { if (commandService.getProperty(ENABLE_PASSWORD_CONFIRMATION) && !arguments.get(0).equals(arguments.get(1))) { commandService.send(player, MessageKey.PASSWORD_MATCH_ERROR); } else { - commandService.getManagement().performRegister(player, arguments.get(0), ""); + commandService.getManagement().performRegister(player, arguments.get(0), "", true); } } @@ -70,7 +70,7 @@ public class RegisterCommand extends PlayerCommand { commandService.send(player, MessageKey.USAGE_REGISTER); } else { String thePass = RandomString.generate(commandService.getProperty(RECOVERY_PASSWORD_LENGTH)); - commandService.getManagement().performRegister(player, thePass, email); + commandService.getManagement().performRegister(player, thePass, email, true); } } diff --git a/src/main/java/fr/xephi/authme/process/Management.java b/src/main/java/fr/xephi/authme/process/Management.java index 4b279c9a..42d09b8e 100644 --- a/src/main/java/fr/xephi/authme/process/Management.java +++ b/src/main/java/fr/xephi/authme/process/Management.java @@ -40,8 +40,8 @@ public class Management { runTask(new AsynchronousLogout(player, plugin, dataSource, processService)); } - public void performRegister(final Player player, final String password, final String email) { - runTask(new AsyncRegister(player, password, email, plugin, dataSource, playerCache, processService)); + public void performRegister(final Player player, final String password, final String email, final boolean autoLogin) { + runTask(new AsyncRegister(player, password, email, plugin, dataSource, playerCache, processService, autoLogin)); } public void performUnregister(final Player player, final String password, final boolean force) { diff --git a/src/main/java/fr/xephi/authme/process/register/AsyncRegister.java b/src/main/java/fr/xephi/authme/process/register/AsyncRegister.java index 5cacb105..77171cd0 100644 --- a/src/main/java/fr/xephi/authme/process/register/AsyncRegister.java +++ b/src/main/java/fr/xephi/authme/process/register/AsyncRegister.java @@ -37,9 +37,10 @@ public class AsyncRegister implements Process { private final DataSource database; private final PlayerCache playerCache; private final ProcessService service; + private final boolean autoLogin; public AsyncRegister(Player player, String password, String email, AuthMe plugin, DataSource data, - PlayerCache playerCache, ProcessService service) { + PlayerCache playerCache, ProcessService service, boolean autoLogin) { this.player = player; this.password = password; this.name = player.getName().toLowerCase(); @@ -49,6 +50,7 @@ public class AsyncRegister implements Process { this.ip = Utils.getPlayerIp(player); this.playerCache = playerCache; this.service = service; + this.autoLogin = autoLogin; } private boolean preRegisterCheck() { @@ -150,7 +152,7 @@ public class AsyncRegister implements Process { return; } - if (!Settings.forceRegLogin) { + if (!Settings.forceRegLogin && autoLogin) { //PlayerCache.getInstance().addPlayer(auth); //database.setLogged(name); // TODO: check this... diff --git a/src/test/java/fr/xephi/authme/command/executable/register/RegisterCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/register/RegisterCommandTest.java index 6f86a5f6..e5faa450 100644 --- a/src/test/java/fr/xephi/authme/command/executable/register/RegisterCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/register/RegisterCommandTest.java @@ -84,7 +84,7 @@ public class RegisterCommandTest { command.executeCommand(sender, Collections.emptyList(), commandService); // then - verify(management).performRegister(sender, "", ""); + verify(management).performRegister(sender, "", "", true); } @Test @@ -204,7 +204,7 @@ public class RegisterCommandTest { // then verify(commandService).validateEmail(playerMail); - verify(management).performRegister(eq(sender), argThat(stringWithLength(passLength)), eq(playerMail)); + verify(management).performRegister(eq(sender), argThat(stringWithLength(passLength)), eq(playerMail), true); } @Test @@ -230,7 +230,7 @@ public class RegisterCommandTest { command.executeCommand(sender, Collections.singletonList("myPass"), commandService); // then - verify(management).performRegister(sender, "myPass", ""); + verify(management).performRegister(sender, "myPass", "", true); } From 2d0bf08c401f4f9ecaf4e7e0fdc33fb3b6688ca7 Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Wed, 4 May 2016 18:10:39 +0200 Subject: [PATCH 018/200] Multiverse was injecting his spigot-api version -_- --- pom.xml | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 145d9372..0e597dc4 100644 --- a/pom.xml +++ b/pom.xml @@ -416,10 +416,10 @@ true - + - org.bukkit - bukkit + org.spigotmc + spigot-api ${bukkit.version} provided @@ -443,6 +443,10 @@ guava com.google.guava + + bungeecord-chat + net.md-5 + @@ -622,6 +626,10 @@ junit junit + + spigot-api + org.spigotmc + From 3fe2f0b6a8ae4c7b9b075b4141447d8095bc1382 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Wed, 4 May 2016 18:40:06 +0200 Subject: [PATCH 019/200] Add missing test matcher - Mock verifications need matchers on all parameters, or on none. Test fails otherwise --- .../authme/command/executable/register/RegisterCommandTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/fr/xephi/authme/command/executable/register/RegisterCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/register/RegisterCommandTest.java index e5faa450..fecc3fcc 100644 --- a/src/test/java/fr/xephi/authme/command/executable/register/RegisterCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/register/RegisterCommandTest.java @@ -204,7 +204,7 @@ public class RegisterCommandTest { // then verify(commandService).validateEmail(playerMail); - verify(management).performRegister(eq(sender), argThat(stringWithLength(passLength)), eq(playerMail), true); + verify(management).performRegister(eq(sender), argThat(stringWithLength(passLength)), eq(playerMail), eq(true)); } @Test From 917a48074a50e30d1aaf54af7ad9c68bbb6a6779 Mon Sep 17 00:00:00 2001 From: games647 Date: Thu, 5 May 2016 16:07:53 +0200 Subject: [PATCH 020/200] Try to fix Jenkins builds --- pom.xml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pom.xml b/pom.xml index 0e597dc4..655131a8 100644 --- a/pom.xml +++ b/pom.xml @@ -52,16 +52,18 @@ UTF-8 1.7 - + CUSTOM - ${pluginName}-${project.version} + ${bukkitplugin.name}-${project.version} AuthMe - ${project.groupId}.${project.artifactId}.${pluginName} + ${project.groupId}.${project.artifactId}.${bukkitplugin.name} Xephi, sgdc3, DNx5, timvisee, games647, ljacqu + ${bukkitplugin.name}-${project.version}-${project.buildNumber} + 1.9.2-R0.1-SNAPSHOT @@ -82,7 +84,7 @@ - ${project.buildNumber}-noshade + ${jarName}-noshade @@ -381,7 +383,6 @@ true - From fb4dd260f8dee391502271332a7cfd62014e18bf Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Thu, 5 May 2016 18:39:35 +0200 Subject: [PATCH 021/200] Correct pom properties --- pom.xml | 15 ++++++++------- src/main/resources/plugin.yml | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/pom.xml b/pom.xml index 655131a8..6bc976c3 100644 --- a/pom.xml +++ b/pom.xml @@ -54,16 +54,16 @@ 1.7 + AuthMe CUSTOM - ${bukkitplugin.name}-${project.version} + ${project.version}-b${project.buildNumber} + ${project.outputName}-${project.version} - AuthMe + ${project.outputName} ${project.groupId}.${project.artifactId}.${bukkitplugin.name} Xephi, sgdc3, DNx5, timvisee, games647, ljacqu - ${bukkitplugin.name}-${project.version}-${project.buildNumber} - 1.9.2-R0.1-SNAPSHOT @@ -84,7 +84,8 @@ - ${jarName}-noshade + + ${project.finalName}-noshade @@ -184,7 +185,7 @@ fr.xephi.authme - target/${jarName}-spigot.jar + target/${project.finalName}-spigot.jar @@ -223,7 +224,7 @@ fr.xephi.authme - target/${jarName}-legacy.jar + target/${project.finalName}-legacy.jar diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 0a50c74c..63a9d81c 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -3,7 +3,7 @@ authors: [${bukkitplugin.authors}] website: ${project.url} description: ${project.description} main: ${bukkitplugin.main} -version: ${project.version}-b${buildNumber} +version: ${project.versionCode} softdepend: - Vault - PermissionsBukkit From 7de4efc025e2d79d3b385bfe7270452932b9e35a Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Thu, 5 May 2016 18:46:39 +0200 Subject: [PATCH 022/200] Relocate Injection API --- pom.xml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 6bc976c3..64af80c0 100644 --- a/pom.xml +++ b/pom.xml @@ -179,6 +179,10 @@ net.ricecode.similarity fr.xephi.authme.libs.similarity + + javax.inject + fr.xephi.authme.libs.inject + org.mcstats @@ -218,6 +222,10 @@ net.ricecode.similarity fr.xephi.authme.libs.similarity + + javax.inject + fr.xephi.authme.libs.inject + org.mcstats @@ -384,8 +392,7 @@ true - - + javax.inject javax.inject From dc65a4bd3999e030f8427bed68756fac4abe76a8 Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Thu, 5 May 2016 19:16:08 +0200 Subject: [PATCH 023/200] Fix #439 --- .../java/fr/xephi/authme/permission/PermissionsManager.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/fr/xephi/authme/permission/PermissionsManager.java b/src/main/java/fr/xephi/authme/permission/PermissionsManager.java index 388eed9e..9245a6a9 100644 --- a/src/main/java/fr/xephi/authme/permission/PermissionsManager.java +++ b/src/main/java/fr/xephi/authme/permission/PermissionsManager.java @@ -555,6 +555,10 @@ public class PermissionsManager implements PermissionsService { switch (this.permsType) { case PERMISSIONS_EX: // Permissions Ex + if(!PermissionsEx.getPermissionManager().getGroupNames().contains(groupName)) { + ConsoleLogger.showError("The plugin tried to set " + player + "'s group to " + groupName + ", but it doesn't exist!"); + return false; + } PermissionUser user = PermissionsEx.getUser(player); user.addGroup(groupName); return true; From 76a1ff29b01bc42828ca4b6922e73be9b09512e0 Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Thu, 5 May 2016 19:36:26 +0200 Subject: [PATCH 024/200] Start working on #423 --- .../process/login/AsynchronousLogin.java | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java b/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java index b7bb3f2b..14ee66b8 100644 --- a/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java +++ b/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java @@ -22,6 +22,7 @@ import fr.xephi.authme.settings.properties.RegistrationSettings; import fr.xephi.authme.settings.properties.RestrictionSettings; import fr.xephi.authme.settings.properties.SecuritySettings; import fr.xephi.authme.task.MessageTask; +import fr.xephi.authme.util.BukkitService; import fr.xephi.authme.util.StringUtils; import fr.xephi.authme.util.Utils; import org.bukkit.Bukkit; @@ -213,6 +214,7 @@ public class AsynchronousLogin implements Process { } } + // TODO: allow translation! private void displayOtherAccounts(PlayerAuth auth) { if (!service.getProperty(RestrictionSettings.DISPLAY_OTHER_ACCOUNTS) || auth == null) { return; @@ -222,13 +224,21 @@ public class AsynchronousLogin implements Process { if (auths.size() < 2) { return; } - String message = "[AuthMe] " + StringUtils.join(", ", auths) + "."; + // TODO: color player names with green if the account is online + String message = StringUtils.join(", ", auths) + "."; + + ConsoleLogger.info("The user " + player.getName() + " has " + auths.size() + " accounts:"); + ConsoleLogger.info(message); + for (Player player : service.getOnlinePlayers()) { - if (plugin.getPermissionsManager().hasPermission(player, AdminPermission.SEE_OTHER_ACCOUNTS) - || (player.getName().equals(this.player.getName()) - && plugin.getPermissionsManager().hasPermission(player, PlayerPermission.SEE_OWN_ACCOUNTS))) { - player.sendMessage("[AuthMe] The player " + auth.getNickname() + " has " + auths.size() + " accounts"); + if ((player.getName().equalsIgnoreCase(this.player.getName()) && plugin.getPermissionsManager().hasPermission(player, PlayerPermission.SEE_OWN_ACCOUNTS))) { + player.sendMessage("You own " + auths.size() + " accounts:"); player.sendMessage(message); + return; + } else if (plugin.getPermissionsManager().hasPermission(player, AdminPermission.SEE_OTHER_ACCOUNTS)) { + player.sendMessage("The user " + player.getName() + " has " + auths.size() + " accounts:"); + player.sendMessage(message); + return; } } } From 09e2845cea0f3b308ae5aaeb8f36b780c82f34c8 Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Thu, 5 May 2016 19:43:12 +0200 Subject: [PATCH 025/200] Delete unused Profiler --- .../java/fr/xephi/authme/util/Profiler.java | 136 ------------------ 1 file changed, 136 deletions(-) delete mode 100644 src/main/java/fr/xephi/authme/util/Profiler.java diff --git a/src/main/java/fr/xephi/authme/util/Profiler.java b/src/main/java/fr/xephi/authme/util/Profiler.java deleted file mode 100644 index 73bdf5e4..00000000 --- a/src/main/java/fr/xephi/authme/util/Profiler.java +++ /dev/null @@ -1,136 +0,0 @@ -package fr.xephi.authme.util; - -import java.text.DecimalFormat; - -public class Profiler { - - /** - * Defines the past time in milliseconds. - */ - private long time = 0; - - /** - * Defines the time in milliseconds the profiler last started at. - */ - private long start = -1; - - /** - * Constructor. This won't start the profiler immediately. - */ - public Profiler() { - this(false); - } - - /** - * Constructor. - * - * @param start True to immediately start the profiler. - */ - public Profiler(boolean start) { - // Should the timer be started - if (start) - start(); - } - - /** - * Start the profiler. - * - * @return True if the profiler was started, false otherwise possibly due to an error. - * True will also be returned if the profiler was started already. - */ - public boolean start() { - // Make sure the timer isn't started already - if (isActive()) - return true; - - // Set the start time - this.start = System.currentTimeMillis(); - return true; - } - - /** - * This will start the profiler if it's not active, or will stop the profiler if it's currently active. - * - * @return True if the profiler has been started, false if the profiler has been stopped. - */ - public boolean pause() { - // Toggle the profiler state - if (isStarted()) - stop(); - else - start(); - - // Return the result - return isStarted(); - } - - /** - * Stop the profiler if it's active. - * - * @return True will be returned if the profiler was stopped while it was active. False will be returned if the - * profiler was stopped already. - */ - public boolean stop() { - // Make sure the profiler is active - if (!isActive()) - return false; - - // Stop the profiler, calculate the passed time - this.time += System.currentTimeMillis() - this.start; - this.start = -1; - return true; - } - - /** - * Check whether the profiler has been started. The profiler doesn't need to be active right now. - * - * @return True if the profiler was started, false otherwise. - */ - public boolean isStarted() { - return isActive() || this.time > 0; - } - - /** - * Check whether the profiler is currently active. - * - * @return True if the profiler is active, false otherwise. - */ - public boolean isActive() { - return this.start >= 0; - } - - /** - * Get the passed time in milliseconds. - * - * @return The passed time in milliseconds. - */ - public long getTime() { - // Check whether the profiler is currently active - if (isActive()) - return this.time + (System.currentTimeMillis() - this.start); - return this.time; - } - - /** - * Get the passed time in a formatted string. - * - * @return The passed time in a formatted string. - */ - public String getTimeFormatted() { - // Get the passed time - long time = getTime(); - - // Return the time if it's less than one millisecond - if (time <= 0) - return "<1 ms"; - - // Return the time in milliseconds - if (time < 1000) - return time + " ms"; - - // Convert the time into seconds with a single decimal - double timeSeconds = ((double) time) / 1000; - DecimalFormat df = new DecimalFormat("#0.0"); - return df.format(timeSeconds) + " s"; - } -} From 084cdd0d3a3bce66746a77f0551f2efa6826eb49 Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Thu, 5 May 2016 20:20:40 +0200 Subject: [PATCH 026/200] Allow tool execution with argument --- src/test/java/tools/ToolsRunner.java | 24 ++++++++++++++----- .../permissions/PermissionsListWriter.java | 5 ++++ 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/test/java/tools/ToolsRunner.java b/src/test/java/tools/ToolsRunner.java index 54e66b19..90dff736 100644 --- a/src/test/java/tools/ToolsRunner.java +++ b/src/test/java/tools/ToolsRunner.java @@ -29,16 +29,28 @@ public final class ToolsRunner { File toolsFolder = new File(ToolsConstants.TOOLS_SOURCE_ROOT); Map tasks = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); collectTasksInDirectory(toolsFolder, tasks); - listAllTasks(tasks); - // Prompt user for task and handle input - System.out.println("Please enter the task to run:"); + String inputTask; Scanner scanner = new Scanner(System.in); - String inputTask = scanner.nextLine(); - ToolTask task = tasks.get(inputTask); + boolean interactive = true; + + if(args == null || args.length == 0) { + listAllTasks(tasks); + // Prompt user for task and handle input + System.out.println("Please enter the task to run:"); + inputTask = scanner.nextLine(); + } else { + interactive = false; + inputTask = args[0]; + } + ToolTask task = tasks.get(inputTask); if (task != null) { - task.execute(scanner); + if(interactive) { + task.execute(scanner); + } else { + task.execute(null); + } } else { System.out.println("Unknown task"); } diff --git a/src/test/java/tools/permissions/PermissionsListWriter.java b/src/test/java/tools/permissions/PermissionsListWriter.java index 79522d9d..0e54a383 100644 --- a/src/test/java/tools/permissions/PermissionsListWriter.java +++ b/src/test/java/tools/permissions/PermissionsListWriter.java @@ -25,6 +25,11 @@ public class PermissionsListWriter implements ToolTask { @Override public void execute(Scanner scanner) { + if(scanner == null) { + generateAndWriteFile(); + return; + } + // Ask if result should be written to file System.out.println("Include description? [Enter 'n' for no]"); boolean includeDescription = !matches("n", scanner); From 5cfb556fda42cf0c666bbfd3bd485067407b9896 Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Thu, 5 May 2016 22:38:17 +0200 Subject: [PATCH 027/200] #513 Doesn't work @ljacqu Could you please check why this isn't working? run the "mvn exec:java" command --- pom.xml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/pom.xml b/pom.xml index 64af80c0..f5fdf394 100644 --- a/pom.xml +++ b/pom.xml @@ -237,6 +237,28 @@ + + + org.codehaus.mojo + exec-maven-plugin + 1.4.0 + + test + ${project.basedir}/target/test-classes + tools.ToolsRunner + true + + + org.jacoco From 3e6223dc5aa85855026fbd3d12f494c2fedfeb92 Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Thu, 5 May 2016 23:01:06 +0200 Subject: [PATCH 028/200] I'm so stupid --- .../java/fr/xephi/authme/process/login/AsynchronousLogin.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java b/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java index 14ee66b8..da98a05b 100644 --- a/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java +++ b/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java @@ -234,11 +234,9 @@ public class AsynchronousLogin implements Process { if ((player.getName().equalsIgnoreCase(this.player.getName()) && plugin.getPermissionsManager().hasPermission(player, PlayerPermission.SEE_OWN_ACCOUNTS))) { player.sendMessage("You own " + auths.size() + " accounts:"); player.sendMessage(message); - return; } else if (plugin.getPermissionsManager().hasPermission(player, AdminPermission.SEE_OTHER_ACCOUNTS)) { player.sendMessage("The user " + player.getName() + " has " + auths.size() + " accounts:"); player.sendMessage(message); - return; } } } From 5e5836f1676a910407c927f32b72b254ae9d308d Mon Sep 17 00:00:00 2001 From: ljacqu Date: Sun, 8 May 2016 11:15:56 +0200 Subject: [PATCH 029/200] #432 Injector improvements - Separate FieldInjection from default fallback for no-Inject public no-args constructor classes - Make CommandInitializer a normal, instantiable service - Add various injections instead of fetching through command service --- src/main/java/fr/xephi/authme/AuthMe.java | 3 - .../xephi/authme/command/CommandHandler.java | 7 +- .../authme/command/CommandInitializer.java | 27 ++++-- .../xephi/authme/command/CommandMapper.java | 5 +- .../xephi/authme/command/CommandService.java | 60 ++----------- .../executable/authme/ConverterCommand.java | 19 +++-- .../executable/authme/FirstSpawnCommand.java | 9 +- .../executable/authme/ForceLoginCommand.java | 13 ++- .../authme/PurgeBannedPlayersCommand.java | 12 ++- .../executable/authme/PurgeCommand.java | 12 ++- .../executable/authme/ReloadCommand.java | 5 +- .../authme/SetFirstSpawnCommand.java | 7 +- .../executable/authme/SetSpawnCommand.java | 7 +- .../executable/authme/SpawnCommand.java | 9 +- .../authme/UnregisterAdminCommand.java | 8 +- .../executable/captcha/CaptchaCommand.java | 9 +- .../executable/email/AddEmailCommand.java | 7 +- .../executable/email/ChangeEmailCommand.java | 7 +- .../executable/email/RecoverEmailCommand.java | 4 +- .../executable/login/LoginCommand.java | 7 +- .../executable/logout/LogoutCommand.java | 7 +- .../executable/register/RegisterCommand.java | 11 ++- .../unregister/UnregisterCommand.java | 7 +- .../AuthMeServiceInitializer.java | 3 +- .../authme/initialization/BaseCommands.java | 14 ---- .../authme/initialization/FieldInjection.java | 4 +- .../initialization/InstantiationFallback.java | 84 +++++++++++++++++++ .../listener/AuthMePlayerListener19.java | 11 ++- .../authme/listener/AuthMeServerListener.java | 7 +- .../authme/command/CommandHandlerTest.java | 25 +++--- .../command/CommandInitializerTest.java | 6 +- .../authme/command/CommandMapperTest.java | 4 +- .../authme/command/CommandServiceTest.java | 48 ----------- .../authme/FirstSpawnCommandTest.java | 23 +++-- .../authme/ForceLoginCommandTest.java | 35 ++++---- .../executable/authme/ReloadCommandTest.java | 24 +++--- .../authme/SetFirstSpawnCommandTest.java | 27 +++--- .../authme/SetSpawnCommandTest.java | 27 +++--- .../executable/authme/SpawnCommandTest.java | 23 +++-- .../executable/email/AddEmailCommandTest.java | 18 ++-- .../email/ChangeEmailCommandTest.java | 26 +++--- .../executable/login/LoginCommandTest.java | 16 ++-- .../executable/logout/LogoutCommandTest.java | 21 +++-- .../register/RegisterCommandTest.java | 29 ++----- .../AuthMeServiceInitializerTest.java | 13 +++ .../ConstructorInjectionTest.java | 5 +- .../initialization/FieldInjectionTest.java | 15 ++++ .../InstantiationFallbackTest.java | 68 +++++++++++++++ .../samples/InstantiationFallbackClasses.java | 45 ++++++++++ .../tools/commands/CommandPageCreater.java | 3 +- 50 files changed, 554 insertions(+), 332 deletions(-) delete mode 100644 src/main/java/fr/xephi/authme/initialization/BaseCommands.java create mode 100644 src/main/java/fr/xephi/authme/initialization/InstantiationFallback.java create mode 100644 src/test/java/fr/xephi/authme/initialization/InstantiationFallbackTest.java create mode 100644 src/test/java/fr/xephi/authme/initialization/samples/InstantiationFallbackClasses.java diff --git a/src/main/java/fr/xephi/authme/AuthMe.java b/src/main/java/fr/xephi/authme/AuthMe.java index 647484cd..cc0fcaa5 100644 --- a/src/main/java/fr/xephi/authme/AuthMe.java +++ b/src/main/java/fr/xephi/authme/AuthMe.java @@ -8,7 +8,6 @@ import fr.xephi.authme.cache.backup.JsonCache; import fr.xephi.authme.cache.limbo.LimboCache; import fr.xephi.authme.cache.limbo.LimboPlayer; import fr.xephi.authme.command.CommandHandler; -import fr.xephi.authme.command.CommandInitializer; import fr.xephi.authme.datasource.CacheDataSource; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.datasource.DataSourceType; @@ -18,7 +17,6 @@ import fr.xephi.authme.datasource.SQLite; import fr.xephi.authme.hooks.BungeeCordMessage; import fr.xephi.authme.hooks.PluginHooks; import fr.xephi.authme.initialization.AuthMeServiceInitializer; -import fr.xephi.authme.initialization.BaseCommands; import fr.xephi.authme.initialization.DataFolder; import fr.xephi.authme.initialization.MetricsStarter; import fr.xephi.authme.listener.AuthMeBlockListener; @@ -261,7 +259,6 @@ public class AuthMe extends JavaPlugin { // Some statically injected things initializer.register(PlayerCache.class, PlayerCache.getInstance()); initializer.register(LimboCache.class, LimboCache.getInstance()); - initializer.provide(BaseCommands.class, CommandInitializer.buildCommands(initializer)); permsMan = initializer.get(PermissionsManager.class); bukkitService = initializer.get(BukkitService.class); diff --git a/src/main/java/fr/xephi/authme/command/CommandHandler.java b/src/main/java/fr/xephi/authme/command/CommandHandler.java index 5f7b32af..2fdd722d 100644 --- a/src/main/java/fr/xephi/authme/command/CommandHandler.java +++ b/src/main/java/fr/xephi/authme/command/CommandHandler.java @@ -2,6 +2,7 @@ package fr.xephi.authme.command; import fr.xephi.authme.AuthMe; import fr.xephi.authme.command.help.HelpProvider; +import fr.xephi.authme.permission.PermissionsManager; import fr.xephi.authme.util.StringUtils; import org.bukkit.ChatColor; import org.bukkit.command.CommandSender; @@ -23,6 +24,7 @@ public class CommandHandler { private static final double SUGGEST_COMMAND_THRESHOLD = 0.75; private final CommandService commandService; + private final PermissionsManager permissionsManager; /** * Create a command handler. @@ -30,8 +32,9 @@ public class CommandHandler { * @param commandService The CommandService instance */ @Inject - public CommandHandler(CommandService commandService) { + public CommandHandler(CommandService commandService, PermissionsManager permissionsManager) { this.commandService = commandService; + this.permissionsManager = permissionsManager; } /** @@ -126,7 +129,7 @@ public class CommandHandler { private void sendImproperArgumentsMessage(CommandSender sender, FoundCommandResult result) { CommandDescription command = result.getCommandDescription(); - if (!commandService.getPermissionsManager().hasPermission(sender, command)) { + if (!permissionsManager.hasPermission(sender, command)) { sendPermissionDeniedError(sender); return; } diff --git a/src/main/java/fr/xephi/authme/command/CommandInitializer.java b/src/main/java/fr/xephi/authme/command/CommandInitializer.java index 14d72f4a..ca4cec3b 100644 --- a/src/main/java/fr/xephi/authme/command/CommandInitializer.java +++ b/src/main/java/fr/xephi/authme/command/CommandInitializer.java @@ -37,6 +37,7 @@ import fr.xephi.authme.initialization.AuthMeServiceInitializer; import fr.xephi.authme.permission.AdminPermission; import fr.xephi.authme.permission.PlayerPermission; +import javax.inject.Inject; import java.util.Arrays; import java.util.Collection; import java.util.List; @@ -48,13 +49,23 @@ import static fr.xephi.authme.permission.DefaultPermission.OP_ONLY; /** * Initializes all available AuthMe commands. */ -public final class CommandInitializer { +public class CommandInitializer { - private CommandInitializer() { - // Helper class + private AuthMeServiceInitializer initializer; + + private Set commands; + + @Inject + public CommandInitializer(AuthMeServiceInitializer initializer) { + this.initializer = initializer; + buildCommands(); } - public static Set buildCommands(AuthMeServiceInitializer initializer) { + public Set getCommands() { + return commands; + } + + private void buildCommands() { // Register the base AuthMe Reloaded command final CommandDescription AUTHME_BASE = CommandDescription.builder() .labels("authme") @@ -402,18 +413,16 @@ public final class CommandInitializer { EMAIL_BASE, CAPTCHA_BASE); - setHelpOnAllBases(baseCommands, initializer); - return baseCommands; + setHelpOnAllBases(baseCommands); + commands = baseCommands; } /** * Set the help command on all base commands, e.g. to register /authme help or /register help. * * @param commands The list of base commands to register a help child command on - * @param initializer The service initializer */ - private static void setHelpOnAllBases(Collection commands, - AuthMeServiceInitializer initializer) { + private void setHelpOnAllBases(Collection commands) { final HelpCommand helpCommandExecutable = initializer.newInstance(HelpCommand.class); final List helpCommandLabels = Arrays.asList("help", "hlp", "h", "sos", "?"); diff --git a/src/main/java/fr/xephi/authme/command/CommandMapper.java b/src/main/java/fr/xephi/authme/command/CommandMapper.java index 18473b28..2bb597a0 100644 --- a/src/main/java/fr/xephi/authme/command/CommandMapper.java +++ b/src/main/java/fr/xephi/authme/command/CommandMapper.java @@ -1,7 +1,6 @@ package fr.xephi.authme.command; import fr.xephi.authme.command.executable.HelpCommand; -import fr.xephi.authme.initialization.BaseCommands; import fr.xephi.authme.permission.PermissionsManager; import fr.xephi.authme.util.CollectionUtils; import fr.xephi.authme.util.StringUtils; @@ -31,8 +30,8 @@ public class CommandMapper { private final PermissionsManager permissionsManager; @Inject - public CommandMapper(@BaseCommands Set baseCommands, PermissionsManager permissionsManager) { - this.baseCommands = baseCommands; + public CommandMapper(CommandInitializer commandInitializer, PermissionsManager permissionsManager) { + this.baseCommands = commandInitializer.getCommands(); this.permissionsManager = permissionsManager; } diff --git a/src/main/java/fr/xephi/authme/command/CommandService.java b/src/main/java/fr/xephi/authme/command/CommandService.java index f88196cc..1e51303d 100644 --- a/src/main/java/fr/xephi/authme/command/CommandService.java +++ b/src/main/java/fr/xephi/authme/command/CommandService.java @@ -1,20 +1,16 @@ package fr.xephi.authme.command; -import fr.xephi.authme.AuthMe; import fr.xephi.authme.command.help.HelpProvider; -import fr.xephi.authme.hooks.PluginHooks; import fr.xephi.authme.output.MessageKey; 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.NewSetting; -import fr.xephi.authme.settings.SpawnLoader; import fr.xephi.authme.settings.domain.Property; import fr.xephi.authme.util.BukkitService; import fr.xephi.authme.util.ValidationService; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; +import org.bukkit.scheduler.BukkitScheduler; +import org.bukkit.scheduler.BukkitTask; import javax.inject.Inject; import java.util.Collection; @@ -27,7 +23,7 @@ import java.util.List; public class CommandService { @Inject - private AuthMe authMe; + private BukkitScheduler scheduler; @Inject private Messages messages; @Inject @@ -36,16 +32,8 @@ public class CommandService { private CommandMapper commandMapper; @SuppressWarnings("unused") @Inject - private PasswordSecurity passwordSecurity; - @Inject - private PermissionsManager permissionsManager; - @Inject private NewSetting settings; @Inject - private PluginHooks pluginHooks; - @Inject - private SpawnLoader spawnLoader; - @Inject private ValidationService validationService; @Inject private BukkitService bukkitService; @@ -86,19 +74,12 @@ public class CommandService { * Run the given task asynchronously with the Bukkit scheduler. * * @param task The task to run + * @return a BukkitTask that contains the id number + * @throws IllegalArgumentException if plugin is null + * @throws IllegalArgumentException if task is null */ - public void runTaskAsynchronously(Runnable task) { - authMe.getServer().getScheduler().runTaskAsynchronously(authMe, task); - } - - /** - * Return the AuthMe instance for further manipulation. Use only if other methods from - * the command service cannot be used. - * - * @return The AuthMe instance - */ - public AuthMe getAuthMe() { - return authMe; + public BukkitTask runTaskAsynchronously(Runnable task) { + return bukkitService.runTaskAsynchronously(task); } /** @@ -115,24 +96,6 @@ public class CommandService { } } - /** - * Return the management instance of the plugin. - * - * @return The Management instance linked to the AuthMe instance - */ - public Management getManagement() { - return authMe.getManagement(); - } - - /** - * Return the permissions manager. - * - * @return the permissions manager - */ - public PermissionsManager getPermissionsManager() { - return permissionsManager; - } - /** * Retrieve a message by its message key. * @@ -163,13 +126,6 @@ public class CommandService { return settings; } - public PluginHooks getPluginHooks() { - return pluginHooks; - } - - public SpawnLoader getSpawnLoader() { - return spawnLoader; - } /** * Verifies whether a password is valid according to the plugin settings. diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/ConverterCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/ConverterCommand.java index e3414480..f4a13640 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/ConverterCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/ConverterCommand.java @@ -13,15 +13,16 @@ import fr.xephi.authme.converter.xAuthConverter; import fr.xephi.authme.output.MessageKey; import org.bukkit.command.CommandSender; +import javax.inject.Inject; import java.util.List; public class ConverterCommand implements ExecutableCommand { + @Inject + private AuthMe authMe; + @Override public void executeCommand(CommandSender sender, List arguments, CommandService commandService) { - // AuthMe plugin instance - final AuthMe plugin = AuthMe.getInstance(); - // Get the conversion job String job = arguments.get(0); @@ -36,22 +37,22 @@ public class ConverterCommand implements ExecutableCommand { Converter converter = null; switch (jobType) { case XAUTH: - converter = new xAuthConverter(plugin, sender); + converter = new xAuthConverter(authMe, sender); break; case CRAZYLOGIN: - converter = new CrazyLoginConverter(plugin, sender); + converter = new CrazyLoginConverter(authMe, sender); break; case RAKAMAK: - converter = new RakamakConverter(plugin, sender); + converter = new RakamakConverter(authMe, sender); break; case ROYALAUTH: - converter = new RoyalAuthConverter(plugin); + converter = new RoyalAuthConverter(authMe); break; case VAUTH: - converter = new vAuthConverter(plugin, sender); + converter = new vAuthConverter(authMe, sender); break; case SQLITETOSQL: - converter = new SqliteToSql(plugin, sender, commandService.getSettings()); + converter = new SqliteToSql(authMe, sender, commandService.getSettings()); break; default: break; diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/FirstSpawnCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/FirstSpawnCommand.java index 50b3b169..9cb3ca9b 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/FirstSpawnCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/FirstSpawnCommand.java @@ -2,8 +2,10 @@ package fr.xephi.authme.command.executable.authme; import fr.xephi.authme.command.CommandService; import fr.xephi.authme.command.PlayerCommand; +import fr.xephi.authme.settings.SpawnLoader; import org.bukkit.entity.Player; +import javax.inject.Inject; import java.util.List; /** @@ -11,10 +13,13 @@ import java.util.List; */ public class FirstSpawnCommand extends PlayerCommand { + @Inject + private SpawnLoader spawnLoader; + @Override public void runCommand(Player player, List arguments, CommandService commandService) { - if (commandService.getSpawnLoader().getFirstSpawn() != null) { - player.teleport(commandService.getSpawnLoader().getFirstSpawn()); + if (spawnLoader.getFirstSpawn() != null) { + player.teleport(spawnLoader.getFirstSpawn()); } else { player.sendMessage("[AuthMe] First spawn has failed, please try to define the first spawn"); } diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/ForceLoginCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/ForceLoginCommand.java index c974e7ea..3535cf6f 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/ForceLoginCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/ForceLoginCommand.java @@ -2,9 +2,12 @@ package fr.xephi.authme.command.executable.authme; import fr.xephi.authme.command.CommandService; import fr.xephi.authme.command.ExecutableCommand; +import fr.xephi.authme.permission.PermissionsManager; +import fr.xephi.authme.process.Management; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; +import javax.inject.Inject; import java.util.List; import static fr.xephi.authme.permission.PlayerPermission.CAN_LOGIN_BE_FORCED; @@ -14,6 +17,12 @@ import static fr.xephi.authme.permission.PlayerPermission.CAN_LOGIN_BE_FORCED; */ public class ForceLoginCommand implements ExecutableCommand { + @Inject + private PermissionsManager permissionsManager; + + @Inject + private Management management; + @Override public void executeCommand(CommandSender sender, List arguments, CommandService commandService) { // Get the player query @@ -22,10 +31,10 @@ public class ForceLoginCommand implements ExecutableCommand { Player player = commandService.getPlayer(playerName); if (player == null || !player.isOnline()) { sender.sendMessage("Player needs to be online!"); - } else if (!commandService.getPermissionsManager().hasPermission(player, CAN_LOGIN_BE_FORCED)) { + } else if (!permissionsManager.hasPermission(player, CAN_LOGIN_BE_FORCED)) { sender.sendMessage("You cannot force login the player " + playerName + "!"); } else { - commandService.getManagement().performLogin(player, "dontneed", true); + management.performLogin(player, "dontneed", true); sender.sendMessage("Force login for " + playerName + " performed!"); } } diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/PurgeBannedPlayersCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/PurgeBannedPlayersCommand.java index 99535027..1cedecd8 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/PurgeBannedPlayersCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/PurgeBannedPlayersCommand.java @@ -4,6 +4,7 @@ import fr.xephi.authme.AuthMe; import fr.xephi.authme.command.CommandService; import fr.xephi.authme.command.ExecutableCommand; import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.hooks.PluginHooks; import fr.xephi.authme.settings.properties.PurgeSettings; import org.bukkit.OfflinePlayer; import org.bukkit.command.CommandSender; @@ -21,11 +22,14 @@ public class PurgeBannedPlayersCommand implements ExecutableCommand { @Inject private DataSource dataSource; + @Inject + private PluginHooks pluginHooks; + + @Inject + private AuthMe plugin; + @Override public void executeCommand(CommandSender sender, List arguments, CommandService commandService) { - // AuthMe plugin instance - final AuthMe plugin = commandService.getAuthMe(); - // Get the list of banned players List bannedPlayers = new ArrayList<>(); for (OfflinePlayer offlinePlayer : commandService.getBukkitService().getBannedPlayers()) { @@ -35,7 +39,7 @@ public class PurgeBannedPlayersCommand implements ExecutableCommand { // Purge the banned players dataSource.purgeBanned(bannedPlayers); if (commandService.getProperty(PurgeSettings.REMOVE_ESSENTIALS_FILES) - && commandService.getPluginHooks().isEssentialsAvailable()) + && pluginHooks.isEssentialsAvailable()) plugin.dataManager.purgeEssentials(bannedPlayers); if (commandService.getProperty(PurgeSettings.REMOVE_PLAYER_DAT)) plugin.dataManager.purgeDat(bannedPlayers); diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/PurgeCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/PurgeCommand.java index 51ca6df1..afc04c5d 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/PurgeCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/PurgeCommand.java @@ -4,6 +4,7 @@ import fr.xephi.authme.AuthMe; import fr.xephi.authme.command.CommandService; import fr.xephi.authme.command.ExecutableCommand; import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.hooks.PluginHooks; import fr.xephi.authme.settings.properties.PurgeSettings; import org.bukkit.ChatColor; import org.bukkit.command.CommandSender; @@ -23,6 +24,12 @@ public class PurgeCommand implements ExecutableCommand { @Inject private DataSource dataSource; + @Inject + private PluginHooks pluginHooks; + + @Inject + private AuthMe plugin; + @Override public void executeCommand(CommandSender sender, List arguments, CommandService commandService) { // Get the days parameter @@ -56,9 +63,8 @@ public class PurgeCommand implements ExecutableCommand { sender.sendMessage(ChatColor.GOLD + "Deleted " + purged.size() + " user accounts"); // Purge other data - AuthMe plugin = commandService.getAuthMe(); - if (commandService.getProperty(PurgeSettings.REMOVE_ESSENTIALS_FILES) && - commandService.getPluginHooks().isEssentialsAvailable()) + if (commandService.getProperty(PurgeSettings.REMOVE_ESSENTIALS_FILES) + && pluginHooks.isEssentialsAvailable()) plugin.dataManager.purgeEssentials(purged); if (commandService.getProperty(PurgeSettings.REMOVE_PLAYER_DAT)) plugin.dataManager.purgeDat(purged); diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/ReloadCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/ReloadCommand.java index 91df5832..3c3bc921 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/ReloadCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/ReloadCommand.java @@ -7,6 +7,7 @@ import fr.xephi.authme.command.ExecutableCommand; import fr.xephi.authme.output.MessageKey; import org.bukkit.command.CommandSender; +import javax.inject.Inject; import java.util.List; /** @@ -14,9 +15,11 @@ import java.util.List; */ public class ReloadCommand implements ExecutableCommand { + @Inject + private AuthMe plugin; + @Override public void executeCommand(CommandSender sender, List arguments, CommandService commandService) { - AuthMe plugin = commandService.getAuthMe(); try { plugin.reload(); commandService.send(sender, MessageKey.CONFIG_RELOAD_SUCCESS); diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/SetFirstSpawnCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/SetFirstSpawnCommand.java index a3cc7d45..5d4b6995 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/SetFirstSpawnCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/SetFirstSpawnCommand.java @@ -2,15 +2,20 @@ package fr.xephi.authme.command.executable.authme; import fr.xephi.authme.command.CommandService; import fr.xephi.authme.command.PlayerCommand; +import fr.xephi.authme.settings.SpawnLoader; import org.bukkit.entity.Player; +import javax.inject.Inject; import java.util.List; public class SetFirstSpawnCommand extends PlayerCommand { + @Inject + private SpawnLoader spawnLoader; + @Override public void runCommand(Player player, List arguments, CommandService commandService) { - if (commandService.getSpawnLoader().setFirstSpawn(player.getLocation())) { + if (spawnLoader.setFirstSpawn(player.getLocation())) { player.sendMessage("[AuthMe] Correctly defined new first spawn point"); } else { player.sendMessage("[AuthMe] SetFirstSpawn has failed, please retry"); diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/SetSpawnCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/SetSpawnCommand.java index 3201ea4c..18cdf13b 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/SetSpawnCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/SetSpawnCommand.java @@ -2,15 +2,20 @@ package fr.xephi.authme.command.executable.authme; import fr.xephi.authme.command.CommandService; import fr.xephi.authme.command.PlayerCommand; +import fr.xephi.authme.settings.SpawnLoader; import org.bukkit.entity.Player; +import javax.inject.Inject; import java.util.List; public class SetSpawnCommand extends PlayerCommand { + @Inject + private SpawnLoader spawnLoader; + @Override public void runCommand(Player player, List arguments, CommandService commandService) { - if (commandService.getSpawnLoader().setSpawn(player.getLocation())) { + if (spawnLoader.setSpawn(player.getLocation())) { player.sendMessage("[AuthMe] Correctly defined new spawn point"); } else { player.sendMessage("[AuthMe] SetSpawn has failed, please retry"); diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/SpawnCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/SpawnCommand.java index 04f8e563..8c142d25 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/SpawnCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/SpawnCommand.java @@ -2,16 +2,21 @@ package fr.xephi.authme.command.executable.authme; import fr.xephi.authme.command.CommandService; import fr.xephi.authme.command.PlayerCommand; +import fr.xephi.authme.settings.SpawnLoader; import org.bukkit.entity.Player; +import javax.inject.Inject; import java.util.List; public class SpawnCommand extends PlayerCommand { + @Inject + private SpawnLoader spawnLoader; + @Override public void runCommand(Player player, List arguments, CommandService commandService) { - if (commandService.getSpawnLoader().getSpawn() != null) { - player.teleport(commandService.getSpawnLoader().getSpawn()); + if (spawnLoader.getSpawn() != null) { + player.teleport(spawnLoader.getSpawn()); } else { player.sendMessage("[AuthMe] Spawn has failed, please try to define the spawn"); } diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/UnregisterAdminCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/UnregisterAdminCommand.java index c69f9aff..e8f38c73 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/UnregisterAdminCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/UnregisterAdminCommand.java @@ -36,6 +36,9 @@ public class UnregisterAdminCommand implements ExecutableCommand { @Inject private PlayerCache playerCache; + @Inject + private AuthMe authMe; + @Override public void executeCommand(final CommandSender sender, List arguments, CommandService commandService) { // Get the player name @@ -79,7 +82,6 @@ public class UnregisterAdminCommand implements ExecutableCommand { * @param service the command service */ private void applyUnregisteredEffectsAndTasks(Player target, CommandService service) { - final AuthMe plugin = service.getAuthMe(); final BukkitService bukkitService = service.getBukkitService(); final String playerNameLowerCase = target.getName().toLowerCase(); @@ -88,11 +90,11 @@ public class UnregisterAdminCommand implements ExecutableCommand { int timeOut = service.getProperty(RestrictionSettings.TIMEOUT) * TICKS_PER_SECOND; int interval = service.getProperty(RegistrationSettings.MESSAGE_INTERVAL); if (timeOut != 0) { - BukkitTask id = bukkitService.runTaskLater(new TimeoutTask(plugin, playerNameLowerCase, target), timeOut); + BukkitTask id = bukkitService.runTaskLater(new TimeoutTask(authMe, playerNameLowerCase, target), timeOut); LimboCache.getInstance().getLimboPlayer(playerNameLowerCase).setTimeoutTask(id); } LimboCache.getInstance().getLimboPlayer(playerNameLowerCase).setMessageTask( - bukkitService.runTask(new MessageTask(service.getBukkitService(), plugin.getMessages(), + bukkitService.runTask(new MessageTask(service.getBukkitService(), authMe.getMessages(), playerNameLowerCase, MessageKey.REGISTER_MESSAGE, interval))); if (service.getProperty(RegistrationSettings.APPLY_BLIND_EFFECT)) { 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 aba0a950..ebb44f80 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 @@ -9,16 +9,21 @@ import fr.xephi.authme.security.RandomString; import fr.xephi.authme.settings.properties.SecuritySettings; import org.bukkit.entity.Player; +import javax.inject.Inject; import java.util.List; public class CaptchaCommand extends PlayerCommand { + @Inject + private AuthMe plugin; + + @Inject + private PlayerCache playerCache; + @Override public void runCommand(Player player, List arguments, CommandService commandService) { final String playerNameLowerCase = player.getName().toLowerCase(); final String captcha = arguments.get(0); - final AuthMe plugin = commandService.getAuthMe(); - PlayerCache playerCache = PlayerCache.getInstance(); // Command logic if (playerCache.isAuthenticated(playerNameLowerCase)) { diff --git a/src/main/java/fr/xephi/authme/command/executable/email/AddEmailCommand.java b/src/main/java/fr/xephi/authme/command/executable/email/AddEmailCommand.java index cd92ebf4..6d9b384d 100644 --- a/src/main/java/fr/xephi/authme/command/executable/email/AddEmailCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/email/AddEmailCommand.java @@ -3,8 +3,10 @@ package fr.xephi.authme.command.executable.email; import fr.xephi.authme.command.CommandService; import fr.xephi.authme.command.PlayerCommand; import fr.xephi.authme.output.MessageKey; +import fr.xephi.authme.process.Management; import org.bukkit.entity.Player; +import javax.inject.Inject; import java.util.List; /** @@ -12,6 +14,9 @@ import java.util.List; */ public class AddEmailCommand extends PlayerCommand { + @Inject + private Management management; + @Override public void runCommand(Player player, List arguments, CommandService commandService) { String email = arguments.get(0); @@ -19,7 +24,7 @@ public class AddEmailCommand extends PlayerCommand { if (email.equals(emailConfirmation)) { // Closer inspection of the mail address handled by the async task - commandService.getManagement().performAddEmail(player, email); + management.performAddEmail(player, email); } else { commandService.send(player, MessageKey.CONFIRM_EMAIL_MESSAGE); } diff --git a/src/main/java/fr/xephi/authme/command/executable/email/ChangeEmailCommand.java b/src/main/java/fr/xephi/authme/command/executable/email/ChangeEmailCommand.java index 6d7281ac..333f795e 100644 --- a/src/main/java/fr/xephi/authme/command/executable/email/ChangeEmailCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/email/ChangeEmailCommand.java @@ -2,8 +2,10 @@ package fr.xephi.authme.command.executable.email; import fr.xephi.authme.command.CommandService; import fr.xephi.authme.command.PlayerCommand; +import fr.xephi.authme.process.Management; import org.bukkit.entity.Player; +import javax.inject.Inject; import java.util.List; /** @@ -11,11 +13,14 @@ import java.util.List; */ public class ChangeEmailCommand extends PlayerCommand { + @Inject + private Management management; + @Override public void runCommand(Player player, List arguments, CommandService commandService) { String playerMailOld = arguments.get(0); String playerMailNew = arguments.get(1); - commandService.getManagement().performChangeEmail(player, playerMailOld, playerMailNew); + management.performChangeEmail(player, playerMailOld, playerMailNew); } } diff --git a/src/main/java/fr/xephi/authme/command/executable/email/RecoverEmailCommand.java b/src/main/java/fr/xephi/authme/command/executable/email/RecoverEmailCommand.java index 56fb5e5c..aba08584 100644 --- a/src/main/java/fr/xephi/authme/command/executable/email/RecoverEmailCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/email/RecoverEmailCommand.java @@ -29,11 +29,13 @@ public class RecoverEmailCommand extends PlayerCommand { @Inject private PlayerCache playerCache; + @Inject + private AuthMe plugin; + @Override public void runCommand(Player player, List arguments, CommandService commandService) { final String playerMail = arguments.get(0); final String playerName = player.getName(); - final AuthMe plugin = commandService.getAuthMe(); if (plugin.mail == null) { ConsoleLogger.showError("Mail API is not set"); diff --git a/src/main/java/fr/xephi/authme/command/executable/login/LoginCommand.java b/src/main/java/fr/xephi/authme/command/executable/login/LoginCommand.java index 5689ec63..d10ef91a 100644 --- a/src/main/java/fr/xephi/authme/command/executable/login/LoginCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/login/LoginCommand.java @@ -2,8 +2,10 @@ package fr.xephi.authme.command.executable.login; import fr.xephi.authme.command.CommandService; import fr.xephi.authme.command.PlayerCommand; +import fr.xephi.authme.process.Management; import org.bukkit.entity.Player; +import javax.inject.Inject; import java.util.List; /** @@ -11,9 +13,12 @@ import java.util.List; */ public class LoginCommand extends PlayerCommand { + @Inject + private Management management; + @Override public void runCommand(Player player, List arguments, CommandService commandService) { final String password = arguments.get(0); - commandService.getManagement().performLogin(player, password, false); + management.performLogin(player, password, false); } } diff --git a/src/main/java/fr/xephi/authme/command/executable/logout/LogoutCommand.java b/src/main/java/fr/xephi/authme/command/executable/logout/LogoutCommand.java index 598d1738..2236f125 100644 --- a/src/main/java/fr/xephi/authme/command/executable/logout/LogoutCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/logout/LogoutCommand.java @@ -2,8 +2,10 @@ package fr.xephi.authme.command.executable.logout; import fr.xephi.authme.command.CommandService; import fr.xephi.authme.command.PlayerCommand; +import fr.xephi.authme.process.Management; import org.bukkit.entity.Player; +import javax.inject.Inject; import java.util.List; /** @@ -11,8 +13,11 @@ import java.util.List; */ public class LogoutCommand extends PlayerCommand { + @Inject + private Management management; + @Override public void runCommand(Player player, List arguments, CommandService commandService) { - commandService.getManagement().performLogout(player); + management.performLogout(player); } } 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 37e2100a..54f054e5 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 @@ -4,12 +4,14 @@ import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.command.CommandService; import fr.xephi.authme.command.PlayerCommand; import fr.xephi.authme.output.MessageKey; +import fr.xephi.authme.process.Management; import fr.xephi.authme.security.HashAlgorithm; import fr.xephi.authme.security.RandomString; import fr.xephi.authme.settings.properties.EmailSettings; import fr.xephi.authme.settings.properties.SecuritySettings; import org.bukkit.entity.Player; +import javax.inject.Inject; import java.util.List; import static fr.xephi.authme.settings.properties.EmailSettings.RECOVERY_PASSWORD_LENGTH; @@ -19,11 +21,14 @@ import static fr.xephi.authme.settings.properties.RestrictionSettings.ENABLE_PAS public class RegisterCommand extends PlayerCommand { + @Inject + private Management management; + @Override public void runCommand(Player player, List arguments, CommandService commandService) { if (commandService.getProperty(SecuritySettings.PASSWORD_HASH) == HashAlgorithm.TWO_FACTOR) { //for two factor auth we don't need to check the usage - commandService.getManagement().performRegister(player, "", "", true); + management.performRegister(player, "", "", true); return; } @@ -50,7 +55,7 @@ public class RegisterCommand extends PlayerCommand { if (commandService.getProperty(ENABLE_PASSWORD_CONFIRMATION) && !arguments.get(0).equals(arguments.get(1))) { commandService.send(player, MessageKey.PASSWORD_MATCH_ERROR); } else { - commandService.getManagement().performRegister(player, arguments.get(0), "", true); + management.performRegister(player, arguments.get(0), "", true); } } @@ -70,7 +75,7 @@ public class RegisterCommand extends PlayerCommand { commandService.send(player, MessageKey.USAGE_REGISTER); } else { String thePass = RandomString.generate(commandService.getProperty(RECOVERY_PASSWORD_LENGTH)); - commandService.getManagement().performRegister(player, thePass, email, true); + management.performRegister(player, thePass, email, true); } } diff --git a/src/main/java/fr/xephi/authme/command/executable/unregister/UnregisterCommand.java b/src/main/java/fr/xephi/authme/command/executable/unregister/UnregisterCommand.java index 195f659e..2d0a5bde 100644 --- a/src/main/java/fr/xephi/authme/command/executable/unregister/UnregisterCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/unregister/UnregisterCommand.java @@ -4,12 +4,17 @@ 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.process.Management; import org.bukkit.entity.Player; +import javax.inject.Inject; import java.util.List; public class UnregisterCommand extends PlayerCommand { + @Inject + private Management management; + @Override public void runCommand(Player player, List arguments, CommandService commandService) { String playerPass = arguments.get(0); @@ -22,6 +27,6 @@ public class UnregisterCommand extends PlayerCommand { } // Unregister the player - commandService.getManagement().performUnregister(player, playerPass, false); + management.performUnregister(player, playerPass, false); } } diff --git a/src/main/java/fr/xephi/authme/initialization/AuthMeServiceInitializer.java b/src/main/java/fr/xephi/authme/initialization/AuthMeServiceInitializer.java index c2241c11..72c2169c 100644 --- a/src/main/java/fr/xephi/authme/initialization/AuthMeServiceInitializer.java +++ b/src/main/java/fr/xephi/authme/initialization/AuthMeServiceInitializer.java @@ -132,7 +132,8 @@ public class AuthMeServiceInitializer { * @return the instantiated object */ private T instantiate(Class clazz, Set> traversedClasses) { - Injection injection = firstNotNull(ConstructorInjection.provide(clazz), FieldInjection.provide(clazz)); + Injection injection = firstNotNull( + ConstructorInjection.provide(clazz), FieldInjection.provide(clazz), InstantiationFallback.provide(clazz)); if (injection == null) { throw new IllegalStateException("Did not find injection method for " + clazz + ". Make sure you have " + "a constructor with @Inject or fields with @Inject. Fields with @Inject require " diff --git a/src/main/java/fr/xephi/authme/initialization/BaseCommands.java b/src/main/java/fr/xephi/authme/initialization/BaseCommands.java deleted file mode 100644 index 8ff263ab..00000000 --- a/src/main/java/fr/xephi/authme/initialization/BaseCommands.java +++ /dev/null @@ -1,14 +0,0 @@ -package fr.xephi.authme.initialization; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Annotation to denote the collection of AuthMe commands. - */ -@Target({ElementType.PARAMETER, ElementType.FIELD}) -@Retention(RetentionPolicy.RUNTIME) -public @interface BaseCommands { -} diff --git a/src/main/java/fr/xephi/authme/initialization/FieldInjection.java b/src/main/java/fr/xephi/authme/initialization/FieldInjection.java index 1e7973e2..b8112b87 100644 --- a/src/main/java/fr/xephi/authme/initialization/FieldInjection.java +++ b/src/main/java/fr/xephi/authme/initialization/FieldInjection.java @@ -74,7 +74,7 @@ class FieldInjection implements Injection { * * @param clazz the class to provide field injection for * @param the class' type - * @return field injection provider for the given class + * @return field injection provider for the given class, or null if not applicable */ public static Provider> provide(final Class clazz) { return new Provider>() { @@ -85,7 +85,7 @@ class FieldInjection implements Injection { return null; } List fields = getInjectionFields(clazz); - return fields == null ? null : new FieldInjection<>(constructor, fields); + return fields.isEmpty() ? null : new FieldInjection<>(constructor, fields); } }; } diff --git a/src/main/java/fr/xephi/authme/initialization/InstantiationFallback.java b/src/main/java/fr/xephi/authme/initialization/InstantiationFallback.java new file mode 100644 index 00000000..f7017ce1 --- /dev/null +++ b/src/main/java/fr/xephi/authme/initialization/InstantiationFallback.java @@ -0,0 +1,84 @@ +package fr.xephi.authme.initialization; + +import javax.inject.Inject; +import javax.inject.Provider; +import java.lang.reflect.AccessibleObject; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; + +/** + * Fallback instantiation method for classes with an accessible no-args constructor + * and no no {@link Inject} annotations whatsoever. + */ +public class InstantiationFallback implements Injection { + + private final Constructor constructor; + + private InstantiationFallback(Constructor constructor) { + this.constructor = constructor; + } + + @Override + public Class[] getDependencies() { + return new Class[0]; + } + + @Override + public Class[] getDependencyAnnotations() { + return new Class[0]; + } + + @Override + public T instantiateWith(Object... values) { + if (values == null || values.length > 0) { + throw new UnsupportedOperationException("Instantiation fallback cannot have parameters"); + } + try { + return constructor.newInstance(); + } catch (InvocationTargetException | IllegalAccessException | InstantiationException e) { + throw new UnsupportedOperationException(e); + } + } + + /** + * Returns an instantiation fallback if the class is applicable. + * + * @param clazz the class + * @param the class' type + * @return instantiation fallback provider for the given class, or null if not applicable + */ + public static Provider> provide(final Class clazz) { + return new Provider>() { + @Override + public InstantiationFallback get() { + Constructor noArgsConstructor = getNoArgsConstructor(clazz); + // Return fallback only if we have no args constructor and no @Inject annotation anywhere + if (noArgsConstructor != null + && !isInjectAnnotationPresent(clazz.getDeclaredConstructors()) + && !isInjectAnnotationPresent(clazz.getDeclaredFields()) + && !isInjectAnnotationPresent(clazz.getDeclaredMethods())) { + return new InstantiationFallback<>(noArgsConstructor); + } + return null; + } + }; + } + + private static Constructor getNoArgsConstructor(Class clazz) { + try { + // Note ljacqu 20160504: getConstructor(), unlike getDeclaredConstructor(), only considers public members + return clazz.getConstructor(); + } catch (NoSuchMethodException e) { + return null; + } + } + + private static boolean isInjectAnnotationPresent(A[] accessibles) { + for (A accessible : accessibles) { + if (accessible.isAnnotationPresent(Inject.class)) { + return true; + } + } + return false; + } +} diff --git a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener19.java b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener19.java index 8be48ba2..f5a20277 100644 --- a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener19.java +++ b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener19.java @@ -1,12 +1,11 @@ package fr.xephi.authme.listener; +import fr.xephi.authme.settings.SpawnLoader; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; import org.spigotmc.event.player.PlayerSpawnLocationEvent; -import fr.xephi.authme.AuthMe; - import javax.inject.Inject; /** @@ -15,13 +14,13 @@ import javax.inject.Inject; public class AuthMePlayerListener19 implements Listener { @Inject - private AuthMe plugin; - - public AuthMePlayerListener19() { } + private SpawnLoader spawnLoader; + + AuthMePlayerListener19() { } @EventHandler(priority = EventPriority.LOWEST) public void onPlayerSpawn(PlayerSpawnLocationEvent event) { - event.setSpawnLocation(plugin.getSpawnLocation(event.getPlayer())); + event.setSpawnLocation(spawnLoader.getSpawnLocation(event.getPlayer())); } } diff --git a/src/main/java/fr/xephi/authme/listener/AuthMeServerListener.java b/src/main/java/fr/xephi/authme/listener/AuthMeServerListener.java index bb85374d..0e3162b8 100644 --- a/src/main/java/fr/xephi/authme/listener/AuthMeServerListener.java +++ b/src/main/java/fr/xephi/authme/listener/AuthMeServerListener.java @@ -5,6 +5,7 @@ import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.hooks.PluginHooks; import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.output.Messages; +import fr.xephi.authme.permission.PermissionsManager; import fr.xephi.authme.settings.NewSetting; import fr.xephi.authme.settings.SpawnLoader; import fr.xephi.authme.settings.properties.ProtectionSettings; @@ -34,6 +35,8 @@ public class AuthMeServerListener implements Listener { private SpawnLoader spawnLoader; @Inject private ValidationService validationService; + @Inject + private PermissionsManager permissionsManager; @EventHandler(priority = EventPriority.HIGHEST) public void onServerPing(ServerListPingEvent event) { @@ -53,7 +56,7 @@ public class AuthMeServerListener implements Listener { } // Call the onPluginDisable method in the permissions manager - plugin.getPermissionsManager().onPluginDisable(event); + permissionsManager.onPluginDisable(event); final String pluginName = event.getPlugin().getName(); if ("Essentials".equalsIgnoreCase(pluginName)) { @@ -86,7 +89,7 @@ public class AuthMeServerListener implements Listener { } // Call the onPluginEnable method in the permissions manager - plugin.getPermissionsManager().onPluginEnable(event); + permissionsManager.onPluginEnable(event); final String pluginName = event.getPlugin().getName(); if ("Essentials".equalsIgnoreCase(pluginName)) { diff --git a/src/test/java/fr/xephi/authme/command/CommandHandlerTest.java b/src/test/java/fr/xephi/authme/command/CommandHandlerTest.java index e5e6b749..914fe195 100644 --- a/src/test/java/fr/xephi/authme/command/CommandHandlerTest.java +++ b/src/test/java/fr/xephi/authme/command/CommandHandlerTest.java @@ -2,11 +2,13 @@ package fr.xephi.authme.command; import fr.xephi.authme.permission.PermissionsManager; import org.bukkit.command.CommandSender; -import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; -import org.mockito.MockitoAnnotations; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; import java.util.Collections; import java.util.List; @@ -35,23 +37,24 @@ import static org.mockito.Mockito.verify; /** * Test for {@link CommandHandler}. */ -@SuppressWarnings("ArraysAsListWithZeroOrOneArgument") // Justification: It's more readable to use asList() everywhere in the test when we often generated two lists where one // often consists of only one element, e.g. myMethod(asList("authme"), asList("my", "args"), ...) +@SuppressWarnings("ArraysAsListWithZeroOrOneArgument") +@RunWith(MockitoJUnitRunner.class) public class CommandHandlerTest { + @InjectMocks private CommandHandler handler; + + @Mock private CommandService serviceMock; + @Mock + private PermissionsManager permissionsManager; + @Captor private ArgumentCaptor> captor; - @Before - public void setUpCommandHandler() { - MockitoAnnotations.initMocks(this); - serviceMock = mock(CommandService.class); - handler = new CommandHandler(serviceMock); - } @Test public void shouldCallMappedCommandWithArgs() { @@ -109,9 +112,7 @@ public class CommandHandlerTest { 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); @@ -135,9 +136,7 @@ public class CommandHandlerTest { 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); diff --git a/src/test/java/fr/xephi/authme/command/CommandInitializerTest.java b/src/test/java/fr/xephi/authme/command/CommandInitializerTest.java index 9eac4f40..786ea477 100644 --- a/src/test/java/fr/xephi/authme/command/CommandInitializerTest.java +++ b/src/test/java/fr/xephi/authme/command/CommandInitializerTest.java @@ -43,7 +43,7 @@ public class CommandInitializerTest { @SuppressWarnings("unchecked") @BeforeClass - public static void initializeCommandManager() { + public static void initializeCommandCollection() { AuthMeServiceInitializer initializer = mock(AuthMeServiceInitializer.class); when(initializer.newInstance(any(Class.class))).thenAnswer(new Answer() { @Override @@ -52,8 +52,8 @@ public class CommandInitializerTest { return mock(clazz); } }); - - commands = CommandInitializer.buildCommands(initializer); + CommandInitializer commandInitializer = new CommandInitializer(initializer); + commands = commandInitializer.getCommands(); } @Test diff --git a/src/test/java/fr/xephi/authme/command/CommandMapperTest.java b/src/test/java/fr/xephi/authme/command/CommandMapperTest.java index 77840e89..ebcd0b64 100644 --- a/src/test/java/fr/xephi/authme/command/CommandMapperTest.java +++ b/src/test/java/fr/xephi/authme/command/CommandMapperTest.java @@ -41,7 +41,9 @@ public class CommandMapperTest { @Before public void setUpMocks() { permissionsManager = mock(PermissionsManager.class); - mapper = new CommandMapper(commands, permissionsManager); + CommandInitializer initializer = mock(CommandInitializer.class); + given(initializer.getCommands()).willReturn(commands); + mapper = new CommandMapper(initializer, permissionsManager); } // ----------- diff --git a/src/test/java/fr/xephi/authme/command/CommandServiceTest.java b/src/test/java/fr/xephi/authme/command/CommandServiceTest.java index 76685fce..996197e6 100644 --- a/src/test/java/fr/xephi/authme/command/CommandServiceTest.java +++ b/src/test/java/fr/xephi/authme/command/CommandServiceTest.java @@ -1,15 +1,9 @@ package fr.xephi.authme.command; -import fr.xephi.authme.AuthMe; import fr.xephi.authme.command.help.HelpProvider; -import fr.xephi.authme.hooks.PluginHooks; import fr.xephi.authme.output.MessageKey; 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.NewSetting; -import fr.xephi.authme.settings.SpawnLoader; import fr.xephi.authme.settings.domain.Property; import fr.xephi.authme.settings.properties.SecuritySettings; import fr.xephi.authme.util.BukkitService; @@ -42,24 +36,14 @@ public class CommandServiceTest { @InjectMocks private CommandService commandService; @Mock - private AuthMe authMe; - @Mock private CommandMapper commandMapper; @Mock private HelpProvider helpProvider; @Mock private Messages messages; @Mock - private PasswordSecurity passwordSecurity; - @Mock - private PermissionsManager permissionsManager; - @Mock private NewSetting settings; @Mock - private PluginHooks pluginHooks; - @Mock - private SpawnLoader spawnLoader; - @Mock private ValidationService validationService; @Mock private BukkitService bukkitService; @@ -122,29 +106,6 @@ public class CommandServiceTest { assertThat(captor.getAllValues(), equalTo(messages)); } - @Test - public void shouldReturnManagementObject() { - // given - Management management = mock(Management.class); - given(authMe.getManagement()).willReturn(management); - - // when - Management result = commandService.getManagement(); - - // then - assertThat(result, equalTo(management)); - verify(authMe).getManagement(); - } - - @Test - public void shouldReturnPermissionsManager() { - // given / when - PermissionsManager result = commandService.getPermissionsManager(); - - // then - assertThat(result, equalTo(permissionsManager)); - } - @Test public void shouldRetrieveMessage() { // given @@ -183,15 +144,6 @@ public class CommandServiceTest { assertThat(result, equalTo(settings)); } - @Test - public void shouldReturnAuthMe() { - // given/when - AuthMe result = commandService.getAuthMe(); - - // then - assertThat(result, equalTo(authMe)); - } - @Test public void shouldValidatePassword() { // given diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/FirstSpawnCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/FirstSpawnCommandTest.java index 0c70bd01..42edaeb2 100644 --- a/src/test/java/fr/xephi/authme/command/executable/authme/FirstSpawnCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/authme/FirstSpawnCommandTest.java @@ -1,11 +1,14 @@ package fr.xephi.authme.command.executable.authme; import fr.xephi.authme.command.CommandService; -import fr.xephi.authme.command.ExecutableCommand; import fr.xephi.authme.settings.SpawnLoader; import org.bukkit.Location; import org.bukkit.entity.Player; import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; import java.util.Collections; @@ -21,18 +24,24 @@ import static org.mockito.Mockito.verify; /** * Test for {@link FirstSpawnCommand}. */ +@RunWith(MockitoJUnitRunner.class) public class FirstSpawnCommandTest { + @InjectMocks + private FirstSpawnCommand command; + + @Mock + private SpawnLoader spawnLoader; + + @Mock + private CommandService service; + @Test public void shouldTeleportToFirstSpawn() { // given Location firstSpawn = mock(Location.class); - SpawnLoader spawnLoader = mock(SpawnLoader.class); given(spawnLoader.getFirstSpawn()).willReturn(firstSpawn); - CommandService service = mock(CommandService.class); - given(service.getSpawnLoader()).willReturn(spawnLoader); Player player = mock(Player.class); - ExecutableCommand command = new FirstSpawnCommand(); // when command.executeCommand(player, Collections.emptyList(), service); @@ -45,12 +54,8 @@ public class FirstSpawnCommandTest { @Test public void shouldHandleMissingFirstSpawn() { // given - SpawnLoader spawnLoader = mock(SpawnLoader.class); given(spawnLoader.getFirstSpawn()).willReturn(null); - CommandService service = mock(CommandService.class); - given(service.getSpawnLoader()).willReturn(spawnLoader); Player player = mock(Player.class); - ExecutableCommand command = new FirstSpawnCommand(); // when command.executeCommand(player, Collections.emptyList(), service); diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/ForceLoginCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/ForceLoginCommandTest.java index f6a01f2d..87819e33 100644 --- a/src/test/java/fr/xephi/authme/command/executable/authme/ForceLoginCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/authme/ForceLoginCommandTest.java @@ -9,6 +9,7 @@ import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; @@ -21,8 +22,8 @@ import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.argThat; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; /** * Test for {@link ForceLoginCommand}. @@ -30,6 +31,15 @@ import static org.mockito.Mockito.verify; @RunWith(MockitoJUnitRunner.class) public class ForceLoginCommandTest { + @InjectMocks + private ForceLoginCommand command; + + @Mock + private Management management; + + @Mock + private PermissionsManager permissionsManager; + @Mock private CommandService commandService; @@ -40,7 +50,6 @@ public class ForceLoginCommandTest { Player player = mockPlayer(false, playerName); given(commandService.getPlayer(playerName)).willReturn(player); CommandSender sender = mock(CommandSender.class); - ExecutableCommand command = new ForceLoginCommand(); // when command.executeCommand(sender, Collections.singletonList(playerName), commandService); @@ -48,7 +57,7 @@ public class ForceLoginCommandTest { // then verify(commandService).getPlayer(playerName); verify(sender).sendMessage(argThat(equalTo("Player needs to be online!"))); - verify(commandService, never()).getManagement(); + verifyZeroInteractions(management); } @Test @@ -65,7 +74,7 @@ public class ForceLoginCommandTest { // then verify(commandService).getPlayer(playerName); verify(sender).sendMessage(argThat(equalTo("Player needs to be online!"))); - verify(commandService, never()).getManagement(); + verifyZeroInteractions(management); } @Test @@ -74,12 +83,8 @@ public class ForceLoginCommandTest { String playerName = "testTest"; Player player = mockPlayer(true, playerName); given(commandService.getPlayer(playerName)).willReturn(player); - PermissionsManager permissionsManager = mock(PermissionsManager.class); given(permissionsManager.hasPermission(player, PlayerPermission.CAN_LOGIN_BE_FORCED)).willReturn(false); - given(commandService.getPermissionsManager()).willReturn(permissionsManager); - CommandSender sender = mock(CommandSender.class); - ExecutableCommand command = new ForceLoginCommand(); // when command.executeCommand(sender, Collections.singletonList(playerName), commandService); @@ -87,7 +92,7 @@ public class ForceLoginCommandTest { // then verify(commandService).getPlayer(playerName); verify(sender).sendMessage(argThat(containsString("You cannot force login the player"))); - verify(commandService, never()).getManagement(); + verifyZeroInteractions(management); } @Test @@ -96,14 +101,8 @@ public class ForceLoginCommandTest { String playerName = "tester23"; Player player = mockPlayer(true, playerName); given(commandService.getPlayer(playerName)).willReturn(player); - PermissionsManager permissionsManager = mock(PermissionsManager.class); given(permissionsManager.hasPermission(player, PlayerPermission.CAN_LOGIN_BE_FORCED)).willReturn(true); - given(commandService.getPermissionsManager()).willReturn(permissionsManager); - Management management = mock(Management.class); - given(commandService.getManagement()).willReturn(management); - CommandSender sender = mock(CommandSender.class); - ExecutableCommand command = new ForceLoginCommand(); // when command.executeCommand(sender, Collections.singletonList(playerName), commandService); @@ -119,15 +118,9 @@ public class ForceLoginCommandTest { String senderName = "tester23"; Player player = mockPlayer(true, senderName); given(commandService.getPlayer(senderName)).willReturn(player); - PermissionsManager permissionsManager = mock(PermissionsManager.class); given(permissionsManager.hasPermission(player, PlayerPermission.CAN_LOGIN_BE_FORCED)).willReturn(true); - given(commandService.getPermissionsManager()).willReturn(permissionsManager); - Management management = mock(Management.class); - given(commandService.getManagement()).willReturn(management); - CommandSender sender = mock(CommandSender.class); given(sender.getName()).willReturn(senderName); - ExecutableCommand command = new ForceLoginCommand(); // when command.executeCommand(sender, Collections.emptyList(), commandService); diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/ReloadCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/ReloadCommandTest.java index 99600c89..42623496 100644 --- a/src/test/java/fr/xephi/authme/command/executable/authme/ReloadCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/authme/ReloadCommandTest.java @@ -3,15 +3,17 @@ package fr.xephi.authme.command.executable.authme; import fr.xephi.authme.AuthMe; import fr.xephi.authme.TestHelper; import fr.xephi.authme.command.CommandService; -import fr.xephi.authme.command.ExecutableCommand; import fr.xephi.authme.output.MessageKey; import org.bukkit.command.CommandSender; import org.junit.BeforeClass; import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; import java.util.Collections; -import static org.mockito.BDDMockito.given; import static org.mockito.Matchers.matches; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; @@ -20,8 +22,18 @@ import static org.mockito.Mockito.verify; /** * Test for {@link ReloadCommand}. */ +@RunWith(MockitoJUnitRunner.class) public class ReloadCommandTest { + @InjectMocks + private ReloadCommand command; + + @Mock + private AuthMe authMe; + + @Mock + private CommandService service; + @BeforeClass public static void setUpLogger() { TestHelper.setupLogger(); @@ -30,11 +42,7 @@ public class ReloadCommandTest { @Test public void shouldReload() throws Exception { // given - AuthMe authMe = mock(AuthMe.class); - CommandService service = mock(CommandService.class); - given(service.getAuthMe()).willReturn(authMe); CommandSender sender = mock(CommandSender.class); - ExecutableCommand command = new ReloadCommand(); // when command.executeCommand(sender, Collections.emptyList(), service); @@ -47,12 +55,8 @@ public class ReloadCommandTest { @Test public void shouldHandleReloadError() throws Exception { // given - AuthMe authMe = mock(AuthMe.class); doThrow(IllegalStateException.class).when(authMe).reload(); - CommandService service = mock(CommandService.class); - given(service.getAuthMe()).willReturn(authMe); CommandSender sender = mock(CommandSender.class); - ExecutableCommand command = new ReloadCommand(); // when command.executeCommand(sender, Collections.emptyList(), service); diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/SetFirstSpawnCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/SetFirstSpawnCommandTest.java index 74964882..17163a3a 100644 --- a/src/test/java/fr/xephi/authme/command/executable/authme/SetFirstSpawnCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/authme/SetFirstSpawnCommandTest.java @@ -1,11 +1,14 @@ package fr.xephi.authme.command.executable.authme; import fr.xephi.authme.command.CommandService; -import fr.xephi.authme.command.ExecutableCommand; import fr.xephi.authme.settings.SpawnLoader; import org.bukkit.Location; import org.bukkit.entity.Player; import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; import java.util.Collections; @@ -18,21 +21,25 @@ import static org.mockito.Mockito.verify; /** * Test for {@link SetFirstSpawnCommand}. */ +@RunWith(MockitoJUnitRunner.class) public class SetFirstSpawnCommandTest { + @InjectMocks + private SetFirstSpawnCommand command; + + @Mock + private SpawnLoader spawnLoader; + + @Mock + private CommandService service; + @Test public void shouldSetFirstSpawn() { // given Player player = mock(Player.class); Location location = mock(Location.class); given(player.getLocation()).willReturn(location); - - SpawnLoader spawnLoader = mock(SpawnLoader.class); given(spawnLoader.setFirstSpawn(location)).willReturn(true); - CommandService service = mock(CommandService.class); - given(service.getSpawnLoader()).willReturn(spawnLoader); - - ExecutableCommand command = new SetFirstSpawnCommand(); // when command.executeCommand(player, Collections.emptyList(), service); @@ -48,13 +55,7 @@ public class SetFirstSpawnCommandTest { Player player = mock(Player.class); Location location = mock(Location.class); given(player.getLocation()).willReturn(location); - - SpawnLoader spawnLoader = mock(SpawnLoader.class); given(spawnLoader.setFirstSpawn(location)).willReturn(false); - CommandService service = mock(CommandService.class); - given(service.getSpawnLoader()).willReturn(spawnLoader); - - ExecutableCommand command = new SetFirstSpawnCommand(); // when command.executeCommand(player, Collections.emptyList(), service); diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/SetSpawnCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/SetSpawnCommandTest.java index e3e01f99..77e916d9 100644 --- a/src/test/java/fr/xephi/authme/command/executable/authme/SetSpawnCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/authme/SetSpawnCommandTest.java @@ -1,11 +1,14 @@ package fr.xephi.authme.command.executable.authme; import fr.xephi.authme.command.CommandService; -import fr.xephi.authme.command.ExecutableCommand; import fr.xephi.authme.settings.SpawnLoader; import org.bukkit.Location; import org.bukkit.entity.Player; import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; import java.util.Collections; @@ -18,21 +21,25 @@ import static org.mockito.Mockito.verify; /** * Test for {@link SetSpawnCommand}. */ +@RunWith(MockitoJUnitRunner.class) public class SetSpawnCommandTest { + @InjectMocks + private SetSpawnCommand command; + + @Mock + private SpawnLoader spawnLoader; + + @Mock + private CommandService service; + @Test public void shouldSetSpawn() { // given Player player = mock(Player.class); Location location = mock(Location.class); given(player.getLocation()).willReturn(location); - - SpawnLoader spawnLoader = mock(SpawnLoader.class); given(spawnLoader.setSpawn(location)).willReturn(true); - CommandService service = mock(CommandService.class); - given(service.getSpawnLoader()).willReturn(spawnLoader); - - ExecutableCommand command = new SetSpawnCommand(); // when command.executeCommand(player, Collections.emptyList(), service); @@ -48,13 +55,7 @@ public class SetSpawnCommandTest { Player player = mock(Player.class); Location location = mock(Location.class); given(player.getLocation()).willReturn(location); - - SpawnLoader spawnLoader = mock(SpawnLoader.class); given(spawnLoader.setSpawn(location)).willReturn(false); - CommandService service = mock(CommandService.class); - given(service.getSpawnLoader()).willReturn(spawnLoader); - - ExecutableCommand command = new SetSpawnCommand(); // when command.executeCommand(player, Collections.emptyList(), service); diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/SpawnCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/SpawnCommandTest.java index 799d1ca4..64d006e2 100644 --- a/src/test/java/fr/xephi/authme/command/executable/authme/SpawnCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/authme/SpawnCommandTest.java @@ -1,11 +1,14 @@ package fr.xephi.authme.command.executable.authme; import fr.xephi.authme.command.CommandService; -import fr.xephi.authme.command.ExecutableCommand; import fr.xephi.authme.settings.SpawnLoader; import org.bukkit.Location; import org.bukkit.entity.Player; import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; import java.util.Collections; @@ -21,18 +24,24 @@ import static org.mockito.Mockito.verify; /** * Test for {@link SpawnCommand}. */ +@RunWith(MockitoJUnitRunner.class) public class SpawnCommandTest { + @InjectMocks + private SpawnCommand command; + + @Mock + private SpawnLoader spawnLoader; + + @Mock + private CommandService service; + @Test public void shouldTeleportToSpawn() { // given Location spawn = mock(Location.class); - SpawnLoader spawnLoader = mock(SpawnLoader.class); given(spawnLoader.getSpawn()).willReturn(spawn); - CommandService service = mock(CommandService.class); - given(service.getSpawnLoader()).willReturn(spawnLoader); Player player = mock(Player.class); - ExecutableCommand command = new SpawnCommand(); // when command.executeCommand(player, Collections.emptyList(), service); @@ -45,12 +54,8 @@ public class SpawnCommandTest { @Test public void shouldHandleMissingSpawn() { // given - SpawnLoader spawnLoader = mock(SpawnLoader.class); given(spawnLoader.getSpawn()).willReturn(null); - CommandService service = mock(CommandService.class); - given(service.getSpawnLoader()).willReturn(spawnLoader); Player player = mock(Player.class); - ExecutableCommand command = new SpawnCommand(); // when command.executeCommand(player, Collections.emptyList(), service); diff --git a/src/test/java/fr/xephi/authme/command/executable/email/AddEmailCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/email/AddEmailCommandTest.java index 1ea41355..9b91ee5b 100644 --- a/src/test/java/fr/xephi/authme/command/executable/email/AddEmailCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/email/AddEmailCommandTest.java @@ -8,6 +8,7 @@ import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; @@ -16,8 +17,8 @@ import java.util.Arrays; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; /** * Test for {@link AddEmailCommand}. @@ -25,20 +26,25 @@ import static org.mockito.Mockito.verify; @RunWith(MockitoJUnitRunner.class) public class AddEmailCommandTest { + @InjectMocks + private AddEmailCommand command; + @Mock private CommandService commandService; + @Mock + private Management management; + @Test public void shouldRejectNonPlayerSender() { // given CommandSender sender = mock(BlockCommandSender.class); - AddEmailCommand command = new AddEmailCommand(); // when command.executeCommand(sender, new ArrayList(), commandService); // then - verify(commandService, never()).getManagement(); + verifyZeroInteractions(management); } @Test @@ -47,9 +53,6 @@ public class AddEmailCommandTest { Player sender = mock(Player.class); String email = "mail@example"; given(commandService.validateEmail(email)).willReturn(true); - Management management = mock(Management.class); - given(commandService.getManagement()).willReturn(management); - AddEmailCommand command = new AddEmailCommand(); // when command.executeCommand(sender, Arrays.asList(email, email), commandService); @@ -64,13 +67,12 @@ public class AddEmailCommandTest { Player sender = mock(Player.class); String email = "asdfasdf@example.com"; given(commandService.validateEmail(email)).willReturn(true); - AddEmailCommand command = new AddEmailCommand(); // when command.executeCommand(sender, Arrays.asList(email, "wrongConf"), commandService); // then - verify(commandService, never()).getManagement(); + verifyZeroInteractions(management); verify(commandService).send(sender, MessageKey.CONFIRM_EMAIL_MESSAGE); } diff --git a/src/test/java/fr/xephi/authme/command/executable/email/ChangeEmailCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/email/ChangeEmailCommandTest.java index 7e6d2fb9..d9f7e0cd 100644 --- a/src/test/java/fr/xephi/authme/command/executable/email/ChangeEmailCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/email/ChangeEmailCommandTest.java @@ -5,49 +5,51 @@ import fr.xephi.authme.process.Management; import org.bukkit.command.BlockCommandSender; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; -import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; import java.util.ArrayList; import java.util.Arrays; -import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; /** * Test for {@link ChangeEmailCommand}. */ +@RunWith(MockitoJUnitRunner.class) public class ChangeEmailCommandTest { + @InjectMocks + private ChangeEmailCommand command; + + @Mock + private Management management; + + @Mock private CommandService commandService; - @Before - public void setUpMocks() { - commandService = mock(CommandService.class); - } @Test public void shouldRejectNonPlayerSender() { // given CommandSender sender = mock(BlockCommandSender.class); - ChangeEmailCommand command = new ChangeEmailCommand(); // when command.executeCommand(sender, new ArrayList(), commandService); // then - verify(commandService, never()).getManagement(); + verifyZeroInteractions(management); } @Test public void shouldForwardData() { // given Player sender = mock(Player.class); - ChangeEmailCommand command = new ChangeEmailCommand(); - Management management = mock(Management.class); - given(commandService.getManagement()).willReturn(management); // when command.executeCommand(sender, Arrays.asList("new.mail@example.org", "old_mail@example.org"), commandService); diff --git a/src/test/java/fr/xephi/authme/command/executable/login/LoginCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/login/LoginCommandTest.java index 9a23bc17..2eede4a1 100644 --- a/src/test/java/fr/xephi/authme/command/executable/login/LoginCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/login/LoginCommandTest.java @@ -7,6 +7,7 @@ import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; @@ -14,12 +15,11 @@ import java.util.ArrayList; import java.util.Collections; import static org.hamcrest.Matchers.containsString; -import static org.mockito.BDDMockito.given; import static org.mockito.Matchers.argThat; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; /** * Test for {@link LoginCommand}. @@ -27,6 +27,12 @@ import static org.mockito.Mockito.verify; @RunWith(MockitoJUnitRunner.class) public class LoginCommandTest { + @InjectMocks + private LoginCommand command; + + @Mock + private Management management; + @Mock private CommandService commandService; @@ -34,13 +40,12 @@ public class LoginCommandTest { public void shouldStopIfSenderIsNotAPlayer() { // given CommandSender sender = mock(BlockCommandSender.class); - LoginCommand command = new LoginCommand(); // when command.executeCommand(sender, new ArrayList(), commandService); // then - verify(commandService, never()).getManagement(); + verifyZeroInteractions(management); verify(sender).sendMessage(argThat(containsString("only for players"))); } @@ -48,9 +53,6 @@ public class LoginCommandTest { public void shouldCallManagementForPlayerCaller() { // given Player sender = mock(Player.class); - LoginCommand command = new LoginCommand(); - Management management = mock(Management.class); - given(commandService.getManagement()).willReturn(management); // when command.executeCommand(sender, Collections.singletonList("password"), commandService); diff --git a/src/test/java/fr/xephi/authme/command/executable/logout/LogoutCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/logout/LogoutCommandTest.java index 7ac3ede6..bca24ac2 100644 --- a/src/test/java/fr/xephi/authme/command/executable/logout/LogoutCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/logout/LogoutCommandTest.java @@ -7,22 +7,33 @@ import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; import java.util.ArrayList; import java.util.Collections; import static org.hamcrest.Matchers.containsString; -import static org.mockito.BDDMockito.given; import static org.mockito.Matchers.argThat; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; /** * Test for {@link LogoutCommand}. */ +@RunWith(MockitoJUnitRunner.class) public class LogoutCommandTest { + @InjectMocks + private LogoutCommand command; + + @Mock + private Management management; + + @Mock private CommandService commandService; @Before @@ -34,13 +45,12 @@ public class LogoutCommandTest { public void shouldStopIfSenderIsNotAPlayer() { // given CommandSender sender = mock(BlockCommandSender.class); - LogoutCommand command = new LogoutCommand(); // when command.executeCommand(sender, new ArrayList(), commandService); // then - verify(commandService, never()).getManagement(); + verifyZeroInteractions(management); verify(sender).sendMessage(argThat(containsString("only for players"))); } @@ -48,9 +58,6 @@ public class LogoutCommandTest { public void shouldCallManagementForPlayerCaller() { // given Player sender = mock(Player.class); - LogoutCommand command = new LogoutCommand(); - Management management = mock(Management.class); - given(commandService.getManagement()).willReturn(management); // when command.executeCommand(sender, Collections.singletonList("password"), commandService); diff --git a/src/test/java/fr/xephi/authme/command/executable/register/RegisterCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/register/RegisterCommandTest.java index fecc3fcc..f33ad87e 100644 --- a/src/test/java/fr/xephi/authme/command/executable/register/RegisterCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/register/RegisterCommandTest.java @@ -2,7 +2,6 @@ package fr.xephi.authme.command.executable.register; import fr.xephi.authme.TestHelper; import fr.xephi.authme.command.CommandService; -import fr.xephi.authme.command.ExecutableCommand; import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.process.Management; import fr.xephi.authme.security.HashAlgorithm; @@ -19,6 +18,7 @@ import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; @@ -40,10 +40,15 @@ import static org.mockito.Mockito.verifyZeroInteractions; @RunWith(MockitoJUnitRunner.class) public class RegisterCommandTest { + @InjectMocks + private RegisterCommand command; + @Mock private CommandService commandService; + @Mock private Management management; + @Mock private Player sender; @@ -54,7 +59,6 @@ public class RegisterCommandTest { @Before public void linkMocksAndProvideSettingDefaults() { - given(commandService.getManagement()).willReturn(management); given(commandService.getProperty(SecuritySettings.PASSWORD_HASH)).willReturn(HashAlgorithm.BCRYPT); given(commandService.getProperty(RegistrationSettings.USE_EMAIL_REGISTRATION)).willReturn(false); given(commandService.getProperty(RestrictionSettings.ENABLE_PASSWORD_CONFIRMATION)).willReturn(false); @@ -64,7 +68,6 @@ public class RegisterCommandTest { public void shouldNotRunForNonPlayerSender() { // given CommandSender sender = mock(BlockCommandSender.class); - RegisterCommand command = new RegisterCommand(); // when command.executeCommand(sender, new ArrayList(), commandService); @@ -78,7 +81,6 @@ public class RegisterCommandTest { public void shouldForwardToManagementForTwoFactor() { // given given(commandService.getProperty(SecuritySettings.PASSWORD_HASH)).willReturn(HashAlgorithm.TWO_FACTOR); - ExecutableCommand command = new RegisterCommand(); // when command.executeCommand(sender, Collections.emptyList(), commandService); @@ -89,10 +91,7 @@ public class RegisterCommandTest { @Test public void shouldReturnErrorForEmptyArguments() { - // given - ExecutableCommand command = new RegisterCommand(); - - // when + // given / when command.executeCommand(sender, Collections.emptyList(), commandService); // then @@ -104,7 +103,6 @@ public class RegisterCommandTest { public void shouldReturnErrorForMissingConfirmation() { // given given(commandService.getProperty(RestrictionSettings.ENABLE_PASSWORD_CONFIRMATION)).willReturn(true); - ExecutableCommand command = new RegisterCommand(); // when command.executeCommand(sender, Collections.singletonList("arrrr"), commandService); @@ -119,7 +117,6 @@ public class RegisterCommandTest { // given given(commandService.getProperty(RegistrationSettings.USE_EMAIL_REGISTRATION)).willReturn(true); given(commandService.getProperty(RegistrationSettings.ENABLE_CONFIRM_EMAIL)).willReturn(true); - ExecutableCommand command = new RegisterCommand(); // when command.executeCommand(sender, Collections.singletonList("test@example.org"), commandService); @@ -135,7 +132,6 @@ public class RegisterCommandTest { given(commandService.getProperty(RegistrationSettings.USE_EMAIL_REGISTRATION)).willReturn(true); given(commandService.getProperty(RegistrationSettings.ENABLE_CONFIRM_EMAIL)).willReturn(false); given(commandService.getProperty(EmailSettings.MAIL_ACCOUNT)).willReturn(""); - ExecutableCommand command = new RegisterCommand(); // when command.executeCommand(sender, Collections.singletonList("myMail@example.tld"), commandService); @@ -155,8 +151,6 @@ public class RegisterCommandTest { given(commandService.getProperty(RegistrationSettings.ENABLE_CONFIRM_EMAIL)).willReturn(true); given(commandService.getProperty(EmailSettings.MAIL_ACCOUNT)).willReturn("server@example.com"); - ExecutableCommand command = new RegisterCommand(); - // when command.executeCommand(sender, Arrays.asList(playerMail, playerMail), commandService); @@ -176,8 +170,6 @@ public class RegisterCommandTest { given(commandService.getProperty(RegistrationSettings.ENABLE_CONFIRM_EMAIL)).willReturn(true); given(commandService.getProperty(EmailSettings.MAIL_ACCOUNT)).willReturn("server@example.com"); - ExecutableCommand command = new RegisterCommand(); - // when command.executeCommand(sender, Arrays.asList(playerMail, "invalid"), commandService); @@ -197,7 +189,6 @@ public class RegisterCommandTest { given(commandService.getProperty(RegistrationSettings.USE_EMAIL_REGISTRATION)).willReturn(true); given(commandService.getProperty(RegistrationSettings.ENABLE_CONFIRM_EMAIL)).willReturn(true); given(commandService.getProperty(EmailSettings.MAIL_ACCOUNT)).willReturn("server@example.com"); - ExecutableCommand command = new RegisterCommand(); // when command.executeCommand(sender, Arrays.asList(playerMail, playerMail), commandService); @@ -211,7 +202,6 @@ public class RegisterCommandTest { public void shouldRejectInvalidPasswordConfirmation() { // given given(commandService.getProperty(RestrictionSettings.ENABLE_PASSWORD_CONFIRMATION)).willReturn(true); - ExecutableCommand command = new RegisterCommand(); // when command.executeCommand(sender, Arrays.asList("myPass", "mypass"), commandService); @@ -223,10 +213,7 @@ public class RegisterCommandTest { @Test public void shouldPerformPasswordValidation() { - // given - ExecutableCommand command = new RegisterCommand(); - - // when + // given / when command.executeCommand(sender, Collections.singletonList("myPass"), commandService); // then diff --git a/src/test/java/fr/xephi/authme/initialization/AuthMeServiceInitializerTest.java b/src/test/java/fr/xephi/authme/initialization/AuthMeServiceInitializerTest.java index 0541c704..63e4d01c 100644 --- a/src/test/java/fr/xephi/authme/initialization/AuthMeServiceInitializerTest.java +++ b/src/test/java/fr/xephi/authme/initialization/AuthMeServiceInitializerTest.java @@ -8,6 +8,7 @@ import fr.xephi.authme.initialization.samples.ClassWithAbstractDependency; import fr.xephi.authme.initialization.samples.ClassWithAnnotations; import fr.xephi.authme.initialization.samples.Duration; import fr.xephi.authme.initialization.samples.FieldInjectionWithAnnotations; +import fr.xephi.authme.initialization.samples.InstantiationFallbackClasses; import fr.xephi.authme.initialization.samples.InvalidClass; import fr.xephi.authme.initialization.samples.InvalidPostConstruct; import fr.xephi.authme.initialization.samples.InvalidStaticFieldInjection; @@ -231,4 +232,16 @@ public class AuthMeServiceInitializerTest { initializer.newInstance(InvalidStaticFieldInjection.class); } + @Test + public void shouldFallbackToSimpleInstantiationForPlainClass() { + // given / when + InstantiationFallbackClasses.HasFallbackDependency result = + initializer.get(InstantiationFallbackClasses.HasFallbackDependency.class); + + // then + assertThat(result, not(nullValue())); + assertThat(result.getGammaService(), not(nullValue())); + assertThat(result.getFallbackDependency(), not(nullValue())); + } + } diff --git a/src/test/java/fr/xephi/authme/initialization/ConstructorInjectionTest.java b/src/test/java/fr/xephi/authme/initialization/ConstructorInjectionTest.java index df5493e7..c53a4321 100644 --- a/src/test/java/fr/xephi/authme/initialization/ConstructorInjectionTest.java +++ b/src/test/java/fr/xephi/authme/initialization/ConstructorInjectionTest.java @@ -1,6 +1,7 @@ package fr.xephi.authme.initialization; import fr.xephi.authme.initialization.samples.AlphaService; +import fr.xephi.authme.initialization.samples.BetaManager; import fr.xephi.authme.initialization.samples.ClassWithAnnotations; import fr.xephi.authme.initialization.samples.Duration; import fr.xephi.authme.initialization.samples.GammaService; @@ -20,7 +21,6 @@ import static org.junit.Assert.assertThat; */ public class ConstructorInjectionTest { - @SuppressWarnings("unchecked") @Test public void shouldReturnDependencies() { // given @@ -74,8 +74,7 @@ public class ConstructorInjectionTest { @Test public void shouldReturnNullForNoConstructorInjection() { // given / when - @SuppressWarnings("rawtypes") - Injection injection = ConstructorInjection.provide(FieldInjection.class).get(); + Injection injection = ConstructorInjection.provide(BetaManager.class).get(); // then assertThat(injection, nullValue()); diff --git a/src/test/java/fr/xephi/authme/initialization/FieldInjectionTest.java b/src/test/java/fr/xephi/authme/initialization/FieldInjectionTest.java index 77035e4d..9b25565e 100644 --- a/src/test/java/fr/xephi/authme/initialization/FieldInjectionTest.java +++ b/src/test/java/fr/xephi/authme/initialization/FieldInjectionTest.java @@ -105,6 +105,15 @@ public class FieldInjectionTest { FieldInjection.provide(InvalidStaticFieldInjection.class).get(); } + @Test + public void shouldNotReturnFieldInjectionForZeroInjectFields() { + // given / when + Injection injection = FieldInjection.provide(NoInjectionClass.class).get(); + + // then + assertThat(injection, nullValue()); + } + private static class ThrowingConstructor { @SuppressWarnings("unused") @Inject @@ -115,4 +124,10 @@ public class FieldInjectionTest { throw new UnsupportedOperationException("Exception in constructor"); } } + + private static class NoInjectionClass { + + private BetaManager betaManager; + + } } diff --git a/src/test/java/fr/xephi/authme/initialization/InstantiationFallbackTest.java b/src/test/java/fr/xephi/authme/initialization/InstantiationFallbackTest.java new file mode 100644 index 00000000..c40648f6 --- /dev/null +++ b/src/test/java/fr/xephi/authme/initialization/InstantiationFallbackTest.java @@ -0,0 +1,68 @@ +package fr.xephi.authme.initialization; + +import fr.xephi.authme.initialization.samples.GammaService; +import fr.xephi.authme.initialization.samples.InstantiationFallbackClasses; +import org.junit.Test; + +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertThat; + +/** + * Test for {@link InstantiationFallback}. + */ +public class InstantiationFallbackTest { + + @Test + public void shouldInstantiateClass() { + // given + Injection instantiation = + InstantiationFallback.provide(InstantiationFallbackClasses.FallbackClass.class).get(); + + // when + InstantiationFallbackClasses.FallbackClass result = instantiation.instantiateWith(); + + // then + assertThat(result, not(nullValue())); + } + + @Test(expected = RuntimeException.class) + public void shouldThrowIfArgumentsAreSupplied() { + // given + Injection instantiation = + InstantiationFallback.provide(InstantiationFallbackClasses.FallbackClass.class).get(); + + // when / then + instantiation.instantiateWith("some argument"); + } + + @Test + public void shouldReturnNullForClassWithInjectMethod() { + // given / when + Injection instantiation = + InstantiationFallback.provide(InstantiationFallbackClasses.InvalidInjectOnMethodClass.class).get(); + + // then + assertThat(instantiation, nullValue()); + } + + @Test + public void shouldReturnNullForMissingNoArgsConstructor() { + // given / when + Injection instantiation = + InstantiationFallback.provide(InstantiationFallbackClasses.InvalidFallbackClass.class).get(); + + // then + assertThat(instantiation, nullValue()); + } + + @Test + public void shouldReturnNullForDifferentInjectionType() { + // given / when + Injection instantiation = InstantiationFallback.provide(GammaService.class).get(); + + // then + assertThat(instantiation, nullValue()); + } + +} diff --git a/src/test/java/fr/xephi/authme/initialization/samples/InstantiationFallbackClasses.java b/src/test/java/fr/xephi/authme/initialization/samples/InstantiationFallbackClasses.java new file mode 100644 index 00000000..ae15029b --- /dev/null +++ b/src/test/java/fr/xephi/authme/initialization/samples/InstantiationFallbackClasses.java @@ -0,0 +1,45 @@ +package fr.xephi.authme.initialization.samples; + +import javax.inject.Inject; + +/** + * Sample class - triggers instantiation fallback. + */ +public abstract class InstantiationFallbackClasses { + + public static final class FallbackClass { + // No @Inject annotations, public no-args constructor + } + + public static final class HasFallbackDependency { + @Inject + private FallbackClass fallbackClass; + + @Inject + private GammaService gammaService; + + public GammaService getGammaService() { + return gammaService; + } + + public FallbackClass getFallbackDependency() { + return fallbackClass; + } + } + + public static final class InvalidFallbackClass { + private InvalidFallbackClass() { + // no-args constructor must be public for fallback instantiation + } + } + + public static final class InvalidInjectOnMethodClass { + // We don't support method injection but this should still be detected and an exception returned + // Only use instantiation fallback if we're sure there isn't some sort of misconfiguration + @Inject + public void setGammaService(GammaService gammaService) { + // -- + } + } + +} diff --git a/src/test/java/tools/commands/CommandPageCreater.java b/src/test/java/tools/commands/CommandPageCreater.java index ddfaf3e3..17308ebf 100644 --- a/src/test/java/tools/commands/CommandPageCreater.java +++ b/src/test/java/tools/commands/CommandPageCreater.java @@ -35,7 +35,8 @@ public class CommandPageCreater implements ToolTask { @Override public void execute(Scanner scanner) { - final Set baseCommands = CommandInitializer.buildCommands(getMockInitializer()); + CommandInitializer commandInitializer = new CommandInitializer(getMockInitializer()); + final Set baseCommands = commandInitializer.getCommands(); NestedTagValue commandTags = new NestedTagValue(); addCommandsInfo(commandTags, baseCommands); From 8e878d6a5a042aa401d235ac8109d10eca5fe354 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Sun, 8 May 2016 13:50:20 +0200 Subject: [PATCH 030/200] Remove BukkitService from CommandService, inject where needed instead --- .../xephi/authme/command/CommandService.java | 34 ------------------- .../executable/authme/AccountsCommand.java | 13 +++---- .../authme/ChangePasswordAdminCommand.java | 6 +++- .../executable/authme/ConverterCommand.java | 6 +++- .../executable/authme/ForceLoginCommand.java | 6 +++- .../executable/authme/GetIpCommand.java | 7 +++- .../authme/PurgeBannedPlayersCommand.java | 6 +++- .../authme/RegisterAdminCommand.java | 10 ++++-- .../executable/authme/SetEmailCommand.java | 6 +++- .../authme/UnregisterAdminCommand.java | 8 +++-- .../executable/authme/VersionCommand.java | 7 +++- .../changepassword/ChangePasswordCommand.java | 6 +++- src/test/java/fr/xephi/authme/TestHelper.java | 5 ++- .../authme/command/CommandServiceTest.java | 15 -------- .../authme/AccountsCommandTest.java | 22 ++++++------ .../ChangePasswordAdminCommandTest.java | 12 ++++--- .../authme/ForceLoginCommandTest.java | 26 +++++++------- .../executable/authme/GetIpCommandTest.java | 20 +++++++---- .../authme/RegisterAdminCommandTest.java | 25 +++++++------- .../ChangePasswordCommandTest.java | 6 +++- 20 files changed, 127 insertions(+), 119 deletions(-) diff --git a/src/main/java/fr/xephi/authme/command/CommandService.java b/src/main/java/fr/xephi/authme/command/CommandService.java index 1e51303d..c479f547 100644 --- a/src/main/java/fr/xephi/authme/command/CommandService.java +++ b/src/main/java/fr/xephi/authme/command/CommandService.java @@ -5,15 +5,10 @@ import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.output.Messages; import fr.xephi.authme.settings.NewSetting; import fr.xephi.authme.settings.domain.Property; -import fr.xephi.authme.util.BukkitService; import fr.xephi.authme.util.ValidationService; import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; -import org.bukkit.scheduler.BukkitScheduler; -import org.bukkit.scheduler.BukkitTask; import javax.inject.Inject; -import java.util.Collection; import java.util.List; /** @@ -22,21 +17,16 @@ import java.util.List; */ public class CommandService { - @Inject - private BukkitScheduler scheduler; @Inject private Messages messages; @Inject private HelpProvider helpProvider; @Inject private CommandMapper commandMapper; - @SuppressWarnings("unused") @Inject private NewSetting settings; @Inject private ValidationService validationService; - @Inject - private BukkitService bukkitService; /** * Send a message to a player. @@ -70,18 +60,6 @@ public class CommandService { return commandMapper.mapPartsToCommand(sender, commandParts); } - /** - * Run the given task asynchronously with the Bukkit scheduler. - * - * @param task The task to run - * @return a BukkitTask that contains the id number - * @throws IllegalArgumentException if plugin is null - * @throws IllegalArgumentException if task is null - */ - public BukkitTask runTaskAsynchronously(Runnable task) { - return bukkitService.runTaskAsynchronously(task); - } - /** * Output the help for a given command. * @@ -146,16 +124,4 @@ public class CommandService { return validationService.isEmailFreeForRegistration(email, sender); } - public Player getPlayer(String name) { - return bukkitService.getPlayerExact(name); - } - - public Collection getOnlinePlayers() { - return bukkitService.getOnlinePlayers(); - } - - public BukkitService getBukkitService() { - return bukkitService; - } - } diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/AccountsCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/AccountsCommand.java index ad5bf9b2..5c3c227e 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/AccountsCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/AccountsCommand.java @@ -5,7 +5,7 @@ import fr.xephi.authme.command.CommandService; import fr.xephi.authme.command.ExecutableCommand; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.output.MessageKey; -import fr.xephi.authme.output.Messages; +import fr.xephi.authme.util.BukkitService; import fr.xephi.authme.util.StringUtils; import org.bukkit.command.CommandSender; @@ -19,8 +19,9 @@ public class AccountsCommand implements ExecutableCommand { @Inject private DataSource dataSource; + @Inject - private Messages messages; + private BukkitService bukkitService; @Override public void executeCommand(final CommandSender sender, List arguments, @@ -29,18 +30,18 @@ public class AccountsCommand implements ExecutableCommand { // Assumption: a player name cannot contain '.' if (!playerName.contains(".")) { - commandService.runTaskAsynchronously(new Runnable() { + bukkitService.runTaskAsynchronously(new Runnable() { @Override public void run() { PlayerAuth auth = dataSource.getAuth(playerName.toLowerCase()); if (auth == null) { - messages.send(sender, MessageKey.UNKNOWN_USER); + commandService.send(sender, MessageKey.UNKNOWN_USER); return; } List accountList = dataSource.getAllAuthsByIp(auth.getIp()); if (accountList.isEmpty()) { - messages.send(sender, MessageKey.USER_NOT_REGISTERED); + commandService.send(sender, MessageKey.USER_NOT_REGISTERED); } else if (accountList.size() == 1) { sender.sendMessage("[AuthMe] " + playerName + " is a single account player"); } else { @@ -49,7 +50,7 @@ public class AccountsCommand implements ExecutableCommand { } }); } else { - commandService.runTaskAsynchronously(new Runnable() { + bukkitService.runTaskAsynchronously(new Runnable() { @Override public void run() { List accountList = dataSource.getAllAuthsByIp(playerName); 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 f81ac07f..2e353f15 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 @@ -9,6 +9,7 @@ import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.security.PasswordSecurity; import fr.xephi.authme.security.crypts.HashedPassword; +import fr.xephi.authme.util.BukkitService; import org.bukkit.command.CommandSender; import javax.inject.Inject; @@ -28,6 +29,9 @@ public class ChangePasswordAdminCommand implements ExecutableCommand { @Inject private DataSource dataSource; + @Inject + private BukkitService bukkitService; + @Override public void executeCommand(final CommandSender sender, List arguments, final CommandService commandService) { @@ -44,7 +48,7 @@ public class ChangePasswordAdminCommand implements ExecutableCommand { // Set the password final String playerNameLowerCase = playerName.toLowerCase(); - commandService.runTaskAsynchronously(new Runnable() { + bukkitService.runTaskAsynchronously(new Runnable() { @Override public void run() { diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/ConverterCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/ConverterCommand.java index f4a13640..26a1d7ee 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/ConverterCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/ConverterCommand.java @@ -11,6 +11,7 @@ import fr.xephi.authme.converter.SqliteToSql; import fr.xephi.authme.converter.vAuthConverter; import fr.xephi.authme.converter.xAuthConverter; import fr.xephi.authme.output.MessageKey; +import fr.xephi.authme.util.BukkitService; import org.bukkit.command.CommandSender; import javax.inject.Inject; @@ -21,6 +22,9 @@ public class ConverterCommand implements ExecutableCommand { @Inject private AuthMe authMe; + @Inject + private BukkitService bukkitService; + @Override public void executeCommand(CommandSender sender, List arguments, CommandService commandService) { // Get the conversion job @@ -59,7 +63,7 @@ public class ConverterCommand implements ExecutableCommand { } // Run the convert job - commandService.runTaskAsynchronously(converter); + bukkitService.runTaskAsynchronously(converter); // Show a status message sender.sendMessage("[AuthMe] Successfully converted from " + jobType.getName()); diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/ForceLoginCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/ForceLoginCommand.java index 3535cf6f..e9f8f777 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/ForceLoginCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/ForceLoginCommand.java @@ -4,6 +4,7 @@ import fr.xephi.authme.command.CommandService; import fr.xephi.authme.command.ExecutableCommand; import fr.xephi.authme.permission.PermissionsManager; import fr.xephi.authme.process.Management; +import fr.xephi.authme.util.BukkitService; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; @@ -23,12 +24,15 @@ public class ForceLoginCommand implements ExecutableCommand { @Inject private Management management; + @Inject + private BukkitService bukkitService; + @Override public void executeCommand(CommandSender sender, List arguments, CommandService commandService) { // Get the player query String playerName = arguments.isEmpty() ? sender.getName() : arguments.get(0); - Player player = commandService.getPlayer(playerName); + Player player = bukkitService.getPlayerExact(playerName); if (player == null || !player.isOnline()) { sender.sendMessage("Player needs to be online!"); } else if (!permissionsManager.hasPermission(player, CAN_LOGIN_BE_FORCED)) { diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/GetIpCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/GetIpCommand.java index 59379aa7..749a88bf 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/GetIpCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/GetIpCommand.java @@ -2,19 +2,24 @@ package fr.xephi.authme.command.executable.authme; import fr.xephi.authme.command.CommandService; import fr.xephi.authme.command.ExecutableCommand; +import fr.xephi.authme.util.BukkitService; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; +import javax.inject.Inject; import java.util.List; public class GetIpCommand implements ExecutableCommand { + @Inject + private BukkitService bukkitService; + @Override public void executeCommand(CommandSender sender, List arguments, CommandService commandService) { // Get the player query String playerName = arguments.get(0); - Player player = commandService.getPlayer(playerName); + Player player = bukkitService.getPlayerExact(playerName); if (player == null) { sender.sendMessage("The player is not online"); return; diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/PurgeBannedPlayersCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/PurgeBannedPlayersCommand.java index 1cedecd8..d9508eea 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/PurgeBannedPlayersCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/PurgeBannedPlayersCommand.java @@ -6,6 +6,7 @@ import fr.xephi.authme.command.ExecutableCommand; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.hooks.PluginHooks; import fr.xephi.authme.settings.properties.PurgeSettings; +import fr.xephi.authme.util.BukkitService; import org.bukkit.OfflinePlayer; import org.bukkit.command.CommandSender; @@ -28,11 +29,14 @@ public class PurgeBannedPlayersCommand implements ExecutableCommand { @Inject private AuthMe plugin; + @Inject + private BukkitService bukkitService; + @Override public void executeCommand(CommandSender sender, List arguments, CommandService commandService) { // Get the list of banned players List bannedPlayers = new ArrayList<>(); - for (OfflinePlayer offlinePlayer : commandService.getBukkitService().getBannedPlayers()) { + for (OfflinePlayer offlinePlayer : bukkitService.getBannedPlayers()) { bannedPlayers.add(offlinePlayer.getName().toLowerCase()); } 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 5ddd0b95..1540e465 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 @@ -8,6 +8,7 @@ import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.security.PasswordSecurity; import fr.xephi.authme.security.crypts.HashedPassword; +import fr.xephi.authme.util.BukkitService; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; @@ -25,6 +26,9 @@ public class RegisterAdminCommand implements ExecutableCommand { @Inject private DataSource dataSource; + @Inject + private BukkitService bukkitService; + @Override public void executeCommand(final CommandSender sender, List arguments, final CommandService commandService) { @@ -40,7 +44,7 @@ public class RegisterAdminCommand implements ExecutableCommand { return; } - commandService.runTaskAsynchronously(new Runnable() { + bukkitService.runTaskAsynchronously(new Runnable() { @Override public void run() { @@ -63,9 +67,9 @@ public class RegisterAdminCommand implements ExecutableCommand { commandService.send(sender, MessageKey.REGISTER_SUCCESS); ConsoleLogger.info(sender.getName() + " registered " + playerName); - final Player player = commandService.getPlayer(playerName); + final Player player = bukkitService.getPlayerExact(playerName); if (player != null) { - commandService.getBukkitService().scheduleSyncDelayedTask(new Runnable() { + bukkitService.scheduleSyncDelayedTask(new Runnable() { @Override public void run() { player.kickPlayer("An admin just registered you, please log in again"); diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/SetEmailCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/SetEmailCommand.java index 989c7210..35c0fa0d 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/SetEmailCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/SetEmailCommand.java @@ -6,6 +6,7 @@ import fr.xephi.authme.command.CommandService; import fr.xephi.authme.command.ExecutableCommand; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.output.MessageKey; +import fr.xephi.authme.util.BukkitService; import org.bukkit.command.CommandSender; import javax.inject.Inject; @@ -22,6 +23,9 @@ public class SetEmailCommand implements ExecutableCommand { @Inject private PlayerCache playerCache; + @Inject + private BukkitService bukkitService; + @Override public void executeCommand(final CommandSender sender, List arguments, final CommandService commandService) { @@ -35,7 +39,7 @@ public class SetEmailCommand implements ExecutableCommand { return; } - commandService.runTaskAsynchronously(new Runnable() { + bukkitService.runTaskAsynchronously(new Runnable() { @Override public void run() { // Validate the user diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/UnregisterAdminCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/UnregisterAdminCommand.java index e8f38c73..d8fe66ad 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/UnregisterAdminCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/UnregisterAdminCommand.java @@ -39,6 +39,9 @@ public class UnregisterAdminCommand implements ExecutableCommand { @Inject private AuthMe authMe; + @Inject + private BukkitService bukkitService; + @Override public void executeCommand(final CommandSender sender, List arguments, CommandService commandService) { // Get the player name @@ -58,7 +61,7 @@ public class UnregisterAdminCommand implements ExecutableCommand { } // Unregister the player - Player target = commandService.getPlayer(playerNameLowerCase); + Player target = bukkitService.getPlayerExact(playerNameLowerCase); playerCache.removePlayer(playerNameLowerCase); Utils.setGroup(target, Utils.GroupType.UNREGISTERED); if (target != null && target.isOnline()) { @@ -82,7 +85,6 @@ public class UnregisterAdminCommand implements ExecutableCommand { * @param service the command service */ private void applyUnregisteredEffectsAndTasks(Player target, CommandService service) { - final BukkitService bukkitService = service.getBukkitService(); final String playerNameLowerCase = target.getName().toLowerCase(); Utils.teleportToSpawn(target); @@ -94,7 +96,7 @@ public class UnregisterAdminCommand implements ExecutableCommand { LimboCache.getInstance().getLimboPlayer(playerNameLowerCase).setTimeoutTask(id); } LimboCache.getInstance().getLimboPlayer(playerNameLowerCase).setMessageTask( - bukkitService.runTask(new MessageTask(service.getBukkitService(), authMe.getMessages(), + bukkitService.runTask(new MessageTask(bukkitService, authMe.getMessages(), playerNameLowerCase, MessageKey.REGISTER_MESSAGE, interval))); if (service.getProperty(RegistrationSettings.APPLY_BLIND_EFFECT)) { diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/VersionCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/VersionCommand.java index 1a15dc04..825e0528 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/VersionCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/VersionCommand.java @@ -3,10 +3,12 @@ package fr.xephi.authme.command.executable.authme; import fr.xephi.authme.AuthMe; import fr.xephi.authme.command.CommandService; import fr.xephi.authme.command.ExecutableCommand; +import fr.xephi.authme.util.BukkitService; import org.bukkit.ChatColor; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; +import javax.inject.Inject; import java.util.Collection; import java.util.List; @@ -14,6 +16,9 @@ import static fr.xephi.authme.settings.properties.PluginSettings.HELP_HEADER; public class VersionCommand implements ExecutableCommand { + @Inject + private BukkitService bukkitService; + @Override public void executeCommand(CommandSender sender, List arguments, CommandService commandService) { // Show some version info @@ -22,7 +27,7 @@ public class VersionCommand implements ExecutableCommand { sender.sendMessage(ChatColor.GOLD + "Version: " + ChatColor.WHITE + AuthMe.getPluginName() + " v" + AuthMe.getPluginVersion() + ChatColor.GRAY + " (build: " + AuthMe.getPluginBuildNumber() + ")"); sender.sendMessage(ChatColor.GOLD + "Developers:"); - Collection onlinePlayers = commandService.getOnlinePlayers(); + Collection onlinePlayers = bukkitService.getOnlinePlayers(); printDeveloper(sender, "Xephi", "xephi59", "Lead Developer", onlinePlayers); printDeveloper(sender, "DNx5", "DNx5", "Developer", onlinePlayers); printDeveloper(sender, "games647", "games647", "Developer", onlinePlayers); 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 67df2d86..310976fc 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 @@ -6,6 +6,7 @@ import fr.xephi.authme.command.CommandService; import fr.xephi.authme.command.PlayerCommand; import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.task.ChangePasswordTask; +import fr.xephi.authme.util.BukkitService; import org.bukkit.entity.Player; import javax.inject.Inject; @@ -19,6 +20,9 @@ public class ChangePasswordCommand extends PlayerCommand { @Inject private PlayerCache playerCache; + @Inject + private BukkitService bukkitService; + @Override public void runCommand(Player player, List arguments, CommandService commandService) { String oldPassword = arguments.get(0); @@ -39,6 +43,6 @@ public class ChangePasswordCommand extends PlayerCommand { AuthMe plugin = AuthMe.getInstance(); // TODO ljacqu 20160117: Call async task via Management - commandService.runTaskAsynchronously(new ChangePasswordTask(plugin, player, oldPassword, newPassword)); + bukkitService.runTaskAsynchronously(new ChangePasswordTask(plugin, player, oldPassword, newPassword)); } } diff --git a/src/test/java/fr/xephi/authme/TestHelper.java b/src/test/java/fr/xephi/authme/TestHelper.java index 463b3870..cc9480a4 100644 --- a/src/test/java/fr/xephi/authme/TestHelper.java +++ b/src/test/java/fr/xephi/authme/TestHelper.java @@ -1,6 +1,5 @@ package fr.xephi.authme; -import fr.xephi.authme.command.CommandService; import fr.xephi.authme.util.BukkitService; import org.mockito.ArgumentCaptor; import org.mockito.Mockito; @@ -60,13 +59,13 @@ public final class TestHelper { } /** - * Execute a {@link Runnable} passed to a mock's {@link CommandService#runTaskAsynchronously} method. + * Execute a {@link Runnable} passed to a mock's {@link BukkitService#runTaskAsynchronously} method. * Note that calling this method expects that there be a runnable sent to the method and will fail * otherwise. * * @param service The mock service */ - public static void runInnerRunnable(CommandService service) { + public static void runInnerRunnable(BukkitService service) { ArgumentCaptor captor = ArgumentCaptor.forClass(Runnable.class); verify(service).runTaskAsynchronously(captor.capture()); Runnable runnable = captor.getValue(); diff --git a/src/test/java/fr/xephi/authme/command/CommandServiceTest.java b/src/test/java/fr/xephi/authme/command/CommandServiceTest.java index 996197e6..ba7e4d28 100644 --- a/src/test/java/fr/xephi/authme/command/CommandServiceTest.java +++ b/src/test/java/fr/xephi/authme/command/CommandServiceTest.java @@ -189,19 +189,4 @@ public class CommandServiceTest { verify(validationService).isEmailFreeForRegistration(email, sender); } - @Test - public void shouldGetPlayer() { - // given - String playerName = "_tester"; - Player player = mock(Player.class); - given(bukkitService.getPlayerExact(playerName)).willReturn(player); - - // when - Player result = commandService.getPlayer(playerName); - - // then - assertThat(result, equalTo(player)); - verify(bukkitService).getPlayerExact(playerName); - } - } diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/AccountsCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/AccountsCommandTest.java index 95124658..404a2f58 100644 --- a/src/test/java/fr/xephi/authme/command/executable/authme/AccountsCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/authme/AccountsCommandTest.java @@ -4,7 +4,7 @@ import fr.xephi.authme.cache.auth.PlayerAuth; import fr.xephi.authme.command.CommandService; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.output.MessageKey; -import fr.xephi.authme.output.Messages; +import fr.xephi.authme.util.BukkitService; import org.bukkit.command.CommandSender; import org.junit.Test; import org.junit.runner.RunWith; @@ -42,7 +42,7 @@ public class AccountsCommandTest { @Mock private DataSource dataSource; @Mock - private Messages messages; + private BukkitService bukkitService; @Test public void shouldGetAccountsOfCurrentUser() { @@ -54,7 +54,7 @@ public class AccountsCommandTest { // when command.executeCommand(sender, arguments, service); - runInnerRunnable(service); + runInnerRunnable(bukkitService); // then String[] messages = getMessagesSentToSender(sender, 2); @@ -70,10 +70,10 @@ public class AccountsCommandTest { // when command.executeCommand(sender, arguments, service); - runInnerRunnable(service); + runInnerRunnable(bukkitService); // then - verify(messages).send(sender, MessageKey.UNKNOWN_USER); + verify(service).send(sender, MessageKey.UNKNOWN_USER); verify(sender, never()).sendMessage(anyString()); } @@ -86,10 +86,10 @@ public class AccountsCommandTest { // when command.executeCommand(sender, arguments, service); - runInnerRunnable(service); + runInnerRunnable(bukkitService); // then - verify(messages).send(sender, MessageKey.USER_NOT_REGISTERED); + verify(service).send(sender, MessageKey.USER_NOT_REGISTERED); verify(sender, never()).sendMessage(anyString()); } @@ -102,7 +102,7 @@ public class AccountsCommandTest { // when command.executeCommand(sender, arguments, service); - runInnerRunnable(service); + runInnerRunnable(bukkitService); // then String[] messages = getMessagesSentToSender(sender, 1); @@ -120,7 +120,7 @@ public class AccountsCommandTest { // when command.executeCommand(sender, arguments, service); - runInnerRunnable(service); + runInnerRunnable(bukkitService); // then String[] messages = getMessagesSentToSender(sender, 1); @@ -135,7 +135,7 @@ public class AccountsCommandTest { // when command.executeCommand(sender, arguments, service); - runInnerRunnable(service); + runInnerRunnable(bukkitService); // then String[] messages = getMessagesSentToSender(sender, 1); @@ -150,7 +150,7 @@ public class AccountsCommandTest { // when command.executeCommand(sender, arguments, service); - runInnerRunnable(service); + runInnerRunnable(bukkitService); // then String[] messages = getMessagesSentToSender(sender, 2); diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/ChangePasswordAdminCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/ChangePasswordAdminCommandTest.java index 163bee15..64059b2d 100644 --- a/src/test/java/fr/xephi/authme/command/executable/authme/ChangePasswordAdminCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/authme/ChangePasswordAdminCommandTest.java @@ -8,6 +8,7 @@ import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.security.PasswordSecurity; import fr.xephi.authme.security.crypts.HashedPassword; +import fr.xephi.authme.util.BukkitService; import org.bukkit.command.CommandSender; import org.junit.BeforeClass; import org.junit.Test; @@ -47,6 +48,9 @@ public class ChangePasswordAdminCommandTest { @Mock private PlayerCache playerCache; + @Mock + private BukkitService bukkitService; + @BeforeClass public static void setUpLogger() { TestHelper.setupLogger(); @@ -77,7 +81,7 @@ public class ChangePasswordAdminCommandTest { // when command.executeCommand(sender, Arrays.asList(player, "password"), service); - runInnerRunnable(service); + runInnerRunnable(bukkitService); // then verify(service).send(sender, MessageKey.UNKNOWN_USER); @@ -101,7 +105,7 @@ public class ChangePasswordAdminCommandTest { // when command.executeCommand(sender, Arrays.asList(player, password), service); - runInnerRunnable(service); + runInnerRunnable(bukkitService); // then verify(service).validatePassword(password, player); @@ -128,7 +132,7 @@ public class ChangePasswordAdminCommandTest { // when command.executeCommand(sender, Arrays.asList(player, password), service); - runInnerRunnable(service); + runInnerRunnable(bukkitService); // then verify(service).validatePassword(password, player); @@ -154,7 +158,7 @@ public class ChangePasswordAdminCommandTest { // when command.executeCommand(sender, Arrays.asList(player, password), service); - runInnerRunnable(service); + runInnerRunnable(bukkitService); // then verify(service).validatePassword(password, player); diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/ForceLoginCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/ForceLoginCommandTest.java index 87819e33..8896be7c 100644 --- a/src/test/java/fr/xephi/authme/command/executable/authme/ForceLoginCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/authme/ForceLoginCommandTest.java @@ -1,10 +1,10 @@ package fr.xephi.authme.command.executable.authme; import fr.xephi.authme.command.CommandService; -import fr.xephi.authme.command.ExecutableCommand; import fr.xephi.authme.permission.PermissionsManager; import fr.xephi.authme.permission.PlayerPermission; import fr.xephi.authme.process.Management; +import fr.xephi.authme.util.BukkitService; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.junit.Test; @@ -39,6 +39,9 @@ public class ForceLoginCommandTest { @Mock private PermissionsManager permissionsManager; + + @Mock + private BukkitService bukkitService; @Mock private CommandService commandService; @@ -48,14 +51,14 @@ public class ForceLoginCommandTest { // given String playerName = "Bobby"; Player player = mockPlayer(false, playerName); - given(commandService.getPlayer(playerName)).willReturn(player); + given(bukkitService.getPlayerExact(playerName)).willReturn(player); CommandSender sender = mock(CommandSender.class); // when command.executeCommand(sender, Collections.singletonList(playerName), commandService); // then - verify(commandService).getPlayer(playerName); + verify(bukkitService).getPlayerExact(playerName); verify(sender).sendMessage(argThat(equalTo("Player needs to be online!"))); verifyZeroInteractions(management); } @@ -64,15 +67,14 @@ public class ForceLoginCommandTest { public void shouldRejectInexistentPlayer() { // given String playerName = "us3rname01"; - given(commandService.getPlayer(playerName)).willReturn(null); + given(bukkitService.getPlayerExact(playerName)).willReturn(null); CommandSender sender = mock(CommandSender.class); - ExecutableCommand command = new ForceLoginCommand(); // when command.executeCommand(sender, Collections.singletonList(playerName), commandService); // then - verify(commandService).getPlayer(playerName); + verify(bukkitService).getPlayerExact(playerName); verify(sender).sendMessage(argThat(equalTo("Player needs to be online!"))); verifyZeroInteractions(management); } @@ -82,7 +84,7 @@ public class ForceLoginCommandTest { // given String playerName = "testTest"; Player player = mockPlayer(true, playerName); - given(commandService.getPlayer(playerName)).willReturn(player); + given(bukkitService.getPlayerExact(playerName)).willReturn(player); given(permissionsManager.hasPermission(player, PlayerPermission.CAN_LOGIN_BE_FORCED)).willReturn(false); CommandSender sender = mock(CommandSender.class); @@ -90,7 +92,7 @@ public class ForceLoginCommandTest { command.executeCommand(sender, Collections.singletonList(playerName), commandService); // then - verify(commandService).getPlayer(playerName); + verify(bukkitService).getPlayerExact(playerName); verify(sender).sendMessage(argThat(containsString("You cannot force login the player"))); verifyZeroInteractions(management); } @@ -100,7 +102,7 @@ public class ForceLoginCommandTest { // given String playerName = "tester23"; Player player = mockPlayer(true, playerName); - given(commandService.getPlayer(playerName)).willReturn(player); + given(bukkitService.getPlayerExact(playerName)).willReturn(player); given(permissionsManager.hasPermission(player, PlayerPermission.CAN_LOGIN_BE_FORCED)).willReturn(true); CommandSender sender = mock(CommandSender.class); @@ -108,7 +110,7 @@ public class ForceLoginCommandTest { command.executeCommand(sender, Collections.singletonList(playerName), commandService); // then - verify(commandService).getPlayer(playerName); + verify(bukkitService).getPlayerExact(playerName); verify(management).performLogin(eq(player), anyString(), eq(true)); } @@ -117,7 +119,7 @@ public class ForceLoginCommandTest { // given String senderName = "tester23"; Player player = mockPlayer(true, senderName); - given(commandService.getPlayer(senderName)).willReturn(player); + given(bukkitService.getPlayerExact(senderName)).willReturn(player); given(permissionsManager.hasPermission(player, PlayerPermission.CAN_LOGIN_BE_FORCED)).willReturn(true); CommandSender sender = mock(CommandSender.class); given(sender.getName()).willReturn(senderName); @@ -126,7 +128,7 @@ public class ForceLoginCommandTest { command.executeCommand(sender, Collections.emptyList(), commandService); // then - verify(commandService).getPlayer(senderName); + verify(bukkitService).getPlayerExact(senderName); verify(management).performLogin(eq(player), anyString(), eq(true)); } diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/GetIpCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/GetIpCommandTest.java index 7e070af6..8d0eadc5 100644 --- a/src/test/java/fr/xephi/authme/command/executable/authme/GetIpCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/authme/GetIpCommandTest.java @@ -1,11 +1,12 @@ package fr.xephi.authme.command.executable.authme; import fr.xephi.authme.command.CommandService; -import fr.xephi.authme.command.ExecutableCommand; +import fr.xephi.authme.util.BukkitService; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; @@ -27,22 +28,28 @@ import static org.mockito.Mockito.verify; @RunWith(MockitoJUnitRunner.class) public class GetIpCommandTest { + @InjectMocks + private GetIpCommand command; + @Mock private CommandService commandService; + + @Mock + private BukkitService bukkitService; + @Mock private CommandSender sender; @Test public void shouldGetIpOfPlayer() { // given - given(commandService.getPlayer(anyString())).willReturn(null); - ExecutableCommand command = new GetIpCommand(); + given(bukkitService.getPlayerExact(anyString())).willReturn(null); // when command.executeCommand(sender, Collections.singletonList("Testt"), commandService); // then - verify(commandService).getPlayer("Testt"); + verify(bukkitService).getPlayerExact("Testt"); verify(sender).sendMessage(argThat(containsString("not online"))); } @@ -52,14 +59,13 @@ public class GetIpCommandTest { String playerName = "charlie"; String ip = "123.34.56.88"; Player player = mockPlayer(playerName, ip); - given(commandService.getPlayer(playerName)).willReturn(player); - ExecutableCommand command = new GetIpCommand(); + given(bukkitService.getPlayerExact(playerName)).willReturn(player); // when command.executeCommand(sender, Collections.singletonList(playerName), commandService); // then - verify(commandService).getPlayer(playerName); + verify(bukkitService).getPlayerExact(playerName); verify(sender).sendMessage(argThat(allOf(containsString(playerName), containsString(ip)))); } diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/RegisterAdminCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/RegisterAdminCommandTest.java index 47f4de1b..2459bc89 100644 --- a/src/test/java/fr/xephi/authme/command/executable/authme/RegisterAdminCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/authme/RegisterAdminCommandTest.java @@ -42,15 +42,18 @@ public class RegisterAdminCommandTest { @Mock private PasswordSecurity passwordSecurity; + @Mock + private DataSource dataSource; + + @Mock + private BukkitService bukkitService; + @Mock private CommandSender sender; @Mock private CommandService commandService; - @Mock - private DataSource dataSource; - @BeforeClass public static void setUpLogger() { TestHelper.setupLogger(); @@ -69,7 +72,7 @@ public class RegisterAdminCommandTest { // then verify(commandService).validatePassword(password, user); verify(commandService).send(sender, MessageKey.INVALID_PASSWORD_LENGTH); - verify(commandService, never()).runTaskAsynchronously(any(Runnable.class)); + verify(bukkitService, never()).runTaskAsynchronously(any(Runnable.class)); } @Test @@ -82,7 +85,7 @@ public class RegisterAdminCommandTest { // when command.executeCommand(sender, Arrays.asList(user, password), commandService); - TestHelper.runInnerRunnable(commandService); + TestHelper.runInnerRunnable(bukkitService); // then verify(commandService).validatePassword(password, user); @@ -103,7 +106,7 @@ public class RegisterAdminCommandTest { // when command.executeCommand(sender, Arrays.asList(user, password), commandService); - TestHelper.runInnerRunnable(commandService); + TestHelper.runInnerRunnable(bukkitService); // then verify(commandService).validatePassword(password, user); @@ -123,11 +126,11 @@ public class RegisterAdminCommandTest { given(dataSource.saveAuth(any(PlayerAuth.class))).willReturn(true); HashedPassword hashedPassword = new HashedPassword("$aea2345EW235dfsa@#R%987048"); given(passwordSecurity.computeHash(password, user)).willReturn(hashedPassword); - given(commandService.getPlayer(user)).willReturn(null); + given(bukkitService.getPlayerExact(user)).willReturn(null); // when command.executeCommand(sender, Arrays.asList(user, password), commandService); - TestHelper.runInnerRunnable(commandService); + TestHelper.runInnerRunnable(bukkitService); // then verify(commandService).validatePassword(password, user); @@ -149,13 +152,11 @@ public class RegisterAdminCommandTest { HashedPassword hashedPassword = new HashedPassword("$aea2345EW235dfsa@#R%987048"); given(passwordSecurity.computeHash(password, user)).willReturn(hashedPassword); Player player = mock(Player.class); - given(commandService.getPlayer(user)).willReturn(player); - BukkitService bukkitService = mock(BukkitService.class); - given(commandService.getBukkitService()).willReturn(bukkitService); + given(bukkitService.getPlayerExact(user)).willReturn(player); // when command.executeCommand(sender, Arrays.asList(user, password), commandService); - TestHelper.runInnerRunnable(commandService); + TestHelper.runInnerRunnable(bukkitService); runSyncDelayedTask(bukkitService); // then 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 1dc4dbb1..289af7c6 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 @@ -7,6 +7,7 @@ import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.settings.properties.RestrictionSettings; import fr.xephi.authme.settings.properties.SecuritySettings; import fr.xephi.authme.task.ChangePasswordTask; +import fr.xephi.authme.util.BukkitService; import org.bukkit.command.BlockCommandSender; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; @@ -49,6 +50,9 @@ public class ChangePasswordCommandTest { @Mock private CommandService commandService; + @Mock + private BukkitService bukkitService; + @Before public void setSettings() { when(commandService.getProperty(SecuritySettings.MIN_PASSWORD_LENGTH)).thenReturn(2); @@ -109,7 +113,7 @@ public class ChangePasswordCommandTest { verify(commandService).validatePassword("abc123", "parker"); verify(commandService, never()).send(eq(sender), any(MessageKey.class)); ArgumentCaptor taskCaptor = ArgumentCaptor.forClass(ChangePasswordTask.class); - verify(commandService).runTaskAsynchronously(taskCaptor.capture()); + verify(bukkitService).runTaskAsynchronously(taskCaptor.capture()); ChangePasswordTask task = taskCaptor.getValue(); assertThat((String) ReflectionTestUtils.getFieldValue(ChangePasswordTask.class, task, "newPassword"), equalTo("abc123")); From 662f28ab4f85a43e164eefc0535b043b4768ebd6 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Sun, 8 May 2016 17:01:06 +0200 Subject: [PATCH 031/200] #513 Fix class loading issue when running from mvn exec:java --- pom.xml | 3 +++ src/test/java/tools/ToolsRunner.java | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index f5fdf394..f249b6fb 100644 --- a/pom.xml +++ b/pom.xml @@ -246,6 +246,9 @@ test ${project.basedir}/target/test-classes tools.ToolsRunner + + writePermissionsList + true - 1.9.2-R0.1-SNAPSHOT + 1.9.4-R0.1-SNAPSHOT From 4bad04b1606f97496f0f06eb3bde4356dfc23a37 Mon Sep 17 00:00:00 2001 From: games647 Date: Wed, 11 May 2016 16:55:22 +0200 Subject: [PATCH 042/200] Add debug statements for finding the source of #419 --- src/main/java/fr/xephi/authme/ConsoleLogger.java | 10 ++++++++++ .../java/fr/xephi/authme/cache/auth/PlayerCache.java | 6 ++++++ .../fr/xephi/authme/datasource/CacheDataSource.java | 1 + 3 files changed, 17 insertions(+) diff --git a/src/main/java/fr/xephi/authme/ConsoleLogger.java b/src/main/java/fr/xephi/authme/ConsoleLogger.java index eca20fbb..acdae309 100644 --- a/src/main/java/fr/xephi/authme/ConsoleLogger.java +++ b/src/main/java/fr/xephi/authme/ConsoleLogger.java @@ -1,6 +1,7 @@ package fr.xephi.authme; import com.google.common.base.Throwables; +import fr.xephi.authme.settings.properties.SecuritySettings; import fr.xephi.authme.util.StringUtils; import java.io.File; @@ -48,6 +49,15 @@ public final class ConsoleLogger { } } + public static void debug(String message) { + if (!AuthMe.getInstance().getSettings().getProperty(SecuritySettings.REMOVE_SPAM_FROM_CONSOLE)) { + logger.fine(message); + if (useLogging) { + writeLog("Debug: " + message); + } + } + } + /** * Print an error message. * diff --git a/src/main/java/fr/xephi/authme/cache/auth/PlayerCache.java b/src/main/java/fr/xephi/authme/cache/auth/PlayerCache.java index 17cdca55..9914ee3f 100644 --- a/src/main/java/fr/xephi/authme/cache/auth/PlayerCache.java +++ b/src/main/java/fr/xephi/authme/cache/auth/PlayerCache.java @@ -1,5 +1,7 @@ package fr.xephi.authme.cache.auth; +import fr.xephi.authme.ConsoleLogger; + import java.util.concurrent.ConcurrentHashMap; /** @@ -22,6 +24,7 @@ public class PlayerCache { if (singleton == null) { singleton = new PlayerCache(); } + return singleton; } @@ -31,6 +34,7 @@ public class PlayerCache { * @param auth PlayerAuth */ public void addPlayer(PlayerAuth auth) { + ConsoleLogger.debug("ADDED PLAYER TO CACHE " + auth.getNickname()); cache.put(auth.getNickname().toLowerCase(), auth); } @@ -40,6 +44,7 @@ public class PlayerCache { * @param auth PlayerAuth */ public void updatePlayer(PlayerAuth auth) { + ConsoleLogger.debug("UPDATE PLAYER " + auth.getNickname()); cache.put(auth.getNickname(), auth); } @@ -49,6 +54,7 @@ public class PlayerCache { * @param user String */ public void removePlayer(String user) { + ConsoleLogger.debug("REMOVE PLAYER " + user); cache.remove(user.toLowerCase()); } diff --git a/src/main/java/fr/xephi/authme/datasource/CacheDataSource.java b/src/main/java/fr/xephi/authme/datasource/CacheDataSource.java index 6aad92b2..7765e61d 100644 --- a/src/main/java/fr/xephi/authme/datasource/CacheDataSource.java +++ b/src/main/java/fr/xephi/authme/datasource/CacheDataSource.java @@ -53,6 +53,7 @@ public class CacheDataSource implements DataSource { return executorService.submit(new Callable>() { @Override public Optional call() { + ConsoleLogger.debug("REFRESH " + key); return load(key); } }); From 5c850e46c45ab7445f4f60d7186f187b9601b2f8 Mon Sep 17 00:00:00 2001 From: games647 Date: Wed, 11 May 2016 17:16:29 +0200 Subject: [PATCH 043/200] Clean up a bit --- src/main/java/fr/xephi/authme/AuthMe.java | 3 ++- .../authme/PurgeBannedPlayersCommand.java | 2 +- .../executable/authme/PurgeCommand.java | 2 +- .../java/fr/xephi/authme/task/PurgeTask.java | 21 +++++++++---------- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/main/java/fr/xephi/authme/AuthMe.java b/src/main/java/fr/xephi/authme/AuthMe.java index d636426c..496ad53e 100644 --- a/src/main/java/fr/xephi/authme/AuthMe.java +++ b/src/main/java/fr/xephi/authme/AuthMe.java @@ -706,7 +706,8 @@ public class AuthMe extends JavaPlugin { ConsoleLogger.info("AutoPurging the Database: " + cleared.size() + " accounts removed!"); ConsoleLogger.info("Purging user accounts..."); - new PurgeTask(plugin, newSettings, Bukkit.getConsoleSender(), cleared).runTaskTimer(plugin, 0, 1); + new PurgeTask(plugin, Bukkit.getConsoleSender(), cleared, true, Bukkit.getOfflinePlayers()) + .runTaskTimer(plugin, 0, 1); } // Return the spawn location of a player diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/PurgeBannedPlayersCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/PurgeBannedPlayersCommand.java index 1f0a7c9c..92a4b50c 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/PurgeBannedPlayersCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/PurgeBannedPlayersCommand.java @@ -47,6 +47,6 @@ public class PurgeBannedPlayersCommand implements ExecutableCommand { // Show a status message sender.sendMessage(ChatColor.GOLD + "Purging user accounts..."); - new PurgeTask(plugin, plugin.getSettings(), sender, namedBanned, bannedPlayers).runTaskTimer(plugin, 0, 1); + new PurgeTask(plugin, sender, namedBanned, bannedPlayers).runTaskTimer(plugin, 0, 1); } } diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/PurgeCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/PurgeCommand.java index dd36e5bc..a0811cfe 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/PurgeCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/PurgeCommand.java @@ -68,6 +68,6 @@ public class PurgeCommand implements ExecutableCommand { // Show a status message sender.sendMessage(ChatColor.GOLD + "Deleted " + purged.size() + " user accounts"); sender.sendMessage(ChatColor.GOLD + "Purging user accounts..."); - new PurgeTask(plugin, plugin.getSettings(), sender, purged).runTaskTimer(plugin, 0, 1); + new PurgeTask(plugin, sender, purged).runTaskTimer(plugin, 0, 1); } } diff --git a/src/main/java/fr/xephi/authme/task/PurgeTask.java b/src/main/java/fr/xephi/authme/task/PurgeTask.java index f8835a26..97ffcc88 100644 --- a/src/main/java/fr/xephi/authme/task/PurgeTask.java +++ b/src/main/java/fr/xephi/authme/task/PurgeTask.java @@ -34,20 +34,19 @@ public class PurgeTask extends BukkitRunnable { private int currentPage = 0; - public PurgeTask(AuthMe plugin, NewSetting newSetting, CommandSender sender - , Set purged, Set offlinePlayers) { - this(plugin, newSetting, sender, purged, false + public PurgeTask(AuthMe plugin, CommandSender sender, Set purged) { + this(plugin, sender, purged, false, Bukkit.getOfflinePlayers()); + } + + public PurgeTask(AuthMe plugin, CommandSender sender, Set purged, Set offlinePlayers) { + this(plugin, sender, purged, false , offlinePlayers.toArray(new OfflinePlayer[offlinePlayers.size()])); } - public PurgeTask(AuthMe plugin, NewSetting newSetting, CommandSender sender, Set purged) { - this(plugin, newSetting, sender, purged, false, Bukkit.getOfflinePlayers()); - } - - public PurgeTask(AuthMe plugin, NewSetting newSetting, CommandSender sender, Set purged - , boolean autoPurging, OfflinePlayer[] offlinePlayers) { + public PurgeTask(AuthMe plugin, CommandSender sender, Set purged + , boolean autoPurge, OfflinePlayer[] offlinePlayers) { this.plugin = plugin; - this.newSetting = newSetting; + this.newSetting = plugin.getSettings(); if (sender instanceof Player) { this.sender = ((Player) sender).getUniqueId(); @@ -57,7 +56,7 @@ public class PurgeTask extends BukkitRunnable { this.toPurge = purged; this.totalPurgeCount = purged.size(); - this.autoPurging = autoPurging; + this.autoPurging = autoPurge; this.offlinePlayers = offlinePlayers; //this is commented out because I assume all players in the database already have an lowercase name From e04f7dc7111633dc22867816c69e1afc51cda3a6 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Thu, 12 May 2016 19:51:10 +0200 Subject: [PATCH 044/200] #704 Implement reloading via injector - Create interfaces Reloadable and SettingsDependent to recognize reloadable classes - Iterate through instances in injector to reload --- src/main/java/fr/xephi/authme/AuthMe.java | 17 ------- .../executable/authme/ReloadCommand.java | 21 +++++++- .../authme/command/help/HelpProvider.java | 12 +++-- .../xephi/authme/datasource/DataSource.java | 4 +- .../AuthMeServiceInitializer.java | 21 ++++++++ .../authme/initialization/Reloadable.java | 13 +++++ .../initialization/SettingsDependent.java | 16 ++++++ .../java/fr/xephi/authme/output/Messages.java | 14 +++-- .../authme/security/PasswordSecurity.java | 4 +- .../xephi/authme/security/crypts/BCRYPT.java | 12 +++-- .../authme/security/crypts/SALTED2MD5.java | 12 +++-- .../fr/xephi/authme/security/crypts/SMF.java | 1 + .../fr/xephi/authme/settings/SpawnLoader.java | 8 +-- .../executable/authme/ReloadCommandTest.java | 51 ++++++++++++++++--- .../AuthMeServiceInitializerTest.java | 33 ++++++++++++ .../initialization/samples/GammaService.java | 14 ++++- .../samples/PostConstructTestClass.java | 17 ++++++- .../initialization/samples/ProvidedClass.java | 14 ++++- .../output/MessagesIntegrationTest.java | 6 ++- 19 files changed, 240 insertions(+), 50 deletions(-) create mode 100644 src/main/java/fr/xephi/authme/initialization/Reloadable.java create mode 100644 src/main/java/fr/xephi/authme/initialization/SettingsDependent.java diff --git a/src/main/java/fr/xephi/authme/AuthMe.java b/src/main/java/fr/xephi/authme/AuthMe.java index cc0fcaa5..1ae1a6e0 100644 --- a/src/main/java/fr/xephi/authme/AuthMe.java +++ b/src/main/java/fr/xephi/authme/AuthMe.java @@ -321,23 +321,6 @@ public class AuthMe extends JavaPlugin { runAutoPurge(); } - /** - * Reload certain components. - * - * @throws Exception if an error occurs - */ - public void reload() throws Exception { - newSettings.reload(); - // We do not change database type for consistency issues, but we'll output a note in the logs - if (!newSettings.getProperty(DatabaseSettings.BACKEND).equals(database.getType())) { - ConsoleLogger.info("Note: cannot change database type during /authme reload"); - } - database.reload(); - messages.reload(newSettings.getMessagesFile()); - passwordSecurity.reload(); - spawnLoader.initialize(newSettings); - } - /** * Set up the mail API, if enabled. */ diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/ReloadCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/ReloadCommand.java index 3c3bc921..ec709796 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/ReloadCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/ReloadCommand.java @@ -4,7 +4,11 @@ import fr.xephi.authme.AuthMe; import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.command.CommandService; import fr.xephi.authme.command.ExecutableCommand; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.initialization.AuthMeServiceInitializer; import fr.xephi.authme.output.MessageKey; +import fr.xephi.authme.settings.NewSetting; +import fr.xephi.authme.settings.properties.DatabaseSettings; import org.bukkit.command.CommandSender; import javax.inject.Inject; @@ -18,10 +22,25 @@ public class ReloadCommand implements ExecutableCommand { @Inject private AuthMe plugin; + @Inject + private AuthMeServiceInitializer initializer; + + @Inject + private NewSetting settings; + + @Inject + private DataSource dataSource; + @Override public void executeCommand(CommandSender sender, List arguments, CommandService commandService) { try { - plugin.reload(); + settings.reload(); + // We do not change database type for consistency issues, but we'll output a note in the logs + if (!settings.getProperty(DatabaseSettings.BACKEND).equals(dataSource.getType())) { + ConsoleLogger.info("Note: cannot change database type during /authme reload"); + sender.sendMessage("Note: cannot change database type during /authme reload"); + } + initializer.performReloadOnServices(); commandService.send(sender, MessageKey.CONFIG_RELOAD_SUCCESS); } catch (Exception e) { sender.sendMessage("Error occurred during reload of AuthMe: aborting"); diff --git a/src/main/java/fr/xephi/authme/command/help/HelpProvider.java b/src/main/java/fr/xephi/authme/command/help/HelpProvider.java index c5a3ec5f..8a2774a5 100644 --- a/src/main/java/fr/xephi/authme/command/help/HelpProvider.java +++ b/src/main/java/fr/xephi/authme/command/help/HelpProvider.java @@ -7,6 +7,7 @@ import fr.xephi.authme.command.CommandDescription; import fr.xephi.authme.command.CommandPermissions; import fr.xephi.authme.command.CommandUtils; import fr.xephi.authme.command.FoundCommandResult; +import fr.xephi.authme.initialization.SettingsDependent; import fr.xephi.authme.permission.DefaultPermission; import fr.xephi.authme.permission.PermissionNode; import fr.xephi.authme.permission.PermissionsManager; @@ -26,7 +27,7 @@ import static java.util.Collections.singletonList; /** * Help syntax generator for AuthMe commands. */ -public class HelpProvider { +public class HelpProvider implements SettingsDependent { // --- Bit flags --- /** Set to not show the command. */ @@ -46,12 +47,12 @@ public class HelpProvider { public static final int ALL_OPTIONS = ~HIDE_COMMAND; private final PermissionsManager permissionsManager; - private final String helpHeader; + private String helpHeader; @Inject public HelpProvider(PermissionsManager permissionsManager, NewSetting settings) { this.permissionsManager = permissionsManager; - this.helpHeader = settings.getProperty(PluginSettings.HELP_HEADER); + loadSettings(settings); } public List printHelp(CommandSender sender, FoundCommandResult result, int options) { @@ -88,6 +89,11 @@ public class HelpProvider { return lines; } + @Override + public void loadSettings(NewSetting settings) { + helpHeader = settings.getProperty(PluginSettings.HELP_HEADER); + } + private static void printDetailedDescription(CommandDescription command, List lines) { lines.add(ChatColor.GOLD + "Short description: " + ChatColor.WHITE + command.getDescription()); lines.add(ChatColor.GOLD + "Detailed description:"); diff --git a/src/main/java/fr/xephi/authme/datasource/DataSource.java b/src/main/java/fr/xephi/authme/datasource/DataSource.java index 4c844a83..d80affc4 100644 --- a/src/main/java/fr/xephi/authme/datasource/DataSource.java +++ b/src/main/java/fr/xephi/authme/datasource/DataSource.java @@ -1,6 +1,7 @@ package fr.xephi.authme.datasource; import fr.xephi.authme.cache.auth.PlayerAuth; +import fr.xephi.authme.initialization.Reloadable; import fr.xephi.authme.security.crypts.HashedPassword; import java.util.List; @@ -8,7 +9,7 @@ import java.util.List; /** * Interface for manipulating {@link PlayerAuth} objects from a data source. */ -public interface DataSource { +public interface DataSource extends Reloadable { /** * Return whether there is a record for the given username. @@ -204,6 +205,7 @@ public interface DataSource { /** * Reload the data source. */ + @Override void reload(); } diff --git a/src/main/java/fr/xephi/authme/initialization/AuthMeServiceInitializer.java b/src/main/java/fr/xephi/authme/initialization/AuthMeServiceInitializer.java index 72c2169c..c6d7fd85 100644 --- a/src/main/java/fr/xephi/authme/initialization/AuthMeServiceInitializer.java +++ b/src/main/java/fr/xephi/authme/initialization/AuthMeServiceInitializer.java @@ -2,6 +2,7 @@ package fr.xephi.authme.initialization; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableSet; +import fr.xephi.authme.settings.NewSetting; import javax.annotation.PostConstruct; import javax.inject.Provider; @@ -122,6 +123,26 @@ public class AuthMeServiceInitializer { return object; } + /** + * Performs a reload on all applicable instances which are registered. + * Requires that the {@link NewSetting settings} instance be registered. + *

+ * Note that the order in which these classes are reloaded is not guaranteed. + */ + public void performReloadOnServices() { + NewSetting settings = (NewSetting) objects.get(NewSetting.class); + if (settings == null) { + throw new IllegalStateException("Settings instance is null"); + } + for (Object object : objects.values()) { + if (object instanceof Reloadable) { + ((Reloadable) object).reload(); + } else if (object instanceof SettingsDependent) { + ((SettingsDependent) object).loadSettings(settings); + } + } + } + /** * Instantiates the given class by locating an @Inject constructor and retrieving * or instantiating its parameters. diff --git a/src/main/java/fr/xephi/authme/initialization/Reloadable.java b/src/main/java/fr/xephi/authme/initialization/Reloadable.java new file mode 100644 index 00000000..6b28fc7d --- /dev/null +++ b/src/main/java/fr/xephi/authme/initialization/Reloadable.java @@ -0,0 +1,13 @@ +package fr.xephi.authme.initialization; + +/** + * Interface for reloadable entities. + */ +public interface Reloadable { + + /** + * Performs the reload action. + */ + void reload(); + +} diff --git a/src/main/java/fr/xephi/authme/initialization/SettingsDependent.java b/src/main/java/fr/xephi/authme/initialization/SettingsDependent.java new file mode 100644 index 00000000..f891b170 --- /dev/null +++ b/src/main/java/fr/xephi/authme/initialization/SettingsDependent.java @@ -0,0 +1,16 @@ +package fr.xephi.authme.initialization; + +import fr.xephi.authme.settings.NewSetting; + +/** + * Interface for classes that keep a local copy of certain settings. + */ +public interface SettingsDependent { + + /** + * Loads the needed settings. + * + * @param settings the settings instance + */ + void loadSettings(NewSetting settings); +} diff --git a/src/main/java/fr/xephi/authme/output/Messages.java b/src/main/java/fr/xephi/authme/output/Messages.java index 8ac42a7b..61bf3d7d 100644 --- a/src/main/java/fr/xephi/authme/output/Messages.java +++ b/src/main/java/fr/xephi/authme/output/Messages.java @@ -1,6 +1,8 @@ package fr.xephi.authme.output; import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.initialization.SettingsDependent; +import fr.xephi.authme.settings.NewSetting; import fr.xephi.authme.util.StringUtils; import org.bukkit.ChatColor; import org.bukkit.command.CommandSender; @@ -14,7 +16,7 @@ import java.io.InputStreamReader; /** * Class for retrieving and sending translatable messages to players. */ -public class Messages { +public class Messages implements SettingsDependent { private FileConfiguration configuration; private String fileName; @@ -114,13 +116,9 @@ public class Messages { return message; } - /** - * Reset the messages manager to retrieve messages from the given file instead of the current one. - * - * @param messagesFile The new file to load messages from - */ - public void reload(File messagesFile) { - initializeFile(messagesFile); + @Override + public void loadSettings(NewSetting settings) { + initializeFile(settings.getMessagesFile()); } private void initializeFile(File messageFile) { diff --git a/src/main/java/fr/xephi/authme/security/PasswordSecurity.java b/src/main/java/fr/xephi/authme/security/PasswordSecurity.java index 4a525531..18620426 100644 --- a/src/main/java/fr/xephi/authme/security/PasswordSecurity.java +++ b/src/main/java/fr/xephi/authme/security/PasswordSecurity.java @@ -3,6 +3,7 @@ package fr.xephi.authme.security; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.events.PasswordEncryptionEvent; import fr.xephi.authme.initialization.AuthMeServiceInitializer; +import fr.xephi.authme.initialization.Reloadable; import fr.xephi.authme.security.crypts.EncryptionMethod; import fr.xephi.authme.security.crypts.HashedPassword; import fr.xephi.authme.settings.NewSetting; @@ -15,7 +16,7 @@ import javax.inject.Inject; /** * Manager class for password-related operations. */ -public class PasswordSecurity { +public class PasswordSecurity implements Reloadable { @Inject private NewSetting settings; @@ -36,6 +37,7 @@ public class PasswordSecurity { * Load or reload the configuration. */ @PostConstruct + @Override public void reload() { this.algorithm = settings.getProperty(SecuritySettings.PASSWORD_HASH); this.supportOldAlgorithm = settings.getProperty(SecuritySettings.SUPPORT_OLD_PASSWORD_HASH); diff --git a/src/main/java/fr/xephi/authme/security/crypts/BCRYPT.java b/src/main/java/fr/xephi/authme/security/crypts/BCRYPT.java index 67d8c794..dc1b3676 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/BCRYPT.java +++ b/src/main/java/fr/xephi/authme/security/crypts/BCRYPT.java @@ -1,6 +1,7 @@ package fr.xephi.authme.security.crypts; import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.initialization.SettingsDependent; import fr.xephi.authme.security.crypts.description.HasSalt; import fr.xephi.authme.security.crypts.description.Recommendation; import fr.xephi.authme.security.crypts.description.SaltType; @@ -13,13 +14,13 @@ import javax.inject.Inject; @Recommendation(Usage.RECOMMENDED) // provided the salt length is >= 8 @HasSalt(value = SaltType.TEXT) // length depends on the bcryptLog2Rounds setting -public class BCRYPT implements EncryptionMethod { +public class BCRYPT implements EncryptionMethod, SettingsDependent { - private final int bCryptLog2Rounds; + private int bCryptLog2Rounds; @Inject public BCRYPT(NewSetting settings) { - this.bCryptLog2Rounds = settings.getProperty(HooksSettings.BCRYPT_LOG2_ROUND); + loadSettings(settings); } @Override @@ -52,4 +53,9 @@ public class BCRYPT implements EncryptionMethod { public boolean hasSeparateSalt() { return false; } + + @Override + public void loadSettings(NewSetting settings) { + bCryptLog2Rounds = settings.getProperty(HooksSettings.BCRYPT_LOG2_ROUND); + } } diff --git a/src/main/java/fr/xephi/authme/security/crypts/SALTED2MD5.java b/src/main/java/fr/xephi/authme/security/crypts/SALTED2MD5.java index bf67432b..a0e92284 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/SALTED2MD5.java +++ b/src/main/java/fr/xephi/authme/security/crypts/SALTED2MD5.java @@ -1,5 +1,6 @@ package fr.xephi.authme.security.crypts; +import fr.xephi.authme.initialization.SettingsDependent; import fr.xephi.authme.security.RandomString; import fr.xephi.authme.security.crypts.description.HasSalt; import fr.xephi.authme.security.crypts.description.Recommendation; @@ -14,13 +15,13 @@ import static fr.xephi.authme.security.HashUtils.md5; @Recommendation(Usage.ACCEPTABLE) // presuming that length is something sensible (>= 8) @HasSalt(value = SaltType.TEXT) // length defined by the doubleMd5SaltLength setting -public class SALTED2MD5 extends SeparateSaltMethod { +public class SALTED2MD5 extends SeparateSaltMethod implements SettingsDependent { - private final int saltLength; + private int saltLength; @Inject public SALTED2MD5(NewSetting settings) { - saltLength = settings.getProperty(SecuritySettings.DOUBLE_MD5_SALT_LENGTH); + loadSettings(settings); } @Override @@ -33,4 +34,9 @@ public class SALTED2MD5 extends SeparateSaltMethod { return RandomString.generateHex(saltLength); } + @Override + public void loadSettings(NewSetting settings) { + saltLength = settings.getProperty(SecuritySettings.DOUBLE_MD5_SALT_LENGTH); + } + } diff --git a/src/main/java/fr/xephi/authme/security/crypts/SMF.java b/src/main/java/fr/xephi/authme/security/crypts/SMF.java index 832afa2a..175efc3f 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/SMF.java +++ b/src/main/java/fr/xephi/authme/security/crypts/SMF.java @@ -4,6 +4,7 @@ import fr.xephi.authme.security.HashUtils; public class SMF extends UsernameSaltMethod { + @Override public HashedPassword computeHash(String password, String name) { return new HashedPassword(HashUtils.sha1(name.toLowerCase() + password)); } diff --git a/src/main/java/fr/xephi/authme/settings/SpawnLoader.java b/src/main/java/fr/xephi/authme/settings/SpawnLoader.java index a046d813..b2d366ae 100644 --- a/src/main/java/fr/xephi/authme/settings/SpawnLoader.java +++ b/src/main/java/fr/xephi/authme/settings/SpawnLoader.java @@ -5,6 +5,7 @@ import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.cache.auth.PlayerCache; import fr.xephi.authme.hooks.PluginHooks; import fr.xephi.authme.initialization.DataFolder; +import fr.xephi.authme.initialization.SettingsDependent; import fr.xephi.authme.settings.properties.RestrictionSettings; import fr.xephi.authme.util.FileUtils; import fr.xephi.authme.util.StringUtils; @@ -27,7 +28,7 @@ import java.io.IOException; * should be taken from. In AuthMe, we can distinguish between the regular spawn and a "first spawn", * to which players will be teleported who have joined for the first time. */ -public class SpawnLoader { +public class SpawnLoader implements SettingsDependent { private final File authMeConfigurationFile; private final PluginHooks pluginHooks; @@ -49,7 +50,7 @@ public class SpawnLoader { FileUtils.copyFileFromResource(spawnFile, "spawn.yml"); this.authMeConfigurationFile = new File(pluginFolder, "spawn.yml"); this.pluginHooks = pluginHooks; - initialize(settings); + loadSettings(settings); } /** @@ -57,7 +58,8 @@ public class SpawnLoader { * * @param settings The settings instance */ - public void initialize(NewSetting settings) { + @Override + public void loadSettings(NewSetting settings) { spawnPriority = settings.getProperty(RestrictionSettings.SPAWN_PRIORITY).split(","); authMeConfiguration = YamlConfiguration.loadConfiguration(authMeConfigurationFile); loadEssentialsSpawn(); diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/ReloadCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/ReloadCommandTest.java index 42623496..5d52bbd8 100644 --- a/src/test/java/fr/xephi/authme/command/executable/authme/ReloadCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/authme/ReloadCommandTest.java @@ -3,7 +3,12 @@ package fr.xephi.authme.command.executable.authme; import fr.xephi.authme.AuthMe; import fr.xephi.authme.TestHelper; import fr.xephi.authme.command.CommandService; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.datasource.DataSourceType; +import fr.xephi.authme.initialization.AuthMeServiceInitializer; import fr.xephi.authme.output.MessageKey; +import fr.xephi.authme.settings.NewSetting; +import fr.xephi.authme.settings.properties.DatabaseSettings; import org.bukkit.command.CommandSender; import org.junit.BeforeClass; import org.junit.Test; @@ -14,6 +19,9 @@ import org.mockito.runners.MockitoJUnitRunner; import java.util.Collections; +import static org.hamcrest.Matchers.containsString; +import static org.mockito.BDDMockito.given; +import static org.mockito.Matchers.argThat; import static org.mockito.Matchers.matches; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; @@ -32,7 +40,13 @@ public class ReloadCommandTest { private AuthMe authMe; @Mock - private CommandService service; + private AuthMeServiceInitializer initializer; + + @Mock + private NewSetting settings; + + @Mock + private DataSource dataSource; @BeforeClass public static void setUpLogger() { @@ -40,30 +54,55 @@ public class ReloadCommandTest { } @Test - public void shouldReload() throws Exception { + public void shouldReload() { // given CommandSender sender = mock(CommandSender.class); + CommandService service = mock(CommandService.class); + given(settings.getProperty(DatabaseSettings.BACKEND)).willReturn(DataSourceType.MYSQL); + given(dataSource.getType()).willReturn(DataSourceType.MYSQL); // when command.executeCommand(sender, Collections.emptyList(), service); // then - verify(authMe).reload(); + verify(settings).reload(); + verify(initializer).performReloadOnServices(); verify(service).send(sender, MessageKey.CONFIG_RELOAD_SUCCESS); } @Test - public void shouldHandleReloadError() throws Exception { + public void shouldHandleReloadError() { // given - doThrow(IllegalStateException.class).when(authMe).reload(); CommandSender sender = mock(CommandSender.class); + CommandService service = mock(CommandService.class); + doThrow(IllegalStateException.class).when(initializer).performReloadOnServices(); + given(settings.getProperty(DatabaseSettings.BACKEND)).willReturn(DataSourceType.MYSQL); + given(dataSource.getType()).willReturn(DataSourceType.MYSQL); // when command.executeCommand(sender, Collections.emptyList(), service); // then - verify(authMe).reload(); + verify(settings).reload(); + verify(initializer).performReloadOnServices(); verify(sender).sendMessage(matches("Error occurred.*")); verify(authMe).stopOrUnload(); } + + @Test + public void shouldIssueWarningForChangedDatasourceSetting() { + // given + CommandSender sender = mock(CommandSender.class); + CommandService service = mock(CommandService.class); + given(settings.getProperty(DatabaseSettings.BACKEND)).willReturn(DataSourceType.MYSQL); + given(dataSource.getType()).willReturn(DataSourceType.SQLITE); + + // when + command.executeCommand(sender, Collections.emptyList(), service); + + // then + verify(settings).reload(); + verify(initializer).performReloadOnServices(); + verify(sender).sendMessage(argThat(containsString("cannot change database type"))); + } } diff --git a/src/test/java/fr/xephi/authme/initialization/AuthMeServiceInitializerTest.java b/src/test/java/fr/xephi/authme/initialization/AuthMeServiceInitializerTest.java index 63e4d01c..a81571d1 100644 --- a/src/test/java/fr/xephi/authme/initialization/AuthMeServiceInitializerTest.java +++ b/src/test/java/fr/xephi/authme/initialization/AuthMeServiceInitializerTest.java @@ -8,6 +8,7 @@ import fr.xephi.authme.initialization.samples.ClassWithAbstractDependency; import fr.xephi.authme.initialization.samples.ClassWithAnnotations; import fr.xephi.authme.initialization.samples.Duration; import fr.xephi.authme.initialization.samples.FieldInjectionWithAnnotations; +import fr.xephi.authme.initialization.samples.GammaService; import fr.xephi.authme.initialization.samples.InstantiationFallbackClasses; import fr.xephi.authme.initialization.samples.InvalidClass; import fr.xephi.authme.initialization.samples.InvalidPostConstruct; @@ -15,6 +16,7 @@ import fr.xephi.authme.initialization.samples.InvalidStaticFieldInjection; import fr.xephi.authme.initialization.samples.PostConstructTestClass; import fr.xephi.authme.initialization.samples.ProvidedClass; import fr.xephi.authme.initialization.samples.Size; +import fr.xephi.authme.settings.NewSetting; import org.junit.Before; import org.junit.Test; @@ -23,6 +25,7 @@ import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.Matchers.sameInstance; import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.mock; /** * Test for {@link AuthMeServiceInitializer}. @@ -244,4 +247,34 @@ public class AuthMeServiceInitializerTest { assertThat(result.getFallbackDependency(), not(nullValue())); } + @Test + public void shouldPerformReloadOnApplicableInstances() { + // given + initializer.provide(Size.class, 12); + initializer.provide(Duration.class, -113L); + initializer.register(NewSetting.class, mock(NewSetting.class)); + + GammaService gammaService = initializer.get(GammaService.class); + PostConstructTestClass postConstructTestClass = initializer.get(PostConstructTestClass.class); + ProvidedClass providedClass = initializer.get(ProvidedClass.class); + initializer.get(ClassWithAnnotations.class); + // Assert that no class was somehow reloaded at initialization + assertThat(gammaService.getWasReloaded() || postConstructTestClass.getWasReloaded() + || providedClass.getWasReloaded(), equalTo(false)); + + // when + initializer.performReloadOnServices(); + + // then + assertThat(gammaService.getWasReloaded(), equalTo(true)); + assertThat(postConstructTestClass.getWasReloaded(), equalTo(true)); + assertThat(providedClass.getWasReloaded(), equalTo(true)); + } + + @Test(expected = RuntimeException.class) + public void shouldThrowForNullSetting() { + // given / when / then + initializer.performReloadOnServices(); + } + } diff --git a/src/test/java/fr/xephi/authme/initialization/samples/GammaService.java b/src/test/java/fr/xephi/authme/initialization/samples/GammaService.java index 9d6f9bfa..158187ea 100644 --- a/src/test/java/fr/xephi/authme/initialization/samples/GammaService.java +++ b/src/test/java/fr/xephi/authme/initialization/samples/GammaService.java @@ -1,13 +1,16 @@ package fr.xephi.authme.initialization.samples; +import fr.xephi.authme.initialization.Reloadable; + import javax.inject.Inject; /** * Sample - class dependent on alpha service. */ -public class GammaService { +public class GammaService implements Reloadable { private AlphaService alphaService; + private boolean wasReloaded; @Inject public GammaService(AlphaService alphaService) { @@ -17,4 +20,13 @@ public class GammaService { public AlphaService getAlphaService() { return alphaService; } + + @Override + public void reload() { + wasReloaded = true; + } + + public boolean getWasReloaded() { + return wasReloaded; + } } diff --git a/src/test/java/fr/xephi/authme/initialization/samples/PostConstructTestClass.java b/src/test/java/fr/xephi/authme/initialization/samples/PostConstructTestClass.java index 0bec84ee..375e58fa 100644 --- a/src/test/java/fr/xephi/authme/initialization/samples/PostConstructTestClass.java +++ b/src/test/java/fr/xephi/authme/initialization/samples/PostConstructTestClass.java @@ -1,12 +1,15 @@ package fr.xephi.authme.initialization.samples; +import fr.xephi.authme.initialization.SettingsDependent; +import fr.xephi.authme.settings.NewSetting; + import javax.annotation.PostConstruct; import javax.inject.Inject; /** * Sample class for testing the execution of @PostConstruct methods. */ -public class PostConstructTestClass { +public class PostConstructTestClass implements SettingsDependent { @Inject @Size @@ -15,6 +18,7 @@ public class PostConstructTestClass { private BetaManager betaManager; private boolean wasPostConstructCalled = false; private boolean wasSecondPostConstructCalled = false; + private boolean wasReloaded = false; @PostConstruct protected void setFieldToTrue() { @@ -34,4 +38,15 @@ public class PostConstructTestClass { public BetaManager getBetaManager() { return betaManager; } + + @Override + public void loadSettings(NewSetting settings) { + if (settings != null) { + wasReloaded = true; + } + } + + public boolean getWasReloaded() { + return wasReloaded; + } } diff --git a/src/test/java/fr/xephi/authme/initialization/samples/ProvidedClass.java b/src/test/java/fr/xephi/authme/initialization/samples/ProvidedClass.java index 7ce81bd3..d3405abe 100644 --- a/src/test/java/fr/xephi/authme/initialization/samples/ProvidedClass.java +++ b/src/test/java/fr/xephi/authme/initialization/samples/ProvidedClass.java @@ -1,11 +1,15 @@ package fr.xephi.authme.initialization.samples; +import fr.xephi.authme.initialization.Reloadable; + import javax.inject.Inject; /** * Sample - class that is always provided to the initializer beforehand. */ -public class ProvidedClass { +public class ProvidedClass implements Reloadable { + + private boolean wasReloaded = false; @Inject public ProvidedClass() { @@ -15,4 +19,12 @@ public class ProvidedClass { public ProvidedClass(String manualConstructor) { } + @Override + public void reload() { + wasReloaded = true; + } + + public boolean getWasReloaded() { + return wasReloaded; + } } diff --git a/src/test/java/fr/xephi/authme/output/MessagesIntegrationTest.java b/src/test/java/fr/xephi/authme/output/MessagesIntegrationTest.java index cd858ad8..5777d1d4 100644 --- a/src/test/java/fr/xephi/authme/output/MessagesIntegrationTest.java +++ b/src/test/java/fr/xephi/authme/output/MessagesIntegrationTest.java @@ -2,6 +2,7 @@ package fr.xephi.authme.output; import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.TestHelper; +import fr.xephi.authme.settings.NewSetting; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.junit.Before; @@ -19,6 +20,7 @@ import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.junit.Assert.assertThat; import static org.junit.Assume.assumeThat; +import static org.mockito.BDDMockito.given; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.argThat; import static org.mockito.Mockito.mock; @@ -247,9 +249,11 @@ public class MessagesIntegrationTest { MessageKey key = MessageKey.WRONG_PASSWORD; // assumption: message comes back as defined in messages_test.yml assumeThat(messages.retrieveSingle(key), equalTo("§cWrong password!")); + NewSetting settings = mock(NewSetting.class); + given(settings.getMessagesFile()).willReturn(TestHelper.getJarFile("/messages_test2.yml")); // when - messages.reload(TestHelper.getJarFile("/messages_test2.yml")); + messages.loadSettings(settings); // then assertThat(messages.retrieveSingle(key), equalTo("test2 - wrong password")); From 9f5b99521718f6b1d33f40f1ed4aecf6cf759dd6 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Thu, 12 May 2016 20:15:44 +0200 Subject: [PATCH 045/200] Fix datasource resource closing tests (#1) --- .../datasource/AbstractResourceClosingTest.java | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/test/java/fr/xephi/authme/datasource/AbstractResourceClosingTest.java b/src/test/java/fr/xephi/authme/datasource/AbstractResourceClosingTest.java index 75908e3e..b17f5135 100644 --- a/src/test/java/fr/xephi/authme/datasource/AbstractResourceClosingTest.java +++ b/src/test/java/fr/xephi/authme/datasource/AbstractResourceClosingTest.java @@ -32,6 +32,7 @@ import java.sql.SQLException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -192,8 +193,8 @@ public abstract class AbstractResourceClosingTest { for (Class paramType : method.getParameterTypes()) { // Checking List.class == paramType instead of Class#isAssignableFrom means we really only accept List, // but that is a sensible assumption and makes our life much easier later on when juggling with Type - Object param = (List.class == paramType) - ? getTypedList(method.getGenericParameterTypes()[index]) + Object param = Collection.class.isAssignableFrom(paramType) + ? getTypedCollection(method.getGenericParameterTypes()[index]) : PARAM_VALUES.get(paramType); Preconditions.checkNotNull(param, "No param type for " + paramType); params.add(param); @@ -208,15 +209,21 @@ public abstract class AbstractResourceClosingTest { * @param type The list type to process and build a test list for * @return Test list with sample elements of the correct type */ - private static List getTypedList(Type type) { + private static Collection getTypedCollection(Type type) { if (type instanceof ParameterizedType) { ParameterizedType parameterizedType = (ParameterizedType) type; - Preconditions.checkArgument(List.class == parameterizedType.getRawType(), type + " should be a List"); + Preconditions.checkArgument(Collection.class.isAssignableFrom((Class) parameterizedType.getRawType()), + type + " should extend from Collection"); Type genericType = parameterizedType.getActualTypeArguments()[0]; Object element = PARAM_VALUES.get(genericType); Preconditions.checkNotNull(element, "No sample element for list of generic type " + genericType); - return Arrays.asList(element, element, element); + if (List.class == parameterizedType.getRawType()) { + return Arrays.asList(element, element, element); + } else if (Set.class == parameterizedType.getRawType()) { + return new HashSet<>(Arrays.asList(element, element, element)); + } + throw new IllegalStateException("Unknown collection type " + parameterizedType.getRawType()); } throw new IllegalStateException("Cannot build list for unexpected Type: " + type); } From 673940f7b235809458b01728480021a1bd0865eb Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Thu, 12 May 2016 22:53:12 +0200 Subject: [PATCH 046/200] Fix travis --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 81706d34..3fa969f2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,7 @@ sudo: false language: java jdk: oraclejdk7 -script: mvn verify -B +script: mvn clean install -B notifications: webhooks: From 246e9ce0f084626c6de67ae86d8a8bd60874d6a1 Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Fri, 13 May 2016 23:27:07 +0200 Subject: [PATCH 047/200] Revert "Fix travis" This reverts commit 673940f7b235809458b01728480021a1bd0865eb. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 3fa969f2..81706d34 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,7 @@ sudo: false language: java jdk: oraclejdk7 -script: mvn clean install -B +script: mvn verify -B notifications: webhooks: From 05f0162a169ecf070f654ea194d12ad0a1d681b5 Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Fri, 13 May 2016 23:29:53 +0200 Subject: [PATCH 048/200] Damn HTTPS --- .travis.yml | 2 +- pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 81706d34..06d6183c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,7 @@ sudo: false language: java jdk: oraclejdk7 -script: mvn verify -B +script: mvn clean verify -B notifications: webhooks: diff --git a/pom.xml b/pom.xml index cf6f9033..e725c0d7 100644 --- a/pom.xml +++ b/pom.xml @@ -330,7 +330,7 @@ onarandombox - http://repo.onarandombox.com/content/groups/public + https://repo.onarandombox.com/content/groups/public From c7485013456b8b39d68024a85361e2756f5b705f Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Sat, 14 May 2016 00:01:26 +0200 Subject: [PATCH 049/200] Fix Multiverse dependency + fix invalid test in Eclipse --- pom.xml | 4 ++-- .../xephi/authme/initialization/ConstructorInjectionTest.java | 1 + .../fr/xephi/authme/initialization/FieldInjectionTest.java | 2 -- .../fr/xephi/authme/settings/ConfigFileConsistencyTest.java | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/pom.xml b/pom.xml index e725c0d7..31537789 100644 --- a/pom.xml +++ b/pom.xml @@ -330,7 +330,7 @@ onarandombox - https://repo.onarandombox.com/content/groups/public + http://repo.onarandombox.com/content/groups/public @@ -612,7 +612,7 @@ com.onarandombox.multiversecore Multiverse-Core - 2.5 + 2.5.0-SNAPSHOT jar provided diff --git a/src/test/java/fr/xephi/authme/initialization/ConstructorInjectionTest.java b/src/test/java/fr/xephi/authme/initialization/ConstructorInjectionTest.java index c53a4321..cc55b4c3 100644 --- a/src/test/java/fr/xephi/authme/initialization/ConstructorInjectionTest.java +++ b/src/test/java/fr/xephi/authme/initialization/ConstructorInjectionTest.java @@ -21,6 +21,7 @@ import static org.junit.Assert.assertThat; */ public class ConstructorInjectionTest { + @SuppressWarnings("unchecked") @Test public void shouldReturnDependencies() { // given diff --git a/src/test/java/fr/xephi/authme/initialization/FieldInjectionTest.java b/src/test/java/fr/xephi/authme/initialization/FieldInjectionTest.java index 9b25565e..b784e4da 100644 --- a/src/test/java/fr/xephi/authme/initialization/FieldInjectionTest.java +++ b/src/test/java/fr/xephi/authme/initialization/FieldInjectionTest.java @@ -127,7 +127,5 @@ public class FieldInjectionTest { private static class NoInjectionClass { - private BetaManager betaManager; - } } diff --git a/src/test/java/fr/xephi/authme/settings/ConfigFileConsistencyTest.java b/src/test/java/fr/xephi/authme/settings/ConfigFileConsistencyTest.java index 8d1c7b4e..1ce7ec41 100644 --- a/src/test/java/fr/xephi/authme/settings/ConfigFileConsistencyTest.java +++ b/src/test/java/fr/xephi/authme/settings/ConfigFileConsistencyTest.java @@ -88,7 +88,7 @@ public class ConfigFileConsistencyTest { // when / then for (Property property : propertyMap.keySet()) { assertThat("Default value of '" + property.getPath() + "' in config.yml should be the same as in Property", - property.getFromFile(configuration), equalTo(property.getDefaultValue())); + property.getFromFile(configuration).equals(property.getDefaultValue()), equalTo(true)); } } From 9e688b410dac6e9f906aa95a00e073f912769bc2 Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Sat, 14 May 2016 00:25:24 +0200 Subject: [PATCH 050/200] Fix #705 Idk why the player becomes null, maybe the isOnline check is enought --- .../java/fr/xephi/authme/listener/AuthMePlayerListener.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java index 7dddaa12..b8419a15 100644 --- a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java +++ b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java @@ -444,7 +444,10 @@ public class AuthMePlayerListener implements Listener { Bukkit.getScheduler().scheduleSyncDelayedTask(plugin, new Runnable() { @Override public void run() { - player.closeInventory(); + // Fix NPE, idk how this is possible -sgdc3 + if(player != null && player.isOnline()) { + player.closeInventory(); + } } }, 1); } From f784da7c2f851ab7599995b279cde8270b9509e8 Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Sat, 14 May 2016 14:33:17 +0200 Subject: [PATCH 051/200] Exclude new transitive dependency --- pom.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pom.xml b/pom.xml index 31537789..b4bdc34d 100644 --- a/pom.xml +++ b/pom.xml @@ -664,6 +664,10 @@ spigot-api org.spigotmc + + jettison + org.codehaus.jettison + From c2c60caa5b1f93d4db81939d6e558714512a8cee Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Sat, 14 May 2016 14:33:30 +0200 Subject: [PATCH 052/200] Revert "Fix #705" This reverts commit 9e688b410dac6e9f906aa95a00e073f912769bc2. --- .../java/fr/xephi/authme/listener/AuthMePlayerListener.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java index b8419a15..7dddaa12 100644 --- a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java +++ b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java @@ -444,10 +444,7 @@ public class AuthMePlayerListener implements Listener { Bukkit.getScheduler().scheduleSyncDelayedTask(plugin, new Runnable() { @Override public void run() { - // Fix NPE, idk how this is possible -sgdc3 - if(player != null && player.isOnline()) { - player.closeInventory(); - } + player.closeInventory(); } }, 1); } From 0fc7674aa44b5b333e2059cf2cff2fd875d3a3a5 Mon Sep 17 00:00:00 2001 From: games647 Date: Sat, 14 May 2016 14:40:35 +0200 Subject: [PATCH 053/200] Introduce isLogin() for backwards compatibility with Plugins like GuiRules (Fixes #705) --- src/main/java/fr/xephi/authme/events/LoginEvent.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/main/java/fr/xephi/authme/events/LoginEvent.java b/src/main/java/fr/xephi/authme/events/LoginEvent.java index 6c2647fd..d36dcc1a 100644 --- a/src/main/java/fr/xephi/authme/events/LoginEvent.java +++ b/src/main/java/fr/xephi/authme/events/LoginEvent.java @@ -30,6 +30,16 @@ public class LoginEvent extends CustomEvent { return player; } + /** + * + * @return + * @deprecated this will always return true because this event is only called if it was successful + */ + @Deprecated + public boolean isLogin() { + return true; + } + /** * Return the list of handlers, equivalent to {@link #getHandlers()} and required by {@link Event}. * From 9db330646e505d60c9c0f2164101ac9668be4930 Mon Sep 17 00:00:00 2001 From: Xephi59 Date: Sun, 15 May 2016 17:52:54 +0200 Subject: [PATCH 054/200] Try to fix #419 --- .../executable/email/RecoverEmailCommand.java | 10 ++-- .../authme/datasource/CacheDataSource.java | 32 ++++++----- .../fr/xephi/authme/datasource/MySQL.java | 43 +++++++------- .../fr/xephi/authme/datasource/SQLite.java | 56 ++++++++++++------- 4 files changed, 81 insertions(+), 60 deletions(-) diff --git a/src/main/java/fr/xephi/authme/command/executable/email/RecoverEmailCommand.java b/src/main/java/fr/xephi/authme/command/executable/email/RecoverEmailCommand.java index aba08584..cb9a4163 100644 --- a/src/main/java/fr/xephi/authme/command/executable/email/RecoverEmailCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/email/RecoverEmailCommand.java @@ -1,5 +1,11 @@ package fr.xephi.authme.command.executable.email; +import java.util.List; + +import javax.inject.Inject; + +import org.bukkit.entity.Player; + import fr.xephi.authme.AuthMe; import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.cache.auth.PlayerAuth; @@ -13,10 +19,6 @@ import fr.xephi.authme.security.RandomString; import fr.xephi.authme.security.crypts.HashedPassword; import fr.xephi.authme.settings.properties.EmailSettings; import fr.xephi.authme.util.StringUtils; -import org.bukkit.entity.Player; - -import javax.inject.Inject; -import java.util.List; public class RecoverEmailCommand extends PlayerCommand { diff --git a/src/main/java/fr/xephi/authme/datasource/CacheDataSource.java b/src/main/java/fr/xephi/authme/datasource/CacheDataSource.java index 7765e61d..12729fc1 100644 --- a/src/main/java/fr/xephi/authme/datasource/CacheDataSource.java +++ b/src/main/java/fr/xephi/authme/datasource/CacheDataSource.java @@ -1,5 +1,11 @@ package fr.xephi.authme.datasource; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + import com.google.common.base.Optional; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; @@ -8,17 +14,12 @@ import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.ThreadFactoryBuilder; + import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.cache.auth.PlayerAuth; import fr.xephi.authme.cache.auth.PlayerCache; import fr.xephi.authme.security.crypts.HashedPassword; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.Callable; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; - /** */ public class CacheDataSource implements DataSource { @@ -41,7 +42,8 @@ public class CacheDataSource implements DataSource { .build()) ); cachedAuths = CacheBuilder.newBuilder() - .refreshAfterWrite(8, TimeUnit.MINUTES) + .refreshAfterWrite(5, TimeUnit.MINUTES) + .expireAfterAccess(15, TimeUnit.MINUTES) .build(new CacheLoader>() { @Override public Optional load(String key) { @@ -110,7 +112,7 @@ public class CacheDataSource implements DataSource { } @Override - public boolean updatePassword(String user, HashedPassword password) { + public synchronized boolean updatePassword(String user, HashedPassword password) { user = user.toLowerCase(); boolean result = source.updatePassword(user, password); if (result) { @@ -120,7 +122,7 @@ public class CacheDataSource implements DataSource { } @Override - public boolean updateSession(PlayerAuth auth) { + public synchronized boolean updateSession(PlayerAuth auth) { boolean result = source.updateSession(auth); if (result) { cachedAuths.refresh(auth.getNickname()); @@ -129,7 +131,7 @@ public class CacheDataSource implements DataSource { } @Override - public boolean updateQuitLoc(final PlayerAuth auth) { + public synchronized boolean updateQuitLoc(final PlayerAuth auth) { boolean result = source.updateQuitLoc(auth); if (result) { cachedAuths.refresh(auth.getNickname()); @@ -199,17 +201,17 @@ public class CacheDataSource implements DataSource { } @Override - public boolean isLogged(String user) { + public synchronized boolean isLogged(String user) { return PlayerCache.getInstance().isAuthenticated(user); } @Override - public void setLogged(final String user) { + public synchronized void setLogged(final String user) { source.setLogged(user.toLowerCase()); } @Override - public void setUnlogged(final String user) { + public synchronized void setUnlogged(final String user) { source.setUnlogged(user.toLowerCase()); } @@ -225,7 +227,7 @@ public class CacheDataSource implements DataSource { } @Override - public boolean updateRealName(String user, String realName) { + public synchronized boolean updateRealName(String user, String realName) { boolean result = source.updateRealName(user, realName); if (result) { cachedAuths.refresh(user); @@ -234,7 +236,7 @@ public class CacheDataSource implements DataSource { } @Override - public boolean updateIp(String user, String ip) { + public synchronized boolean updateIp(String user, String ip) { boolean result = source.updateIp(user, ip); if (result) { cachedAuths.refresh(user); diff --git a/src/main/java/fr/xephi/authme/datasource/MySQL.java b/src/main/java/fr/xephi/authme/datasource/MySQL.java index 4f6317bc..9e274e5e 100644 --- a/src/main/java/fr/xephi/authme/datasource/MySQL.java +++ b/src/main/java/fr/xephi/authme/datasource/MySQL.java @@ -1,20 +1,5 @@ package fr.xephi.authme.datasource; -import com.google.common.annotations.VisibleForTesting; -import com.zaxxer.hikari.HikariDataSource; -import com.zaxxer.hikari.pool.HikariPool.PoolInitializationException; -import fr.xephi.authme.ConsoleLogger; -import fr.xephi.authme.cache.auth.PlayerAuth; -import fr.xephi.authme.security.HashAlgorithm; -import fr.xephi.authme.security.crypts.HashedPassword; -import fr.xephi.authme.security.crypts.XFBCRYPT; -import fr.xephi.authme.settings.NewSetting; -import fr.xephi.authme.settings.Settings; -import fr.xephi.authme.settings.properties.DatabaseSettings; -import fr.xephi.authme.settings.properties.HooksSettings; -import fr.xephi.authme.settings.properties.SecuritySettings; -import fr.xephi.authme.util.StringUtils; - import java.sql.Blob; import java.sql.Connection; import java.sql.DatabaseMetaData; @@ -26,6 +11,22 @@ import java.sql.Types; import java.util.ArrayList; import java.util.List; +import com.google.common.annotations.VisibleForTesting; +import com.zaxxer.hikari.HikariDataSource; +import com.zaxxer.hikari.pool.HikariPool.PoolInitializationException; + +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.cache.auth.PlayerAuth; +import fr.xephi.authme.security.HashAlgorithm; +import fr.xephi.authme.security.crypts.HashedPassword; +import fr.xephi.authme.security.crypts.XFBCRYPT; +import fr.xephi.authme.settings.NewSetting; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.properties.DatabaseSettings; +import fr.xephi.authme.settings.properties.HooksSettings; +import fr.xephi.authme.settings.properties.SecuritySettings; +import fr.xephi.authme.util.StringUtils; + /** */ public class MySQL implements DataSource { @@ -535,7 +536,7 @@ public class MySQL implements DataSource { } @Override - public boolean updatePassword(String user, HashedPassword password) { + public synchronized boolean updatePassword(String user, HashedPassword password) { user = user.toLowerCase(); try (Connection con = getConnection()) { boolean useSalt = !col.SALT.isEmpty(); @@ -756,7 +757,7 @@ public class MySQL implements DataSource { } @Override - public boolean isLogged(String user) { + public synchronized boolean isLogged(String user) { String sql = "SELECT " + col.IS_LOGGED + " FROM " + tableName + " WHERE " + col.NAME + "=?;"; try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) { pst.setString(1, user); @@ -770,7 +771,7 @@ public class MySQL implements DataSource { } @Override - public void setLogged(String user) { + public synchronized void setLogged(String user) { String sql = "UPDATE " + tableName + " SET " + col.IS_LOGGED + "=? WHERE " + col.NAME + "=?;"; try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) { pst.setInt(1, 1); @@ -782,7 +783,7 @@ public class MySQL implements DataSource { } @Override - public void setUnlogged(String user) { + public synchronized void setUnlogged(String user) { String sql = "UPDATE " + tableName + " SET " + col.IS_LOGGED + "=? WHERE " + col.NAME + "=?;"; try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) { pst.setInt(1, 0); @@ -822,7 +823,7 @@ public class MySQL implements DataSource { } @Override - public boolean updateRealName(String user, String realName) { + public synchronized boolean updateRealName(String user, String realName) { String sql = "UPDATE " + tableName + " SET " + col.REAL_NAME + "=? WHERE " + col.NAME + "=?;"; try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) { pst.setString(1, realName); @@ -836,7 +837,7 @@ public class MySQL implements DataSource { } @Override - public boolean updateIp(String user, String ip) { + public synchronized boolean updateIp(String user, String ip) { String sql = "UPDATE " + tableName + " SET " + col.IP + "=? WHERE " + col.NAME + "=?;"; try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) { pst.setString(1, ip); diff --git a/src/main/java/fr/xephi/authme/datasource/SQLite.java b/src/main/java/fr/xephi/authme/datasource/SQLite.java index dfc74740..fe670813 100644 --- a/src/main/java/fr/xephi/authme/datasource/SQLite.java +++ b/src/main/java/fr/xephi/authme/datasource/SQLite.java @@ -1,14 +1,5 @@ package fr.xephi.authme.datasource; -import com.google.common.annotations.VisibleForTesting; -import fr.xephi.authme.ConsoleLogger; -import fr.xephi.authme.cache.auth.PlayerAuth; -import fr.xephi.authme.security.crypts.HashedPassword; -import fr.xephi.authme.settings.NewSetting; -import fr.xephi.authme.settings.Settings; -import fr.xephi.authme.settings.properties.DatabaseSettings; -import fr.xephi.authme.util.StringUtils; - import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; @@ -18,6 +9,16 @@ import java.sql.Statement; import java.util.ArrayList; import java.util.List; +import com.google.common.annotations.VisibleForTesting; + +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.cache.auth.PlayerAuth; +import fr.xephi.authme.security.crypts.HashedPassword; +import fr.xephi.authme.settings.NewSetting; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.properties.DatabaseSettings; +import fr.xephi.authme.util.StringUtils; + /** */ public class SQLite implements DataSource { @@ -65,7 +66,6 @@ public class SQLite implements DataSource { Class.forName("org.sqlite.JDBC"); ConsoleLogger.info("SQLite driver loaded"); this.con = DriverManager.getConnection("jdbc:sqlite:plugins/AuthMe/" + database + ".db"); - } private synchronized void setup() throws SQLException { @@ -131,7 +131,13 @@ public class SQLite implements DataSource { @Override public void reload() { - // TODO 20160309: Implement reloading + close(con); + try { + this.connect(); + this.setup(); + } catch (ClassNotFoundException | SQLException ex) { + ConsoleLogger.logException("Error during SQLite initialization:", ex); + } } @Override @@ -239,7 +245,7 @@ public class SQLite implements DataSource { } @Override - public boolean updatePassword(String user, HashedPassword password) { + public synchronized boolean updatePassword(String user, HashedPassword password) { user = user.toLowerCase(); PreparedStatement pst = null; try { @@ -266,7 +272,7 @@ public class SQLite implements DataSource { } @Override - public boolean updateSession(PlayerAuth auth) { + public synchronized boolean updateSession(PlayerAuth auth) { PreparedStatement pst = null; try { pst = con.prepareStatement("UPDATE " + tableName + " SET " + col.IP + "=?, " + col.LAST_LOGIN + "=?, " + col.REAL_NAME + "=? WHERE " + col.NAME + "=?;"); @@ -322,7 +328,7 @@ public class SQLite implements DataSource { } @Override - public boolean updateQuitLoc(PlayerAuth auth) { + public synchronized boolean updateQuitLoc(PlayerAuth auth) { PreparedStatement pst = null; try { pst = con.prepareStatement("UPDATE " + tableName + " SET " + col.LASTLOC_X + "=?, " + col.LASTLOC_Y + "=?, " + col.LASTLOC_Z + "=?, " + col.LASTLOC_WORLD + "=? WHERE " + col.NAME + "=?;"); @@ -342,7 +348,7 @@ public class SQLite implements DataSource { } @Override - public boolean updateEmail(PlayerAuth auth) { + public synchronized boolean updateEmail(PlayerAuth auth) { String sql = "UPDATE " + tableName + " SET " + col.EMAIL + "=? WHERE " + col.NAME + "=?;"; try (PreparedStatement pst = con.prepareStatement(sql)) { pst.setString(1, auth.getEmail()); @@ -376,6 +382,16 @@ public class SQLite implements DataSource { } } + private void close(Connection con) { + if (con != null) { + try { + con.close(); + } catch (SQLException ex) { + logSqlException(ex); + } + } + } + private void close(ResultSet rs) { if (rs != null) { try { @@ -443,7 +459,7 @@ public class SQLite implements DataSource { } @Override - public boolean isLogged(String user) { + public synchronized boolean isLogged(String user) { PreparedStatement pst = null; ResultSet rs = null; try { @@ -462,7 +478,7 @@ public class SQLite implements DataSource { } @Override - public void setLogged(String user) { + public synchronized void setLogged(String user) { PreparedStatement pst = null; try { pst = con.prepareStatement("UPDATE " + tableName + " SET " + col.IS_LOGGED + "=? WHERE LOWER(" + col.NAME + ")=?;"); @@ -477,7 +493,7 @@ public class SQLite implements DataSource { } @Override - public void setUnlogged(String user) { + public synchronized void setUnlogged(String user) { PreparedStatement pst = null; if (user != null) try { @@ -521,7 +537,7 @@ public class SQLite implements DataSource { } @Override - public boolean updateRealName(String user, String realName) { + public synchronized boolean updateRealName(String user, String realName) { String sql = "UPDATE " + tableName + " SET " + col.REAL_NAME + "=? WHERE " + col.NAME + "=?;"; try (PreparedStatement pst = con.prepareStatement(sql)) { pst.setString(1, realName); @@ -535,7 +551,7 @@ public class SQLite implements DataSource { } @Override - public boolean updateIp(String user, String ip) { + public synchronized boolean updateIp(String user, String ip) { String sql = "UPDATE " + tableName + " SET " + col.IP + "=? WHERE " + col.NAME + "=?;"; try (PreparedStatement pst = con.prepareStatement(sql)) { pst.setString(1, ip); From 3ad76b8ec50c713e60e67063609d4b70046fd950 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Mon, 16 May 2016 17:50:26 +0200 Subject: [PATCH 055/200] #712 fr messages - add untranslated messages and minor fixes --- src/main/resources/messages/messages_fr.yml | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/main/resources/messages/messages_fr.yml b/src/main/resources/messages/messages_fr.yml index 153cceb4..53efa63a 100644 --- a/src/main/resources/messages/messages_fr.yml +++ b/src/main/resources/messages/messages_fr.yml @@ -2,13 +2,13 @@ unknown_user: '&fUtilisateur non enregistré' unsafe_spawn: '&fTéléportation dans un endroit sûr' not_logged_in: '&cNon connecté!' -reg_voluntarily: '&fVous venez d''arriver? faites un "/register motdepasse confirmermotdepasse"' +reg_voluntarily: '&fVous venez d''arriver? Faites un "/register motdepasse confirmermotdepasse"' usage_log: '&cUtilisez: /login motdepasse' wrong_pwd: '&cMauvais MotdePasse' unregistered: '&cCe compte a été supprimé!' reg_disabled: '&cL''enregistrement est désactivé' valid_session: '&cVous êtes authentifié' -login: '&cConnection effectuée!' +login: '&cConnexion effectuée!' vb_nonActiv: '&fCe compte n''est pas actif, consultez vos emails!' user_regged: '&cCe nom est deja utilisé.' usage_reg: '&cUtilisez la commande /register motdepasse confirmermotdepasse' @@ -25,7 +25,7 @@ pwd_changed: '&cMotdePasse changé avec succès!' user_unknown: '&c Ce compte n''est pas enregistré' password_error: '&fCe mot de passe est incorrect' invalid_session: '&fSession invalide, relancez le jeu ou attendez la fin de la session' -reg_only: '&fSeul les joueurs enregistré sont admis! Visite http://example.com' +reg_only: '&fSeul les joueurs enregistrés sont admis! Visitez http://example.com' logged_in: '&cVous êtes déjà connecté!' logout: '&cVous avez été déconnecté!' same_nick: '&fUne personne ayant ce même pseudo joue déjà.' @@ -37,14 +37,14 @@ usage_changepassword: '&fPour changer de mot de passe, utilisez: /changepassword name_len: '&cVotre pseudo est trop long ou trop court' regex: '&cCaractères autorisés: REG_EX' add_email: '&cMerci d''ajouter votre email : /email add yourEmail confirmEmail' -recovery_email: '&cVous avez oublié votre MotdePasse? Utilisez /email recovery ' +recovery_email: '&cVous avez oublié votre MotdePasse? Utilisez /email recovery ' usage_captcha: '&cTrop de tentatives de connexion échouées, utilisez: /captcha ' wrong_captcha: '&cCaptcha incorrect, écrivez de nouveau : /captcha THE_CAPTCHA' valid_captcha: '&cLe Captcha est valide, merci!' kick_forvip: '&cUn joueur VIP a rejoint le serveur plein!' kick_fullserver: '&cLe serveur est actuellement plein, désolé!' usage_email_add: '&fUsage: /email add ' -usage_email_change: '&fUsage: /email change ' +usage_email_change: '&fUsage: /email change ' usage_email_recovery: '&fUsage: /email recovery ' new_email_invalid: '[AuthMe] Nouvel email invalide!' old_email_invalid: '[AuthMe] Ancien email invalide!' @@ -54,12 +54,11 @@ email_confirm: '[AuthMe] Confirmez votre email !' email_changed: '[AuthMe] Email changé !' email_send: '[AuthMe] Email de récupération envoyé!' country_banned: 'Votre pays est banni de ce serveur' -antibot_auto_enabled: '[AuthMe] AntiBotMod a été activé automatiquement à cause de nombreuses connections!' +antibot_auto_enabled: '[AuthMe] AntiBotMod a été activé automatiquement à cause de nombreuses connexions!' antibot_auto_disabled: '[AuthMe] AntiBotMod a été désactivé automatiquement après %m minutes, espérons que l''invasion soit arrêtée!' kick_antibot: 'AntiBotMod est activé ! Veuillez attendre quelques minutes avant de joindre le serveur.' email_exists: '&cUn email de restauration a déjà été envoyé ! Vous pouvez le jeter et vous en faire envoyez un nouveau en utilisant :' -# TODO two_factor_create: Missing tag %url -two_factor_create: '&2Votre code secret est %code' -# TODO email_already_used: '&4The email address is already being used' -# TODO invalid_name_case: 'You should join using username %valid, not %invalid.' -# TODO not_owner_error: 'You are not the owner of this account. Please try another name!' \ No newline at end of file +two_factor_create: '&2Votre code secret est %code. Vous pouvez le scanner depuis %url' +email_already_used: '&4L''adresse email a déjà été utilisée' +not_owner_error: 'Vous n''êtes pas le propriétaire de ce compte. Veuillez utiliser un autre nom !' +invalid_name_case: 'Veuillez vous connecter avec %valid et non pas avec %invalid.' From f5c89e897f894c6accff77439e5eda9d28ac9973 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Tue, 17 May 2016 19:49:06 +0200 Subject: [PATCH 056/200] #707 Convert async processes as services (work in progress - rough, untested changes) --- .../fr/xephi/authme/process/Management.java | 91 +++++++++++---- .../fr/xephi/authme/process/NewProcess.java | 7 ++ .../xephi/authme/process/ProcessService.java | 36 ------ .../authme/process/email/AsyncAddEmail.java | 35 +++--- .../process/email/AsyncChangeEmail.java | 41 +++---- .../authme/process/join/AsynchronousJoin.java | 79 +++++++------ .../process/login/AsynchronousLogin.java | 105 +++++++++--------- .../process/logout/AsynchronousLogout.java | 65 +++++------ .../authme/process/quit/AsynchronousQuit.java | 59 +++++----- .../process/register/AsyncRegister.java | 64 +++++------ .../unregister/AsynchronousUnregister.java | 82 ++++++-------- .../authme/process/ProcessServiceTest.java | 27 ----- .../process/email/AsyncAddEmailTest.java | 41 +++---- .../process/email/AsyncChangeEmailTest.java | 39 +++---- 14 files changed, 359 insertions(+), 412 deletions(-) create mode 100644 src/main/java/fr/xephi/authme/process/NewProcess.java diff --git a/src/main/java/fr/xephi/authme/process/Management.java b/src/main/java/fr/xephi/authme/process/Management.java index cb3efa81..2cd0b388 100644 --- a/src/main/java/fr/xephi/authme/process/Management.java +++ b/src/main/java/fr/xephi/authme/process/Management.java @@ -1,9 +1,5 @@ package fr.xephi.authme.process; -import fr.xephi.authme.AuthMe; -import fr.xephi.authme.cache.auth.PlayerCache; -import fr.xephi.authme.datasource.DataSource; -import fr.xephi.authme.hooks.PluginHooks; import fr.xephi.authme.process.email.AsyncAddEmail; import fr.xephi.authme.process.email.AsyncChangeEmail; import fr.xephi.authme.process.join.AsynchronousJoin; @@ -12,63 +8,110 @@ import fr.xephi.authme.process.logout.AsynchronousLogout; import fr.xephi.authme.process.quit.AsynchronousQuit; import fr.xephi.authme.process.register.AsyncRegister; import fr.xephi.authme.process.unregister.AsynchronousUnregister; +import fr.xephi.authme.util.BukkitService; import org.bukkit.entity.Player; -import org.bukkit.scheduler.BukkitScheduler; import javax.inject.Inject; -/** - */ + public class Management { @Inject - private AuthMe plugin; + private BukkitService bukkitService; + + // Processes @Inject - private BukkitScheduler sched; + private AsyncAddEmail asyncAddEmail; @Inject - private ProcessService processService; + private AsyncChangeEmail asyncChangeEmail; @Inject - private DataSource dataSource; + private AsynchronousLogout asynchronousLogout; @Inject - private PlayerCache playerCache; + private AsynchronousQuit asynchronousQuit; @Inject - private PluginHooks pluginHooks; + private AsynchronousJoin asynchronousJoin; + @Inject + private AsyncRegister asyncRegister; + @Inject + private AsynchronousLogin asynchronousLogin; + @Inject + private AsynchronousUnregister asynchronousUnregister; Management() { } public void performLogin(final Player player, final String password, final boolean forceLogin) { - runTask(new AsynchronousLogin(player, password, forceLogin, plugin, dataSource, processService)); + runTask(new Runnable() { + @Override + public void run() { + asynchronousLogin.login(player, password, forceLogin); + } + }); } public void performLogout(final Player player) { - runTask(new AsynchronousLogout(player, plugin, dataSource, processService)); + runTask(new Runnable() { + @Override + public void run() { + asynchronousLogout.logout(player); + } + }); } public void performRegister(final Player player, final String password, final String email, final boolean autoLogin) { - runTask(new AsyncRegister(player, password, email, plugin, dataSource, playerCache, processService, autoLogin)); + runTask(new Runnable() { + @Override + public void run() { + asyncRegister.register(player, password, email, autoLogin); + } + }); } - public void performUnregister(final Player player, final String password, final boolean force) { - runTask(new AsynchronousUnregister(player, password, force, plugin, processService)); + public void performUnregister(final Player player, final String password, final boolean isForce) { + runTask(new Runnable() { + @Override + public void run() { + asynchronousUnregister.unregister(player, password, isForce); + } + }); } public void performJoin(final Player player) { - runTask(new AsynchronousJoin(player, plugin, dataSource, playerCache, pluginHooks, processService)); + runTask(new Runnable() { + @Override + public void run() { + asynchronousJoin.processJoin(player); + } + }); } public void performQuit(final Player player, final boolean isKick) { - runTask(new AsynchronousQuit(player, plugin, dataSource, isKick, processService)); + runTask(new Runnable() { + @Override + public void run() { + asynchronousQuit.processQuit(player, isKick); + } + }); } public void performAddEmail(final Player player, final String newEmail) { - runTask(new AsyncAddEmail(player, newEmail, dataSource, playerCache, processService)); + runTask(new Runnable() { + @Override + public void run() { + asyncAddEmail.addEmail(player, newEmail); + } + }); } public void performChangeEmail(final Player player, final String oldEmail, final String newEmail) { - runTask(new AsyncChangeEmail(player, oldEmail, newEmail, dataSource, playerCache, processService)); + runTask(new Runnable() { + @Override + public void run() { + asyncChangeEmail.changeEmail(player, oldEmail, newEmail); + } + }); } - private void runTask(Process process) { - sched.runTaskAsynchronously(plugin, process); + private void runTask(Runnable runnable) { + bukkitService.runTaskAsynchronously(runnable); } } diff --git a/src/main/java/fr/xephi/authme/process/NewProcess.java b/src/main/java/fr/xephi/authme/process/NewProcess.java new file mode 100644 index 00000000..524f2c77 --- /dev/null +++ b/src/main/java/fr/xephi/authme/process/NewProcess.java @@ -0,0 +1,7 @@ +package fr.xephi.authme.process; + +/** + * Marker interface for AuthMe processes. + */ +public interface NewProcess { +} diff --git a/src/main/java/fr/xephi/authme/process/ProcessService.java b/src/main/java/fr/xephi/authme/process/ProcessService.java index a338cad4..e8f0a73b 100644 --- a/src/main/java/fr/xephi/authme/process/ProcessService.java +++ b/src/main/java/fr/xephi/authme/process/ProcessService.java @@ -1,14 +1,11 @@ package fr.xephi.authme.process; import fr.xephi.authme.AuthMe; -import fr.xephi.authme.datasource.DataSource; -import fr.xephi.authme.hooks.PluginHooks; import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.output.Messages; import fr.xephi.authme.security.PasswordSecurity; import fr.xephi.authme.security.crypts.HashedPassword; import fr.xephi.authme.settings.NewSetting; -import fr.xephi.authme.settings.SpawnLoader; import fr.xephi.authme.settings.domain.Property; import fr.xephi.authme.util.BukkitService; import fr.xephi.authme.util.ValidationService; @@ -32,14 +29,8 @@ public class ProcessService { @Inject private AuthMe authMe; @Inject - private DataSource dataSource; - @Inject private PasswordSecurity passwordSecurity; @Inject - private PluginHooks pluginHooks; - @Inject - private SpawnLoader spawnLoader; - @Inject private ValidationService validationService; @Inject private BukkitService bukkitService; @@ -165,33 +156,6 @@ public class ProcessService { return passwordSecurity.computeHash(password, username); } - /** - * Return the PluginHooks manager. - * - * @return PluginHooks instance - */ - public PluginHooks getPluginHooks() { - return pluginHooks; - } - - /** - * Return the spawn manager. - * - * @return SpawnLoader instance - */ - public SpawnLoader getSpawnLoader() { - return spawnLoader; - } - - /** - * Return the plugin's datasource. - * - * @return the datasource - */ - public DataSource getDataSource() { - return dataSource; - } - /** * Verifies whether a password is valid according to the plugin settings. * diff --git a/src/main/java/fr/xephi/authme/process/email/AsyncAddEmail.java b/src/main/java/fr/xephi/authme/process/email/AsyncAddEmail.java index 282f8f09..f4f1ed87 100644 --- a/src/main/java/fr/xephi/authme/process/email/AsyncAddEmail.java +++ b/src/main/java/fr/xephi/authme/process/email/AsyncAddEmail.java @@ -5,33 +5,30 @@ import fr.xephi.authme.cache.auth.PlayerAuth; import fr.xephi.authme.cache.auth.PlayerCache; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.output.MessageKey; -import fr.xephi.authme.process.Process; +import fr.xephi.authme.process.NewProcess; import fr.xephi.authme.process.ProcessService; import fr.xephi.authme.settings.properties.RegistrationSettings; import org.bukkit.entity.Player; +import javax.inject.Inject; + /** * Async task to add an email to an account. */ -public class AsyncAddEmail implements Process { +public class AsyncAddEmail implements NewProcess { - private final Player player; - private final String email; - private final ProcessService service; - private final DataSource dataSource; - private final PlayerCache playerCache; + @Inject + private ProcessService service; - public AsyncAddEmail(Player player, String email, DataSource dataSource, PlayerCache playerCache, - ProcessService service) { - this.player = player; - this.email = email; - this.dataSource = dataSource; - this.playerCache = playerCache; - this.service = service; - } + @Inject + private DataSource dataSource; - @Override - public void run() { + @Inject + private PlayerCache playerCache; + + AsyncAddEmail() { } + + public void addEmail(Player player, String email) { String playerName = player.getName().toLowerCase(); if (playerCache.isAuthenticated(playerName)) { @@ -55,11 +52,11 @@ public class AsyncAddEmail implements Process { } } } else { - sendUnloggedMessage(dataSource); + sendUnloggedMessage(player); } } - private void sendUnloggedMessage(DataSource dataSource) { + private void sendUnloggedMessage(Player player) { if (dataSource.isAuthAvailable(player.getName())) { service.send(player, MessageKey.LOGIN_MESSAGE); } else if (service.getProperty(RegistrationSettings.USE_EMAIL_REGISTRATION)) { diff --git a/src/main/java/fr/xephi/authme/process/email/AsyncChangeEmail.java b/src/main/java/fr/xephi/authme/process/email/AsyncChangeEmail.java index 7489327b..2f4e90b5 100644 --- a/src/main/java/fr/xephi/authme/process/email/AsyncChangeEmail.java +++ b/src/main/java/fr/xephi/authme/process/email/AsyncChangeEmail.java @@ -4,35 +4,30 @@ import fr.xephi.authme.cache.auth.PlayerAuth; import fr.xephi.authme.cache.auth.PlayerCache; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.output.MessageKey; -import fr.xephi.authme.process.Process; +import fr.xephi.authme.process.NewProcess; import fr.xephi.authme.process.ProcessService; import fr.xephi.authme.settings.properties.RegistrationSettings; import org.bukkit.entity.Player; +import javax.inject.Inject; + /** * Async task for changing the email. */ -public class AsyncChangeEmail implements Process { +public class AsyncChangeEmail implements NewProcess { - private final Player player; - private final String oldEmail; - private final String newEmail; - private final ProcessService service; - private final PlayerCache playerCache; - private final DataSource dataSource; + @Inject + private ProcessService service; - public AsyncChangeEmail(Player player, String oldEmail, String newEmail, DataSource dataSource, - PlayerCache playerCache, ProcessService service) { - this.player = player; - this.oldEmail = oldEmail; - this.newEmail = newEmail; - this.playerCache = playerCache; - this.dataSource = dataSource; - this.service = service; - } + @Inject + private PlayerCache playerCache; - @Override - public void run() { + @Inject + private DataSource dataSource; + + AsyncChangeEmail() { } + + public void changeEmail(Player player, String oldEmail, String newEmail) { String playerName = player.getName().toLowerCase(); if (playerCache.isAuthenticated(playerName)) { PlayerAuth auth = playerCache.getAuth(playerName); @@ -47,14 +42,14 @@ public class AsyncChangeEmail implements Process { } else if (!service.isEmailFreeForRegistration(newEmail, player)) { service.send(player, MessageKey.EMAIL_ALREADY_USED_ERROR); } else { - saveNewEmail(auth); + saveNewEmail(auth, player, newEmail); } } else { - outputUnloggedMessage(); + outputUnloggedMessage(player); } } - private void saveNewEmail(PlayerAuth auth) { + private void saveNewEmail(PlayerAuth auth, Player player, String newEmail) { auth.setEmail(newEmail); if (dataSource.updateEmail(auth)) { playerCache.updatePlayer(auth); @@ -64,7 +59,7 @@ public class AsyncChangeEmail implements Process { } } - private void outputUnloggedMessage() { + private void outputUnloggedMessage(Player player) { if (dataSource.isAuthAvailable(player.getName())) { service.send(player, MessageKey.LOGIN_MESSAGE); } else if (service.getProperty(RegistrationSettings.USE_EMAIL_REGISTRATION)) { diff --git a/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java b/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java index 3aff8941..f3504475 100644 --- a/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java +++ b/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java @@ -14,9 +14,10 @@ import fr.xephi.authme.hooks.PluginHooks; import fr.xephi.authme.listener.AuthMePlayerListener; import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.permission.PlayerStatePermission; -import fr.xephi.authme.process.Process; +import fr.xephi.authme.process.NewProcess; import fr.xephi.authme.process.ProcessService; import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.SpawnLoader; import fr.xephi.authme.settings.properties.HooksSettings; import fr.xephi.authme.settings.properties.PluginSettings; import fr.xephi.authme.settings.properties.RegistrationSettings; @@ -35,42 +36,48 @@ import org.bukkit.potion.PotionEffect; import org.bukkit.potion.PotionEffectType; import org.bukkit.scheduler.BukkitTask; +import javax.inject.Inject; + import static fr.xephi.authme.settings.properties.RestrictionSettings.PROTECT_INVENTORY_BEFORE_LOGIN; /** */ -public class AsynchronousJoin implements Process { +public class AsynchronousJoin implements NewProcess { - private final AuthMe plugin; - private final Player player; - private final DataSource database; - private final String name; - private final ProcessService service; - private final PlayerCache playerCache; - private final PluginHooks pluginHooks; + @Inject + private AuthMe plugin; - private final boolean disableCollisions = MethodUtils + @Inject + private DataSource database; + + @Inject + private ProcessService service; + + @Inject + private PlayerCache playerCache; + + @Inject + private LimboCache limboCache; + + @Inject + private PluginHooks pluginHooks; + + @Inject + private SpawnLoader spawnLoader; + + private static final boolean DISABLE_COLLISIONS = MethodUtils .getAccessibleMethod(LivingEntity.class, "setCollidable", new Class[]{}) != null; - public AsynchronousJoin(Player player, AuthMe plugin, DataSource database, PlayerCache playerCache, - PluginHooks pluginHooks, ProcessService service) { - this.player = player; - this.plugin = plugin; - this.database = database; - this.name = player.getName().toLowerCase(); - this.service = service; - this.playerCache = playerCache; - this.pluginHooks = pluginHooks; - } + AsynchronousJoin() { } - @Override - public void run() { + public void processJoin(final Player player) { if (Utils.isUnrestricted(player)) { return; } + final String name = player.getName().toLowerCase(); if (service.getProperty(HooksSettings.DISABLE_SOCIAL_SPY)) { - service.getPluginHooks().setEssentialsSocialSpyStatus(player, false); + pluginHooks.setEssentialsSocialSpyStatus(player, false); } final String ip = Utils.getPlayerIp(player); @@ -101,10 +108,10 @@ public class AsynchronousJoin implements Process { return; } // Prevent player collisions in 1.9 - if (disableCollisions) { + if (DISABLE_COLLISIONS) { ((LivingEntity) player).setCollidable(false); } - final Location spawnLoc = service.getSpawnLoader().getSpawnLocation(player); + final Location spawnLoc = spawnLoader.getSpawnLocation(player); final boolean isAuthAvailable = database.isAuthAvailable(name); if (isAuthAvailable) { if (!service.getProperty(RestrictionSettings.NO_TELEPORT)) { @@ -123,7 +130,7 @@ public class AsynchronousJoin implements Process { } } placePlayerSafely(player, spawnLoc); - LimboCache.getInstance().updateLimboPlayer(player); + limboCache.updateLimboPlayer(player); // protect inventory if (service.getProperty(PROTECT_INVENTORY_BEFORE_LOGIN) && plugin.inventoryProtector != null) { @@ -144,7 +151,7 @@ public class AsynchronousJoin implements Process { } PlayerAuth auth = database.getAuth(name); database.setUnlogged(name); - PlayerCache.getInstance().removePlayer(name); + playerCache.removePlayer(name); if (auth != null && auth.getIp().equals(ip)) { service.send(player, MessageKey.SESSION_RECONNECTION); plugin.getManagement().performLogin(player, "dontneed", true); @@ -161,12 +168,12 @@ public class AsynchronousJoin implements Process { return; } - if (!Settings.noTeleport && !needFirstSpawn() && Settings.isTeleportToSpawnEnabled + if (!Settings.noTeleport && !needFirstSpawn(player) && Settings.isTeleportToSpawnEnabled || (Settings.isForceSpawnLocOnJoinEnabled && Settings.getForcedWorlds.contains(player.getWorld().getName()))) { service.scheduleSyncDelayedTask(new Runnable() { @Override public void run() { - SpawnTeleportEvent tpEvent = new SpawnTeleportEvent(player, player.getLocation(), spawnLoc, PlayerCache.getInstance().isAuthenticated(name)); + SpawnTeleportEvent tpEvent = new SpawnTeleportEvent(player, player.getLocation(), spawnLoc, playerCache.isAuthenticated(name)); service.callEvent(tpEvent); if (!tpEvent.isCancelled() && player.isOnline() && tpEvent.getTo() != null && tpEvent.getTo().getWorld() != null) { @@ -177,8 +184,8 @@ public class AsynchronousJoin implements Process { } } - if (!LimboCache.getInstance().hasLimboPlayer(name)) { - LimboCache.getInstance().addLimboPlayer(player); + if (!limboCache.hasLimboPlayer(name)) { + limboCache.addLimboPlayer(player); } Utils.setGroup(player, isAuthAvailable ? GroupType.NOTLOGGEDIN : GroupType.UNREGISTERED); @@ -209,7 +216,7 @@ public class AsynchronousJoin implements Process { int msgInterval = service.getProperty(RegistrationSettings.MESSAGE_INTERVAL); if (registrationTimeout > 0) { BukkitTask id = service.runTaskLater(new TimeoutTask(plugin, name, player), registrationTimeout); - LimboPlayer limboPlayer = LimboCache.getInstance().getLimboPlayer(name); + LimboPlayer limboPlayer = limboCache.getLimboPlayer(name); if (limboPlayer != null) { limboPlayer.setTimeoutTask(id); } @@ -223,21 +230,21 @@ public class AsynchronousJoin implements Process { ? MessageKey.REGISTER_EMAIL_MESSAGE : MessageKey.REGISTER_MESSAGE; } - if (msgInterval > 0 && LimboCache.getInstance().getLimboPlayer(name) != null) { + if (msgInterval > 0 && limboCache.getLimboPlayer(name) != null) { BukkitTask msgTask = service.runTask(new MessageTask(service.getBukkitService(), plugin.getMessages(), name, msg, msgInterval)); - LimboPlayer limboPlayer = LimboCache.getInstance().getLimboPlayer(name); + LimboPlayer limboPlayer = limboCache.getLimboPlayer(name); if (limboPlayer != null) { limboPlayer.setMessageTask(msgTask); } } } - private boolean needFirstSpawn() { + private boolean needFirstSpawn(final Player player) { if (player.hasPlayedBefore()) { return false; } - Location firstSpawn = service.getSpawnLoader().getFirstSpawn(); + Location firstSpawn = spawnLoader.getFirstSpawn(); if (firstSpawn == null) { return false; } diff --git a/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java b/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java index da98a05b..1bbc6baf 100644 --- a/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java +++ b/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java @@ -10,9 +10,10 @@ import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.events.AuthMeAsyncPreLoginEvent; import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.permission.AdminPermission; +import fr.xephi.authme.permission.PermissionsManager; import fr.xephi.authme.permission.PlayerPermission; import fr.xephi.authme.permission.PlayerStatePermission; -import fr.xephi.authme.process.Process; +import fr.xephi.authme.process.NewProcess; import fr.xephi.authme.process.ProcessService; import fr.xephi.authme.security.RandomString; import fr.xephi.authme.settings.Settings; @@ -22,43 +23,41 @@ import fr.xephi.authme.settings.properties.RegistrationSettings; import fr.xephi.authme.settings.properties.RestrictionSettings; import fr.xephi.authme.settings.properties.SecuritySettings; import fr.xephi.authme.task.MessageTask; -import fr.xephi.authme.util.BukkitService; import fr.xephi.authme.util.StringUtils; import fr.xephi.authme.util.Utils; import org.bukkit.Bukkit; import org.bukkit.entity.Player; import org.bukkit.scheduler.BukkitTask; +import javax.inject.Inject; import java.util.List; /** */ -public class AsynchronousLogin implements Process { +public class AsynchronousLogin implements NewProcess { - private final Player player; - private final String name; - private final String realName; - private final String password; - private final boolean forceLogin; - private final AuthMe plugin; - private final DataSource database; - private final String ip; - private final ProcessService service; + @Inject + private AuthMe plugin; - public AsynchronousLogin(Player player, String password, boolean forceLogin, AuthMe plugin, DataSource data, - ProcessService service) { - this.player = player; - this.name = player.getName().toLowerCase(); - this.password = password; - this.realName = player.getName(); - this.forceLogin = forceLogin; - this.plugin = plugin; - this.database = data; - this.ip = Utils.getPlayerIp(player); - this.service = service; - } + @Inject + private DataSource database; - private boolean needsCaptcha() { + @Inject + private ProcessService service; + + @Inject + private PermissionsManager permissionsManager; + + @Inject + private PlayerCache playerCache; + + @Inject + private LimboCache limboCache; + + AsynchronousLogin() { } + + private boolean needsCaptcha(Player player) { + final String name = player.getName().toLowerCase(); if (service.getProperty(SecuritySettings.USE_CAPTCHA)) { if (!plugin.captcha.containsKey(name)) { plugin.captcha.putIfAbsent(name, 1); @@ -82,8 +81,9 @@ public class AsynchronousLogin implements Process { * * @return PlayerAuth */ - private PlayerAuth preAuth() { - if (PlayerCache.getInstance().isAuthenticated(name)) { + private PlayerAuth preAuth(Player player) { + final String name = player.getName().toLowerCase(); + if (playerCache.isAuthenticated(name)) { service.send(player, MessageKey.ALREADY_LOGGED_IN_ERROR); return null; } @@ -92,7 +92,7 @@ public class AsynchronousLogin implements Process { if (pAuth == null) { service.send(player, MessageKey.USER_NOT_REGISTERED); - LimboPlayer limboPlayer = LimboCache.getInstance().getLimboPlayer(name); + LimboPlayer limboPlayer = limboCache.getLimboPlayer(name); if (limboPlayer != null) { limboPlayer.getMessageTask().cancel(); String[] msg = service.getProperty(RegistrationSettings.USE_EMAIL_REGISTRATION) @@ -110,9 +110,10 @@ public class AsynchronousLogin implements Process { return null; } + final String ip = Utils.getPlayerIp(player); if (Settings.getMaxLoginPerIp > 0 - && !plugin.getPermissionsManager().hasPermission(player, PlayerStatePermission.ALLOW_MULTIPLE_ACCOUNTS) - && !ip.equalsIgnoreCase("127.0.0.1") && !ip.equalsIgnoreCase("localhost")) { + && !permissionsManager.hasPermission(player, PlayerStatePermission.ALLOW_MULTIPLE_ACCOUNTS) + && !"127.0.0.1".equalsIgnoreCase(ip) && !"localhost".equalsIgnoreCase(ip)) { if (plugin.isLoggedIp(name, ip)) { service.send(player, MessageKey.ALREADY_LOGGED_IN_ERROR); return null; @@ -127,13 +128,13 @@ public class AsynchronousLogin implements Process { return pAuth; } - @Override - public void run() { - PlayerAuth pAuth = preAuth(); - if (pAuth == null || needsCaptcha()) { + public void login(final Player player, String password, boolean forceLogin) { + PlayerAuth pAuth = preAuth(player); + if (pAuth == null || needsCaptcha(player)) { return; } + final String ip = Utils.getPlayerIp(player); if ("127.0.0.1".equals(pAuth.getIp()) && !pAuth.getIp().equals(ip)) { pAuth.setIp(ip); database.updateIp(pAuth.getNickname(), ip); @@ -141,12 +142,13 @@ public class AsynchronousLogin implements Process { String email = pAuth.getEmail(); boolean passwordVerified = forceLogin || plugin.getPasswordSecurity() - .comparePassword(password, pAuth.getPassword(), realName); + .comparePassword(password, pAuth.getPassword(), player.getName()); + final String name = player.getName().toLowerCase(); if (passwordVerified && player.isOnline()) { PlayerAuth auth = PlayerAuth.builder() .name(name) - .realName(realName) + .realName(player.getName()) .ip(ip) .email(email) .password(pAuth.getPassword()) @@ -166,7 +168,7 @@ public class AsynchronousLogin implements Process { if (!forceLogin) service.send(player, MessageKey.LOGIN_SUCCESS); - displayOtherAccounts(auth); + displayOtherAccounts(auth, player); if (service.getProperty(EmailSettings.RECALL_PLAYERS) && (StringUtils.isEmpty(email) || "your@email.com".equalsIgnoreCase(email))) { @@ -174,11 +176,11 @@ public class AsynchronousLogin implements Process { } if (!service.getProperty(SecuritySettings.REMOVE_SPAM_FROM_CONSOLE)) { - ConsoleLogger.info(realName + " logged in!"); + ConsoleLogger.info(player.getName() + " logged in!"); } // makes player isLoggedin via API - PlayerCache.getInstance().addPlayer(auth); + playerCache.addPlayer(auth); database.setLogged(name); // As the scheduling executes the Task most likely after the current @@ -197,7 +199,7 @@ public class AsynchronousLogin implements Process { Bukkit.getScheduler().scheduleSyncDelayedTask(plugin, syncPlayerLogin); } else if (player.isOnline()) { if (!service.getProperty(SecuritySettings.REMOVE_SPAM_FROM_CONSOLE)) { - ConsoleLogger.info(realName + " used the wrong password"); + ConsoleLogger.info(player.getName() + " used the wrong password"); } if (service.getProperty(RestrictionSettings.KICK_ON_WRONG_PASSWORD)) { service.scheduleSyncDelayedTask(new Runnable() { @@ -214,29 +216,30 @@ public class AsynchronousLogin implements Process { } } - // TODO: allow translation! - private void displayOtherAccounts(PlayerAuth auth) { + // TODO #423: allow translation! + private void displayOtherAccounts(PlayerAuth auth, Player player) { if (!service.getProperty(RestrictionSettings.DISPLAY_OTHER_ACCOUNTS) || auth == null) { return; } - List auths = this.database.getAllAuthsByIp(auth.getIp()); + List auths = database.getAllAuthsByIp(auth.getIp()); if (auths.size() < 2) { return; } - // TODO: color player names with green if the account is online + // TODO #423: color player names with green if the account is online String message = StringUtils.join(", ", auths) + "."; ConsoleLogger.info("The user " + player.getName() + " has " + auths.size() + " accounts:"); ConsoleLogger.info(message); - for (Player player : service.getOnlinePlayers()) { - if ((player.getName().equalsIgnoreCase(this.player.getName()) && plugin.getPermissionsManager().hasPermission(player, PlayerPermission.SEE_OWN_ACCOUNTS))) { - player.sendMessage("You own " + auths.size() + " accounts:"); - player.sendMessage(message); - } else if (plugin.getPermissionsManager().hasPermission(player, AdminPermission.SEE_OTHER_ACCOUNTS)) { - player.sendMessage("The user " + player.getName() + " has " + auths.size() + " accounts:"); - player.sendMessage(message); + for (Player onlinePlayer : service.getOnlinePlayers()) { + if ((onlinePlayer.getName().equalsIgnoreCase(onlinePlayer.getName()) + && permissionsManager.hasPermission(onlinePlayer, PlayerPermission.SEE_OWN_ACCOUNTS))) { + onlinePlayer.sendMessage("You own " + auths.size() + " accounts:"); + onlinePlayer.sendMessage(message); + } else if (permissionsManager.hasPermission(onlinePlayer, AdminPermission.SEE_OTHER_ACCOUNTS)) { + onlinePlayer.sendMessage("The user " + onlinePlayer.getName() + " has " + auths.size() + " accounts:"); + onlinePlayer.sendMessage(message); } } } diff --git a/src/main/java/fr/xephi/authme/process/logout/AsynchronousLogout.java b/src/main/java/fr/xephi/authme/process/logout/AsynchronousLogout.java index a882ab30..2ea02d47 100644 --- a/src/main/java/fr/xephi/authme/process/logout/AsynchronousLogout.java +++ b/src/main/java/fr/xephi/authme/process/logout/AsynchronousLogout.java @@ -6,50 +6,37 @@ import fr.xephi.authme.cache.auth.PlayerCache; import fr.xephi.authme.cache.limbo.LimboCache; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.output.MessageKey; -import fr.xephi.authme.process.Process; +import fr.xephi.authme.process.NewProcess; import fr.xephi.authme.process.ProcessService; import fr.xephi.authme.util.Utils; import fr.xephi.authme.util.Utils.GroupType; import org.bukkit.entity.Player; -/** - */ -public class AsynchronousLogout implements Process { +import javax.inject.Inject; - private final Player player; - private final String name; - private final AuthMe plugin; - private final DataSource database; - private boolean canLogout = true; - private final ProcessService service; +public class AsynchronousLogout implements NewProcess { - /** - * Constructor for AsynchronousLogout. - * - * @param player Player - * @param plugin AuthMe - * @param database DataSource - * @param service The process service - */ - public AsynchronousLogout(Player player, AuthMe plugin, DataSource database, ProcessService service) { - this.player = player; - this.plugin = plugin; - this.database = database; - this.name = player.getName().toLowerCase(); - this.service = service; - } + @Inject + private AuthMe plugin; - private void preLogout() { - if (!PlayerCache.getInstance().isAuthenticated(name)) { + @Inject + private DataSource database; + + @Inject + private ProcessService service; + + @Inject + private PlayerCache playerCache; + + @Inject + private LimboCache limboCache; + + AsynchronousLogout() { } + + public void logout(Player player) { + final String name = player.getName().toLowerCase(); + if (!playerCache.isAuthenticated(name)) { service.send(player, MessageKey.NOT_LOGGED_IN); - canLogout = false; - } - } - - @Override - public void run() { - preLogout(); - if (!canLogout) { return; } final Player p = player; @@ -61,7 +48,7 @@ public class AsynchronousLogout implements Process { auth.setWorld(p.getWorld().getName()); database.updateQuitLoc(auth); - PlayerCache.getInstance().removePlayer(name); + playerCache.removePlayer(name); database.setUnlogged(name); service.scheduleSyncDelayedTask(new Runnable() { @Override @@ -69,10 +56,10 @@ public class AsynchronousLogout implements Process { Utils.teleportToSpawn(p); } }); - if (LimboCache.getInstance().hasLimboPlayer(name)) { - LimboCache.getInstance().deleteLimboPlayer(name); + if (limboCache.hasLimboPlayer(name)) { + limboCache.deleteLimboPlayer(name); } - LimboCache.getInstance().addLimboPlayer(player); + limboCache.addLimboPlayer(player); Utils.setGroup(player, GroupType.NOTLOGGEDIN); service.scheduleSyncDelayedTask(new ProcessSynchronousPlayerLogout(p, plugin, service)); } diff --git a/src/main/java/fr/xephi/authme/process/quit/AsynchronousQuit.java b/src/main/java/fr/xephi/authme/process/quit/AsynchronousQuit.java index c4e1a7f5..110faa33 100644 --- a/src/main/java/fr/xephi/authme/process/quit/AsynchronousQuit.java +++ b/src/main/java/fr/xephi/authme/process/quit/AsynchronousQuit.java @@ -7,7 +7,7 @@ import fr.xephi.authme.cache.limbo.LimboCache; import fr.xephi.authme.cache.limbo.LimboPlayer; import fr.xephi.authme.datasource.CacheDataSource; import fr.xephi.authme.datasource.DataSource; -import fr.xephi.authme.process.Process; +import fr.xephi.authme.process.NewProcess; import fr.xephi.authme.process.ProcessService; import fr.xephi.authme.settings.Settings; import fr.xephi.authme.settings.properties.RestrictionSettings; @@ -17,35 +17,37 @@ import org.bukkit.Location; import org.bukkit.entity.Player; import org.bukkit.scheduler.BukkitTask; -public class AsynchronousQuit implements Process { +import javax.inject.Inject; - private final AuthMe plugin; - private final DataSource database; - private final Player player; - private final String name; - private boolean isOp = false; - private boolean needToChange = false; - private final boolean isKick; - private final ProcessService service; +public class AsynchronousQuit implements NewProcess { - public AsynchronousQuit(Player p, AuthMe plugin, DataSource database, boolean isKick, ProcessService service) { - this.player = p; - this.plugin = plugin; - this.database = database; - this.name = p.getName().toLowerCase(); - this.isKick = isKick; - this.service = service; - } + @Inject + private AuthMe plugin; - @Override - public void run() { + @Inject + private DataSource database; + + @Inject + private ProcessService service; + + @Inject + private PlayerCache playerCache; + + @Inject + private LimboCache limboCache; + + AsynchronousQuit() { } + + + public void processQuit(Player player, boolean isKick) { if (player == null || Utils.isUnrestricted(player)) { return; } + final String name = player.getName().toLowerCase(); String ip = Utils.getPlayerIp(player); - if (PlayerCache.getInstance().isAuthenticated(name)) { + if (playerCache.isAuthenticated(name)) { if (service.getProperty(RestrictionSettings.SAVE_QUIT_LOCATION)) { Location loc = player.getLocation(); PlayerAuth auth = PlayerAuth.builder() @@ -62,14 +64,17 @@ public class AsynchronousQuit implements Process { database.updateSession(auth); } - LimboPlayer limbo = LimboCache.getInstance().getLimboPlayer(name); + boolean needToChange = false; + boolean isOp = false; + + LimboPlayer limbo = limboCache.getLimboPlayer(name); if (limbo != null) { if (!StringUtils.isEmpty(limbo.getGroup())) { Utils.addNormal(player, limbo.getGroup()); } needToChange = true; isOp = limbo.isOperator(); - LimboCache.getInstance().deleteLimboPlayer(name); + limboCache.deleteLimboPlayer(name); } if (Settings.isSessionsEnabled && !isKick) { if (Settings.getSessionTimeout != 0) { @@ -78,7 +83,7 @@ public class AsynchronousQuit implements Process { @Override public void run() { - postLogout(); + postLogout(name); } }, Settings.getSessionTimeout * 20 * 60); @@ -86,11 +91,11 @@ public class AsynchronousQuit implements Process { plugin.sessions.put(name, task); } else { //plugin is disabled; we cannot schedule more tasks so run it directly here - postLogout(); + postLogout(name); } } } else { - PlayerCache.getInstance().removePlayer(name); + playerCache.removePlayer(name); database.setUnlogged(name); } @@ -103,7 +108,7 @@ public class AsynchronousQuit implements Process { } } - private void postLogout() { + private void postLogout(String name) { PlayerCache.getInstance().removePlayer(name); database.setUnlogged(name); plugin.sessions.remove(name); diff --git a/src/main/java/fr/xephi/authme/process/register/AsyncRegister.java b/src/main/java/fr/xephi/authme/process/register/AsyncRegister.java index 77171cd0..6ab88980 100644 --- a/src/main/java/fr/xephi/authme/process/register/AsyncRegister.java +++ b/src/main/java/fr/xephi/authme/process/register/AsyncRegister.java @@ -6,7 +6,7 @@ import fr.xephi.authme.cache.auth.PlayerCache; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.permission.PlayerStatePermission; -import fr.xephi.authme.process.Process; +import fr.xephi.authme.process.NewProcess; import fr.xephi.authme.process.ProcessService; import fr.xephi.authme.security.HashAlgorithm; import fr.xephi.authme.security.crypts.HashedPassword; @@ -18,42 +18,30 @@ import fr.xephi.authme.settings.properties.RestrictionSettings; import fr.xephi.authme.settings.properties.SecuritySettings; import fr.xephi.authme.util.StringUtils; import fr.xephi.authme.util.Utils; - import org.bukkit.Bukkit; import org.bukkit.entity.Player; +import javax.inject.Inject; import java.util.List; -/** - */ -public class AsyncRegister implements Process { +public class AsyncRegister implements NewProcess { - private final Player player; - private final String name; - private final String password; - private final String ip; - private final String email; - private final AuthMe plugin; - private final DataSource database; - private final PlayerCache playerCache; - private final ProcessService service; - private final boolean autoLogin; + @Inject + private AuthMe plugin; - public AsyncRegister(Player player, String password, String email, AuthMe plugin, DataSource data, - PlayerCache playerCache, ProcessService service, boolean autoLogin) { - this.player = player; - this.password = password; - this.name = player.getName().toLowerCase(); - this.email = email; - this.plugin = plugin; - this.database = data; - this.ip = Utils.getPlayerIp(player); - this.playerCache = playerCache; - this.service = service; - this.autoLogin = autoLogin; - } + @Inject + private DataSource database; - private boolean preRegisterCheck() { + @Inject + private PlayerCache playerCache; + + @Inject + private ProcessService service; + + AsyncRegister() { } + + private boolean preRegisterCheck(Player player, String password) { + final String name = player.getName().toLowerCase(); if (playerCache.isAuthenticated(name)) { service.send(player, MessageKey.ALREADY_LOGGED_IN_ERROR); return false; @@ -78,6 +66,7 @@ public class AsyncRegister implements Process { } final int maxRegPerIp = service.getProperty(RestrictionSettings.MAX_REGISTRATION_PER_IP); + final String ip = Utils.getPlayerIp(player); if (maxRegPerIp > 0 && !"127.0.0.1".equalsIgnoreCase(ip) && !"localhost".equalsIgnoreCase(ip) @@ -92,18 +81,18 @@ public class AsyncRegister implements Process { return true; } - @Override - public void run() { - if (preRegisterCheck()) { + public void register(Player player, String password, String email, boolean autoLogin) { + if (preRegisterCheck(player, password)) { if (!StringUtils.isEmpty(email)) { - emailRegister(); + emailRegister(player, password, email); } else { - passwordRegister(); + passwordRegister(player, password, autoLogin); } } } - private void emailRegister() { + private void emailRegister(Player player, String password, String email) { + final String name = player.getName().toLowerCase(); final int maxRegPerEmail = service.getProperty(EmailSettings.MAX_REG_PER_EMAIL); if (maxRegPerEmail > 0 && !plugin.getPermissionsManager().hasPermission(player, PlayerStatePermission.ALLOW_MULTIPLE_ACCOUNTS)) { @@ -116,6 +105,7 @@ public class AsyncRegister implements Process { } final HashedPassword hashedPassword = service.computeHash(password, name); + final String ip = Utils.getPlayerIp(player); PlayerAuth auth = PlayerAuth.builder() .name(name) .realName(player.getName()) @@ -137,7 +127,9 @@ public class AsyncRegister implements Process { } - private void passwordRegister() { + private void passwordRegister(Player player, String password, boolean autoLogin) { + final String name = player.getName().toLowerCase(); + final String ip = Utils.getPlayerIp(player); final HashedPassword hashedPassword = service.computeHash(password, name); PlayerAuth auth = PlayerAuth.builder() .name(name) diff --git a/src/main/java/fr/xephi/authme/process/unregister/AsynchronousUnregister.java b/src/main/java/fr/xephi/authme/process/unregister/AsynchronousUnregister.java index cd3f41a2..02289963 100644 --- a/src/main/java/fr/xephi/authme/process/unregister/AsynchronousUnregister.java +++ b/src/main/java/fr/xephi/authme/process/unregister/AsynchronousUnregister.java @@ -4,12 +4,13 @@ import fr.xephi.authme.AuthMe; import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.cache.auth.PlayerAuth; import fr.xephi.authme.cache.auth.PlayerCache; -import fr.xephi.authme.cache.backup.JsonCache; import fr.xephi.authme.cache.limbo.LimboCache; import fr.xephi.authme.cache.limbo.LimboPlayer; +import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.output.MessageKey; -import fr.xephi.authme.process.Process; +import fr.xephi.authme.process.NewProcess; import fr.xephi.authme.process.ProcessService; +import fr.xephi.authme.security.PasswordSecurity; import fr.xephi.authme.settings.Settings; import fr.xephi.authme.settings.properties.RegistrationSettings; import fr.xephi.authme.settings.properties.RestrictionSettings; @@ -22,55 +23,50 @@ import org.bukkit.potion.PotionEffect; import org.bukkit.potion.PotionEffectType; import org.bukkit.scheduler.BukkitTask; -public class AsynchronousUnregister implements Process { +import javax.inject.Inject; - private final Player player; - private final String name; - private final String password; - private final boolean force; - private final AuthMe plugin; - private final JsonCache playerCache; - private final ProcessService service; +import static fr.xephi.authme.util.BukkitService.TICKS_PER_SECOND; - /** - * Constructor. - * - * @param player The player to perform the action for - * @param password The password - * @param force True to bypass password validation - * @param plugin The plugin instance - * @param service The process service - */ - public AsynchronousUnregister(Player player, String password, boolean force, AuthMe plugin, - ProcessService service) { - this.player = player; - this.name = player.getName().toLowerCase(); - this.password = password; - this.force = force; - this.plugin = plugin; - this.playerCache = new JsonCache(); - this.service = service; - } +public class AsynchronousUnregister implements NewProcess { - @Override - public void run() { - PlayerAuth cachedAuth = PlayerCache.getInstance().getAuth(name); - if (force || plugin.getPasswordSecurity().comparePassword( - password, cachedAuth.getPassword(), player.getName())) { - if (!service.getDataSource().removeAuth(name)) { + @Inject + private AuthMe plugin; + + @Inject + private DataSource dataSource; + + @Inject + private ProcessService service; + + @Inject + private PasswordSecurity passwordSecurity; + + @Inject + private PlayerCache playerCache; + + @Inject + private LimboCache limboCache; + + AsynchronousUnregister() { } + + public void unregister(Player player, String password, boolean force) { + final String name = player.getName().toLowerCase(); + PlayerAuth cachedAuth = playerCache.getAuth(name); + if (force || passwordSecurity.comparePassword(password, cachedAuth.getPassword(), player.getName())) { + if (!dataSource.removeAuth(name)) { service.send(player, MessageKey.ERROR); return; } - int timeOut = service.getProperty(RestrictionSettings.TIMEOUT) * 20; + int timeOut = service.getProperty(RestrictionSettings.TIMEOUT) * TICKS_PER_SECOND; if (Settings.isForcedRegistrationEnabled) { Utils.teleportToSpawn(player); player.saveData(); - PlayerCache.getInstance().removePlayer(player.getName().toLowerCase()); + playerCache.removePlayer(player.getName().toLowerCase()); if (!Settings.getRegisteredGroup.isEmpty()) { Utils.setGroup(player, GroupType.UNREGISTERED); } - LimboCache.getInstance().addLimboPlayer(player); - LimboPlayer limboPlayer = LimboCache.getInstance().getLimboPlayer(name); + limboCache.addLimboPlayer(player); + LimboPlayer limboPlayer = limboCache.getLimboPlayer(name); int interval = service.getProperty(RegistrationSettings.MESSAGE_INTERVAL); if (timeOut != 0) { BukkitTask id = service.runTaskLater(new TimeoutTask(plugin, name, player), timeOut); @@ -85,12 +81,8 @@ public class AsynchronousUnregister implements Process { if (!Settings.unRegisteredGroup.isEmpty()) { Utils.setGroup(player, Utils.GroupType.UNREGISTERED); } - PlayerCache.getInstance().removePlayer(name); - // check if Player cache File Exist and delete it, preventing - // duplication of items - if (playerCache.doesCacheExist(player)) { - playerCache.removeCache(player); - } + playerCache.removePlayer(name); + // Apply blind effect if (service.getProperty(RegistrationSettings.APPLY_BLIND_EFFECT)) { player.addPotionEffect(new PotionEffect(PotionEffectType.BLINDNESS, timeOut, 2)); diff --git a/src/test/java/fr/xephi/authme/process/ProcessServiceTest.java b/src/test/java/fr/xephi/authme/process/ProcessServiceTest.java index f641b1f0..7bedb914 100644 --- a/src/test/java/fr/xephi/authme/process/ProcessServiceTest.java +++ b/src/test/java/fr/xephi/authme/process/ProcessServiceTest.java @@ -140,33 +140,6 @@ public class ProcessServiceTest { assertThat(result, equalTo(authMe)); } - @Test - public void shouldReturnPluginHooks() { - // given / when - PluginHooks result = processService.getPluginHooks(); - - // then - assertThat(result, equalTo(pluginHooks)); - } - - @Test - public void shouldReturnSpawnLoader() { - // given / when - SpawnLoader result = processService.getSpawnLoader(); - - // then - assertThat(result, equalTo(spawnLoader)); - } - - @Test - public void shouldReturnDatasource() { - // given / when - DataSource result = processService.getDataSource(); - - // then - assertThat(result, equalTo(dataSource)); - } - @Test public void shouldComputeHash() { // given diff --git a/src/test/java/fr/xephi/authme/process/email/AsyncAddEmailTest.java b/src/test/java/fr/xephi/authme/process/email/AsyncAddEmailTest.java index 9f92a5c8..4d013bd3 100644 --- a/src/test/java/fr/xephi/authme/process/email/AsyncAddEmailTest.java +++ b/src/test/java/fr/xephi/authme/process/email/AsyncAddEmailTest.java @@ -11,6 +11,7 @@ import org.bukkit.entity.Player; import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; @@ -26,12 +27,18 @@ import static org.mockito.Mockito.verify; @RunWith(MockitoJUnitRunner.class) public class AsyncAddEmailTest { + @InjectMocks + private AsyncAddEmail asyncAddEmail; + @Mock private Player player; + @Mock private DataSource dataSource; + @Mock private PlayerCache playerCache; + @Mock private ProcessService service; @@ -44,7 +51,6 @@ public class AsyncAddEmailTest { public void shouldAddEmail() { // given String email = "my.mail@example.org"; - AsyncAddEmail process = createProcess(email); given(player.getName()).willReturn("testEr"); given(playerCache.isAuthenticated("tester")).willReturn(true); PlayerAuth auth = mock(PlayerAuth.class); @@ -55,7 +61,7 @@ public class AsyncAddEmailTest { given(service.isEmailFreeForRegistration(email, player)).willReturn(true); // when - process.run(); + asyncAddEmail.addEmail(player, email); // then verify(dataSource).updateEmail(auth); @@ -68,7 +74,6 @@ public class AsyncAddEmailTest { public void shouldReturnErrorWhenMailCannotBeSaved() { // given String email = "my.mail@example.org"; - AsyncAddEmail process = createProcess(email); given(player.getName()).willReturn("testEr"); given(playerCache.isAuthenticated("tester")).willReturn(true); PlayerAuth auth = mock(PlayerAuth.class); @@ -80,7 +85,7 @@ public class AsyncAddEmailTest { given(service.isEmailFreeForRegistration(email, player)).willReturn(true); // when - process.run(); + asyncAddEmail.addEmail(player, email); // then verify(dataSource).updateEmail(auth); @@ -90,7 +95,6 @@ public class AsyncAddEmailTest { @Test public void shouldNotAddMailIfPlayerAlreadyHasEmail() { // given - AsyncAddEmail process = createProcess("some.mail@example.org"); given(player.getName()).willReturn("my_Player"); given(playerCache.isAuthenticated("my_player")).willReturn(true); PlayerAuth auth = mock(PlayerAuth.class); @@ -98,7 +102,7 @@ public class AsyncAddEmailTest { given(playerCache.getAuth("my_player")).willReturn(auth); // when - process.run(); + asyncAddEmail.addEmail(player, "some.mail@example.org"); // then verify(service).send(player, MessageKey.USAGE_CHANGE_EMAIL); @@ -109,7 +113,6 @@ public class AsyncAddEmailTest { public void shouldNotAddMailIfItIsInvalid() { // given String email = "invalid_mail"; - AsyncAddEmail process = createProcess(email); given(player.getName()).willReturn("my_Player"); given(playerCache.isAuthenticated("my_player")).willReturn(true); PlayerAuth auth = mock(PlayerAuth.class); @@ -118,7 +121,7 @@ public class AsyncAddEmailTest { given(service.validateEmail(email)).willReturn(false); // when - process.run(); + asyncAddEmail.addEmail(player, email); // then verify(service).send(player, MessageKey.INVALID_EMAIL); @@ -129,7 +132,6 @@ public class AsyncAddEmailTest { public void shouldNotAddMailIfAlreadyUsed() { // given String email = "player@mail.tld"; - AsyncAddEmail process = createProcess(email); given(player.getName()).willReturn("TestName"); given(playerCache.isAuthenticated("testname")).willReturn(true); PlayerAuth auth = mock(PlayerAuth.class); @@ -139,7 +141,7 @@ public class AsyncAddEmailTest { given(service.isEmailFreeForRegistration(email, player)).willReturn(false); // when - process.run(); + asyncAddEmail.addEmail(player, email); // then verify(service).send(player, MessageKey.EMAIL_ALREADY_USED_ERROR); @@ -149,13 +151,12 @@ public class AsyncAddEmailTest { @Test public void shouldShowLoginMessage() { // given - AsyncAddEmail process = createProcess("test@mail.com"); given(player.getName()).willReturn("Username12"); given(playerCache.isAuthenticated("username12")).willReturn(false); given(dataSource.isAuthAvailable("Username12")).willReturn(true); // when - process.run(); + asyncAddEmail.addEmail(player, "test@mail.com"); // then verify(service).send(player, MessageKey.LOGIN_MESSAGE); @@ -165,14 +166,13 @@ public class AsyncAddEmailTest { @Test public void shouldShowEmailRegisterMessage() { // given - AsyncAddEmail process = createProcess("test@mail.com"); given(player.getName()).willReturn("user"); given(playerCache.isAuthenticated("user")).willReturn(false); given(dataSource.isAuthAvailable("user")).willReturn(false); given(service.getProperty(RegistrationSettings.USE_EMAIL_REGISTRATION)).willReturn(true); // when - process.run(); + asyncAddEmail.addEmail(player, "test@mail.com"); // then verify(service).send(player, MessageKey.REGISTER_EMAIL_MESSAGE); @@ -182,28 +182,17 @@ public class AsyncAddEmailTest { @Test public void shouldShowRegularRegisterMessage() { // given - AsyncAddEmail process = createProcess("test@mail.com"); given(player.getName()).willReturn("user"); given(playerCache.isAuthenticated("user")).willReturn(false); given(dataSource.isAuthAvailable("user")).willReturn(false); given(service.getProperty(RegistrationSettings.USE_EMAIL_REGISTRATION)).willReturn(false); // when - process.run(); + asyncAddEmail.addEmail(player, "test@mail.com"); // then verify(service).send(player, MessageKey.REGISTER_MESSAGE); verify(playerCache, never()).updatePlayer(any(PlayerAuth.class)); } - /** - * Create an instance of {@link AsyncAddEmail} with the class' mocks. - * - * @param email The email to use - * @return The created process - */ - private AsyncAddEmail createProcess(String email) { - return new AsyncAddEmail(player, email, dataSource, playerCache, service); - } - } diff --git a/src/test/java/fr/xephi/authme/process/email/AsyncChangeEmailTest.java b/src/test/java/fr/xephi/authme/process/email/AsyncChangeEmailTest.java index a90ff9b2..7b9dbfdb 100644 --- a/src/test/java/fr/xephi/authme/process/email/AsyncChangeEmailTest.java +++ b/src/test/java/fr/xephi/authme/process/email/AsyncChangeEmailTest.java @@ -5,11 +5,11 @@ import fr.xephi.authme.cache.auth.PlayerCache; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.process.ProcessService; -import fr.xephi.authme.settings.properties.EmailSettings; import fr.xephi.authme.settings.properties.RegistrationSettings; import org.bukkit.entity.Player; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; @@ -26,12 +26,18 @@ import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.class) public class AsyncChangeEmailTest { + @InjectMocks + private AsyncChangeEmail process; + @Mock private Player player; + @Mock private PlayerCache playerCache; + @Mock private DataSource dataSource; + @Mock private ProcessService service; @@ -39,7 +45,6 @@ public class AsyncChangeEmailTest { public void shouldAddEmail() { // given String newEmail = "new@mail.tld"; - AsyncChangeEmail process = createProcess("old@mail.tld", newEmail); given(player.getName()).willReturn("Bobby"); given(playerCache.isAuthenticated("bobby")).willReturn(true); PlayerAuth auth = authWithMail("old@mail.tld"); @@ -49,7 +54,7 @@ public class AsyncChangeEmailTest { given(service.isEmailFreeForRegistration(newEmail, player)).willReturn(true); // when - process.run(); + process.changeEmail(player, "old@mail.tld", newEmail); // then verify(dataSource).updateEmail(auth); @@ -61,7 +66,6 @@ public class AsyncChangeEmailTest { public void shouldShowErrorIfSaveFails() { // given String newEmail = "new@mail.tld"; - AsyncChangeEmail process = createProcess("old@mail.tld", newEmail); given(player.getName()).willReturn("Bobby"); given(playerCache.isAuthenticated("bobby")).willReturn(true); PlayerAuth auth = authWithMail("old@mail.tld"); @@ -71,7 +75,7 @@ public class AsyncChangeEmailTest { given(service.isEmailFreeForRegistration(newEmail, player)).willReturn(true); // when - process.run(); + process.changeEmail(player, "old@mail.tld", newEmail); // then verify(dataSource).updateEmail(auth); @@ -82,14 +86,13 @@ public class AsyncChangeEmailTest { @Test public void shouldShowAddEmailUsage() { // given - AsyncChangeEmail process = createProcess("old@mail.tld", "new@mail.tld"); given(player.getName()).willReturn("Bobby"); given(playerCache.isAuthenticated("bobby")).willReturn(true); PlayerAuth auth = authWithMail(null); given(playerCache.getAuth("bobby")).willReturn(auth); // when - process.run(); + process.changeEmail(player, "old@mail.tld", "new@mailt.tld"); // then verify(dataSource, never()).updateEmail(any(PlayerAuth.class)); @@ -101,7 +104,6 @@ public class AsyncChangeEmailTest { public void shouldRejectInvalidNewMail() { // given String newEmail = "bogus"; - AsyncChangeEmail process = createProcess("old@mail.tld", newEmail); given(player.getName()).willReturn("Bobby"); given(playerCache.isAuthenticated("bobby")).willReturn(true); PlayerAuth auth = authWithMail("old@mail.tld"); @@ -109,7 +111,7 @@ public class AsyncChangeEmailTest { given(service.validateEmail(newEmail)).willReturn(false); // when - process.run(); + process.changeEmail(player, "old@mail.tld", newEmail); // then verify(dataSource, never()).updateEmail(any(PlayerAuth.class)); @@ -121,7 +123,6 @@ public class AsyncChangeEmailTest { public void shouldRejectInvalidOldEmail() { // given String newEmail = "new@mail.tld"; - AsyncChangeEmail process = createProcess("old@mail.tld", newEmail); given(player.getName()).willReturn("Bobby"); given(playerCache.isAuthenticated("bobby")).willReturn(true); PlayerAuth auth = authWithMail("other@address.email"); @@ -131,7 +132,7 @@ public class AsyncChangeEmailTest { // when - process.run(); + process.changeEmail(player, "old@mail.tld", newEmail); // then verify(dataSource, never()).updateEmail(any(PlayerAuth.class)); @@ -143,7 +144,6 @@ public class AsyncChangeEmailTest { public void shouldRejectAlreadyUsedEmail() { // given String newEmail = "new@example.com"; - AsyncChangeEmail process = createProcess("old@example.com", newEmail); given(player.getName()).willReturn("Username"); given(playerCache.isAuthenticated("username")).willReturn(true); PlayerAuth auth = authWithMail("old@example.com"); @@ -152,7 +152,7 @@ public class AsyncChangeEmailTest { given(service.isEmailFreeForRegistration(newEmail, player)).willReturn(false); // when - process.run(); + process.changeEmail(player, "old@example.com", newEmail); // then verify(dataSource, never()).updateEmail(any(PlayerAuth.class)); @@ -163,13 +163,12 @@ public class AsyncChangeEmailTest { @Test public void shouldSendLoginMessage() { // given - AsyncChangeEmail process = createProcess("old@mail.tld", "new@mail.tld"); given(player.getName()).willReturn("Bobby"); given(playerCache.isAuthenticated("bobby")).willReturn(false); given(dataSource.isAuthAvailable("Bobby")).willReturn(true); // when - process.run(); + process.changeEmail(player, "old@mail.tld", "new@mail.tld"); // then verify(dataSource, never()).updateEmail(any(PlayerAuth.class)); @@ -180,14 +179,13 @@ public class AsyncChangeEmailTest { @Test public void shouldShowEmailRegistrationMessage() { // given - AsyncChangeEmail process = createProcess("old@mail.tld", "new@mail.tld"); given(player.getName()).willReturn("Bobby"); given(playerCache.isAuthenticated("bobby")).willReturn(false); given(dataSource.isAuthAvailable("Bobby")).willReturn(false); given(service.getProperty(RegistrationSettings.USE_EMAIL_REGISTRATION)).willReturn(true); // when - process.run(); + process.changeEmail(player, "old@mail.tld", "new@mail.tld"); // then verify(dataSource, never()).updateEmail(any(PlayerAuth.class)); @@ -198,14 +196,13 @@ public class AsyncChangeEmailTest { @Test public void shouldShowRegistrationMessage() { // given - AsyncChangeEmail process = createProcess("old@mail.tld", "new@mail.tld"); given(player.getName()).willReturn("Bobby"); given(playerCache.isAuthenticated("bobby")).willReturn(false); given(dataSource.isAuthAvailable("Bobby")).willReturn(false); given(service.getProperty(RegistrationSettings.USE_EMAIL_REGISTRATION)).willReturn(false); // when - process.run(); + process.changeEmail(player, "old@mail.tld", "new@mail.tld"); // then verify(dataSource, never()).updateEmail(any(PlayerAuth.class)); @@ -219,8 +216,4 @@ public class AsyncChangeEmailTest { return auth; } - private AsyncChangeEmail createProcess(String oldEmail, String newEmail) { - given(service.getProperty(EmailSettings.MAX_REG_PER_EMAIL)).willReturn(5); - return new AsyncChangeEmail(player, oldEmail, newEmail, dataSource, playerCache, service); - } } From 7229a8b02b850e38a51dd2d8963938c994cc16bf Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Wed, 18 May 2016 07:49:37 +0200 Subject: [PATCH 057/200] Update exec plugin, the doc task should run manually --- pom.xml | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/pom.xml b/pom.xml index b4bdc34d..c80dcaa1 100644 --- a/pom.xml +++ b/pom.xml @@ -241,7 +241,7 @@ org.codehaus.mojo exec-maven-plugin - 1.4.0 + 1.5.0 test ${project.basedir}/target/test-classes @@ -251,16 +251,6 @@ true - From 92a8a5dd41a77fac334753216555667001c36202 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Wed, 18 May 2016 19:09:25 +0200 Subject: [PATCH 058/200] #704 Remove reloading from hash algorithms - A new instance is created for every hash operation, so reloading will never happen on those classes --- .../java/fr/xephi/authme/security/crypts/BCRYPT.java | 11 +++-------- .../fr/xephi/authme/security/crypts/SALTED2MD5.java | 11 +++-------- 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/src/main/java/fr/xephi/authme/security/crypts/BCRYPT.java b/src/main/java/fr/xephi/authme/security/crypts/BCRYPT.java index dc1b3676..cda26091 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/BCRYPT.java +++ b/src/main/java/fr/xephi/authme/security/crypts/BCRYPT.java @@ -1,7 +1,6 @@ package fr.xephi.authme.security.crypts; import fr.xephi.authme.ConsoleLogger; -import fr.xephi.authme.initialization.SettingsDependent; import fr.xephi.authme.security.crypts.description.HasSalt; import fr.xephi.authme.security.crypts.description.Recommendation; import fr.xephi.authme.security.crypts.description.SaltType; @@ -14,13 +13,13 @@ import javax.inject.Inject; @Recommendation(Usage.RECOMMENDED) // provided the salt length is >= 8 @HasSalt(value = SaltType.TEXT) // length depends on the bcryptLog2Rounds setting -public class BCRYPT implements EncryptionMethod, SettingsDependent { +public class BCRYPT implements EncryptionMethod { - private int bCryptLog2Rounds; + private final int bCryptLog2Rounds; @Inject public BCRYPT(NewSetting settings) { - loadSettings(settings); + bCryptLog2Rounds = settings.getProperty(HooksSettings.BCRYPT_LOG2_ROUND); } @Override @@ -54,8 +53,4 @@ public class BCRYPT implements EncryptionMethod, SettingsDependent { return false; } - @Override - public void loadSettings(NewSetting settings) { - bCryptLog2Rounds = settings.getProperty(HooksSettings.BCRYPT_LOG2_ROUND); - } } diff --git a/src/main/java/fr/xephi/authme/security/crypts/SALTED2MD5.java b/src/main/java/fr/xephi/authme/security/crypts/SALTED2MD5.java index a0e92284..a96cc0d5 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/SALTED2MD5.java +++ b/src/main/java/fr/xephi/authme/security/crypts/SALTED2MD5.java @@ -1,6 +1,5 @@ package fr.xephi.authme.security.crypts; -import fr.xephi.authme.initialization.SettingsDependent; import fr.xephi.authme.security.RandomString; import fr.xephi.authme.security.crypts.description.HasSalt; import fr.xephi.authme.security.crypts.description.Recommendation; @@ -15,13 +14,13 @@ import static fr.xephi.authme.security.HashUtils.md5; @Recommendation(Usage.ACCEPTABLE) // presuming that length is something sensible (>= 8) @HasSalt(value = SaltType.TEXT) // length defined by the doubleMd5SaltLength setting -public class SALTED2MD5 extends SeparateSaltMethod implements SettingsDependent { +public class SALTED2MD5 extends SeparateSaltMethod { - private int saltLength; + private final int saltLength; @Inject public SALTED2MD5(NewSetting settings) { - loadSettings(settings); + saltLength = settings.getProperty(SecuritySettings.DOUBLE_MD5_SALT_LENGTH); } @Override @@ -34,9 +33,5 @@ public class SALTED2MD5 extends SeparateSaltMethod implements SettingsDependent return RandomString.generateHex(saltLength); } - @Override - public void loadSettings(NewSetting settings) { - saltLength = settings.getProperty(SecuritySettings.DOUBLE_MD5_SALT_LENGTH); - } } From 14002ee75cc45312ac705b03d8c6131c5d25d847 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Thu, 19 May 2016 19:46:02 +0200 Subject: [PATCH 059/200] #704 Reload settings of ConsoleLogger on /authme reload --- src/main/java/fr/xephi/authme/AuthMe.java | 35 +++++++++---------- .../java/fr/xephi/authme/ConsoleLogger.java | 13 ++++--- .../executable/authme/ReloadCommand.java | 1 + .../executable/authme/ReloadCommandTest.java | 9 +++++ 4 files changed, 35 insertions(+), 23 deletions(-) diff --git a/src/main/java/fr/xephi/authme/AuthMe.java b/src/main/java/fr/xephi/authme/AuthMe.java index 11001ae7..baeb1bfd 100644 --- a/src/main/java/fr/xephi/authme/AuthMe.java +++ b/src/main/java/fr/xephi/authme/AuthMe.java @@ -44,16 +44,10 @@ import fr.xephi.authme.settings.SettingsMigrationService; import fr.xephi.authme.settings.SpawnLoader; import fr.xephi.authme.settings.properties.DatabaseSettings; import fr.xephi.authme.settings.properties.EmailSettings; - -import static fr.xephi.authme.settings.properties.EmailSettings.MAIL_ACCOUNT; -import static fr.xephi.authme.settings.properties.EmailSettings.MAIL_PASSWORD; -import static fr.xephi.authme.settings.properties.EmailSettings.RECALL_PLAYERS; - import fr.xephi.authme.settings.properties.HooksSettings; import fr.xephi.authme.settings.properties.PluginSettings; import fr.xephi.authme.settings.properties.PurgeSettings; import fr.xephi.authme.settings.properties.RestrictionSettings; -import fr.xephi.authme.settings.properties.SecuritySettings; import fr.xephi.authme.settings.properties.SettingsFieldRetriever; import fr.xephi.authme.settings.propertymap.PropertyMap; import fr.xephi.authme.task.PurgeTask; @@ -64,6 +58,17 @@ import fr.xephi.authme.util.GeoLiteAPI; import fr.xephi.authme.util.MigrationService; import fr.xephi.authme.util.StringUtils; import fr.xephi.authme.util.Utils; +import org.apache.logging.log4j.LogManager; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.Server; +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.BukkitScheduler; +import org.bukkit.scheduler.BukkitTask; import java.io.File; import java.sql.SQLException; @@ -76,17 +81,9 @@ 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.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.BukkitScheduler; -import org.bukkit.scheduler.BukkitTask; +import static fr.xephi.authme.settings.properties.EmailSettings.MAIL_ACCOUNT; +import static fr.xephi.authme.settings.properties.EmailSettings.MAIL_PASSWORD; +import static fr.xephi.authme.settings.properties.EmailSettings.RECALL_PLAYERS; /** * The AuthMe main class. @@ -223,8 +220,8 @@ public class AuthMe extends JavaPlugin { getServer().shutdown(); return; } - ConsoleLogger.setLoggingOptions(newSettings.getProperty(SecuritySettings.USE_LOGGING), - new File(getDataFolder(), "authme.log")); + ConsoleLogger.setLogFile(new File(getDataFolder(), "authme.log")); + ConsoleLogger.setLoggingOptions(newSettings); // Old settings manager if (!loadSettings()) { diff --git a/src/main/java/fr/xephi/authme/ConsoleLogger.java b/src/main/java/fr/xephi/authme/ConsoleLogger.java index acdae309..ebec0291 100644 --- a/src/main/java/fr/xephi/authme/ConsoleLogger.java +++ b/src/main/java/fr/xephi/authme/ConsoleLogger.java @@ -1,6 +1,7 @@ package fr.xephi.authme; import com.google.common.base.Throwables; +import fr.xephi.authme.settings.NewSetting; import fr.xephi.authme.settings.properties.SecuritySettings; import fr.xephi.authme.util.StringUtils; @@ -21,22 +22,26 @@ public final class ConsoleLogger { private static final String NEW_LINE = System.getProperty("line.separator"); private static final DateFormat DATE_FORMAT = new SimpleDateFormat("[MM-dd HH:mm:ss]"); private static Logger logger; + private static boolean enableDebug = false; private static boolean useLogging = false; private static File logFile; private ConsoleLogger() { - // Service class } public static void setLogger(Logger logger) { ConsoleLogger.logger = logger; } - public static void setLoggingOptions(boolean useLogging, File logFile) { - ConsoleLogger.useLogging = useLogging; + public static void setLogFile(File logFile) { ConsoleLogger.logFile = logFile; } + public static void setLoggingOptions(NewSetting settings) { + ConsoleLogger.useLogging = settings.getProperty(SecuritySettings.USE_LOGGING); + ConsoleLogger.enableDebug = !settings.getProperty(SecuritySettings.REMOVE_SPAM_FROM_CONSOLE); + } + /** * Print an info message. * @@ -50,7 +55,7 @@ public final class ConsoleLogger { } public static void debug(String message) { - if (!AuthMe.getInstance().getSettings().getProperty(SecuritySettings.REMOVE_SPAM_FROM_CONSOLE)) { + if (enableDebug) { logger.fine(message); if (useLogging) { writeLog("Debug: " + message); diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/ReloadCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/ReloadCommand.java index ec709796..48a43f0a 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/ReloadCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/ReloadCommand.java @@ -40,6 +40,7 @@ public class ReloadCommand implements ExecutableCommand { ConsoleLogger.info("Note: cannot change database type during /authme reload"); sender.sendMessage("Note: cannot change database type during /authme reload"); } + ConsoleLogger.setLoggingOptions(settings); initializer.performReloadOnServices(); commandService.send(sender, MessageKey.CONFIG_RELOAD_SUCCESS); } catch (Exception e) { diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/ReloadCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/ReloadCommandTest.java index 5d52bbd8..d9c28b64 100644 --- a/src/test/java/fr/xephi/authme/command/executable/authme/ReloadCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/authme/ReloadCommandTest.java @@ -9,7 +9,9 @@ import fr.xephi.authme.initialization.AuthMeServiceInitializer; import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.settings.NewSetting; import fr.xephi.authme.settings.properties.DatabaseSettings; +import fr.xephi.authme.settings.properties.SecuritySettings; import org.bukkit.command.CommandSender; +import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; @@ -53,6 +55,13 @@ public class ReloadCommandTest { TestHelper.setupLogger(); } + @Before + public void setDefaultSettings() { + // Mock properties retrieved by ConsoleLogger + given(settings.getProperty(SecuritySettings.REMOVE_SPAM_FROM_CONSOLE)).willReturn(false); + given(settings.getProperty(SecuritySettings.USE_LOGGING)).willReturn(false); + } + @Test public void shouldReload() { // given From 383820cd227b5d64238e5ea3e33c5790f48c0c30 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Thu, 19 May 2016 21:44:24 +0200 Subject: [PATCH 060/200] #702 Implement SHA256 php registration - Refactor Bcrypt and Sha256 examples to use common abstract parent class - Implement hashing logic for Sha256 --- .../website_integration/AuthMeController.php | 126 ++++++++++++++++++ samples/website_integration/Bcrypt.php | 20 +++ samples/website_integration/Sha256.php | 48 +++++++ .../bcrypt/integration.php | 107 --------------- .../{bcrypt/form.php => index.php} | 31 +++-- samples/website_integration/sha256/form.php | 52 -------- .../sha256/integration.php | 67 ---------- 7 files changed, 213 insertions(+), 238 deletions(-) create mode 100644 samples/website_integration/AuthMeController.php create mode 100644 samples/website_integration/Bcrypt.php create mode 100644 samples/website_integration/Sha256.php delete mode 100644 samples/website_integration/bcrypt/integration.php rename samples/website_integration/{bcrypt/form.php => index.php} (66%) delete mode 100644 samples/website_integration/sha256/form.php delete mode 100644 samples/website_integration/sha256/integration.php diff --git a/samples/website_integration/AuthMeController.php b/samples/website_integration/AuthMeController.php new file mode 100644 index 00000000..d1e94ddf --- /dev/null +++ b/samples/website_integration/AuthMeController.php @@ -0,0 +1,126 @@ +getHashFromDatabase($username); + if ($hash) { + return $this->isValidPassword($password, $hash); + } + } + return false; + } + + /** + * Returns whether the user exists in the database or not. + * + * @param string $username the username to check + * @return bool true if the user exists; false otherwise + */ + function isUserRegistered($username) { + $mysqli = $this->getAuthmeMySqli(); + if ($mysqli !== null) { + $stmt = $mysqli->prepare('SELECT 1 FROM ' . self::AUTHME_TABLE . ' WHERE username = ?'); + $stmt->bind_param('s', $username); + $stmt->execute(); + return $stmt->fetch(); + } + + // Defensive default to true; we actually don't know + return true; + } + + /** + * Registers a player with the given username. + * + * @param string $username the username to register + * @param string $password the password to associate to the user + * @return bool whether or not the registration was successful + */ + function register($username, $password) { + $mysqli = $this->getAuthmeMySqli(); + if ($mysqli !== null) { + $hash = $this->hash($password); + $stmt = $mysqli->prepare('INSERT INTO ' . self::AUTHME_TABLE . ' (username, realname, password, ip) ' + . 'VALUES (?, ?, ?, ?)'); + $username_low = strtolower($username); + $stmt->bind_param('ssss', $username, $username_low, $hash, $_SERVER['REMOTE_ADDR']); + return $stmt->execute(); + } + return false; + } + + /** + * Hashes the given password. + * + * @param $password string the clear-text password to hash + * @return string the resulting hash + */ + protected abstract function hash($password); + + /** + * Checks whether the given password matches the hash. + * + * @param $password string the clear-text password + * @param $hash string the password hash + * @return boolean true if the password matches, false otherwise + */ + protected abstract function isValidPassword($password, $hash); + + /** + * Returns a connection to the database. + * + * @return mysqli|null the mysqli object or null upon error + */ + private function getAuthmeMySqli() { + // CHANGE YOUR DATABASE DETAILS HERE BELOW: host, user, password, database name + $mysqli = new mysqli('localhost', 'root', '', 'authme'); + if (mysqli_connect_error()) { + printf('Could not connect to AuthMe database. Errno: %d, error: "%s"', + mysqli_connect_errno(), mysqli_connect_error()); + return null; + } + return $mysqli; + } + + /** + * Retrieves the hash associated with the given user from the database. + * + * @param string $username the username whose hash should be retrieved + * @return string|null the hash, or null if unavailable (e.g. username doesn't exist) + */ + private function getHashFromDatabase($username) { + // Add here your database host, username, password and database name + $mysqli = $this->getAuthmeMySqli(); + if ($mysqli !== null) { + $stmt = $mysqli->prepare('SELECT password FROM ' . self::AUTHME_TABLE . ' WHERE username = ?'); + $stmt->bind_param('s', $username); + $stmt->execute(); + $stmt->bind_result($password); + if ($stmt->fetch()) { + return $password; + } + } + return null; + } + +} \ No newline at end of file diff --git a/samples/website_integration/Bcrypt.php b/samples/website_integration/Bcrypt.php new file mode 100644 index 00000000..225baf80 --- /dev/null +++ b/samples/website_integration/Bcrypt.php @@ -0,0 +1,20 @@ +CHARS = self::initRandomChars(); + } + + protected function isValidPassword($password, $hash) { + // $SHA$salt$hash, where hash := sha256(sha256(password) . salt) + $parts = explode('$', $hash); + return count($parts) === 4 && $parts[3] === hash('sha256', hash('sha256', $password) . $parts[2]); + } + + protected function hash($password) { + $salt = $this->generateSalt(); + return '$SHA$' . $salt . '$' . hash('sha256', hash('sha256', $password) . $salt); + } + + /** + * @return string randomly generated salt + */ + private function generateSalt() { + $maxCharIndex = count($this->CHARS) - 1; + $salt = ''; + for ($i = 0; $i < self::SALT_LENGTH; ++$i) { + $salt .= $this->CHARS[mt_rand(0, $maxCharIndex)]; + } + return $salt; + } + + private static function initRandomChars() { + return array_merge(range('0', '9'), range('a', 'f')); + } + +} diff --git a/samples/website_integration/bcrypt/integration.php b/samples/website_integration/bcrypt/integration.php deleted file mode 100644 index 75911838..00000000 --- a/samples/website_integration/bcrypt/integration.php +++ /dev/null @@ -1,107 +0,0 @@ -prepare('SELECT password FROM ' . AUTHME_TABLE . ' WHERE username = ?'); - $stmt->bind_param('s', $username); - $stmt->execute(); - $stmt->bind_result($password); - if ($stmt->fetch()) { - return $password; - } - } - return null; -} - -/** - * Returns whether the user exists in the database or not. - * - * @param string $username the username to check - * @return bool true if the user exists; false otherwise - */ -function authme_has_user($username) { - $mysqli = authme_get_mysqli(); - if ($mysqli !== null) { - $stmt = $mysqli->prepare('SELECT 1 FROM ' . AUTHME_TABLE . ' WHERE username = ?'); - $stmt->bind_param('s', $username); - $stmt->execute(); - return $stmt->fetch(); - } - - // Defensive default to true; we actually don't know - return true; -} - -/** - * Registers a player with the given username. - * - * @param string $username the username to register - * @param string $password the password to associate to the user - * @return bool whether or not the registration was successful - */ -function authme_register($username, $password) { - $mysqli = authme_get_mysqli(); - if ($mysqli !== null) { - $hash = password_hash($password, PASSWORD_BCRYPT); - $stmt = $mysqli->prepare('INSERT INTO ' . AUTHME_TABLE . ' (username, realname, password, ip) ' - . 'VALUES (?, ?, ?, ?)'); - $username_low = strtolower($username); - $stmt->bind_param('ssss', $username, $username_low, $hash, $_SERVER['REMOTE_ADDR']); - return $stmt->execute(); - } - return false; -} - diff --git a/samples/website_integration/bcrypt/form.php b/samples/website_integration/index.php similarity index 66% rename from samples/website_integration/bcrypt/form.php rename to samples/website_integration/index.php index 7801be4d..a5509da8 100644 --- a/samples/website_integration/bcrypt/form.php +++ b/samples/website_integration/index.php @@ -1,6 +1,6 @@ @@ -12,17 +12,24 @@ checkPassword($user, $pass)) { printf('

Hello, %s!

', htmlspecialchars($user)); echo 'Successful login. Nice to have you back!' - . '
Back to form'; + . '
Back to form'; return true; } else { echo '

Error

Invalid username or password.'; @@ -63,15 +70,15 @@ function process_login($user, $pass) { } // Register logic -function process_register($user, $pass) { - if (authme_has_user($user)) { +function process_register($user, $pass, AuthMeController $controller) { + if ($controller->isUserRegistered($user)) { echo '

Error

This user already exists.'; } else { // Note that we don't validate the password or username at all in this demo... - $register_success = authme_register($user, $pass); + $register_success = $controller->register($user, $pass); if ($register_success) { printf('

Welcome, %s!

Thanks for registering', htmlspecialchars($user)); - echo '
Back to form'; + echo '
Back to form'; return true; } else { echo '

Error

Unfortunately, there was an error during the registration.'; diff --git a/samples/website_integration/sha256/form.php b/samples/website_integration/sha256/form.php deleted file mode 100644 index 5ffecf34..00000000 --- a/samples/website_integration/sha256/form.php +++ /dev/null @@ -1,52 +0,0 @@ - - - - - AuthMe Integration Sample - - - -Hello, %s!', htmlspecialchars($user)); - echo 'Successful login. Nice to have you back!' - . '
Back to form'; - $was_successful = true; - } else { - echo '

Error

Invalid username or password.'; - } -} - -if (!$was_successful) { - echo '

Login sample

-This is a demo form for AuthMe website integration. Enter your AuthMe login details -into the following form to test it. -
- - - - -
Name
Pass
-
'; -} - -function get_from_post_or_empty($index_name) { - return trim( - filter_input(INPUT_POST, $index_name, FILTER_UNSAFE_RAW, FILTER_REQUIRE_SCALAR | FILTER_FLAG_STRIP_LOW) - ?: ''); -} -?> - - - diff --git a/samples/website_integration/sha256/integration.php b/samples/website_integration/sha256/integration.php deleted file mode 100644 index e0de0bb1..00000000 --- a/samples/website_integration/sha256/integration.php +++ /dev/null @@ -1,67 +0,0 @@ -prepare("SELECT password FROM $authme_table WHERE username = ?"); - $stmt->bind_param('s', $username); - $stmt->execute(); - $stmt->bind_result($password); - if ($stmt->fetch()) { - return $password; - } - } - return null; -} - -/** - * Checks the given clear-text password against the hash. - * - * @param string $password the clear-text password to check - * @param string $hash the hash to check the password against - * @return bool true iff the password matches the hash, false otherwise - */ -function authme_check_hash($password, $hash) { - // $SHA$salt$hash, where hash := sha256(sha256(password) . salt) - $parts = explode('$', $hash); - return count($parts) === 4 - && $parts[3] === hash('sha256', hash('sha256', $password) . $parts[2]); -} From f0144857894d9a97ae456c1c21f29afff15e0402 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Thu, 19 May 2016 21:50:48 +0200 Subject: [PATCH 061/200] Service injector - implement stricter requirements for PostConstruct methods - Implement similar restrictions as prescribed by the PostConstruct documentation: - Class may have at most one method annotated with PostConstruct - PostConstruct method must return void - Javadoc: replace mentions of injector construction where any injection method was meant --- .../AuthMeServiceInitializer.java | 64 +++++++++++++------ .../authme/permission/PermissionsManager.java | 7 +- .../AuthMeServiceInitializerTest.java | 14 +++- .../samples/InvalidPostConstruct.java | 26 +++++++- .../samples/PostConstructTestClass.java | 13 +--- 5 files changed, 86 insertions(+), 38 deletions(-) diff --git a/src/main/java/fr/xephi/authme/initialization/AuthMeServiceInitializer.java b/src/main/java/fr/xephi/authme/initialization/AuthMeServiceInitializer.java index c6d7fd85..23c2f01d 100644 --- a/src/main/java/fr/xephi/authme/initialization/AuthMeServiceInitializer.java +++ b/src/main/java/fr/xephi/authme/initialization/AuthMeServiceInitializer.java @@ -144,8 +144,8 @@ public class AuthMeServiceInitializer { } /** - * Instantiates the given class by locating an @Inject constructor and retrieving - * or instantiating its parameters. + * Instantiates the given class by locating its @Inject elements and retrieving + * or instantiating the required instances. * * @param clazz the class to instantiate * @param traversedClasses collection of classes already traversed @@ -164,13 +164,13 @@ public class AuthMeServiceInitializer { validateInjectionHasNoCircularDependencies(injection.getDependencies(), traversedClasses); Object[] dependencies = resolveDependencies(injection, traversedClasses); T object = injection.instantiateWith(dependencies); - executePostConstructMethods(object); + executePostConstructMethod(object); return object; } /** - * Resolves the dependencies for the given constructor, i.e. returns a collection that satisfy - * the constructor's parameter types by retrieving elements or instantiating them where necessary. + * Resolves the dependencies for the given class instantiation, i.e. returns a collection that satisfy + * the class' dependencies by retrieving elements or instantiating them where necessary. * * @param injection the injection parameters * @param traversedClasses collection of traversed classes @@ -247,21 +247,20 @@ public class AuthMeServiceInitializer { + "allowed packages. It must be provided explicitly or the package must be passed to the constructor."); } - private static void executePostConstructMethods(Object object) { - for (Method method : object.getClass().getDeclaredMethods()) { - if (method.isAnnotationPresent(PostConstruct.class)) { - if (method.getParameterTypes().length == 0 && !Modifier.isStatic(method.getModifiers())) { - try { - method.setAccessible(true); - method.invoke(object); - } catch (IllegalAccessException | InvocationTargetException e) { - throw new UnsupportedOperationException(e); - } - } else { - throw new IllegalStateException(String.format("@PostConstruct methods may not be static or have " - + " any parameters. Method '%s' of class '%s' is either static or has parameters", - method.getName(), object.getClass().getSimpleName())); - } + /** + * Executes an object's method annotated with {@link PostConstruct} if present. + * Throws an exception if there are multiple such methods, or if the method is static. + * + * @param object the object to execute the post construct method for + */ + private static void executePostConstructMethod(Object object) { + Method postConstructMethod = getAndValidatePostConstructMethod(object.getClass()); + if (postConstructMethod != null) { + try { + postConstructMethod.setAccessible(true); + postConstructMethod.invoke(object); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new UnsupportedOperationException(e); } } } @@ -272,6 +271,31 @@ public class AuthMeServiceInitializer { } } + /** + * Validate and locate the given class' post construct method. Returns {@code null} if none present. + * + * @param clazz the class to search + * @return post construct method, or null + */ + private static Method getAndValidatePostConstructMethod(Class clazz) { + Method postConstructMethod = null; + for (Method method : clazz.getDeclaredMethods()) { + if (method.isAnnotationPresent(PostConstruct.class)) { + if (postConstructMethod != null) { + throw new IllegalStateException("Multiple methods with @PostConstruct on " + clazz); + } else if (method.getParameterTypes().length > 0 || Modifier.isStatic(method.getModifiers())) { + throw new IllegalStateException("@PostConstruct method may not be static or have any parameters. " + + "Invalid method in " + clazz); + } else if (method.getReturnType() != void.class) { + throw new IllegalStateException("@PostConstruct method must be void. Offending class: " + clazz); + } else { + postConstructMethod = method; + } + } + } + return postConstructMethod; + } + @SafeVarargs private static Injection firstNotNull(Provider>... providers) { for (Provider> provider : providers) { diff --git a/src/main/java/fr/xephi/authme/permission/PermissionsManager.java b/src/main/java/fr/xephi/authme/permission/PermissionsManager.java index 9245a6a9..2c94d6f6 100644 --- a/src/main/java/fr/xephi/authme/permission/PermissionsManager.java +++ b/src/main/java/fr/xephi/authme/permission/PermissionsManager.java @@ -96,11 +96,9 @@ public class PermissionsManager implements PermissionsService { /** * Setup and hook into the permissions systems. - * - * @return The detected permissions system. */ @PostConstruct - public PermissionsSystemType setup() { + public void setup() { // Force-unhook from current hooked permissions systems unhook(); @@ -177,7 +175,7 @@ public class PermissionsManager implements PermissionsService { ConsoleLogger.info("Hooked into " + type.getName() + "!"); // Return the used permissions system type - return type; + return; } catch (Exception ex) { // An error occurred, show a warning message @@ -187,7 +185,6 @@ public class PermissionsManager implements PermissionsService { // No recognized permissions system found, show a message and return ConsoleLogger.info("No supported permissions system found! Permissions are disabled!"); - return null; } /** diff --git a/src/test/java/fr/xephi/authme/initialization/AuthMeServiceInitializerTest.java b/src/test/java/fr/xephi/authme/initialization/AuthMeServiceInitializerTest.java index a81571d1..81b2eb66 100644 --- a/src/test/java/fr/xephi/authme/initialization/AuthMeServiceInitializerTest.java +++ b/src/test/java/fr/xephi/authme/initialization/AuthMeServiceInitializerTest.java @@ -166,7 +166,7 @@ public class AuthMeServiceInitializerTest { PostConstructTestClass testClass = initializer.get(PostConstructTestClass.class); // then - assertThat(testClass.werePostConstructsCalled(), equalTo(true)); + assertThat(testClass.wasPostConstructCalled(), equalTo(true)); assertThat(testClass.getBetaManager(), not(nullValue())); } @@ -188,6 +188,18 @@ public class AuthMeServiceInitializerTest { initializer.get(InvalidPostConstruct.ThrowsException.class); } + @Test(expected = RuntimeException.class) + public void shouldThrowForMultiplePostConstructMethods() { + // given / when / then + initializer.get(InvalidPostConstruct.MultiplePostConstructs.class); + } + + @Test(expected = RuntimeException.class) + public void shouldThrowForPostConstructNotReturningVoid() { + // given / when / then + initializer.get(InvalidPostConstruct.NotVoidReturnType.class); + } + @Test(expected = RuntimeException.class) public void shouldThrowForAbstractNonRegisteredDependency() { // given / when / then diff --git a/src/test/java/fr/xephi/authme/initialization/samples/InvalidPostConstruct.java b/src/test/java/fr/xephi/authme/initialization/samples/InvalidPostConstruct.java index 23130e5e..80b6c83e 100644 --- a/src/test/java/fr/xephi/authme/initialization/samples/InvalidPostConstruct.java +++ b/src/test/java/fr/xephi/authme/initialization/samples/InvalidPostConstruct.java @@ -9,10 +9,8 @@ import javax.inject.Inject; public abstract class InvalidPostConstruct { public static final class WithParams { - @SuppressWarnings("unused") @Inject private AlphaService alphaService; - @SuppressWarnings("unused") @Inject private ProvidedClass providedClass; @@ -41,4 +39,28 @@ public abstract class InvalidPostConstruct { throw new IllegalStateException("Exception in post construct"); } } + + public static final class NotVoidReturnType { + @Inject + private ProvidedClass providedClass; + + @PostConstruct + public int returnsInt() { + return 42; + } + } + + public static final class MultiplePostConstructs { + @Inject + private ProvidedClass providedClass; + + @PostConstruct + public void postConstruct1() { + // -- + } + @PostConstruct + public void postConstruct2() { + // -- + } + } } diff --git a/src/test/java/fr/xephi/authme/initialization/samples/PostConstructTestClass.java b/src/test/java/fr/xephi/authme/initialization/samples/PostConstructTestClass.java index 375e58fa..a8b9e64e 100644 --- a/src/test/java/fr/xephi/authme/initialization/samples/PostConstructTestClass.java +++ b/src/test/java/fr/xephi/authme/initialization/samples/PostConstructTestClass.java @@ -17,22 +17,15 @@ public class PostConstructTestClass implements SettingsDependent { @Inject private BetaManager betaManager; private boolean wasPostConstructCalled = false; - private boolean wasSecondPostConstructCalled = false; private boolean wasReloaded = false; @PostConstruct - protected void setFieldToTrue() { + public void postConstructMethod() { wasPostConstructCalled = true; } - @PostConstruct - public int otherPostConstructMethod() { - wasSecondPostConstructCalled = true; - return 42; - } - - public boolean werePostConstructsCalled() { - return wasPostConstructCalled && wasSecondPostConstructCalled; + public boolean wasPostConstructCalled() { + return wasPostConstructCalled; } public BetaManager getBetaManager() { From 9a72fe53b07608a3a80157d7134879cb10a0ef6f Mon Sep 17 00:00:00 2001 From: ljacqu Date: Thu, 19 May 2016 21:55:42 +0200 Subject: [PATCH 062/200] Minor - code householding - Update inaccurate javadoc - Remove unnecessary require call in PHP integration sample --- samples/website_integration/index.php | 1 - .../authme/datasource/AbstractResourceClosingTest.java | 7 ++++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/samples/website_integration/index.php b/samples/website_integration/index.php index a5509da8..0c2a1a1c 100644 --- a/samples/website_integration/index.php +++ b/samples/website_integration/index.php @@ -25,7 +25,6 @@ $pass = get_from_post_or_empty('password'); $was_successful = false; if ($action && $user && $pass) { - require_once('Bcrypt.php'); if ($action === 'Log in') { $was_successful = process_login($user, $pass, $authme_controller); } else if ($action === 'Register') { diff --git a/src/test/java/fr/xephi/authme/datasource/AbstractResourceClosingTest.java b/src/test/java/fr/xephi/authme/datasource/AbstractResourceClosingTest.java index b17f5135..1ecdb637 100644 --- a/src/test/java/fr/xephi/authme/datasource/AbstractResourceClosingTest.java +++ b/src/test/java/fr/xephi/authme/datasource/AbstractResourceClosingTest.java @@ -204,10 +204,11 @@ public abstract class AbstractResourceClosingTest { } /** - * Return a list with some test elements that correspond to the given list type's generic type. + * Return a collection of the required type with some test elements that correspond to the + * collection's generic type. * - * @param type The list type to process and build a test list for - * @return Test list with sample elements of the correct type + * @param type The collection type to process and build a test collection for + * @return Test collection with sample elements of the correct type */ private static Collection getTypedCollection(Type type) { if (type instanceof ParameterizedType) { From 4d634086cd20aed9d8e71dcff736bee6e23052ab Mon Sep 17 00:00:00 2001 From: ljacqu Date: Thu, 19 May 2016 22:02:15 +0200 Subject: [PATCH 063/200] #701 Alternate accounts are shown to all players with their own name --- .../authme/process/login/AsynchronousLogin.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java b/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java index da98a05b..fc6bba1a 100644 --- a/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java +++ b/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java @@ -22,7 +22,6 @@ import fr.xephi.authme.settings.properties.RegistrationSettings; import fr.xephi.authme.settings.properties.RestrictionSettings; import fr.xephi.authme.settings.properties.SecuritySettings; import fr.xephi.authme.task.MessageTask; -import fr.xephi.authme.util.BukkitService; import fr.xephi.authme.util.StringUtils; import fr.xephi.authme.util.Utils; import org.bukkit.Bukkit; @@ -230,13 +229,14 @@ public class AsynchronousLogin implements Process { ConsoleLogger.info("The user " + player.getName() + " has " + auths.size() + " accounts:"); ConsoleLogger.info(message); - for (Player player : service.getOnlinePlayers()) { - if ((player.getName().equalsIgnoreCase(this.player.getName()) && plugin.getPermissionsManager().hasPermission(player, PlayerPermission.SEE_OWN_ACCOUNTS))) { - player.sendMessage("You own " + auths.size() + " accounts:"); - player.sendMessage(message); - } else if (plugin.getPermissionsManager().hasPermission(player, AdminPermission.SEE_OTHER_ACCOUNTS)) { - player.sendMessage("The user " + player.getName() + " has " + auths.size() + " accounts:"); - player.sendMessage(message); + for (Player onlinePlayer : service.getOnlinePlayers()) { + if (onlinePlayer.getName().equalsIgnoreCase(player.getName()) + && plugin.getPermissionsManager().hasPermission(onlinePlayer, PlayerPermission.SEE_OWN_ACCOUNTS)) { + onlinePlayer.sendMessage("You own " + auths.size() + " accounts:"); + onlinePlayer.sendMessage(message); + } else if (plugin.getPermissionsManager().hasPermission(onlinePlayer, AdminPermission.SEE_OTHER_ACCOUNTS)) { + onlinePlayer.sendMessage("The user " + player.getName() + " has " + auths.size() + " accounts:"); + onlinePlayer.sendMessage(message); } } } From 95b65ae20a855d8d89aa42432553fc6f051b5c46 Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Thu, 19 May 2016 23:06:55 +0200 Subject: [PATCH 064/200] Cleanup --- .../command/executable/authme/PurgeCommand.java | 8 -------- .../xephi/authme/initialization/FieldInjection.java | 1 + .../initialization/samples/InvalidPostConstruct.java | 11 ----------- 3 files changed, 1 insertion(+), 19 deletions(-) diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/PurgeCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/PurgeCommand.java index a0811cfe..db91aad6 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/PurgeCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/PurgeCommand.java @@ -4,9 +4,7 @@ import fr.xephi.authme.AuthMe; import fr.xephi.authme.command.CommandService; import fr.xephi.authme.command.ExecutableCommand; import fr.xephi.authme.datasource.DataSource; -import fr.xephi.authme.hooks.PluginHooks; import fr.xephi.authme.task.PurgeTask; -import fr.xephi.authme.util.BukkitService; import org.bukkit.ChatColor; import org.bukkit.command.CommandSender; @@ -26,12 +24,6 @@ public class PurgeCommand implements ExecutableCommand { @Inject private DataSource dataSource; - @Inject - private PluginHooks pluginHooks; - - @Inject - private BukkitService bukkitService; - @Inject private AuthMe plugin; diff --git a/src/main/java/fr/xephi/authme/initialization/FieldInjection.java b/src/main/java/fr/xephi/authme/initialization/FieldInjection.java index b8112b87..096c5f52 100644 --- a/src/main/java/fr/xephi/authme/initialization/FieldInjection.java +++ b/src/main/java/fr/xephi/authme/initialization/FieldInjection.java @@ -114,6 +114,7 @@ class FieldInjection implements Injection { return null; } + @SuppressWarnings("unchecked") private static Constructor getDefaultConstructor(Class clazz) { try { Constructor defaultConstructor = clazz.getDeclaredConstructor(); diff --git a/src/test/java/fr/xephi/authme/initialization/samples/InvalidPostConstruct.java b/src/test/java/fr/xephi/authme/initialization/samples/InvalidPostConstruct.java index 80b6c83e..e6d6e3a6 100644 --- a/src/test/java/fr/xephi/authme/initialization/samples/InvalidPostConstruct.java +++ b/src/test/java/fr/xephi/authme/initialization/samples/InvalidPostConstruct.java @@ -9,11 +9,6 @@ import javax.inject.Inject; public abstract class InvalidPostConstruct { public static final class WithParams { - @Inject - private AlphaService alphaService; - @Inject - private ProvidedClass providedClass; - WithParams() { } @PostConstruct @@ -41,9 +36,6 @@ public abstract class InvalidPostConstruct { } public static final class NotVoidReturnType { - @Inject - private ProvidedClass providedClass; - @PostConstruct public int returnsInt() { return 42; @@ -51,9 +43,6 @@ public abstract class InvalidPostConstruct { } public static final class MultiplePostConstructs { - @Inject - private ProvidedClass providedClass; - @PostConstruct public void postConstruct1() { // -- From 92287cb5ddc5952cb42f82886d1c0864ff304324 Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Thu, 19 May 2016 23:18:16 +0200 Subject: [PATCH 065/200] Delay the first "please login/register" message on join --- .../java/fr/xephi/authme/process/join/AsynchronousJoin.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java b/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java index 3aff8941..26336869 100644 --- a/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java +++ b/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java @@ -224,9 +224,9 @@ public class AsynchronousJoin implements Process { : MessageKey.REGISTER_MESSAGE; } if (msgInterval > 0 && LimboCache.getInstance().getLimboPlayer(name) != null) { - BukkitTask msgTask = service.runTask(new MessageTask(service.getBukkitService(), plugin.getMessages(), - name, msg, msgInterval)); - LimboPlayer limboPlayer = LimboCache.getInstance().getLimboPlayer(name); + BukkitTask msgTask = service.runTaskLater((Runnable)new MessageTask(service.getBukkitService(), plugin.getMessages(), + name, msg, msgInterval), 20L); + LimboPlayer limboPlayer = LimboCache.getInstance().getLimboPlayer(name); if (limboPlayer != null) { limboPlayer.setMessageTask(msgTask); } From 6abad1970c71aa80dde864f6e3c2f31ddc479ba5 Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Fri, 20 May 2016 14:58:41 +0200 Subject: [PATCH 066/200] Revert "Cleanup" This reverts commit 95b65ae20a855d8d89aa42432553fc6f051b5c46. --- .../command/executable/authme/PurgeCommand.java | 8 ++++++++ .../xephi/authme/initialization/FieldInjection.java | 1 - .../initialization/samples/InvalidPostConstruct.java | 11 +++++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/PurgeCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/PurgeCommand.java index db91aad6..a0811cfe 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/PurgeCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/PurgeCommand.java @@ -4,7 +4,9 @@ import fr.xephi.authme.AuthMe; import fr.xephi.authme.command.CommandService; import fr.xephi.authme.command.ExecutableCommand; import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.hooks.PluginHooks; import fr.xephi.authme.task.PurgeTask; +import fr.xephi.authme.util.BukkitService; import org.bukkit.ChatColor; import org.bukkit.command.CommandSender; @@ -24,6 +26,12 @@ public class PurgeCommand implements ExecutableCommand { @Inject private DataSource dataSource; + @Inject + private PluginHooks pluginHooks; + + @Inject + private BukkitService bukkitService; + @Inject private AuthMe plugin; diff --git a/src/main/java/fr/xephi/authme/initialization/FieldInjection.java b/src/main/java/fr/xephi/authme/initialization/FieldInjection.java index 096c5f52..b8112b87 100644 --- a/src/main/java/fr/xephi/authme/initialization/FieldInjection.java +++ b/src/main/java/fr/xephi/authme/initialization/FieldInjection.java @@ -114,7 +114,6 @@ class FieldInjection implements Injection { return null; } - @SuppressWarnings("unchecked") private static Constructor getDefaultConstructor(Class clazz) { try { Constructor defaultConstructor = clazz.getDeclaredConstructor(); diff --git a/src/test/java/fr/xephi/authme/initialization/samples/InvalidPostConstruct.java b/src/test/java/fr/xephi/authme/initialization/samples/InvalidPostConstruct.java index e6d6e3a6..80b6c83e 100644 --- a/src/test/java/fr/xephi/authme/initialization/samples/InvalidPostConstruct.java +++ b/src/test/java/fr/xephi/authme/initialization/samples/InvalidPostConstruct.java @@ -9,6 +9,11 @@ import javax.inject.Inject; public abstract class InvalidPostConstruct { public static final class WithParams { + @Inject + private AlphaService alphaService; + @Inject + private ProvidedClass providedClass; + WithParams() { } @PostConstruct @@ -36,6 +41,9 @@ public abstract class InvalidPostConstruct { } public static final class NotVoidReturnType { + @Inject + private ProvidedClass providedClass; + @PostConstruct public int returnsInt() { return 42; @@ -43,6 +51,9 @@ public abstract class InvalidPostConstruct { } public static final class MultiplePostConstructs { + @Inject + private ProvidedClass providedClass; + @PostConstruct public void postConstruct1() { // -- From 4303dca46944d4b6dbed75fadf6d01c44205dd12 Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Fri, 20 May 2016 15:45:34 +0200 Subject: [PATCH 067/200] Apply no teleport to the respawn listener, remove Settings usage from the PlayerListener --- .../authme/listener/AuthMePlayerListener.java | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java index 7dddaa12..a11d2f8b 100644 --- a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java +++ b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java @@ -17,7 +17,7 @@ import fr.xephi.authme.permission.PermissionsManager; import fr.xephi.authme.permission.PlayerStatePermission; import fr.xephi.authme.process.Management; import fr.xephi.authme.settings.NewSetting; -import fr.xephi.authme.settings.Settings; +//import fr.xephi.authme.settings.Settings; import fr.xephi.authme.settings.SpawnLoader; import fr.xephi.authme.settings.properties.HooksSettings; import fr.xephi.authme.settings.properties.ProtectionSettings; @@ -57,6 +57,7 @@ import org.bukkit.event.player.PlayerShearEntityEvent; import javax.inject.Inject; import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Pattern; import static fr.xephi.authme.listener.ListenerService.shouldCancelEvent; import static fr.xephi.authme.settings.properties.RestrictionSettings.ALLOWED_MOVEMENT_RADIUS; @@ -354,14 +355,16 @@ public class AuthMePlayerListener implements Listener { return; } - if (name.length() > Settings.getMaxNickLength || name.length() < Settings.getMinNickLength) { + if (name.length() > settings.getProperty(RestrictionSettings.MAX_NICKNAME_LENGTH) || name.length() < settings.getProperty(RestrictionSettings.MIN_NICKNAME_LENGTH)) { event.setKickMessage(m.retrieveSingle(MessageKey.INVALID_NAME_LENGTH)); event.setResult(PlayerLoginEvent.Result.KICK_OTHER); return; } - if (!Settings.nickPattern.matcher(player.getName()).matches() || name.equalsIgnoreCase("Player")) { - event.setKickMessage(m.retrieveSingle(MessageKey.INVALID_NAME_CHARACTERS).replace("REG_EX", Settings.getNickRegex)); + String nickRegEx = settings.getProperty(RestrictionSettings.ALLOWED_NICKNAME_CHARACTERS); + Pattern nickPattern = Pattern.compile(nickRegEx); + if (nickPattern.matcher(player.getName()).matches() || name.equalsIgnoreCase("Player")) { + event.setKickMessage(m.retrieveSingle(MessageKey.INVALID_NAME_CHARACTERS).replace("REG_EX", nickRegEx)); event.setResult(PlayerLoginEvent.Result.KICK_OTHER); return; } @@ -508,11 +511,13 @@ public class AuthMePlayerListener implements Listener { if (!shouldCancelEvent(event)) { return; } - + if (settings.getProperty(RestrictionSettings.NO_TELEPORT)) { + return; + } Player player = event.getPlayer(); String name = player.getName().toLowerCase(); Location spawn = spawnLoader.getSpawnLocation(player); - if (Settings.isSaveQuitLocationEnabled && dataSource.isAuthAvailable(name)) { + if (settings.getProperty(RestrictionSettings.SAVE_QUIT_LOCATION) && dataSource.isAuthAvailable(name)) { PlayerAuth auth = PlayerAuth.builder() .name(name) .realName(player.getName()) From 8dd5420e1a51537dde9507ddd2aafe6b8d193384 Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Fri, 20 May 2016 15:46:46 +0200 Subject: [PATCH 068/200] Whoops --- .../java/fr/xephi/authme/listener/AuthMePlayerListener.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java index a11d2f8b..5d700e4e 100644 --- a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java +++ b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java @@ -17,7 +17,6 @@ import fr.xephi.authme.permission.PermissionsManager; import fr.xephi.authme.permission.PlayerStatePermission; import fr.xephi.authme.process.Management; import fr.xephi.authme.settings.NewSetting; -//import fr.xephi.authme.settings.Settings; import fr.xephi.authme.settings.SpawnLoader; import fr.xephi.authme.settings.properties.HooksSettings; import fr.xephi.authme.settings.properties.ProtectionSettings; @@ -508,10 +507,10 @@ public class AuthMePlayerListener implements Listener { @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST) public void onPlayerRespawn(PlayerRespawnEvent event) { - if (!shouldCancelEvent(event)) { + if (settings.getProperty(RestrictionSettings.NO_TELEPORT)) { return; } - if (settings.getProperty(RestrictionSettings.NO_TELEPORT)) { + if (!shouldCancelEvent(event)) { return; } Player player = event.getPlayer(); From 2edcb703c11ecd5fd9a31aba15f507322d76a456 Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Fri, 20 May 2016 15:53:39 +0200 Subject: [PATCH 069/200] Wtf was that? D: --- .../authme/listener/AuthMePlayerListener19.java | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener19.java b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener19.java index f5a20277..f5b073cc 100644 --- a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener19.java +++ b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener19.java @@ -1,6 +1,9 @@ package fr.xephi.authme.listener; +import fr.xephi.authme.settings.NewSetting; import fr.xephi.authme.settings.SpawnLoader; +import fr.xephi.authme.settings.properties.RestrictionSettings; + import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; @@ -16,11 +19,20 @@ public class AuthMePlayerListener19 implements Listener { @Inject private SpawnLoader spawnLoader; - AuthMePlayerListener19() { } + @Inject + private NewSetting settings; + /* WTF was that? We need to check all the settings before moving the player to the spawn! + * + * TODO: fixme please! + * @EventHandler(priority = EventPriority.LOWEST) public void onPlayerSpawn(PlayerSpawnLocationEvent event) { + if(settings.getProperty(RestrictionSettings.NO_TELEPORT)) { + return; + } event.setSpawnLocation(spawnLoader.getSpawnLocation(event.getPlayer())); } + */ } From a355c325c5e243b4b2f05cd3ca16d8bf5ef84af2 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Fri, 20 May 2016 17:15:53 +0200 Subject: [PATCH 070/200] #513 Allow to run updateDocs task from command line --- pom.xml | 2 +- src/test/java/tools/docs/UpdateDocsTask.java | 48 +++++++++++++++----- 2 files changed, 38 insertions(+), 12 deletions(-) diff --git a/pom.xml b/pom.xml index c80dcaa1..dc62638d 100644 --- a/pom.xml +++ b/pom.xml @@ -247,7 +247,7 @@ ${project.basedir}/target/test-classes tools.ToolsRunner - writePermissionsList + updateDocs true diff --git a/src/test/java/tools/docs/UpdateDocsTask.java b/src/test/java/tools/docs/UpdateDocsTask.java index c3fb8562..48624afe 100644 --- a/src/test/java/tools/docs/UpdateDocsTask.java +++ b/src/test/java/tools/docs/UpdateDocsTask.java @@ -4,6 +4,7 @@ import com.google.common.collect.ImmutableSet; import tools.commands.CommandPageCreater; import tools.hashmethods.HashAlgorithmsDescriptionTask; import tools.permissions.PermissionsListWriter; +import tools.utils.AutoToolTask; import tools.utils.ToolTask; import java.util.Scanner; @@ -12,9 +13,9 @@ import java.util.Set; /** * Task that runs all tasks which update files in the docs folder. */ -public class UpdateDocsTask implements ToolTask { +public class UpdateDocsTask implements AutoToolTask { - private final Set> TASKS = ImmutableSet.>of( + private static final Set> TASKS = ImmutableSet.>of( CommandPageCreater.class, HashAlgorithmsDescriptionTask.class, PermissionsListWriter.class); @Override @@ -23,17 +24,25 @@ public class UpdateDocsTask implements ToolTask { } @Override - public void execute(Scanner scanner) { - for (Class taskClass : TASKS) { - try { - ToolTask task = instantiateTask(taskClass); - System.out.println("\nRunning " + task.getTaskName() + "\n-------------------"); + public void execute(final Scanner scanner) { + executeTasks(new TaskRunner() { + @Override + public void execute(ToolTask task) { task.execute(scanner); - } catch (UnsupportedOperationException e) { - System.err.println("Error running task of class '" + taskClass + "'"); - e.printStackTrace(); } - } + }); + } + + @Override + public void executeDefault() { + executeTasks(new TaskRunner() { + @Override + public void execute(ToolTask task) { + if (task instanceof AutoToolTask) { + ((AutoToolTask) task).executeDefault(); + } + } + }); } private static ToolTask instantiateTask(Class clazz) { @@ -43,4 +52,21 @@ public class UpdateDocsTask implements ToolTask { throw new UnsupportedOperationException(e); } } + + private static void executeTasks(TaskRunner runner) { + for (Class taskClass : TASKS) { + try { + ToolTask task = instantiateTask(taskClass); + System.out.println("\nRunning " + task.getTaskName() + "\n-------------------"); + runner.execute(task); + } catch (UnsupportedOperationException e) { + System.err.println("Error running task of class '" + taskClass + "'"); + e.printStackTrace(); + } + } + } + + private interface TaskRunner { + void execute(ToolTask task); + } } From 244e1a2b7d0503c455ca04c92667f0ee2b049671 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Fri, 20 May 2016 17:57:14 +0200 Subject: [PATCH 071/200] Injector - don't use instantiation fallback if PostConstruct method is present - Do not instantiate classes with instantiation fallback if they have a PostConstruct method - thanks @sgdc3 for the hint - Change Injector test to check exception messages also --- .../AuthMeServiceInitializer.java | 5 +- .../initialization/InstantiationFallback.java | 13 ++-- .../AuthMeServiceInitializerTest.java | 72 +++++++++++++------ .../initialization/FieldInjectionTest.java | 2 - .../InstantiationFallbackTest.java | 10 +++ .../samples/InstantiationFallbackClasses.java | 11 ++- .../samples/InvalidPostConstruct.java | 3 + 7 files changed, 85 insertions(+), 31 deletions(-) diff --git a/src/main/java/fr/xephi/authme/initialization/AuthMeServiceInitializer.java b/src/main/java/fr/xephi/authme/initialization/AuthMeServiceInitializer.java index 23c2f01d..9bc8525c 100644 --- a/src/main/java/fr/xephi/authme/initialization/AuthMeServiceInitializer.java +++ b/src/main/java/fr/xephi/authme/initialization/AuthMeServiceInitializer.java @@ -260,7 +260,7 @@ public class AuthMeServiceInitializer { postConstructMethod.setAccessible(true); postConstructMethod.invoke(object); } catch (IllegalAccessException | InvocationTargetException e) { - throw new UnsupportedOperationException(e); + throw new UnsupportedOperationException("Error executing @PostConstruct method", e); } } } @@ -287,7 +287,8 @@ public class AuthMeServiceInitializer { throw new IllegalStateException("@PostConstruct method may not be static or have any parameters. " + "Invalid method in " + clazz); } else if (method.getReturnType() != void.class) { - throw new IllegalStateException("@PostConstruct method must be void. Offending class: " + clazz); + throw new IllegalStateException("@PostConstruct method must have return type void. " + + "Offending class: " + clazz); } else { postConstructMethod = method; } diff --git a/src/main/java/fr/xephi/authme/initialization/InstantiationFallback.java b/src/main/java/fr/xephi/authme/initialization/InstantiationFallback.java index f7017ce1..7a1c0849 100644 --- a/src/main/java/fr/xephi/authme/initialization/InstantiationFallback.java +++ b/src/main/java/fr/xephi/authme/initialization/InstantiationFallback.java @@ -1,5 +1,6 @@ package fr.xephi.authme.initialization; +import javax.annotation.PostConstruct; import javax.inject.Inject; import javax.inject.Provider; import java.lang.reflect.AccessibleObject; @@ -8,7 +9,7 @@ import java.lang.reflect.InvocationTargetException; /** * Fallback instantiation method for classes with an accessible no-args constructor - * and no no {@link Inject} annotations whatsoever. + * and no elements whatsoever annotated with {@link Inject} or {@link PostConstruct}. */ public class InstantiationFallback implements Injection { @@ -54,9 +55,9 @@ public class InstantiationFallback implements Injection { Constructor noArgsConstructor = getNoArgsConstructor(clazz); // Return fallback only if we have no args constructor and no @Inject annotation anywhere if (noArgsConstructor != null - && !isInjectAnnotationPresent(clazz.getDeclaredConstructors()) - && !isInjectAnnotationPresent(clazz.getDeclaredFields()) - && !isInjectAnnotationPresent(clazz.getDeclaredMethods())) { + && !isInjectionAnnotationPresent(clazz.getDeclaredConstructors()) + && !isInjectionAnnotationPresent(clazz.getDeclaredFields()) + && !isInjectionAnnotationPresent(clazz.getDeclaredMethods())) { return new InstantiationFallback<>(noArgsConstructor); } return null; @@ -73,9 +74,9 @@ public class InstantiationFallback implements Injection { } } - private static boolean isInjectAnnotationPresent(A[] accessibles) { + private static boolean isInjectionAnnotationPresent(A[] accessibles) { for (A accessible : accessibles) { - if (accessible.isAnnotationPresent(Inject.class)) { + if (accessible.isAnnotationPresent(Inject.class) || accessible.isAnnotationPresent(PostConstruct.class)) { return true; } } diff --git a/src/test/java/fr/xephi/authme/initialization/AuthMeServiceInitializerTest.java b/src/test/java/fr/xephi/authme/initialization/AuthMeServiceInitializerTest.java index 81b2eb66..d85377f5 100644 --- a/src/test/java/fr/xephi/authme/initialization/AuthMeServiceInitializerTest.java +++ b/src/test/java/fr/xephi/authme/initialization/AuthMeServiceInitializerTest.java @@ -18,8 +18,11 @@ import fr.xephi.authme.initialization.samples.ProvidedClass; import fr.xephi.authme.initialization.samples.Size; import fr.xephi.authme.settings.NewSetting; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.nullValue; @@ -36,6 +39,11 @@ public class AuthMeServiceInitializerTest { private AuthMeServiceInitializer initializer; + // As we test many cases that throw exceptions, we use JUnit's ExpectedException Rule + // to make sure that we receive the exception we expect + @Rule + public ExpectedException expectedException = ExpectedException.none(); + @Before public void setInitializer() { initializer = new AuthMeServiceInitializer(ALLOWED_PACKAGE); @@ -54,15 +62,17 @@ public class AuthMeServiceInitializerTest { } } - @Test(expected = RuntimeException.class) + @Test public void shouldThrowForInvalidPackage() { // given / when / then + expectRuntimeExceptionWith("outside of the allowed packages"); initializer.get(InvalidClass.class); } - @Test(expected = RuntimeException.class) + @Test public void shouldThrowForUnregisteredPrimitiveType() { // given / when / then + expectRuntimeExceptionWith("Primitive types must be provided"); initializer.get(int.class); } @@ -85,24 +95,27 @@ public class AuthMeServiceInitializerTest { assertThat(object.getGammaService(), equalTo(initializer.get(BetaManager.class).getDependencies()[1])); } - @Test(expected = RuntimeException.class) + @Test public void shouldRecognizeCircularReferences() { // given / when / then + expectRuntimeExceptionWith("Found cyclic dependency"); initializer.get(CircularClasses.Circular3.class); } - @Test(expected = RuntimeException.class) + @Test public void shouldThrowForUnregisteredAnnotation() { // given initializer.provide(Size.class, 4523); // when / then + expectRuntimeExceptionWith("must be registered beforehand"); initializer.get(ClassWithAnnotations.class); } - @Test(expected = RuntimeException.class) - public void shouldThrowForFieldInjectionWithNoDefaultConstructor() { + @Test + public void shouldThrowForFieldInjectionWithoutNoArgsConstructor() { // given / when / then + expectRuntimeExceptionWith("Did not find injection method"); initializer.get(BadFieldInjection.class); } @@ -124,36 +137,41 @@ public class AuthMeServiceInitializerTest { equalTo(result.getBetaManager().getDependencies()[1])); } - @Test(expected = RuntimeException.class) + @Test public void shouldThrowForAnnotationAsKey() { // given / when / then + expectRuntimeExceptionWith("Cannot retrieve annotated elements in this way"); initializer.get(Size.class); } - @Test(expected = RuntimeException.class) + @Test public void shouldThrowForSecondRegistration() { // given / when / then + expectRuntimeExceptionWith("There is already an object present"); initializer.register(ProvidedClass.class, new ProvidedClass("")); } - @Test(expected = RuntimeException.class) + @Test public void shouldThrowForSecondAnnotationRegistration() { // given initializer.provide(Size.class, 12); // when / then + expectRuntimeExceptionWith("already registered"); initializer.provide(Size.class, -8); } - @Test(expected = NullPointerException.class) + @Test public void shouldThrowForNullValueAssociatedToAnnotation() { // given / when / then + expectedException.expect(NullPointerException.class); initializer.provide(Duration.class, null); } - @Test(expected = NullPointerException.class) + @Test public void shouldThrowForRegisterWithNull() { // given / when / then + expectedException.expect(NullPointerException.class); initializer.register(String.class, null); } @@ -170,39 +188,45 @@ public class AuthMeServiceInitializerTest { assertThat(testClass.getBetaManager(), not(nullValue())); } - @Test(expected = RuntimeException.class) + @Test public void shouldThrowForInvalidPostConstructMethod() { // given / when / then + expectRuntimeExceptionWith("@PostConstruct method may not be static or have any parameters"); initializer.get(InvalidPostConstruct.WithParams.class); } - @Test(expected = RuntimeException.class) + @Test public void shouldThrowForStaticPostConstructMethod() { // given / when / then + expectRuntimeExceptionWith("@PostConstruct method may not be static or have any parameters"); initializer.get(InvalidPostConstruct.Static.class); } - @Test(expected = RuntimeException.class) + @Test public void shouldForwardExceptionFromPostConstruct() { // given / when / then + expectRuntimeExceptionWith("Error executing @PostConstruct method"); initializer.get(InvalidPostConstruct.ThrowsException.class); } - @Test(expected = RuntimeException.class) + @Test public void shouldThrowForMultiplePostConstructMethods() { // given / when / then + expectRuntimeExceptionWith("Multiple methods with @PostConstruct"); initializer.get(InvalidPostConstruct.MultiplePostConstructs.class); } - @Test(expected = RuntimeException.class) + @Test public void shouldThrowForPostConstructNotReturningVoid() { // given / when / then + expectRuntimeExceptionWith("@PostConstruct method must have return type void"); initializer.get(InvalidPostConstruct.NotVoidReturnType.class); } - @Test(expected = RuntimeException.class) + @Test public void shouldThrowForAbstractNonRegisteredDependency() { // given / when / then + expectRuntimeExceptionWith("cannot be instantiated"); initializer.get(ClassWithAbstractDependency.class); } @@ -220,12 +244,13 @@ public class AuthMeServiceInitializerTest { assertThat(cwad.getAlphaService(), not(nullValue())); } - @Test(expected = RuntimeException.class) + @Test public void shouldThrowForAlreadyRegisteredClass() { // given initializer.register(BetaManager.class, new BetaManager()); // when / then + expectRuntimeExceptionWith("There is already an object present"); initializer.register(BetaManager.class, new BetaManager()); } @@ -241,9 +266,10 @@ public class AuthMeServiceInitializerTest { assertThat(singletonScoped, not(sameInstance(requestScoped))); } - @Test(expected = RuntimeException.class) + @Test public void shouldThrowForStaticFieldInjection() { // given / when / then + expectRuntimeExceptionWith("is static but annotated with @Inject"); initializer.newInstance(InvalidStaticFieldInjection.class); } @@ -283,10 +309,16 @@ public class AuthMeServiceInitializerTest { assertThat(providedClass.getWasReloaded(), equalTo(true)); } - @Test(expected = RuntimeException.class) + @Test public void shouldThrowForNullSetting() { // given / when / then + expectRuntimeExceptionWith("Settings instance is null"); initializer.performReloadOnServices(); } + private void expectRuntimeExceptionWith(String message) { + expectedException.expect(RuntimeException.class); + expectedException.expectMessage(containsString(message)); + } + } diff --git a/src/test/java/fr/xephi/authme/initialization/FieldInjectionTest.java b/src/test/java/fr/xephi/authme/initialization/FieldInjectionTest.java index b784e4da..39675343 100644 --- a/src/test/java/fr/xephi/authme/initialization/FieldInjectionTest.java +++ b/src/test/java/fr/xephi/authme/initialization/FieldInjectionTest.java @@ -115,11 +115,9 @@ public class FieldInjectionTest { } private static class ThrowingConstructor { - @SuppressWarnings("unused") @Inject private ProvidedClass providedClass; - @SuppressWarnings("unused") public ThrowingConstructor() { throw new UnsupportedOperationException("Exception in constructor"); } diff --git a/src/test/java/fr/xephi/authme/initialization/InstantiationFallbackTest.java b/src/test/java/fr/xephi/authme/initialization/InstantiationFallbackTest.java index c40648f6..2676c404 100644 --- a/src/test/java/fr/xephi/authme/initialization/InstantiationFallbackTest.java +++ b/src/test/java/fr/xephi/authme/initialization/InstantiationFallbackTest.java @@ -65,4 +65,14 @@ public class InstantiationFallbackTest { assertThat(instantiation, nullValue()); } + @Test + public void shouldReturnNullForClassWithPostConstruct() { + // given / when + Injection instantiation = + InstantiationFallback.provide(InstantiationFallbackClasses.ClassWithPostConstruct.class).get(); + + // then + assertThat(instantiation, nullValue()); + } + } diff --git a/src/test/java/fr/xephi/authme/initialization/samples/InstantiationFallbackClasses.java b/src/test/java/fr/xephi/authme/initialization/samples/InstantiationFallbackClasses.java index ae15029b..c7fbd7e2 100644 --- a/src/test/java/fr/xephi/authme/initialization/samples/InstantiationFallbackClasses.java +++ b/src/test/java/fr/xephi/authme/initialization/samples/InstantiationFallbackClasses.java @@ -1,9 +1,10 @@ package fr.xephi.authme.initialization.samples; +import javax.annotation.PostConstruct; import javax.inject.Inject; /** - * Sample class - triggers instantiation fallback. + * Sample class - tests various situations for the instantiation fallback. */ public abstract class InstantiationFallbackClasses { @@ -42,4 +43,12 @@ public abstract class InstantiationFallbackClasses { } } + // Class with @PostConstruct method should never be instantiated by instantiation fallback + public static final class ClassWithPostConstruct { + @PostConstruct + public void postConstructMethod() { + // -- + } + } + } diff --git a/src/test/java/fr/xephi/authme/initialization/samples/InvalidPostConstruct.java b/src/test/java/fr/xephi/authme/initialization/samples/InvalidPostConstruct.java index 80b6c83e..501fad6a 100644 --- a/src/test/java/fr/xephi/authme/initialization/samples/InvalidPostConstruct.java +++ b/src/test/java/fr/xephi/authme/initialization/samples/InvalidPostConstruct.java @@ -34,6 +34,9 @@ public abstract class InvalidPostConstruct { } public static final class ThrowsException { + @Inject + private ProvidedClass providedClass; + @PostConstruct public void throwingPostConstruct() { throw new IllegalStateException("Exception in post construct"); From 5adf81991092e0d76bd99ae6eeaa062513b312d8 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Fri, 20 May 2016 18:03:22 +0200 Subject: [PATCH 072/200] Minor - remove unused services from PurgeCommand - Found by sgdc3 in 95b65ae20a855d8d89aa42432553fc6f051b5c46 --- .../authme/command/executable/authme/PurgeCommand.java | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/PurgeCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/PurgeCommand.java index a0811cfe..db91aad6 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/PurgeCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/PurgeCommand.java @@ -4,9 +4,7 @@ import fr.xephi.authme.AuthMe; import fr.xephi.authme.command.CommandService; import fr.xephi.authme.command.ExecutableCommand; import fr.xephi.authme.datasource.DataSource; -import fr.xephi.authme.hooks.PluginHooks; import fr.xephi.authme.task.PurgeTask; -import fr.xephi.authme.util.BukkitService; import org.bukkit.ChatColor; import org.bukkit.command.CommandSender; @@ -26,12 +24,6 @@ public class PurgeCommand implements ExecutableCommand { @Inject private DataSource dataSource; - @Inject - private PluginHooks pluginHooks; - - @Inject - private BukkitService bukkitService; - @Inject private AuthMe plugin; From 3f039d641acc3248c818fffd1d7cfcd87f2e2698 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Fri, 20 May 2016 19:42:30 +0200 Subject: [PATCH 073/200] #707 Convert sync processes into services --- .../authme/process/AsynchronousProcess.java | 10 ++ .../fr/xephi/authme/process/NewProcess.java | 7 - .../java/fr/xephi/authme/process/Process.java | 8 - .../xephi/authme/process/ProcessService.java | 15 -- .../authme/process/SyncProcessManager.java | 86 +++++++++++ .../authme/process/SynchronousProcess.java | 10 ++ .../authme/process/email/AsyncAddEmail.java | 4 +- .../process/email/AsyncChangeEmail.java | 4 +- .../authme/process/join/AsynchronousJoin.java | 7 +- .../process/login/AsynchronousLogin.java | 29 ++-- .../process/login/ProcessSyncPlayerLogin.java | 143 ++++++++---------- .../process/logout/AsynchronousLogout.java | 25 +-- .../ProcessSynchronousPlayerLogout.java | 57 ++++--- .../authme/process/quit/AsynchronousQuit.java | 14 +- .../quit/ProcessSyncronousPlayerQuit.java | 34 +---- .../process/register/AsyncRegister.java | 24 +-- .../register/ProcessSyncEmailRegister.java | 39 +++-- .../register/ProcessSyncPasswordRegister.java | 41 +++-- .../unregister/AsynchronousUnregister.java | 4 +- .../authme/process/ProcessServiceTest.java | 20 --- 20 files changed, 303 insertions(+), 278 deletions(-) create mode 100644 src/main/java/fr/xephi/authme/process/AsynchronousProcess.java delete mode 100644 src/main/java/fr/xephi/authme/process/NewProcess.java delete mode 100644 src/main/java/fr/xephi/authme/process/Process.java create mode 100644 src/main/java/fr/xephi/authme/process/SyncProcessManager.java create mode 100644 src/main/java/fr/xephi/authme/process/SynchronousProcess.java diff --git a/src/main/java/fr/xephi/authme/process/AsynchronousProcess.java b/src/main/java/fr/xephi/authme/process/AsynchronousProcess.java new file mode 100644 index 00000000..80c06313 --- /dev/null +++ b/src/main/java/fr/xephi/authme/process/AsynchronousProcess.java @@ -0,0 +1,10 @@ +package fr.xephi.authme.process; + +/** + * Marker interface for asynchronous AuthMe processes. + *

+ * These processes handle intensive (I/O or otherwise) actions and are + * therefore scheduled to run asynchronously. + */ +public interface AsynchronousProcess { +} diff --git a/src/main/java/fr/xephi/authme/process/NewProcess.java b/src/main/java/fr/xephi/authme/process/NewProcess.java deleted file mode 100644 index 524f2c77..00000000 --- a/src/main/java/fr/xephi/authme/process/NewProcess.java +++ /dev/null @@ -1,7 +0,0 @@ -package fr.xephi.authme.process; - -/** - * Marker interface for AuthMe processes. - */ -public interface NewProcess { -} diff --git a/src/main/java/fr/xephi/authme/process/Process.java b/src/main/java/fr/xephi/authme/process/Process.java deleted file mode 100644 index d6efd2b2..00000000 --- a/src/main/java/fr/xephi/authme/process/Process.java +++ /dev/null @@ -1,8 +0,0 @@ -package fr.xephi.authme.process; - -/** - * Common interface for AuthMe processes. - */ -public interface Process extends Runnable { - -} diff --git a/src/main/java/fr/xephi/authme/process/ProcessService.java b/src/main/java/fr/xephi/authme/process/ProcessService.java index e8f0a73b..3dede747 100644 --- a/src/main/java/fr/xephi/authme/process/ProcessService.java +++ b/src/main/java/fr/xephi/authme/process/ProcessService.java @@ -3,8 +3,6 @@ package fr.xephi.authme.process; import fr.xephi.authme.AuthMe; import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.output.Messages; -import fr.xephi.authme.security.PasswordSecurity; -import fr.xephi.authme.security.crypts.HashedPassword; import fr.xephi.authme.settings.NewSetting; import fr.xephi.authme.settings.domain.Property; import fr.xephi.authme.util.BukkitService; @@ -29,8 +27,6 @@ public class ProcessService { @Inject private AuthMe authMe; @Inject - private PasswordSecurity passwordSecurity; - @Inject private ValidationService validationService; @Inject private BukkitService bukkitService; @@ -145,17 +141,6 @@ public class ProcessService { return authMe; } - /** - * Compute the hash for the given password. - * - * @param password the password to hash - * @param username the user to hash for - * @return the resulting hash - */ - public HashedPassword computeHash(String password, String username) { - return passwordSecurity.computeHash(password, username); - } - /** * Verifies whether a password is valid according to the plugin settings. * diff --git a/src/main/java/fr/xephi/authme/process/SyncProcessManager.java b/src/main/java/fr/xephi/authme/process/SyncProcessManager.java new file mode 100644 index 00000000..221e1e8c --- /dev/null +++ b/src/main/java/fr/xephi/authme/process/SyncProcessManager.java @@ -0,0 +1,86 @@ +package fr.xephi.authme.process; + +import fr.xephi.authme.process.login.ProcessSyncPlayerLogin; +import fr.xephi.authme.process.logout.ProcessSynchronousPlayerLogout; +import fr.xephi.authme.process.quit.ProcessSyncronousPlayerQuit; +import fr.xephi.authme.process.register.ProcessSyncEmailRegister; +import fr.xephi.authme.process.register.ProcessSyncPasswordRegister; +import fr.xephi.authme.util.BukkitService; +import org.bukkit.entity.Player; + +import javax.inject.Inject; + +/** + * Manager for scheduling synchronous processes internally from the asynchronous processes. + * These synchronous processes are a continuation of the associated async processes; they only + * contain certain tasks which may only be run synchronously (most interactions with Bukkit). + * These synchronous tasks should never be called aside from the asynchronous processes. + * + * @see Management + */ +public class SyncProcessManager { + + @Inject + private BukkitService bukkitService; + + @Inject + private ProcessSyncEmailRegister processSyncEmailRegister; + @Inject + private ProcessSyncPasswordRegister processSyncPasswordRegister; + @Inject + private ProcessSyncPlayerLogin processSyncPlayerLogin; + @Inject + private ProcessSynchronousPlayerLogout processSynchronousPlayerLogout; + @Inject + private ProcessSyncronousPlayerQuit processSyncronousPlayerQuit; + + + public void processSyncEmailRegister(final Player player) { + runTask(new Runnable() { + @Override + public void run() { + processSyncEmailRegister.processEmailRegister(player); + } + }); + } + + public void processSyncPasswordRegister(final Player player) { + runTask(new Runnable() { + @Override + public void run() { + processSyncPasswordRegister.processPasswordRegister(player); + } + }); + } + + public void processSyncPlayerLogout(final Player player) { + runTask(new Runnable() { + @Override + public void run() { + processSynchronousPlayerLogout.processSyncLogout(player); + } + }); + } + + public void processSyncPlayerLogin(final Player player) { + runTask(new Runnable() { + @Override + public void run() { + processSyncPlayerLogin.processPlayerLogin(player); + } + }); + } + + public void processSyncPlayerQuit(final Player player, final boolean isOp, final boolean needToChange) { + runTask(new Runnable() { + @Override + public void run() { + processSyncronousPlayerQuit.processSyncQuit(player, isOp, needToChange); + } + }); + } + + private void runTask(Runnable runnable) { + bukkitService.scheduleSyncDelayedTask(runnable); + } +} diff --git a/src/main/java/fr/xephi/authme/process/SynchronousProcess.java b/src/main/java/fr/xephi/authme/process/SynchronousProcess.java new file mode 100644 index 00000000..6c23a0f8 --- /dev/null +++ b/src/main/java/fr/xephi/authme/process/SynchronousProcess.java @@ -0,0 +1,10 @@ +package fr.xephi.authme.process; + +/** + * Marker interface for synchronous processes. + *

+ * Such processes are scheduled by {@link AsynchronousProcess asynchronous tasks} to perform tasks + * which are required to be executed synchronously (e.g. interactions with the Bukkit API). + */ +public interface SynchronousProcess { +} diff --git a/src/main/java/fr/xephi/authme/process/email/AsyncAddEmail.java b/src/main/java/fr/xephi/authme/process/email/AsyncAddEmail.java index f4f1ed87..d7aa3127 100644 --- a/src/main/java/fr/xephi/authme/process/email/AsyncAddEmail.java +++ b/src/main/java/fr/xephi/authme/process/email/AsyncAddEmail.java @@ -5,7 +5,7 @@ import fr.xephi.authme.cache.auth.PlayerAuth; import fr.xephi.authme.cache.auth.PlayerCache; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.output.MessageKey; -import fr.xephi.authme.process.NewProcess; +import fr.xephi.authme.process.AsynchronousProcess; import fr.xephi.authme.process.ProcessService; import fr.xephi.authme.settings.properties.RegistrationSettings; import org.bukkit.entity.Player; @@ -15,7 +15,7 @@ import javax.inject.Inject; /** * Async task to add an email to an account. */ -public class AsyncAddEmail implements NewProcess { +public class AsyncAddEmail implements AsynchronousProcess { @Inject private ProcessService service; diff --git a/src/main/java/fr/xephi/authme/process/email/AsyncChangeEmail.java b/src/main/java/fr/xephi/authme/process/email/AsyncChangeEmail.java index 2f4e90b5..3ca6a246 100644 --- a/src/main/java/fr/xephi/authme/process/email/AsyncChangeEmail.java +++ b/src/main/java/fr/xephi/authme/process/email/AsyncChangeEmail.java @@ -4,7 +4,7 @@ import fr.xephi.authme.cache.auth.PlayerAuth; import fr.xephi.authme.cache.auth.PlayerCache; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.output.MessageKey; -import fr.xephi.authme.process.NewProcess; +import fr.xephi.authme.process.AsynchronousProcess; import fr.xephi.authme.process.ProcessService; import fr.xephi.authme.settings.properties.RegistrationSettings; import org.bukkit.entity.Player; @@ -14,7 +14,7 @@ import javax.inject.Inject; /** * Async task for changing the email. */ -public class AsyncChangeEmail implements NewProcess { +public class AsyncChangeEmail implements AsynchronousProcess { @Inject private ProcessService service; diff --git a/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java b/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java index f3504475..6689172e 100644 --- a/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java +++ b/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java @@ -14,7 +14,7 @@ import fr.xephi.authme.hooks.PluginHooks; import fr.xephi.authme.listener.AuthMePlayerListener; import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.permission.PlayerStatePermission; -import fr.xephi.authme.process.NewProcess; +import fr.xephi.authme.process.AsynchronousProcess; import fr.xephi.authme.process.ProcessService; import fr.xephi.authme.settings.Settings; import fr.xephi.authme.settings.SpawnLoader; @@ -40,9 +40,8 @@ import javax.inject.Inject; import static fr.xephi.authme.settings.properties.RestrictionSettings.PROTECT_INVENTORY_BEFORE_LOGIN; -/** - */ -public class AsynchronousJoin implements NewProcess { + +public class AsynchronousJoin implements AsynchronousProcess { @Inject private AuthMe plugin; diff --git a/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java b/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java index 1bbc6baf..74de3b15 100644 --- a/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java +++ b/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java @@ -13,8 +13,9 @@ import fr.xephi.authme.permission.AdminPermission; import fr.xephi.authme.permission.PermissionsManager; import fr.xephi.authme.permission.PlayerPermission; import fr.xephi.authme.permission.PlayerStatePermission; -import fr.xephi.authme.process.NewProcess; +import fr.xephi.authme.process.AsynchronousProcess; import fr.xephi.authme.process.ProcessService; +import fr.xephi.authme.process.SyncProcessManager; import fr.xephi.authme.security.RandomString; import fr.xephi.authme.settings.Settings; import fr.xephi.authme.settings.properties.DatabaseSettings; @@ -34,7 +35,7 @@ import java.util.List; /** */ -public class AsynchronousLogin implements NewProcess { +public class AsynchronousLogin implements AsynchronousProcess { @Inject private AuthMe plugin; @@ -54,6 +55,10 @@ public class AsynchronousLogin implements NewProcess { @Inject private LimboCache limboCache; + @Inject + private SyncProcessManager syncProcessManager; + + AsynchronousLogin() { } private boolean needsCaptcha(Player player) { @@ -187,16 +192,16 @@ public class AsynchronousLogin implements NewProcess { // task, we schedule it in the end // so that we can be sure, and have not to care if it might be // processed in other order. - ProcessSyncPlayerLogin syncPlayerLogin = new ProcessSyncPlayerLogin(player, plugin, database, service); - if (syncPlayerLogin.getLimbo() != null) { - if (syncPlayerLogin.getLimbo().getTimeoutTask() != null) { - syncPlayerLogin.getLimbo().getTimeoutTask().cancel(); + LimboPlayer limboPlayer = limboCache.getLimboPlayer(name); + if (limboPlayer != null) { + if (limboPlayer.getTimeoutTask() != null) { + limboPlayer.getTimeoutTask().cancel(); } - if (syncPlayerLogin.getLimbo().getMessageTask() != null) { - syncPlayerLogin.getLimbo().getMessageTask().cancel(); + if (limboPlayer.getMessageTask() != null) { + limboPlayer.getMessageTask().cancel(); } } - Bukkit.getScheduler().scheduleSyncDelayedTask(plugin, syncPlayerLogin); + syncProcessManager.processSyncPlayerLogin(player); } else if (player.isOnline()) { if (!service.getProperty(SecuritySettings.REMOVE_SPAM_FROM_CONSOLE)) { ConsoleLogger.info(player.getName() + " used the wrong password"); @@ -233,12 +238,12 @@ public class AsynchronousLogin implements NewProcess { ConsoleLogger.info(message); for (Player onlinePlayer : service.getOnlinePlayers()) { - if ((onlinePlayer.getName().equalsIgnoreCase(onlinePlayer.getName()) - && permissionsManager.hasPermission(onlinePlayer, PlayerPermission.SEE_OWN_ACCOUNTS))) { + if (onlinePlayer.getName().equalsIgnoreCase(player.getName()) + && permissionsManager.hasPermission(onlinePlayer, PlayerPermission.SEE_OWN_ACCOUNTS)) { onlinePlayer.sendMessage("You own " + auths.size() + " accounts:"); onlinePlayer.sendMessage(message); } else if (permissionsManager.hasPermission(onlinePlayer, AdminPermission.SEE_OTHER_ACCOUNTS)) { - onlinePlayer.sendMessage("The user " + onlinePlayer.getName() + " has " + auths.size() + " accounts:"); + onlinePlayer.sendMessage("The user " + player.getName() + " has " + auths.size() + " accounts:"); onlinePlayer.sendMessage(message); } } diff --git a/src/main/java/fr/xephi/authme/process/login/ProcessSyncPlayerLogin.java b/src/main/java/fr/xephi/authme/process/login/ProcessSyncPlayerLogin.java index 9f5de6bd..e40226d6 100644 --- a/src/main/java/fr/xephi/authme/process/login/ProcessSyncPlayerLogin.java +++ b/src/main/java/fr/xephi/authme/process/login/ProcessSyncPlayerLogin.java @@ -4,7 +4,6 @@ import com.google.common.io.ByteArrayDataOutput; import com.google.common.io.ByteStreams; import fr.xephi.authme.AuthMe; import fr.xephi.authme.cache.auth.PlayerAuth; -import fr.xephi.authme.cache.backup.JsonCache; import fr.xephi.authme.cache.limbo.LimboCache; import fr.xephi.authme.cache.limbo.LimboPlayer; import fr.xephi.authme.datasource.DataSource; @@ -13,8 +12,8 @@ import fr.xephi.authme.events.LoginEvent; import fr.xephi.authme.events.RestoreInventoryEvent; import fr.xephi.authme.events.SpawnTeleportEvent; import fr.xephi.authme.listener.AuthMePlayerListener; -import fr.xephi.authme.process.Process; import fr.xephi.authme.process.ProcessService; +import fr.xephi.authme.process.SynchronousProcess; import fr.xephi.authme.settings.Settings; import fr.xephi.authme.settings.properties.HooksSettings; import fr.xephi.authme.settings.properties.RegistrationSettings; @@ -29,125 +28,106 @@ import org.bukkit.entity.Player; import org.bukkit.plugin.PluginManager; import org.bukkit.potion.PotionEffectType; +import javax.inject.Inject; + import static fr.xephi.authme.settings.properties.PluginSettings.KEEP_COLLISIONS_DISABLED; import static fr.xephi.authme.settings.properties.RestrictionSettings.PROTECT_INVENTORY_BEFORE_LOGIN; -public class ProcessSyncPlayerLogin implements Process { +public class ProcessSyncPlayerLogin implements SynchronousProcess { - private final LimboPlayer limbo; - private final Player player; - private final String name; - private final PlayerAuth auth; - private final AuthMe plugin; - private final PluginManager pm; - private final JsonCache playerCache; - private final ProcessService service; + @Inject + private AuthMe plugin; + + @Inject + private ProcessService service; + + @Inject + private LimboCache limboCache; + + @Inject + private DataSource dataSource; + + @Inject + // TODO ljacqu 20160520: Need to check whether we want to inject PluginManager, or some intermediate service + private PluginManager pluginManager; private final boolean restoreCollisions = MethodUtils .getAccessibleMethod(LivingEntity.class, "setCollidable", new Class[]{}) != null; - /** - * Constructor for ProcessSyncPlayerLogin. - * - * @param player Player - * @param plugin AuthMe - * @param database DataSource - * @param service The process service - */ - public ProcessSyncPlayerLogin(Player player, AuthMe plugin, DataSource database, ProcessService service) { - this.plugin = plugin; - this.pm = plugin.getServer().getPluginManager(); - this.player = player; - this.name = player.getName().toLowerCase(); - this.limbo = LimboCache.getInstance().getLimboPlayer(name); - this.auth = database.getAuth(name); - this.playerCache = new JsonCache(); - this.service = service; - } + ProcessSyncPlayerLogin() { } - public LimboPlayer getLimbo() { - return limbo; - } - private void restoreOpState() { - player.setOp(limbo.isOperator()); - } - - private void packQuitLocation() { + private void packQuitLocation(Player player, PlayerAuth auth) { Utils.packCoords(auth.getQuitLocX(), auth.getQuitLocY(), auth.getQuitLocZ(), auth.getWorld(), player); } - private void teleportBackFromSpawn() { - AuthMeTeleportEvent tpEvent = new AuthMeTeleportEvent(player, limbo.getLoc()); - pm.callEvent(tpEvent); + private void teleportBackFromSpawn(Player player, LimboPlayer limboPlayer) { + AuthMeTeleportEvent tpEvent = new AuthMeTeleportEvent(player, limboPlayer.getLoc()); + pluginManager.callEvent(tpEvent); if (!tpEvent.isCancelled() && tpEvent.getTo() != null) { player.teleport(tpEvent.getTo()); } } - private void teleportToSpawn() { + private void teleportToSpawn(Player player) { Location spawnL = plugin.getSpawnLocation(player); SpawnTeleportEvent tpEvent = new SpawnTeleportEvent(player, player.getLocation(), spawnL, true); - pm.callEvent(tpEvent); + pluginManager.callEvent(tpEvent); if (!tpEvent.isCancelled() && tpEvent.getTo() != null) { player.teleport(tpEvent.getTo()); } } - private void restoreSpeedEffects() { - if (!service.getProperty(RestrictionSettings.ALLOW_UNAUTHED_MOVEMENT) && service.getProperty(RestrictionSettings.REMOVE_SPEED)) { + private void restoreSpeedEffects(Player player) { + if (!service.getProperty(RestrictionSettings.ALLOW_UNAUTHED_MOVEMENT) + && service.getProperty(RestrictionSettings.REMOVE_SPEED)) { player.setWalkSpeed(0.2F); player.setFlySpeed(0.1F); } } - private void restoreInventory() { + private void restoreInventory(Player player) { RestoreInventoryEvent event = new RestoreInventoryEvent(player); - pm.callEvent(event); + pluginManager.callEvent(event); if (!event.isCancelled() && plugin.inventoryProtector != null) { plugin.inventoryProtector.sendInventoryPacket(player); } } - private void forceCommands() { + private void forceCommands(Player player) { for (String command : service.getProperty(RegistrationSettings.FORCE_COMMANDS)) { player.performCommand(command.replace("%p", player.getName())); } for (String command : service.getProperty(RegistrationSettings.FORCE_COMMANDS_AS_CONSOLE)) { - Bukkit.getServer().dispatchCommand(Bukkit.getServer().getConsoleSender(), command.replace("%p", player.getName())); + Bukkit.getServer().dispatchCommand( + Bukkit.getServer().getConsoleSender(), command.replace("%p", player.getName())); } } - private void sendBungeeMessage() { - ByteArrayDataOutput out = ByteStreams.newDataOutput(); - out.writeUTF("Forward"); - out.writeUTF("ALL"); - out.writeUTF("AuthMe"); - out.writeUTF("login;" + name); - player.sendPluginMessage(plugin, "BungeeCord", out.toByteArray()); - } - - @Override - public void run() { + public void processPlayerLogin(Player player) { + final String name = player.getName().toLowerCase(); // Limbo contains the State of the Player before /login + final LimboPlayer limbo = limboCache.getLimboPlayer(name); + final PlayerAuth auth = dataSource.getAuth(name); + if (limbo != null) { // Restore Op state and Permission Group - restoreOpState(); + restoreOpState(player, limbo); Utils.setGroup(player, GroupType.LOGGEDIN); if (!Settings.noTeleport) { if (Settings.isTeleportToSpawnEnabled && !Settings.isForceSpawnLocOnJoinEnabled && Settings.getForcedWorlds.contains(player.getWorld().getName())) { if (Settings.isSaveQuitLocationEnabled && auth.getQuitLocY() != 0) { - packQuitLocation(); + packQuitLocation(player, auth); } else { - teleportBackFromSpawn(); + teleportBackFromSpawn(player, limbo); } } else if (Settings.isForceSpawnLocOnJoinEnabled && Settings.getForcedWorlds.contains(player.getWorld().getName())) { - teleportToSpawn(); + teleportToSpawn(player); } else if (Settings.isSaveQuitLocationEnabled && auth.getQuitLocY() != 0) { - packQuitLocation(); + packQuitLocation(player, auth); } else { - teleportBackFromSpawn(); + teleportBackFromSpawn(player, limbo); } } @@ -156,18 +136,15 @@ public class ProcessSyncPlayerLogin implements Process { } if (service.getProperty(PROTECT_INVENTORY_BEFORE_LOGIN)) { - restoreInventory(); + restoreInventory(player); } if (service.getProperty(RestrictionSettings.HIDE_TABLIST_BEFORE_LOGIN) && plugin.tablistHider != null) { plugin.tablistHider.sendTablist(player); } - // Cleanup no longer used temporary data - LimboCache.getInstance().deleteLimboPlayer(name); - if (playerCache.doesCacheExist(player)) { - playerCache.removeCache(player); - } + // Clean up no longer used temporary data + limboCache.deleteLimboPlayer(name); } // We can now display the join message (if delayed) @@ -183,7 +160,7 @@ public class ProcessSyncPlayerLogin implements Process { AuthMePlayerListener.joinMessage.remove(name); } - restoreSpeedEffects(); + restoreSpeedEffects(player); if (service.getProperty(RegistrationSettings.APPLY_BLIND_EFFECT)) { player.removePotionEffect(PotionEffectType.BLINDNESS); } @@ -192,7 +169,7 @@ public class ProcessSyncPlayerLogin implements Process { Bukkit.getServer().getPluginManager().callEvent(new LoginEvent(player)); player.saveData(); if (service.getProperty(HooksSettings.BUNGEECORD)) { - sendBungeeMessage(); + sendBungeeMessage(player); } // Login is done, display welcome message if (service.getProperty(RegistrationSettings.USE_WELCOME_MESSAGE)) { @@ -208,12 +185,16 @@ public class ProcessSyncPlayerLogin implements Process { } // Login is now finished; we can force all commands - forceCommands(); + forceCommands(player); - sendTo(); + sendTo(player); } - private void sendTo() { + private void restoreOpState(Player player, LimboPlayer limboPlayer) { + player.setOp(limboPlayer.isOperator()); + } + + private void sendTo(Player player) { if (!service.getProperty(HooksSettings.BUNGEECORD_SERVER).isEmpty()) { ByteArrayDataOutput out = ByteStreams.newDataOutput(); out.writeUTF("Connect"); @@ -221,4 +202,14 @@ public class ProcessSyncPlayerLogin implements Process { player.sendPluginMessage(plugin, "BungeeCord", out.toByteArray()); } } + + private void sendBungeeMessage(Player player) { + ByteArrayDataOutput out = ByteStreams.newDataOutput(); + out.writeUTF("Forward"); + out.writeUTF("ALL"); + out.writeUTF("AuthMe"); + out.writeUTF("login;" + player.getName()); + player.sendPluginMessage(plugin, "BungeeCord", out.toByteArray()); + } + } diff --git a/src/main/java/fr/xephi/authme/process/logout/AsynchronousLogout.java b/src/main/java/fr/xephi/authme/process/logout/AsynchronousLogout.java index 2ea02d47..840ebfc9 100644 --- a/src/main/java/fr/xephi/authme/process/logout/AsynchronousLogout.java +++ b/src/main/java/fr/xephi/authme/process/logout/AsynchronousLogout.java @@ -6,15 +6,16 @@ import fr.xephi.authme.cache.auth.PlayerCache; import fr.xephi.authme.cache.limbo.LimboCache; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.output.MessageKey; -import fr.xephi.authme.process.NewProcess; +import fr.xephi.authme.process.AsynchronousProcess; import fr.xephi.authme.process.ProcessService; +import fr.xephi.authme.process.SyncProcessManager; import fr.xephi.authme.util.Utils; import fr.xephi.authme.util.Utils.GroupType; import org.bukkit.entity.Player; import javax.inject.Inject; -public class AsynchronousLogout implements NewProcess { +public class AsynchronousLogout implements AsynchronousProcess { @Inject private AuthMe plugin; @@ -31,21 +32,23 @@ public class AsynchronousLogout implements NewProcess { @Inject private LimboCache limboCache; + @Inject + private SyncProcessManager syncProcessManager; + AsynchronousLogout() { } - public void logout(Player player) { + public void logout(final Player player) { final String name = player.getName().toLowerCase(); if (!playerCache.isAuthenticated(name)) { service.send(player, MessageKey.NOT_LOGGED_IN); return; } - final Player p = player; - PlayerAuth auth = PlayerCache.getInstance().getAuth(name); + PlayerAuth auth = playerCache.getAuth(name); database.updateSession(auth); - auth.setQuitLocX(p.getLocation().getX()); - auth.setQuitLocY(p.getLocation().getY()); - auth.setQuitLocZ(p.getLocation().getZ()); - auth.setWorld(p.getWorld().getName()); + auth.setQuitLocX(player.getLocation().getX()); + auth.setQuitLocY(player.getLocation().getY()); + auth.setQuitLocZ(player.getLocation().getZ()); + auth.setWorld(player.getWorld().getName()); database.updateQuitLoc(auth); playerCache.removePlayer(name); @@ -53,7 +56,7 @@ public class AsynchronousLogout implements NewProcess { service.scheduleSyncDelayedTask(new Runnable() { @Override public void run() { - Utils.teleportToSpawn(p); + Utils.teleportToSpawn(player); } }); if (limboCache.hasLimboPlayer(name)) { @@ -61,6 +64,6 @@ public class AsynchronousLogout implements NewProcess { } limboCache.addLimboPlayer(player); Utils.setGroup(player, GroupType.NOTLOGGEDIN); - service.scheduleSyncDelayedTask(new ProcessSynchronousPlayerLogout(p, plugin, service)); + syncProcessManager.processSyncPlayerLogout(player); } } diff --git a/src/main/java/fr/xephi/authme/process/logout/ProcessSynchronousPlayerLogout.java b/src/main/java/fr/xephi/authme/process/logout/ProcessSynchronousPlayerLogout.java index d6d66b90..7f781be2 100644 --- a/src/main/java/fr/xephi/authme/process/logout/ProcessSynchronousPlayerLogout.java +++ b/src/main/java/fr/xephi/authme/process/logout/ProcessSynchronousPlayerLogout.java @@ -7,8 +7,8 @@ import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.cache.limbo.LimboCache; import fr.xephi.authme.events.LogoutEvent; import fr.xephi.authme.output.MessageKey; -import fr.xephi.authme.process.Process; import fr.xephi.authme.process.ProcessService; +import fr.xephi.authme.process.SynchronousProcess; import fr.xephi.authme.settings.Settings; import fr.xephi.authme.settings.properties.RegistrationSettings; import fr.xephi.authme.settings.properties.RestrictionSettings; @@ -20,47 +20,42 @@ import org.bukkit.potion.PotionEffect; import org.bukkit.potion.PotionEffectType; import org.bukkit.scheduler.BukkitTask; -/** - */ -public class ProcessSynchronousPlayerLogout implements Process { +import javax.inject.Inject; - private final Player player; - private final AuthMe plugin; - private final String name; - private final ProcessService service; +import static fr.xephi.authme.util.BukkitService.TICKS_PER_SECOND; - /** - * Constructor for ProcessSynchronousPlayerLogout. - * - * @param player Player - * @param plugin AuthMe - * @param service The process service - */ - public ProcessSynchronousPlayerLogout(Player player, AuthMe plugin, ProcessService service) { - this.player = player; - this.plugin = plugin; - this.name = player.getName().toLowerCase(); - this.service = service; - } - protected void sendBungeeMessage() { +public class ProcessSynchronousPlayerLogout implements SynchronousProcess { + + @Inject + private AuthMe plugin; + + @Inject + private ProcessService service; + + @Inject + private LimboCache limboCache; + + ProcessSynchronousPlayerLogout() { } + + private void sendBungeeMessage(Player player) { ByteArrayDataOutput out = ByteStreams.newDataOutput(); out.writeUTF("Forward"); out.writeUTF("ALL"); out.writeUTF("AuthMe"); - out.writeUTF("logout;" + name); + out.writeUTF("logout;" + player.getName()); player.sendPluginMessage(plugin, "BungeeCord", out.toByteArray()); } - protected void restoreSpeedEffect() { + private void restoreSpeedEffect(Player player) { if (Settings.isRemoveSpeedEnabled) { player.setWalkSpeed(0.0F); player.setFlySpeed(0.0F); } } - @Override - public void run() { + public void processSyncLogout(Player player) { + final String name = player.getName().toLowerCase(); if (plugin.sessions.containsKey(name)) { plugin.sessions.get(name).cancel(); plugin.sessions.remove(name); @@ -68,15 +63,15 @@ public class ProcessSynchronousPlayerLogout implements Process { if (Settings.protectInventoryBeforeLogInEnabled) { plugin.inventoryProtector.sendBlankInventoryPacket(player); } - int timeOut = service.getProperty(RestrictionSettings.TIMEOUT) * 20; + int timeOut = service.getProperty(RestrictionSettings.TIMEOUT) * TICKS_PER_SECOND; int interval = service.getProperty(RegistrationSettings.MESSAGE_INTERVAL); if (timeOut != 0) { BukkitTask id = service.runTaskLater(new TimeoutTask(plugin, name, player), timeOut); - LimboCache.getInstance().getLimboPlayer(name).setTimeoutTask(id); + limboCache.getLimboPlayer(name).setTimeoutTask(id); } BukkitTask msgT = service.runTask(new MessageTask(service.getBukkitService(), plugin.getMessages(), name, MessageKey.LOGIN_MESSAGE, interval)); - LimboCache.getInstance().getLimboPlayer(name).setMessageTask(msgT); + limboCache.getLimboPlayer(name).setMessageTask(msgT); if (player.isInsideVehicle() && player.getVehicle() != null) { player.getVehicle().eject(); } @@ -84,11 +79,11 @@ public class ProcessSynchronousPlayerLogout implements Process { player.addPotionEffect(new PotionEffect(PotionEffectType.BLINDNESS, timeOut, 2)); } player.setOp(false); - restoreSpeedEffect(); + restoreSpeedEffect(player); // Player is now logout... Time to fire event ! Bukkit.getServer().getPluginManager().callEvent(new LogoutEvent(player)); if (Settings.bungee) { - sendBungeeMessage(); + sendBungeeMessage(player); } service.send(player, MessageKey.LOGOUT_SUCCESS); ConsoleLogger.info(player.getName() + " logged out"); diff --git a/src/main/java/fr/xephi/authme/process/quit/AsynchronousQuit.java b/src/main/java/fr/xephi/authme/process/quit/AsynchronousQuit.java index 110faa33..d31f305c 100644 --- a/src/main/java/fr/xephi/authme/process/quit/AsynchronousQuit.java +++ b/src/main/java/fr/xephi/authme/process/quit/AsynchronousQuit.java @@ -7,8 +7,9 @@ import fr.xephi.authme.cache.limbo.LimboCache; import fr.xephi.authme.cache.limbo.LimboPlayer; import fr.xephi.authme.datasource.CacheDataSource; import fr.xephi.authme.datasource.DataSource; -import fr.xephi.authme.process.NewProcess; +import fr.xephi.authme.process.AsynchronousProcess; import fr.xephi.authme.process.ProcessService; +import fr.xephi.authme.process.SyncProcessManager; import fr.xephi.authme.settings.Settings; import fr.xephi.authme.settings.properties.RestrictionSettings; import fr.xephi.authme.util.StringUtils; @@ -19,7 +20,9 @@ import org.bukkit.scheduler.BukkitTask; import javax.inject.Inject; -public class AsynchronousQuit implements NewProcess { +import static fr.xephi.authme.util.BukkitService.TICKS_PER_MINUTE; + +public class AsynchronousQuit implements AsynchronousProcess { @Inject private AuthMe plugin; @@ -36,6 +39,9 @@ public class AsynchronousQuit implements NewProcess { @Inject private LimboCache limboCache; + @Inject + private SyncProcessManager syncProcessManager; + AsynchronousQuit() { } @@ -86,7 +92,7 @@ public class AsynchronousQuit implements NewProcess { postLogout(name); } - }, Settings.getSessionTimeout * 20 * 60); + }, Settings.getSessionTimeout * TICKS_PER_MINUTE); plugin.sessions.put(name, task); } else { @@ -100,7 +106,7 @@ public class AsynchronousQuit implements NewProcess { } if (plugin.isEnabled()) { - service.scheduleSyncDelayedTask(new ProcessSyncronousPlayerQuit(plugin, player, isOp, needToChange)); + syncProcessManager.processSyncPlayerQuit(player, isOp, needToChange); } // remove player from cache if (database instanceof CacheDataSource) { diff --git a/src/main/java/fr/xephi/authme/process/quit/ProcessSyncronousPlayerQuit.java b/src/main/java/fr/xephi/authme/process/quit/ProcessSyncronousPlayerQuit.java index bf7ab967..c1382c2c 100644 --- a/src/main/java/fr/xephi/authme/process/quit/ProcessSyncronousPlayerQuit.java +++ b/src/main/java/fr/xephi/authme/process/quit/ProcessSyncronousPlayerQuit.java @@ -1,40 +1,12 @@ package fr.xephi.authme.process.quit; -import fr.xephi.authme.AuthMe; +import fr.xephi.authme.process.SynchronousProcess; import org.bukkit.entity.Player; -/** - */ -public class ProcessSyncronousPlayerQuit implements Runnable { - protected final AuthMe plugin; - protected final Player player; - protected final boolean isOp; - protected final boolean needToChange; +public class ProcessSyncronousPlayerQuit implements SynchronousProcess { - /** - * Constructor for ProcessSyncronousPlayerQuit. - * - * @param plugin AuthMe - * @param player Player - * @param isOp boolean - * @param needToChange boolean - */ - public ProcessSyncronousPlayerQuit(AuthMe plugin, Player player - , boolean isOp, boolean needToChange) { - this.plugin = plugin; - this.player = player; - this.isOp = isOp; - this.needToChange = needToChange; - } - - /** - * Method run. - * - * @see java.lang.Runnable#run() - */ - @Override - public void run() { + public void processSyncQuit(Player player, boolean isOp, boolean needToChange) { if (needToChange) { player.setOp(isOp); } diff --git a/src/main/java/fr/xephi/authme/process/register/AsyncRegister.java b/src/main/java/fr/xephi/authme/process/register/AsyncRegister.java index 6ab88980..fbc6c86d 100644 --- a/src/main/java/fr/xephi/authme/process/register/AsyncRegister.java +++ b/src/main/java/fr/xephi/authme/process/register/AsyncRegister.java @@ -6,9 +6,11 @@ import fr.xephi.authme.cache.auth.PlayerCache; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.permission.PlayerStatePermission; -import fr.xephi.authme.process.NewProcess; +import fr.xephi.authme.process.AsynchronousProcess; import fr.xephi.authme.process.ProcessService; +import fr.xephi.authme.process.SyncProcessManager; import fr.xephi.authme.security.HashAlgorithm; +import fr.xephi.authme.security.PasswordSecurity; import fr.xephi.authme.security.crypts.HashedPassword; import fr.xephi.authme.security.crypts.TwoFactor; import fr.xephi.authme.settings.Settings; @@ -24,7 +26,7 @@ import org.bukkit.entity.Player; import javax.inject.Inject; import java.util.List; -public class AsyncRegister implements NewProcess { +public class AsyncRegister implements AsynchronousProcess { @Inject private AuthMe plugin; @@ -35,9 +37,15 @@ public class AsyncRegister implements NewProcess { @Inject private PlayerCache playerCache; + @Inject + private PasswordSecurity passwordSecurity; + @Inject private ProcessService service; + @Inject + private SyncProcessManager syncProcessManager; + AsyncRegister() { } private boolean preRegisterCheck(Player player, String password) { @@ -104,7 +112,7 @@ public class AsyncRegister implements NewProcess { } } - final HashedPassword hashedPassword = service.computeHash(password, name); + final HashedPassword hashedPassword = passwordSecurity.computeHash(password, name); final String ip = Utils.getPlayerIp(player); PlayerAuth auth = PlayerAuth.builder() .name(name) @@ -122,15 +130,13 @@ public class AsyncRegister implements NewProcess { database.updateEmail(auth); database.updateSession(auth); plugin.mail.main(auth, password); - ProcessSyncEmailRegister sync = new ProcessSyncEmailRegister(player, service); - service.scheduleSyncDelayedTask(sync); - + syncProcessManager.processSyncEmailRegister(player); } private void passwordRegister(Player player, String password, boolean autoLogin) { final String name = player.getName().toLowerCase(); final String ip = Utils.getPlayerIp(player); - final HashedPassword hashedPassword = service.computeHash(password, name); + final HashedPassword hashedPassword = passwordSecurity.computeHash(password, name); PlayerAuth auth = PlayerAuth.builder() .name(name) .realName(player.getName()) @@ -150,9 +156,7 @@ public class AsyncRegister implements NewProcess { // TODO: check this... plugin.getManagement().performLogin(player, "dontneed", true); } - - ProcessSyncPasswordRegister sync = new ProcessSyncPasswordRegister(player, plugin, service); - service.scheduleSyncDelayedTask(sync); + syncProcessManager.processSyncPasswordRegister(player); //give the user the secret code to setup their app code generation if (service.getProperty(SecuritySettings.PASSWORD_HASH) == HashAlgorithm.TWO_FACTOR) { diff --git a/src/main/java/fr/xephi/authme/process/register/ProcessSyncEmailRegister.java b/src/main/java/fr/xephi/authme/process/register/ProcessSyncEmailRegister.java index 461857aa..89400471 100644 --- a/src/main/java/fr/xephi/authme/process/register/ProcessSyncEmailRegister.java +++ b/src/main/java/fr/xephi/authme/process/register/ProcessSyncEmailRegister.java @@ -4,8 +4,8 @@ import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.cache.limbo.LimboCache; import fr.xephi.authme.cache.limbo.LimboPlayer; import fr.xephi.authme.output.MessageKey; -import fr.xephi.authme.process.Process; import fr.xephi.authme.process.ProcessService; +import fr.xephi.authme.process.SynchronousProcess; import fr.xephi.authme.settings.Settings; import fr.xephi.authme.settings.properties.RegistrationSettings; import fr.xephi.authme.settings.properties.RestrictionSettings; @@ -16,34 +16,29 @@ import fr.xephi.authme.util.Utils; import org.bukkit.entity.Player; import org.bukkit.scheduler.BukkitTask; -/** - */ -public class ProcessSyncEmailRegister implements Process { +import javax.inject.Inject; - private final Player player; - private final String name; - private final ProcessService service; +import static fr.xephi.authme.util.BukkitService.TICKS_PER_SECOND; - /** - * Constructor for ProcessSyncEmailRegister. - * - * @param player The player to process an email registration for - * @param service The process service - */ - public ProcessSyncEmailRegister(Player player, ProcessService service) { - this.player = player; - this.name = player.getName().toLowerCase(); - this.service = service; - } - @Override - public void run() { - LimboPlayer limbo = LimboCache.getInstance().getLimboPlayer(name); +public class ProcessSyncEmailRegister implements SynchronousProcess { + + @Inject + private ProcessService service; + + @Inject + private LimboCache limboCache; + + public ProcessSyncEmailRegister() { } + + public void processEmailRegister(Player player) { + final String name = player.getName().toLowerCase(); + LimboPlayer limbo = limboCache.getLimboPlayer(name); if (!Settings.getRegisteredGroup.isEmpty()) { Utils.setGroup(player, Utils.GroupType.REGISTERED); } service.send(player, MessageKey.ACCOUNT_NOT_ACTIVATED); - int time = service.getProperty(RestrictionSettings.TIMEOUT) * 20; + int time = service.getProperty(RestrictionSettings.TIMEOUT) * TICKS_PER_SECOND; int msgInterval = service.getProperty(RegistrationSettings.MESSAGE_INTERVAL); if (limbo != null) { diff --git a/src/main/java/fr/xephi/authme/process/register/ProcessSyncPasswordRegister.java b/src/main/java/fr/xephi/authme/process/register/ProcessSyncPasswordRegister.java index b9505143..b46807a2 100644 --- a/src/main/java/fr/xephi/authme/process/register/ProcessSyncPasswordRegister.java +++ b/src/main/java/fr/xephi/authme/process/register/ProcessSyncPasswordRegister.java @@ -9,7 +9,7 @@ import fr.xephi.authme.cache.limbo.LimboPlayer; import fr.xephi.authme.events.LoginEvent; import fr.xephi.authme.events.RestoreInventoryEvent; import fr.xephi.authme.output.MessageKey; -import fr.xephi.authme.process.Process; +import fr.xephi.authme.process.SynchronousProcess; import fr.xephi.authme.process.ProcessService; import fr.xephi.authme.settings.Settings; import fr.xephi.authme.settings.properties.EmailSettings; @@ -25,35 +25,33 @@ import org.bukkit.entity.Player; import org.bukkit.potion.PotionEffectType; import org.bukkit.scheduler.BukkitTask; +import javax.inject.Inject; + import static fr.xephi.authme.settings.properties.RestrictionSettings.HIDE_TABLIST_BEFORE_LOGIN; import static fr.xephi.authme.util.BukkitService.TICKS_PER_SECOND; /** */ -public class ProcessSyncPasswordRegister implements Process { +public class ProcessSyncPasswordRegister implements SynchronousProcess { - private final Player player; - private final String name; - private final AuthMe plugin; - private final ProcessService service; + @Inject + private AuthMe plugin; - public ProcessSyncPasswordRegister(Player player, AuthMe plugin, ProcessService service) { - this.player = player; - this.name = player.getName().toLowerCase(); - this.plugin = plugin; - this.service = service; - } + @Inject + private ProcessService service; - private void sendBungeeMessage() { + ProcessSyncPasswordRegister() { } + + private void sendBungeeMessage(Player player) { ByteArrayDataOutput out = ByteStreams.newDataOutput(); out.writeUTF("Forward"); out.writeUTF("ALL"); out.writeUTF("AuthMe"); - out.writeUTF("register;" + name); + out.writeUTF("register;" + player.getName()); player.sendPluginMessage(plugin, "BungeeCord", out.toByteArray()); } - private void forceCommands() { + private void forceCommands(Player player) { for (String command : service.getProperty(RegistrationSettings.FORCE_REGISTER_COMMANDS)) { player.performCommand(command.replace("%p", player.getName())); } @@ -69,6 +67,7 @@ public class ProcessSyncPasswordRegister implements Process { * @param player the player */ private void requestLogin(Player player) { + final String name = player.getName().toLowerCase(); Utils.teleportToSpawn(player); LimboCache cache = LimboCache.getInstance(); cache.updateLimboPlayer(player); @@ -87,8 +86,8 @@ public class ProcessSyncPasswordRegister implements Process { } } - @Override - public void run() { + public void processPasswordRegister(Player player) { + final String name = player.getName().toLowerCase(); LimboPlayer limbo = LimboCache.getInstance().getLimboPlayer(name); if (limbo != null) { if (service.getProperty(RestrictionSettings.HIDE_TABLIST_BEFORE_LOGIN) && plugin.tablistHider != null) { @@ -137,7 +136,7 @@ public class ProcessSyncPasswordRegister implements Process { } // Register is now finished; we can force all commands - forceCommands(); + forceCommands(player); // Request login after registration if (service.getProperty(RegistrationSettings.FORCE_LOGIN_AFTER_REGISTER)) { @@ -146,13 +145,13 @@ public class ProcessSyncPasswordRegister implements Process { } if (service.getProperty(HooksSettings.BUNGEECORD)) { - sendBungeeMessage(); + sendBungeeMessage(player); } - sendTo(); + sendTo(player); } - private void sendTo() { + private void sendTo(Player player) { if (!service.getProperty(HooksSettings.BUNGEECORD_SERVER).isEmpty()) { ByteArrayDataOutput out = ByteStreams.newDataOutput(); out.writeUTF("Connect"); diff --git a/src/main/java/fr/xephi/authme/process/unregister/AsynchronousUnregister.java b/src/main/java/fr/xephi/authme/process/unregister/AsynchronousUnregister.java index 02289963..bf3dbb30 100644 --- a/src/main/java/fr/xephi/authme/process/unregister/AsynchronousUnregister.java +++ b/src/main/java/fr/xephi/authme/process/unregister/AsynchronousUnregister.java @@ -8,7 +8,7 @@ import fr.xephi.authme.cache.limbo.LimboCache; import fr.xephi.authme.cache.limbo.LimboPlayer; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.output.MessageKey; -import fr.xephi.authme.process.NewProcess; +import fr.xephi.authme.process.AsynchronousProcess; import fr.xephi.authme.process.ProcessService; import fr.xephi.authme.security.PasswordSecurity; import fr.xephi.authme.settings.Settings; @@ -27,7 +27,7 @@ import javax.inject.Inject; import static fr.xephi.authme.util.BukkitService.TICKS_PER_SECOND; -public class AsynchronousUnregister implements NewProcess { +public class AsynchronousUnregister implements AsynchronousProcess { @Inject private AuthMe plugin; diff --git a/src/test/java/fr/xephi/authme/process/ProcessServiceTest.java b/src/test/java/fr/xephi/authme/process/ProcessServiceTest.java index 7bedb914..f35babb5 100644 --- a/src/test/java/fr/xephi/authme/process/ProcessServiceTest.java +++ b/src/test/java/fr/xephi/authme/process/ProcessServiceTest.java @@ -5,8 +5,6 @@ import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.hooks.PluginHooks; import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.output.Messages; -import fr.xephi.authme.security.PasswordSecurity; -import fr.xephi.authme.security.crypts.HashedPassword; import fr.xephi.authme.settings.NewSetting; import fr.xephi.authme.settings.SpawnLoader; import fr.xephi.authme.settings.properties.SecuritySettings; @@ -40,8 +38,6 @@ public class ProcessServiceTest { @Mock private Messages messages; @Mock - private PasswordSecurity passwordSecurity; - @Mock private AuthMe authMe; @Mock private DataSource dataSource; @@ -140,22 +136,6 @@ public class ProcessServiceTest { assertThat(result, equalTo(authMe)); } - @Test - public void shouldComputeHash() { - // given - String password = "test123"; - String username = "Username"; - HashedPassword hashedPassword = new HashedPassword("hashedResult", "salt12342"); - given(passwordSecurity.computeHash(password, username)).willReturn(hashedPassword); - - // when - HashedPassword result = processService.computeHash(password, username); - - // then - assertThat(result, equalTo(hashedPassword)); - verify(passwordSecurity).computeHash(password, username); - } - @Test public void shouldValidatePassword() { // given From 0c2ee34db392a705e1a2335acfc8a74398054ce9 Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Fri, 20 May 2016 21:27:47 +0200 Subject: [PATCH 074/200] Idk why, but it doesn't work! --- .../fr/xephi/authme/listener/AuthMePlayerListener.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java index 5d700e4e..2dd76c70 100644 --- a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java +++ b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java @@ -17,6 +17,7 @@ import fr.xephi.authme.permission.PermissionsManager; import fr.xephi.authme.permission.PlayerStatePermission; import fr.xephi.authme.process.Management; import fr.xephi.authme.settings.NewSetting; +import fr.xephi.authme.settings.Settings; import fr.xephi.authme.settings.SpawnLoader; import fr.xephi.authme.settings.properties.HooksSettings; import fr.xephi.authme.settings.properties.ProtectionSettings; @@ -360,10 +361,11 @@ public class AuthMePlayerListener implements Listener { return; } - String nickRegEx = settings.getProperty(RestrictionSettings.ALLOWED_NICKNAME_CHARACTERS); - Pattern nickPattern = Pattern.compile(nickRegEx); - if (nickPattern.matcher(player.getName()).matches() || name.equalsIgnoreCase("Player")) { - event.setKickMessage(m.retrieveSingle(MessageKey.INVALID_NAME_CHARACTERS).replace("REG_EX", nickRegEx)); + // TODO: fixme! (the regex doesn't work if compiled from the new settings provider) + //String nickRegEx = settings.getProperty(RestrictionSettings.ALLOWED_NICKNAME_CHARACTERS); + //Pattern nickPattern = Pattern.compile(nickRegEx); + if (Settings.nickPattern.matcher(player.getName()).matches() || name.equalsIgnoreCase("Player")) { + event.setKickMessage(m.retrieveSingle(MessageKey.INVALID_NAME_CHARACTERS).replace("REG_EX", Settings.getNickRegex)); event.setResult(PlayerLoginEvent.Result.KICK_OTHER); return; } From 6285a2137fc8cc9e21c23ebca77738430705c8e7 Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Fri, 20 May 2016 22:00:52 +0200 Subject: [PATCH 075/200] Revert "Idk why, but it doesn't work!" This reverts commit 0c2ee34db392a705e1a2335acfc8a74398054ce9. --- .../fr/xephi/authme/listener/AuthMePlayerListener.java | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java index 2dd76c70..5d700e4e 100644 --- a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java +++ b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java @@ -17,7 +17,6 @@ import fr.xephi.authme.permission.PermissionsManager; import fr.xephi.authme.permission.PlayerStatePermission; import fr.xephi.authme.process.Management; import fr.xephi.authme.settings.NewSetting; -import fr.xephi.authme.settings.Settings; import fr.xephi.authme.settings.SpawnLoader; import fr.xephi.authme.settings.properties.HooksSettings; import fr.xephi.authme.settings.properties.ProtectionSettings; @@ -361,11 +360,10 @@ public class AuthMePlayerListener implements Listener { return; } - // TODO: fixme! (the regex doesn't work if compiled from the new settings provider) - //String nickRegEx = settings.getProperty(RestrictionSettings.ALLOWED_NICKNAME_CHARACTERS); - //Pattern nickPattern = Pattern.compile(nickRegEx); - if (Settings.nickPattern.matcher(player.getName()).matches() || name.equalsIgnoreCase("Player")) { - event.setKickMessage(m.retrieveSingle(MessageKey.INVALID_NAME_CHARACTERS).replace("REG_EX", Settings.getNickRegex)); + String nickRegEx = settings.getProperty(RestrictionSettings.ALLOWED_NICKNAME_CHARACTERS); + Pattern nickPattern = Pattern.compile(nickRegEx); + if (nickPattern.matcher(player.getName()).matches() || name.equalsIgnoreCase("Player")) { + event.setKickMessage(m.retrieveSingle(MessageKey.INVALID_NAME_CHARACTERS).replace("REG_EX", nickRegEx)); event.setResult(PlayerLoginEvent.Result.KICK_OTHER); return; } From 6af65e6cd40a69bd27d2a4246c6d0408bd7f95e1 Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Fri, 20 May 2016 22:02:26 +0200 Subject: [PATCH 076/200] Fix wrong logic (my fault) --- .../java/fr/xephi/authme/listener/AuthMePlayerListener.java | 2 +- src/main/java/fr/xephi/authme/settings/Settings.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java index 5d700e4e..892de53d 100644 --- a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java +++ b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java @@ -362,7 +362,7 @@ public class AuthMePlayerListener implements Listener { String nickRegEx = settings.getProperty(RestrictionSettings.ALLOWED_NICKNAME_CHARACTERS); Pattern nickPattern = Pattern.compile(nickRegEx); - if (nickPattern.matcher(player.getName()).matches() || name.equalsIgnoreCase("Player")) { + if (name.equalsIgnoreCase("Player") || !nickPattern.matcher(player.getName()).matches()) { event.setKickMessage(m.retrieveSingle(MessageKey.INVALID_NAME_CHARACTERS).replace("REG_EX", nickRegEx)); event.setResult(PlayerLoginEvent.Result.KICK_OTHER); return; diff --git a/src/main/java/fr/xephi/authme/settings/Settings.java b/src/main/java/fr/xephi/authme/settings/Settings.java index 0b6d719f..e66fa2e0 100644 --- a/src/main/java/fr/xephi/authme/settings/Settings.java +++ b/src/main/java/fr/xephi/authme/settings/Settings.java @@ -61,7 +61,7 @@ public final class Settings { getSessionTimeout = configFile.getInt("settings.sessions.timeout", 10); getMaxNickLength = configFile.getInt("settings.restrictions.maxNicknameLength", 20); getMinNickLength = configFile.getInt("settings.restrictions.minNicknameLength", 3); - getNickRegex = configFile.getString("settings.restrictions.allowedNicknameCharacters", "[a-zA-Z0-9_?]*"); + getNickRegex = load(RestrictionSettings.ALLOWED_NICKNAME_CHARACTERS); nickPattern = Pattern.compile(getNickRegex); isAllowRestrictedIp = load(RestrictionSettings.ENABLE_RESTRICTED_USERS); isRemoveSpeedEnabled = load(RestrictionSettings.REMOVE_SPEED); From 53043ddc0d7706c1538fb7f877848ecbcd52ed00 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Sat, 21 May 2016 11:54:54 +0200 Subject: [PATCH 077/200] Create tool task that checks Mock fields of test classes --- .../initialization/ConstructorInjection.java | 2 +- .../authme/initialization/FieldInjection.java | 2 +- .../authme/initialization/Injection.java | 2 +- .../authme/command/CommandServiceTest.java | 3 - src/test/java/tools/ToolsRunner.java | 6 +- .../tools/checktestmocks/CheckTestMocks.java | 171 ++++++++++++++++++ src/test/java/tools/utils/ToolsConstants.java | 3 + 7 files changed, 180 insertions(+), 9 deletions(-) create mode 100644 src/test/java/tools/checktestmocks/CheckTestMocks.java diff --git a/src/main/java/fr/xephi/authme/initialization/ConstructorInjection.java b/src/main/java/fr/xephi/authme/initialization/ConstructorInjection.java index e80ea128..74ea28b2 100644 --- a/src/main/java/fr/xephi/authme/initialization/ConstructorInjection.java +++ b/src/main/java/fr/xephi/authme/initialization/ConstructorInjection.java @@ -11,7 +11,7 @@ import java.lang.reflect.InvocationTargetException; /** * Functionality for constructor injection. */ -class ConstructorInjection implements Injection { +public class ConstructorInjection implements Injection { private final Constructor constructor; diff --git a/src/main/java/fr/xephi/authme/initialization/FieldInjection.java b/src/main/java/fr/xephi/authme/initialization/FieldInjection.java index b8112b87..e9717b33 100644 --- a/src/main/java/fr/xephi/authme/initialization/FieldInjection.java +++ b/src/main/java/fr/xephi/authme/initialization/FieldInjection.java @@ -16,7 +16,7 @@ import java.util.List; /** * Functionality for field injection. */ -class FieldInjection implements Injection { +public class FieldInjection implements Injection { private final Field[] fields; private final Constructor defaultConstructor; diff --git a/src/main/java/fr/xephi/authme/initialization/Injection.java b/src/main/java/fr/xephi/authme/initialization/Injection.java index 6e85b4ce..65acf796 100644 --- a/src/main/java/fr/xephi/authme/initialization/Injection.java +++ b/src/main/java/fr/xephi/authme/initialization/Injection.java @@ -5,7 +5,7 @@ package fr.xephi.authme.initialization; * * @param the type of the concerned object */ -interface Injection { +public interface Injection { /** * Returns the dependencies that must be provided to instantiate the given item. diff --git a/src/test/java/fr/xephi/authme/command/CommandServiceTest.java b/src/test/java/fr/xephi/authme/command/CommandServiceTest.java index ba7e4d28..c07d44d5 100644 --- a/src/test/java/fr/xephi/authme/command/CommandServiceTest.java +++ b/src/test/java/fr/xephi/authme/command/CommandServiceTest.java @@ -6,7 +6,6 @@ import fr.xephi.authme.output.Messages; import fr.xephi.authme.settings.NewSetting; import fr.xephi.authme.settings.domain.Property; import fr.xephi.authme.settings.properties.SecuritySettings; -import fr.xephi.authme.util.BukkitService; import fr.xephi.authme.util.ValidationService; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; @@ -45,8 +44,6 @@ public class CommandServiceTest { private NewSetting settings; @Mock private ValidationService validationService; - @Mock - private BukkitService bukkitService; @Test public void shouldSendMessage() { diff --git a/src/test/java/tools/ToolsRunner.java b/src/test/java/tools/ToolsRunner.java index 9986e981..f8e74aea 100644 --- a/src/test/java/tools/ToolsRunner.java +++ b/src/test/java/tools/ToolsRunner.java @@ -81,7 +81,7 @@ public final class ToolsRunner { private static void collectTasksInDirectory(File dir, Map taskCollection) { File[] files = dir.listFiles(); if (files == null) { - throw new RuntimeException("Cannot read folder '" + dir + "'"); + throw new IllegalStateException("Cannot read folder '" + dir + "'"); } for (File file : files) { if (file.isDirectory()) { @@ -112,7 +112,7 @@ public final class ToolsRunner { return constructor.newInstance(); } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException | InstantiationException e) { - throw new RuntimeException("Cannot instantiate task '" + taskClass + "'"); + throw new IllegalStateException("Cannot instantiate task '" + taskClass + "'"); } } @@ -137,7 +137,7 @@ public final class ToolsRunner { ? (Class) clazz : null; } catch (ClassNotFoundException e) { - throw new RuntimeException(e); + throw new IllegalStateException(e); } } diff --git a/src/test/java/tools/checktestmocks/CheckTestMocks.java b/src/test/java/tools/checktestmocks/CheckTestMocks.java new file mode 100644 index 00000000..2508cdd6 --- /dev/null +++ b/src/test/java/tools/checktestmocks/CheckTestMocks.java @@ -0,0 +1,171 @@ +package tools.checktestmocks; + +import com.google.common.base.Function; +import com.google.common.collect.Collections2; +import com.google.common.collect.Sets; +import fr.xephi.authme.initialization.ConstructorInjection; +import fr.xephi.authme.initialization.FieldInjection; +import fr.xephi.authme.initialization.Injection; +import fr.xephi.authme.util.StringUtils; +import org.mockito.Mock; +import tools.utils.AutoToolTask; +import tools.utils.ToolsConstants; + +import java.io.File; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Scanner; +import java.util.Set; + +/** + * Task checking if all tests' {@code @Mock} fields have a corresponding + * {@code @Inject} field in the class they are testing. + */ +public class CheckTestMocks implements AutoToolTask { + + private List errors = new ArrayList<>(); + + @Override + public String getTaskName() { + return "checkTestMocks"; + } + + @Override + public void execute(Scanner scanner) { + executeDefault(); + } + + @Override + public void executeDefault() { + readAndCheckFiles(new File(ToolsConstants.TEST_SOURCE_ROOT)); + System.out.println(StringUtils.join("\n", errors)); + } + + /** + * Recursively reads directories and checks the contained classes. + * + * @param dir the directory to read + */ + private void readAndCheckFiles(File dir) { + File[] files = dir.listFiles(); + if (files == null) { + throw new IllegalStateException("Cannot read folder '" + dir + "'"); + } + for (File file : files) { + if (file.isDirectory()) { + readAndCheckFiles(file); + } else if (file.isFile()) { + Class clazz = loadTestClass(file); + if (clazz != null) { + checkClass(clazz); + } + // else System.out.format("No @Mock fields found in class of file '%s'%n", file.getName()) + } + } + } + + /** + * Checks the given test class' @Mock fields against the corresponding production class' @Inject fields. + * + * @param testClass the test class to verify + */ + private void checkClass(Class testClass) { + Class realClass = returnRealClass(testClass); + if (realClass != null) { + Set> mockFields = getMocks(testClass); + Set> injectFields = getRealClassDependencies(realClass); + if (!injectFields.containsAll(mockFields)) { + addErrorEntry(testClass, "Error - Found the following mocks absent as @Inject: " + + formatClassList(Sets.difference(mockFields, injectFields))); + } else if (!mockFields.containsAll(injectFields)) { + addErrorEntry(testClass, "Info - Found @Inject fields which are not present as @Mock: " + + formatClassList(Sets.difference(injectFields, mockFields))); + } + } + } + + private void addErrorEntry(Class clazz, String message) { + errors.add(clazz.getSimpleName() + ": " + message); + } + + private static Class loadTestClass(File file) { + String fileName = file.getPath(); + String className = fileName + // Strip source folders and .java ending + .substring("src/test/java/".length(), fileName.length() - 5) + .replace(File.separator, "."); + try { + Class clazz = CheckTestMocks.class.getClassLoader().loadClass(className); + return isTestClassWithMocks(clazz) ? clazz : null; + } catch (ClassNotFoundException e) { + throw new UnsupportedOperationException(e); + } + } + + private static Set> getMocks(Class clazz) { + Set> result = new HashSet<>(); + for (Field field : clazz.getDeclaredFields()) { + if (field.isAnnotationPresent(Mock.class)) { + result.add(field.getType()); + } + } + return result; + } + + /** + * Returns the production class ("real class") corresponding to the given test class. + * Returns null if the production class could not be mapped or loaded. + * + * @param testClass the test class to find the corresponding production class for + * @return production class, or null if not found + */ + private static Class returnRealClass(Class testClass) { + String testClassName = testClass.getName(); + String realClassName = testClassName.replaceAll("(Integration|Consistency)?Test$", ""); + if (realClassName.equals(testClassName)) { + System.out.format("%s doesn't match typical test class naming pattern.%n", testClassName); + return null; + } + try { + return CheckTestMocks.class.getClassLoader().loadClass(realClassName); + } catch (ClassNotFoundException e) { + System.out.format("Real class '%s' not found for test class '%s'%n", realClassName, testClassName); + return null; + } + } + + private static Set> getRealClassDependencies(Class realClass) { + Injection injection = ConstructorInjection.provide(realClass).get(); + if (injection != null) { + return Sets.newHashSet(injection.getDependencies()); + } + injection = FieldInjection.provide(realClass).get(); + return injection == null + ? Collections.>emptySet() + : Sets.newHashSet(injection.getDependencies()); + } + + private static boolean isTestClassWithMocks(Class clazz) { + for (Field field : clazz.getDeclaredFields()) { + if (field.isAnnotationPresent(Mock.class)) { + return true; + } + } + return false; + } + + private static String formatClassList(Collection> coll) { + Collection classNames = Collections2.transform(coll, new Function, String>() { + @Override + public String apply(Class input) { + return input.getSimpleName(); + } + }); + return StringUtils.join(", ", classNames); + } + +} diff --git a/src/test/java/tools/utils/ToolsConstants.java b/src/test/java/tools/utils/ToolsConstants.java index 84ad7fad..c94a22b1 100644 --- a/src/test/java/tools/utils/ToolsConstants.java +++ b/src/test/java/tools/utils/ToolsConstants.java @@ -12,6 +12,9 @@ public final class ToolsConstants { public static final String MAIN_RESOURCES_ROOT = "src/main/resources/"; + // Add specific `fr.xephi.authme` package as not to include the tool tasks in the `tools` package + public static final String TEST_SOURCE_ROOT = "src/test/java/fr/xephi/authme"; + public static final String TOOLS_SOURCE_ROOT = "src/test/java/tools/"; public static final String DOCS_FOLDER = "docs/"; From 4d06b63e648e5ec023ee2b2b51f0abdae4b0b0c1 Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Sat, 21 May 2016 14:18:31 +0200 Subject: [PATCH 078/200] Try to fix spambot resource leak #719 --- .../authme/listener/AuthMePlayerListener.java | 34 +++++++++++++++++++ .../listener/AuthMePlayerListener19.java | 12 +++---- 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java index 892de53d..ca15b12f 100644 --- a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java +++ b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java @@ -39,6 +39,7 @@ import org.bukkit.event.inventory.InventoryOpenEvent; import org.bukkit.event.player.AsyncPlayerChatEvent; import org.bukkit.event.player.AsyncPlayerPreLoginEvent; import org.bukkit.event.player.PlayerBedEnterEvent; +import org.bukkit.event.player.PlayerChatEvent; import org.bukkit.event.player.PlayerCommandPreprocessEvent; import org.bukkit.event.player.PlayerDropItemEvent; import org.bukkit.event.player.PlayerFishEvent; @@ -55,6 +56,9 @@ import org.bukkit.event.player.PlayerRespawnEvent; import org.bukkit.event.player.PlayerShearEntityEvent; import javax.inject.Inject; + +import java.util.Iterator; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Pattern; @@ -89,6 +93,7 @@ public class AuthMePlayerListener implements Listener { @Inject private ValidationService validationService; + /* private void handleChat(AsyncPlayerChatEvent event) { if (settings.getProperty(RestrictionSettings.ALLOW_CHAT)) { return; @@ -106,6 +111,7 @@ public class AuthMePlayerListener implements Listener { } } } + */ private void sendLoginOrRegisterMessage(final Player player) { bukkitService.runTaskAsynchronously(new Runnable() { @@ -144,6 +150,33 @@ public class AuthMePlayerListener implements Listener { sendLoginOrRegisterMessage(event.getPlayer()); } + // I think it should be sync! -sgdc3 + @SuppressWarnings("deprecation") + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOW) + public void onPlayerNormalChat(PlayerChatEvent event) { + if (settings.getProperty(RestrictionSettings.ALLOW_CHAT)) { + return; + } + + final Player player = event.getPlayer(); + if (shouldCancelEvent(player)) { + event.setCancelled(true); + // TODO: a spambot calls this too often, too may threads checking if auth is available. + // Possible solution: add a cooldown. + // sendLoginOrRegisterMessage(player); + } else if (settings.getProperty(RestrictionSettings.HIDE_CHAT)) { + Set recipients = event.getRecipients(); + Iterator iter = recipients.iterator(); + if(iter.hasNext()) { + Player p = iter.next(); + if(PlayerCache.getInstance().isAuthenticated(p.getName())) { + iter.remove(); + } + } + } + } + + /* @EventHandler(ignoreCancelled = true, priority = EventPriority.NORMAL) public void onPlayerNormalChat(AsyncPlayerChatEvent event) { handleChat(event); @@ -168,6 +201,7 @@ public class AuthMePlayerListener implements Listener { public void onPlayerLowChat(AsyncPlayerChatEvent event) { handleChat(event); } + */ @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST) public void onPlayerMove(PlayerMoveEvent event) { diff --git a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener19.java b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener19.java index f5b073cc..6cfe854c 100644 --- a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener19.java +++ b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener19.java @@ -1,26 +1,24 @@ package fr.xephi.authme.listener; +/* import fr.xephi.authme.settings.NewSetting; import fr.xephi.authme.settings.SpawnLoader; -import fr.xephi.authme.settings.properties.RestrictionSettings; - -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.Listener; -import org.spigotmc.event.player.PlayerSpawnLocationEvent; - import javax.inject.Inject; +*/ +import org.bukkit.event.Listener; /** * Listener of player events for events introduced in Minecraft 1.9. */ public class AuthMePlayerListener19 implements Listener { + /* @Inject private SpawnLoader spawnLoader; @Inject private NewSetting settings; + */ /* WTF was that? We need to check all the settings before moving the player to the spawn! * From 7ad39e2a5dd865bf4b2f5a4f98aa35aa5f391dc0 Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Sat, 21 May 2016 14:54:03 +0200 Subject: [PATCH 079/200] Use default Hikari pool parameters --- .../fr/xephi/authme/datasource/MySQL.java | 32 ++++++++++--------- .../authme/listener/AuthMePlayerListener.java | 2 +- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/src/main/java/fr/xephi/authme/datasource/MySQL.java b/src/main/java/fr/xephi/authme/datasource/MySQL.java index 694c2fc8..a85e2435 100644 --- a/src/main/java/fr/xephi/authme/datasource/MySQL.java +++ b/src/main/java/fr/xephi/authme/datasource/MySQL.java @@ -109,26 +109,28 @@ public class MySQL implements DataSource { private synchronized void setConnectionArguments() throws RuntimeException { ds = new HikariDataSource(); ds.setPoolName("AuthMeMYSQLPool"); - ds.setDriverClassName("com.mysql.jdbc.Driver"); - ds.setJdbcUrl("jdbc:mysql://" + this.host + ":" + this.port + "/" + this.database); - ds.addDataSourceProperty("rewriteBatchedStatements", "true"); - ds.addDataSourceProperty("jdbcCompliantTruncation", "false"); - ds.addDataSourceProperty("cachePrepStmts", "true"); - ds.addDataSourceProperty("prepStmtCacheSize", "250"); - ds.addDataSourceProperty("prepStmtCacheSqlLimit", "2048"); - //set utf-8 as default encoding + // Database URL + ds.setJdbcUrl("jdbc:mysql://" + this.host + ":" + this.port + "/" + this.database); + + // Auth + ds.setUsername(this.username); + ds.setPassword(this.password); + + // Encoding ds.addDataSourceProperty("characterEncoding", "utf8"); ds.addDataSourceProperty("encoding","UTF-8"); ds.addDataSourceProperty("useUnicode", "true"); - ds.setUsername(this.username); - ds.setPassword(this.password); - ds.setInitializationFailFast(true); // Don't start the plugin if the database is unavailable - ds.setMaxLifetime(180000); // 3 Min - ds.setIdleTimeout(60000); // 1 Min - ds.setMinimumIdle(2); - ds.setMaximumPoolSize((Runtime.getRuntime().availableProcessors() * 2) + 1); + // Random stuff + ds.addDataSourceProperty("rewriteBatchedStatements", "true"); + ds.addDataSourceProperty("jdbcCompliantTruncation", "false"); + + // Caching + ds.addDataSourceProperty("cachePrepStmts", "true"); + ds.addDataSourceProperty("prepStmtCacheSize", "250"); + ds.addDataSourceProperty("prepStmtCacheSqlLimit", "2048"); + ConsoleLogger.info("Connection arguments loaded, Hikari ConnectionPool ready!"); } diff --git a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java index ca15b12f..b2f2d0c3 100644 --- a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java +++ b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java @@ -161,7 +161,7 @@ public class AuthMePlayerListener implements Listener { final Player player = event.getPlayer(); if (shouldCancelEvent(player)) { event.setCancelled(true); - // TODO: a spambot calls this too often, too may threads checking if auth is available. + // TODO: a spambot calls this too often, too many threads checking if auth is available. // Possible solution: add a cooldown. // sendLoginOrRegisterMessage(player); } else if (settings.getProperty(RestrictionSettings.HIDE_CHAT)) { From 7a21294581988c870ed58541e55db70c8003242e Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Sat, 21 May 2016 14:58:57 +0200 Subject: [PATCH 080/200] Don't use synchronized methods #719 --- .../fr/xephi/authme/datasource/MySQL.java | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/src/main/java/fr/xephi/authme/datasource/MySQL.java b/src/main/java/fr/xephi/authme/datasource/MySQL.java index a85e2435..94e43e00 100644 --- a/src/main/java/fr/xephi/authme/datasource/MySQL.java +++ b/src/main/java/fr/xephi/authme/datasource/MySQL.java @@ -106,7 +106,7 @@ public class MySQL implements DataSource { ds = hikariDataSource; } - private synchronized void setConnectionArguments() throws RuntimeException { + private void setConnectionArguments() throws RuntimeException { ds = new HikariDataSource(); ds.setPoolName("AuthMeMYSQLPool"); @@ -135,7 +135,7 @@ public class MySQL implements DataSource { } @Override - public synchronized void reload() throws RuntimeException { + public void reload() throws RuntimeException { if (ds != null) { ds.close(); } @@ -143,11 +143,11 @@ public class MySQL implements DataSource { ConsoleLogger.info("Hikari ConnectionPool arguments reloaded!"); } - private synchronized Connection getConnection() throws SQLException { + private Connection getConnection() throws SQLException { return ds.getConnection(); } - private synchronized void setupConnection() throws SQLException { + private void setupConnection() throws SQLException { try (Connection con = getConnection()) { Statement st = con.createStatement(); DatabaseMetaData md = con.getMetaData(); @@ -260,7 +260,7 @@ public class MySQL implements DataSource { } @Override - public synchronized boolean isAuthAvailable(String user) { + public boolean isAuthAvailable(String user) { String sql = "SELECT " + col.NAME + " FROM " + tableName + " WHERE " + col.NAME + "=?;"; ResultSet rs = null; try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) { @@ -296,7 +296,7 @@ public class MySQL implements DataSource { } @Override - public synchronized PlayerAuth getAuth(String user) { + public PlayerAuth getAuth(String user) { String sql = "SELECT * FROM " + tableName + " WHERE " + col.NAME + "=?;"; PlayerAuth auth; try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) { @@ -330,7 +330,7 @@ public class MySQL implements DataSource { } @Override - public synchronized boolean saveAuth(PlayerAuth auth) { + public boolean saveAuth(PlayerAuth auth) { try (Connection con = getConnection()) { PreparedStatement pst; PreparedStatement pst2; @@ -533,12 +533,12 @@ public class MySQL implements DataSource { } @Override - public synchronized boolean updatePassword(PlayerAuth auth) { + public boolean updatePassword(PlayerAuth auth) { return updatePassword(auth.getNickname(), auth.getPassword()); } @Override - public synchronized boolean updatePassword(String user, HashedPassword password) { + public boolean updatePassword(String user, HashedPassword password) { user = user.toLowerCase(); try (Connection con = getConnection()) { boolean useSalt = !col.SALT.isEmpty(); @@ -596,7 +596,7 @@ public class MySQL implements DataSource { } @Override - public synchronized boolean updateSession(PlayerAuth auth) { + public boolean updateSession(PlayerAuth auth) { String sql = "UPDATE " + tableName + " SET " + col.IP + "=?, " + col.LAST_LOGIN + "=?, " + col.REAL_NAME + "=? WHERE " + col.NAME + "=?;"; try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) { @@ -613,7 +613,7 @@ public class MySQL implements DataSource { } @Override - public synchronized Set autoPurgeDatabase(long until) { + public Set autoPurgeDatabase(long until) { Set list = new HashSet<>(); String select = "SELECT " + col.NAME + " FROM " + tableName + " WHERE " + col.LAST_LOGIN + " getAllAuthsByIp(String ip) { + public List getAllAuthsByIp(String ip) { List result = new ArrayList<>(); String sql = "SELECT " + col.NAME + " FROM " + tableName + " WHERE " + col.IP + "=?;"; try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) { @@ -726,7 +726,7 @@ public class MySQL implements DataSource { } @Override - public synchronized int countAuthsByEmail(String email) { + public int countAuthsByEmail(String email) { String sql = "SELECT COUNT(1) FROM " + tableName + " WHERE UPPER(" + col.EMAIL + ") = UPPER(?)"; try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) { pst.setString(1, email); @@ -742,7 +742,7 @@ public class MySQL implements DataSource { } @Override - public synchronized void purgeBanned(Set banned) { + public void purgeBanned(Set banned) { String sql = "DELETE FROM " + tableName + " WHERE " + col.NAME + "=?;"; try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) { for (String name : banned) { @@ -760,7 +760,7 @@ public class MySQL implements DataSource { } @Override - public synchronized boolean isLogged(String user) { + public boolean isLogged(String user) { String sql = "SELECT " + col.IS_LOGGED + " FROM " + tableName + " WHERE " + col.NAME + "=?;"; try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) { pst.setString(1, user); @@ -774,7 +774,7 @@ public class MySQL implements DataSource { } @Override - public synchronized void setLogged(String user) { + public void setLogged(String user) { String sql = "UPDATE " + tableName + " SET " + col.IS_LOGGED + "=? WHERE " + col.NAME + "=?;"; try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) { pst.setInt(1, 1); @@ -786,7 +786,7 @@ public class MySQL implements DataSource { } @Override - public synchronized void setUnlogged(String user) { + public void setUnlogged(String user) { String sql = "UPDATE " + tableName + " SET " + col.IS_LOGGED + "=? WHERE " + col.NAME + "=?;"; try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) { pst.setInt(1, 0); @@ -826,7 +826,7 @@ public class MySQL implements DataSource { } @Override - public synchronized boolean updateRealName(String user, String realName) { + public boolean updateRealName(String user, String realName) { String sql = "UPDATE " + tableName + " SET " + col.REAL_NAME + "=? WHERE " + col.NAME + "=?;"; try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) { pst.setString(1, realName); @@ -840,7 +840,7 @@ public class MySQL implements DataSource { } @Override - public synchronized boolean updateIp(String user, String ip) { + public boolean updateIp(String user, String ip) { String sql = "UPDATE " + tableName + " SET " + col.IP + "=? WHERE " + col.NAME + "=?;"; try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) { pst.setString(1, ip); From b2b65710b17597692a0a43f8187ea3506638208d Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Sat, 21 May 2016 15:00:48 +0200 Subject: [PATCH 081/200] Also for SQLITE #719 --- .../fr/xephi/authme/datasource/SQLite.java | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/main/java/fr/xephi/authme/datasource/SQLite.java b/src/main/java/fr/xephi/authme/datasource/SQLite.java index fa204a6f..8072d58f 100644 --- a/src/main/java/fr/xephi/authme/datasource/SQLite.java +++ b/src/main/java/fr/xephi/authme/datasource/SQLite.java @@ -64,13 +64,13 @@ public class SQLite implements DataSource { ConsoleLogger.logException("Error while executing SQL statement:", e); } - private synchronized void connect() throws ClassNotFoundException, SQLException { + private void connect() throws ClassNotFoundException, SQLException { Class.forName("org.sqlite.JDBC"); ConsoleLogger.info("SQLite driver loaded"); this.con = DriverManager.getConnection("jdbc:sqlite:plugins/AuthMe/" + database + ".db"); } - private synchronized void setup() throws SQLException { + private void setup() throws SQLException { Statement st = null; ResultSet rs = null; try { @@ -143,7 +143,7 @@ public class SQLite implements DataSource { } @Override - public synchronized boolean isAuthAvailable(String user) { + public boolean isAuthAvailable(String user) { PreparedStatement pst = null; ResultSet rs = null; try { @@ -181,7 +181,7 @@ public class SQLite implements DataSource { } @Override - public synchronized PlayerAuth getAuth(String user) { + public PlayerAuth getAuth(String user) { PreparedStatement pst = null; ResultSet rs = null; try { @@ -201,7 +201,7 @@ public class SQLite implements DataSource { } @Override - public synchronized boolean saveAuth(PlayerAuth auth) { + public boolean saveAuth(PlayerAuth auth) { PreparedStatement pst = null; try { HashedPassword password = auth.getPassword(); @@ -242,12 +242,12 @@ public class SQLite implements DataSource { } @Override - public synchronized boolean updatePassword(PlayerAuth auth) { + public boolean updatePassword(PlayerAuth auth) { return updatePassword(auth.getNickname(), auth.getPassword()); } @Override - public synchronized boolean updatePassword(String user, HashedPassword password) { + public boolean updatePassword(String user, HashedPassword password) { user = user.toLowerCase(); PreparedStatement pst = null; try { @@ -274,7 +274,7 @@ public class SQLite implements DataSource { } @Override - public synchronized boolean updateSession(PlayerAuth auth) { + public boolean updateSession(PlayerAuth auth) { PreparedStatement pst = null; try { pst = con.prepareStatement("UPDATE " + tableName + " SET " + col.IP + "=?, " + col.LAST_LOGIN + "=?, " + col.REAL_NAME + "=? WHERE " + col.NAME + "=?;"); @@ -315,7 +315,7 @@ public class SQLite implements DataSource { } @Override - public synchronized boolean removeAuth(String user) { + public boolean removeAuth(String user) { PreparedStatement pst = null; try { pst = con.prepareStatement("DELETE FROM " + tableName + " WHERE " + col.NAME + "=?;"); @@ -331,7 +331,7 @@ public class SQLite implements DataSource { } @Override - public synchronized boolean updateQuitLoc(PlayerAuth auth) { + public boolean updateQuitLoc(PlayerAuth auth) { PreparedStatement pst = null; try { pst = con.prepareStatement("UPDATE " + tableName + " SET " + col.LASTLOC_X + "=?, " + col.LASTLOC_Y + "=?, " + col.LASTLOC_Z + "=?, " + col.LASTLOC_WORLD + "=? WHERE " + col.NAME + "=?;"); @@ -351,7 +351,7 @@ public class SQLite implements DataSource { } @Override - public synchronized boolean updateEmail(PlayerAuth auth) { + public boolean updateEmail(PlayerAuth auth) { String sql = "UPDATE " + tableName + " SET " + col.EMAIL + "=? WHERE " + col.NAME + "=?;"; try (PreparedStatement pst = con.prepareStatement(sql)) { pst.setString(1, auth.getEmail()); @@ -365,7 +365,7 @@ public class SQLite implements DataSource { } @Override - public synchronized void close() { + public void close() { try { if (con != null && !con.isClosed()) { con.close(); @@ -462,7 +462,7 @@ public class SQLite implements DataSource { } @Override - public synchronized boolean isLogged(String user) { + public boolean isLogged(String user) { PreparedStatement pst = null; ResultSet rs = null; try { @@ -481,7 +481,7 @@ public class SQLite implements DataSource { } @Override - public synchronized void setLogged(String user) { + public void setLogged(String user) { PreparedStatement pst = null; try { pst = con.prepareStatement("UPDATE " + tableName + " SET " + col.IS_LOGGED + "=? WHERE LOWER(" + col.NAME + ")=?;"); @@ -496,7 +496,7 @@ public class SQLite implements DataSource { } @Override - public synchronized void setUnlogged(String user) { + public void setUnlogged(String user) { PreparedStatement pst = null; if (user != null) try { @@ -540,7 +540,7 @@ public class SQLite implements DataSource { } @Override - public synchronized boolean updateRealName(String user, String realName) { + public boolean updateRealName(String user, String realName) { String sql = "UPDATE " + tableName + " SET " + col.REAL_NAME + "=? WHERE " + col.NAME + "=?;"; try (PreparedStatement pst = con.prepareStatement(sql)) { pst.setString(1, realName); @@ -554,7 +554,7 @@ public class SQLite implements DataSource { } @Override - public synchronized boolean updateIp(String user, String ip) { + public boolean updateIp(String user, String ip) { String sql = "UPDATE " + tableName + " SET " + col.IP + "=? WHERE " + col.NAME + "=?;"; try (PreparedStatement pst = con.prepareStatement(sql)) { pst.setString(1, ip); From 410d07a64c81f804d72bb2f4ac24a8b0cf536ac4 Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Sat, 21 May 2016 15:08:40 +0200 Subject: [PATCH 082/200] Remove commented code --- .../authme/listener/AuthMePlayerListener.java | 48 ------------------- 1 file changed, 48 deletions(-) diff --git a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java index b2f2d0c3..f270862f 100644 --- a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java +++ b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java @@ -36,7 +36,6 @@ import org.bukkit.event.block.SignChangeEvent; import org.bukkit.event.entity.EntityDamageByEntityEvent; import org.bukkit.event.inventory.InventoryClickEvent; import org.bukkit.event.inventory.InventoryOpenEvent; -import org.bukkit.event.player.AsyncPlayerChatEvent; import org.bukkit.event.player.AsyncPlayerPreLoginEvent; import org.bukkit.event.player.PlayerBedEnterEvent; import org.bukkit.event.player.PlayerChatEvent; @@ -93,26 +92,6 @@ public class AuthMePlayerListener implements Listener { @Inject private ValidationService validationService; - /* - private void handleChat(AsyncPlayerChatEvent event) { - if (settings.getProperty(RestrictionSettings.ALLOW_CHAT)) { - return; - } - - final Player player = event.getPlayer(); - if (shouldCancelEvent(player)) { - event.setCancelled(true); - sendLoginOrRegisterMessage(player); - } else if (settings.getProperty(RestrictionSettings.HIDE_CHAT)) { - for (Player p : bukkitService.getOnlinePlayers()) { - if (!PlayerCache.getInstance().isAuthenticated(p.getName())) { - event.getRecipients().remove(p); - } - } - } - } - */ - private void sendLoginOrRegisterMessage(final Player player) { bukkitService.runTaskAsynchronously(new Runnable() { @Override @@ -176,33 +155,6 @@ public class AuthMePlayerListener implements Listener { } } - /* - @EventHandler(ignoreCancelled = true, priority = EventPriority.NORMAL) - public void onPlayerNormalChat(AsyncPlayerChatEvent event) { - handleChat(event); - } - - @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGH) - public void onPlayerHighChat(AsyncPlayerChatEvent event) { - handleChat(event); - } - - @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST) - public void onPlayerHighestChat(AsyncPlayerChatEvent event) { - handleChat(event); - } - - @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) - public void onPlayerEarlyChat(AsyncPlayerChatEvent event) { - handleChat(event); - } - - @EventHandler(ignoreCancelled = true, priority = EventPriority.LOW) - public void onPlayerLowChat(AsyncPlayerChatEvent event) { - handleChat(event); - } - */ - @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST) public void onPlayerMove(PlayerMoveEvent event) { if (settings.getProperty(ALLOW_UNAUTHED_MOVEMENT) && settings.getProperty(ALLOWED_MOVEMENT_RADIUS) <= 0) { From fb6303bf4a37e0b2c8edc00a642e6485054ffec2 Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Sat, 21 May 2016 15:37:54 +0200 Subject: [PATCH 083/200] Cleanup --- src/main/java/fr/xephi/authme/AuthMe.java | 8 ----- .../authme/listener/AuthMePlayerListener.java | 6 ++-- .../listener/AuthMePlayerListener19.java | 36 ------------------- 3 files changed, 2 insertions(+), 48 deletions(-) delete mode 100644 src/main/java/fr/xephi/authme/listener/AuthMePlayerListener19.java diff --git a/src/main/java/fr/xephi/authme/AuthMe.java b/src/main/java/fr/xephi/authme/AuthMe.java index baeb1bfd..4553b207 100644 --- a/src/main/java/fr/xephi/authme/AuthMe.java +++ b/src/main/java/fr/xephi/authme/AuthMe.java @@ -24,7 +24,6 @@ import fr.xephi.authme.listener.AuthMeInventoryPacketAdapter; import fr.xephi.authme.listener.AuthMePlayerListener; import fr.xephi.authme.listener.AuthMePlayerListener16; import fr.xephi.authme.listener.AuthMePlayerListener18; -import fr.xephi.authme.listener.AuthMePlayerListener19; import fr.xephi.authme.listener.AuthMeServerListener; import fr.xephi.authme.listener.AuthMeTabCompletePacketAdapter; import fr.xephi.authme.listener.AuthMeTablistPacketAdapter; @@ -374,13 +373,6 @@ public class AuthMe extends JavaPlugin { pluginManager.registerEvents(initializer.get(AuthMePlayerListener18.class), this); } catch (ClassNotFoundException ignore) { } - - // Try to register 1.9 player listeners - try { - Class.forName("org.spigotmc.event.player.PlayerSpawnLocationEvent"); - pluginManager.registerEvents(initializer.get(AuthMePlayerListener19.class), this); - } catch (ClassNotFoundException ignore) { - } } private void reloadSupportHook() { diff --git a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java index f270862f..82979a02 100644 --- a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java +++ b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java @@ -129,10 +129,9 @@ public class AuthMePlayerListener implements Listener { sendLoginOrRegisterMessage(event.getPlayer()); } - // I think it should be sync! -sgdc3 @SuppressWarnings("deprecation") @EventHandler(ignoreCancelled = true, priority = EventPriority.LOW) - public void onPlayerNormalChat(PlayerChatEvent event) { + public void onPlayerChat(PlayerChatEvent event) { if (settings.getProperty(RestrictionSettings.ALLOW_CHAT)) { return; } @@ -141,7 +140,7 @@ public class AuthMePlayerListener implements Listener { if (shouldCancelEvent(player)) { event.setCancelled(true); // TODO: a spambot calls this too often, too many threads checking if auth is available. - // Possible solution: add a cooldown. + // Possible solution: add a cooldown. -sgdc3 // sendLoginOrRegisterMessage(player); } else if (settings.getProperty(RestrictionSettings.HIDE_CHAT)) { Set recipients = event.getRecipients(); @@ -178,7 +177,6 @@ public class AuthMePlayerListener implements Listener { if (!settings.getProperty(RestrictionSettings.ALLOW_UNAUTHED_MOVEMENT)) { event.setTo(event.getFrom()); - // sgdc3 TODO: remove this, maybe we should set the effect every x ticks, idk! if (settings.getProperty(RestrictionSettings.REMOVE_SPEED)) { player.setFlySpeed(0.0f); player.setWalkSpeed(0.0f); diff --git a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener19.java b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener19.java deleted file mode 100644 index 6cfe854c..00000000 --- a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener19.java +++ /dev/null @@ -1,36 +0,0 @@ -package fr.xephi.authme.listener; - -/* -import fr.xephi.authme.settings.NewSetting; -import fr.xephi.authme.settings.SpawnLoader; -import javax.inject.Inject; -*/ -import org.bukkit.event.Listener; - -/** - * Listener of player events for events introduced in Minecraft 1.9. - */ -public class AuthMePlayerListener19 implements Listener { - - /* - @Inject - private SpawnLoader spawnLoader; - - @Inject - private NewSetting settings; - */ - - /* WTF was that? We need to check all the settings before moving the player to the spawn! - * - * TODO: fixme please! - * - @EventHandler(priority = EventPriority.LOWEST) - public void onPlayerSpawn(PlayerSpawnLocationEvent event) { - if(settings.getProperty(RestrictionSettings.NO_TELEPORT)) { - return; - } - event.setSpawnLocation(spawnLoader.getSpawnLocation(event.getPlayer())); - } - */ - -} From e0ef0e40c77bc6eac7c4a606edfc01ba005563c7 Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Sat, 21 May 2016 18:01:21 +0200 Subject: [PATCH 084/200] Add denied chat message #719 --- src/main/java/fr/xephi/authme/output/MessageKey.java | 2 ++ src/main/resources/messages/messages_en.yml | 1 + 2 files changed, 3 insertions(+) diff --git a/src/main/java/fr/xephi/authme/output/MessageKey.java b/src/main/java/fr/xephi/authme/output/MessageKey.java index 3fd7c747..a4ab3736 100644 --- a/src/main/java/fr/xephi/authme/output/MessageKey.java +++ b/src/main/java/fr/xephi/authme/output/MessageKey.java @@ -5,6 +5,8 @@ package fr.xephi.authme.output; */ public enum MessageKey { + DENIED_CHAT_MESSAGE("denied_chat"), + KICK_ANTIBOT("kick_antibot"), UNKNOWN_USER("unknown_user"), diff --git a/src/main/resources/messages/messages_en.yml b/src/main/resources/messages/messages_en.yml index 5b474e2d..203ce720 100644 --- a/src/main/resources/messages/messages_en.yml +++ b/src/main/resources/messages/messages_en.yml @@ -1,3 +1,4 @@ +denied_chat: '&cIn order to be able to chat you must be authenticated!' kick_antibot: 'AntiBot protection mode is enabled! You have to wait some minutes before joining the server.' unknown_user: '&cCan''t find the requested user in the database!' unsafe_spawn: '&cYour quit location was unsafe, you have been teleported to the world''s spawnpoint.' From dd5fb49065c095cf421f6cde23631e001a62c33c Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Sat, 21 May 2016 18:08:26 +0200 Subject: [PATCH 085/200] Whoops forgot to commit this #719 --- .../fr/xephi/authme/listener/AuthMePlayerListener.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java index 82979a02..03aaca4f 100644 --- a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java +++ b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java @@ -139,9 +139,12 @@ public class AuthMePlayerListener implements Listener { final Player player = event.getPlayer(); if (shouldCancelEvent(player)) { event.setCancelled(true); - // TODO: a spambot calls this too often, too many threads checking if auth is available. - // Possible solution: add a cooldown. -sgdc3 - // sendLoginOrRegisterMessage(player); + bukkitService.runTaskAsynchronously(new Runnable() { + @Override + public void run() { + m.send(player, MessageKey.DENIED_CHAT_MESSAGE); + } + }); } else if (settings.getProperty(RestrictionSettings.HIDE_CHAT)) { Set recipients = event.getRecipients(); Iterator iter = recipients.iterator(); From 11350ec43af04e958a4a33ae47419be6ad47ae53 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Sun, 22 May 2016 10:52:34 +0200 Subject: [PATCH 086/200] Iterate over all chat recipients --- .../java/fr/xephi/authme/listener/AuthMePlayerListener.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java index 03aaca4f..673d3b83 100644 --- a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java +++ b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java @@ -129,7 +129,6 @@ public class AuthMePlayerListener implements Listener { sendLoginOrRegisterMessage(event.getPlayer()); } - @SuppressWarnings("deprecation") @EventHandler(ignoreCancelled = true, priority = EventPriority.LOW) public void onPlayerChat(PlayerChatEvent event) { if (settings.getProperty(RestrictionSettings.ALLOW_CHAT)) { @@ -148,9 +147,9 @@ public class AuthMePlayerListener implements Listener { } else if (settings.getProperty(RestrictionSettings.HIDE_CHAT)) { Set recipients = event.getRecipients(); Iterator iter = recipients.iterator(); - if(iter.hasNext()) { + while (iter.hasNext()) { Player p = iter.next(); - if(PlayerCache.getInstance().isAuthenticated(p.getName())) { + if (PlayerCache.getInstance().isAuthenticated(p.getName())) { iter.remove(); } } From 37b6a2f96f3f56e872473e31bc5142abda63f915 Mon Sep 17 00:00:00 2001 From: Xephi Date: Mon, 23 May 2016 10:03:10 +0200 Subject: [PATCH 087/200] Enable antibot automatically and check for bot into AsyncPreLogin #719 --- src/main/java/fr/xephi/authme/AntiBot.java | 2 ++ .../authme/listener/AuthMePlayerListener.java | 36 +++++++++++++++++-- .../properties/ProtectionSettings.java | 4 +-- src/main/resources/config.yml | 8 ++--- 4 files changed, 42 insertions(+), 8 deletions(-) diff --git a/src/main/java/fr/xephi/authme/AntiBot.java b/src/main/java/fr/xephi/authme/AntiBot.java index fcdb2206..22c9feab 100644 --- a/src/main/java/fr/xephi/authme/AntiBot.java +++ b/src/main/java/fr/xephi/authme/AntiBot.java @@ -1,5 +1,6 @@ package fr.xephi.authme; +import fr.xephi.authme.listener.AuthMePlayerListener; import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.output.Messages; import fr.xephi.authme.permission.PermissionsManager; @@ -77,6 +78,7 @@ public class AntiBot { if (antiBotStatus == AntiBotStatus.ACTIVE) { antiBotStatus = AntiBotStatus.LISTENING; antibotPlayers.clear(); + AuthMePlayerListener.antibotKicked.clear(); for (String s : messages.retrieve(MessageKey.ANTIBOT_AUTO_DISABLED_MESSAGE)) { bukkitService.broadcastMessage(s.replace("%m", Integer.toString(duration))); } diff --git a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java index 892de53d..333b7a77 100644 --- a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java +++ b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java @@ -28,6 +28,7 @@ import fr.xephi.authme.util.ValidationService; import org.bukkit.Bukkit; import org.bukkit.GameMode; import org.bukkit.Location; +import org.bukkit.OfflinePlayer; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; @@ -55,7 +56,12 @@ import org.bukkit.event.player.PlayerRespawnEvent; import org.bukkit.event.player.PlayerShearEntityEvent; import javax.inject.Inject; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.regex.Pattern; import static fr.xephi.authme.listener.ListenerService.shouldCancelEvent; @@ -70,6 +76,7 @@ public class AuthMePlayerListener implements Listener { public static final ConcurrentHashMap joinMessage = new ConcurrentHashMap<>(); public static final ConcurrentHashMap causeByAuthMe = new ConcurrentHashMap<>(); + public static final CopyOnWriteArrayList antibotKicked = new CopyOnWriteArrayList(); @Inject private AuthMe plugin; @Inject @@ -253,7 +260,7 @@ public class AuthMePlayerListener implements Listener { player.setGameMode(GameMode.SURVIVAL); } - // Shedule login task so works after the prelogin + // Schedule login task so works after the prelogin // (Fix found by Koolaid5000) bukkitService.runTask(new Runnable() { @Override @@ -266,6 +273,23 @@ public class AuthMePlayerListener implements Listener { @EventHandler(priority = EventPriority.HIGHEST) public void onPreLogin(AsyncPlayerPreLoginEvent event) { PlayerAuth auth = dataSource.getAuth(event.getName()); + if (auth == null && antiBot.getAntiBotStatus() == AntiBotStatus.ACTIVE) { + event.setKickMessage(m.retrieveSingle(MessageKey.KICK_ANTIBOT)); + event.setLoginResult(AsyncPlayerPreLoginEvent.Result.KICK_OTHER); + antibotKicked.addIfAbsent(event.getName()); + return; + } + if (auth == null && settings.getProperty(RestrictionSettings.KICK_NON_REGISTERED)) { + event.setKickMessage(m.retrieveSingle(MessageKey.MUST_REGISTER_MESSAGE)); + event.setLoginResult(AsyncPlayerPreLoginEvent.Result.KICK_OTHER); + return; + } + final String name = event.getName().toLowerCase(); + if (name.length() > settings.getProperty(RestrictionSettings.MAX_NICKNAME_LENGTH) || name.length() < settings.getProperty(RestrictionSettings.MIN_NICKNAME_LENGTH)) { + event.setKickMessage(m.retrieveSingle(MessageKey.INVALID_NAME_LENGTH)); + event.setLoginResult(AsyncPlayerPreLoginEvent.Result.KICK_OTHER); + return; + } if (settings.getProperty(RegistrationSettings.PREVENT_OTHER_CASE) && auth != null && auth.getRealName() != null) { String realName = auth.getRealName(); if (!realName.isEmpty() && !"Player".equals(realName) && !realName.equals(event.getName())) { @@ -287,7 +311,6 @@ public class AuthMePlayerListener implements Listener { } } - final String name = event.getName().toLowerCase(); final Player player = bukkitService.getPlayerExact(name); // Check if forceSingleSession is set to true, so kick player that has // joined with same nick of online player @@ -345,6 +368,7 @@ public class AuthMePlayerListener implements Listener { if (antiBot.getAntiBotStatus() == AntiBotStatus.ACTIVE && !isAuthAvailable) { event.setKickMessage(m.retrieveSingle(MessageKey.KICK_ANTIBOT)); event.setResult(PlayerLoginEvent.Result.KICK_OTHER); + antibotKicked.addIfAbsent(player.getName()); return; } @@ -389,6 +413,10 @@ public class AuthMePlayerListener implements Listener { event.setQuitMessage(null); } + if (antibotKicked.contains(player.getName())) { + return; + } + management.performQuit(player, false); } @@ -406,6 +434,10 @@ public class AuthMePlayerListener implements Listener { return; } + if (antibotKicked.contains(player.getName())) { + return; + } + plugin.getManagement().performQuit(player, true); } diff --git a/src/main/java/fr/xephi/authme/settings/properties/ProtectionSettings.java b/src/main/java/fr/xephi/authme/settings/properties/ProtectionSettings.java index c32245c4..312f3f51 100644 --- a/src/main/java/fr/xephi/authme/settings/properties/ProtectionSettings.java +++ b/src/main/java/fr/xephi/authme/settings/properties/ProtectionSettings.java @@ -28,11 +28,11 @@ public class ProtectionSettings implements SettingsClass { @Comment("Do we need to enable automatic antibot system?") public static final Property ENABLE_ANTIBOT = - newProperty("Protection.enableAntiBot", false); + newProperty("Protection.enableAntiBot", true); @Comment("Max number of players allowed to login in 5 secs before the AntiBot system is enabled automatically") public static final Property ANTIBOT_SENSIBILITY = - newProperty("Protection.antiBotSensibility", 5); + newProperty("Protection.antiBotSensibility", 10); @Comment("Duration in minutes of the antibot automatic system") public static final Property ANTIBOT_DURATION = diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 8ca9c27f..3cb5f23f 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -88,7 +88,7 @@ settings: # due to "Logged in from another Location" # This setting will prevent potetial security exploits. ForceSingleSession: true - ForceSpawnLocOnJoin: + ForceSpawnLocOnJoin: # If enabled, every player will be teleported to the world spawnpoint # after successful authentication. # The quit location of the player will be overwritten. @@ -97,7 +97,7 @@ settings: enabled: false # WorldNames where we need to force the spawn location # Case-sensitive! - worlds: + worlds: - 'world' - 'world_nether' - 'world_the_end' @@ -418,8 +418,8 @@ Protection: countriesBlacklist: - 'A1' # Do we need to enable automatic antibot system? - enableAntiBot: false + enableAntiBot: true # Max number of player allowed to login in 5 secs before enable AntiBot system automatically - antiBotSensibility: 5 + antiBotSensibility: 10 # Duration in minutes of the antibot automatic system antiBotDuration: 10 From c0e8650c6ff840f678e15b0a863c6fcbdb4329b1 Mon Sep 17 00:00:00 2001 From: Xephi Date: Mon, 23 May 2016 10:10:01 +0200 Subject: [PATCH 088/200] Move to antibot class --- src/main/java/fr/xephi/authme/AntiBot.java | 6 ++++-- .../fr/xephi/authme/listener/AuthMePlayerListener.java | 10 +++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/main/java/fr/xephi/authme/AntiBot.java b/src/main/java/fr/xephi/authme/AntiBot.java index 22c9feab..5670e90e 100644 --- a/src/main/java/fr/xephi/authme/AntiBot.java +++ b/src/main/java/fr/xephi/authme/AntiBot.java @@ -13,6 +13,7 @@ import org.bukkit.entity.Player; import javax.inject.Inject; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; import static fr.xephi.authme.util.BukkitService.TICKS_PER_MINUTE; import static fr.xephi.authme.util.BukkitService.TICKS_PER_SECOND; @@ -26,7 +27,8 @@ public class AntiBot { private final Messages messages; private final PermissionsManager permissionsManager; private final BukkitService bukkitService; - private final List antibotPlayers = new ArrayList<>(); + public final CopyOnWriteArrayList antibotKicked = new CopyOnWriteArrayList(); + private final CopyOnWriteArrayList antibotPlayers = new CopyOnWriteArrayList(); private AntiBotStatus antiBotStatus = AntiBotStatus.DISABLED; @Inject @@ -78,7 +80,7 @@ public class AntiBot { if (antiBotStatus == AntiBotStatus.ACTIVE) { antiBotStatus = AntiBotStatus.LISTENING; antibotPlayers.clear(); - AuthMePlayerListener.antibotKicked.clear(); + antibotKicked.clear(); for (String s : messages.retrieve(MessageKey.ANTIBOT_AUTO_DISABLED_MESSAGE)) { bukkitService.broadcastMessage(s.replace("%m", Integer.toString(duration))); } diff --git a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java index 333b7a77..ed3d8912 100644 --- a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java +++ b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java @@ -76,7 +76,7 @@ public class AuthMePlayerListener implements Listener { public static final ConcurrentHashMap joinMessage = new ConcurrentHashMap<>(); public static final ConcurrentHashMap causeByAuthMe = new ConcurrentHashMap<>(); - public static final CopyOnWriteArrayList antibotKicked = new CopyOnWriteArrayList(); + @Inject private AuthMe plugin; @Inject @@ -276,7 +276,7 @@ public class AuthMePlayerListener implements Listener { if (auth == null && antiBot.getAntiBotStatus() == AntiBotStatus.ACTIVE) { event.setKickMessage(m.retrieveSingle(MessageKey.KICK_ANTIBOT)); event.setLoginResult(AsyncPlayerPreLoginEvent.Result.KICK_OTHER); - antibotKicked.addIfAbsent(event.getName()); + antiBot.antibotKicked.addIfAbsent(event.getName()); return; } if (auth == null && settings.getProperty(RestrictionSettings.KICK_NON_REGISTERED)) { @@ -368,7 +368,7 @@ public class AuthMePlayerListener implements Listener { if (antiBot.getAntiBotStatus() == AntiBotStatus.ACTIVE && !isAuthAvailable) { event.setKickMessage(m.retrieveSingle(MessageKey.KICK_ANTIBOT)); event.setResult(PlayerLoginEvent.Result.KICK_OTHER); - antibotKicked.addIfAbsent(player.getName()); + antiBot.antibotKicked.addIfAbsent(player.getName()); return; } @@ -413,7 +413,7 @@ public class AuthMePlayerListener implements Listener { event.setQuitMessage(null); } - if (antibotKicked.contains(player.getName())) { + if (antiBot.antibotKicked.contains(player.getName())) { return; } @@ -434,7 +434,7 @@ public class AuthMePlayerListener implements Listener { return; } - if (antibotKicked.contains(player.getName())) { + if (antiBot.antibotKicked.contains(player.getName())) { return; } From 22f54c1f3ebd4354b2a524c474f5f0c0d6c40856 Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Mon, 23 May 2016 21:05:18 +0200 Subject: [PATCH 089/200] Fix Chat Hider #719 --- .../java/fr/xephi/authme/listener/AuthMePlayerListener.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java index 673d3b83..9ed45ecc 100644 --- a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java +++ b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java @@ -149,7 +149,7 @@ public class AuthMePlayerListener implements Listener { Iterator iter = recipients.iterator(); while (iter.hasNext()) { Player p = iter.next(); - if (PlayerCache.getInstance().isAuthenticated(p.getName())) { + if (shouldCancelEvent(p)) { iter.remove(); } } From 97e96ab16b81cd16d12b9449b622ebe6e092f266 Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Mon, 23 May 2016 21:16:11 +0200 Subject: [PATCH 090/200] Remove unused imports --- src/main/java/fr/xephi/authme/AntiBot.java | 3 --- .../java/fr/xephi/authme/listener/AuthMePlayerListener.java | 5 ----- 2 files changed, 8 deletions(-) diff --git a/src/main/java/fr/xephi/authme/AntiBot.java b/src/main/java/fr/xephi/authme/AntiBot.java index 5670e90e..071842bc 100644 --- a/src/main/java/fr/xephi/authme/AntiBot.java +++ b/src/main/java/fr/xephi/authme/AntiBot.java @@ -1,6 +1,5 @@ package fr.xephi.authme; -import fr.xephi.authme.listener.AuthMePlayerListener; import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.output.Messages; import fr.xephi.authme.permission.PermissionsManager; @@ -11,8 +10,6 @@ import fr.xephi.authme.util.BukkitService; import org.bukkit.entity.Player; import javax.inject.Inject; -import java.util.ArrayList; -import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import static fr.xephi.authme.util.BukkitService.TICKS_PER_MINUTE; diff --git a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java index 7e9fe611..a65177b4 100644 --- a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java +++ b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java @@ -28,7 +28,6 @@ import fr.xephi.authme.util.ValidationService; import org.bukkit.Bukkit; import org.bukkit.GameMode; import org.bukkit.Location; -import org.bukkit.OfflinePlayer; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; @@ -59,11 +58,7 @@ import javax.inject.Inject; import java.util.Iterator; import java.util.Set; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CopyOnWriteArrayList; import java.util.regex.Pattern; import static fr.xephi.authme.listener.ListenerService.shouldCancelEvent; From 7865d7dc2cab760ca3b86c57d8a1ef9b6fe7318c Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Mon, 23 May 2016 21:18:31 +0200 Subject: [PATCH 091/200] Remove synchronized methods from the Cache (please check) @Xephi @ljacqu @games647 --- .../authme/datasource/CacheDataSource.java | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/main/java/fr/xephi/authme/datasource/CacheDataSource.java b/src/main/java/fr/xephi/authme/datasource/CacheDataSource.java index 9f2843d1..12d45d20 100644 --- a/src/main/java/fr/xephi/authme/datasource/CacheDataSource.java +++ b/src/main/java/fr/xephi/authme/datasource/CacheDataSource.java @@ -72,7 +72,7 @@ public class CacheDataSource implements DataSource { } @Override - public synchronized boolean isAuthAvailable(String user) { + public boolean isAuthAvailable(String user) { return getAuth(user) != null; } @@ -87,13 +87,13 @@ public class CacheDataSource implements DataSource { } @Override - public synchronized PlayerAuth getAuth(String user) { + public PlayerAuth getAuth(String user) { user = user.toLowerCase(); return cachedAuths.getUnchecked(user).orNull(); } @Override - public synchronized boolean saveAuth(PlayerAuth auth) { + public boolean saveAuth(PlayerAuth auth) { boolean result = source.saveAuth(auth); if (result) { cachedAuths.refresh(auth.getNickname()); @@ -102,7 +102,7 @@ public class CacheDataSource implements DataSource { } @Override - public synchronized boolean updatePassword(PlayerAuth auth) { + public boolean updatePassword(PlayerAuth auth) { boolean result = source.updatePassword(auth); if (result) { cachedAuths.refresh(auth.getNickname()); @@ -111,7 +111,7 @@ public class CacheDataSource implements DataSource { } @Override - public synchronized boolean updatePassword(String user, HashedPassword password) { + public boolean updatePassword(String user, HashedPassword password) { user = user.toLowerCase(); boolean result = source.updatePassword(user, password); if (result) { @@ -121,7 +121,7 @@ public class CacheDataSource implements DataSource { } @Override - public synchronized boolean updateSession(PlayerAuth auth) { + public boolean updateSession(PlayerAuth auth) { boolean result = source.updateSession(auth); if (result) { cachedAuths.refresh(auth.getNickname()); @@ -130,7 +130,7 @@ public class CacheDataSource implements DataSource { } @Override - public synchronized boolean updateQuitLoc(final PlayerAuth auth) { + public boolean updateQuitLoc(final PlayerAuth auth) { boolean result = source.updateQuitLoc(auth); if (result) { cachedAuths.refresh(auth.getNickname()); @@ -149,7 +149,7 @@ public class CacheDataSource implements DataSource { } @Override - public synchronized boolean removeAuth(String name) { + public boolean removeAuth(String name) { name = name.toLowerCase(); boolean result = source.removeAuth(name); if (result) { @@ -159,7 +159,7 @@ public class CacheDataSource implements DataSource { } @Override - public synchronized void close() { + public void close() { source.close(); cachedAuths.invalidateAll(); executorService.shutdown(); @@ -171,7 +171,7 @@ public class CacheDataSource implements DataSource { } @Override - public synchronized boolean updateEmail(final PlayerAuth auth) { + public boolean updateEmail(final PlayerAuth auth) { boolean result = source.updateEmail(auth); if (result) { cachedAuths.refresh(auth.getNickname()); @@ -180,17 +180,17 @@ public class CacheDataSource implements DataSource { } @Override - public synchronized List getAllAuthsByIp(final String ip) { + public List getAllAuthsByIp(final String ip) { return source.getAllAuthsByIp(ip); } @Override - public synchronized int countAuthsByEmail(final String email) { + public int countAuthsByEmail(final String email) { return source.countAuthsByEmail(email); } @Override - public synchronized void purgeBanned(final Set banned) { + public void purgeBanned(final Set banned) { source.purgeBanned(banned); cachedAuths.invalidateAll(banned); } @@ -201,17 +201,17 @@ public class CacheDataSource implements DataSource { } @Override - public synchronized boolean isLogged(String user) { + public boolean isLogged(String user) { return PlayerCache.getInstance().isAuthenticated(user); } @Override - public synchronized void setLogged(final String user) { + public void setLogged(final String user) { source.setLogged(user.toLowerCase()); } @Override - public synchronized void setUnlogged(final String user) { + public void setUnlogged(final String user) { source.setUnlogged(user.toLowerCase()); } @@ -227,7 +227,7 @@ public class CacheDataSource implements DataSource { } @Override - public synchronized boolean updateRealName(String user, String realName) { + public boolean updateRealName(String user, String realName) { boolean result = source.updateRealName(user, realName); if (result) { cachedAuths.refresh(user); @@ -236,7 +236,7 @@ public class CacheDataSource implements DataSource { } @Override - public synchronized boolean updateIp(String user, String ip) { + public boolean updateIp(String user, String ip) { boolean result = source.updateIp(user, ip); if (result) { cachedAuths.refresh(user); From da6431b4fc8c679d3fda4a4875f422bb612ca0fc Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Mon, 23 May 2016 23:16:40 +0200 Subject: [PATCH 092/200] Suppress injection test warnings --- .../java/fr/xephi/authme/initialization/FieldInjection.java | 1 + .../fr/xephi/authme/process/logout/AsynchronousLogout.java | 4 ---- .../fr/xephi/authme/initialization/FieldInjectionTest.java | 3 +++ .../authme/initialization/samples/InvalidPostConstruct.java | 2 ++ 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main/java/fr/xephi/authme/initialization/FieldInjection.java b/src/main/java/fr/xephi/authme/initialization/FieldInjection.java index e9717b33..c74e7c27 100644 --- a/src/main/java/fr/xephi/authme/initialization/FieldInjection.java +++ b/src/main/java/fr/xephi/authme/initialization/FieldInjection.java @@ -114,6 +114,7 @@ public class FieldInjection implements Injection { return null; } + @SuppressWarnings("unchecked") private static Constructor getDefaultConstructor(Class clazz) { try { Constructor defaultConstructor = clazz.getDeclaredConstructor(); diff --git a/src/main/java/fr/xephi/authme/process/logout/AsynchronousLogout.java b/src/main/java/fr/xephi/authme/process/logout/AsynchronousLogout.java index 840ebfc9..6cb7c77f 100644 --- a/src/main/java/fr/xephi/authme/process/logout/AsynchronousLogout.java +++ b/src/main/java/fr/xephi/authme/process/logout/AsynchronousLogout.java @@ -1,6 +1,5 @@ package fr.xephi.authme.process.logout; -import fr.xephi.authme.AuthMe; import fr.xephi.authme.cache.auth.PlayerAuth; import fr.xephi.authme.cache.auth.PlayerCache; import fr.xephi.authme.cache.limbo.LimboCache; @@ -17,9 +16,6 @@ import javax.inject.Inject; public class AsynchronousLogout implements AsynchronousProcess { - @Inject - private AuthMe plugin; - @Inject private DataSource database; diff --git a/src/test/java/fr/xephi/authme/initialization/FieldInjectionTest.java b/src/test/java/fr/xephi/authme/initialization/FieldInjectionTest.java index 39675343..89e460e5 100644 --- a/src/test/java/fr/xephi/authme/initialization/FieldInjectionTest.java +++ b/src/test/java/fr/xephi/authme/initialization/FieldInjectionTest.java @@ -22,6 +22,7 @@ import static org.junit.Assert.assertThat; /** * Test for {@link FieldInjection}. */ + public class FieldInjectionTest { @SuppressWarnings("unchecked") @@ -114,6 +115,8 @@ public class FieldInjectionTest { assertThat(injection, nullValue()); } + + @SuppressWarnings("unused") private static class ThrowingConstructor { @Inject private ProvidedClass providedClass; diff --git a/src/test/java/fr/xephi/authme/initialization/samples/InvalidPostConstruct.java b/src/test/java/fr/xephi/authme/initialization/samples/InvalidPostConstruct.java index 501fad6a..1e832549 100644 --- a/src/test/java/fr/xephi/authme/initialization/samples/InvalidPostConstruct.java +++ b/src/test/java/fr/xephi/authme/initialization/samples/InvalidPostConstruct.java @@ -6,6 +6,8 @@ import javax.inject.Inject; /** * Class with invalid @PostConstruct method. */ + +@SuppressWarnings("unused") public abstract class InvalidPostConstruct { public static final class WithParams { From 740b44ca361577e9062cd91fb659252c8149be3d Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Mon, 23 May 2016 23:49:56 +0200 Subject: [PATCH 093/200] cleanup --- .../authme/listener/AuthMePlayerListener.java | 8 +---- .../fr/xephi/authme/output/MessageKey.java | 4 ++- .../xephi/authme/process/ProcessService.java | 17 ++++++---- .../authme/process/join/AsynchronousJoin.java | 34 +++++++++++++------ src/main/resources/messages/messages_en.yml | 1 + 5 files changed, 40 insertions(+), 24 deletions(-) diff --git a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java index a65177b4..12c7cba0 100644 --- a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java +++ b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java @@ -26,7 +26,6 @@ import fr.xephi.authme.util.BukkitService; import fr.xephi.authme.util.Utils; import fr.xephi.authme.util.ValidationService; import org.bukkit.Bukkit; -import org.bukkit.GameMode; import org.bukkit.Location; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; @@ -142,7 +141,7 @@ public class AuthMePlayerListener implements Listener { bukkitService.runTaskAsynchronously(new Runnable() { @Override public void run() { - m.send(player, MessageKey.DENIED_CHAT_MESSAGE); + m.send(player, MessageKey.DENIED_CHAT); } }); } else if (settings.getProperty(RestrictionSettings.HIDE_CHAT)) { @@ -235,11 +234,6 @@ public class AuthMePlayerListener implements Listener { return; } - if (settings.getProperty(RestrictionSettings.FORCE_SURVIVAL_MODE) - && !player.hasPermission(PlayerStatePermission.BYPASS_FORCE_SURVIVAL.getNode())) { - player.setGameMode(GameMode.SURVIVAL); - } - // Schedule login task so works after the prelogin // (Fix found by Koolaid5000) bukkitService.runTask(new Runnable() { diff --git a/src/main/java/fr/xephi/authme/output/MessageKey.java b/src/main/java/fr/xephi/authme/output/MessageKey.java index a4ab3736..0f052c27 100644 --- a/src/main/java/fr/xephi/authme/output/MessageKey.java +++ b/src/main/java/fr/xephi/authme/output/MessageKey.java @@ -5,7 +5,9 @@ package fr.xephi.authme.output; */ public enum MessageKey { - DENIED_CHAT_MESSAGE("denied_chat"), + SAME_IP_ONLINE("same_ip_online"), + + DENIED_CHAT("denied_chat"), KICK_ANTIBOT("kick_antibot"), diff --git a/src/main/java/fr/xephi/authme/process/ProcessService.java b/src/main/java/fr/xephi/authme/process/ProcessService.java index 3dede747..3819ca64 100644 --- a/src/main/java/fr/xephi/authme/process/ProcessService.java +++ b/src/main/java/fr/xephi/authme/process/ProcessService.java @@ -3,6 +3,7 @@ package fr.xephi.authme.process; import fr.xephi.authme.AuthMe; import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.output.Messages; +import fr.xephi.authme.permission.PermissionNode; import fr.xephi.authme.settings.NewSetting; import fr.xephi.authme.settings.domain.Property; import fr.xephi.authme.util.BukkitService; @@ -25,7 +26,7 @@ public class ProcessService { @Inject private Messages messages; @Inject - private AuthMe authMe; + private AuthMe plugin; @Inject private ValidationService validationService; @Inject @@ -99,7 +100,7 @@ public class ProcessService { * @return the assigned task id */ public BukkitTask runTask(Runnable task) { - return authMe.getServer().getScheduler().runTask(authMe, task); + return plugin.getServer().getScheduler().runTask(plugin, task); } /** @@ -110,7 +111,7 @@ public class ProcessService { * @return the assigned task id */ public BukkitTask runTaskLater(Runnable task, long delay) { - return authMe.getServer().getScheduler().runTaskLater(authMe, task, delay); + return plugin.getServer().getScheduler().runTaskLater(plugin, task, delay); } /** @@ -120,7 +121,7 @@ public class ProcessService { * @return the task id */ public int scheduleSyncDelayedTask(Runnable task) { - return authMe.getServer().getScheduler().scheduleSyncDelayedTask(authMe, task); + return plugin.getServer().getScheduler().scheduleSyncDelayedTask(plugin, task); } /** @@ -129,7 +130,7 @@ public class ProcessService { * @param event the event to emit */ public void callEvent(Event event) { - authMe.getServer().getPluginManager().callEvent(event); + plugin.getServer().getPluginManager().callEvent(event); } /** @@ -138,7 +139,7 @@ public class ProcessService { * @return AuthMe instance */ public AuthMe getAuthMe() { - return authMe; + return plugin; } /** @@ -168,4 +169,8 @@ public class ProcessService { return bukkitService; } + public boolean hasPermission(Player player, PermissionNode node) { + return plugin.getPermissionsManager().hasPermission(player, node); + } + } diff --git a/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java b/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java index 205a0efb..1cbbed26 100644 --- a/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java +++ b/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java @@ -28,6 +28,7 @@ import fr.xephi.authme.task.TimeoutTask; import fr.xephi.authme.util.Utils; import fr.xephi.authme.util.Utils.GroupType; import org.apache.commons.lang.reflect.MethodUtils; +import org.bukkit.GameMode; import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.entity.LivingEntity; @@ -73,13 +74,24 @@ public class AsynchronousJoin implements AsynchronousProcess { if (Utils.isUnrestricted(player)) { return; } + final String name = player.getName().toLowerCase(); + final String ip = Utils.getPlayerIp(player); + + // Prevent player collisions in 1.9 + if (DISABLE_COLLISIONS) { + ((LivingEntity) player).setCollidable(false); + } + + if (service.getProperty(RestrictionSettings.FORCE_SURVIVAL_MODE) + && !service.hasPermission(player, PlayerStatePermission.BYPASS_FORCE_SURVIVAL)) { + player.setGameMode(GameMode.SURVIVAL); + } if (service.getProperty(HooksSettings.DISABLE_SOCIAL_SPY)) { pluginHooks.setEssentialsSocialSpyStatus(player, false); } - final String ip = Utils.getPlayerIp(player); if (isNameRestricted(name, ip, player.getAddress().getHostName())) { service.scheduleSyncDelayedTask(new Runnable() { @Override @@ -93,25 +105,27 @@ public class AsynchronousJoin implements AsynchronousProcess { }); return; } + if (service.getProperty(RestrictionSettings.MAX_JOIN_PER_IP) > 0 - && !plugin.getPermissionsManager().hasPermission(player, PlayerStatePermission.ALLOW_MULTIPLE_ACCOUNTS) - && !"127.0.0.1".equalsIgnoreCase(ip) - && !"localhost".equalsIgnoreCase(ip) - && hasJoinedIp(player.getName(), ip)) { + && !service.hasPermission(player, PlayerStatePermission.ALLOW_MULTIPLE_ACCOUNTS) + && !"127.0.0.1".equalsIgnoreCase(ip) + && !"localhost".equalsIgnoreCase(ip) + && hasJoinedIp(player.getName(), ip)) { + service.scheduleSyncDelayedTask(new Runnable() { @Override public void run() { - player.kickPlayer("A player with the same IP is already in game!"); + // TODO: Messages entry + player.kickPlayer(service.retrieveSingleMessage(MessageKey.SAME_IP_ONLINE)); } }); return; } - // Prevent player collisions in 1.9 - if (DISABLE_COLLISIONS) { - ((LivingEntity) player).setCollidable(false); - } + final Location spawnLoc = spawnLoader.getSpawnLocation(player); final boolean isAuthAvailable = database.isAuthAvailable(name); + + // TODO: continue cleanup from this -sgdc3 if (isAuthAvailable) { if (!service.getProperty(RestrictionSettings.NO_TELEPORT)) { if (Settings.isTeleportToSpawnEnabled || (Settings.isForceSpawnLocOnJoinEnabled && Settings.getForcedWorlds.contains(player.getWorld().getName()))) { diff --git a/src/main/resources/messages/messages_en.yml b/src/main/resources/messages/messages_en.yml index 203ce720..2623ac0b 100644 --- a/src/main/resources/messages/messages_en.yml +++ b/src/main/resources/messages/messages_en.yml @@ -1,3 +1,4 @@ +same_ip_online: 'A player with the same IP is already in game!' denied_chat: '&cIn order to be able to chat you must be authenticated!' kick_antibot: 'AntiBot protection mode is enabled! You have to wait some minutes before joining the server.' unknown_user: '&cCan''t find the requested user in the database!' From dbae4953cc96c963a4c0e90b3ebd864bf76149c8 Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Tue, 24 May 2016 15:50:28 +0200 Subject: [PATCH 094/200] Can't cancel sync player chat #722 --- .../java/fr/xephi/authme/listener/AuthMePlayerListener.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java index 12c7cba0..d2c8ad51 100644 --- a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java +++ b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java @@ -35,9 +35,9 @@ import org.bukkit.event.block.SignChangeEvent; import org.bukkit.event.entity.EntityDamageByEntityEvent; import org.bukkit.event.inventory.InventoryClickEvent; import org.bukkit.event.inventory.InventoryOpenEvent; +import org.bukkit.event.player.AsyncPlayerChatEvent; import org.bukkit.event.player.AsyncPlayerPreLoginEvent; import org.bukkit.event.player.PlayerBedEnterEvent; -import org.bukkit.event.player.PlayerChatEvent; import org.bukkit.event.player.PlayerCommandPreprocessEvent; import org.bukkit.event.player.PlayerDropItemEvent; import org.bukkit.event.player.PlayerFishEvent; @@ -129,8 +129,8 @@ public class AuthMePlayerListener implements Listener { sendLoginOrRegisterMessage(event.getPlayer()); } - @EventHandler(ignoreCancelled = true, priority = EventPriority.LOW) - public void onPlayerChat(PlayerChatEvent event) { + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + public void onPlayerChat(AsyncPlayerChatEvent event) { if (settings.getProperty(RestrictionSettings.ALLOW_CHAT)) { return; } From 5007ca45f0a5918682b33eb7fe8e1e7661301c54 Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Tue, 24 May 2016 15:55:09 +0200 Subject: [PATCH 095/200] Update HU language #720 Thanks to @rErEaT --- src/main/resources/messages/messages_hu.yml | 43 ++++++++++----------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/src/main/resources/messages/messages_hu.yml b/src/main/resources/messages/messages_hu.yml index 7aa03b39..68494794 100644 --- a/src/main/resources/messages/messages_hu.yml +++ b/src/main/resources/messages/messages_hu.yml @@ -1,45 +1,45 @@ -reg_only: '&4Csak regisztrált játékosok tudnak csatlakozni a szerverhez! Kérlek kattints a http://example.com weboldalra és regisztráld magad!' +reg_only: Csak regisztrált játékosoknak! Jelentkezni a mail@email.com e-mail címen lehet usage_unreg: '&cHasználat: "/unregister "' registered: '&aSikeres regisztráció.' -user_regged: '&cEz a játékosnév már regisztrálva van!' +user_regged: '&cJátékosnév már regisztrálva' login_msg: '&cKérlek jelentkezz be: "/login "' not_logged_in: '&cNem vagy bejelentkezve!' logout: '&cSikeresen kijelentkeztél!' usage_log: '&cBejelentkezés: "/login "' -unknown_user: '&cA kért felhasználó nem található az adatbázisban!' -reg_voluntarily: Regisztrálhatod magad a szerveren a következö parancsal "/register " +unknown_user: '&cA kért felhasználó nem telálható az adatbázisban!' +reg_voluntarily: Regisztrálhatod magad a szerveren a következö parancsal "/register " reg_disabled: '&cRegisztráció letiltva!' no_perm: '&cNincs jogod ehhez!' -usage_reg: '&cHasználat: "/register "' +usage_reg: '&cHasználat: /register ""' password_error_nick: '&cNem használhatod a felhasználóneved jelszónak, kérlek válassz másikat...' password_error_unsafe: '&cA választott jelszó nem biztonságos, kérlek válassz másikat...' unregistered: '&cRegisztráció sikeresen törölve!' same_nick: 'Ezzel a játékosnévvel már játszanak a szerveren.' -valid_session: '&2A megadott időkereten belül csatlakoztál vissza így a rendszer automatikusan beléptetett.' +valid_session: '&2A hálózati kapcsolat újraépítése megtörtént.' pwd_changed: '&cJelszó cserélve!' reload: 'Beálítások és adatbázis újratöltve!' timeout: 'Bejelentkezési időtúllépés!' error: 'Hiba lépett fel! Lépj kapcsolatba a tulajjal!' logged_in: '&cMár be vagy jelentkezve!' login: '&aSikeresen beléptél!' -wrong_pwd: '&4Hibás jelszó!' +wrong_pwd: '&4Hibás jelszó! &rÚj jelszó igénylése --> &4IP &rcímed és a választott jelszót küld el: foxika69@gmail.com' user_unknown: '&cEz a felhasználó nincs regisztrálva!' -reg_msg: '&cKérlek Regisztrálj: "/register "' +reg_msg: '&cKérlek Regisztrálj: "/register "' reg_email_msg: '&cKérlek regisztrálj: "/register "' -unsafe_spawn: 'A kilépési helyzeted nem biztonságos, teleportálás a kezdő pozícióra.' -max_reg: '&cElérted a maximálisan beregisztrálható karakterek számát. (%reg_count/%max_acc %reg_names)!' +unsafe_spawn: 'A kilépési helyzeted nem biztonságos, teleportálás a Spawnra.' +max_reg: 'Csak egy karakterrel registrálhatsz!' password_error: 'A két jelszó nem egyezik!' -invalid_session: '&cAz IP címed megváltozott, ezért a visszacsatlakozási időkereted lejárt.' +invalid_session: '&cAz IP címed megváltozott és a hálózati kapcsolatod lejárt. Kapcsolódj újra.' pass_len: 'A jelszavad nem éri el a minimális hosszúságot!' -vb_nonActiv: '&cA felhasználód aktiválása még nem történt meg, ellenőrizd a megadott emailed!' +vb_nonActiv: '&cA felhasználód aktiválása még nem történt meg, ellenőrizd a leveleid!' usage_changepassword: 'Használat: "/changepassword <új Jelszó>"' name_len: '&4A felhasználó neved túl hosszú, vagy túl rövid! Válassz másikat!' regex: '&4A felhasználóneved nem használható karaktereket tartalmaz. Elfogadott karakterek: REG_EX' add_email: '&3Kérlek add hozzá a felhasználódhoz az email címedet "/email add "' recovery_email: '&3Ha elfelejtetted a jelszavad, használd az: "/email recovery "' -usage_captcha: '&3A bejelentkezéshez CAPTCHA szükséges, kérlek használd a következő parancsot "/captcha "' -wrong_captcha: '&cHibás CAPTCHA, kérlek írd be a következő parancsot "/captcha THE_CAPTCHA"!' -valid_captcha: '&2CAPTCHA sikeresen feloldva!' +usage_captcha: '&3A bejelentkezéshez CAPTCHA szükséges, kérem használd a következő parancsot "/captcha "' +wrong_captcha: '&cHibás captcha, kérlek írd be a következő parancsot "/captcha THE_CAPTCHA" a csevegőbe!' +valid_captcha: '&2Captcha sikeresen feloldva!' kick_forvip: '&3VIP játékos csatlakozott a szerverhez!' kick_fullserver: '&4A szerver megtelt, próbálj csatlakozni később!' usage_email_add: '&cHasználat: "/email add "' @@ -52,12 +52,11 @@ email_added: '&2Az email címed rögzítése sikeresen megtörtént!' email_confirm: '&cKérlek ellenőrízd az email címedet!' email_changed: '&2Az email cím cseréje sikeresen megtörtént!' email_send: '&2A jelszó visszaállításhoz szükséges emailt elküldtük! Ellenőrízd a leveleidet!' -email_exists: '&cA visszaállító emailt elküldtük! Hiba esetén újra kérheted az alábbi parancs segítségével:' -email_already_used: '&4Ez az email cím már használatban van!' +email_exists: '&cA visszaállító emailt elküldtük! Hiba esetén újkérheted az alábbi parancs segítségével:' country_banned: '&4Az országod tiltólistán van ezen a szerveren!' -antibot_auto_enabled: '&4[AntiBot] Az AntiBot védelem bekapcsolt, mert a megszabott időn belül több felhasználó csatlakozott!' -antibot_auto_disabled: '&2[AntiBot] Az AntiBot kikapcsol %m perc múlva!' -kick_antibot: 'Az AntiBot védelem bekapcsolva! Kérlek várj pár percet mielőtt csatlakozol.' -not_owner_error: 'Ez nem a te felhasználód. Kérlek válassz másik nevet!' +antibot_auto_enabled: '&4[AntiBot] Az AntiBot védelem bekapcsolt a nagy számú hálózati kapcsolat miatt!' +antibot_auto_disabled: '&2[AntiBot] Az AntiBot kikapcsol %m múlva!' +kick_antibot: 'Az AntiBot védelem bekapcsolva! Kérünk várj pár másodpercet a csatlakozáshoz.' +two_factor_create: '&2A te titkos kódod a következő: %code' invalid_name_case: '%valid a felhasználó neved nem? Akkor ne %invalid névvel próbálj feljönni.' -two_factor_create: '&2A te titkos kódod a következő: %code. Vagy skenneld be a következő oldalról: %url' +denied_chat: '&cAmíg nem vagy bejelentkezve, nem használhatod a csevegőt!' From 55cc8820029220670b7438420096e6bd6be1602b Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Tue, 24 May 2016 16:06:59 +0200 Subject: [PATCH 096/200] Remove scheduler methods from the ProcessService @ljacqu it's ok now? ;) --- .../xephi/authme/process/ProcessService.java | 44 ++----------------- .../authme/process/join/AsynchronousJoin.java | 24 +++++----- .../process/login/AsynchronousLogin.java | 10 +++-- .../process/login/ProcessSyncPlayerLogin.java | 6 ++- .../process/logout/AsynchronousLogout.java | 6 ++- .../ProcessSynchronousPlayerLogout.java | 9 +++- .../register/ProcessSyncEmailRegister.java | 10 +++-- .../register/ProcessSyncPasswordRegister.java | 8 +++- .../unregister/AsynchronousUnregister.java | 8 +++- 9 files changed, 60 insertions(+), 65 deletions(-) diff --git a/src/main/java/fr/xephi/authme/process/ProcessService.java b/src/main/java/fr/xephi/authme/process/ProcessService.java index 3819ca64..d1e4c7d1 100644 --- a/src/main/java/fr/xephi/authme/process/ProcessService.java +++ b/src/main/java/fr/xephi/authme/process/ProcessService.java @@ -23,14 +23,15 @@ public class ProcessService { @Inject private NewSetting settings; + @Inject private Messages messages; + @Inject private AuthMe plugin; + @Inject private ValidationService validationService; - @Inject - private BukkitService bukkitService; /** * Retrieve a property's value. @@ -93,37 +94,6 @@ public class ProcessService { return messages.retrieveSingle(key); } - /** - * Run a task. - * - * @param task the task to run - * @return the assigned task id - */ - public BukkitTask runTask(Runnable task) { - return plugin.getServer().getScheduler().runTask(plugin, task); - } - - /** - * Run a task at a later time. - * - * @param task the task to run - * @param delay the delay before running the task - * @return the assigned task id - */ - public BukkitTask runTaskLater(Runnable task, long delay) { - return plugin.getServer().getScheduler().runTaskLater(plugin, task, delay); - } - - /** - * Schedule a synchronous delayed task. - * - * @param task the task to schedule - * @return the task id - */ - public int scheduleSyncDelayedTask(Runnable task) { - return plugin.getServer().getScheduler().scheduleSyncDelayedTask(plugin, task); - } - /** * Emit an event. * @@ -161,14 +131,6 @@ public class ProcessService { return validationService.isEmailFreeForRegistration(email, sender); } - public Collection getOnlinePlayers() { - return bukkitService.getOnlinePlayers(); - } - - public BukkitService getBukkitService() { - return bukkitService; - } - public boolean hasPermission(Player player, PermissionNode node) { return plugin.getPermissionsManager().hasPermission(player, node); } diff --git a/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java b/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java index 1cbbed26..245380ed 100644 --- a/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java +++ b/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java @@ -25,6 +25,7 @@ import fr.xephi.authme.settings.properties.RestrictionSettings; import fr.xephi.authme.settings.properties.SecuritySettings; import fr.xephi.authme.task.MessageTask; import fr.xephi.authme.task.TimeoutTask; +import fr.xephi.authme.util.BukkitService; import fr.xephi.authme.util.Utils; import fr.xephi.authme.util.Utils.GroupType; import org.apache.commons.lang.reflect.MethodUtils; @@ -65,6 +66,9 @@ public class AsynchronousJoin implements AsynchronousProcess { @Inject private SpawnLoader spawnLoader; + @Inject + private BukkitService bukkitService; + private static final boolean DISABLE_COLLISIONS = MethodUtils .getAccessibleMethod(LivingEntity.class, "setCollidable", new Class[]{}) != null; @@ -93,7 +97,7 @@ public class AsynchronousJoin implements AsynchronousProcess { } if (isNameRestricted(name, ip, player.getAddress().getHostName())) { - service.scheduleSyncDelayedTask(new Runnable() { + bukkitService.scheduleSyncDelayedTask(new Runnable() { @Override public void run() { AuthMePlayerListener.causeByAuthMe.putIfAbsent(name, true); @@ -112,7 +116,7 @@ public class AsynchronousJoin implements AsynchronousProcess { && !"localhost".equalsIgnoreCase(ip) && hasJoinedIp(player.getName(), ip)) { - service.scheduleSyncDelayedTask(new Runnable() { + bukkitService.scheduleSyncDelayedTask(new Runnable() { @Override public void run() { // TODO: Messages entry @@ -129,7 +133,7 @@ public class AsynchronousJoin implements AsynchronousProcess { if (isAuthAvailable) { if (!service.getProperty(RestrictionSettings.NO_TELEPORT)) { if (Settings.isTeleportToSpawnEnabled || (Settings.isForceSpawnLocOnJoinEnabled && Settings.getForcedWorlds.contains(player.getWorld().getName()))) { - service.scheduleSyncDelayedTask(new Runnable() { + bukkitService.scheduleSyncDelayedTask(new Runnable() { @Override public void run() { SpawnTeleportEvent tpEvent = new SpawnTeleportEvent(player, player.getLocation(), spawnLoc, playerCache.isAuthenticated(name)); @@ -183,7 +187,7 @@ public class AsynchronousJoin implements AsynchronousProcess { if (!Settings.noTeleport && !needFirstSpawn(player) && Settings.isTeleportToSpawnEnabled || (Settings.isForceSpawnLocOnJoinEnabled && Settings.getForcedWorlds.contains(player.getWorld().getName()))) { - service.scheduleSyncDelayedTask(new Runnable() { + bukkitService.scheduleSyncDelayedTask(new Runnable() { @Override public void run() { SpawnTeleportEvent tpEvent = new SpawnTeleportEvent(player, player.getLocation(), spawnLoc, playerCache.isAuthenticated(name)); @@ -204,7 +208,7 @@ public class AsynchronousJoin implements AsynchronousProcess { final int registrationTimeout = service.getProperty(RestrictionSettings.TIMEOUT) * 20; - service.scheduleSyncDelayedTask(new Runnable() { + bukkitService.scheduleSyncDelayedTask(new Runnable() { @Override public void run() { player.setOp(false); @@ -228,7 +232,7 @@ public class AsynchronousJoin implements AsynchronousProcess { int msgInterval = service.getProperty(RegistrationSettings.MESSAGE_INTERVAL); if (registrationTimeout > 0) { - BukkitTask id = service.runTaskLater(new TimeoutTask(plugin, name, player), registrationTimeout); + BukkitTask id = bukkitService.runTaskLater(new TimeoutTask(plugin, name, player), registrationTimeout); LimboPlayer limboPlayer = limboCache.getLimboPlayer(name); if (limboPlayer != null) { limboPlayer.setTimeoutTask(id); @@ -244,7 +248,7 @@ public class AsynchronousJoin implements AsynchronousProcess { : MessageKey.REGISTER_MESSAGE; } if (msgInterval > 0 && limboCache.getLimboPlayer(name) != null) { - BukkitTask msgTask = service.runTaskLater(new MessageTask(service.getBukkitService(), plugin.getMessages(), + BukkitTask msgTask = bukkitService.runTaskLater(new MessageTask(bukkitService, plugin.getMessages(), name, msg, msgInterval), 20L); LimboPlayer limboPlayer = limboCache.getLimboPlayer(name); if (limboPlayer != null) { @@ -267,7 +271,7 @@ public class AsynchronousJoin implements AsynchronousProcess { if (!tpEvent.isCancelled()) { if (player.isOnline() && tpEvent.getTo() != null && tpEvent.getTo().getWorld() != null) { final Location fLoc = tpEvent.getTo(); - service.scheduleSyncDelayedTask(new Runnable() { + bukkitService.scheduleSyncDelayedTask(new Runnable() { @Override public void run() { player.teleport(fLoc); @@ -285,7 +289,7 @@ public class AsynchronousJoin implements AsynchronousProcess { return; if (!player.hasPlayedBefore()) return; - service.scheduleSyncDelayedTask(new Runnable() { + bukkitService.scheduleSyncDelayedTask(new Runnable() { @Override public void run() { if (spawnLoc.getWorld() == null) { @@ -334,7 +338,7 @@ public class AsynchronousJoin implements AsynchronousProcess { private boolean hasJoinedIp(String name, String ip) { int count = 0; - for (Player player : service.getOnlinePlayers()) { + for (Player player : bukkitService.getOnlinePlayers()) { if (ip.equalsIgnoreCase(Utils.getPlayerIp(player)) && !player.getName().equalsIgnoreCase(name)) { count++; diff --git a/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java b/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java index 74de3b15..d829a6ce 100644 --- a/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java +++ b/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java @@ -24,6 +24,7 @@ import fr.xephi.authme.settings.properties.RegistrationSettings; import fr.xephi.authme.settings.properties.RestrictionSettings; import fr.xephi.authme.settings.properties.SecuritySettings; import fr.xephi.authme.task.MessageTask; +import fr.xephi.authme.util.BukkitService; import fr.xephi.authme.util.StringUtils; import fr.xephi.authme.util.Utils; import org.bukkit.Bukkit; @@ -58,6 +59,9 @@ public class AsynchronousLogin implements AsynchronousProcess { @Inject private SyncProcessManager syncProcessManager; + @Inject + private BukkitService bukkitService; + AsynchronousLogin() { } @@ -103,7 +107,7 @@ public class AsynchronousLogin implements AsynchronousProcess { String[] msg = service.getProperty(RegistrationSettings.USE_EMAIL_REGISTRATION) ? service.retrieveMessage(MessageKey.REGISTER_EMAIL_MESSAGE) : service.retrieveMessage(MessageKey.REGISTER_MESSAGE); - BukkitTask messageTask = service.runTask(new MessageTask(service.getBukkitService(), + BukkitTask messageTask = bukkitService.runTask(new MessageTask(bukkitService, name, msg, service.getProperty(RegistrationSettings.MESSAGE_INTERVAL))); limboPlayer.setMessageTask(messageTask); } @@ -207,7 +211,7 @@ public class AsynchronousLogin implements AsynchronousProcess { ConsoleLogger.info(player.getName() + " used the wrong password"); } if (service.getProperty(RestrictionSettings.KICK_ON_WRONG_PASSWORD)) { - service.scheduleSyncDelayedTask(new Runnable() { + bukkitService.scheduleSyncDelayedTask(new Runnable() { @Override public void run() { player.kickPlayer(service.retrieveSingleMessage(MessageKey.WRONG_PASSWORD)); @@ -237,7 +241,7 @@ public class AsynchronousLogin implements AsynchronousProcess { ConsoleLogger.info("The user " + player.getName() + " has " + auths.size() + " accounts:"); ConsoleLogger.info(message); - for (Player onlinePlayer : service.getOnlinePlayers()) { + for (Player onlinePlayer : bukkitService.getOnlinePlayers()) { if (onlinePlayer.getName().equalsIgnoreCase(player.getName()) && permissionsManager.hasPermission(onlinePlayer, PlayerPermission.SEE_OWN_ACCOUNTS)) { onlinePlayer.sendMessage("You own " + auths.size() + " accounts:"); diff --git a/src/main/java/fr/xephi/authme/process/login/ProcessSyncPlayerLogin.java b/src/main/java/fr/xephi/authme/process/login/ProcessSyncPlayerLogin.java index e40226d6..574245e6 100644 --- a/src/main/java/fr/xephi/authme/process/login/ProcessSyncPlayerLogin.java +++ b/src/main/java/fr/xephi/authme/process/login/ProcessSyncPlayerLogin.java @@ -18,6 +18,7 @@ import fr.xephi.authme.settings.Settings; import fr.xephi.authme.settings.properties.HooksSettings; import fr.xephi.authme.settings.properties.RegistrationSettings; import fr.xephi.authme.settings.properties.RestrictionSettings; +import fr.xephi.authme.util.BukkitService; import fr.xephi.authme.util.Utils; import fr.xephi.authme.util.Utils.GroupType; import org.apache.commons.lang.reflect.MethodUtils; @@ -47,6 +48,9 @@ public class ProcessSyncPlayerLogin implements SynchronousProcess { @Inject private DataSource dataSource; + @Inject + private BukkitService bukkitService; + @Inject // TODO ljacqu 20160520: Need to check whether we want to inject PluginManager, or some intermediate service private PluginManager pluginManager; @@ -151,7 +155,7 @@ public class ProcessSyncPlayerLogin implements SynchronousProcess { String jm = AuthMePlayerListener.joinMessage.get(name); if (jm != null) { if (!jm.isEmpty()) { - for (Player p : service.getOnlinePlayers()) { + for (Player p : bukkitService.getOnlinePlayers()) { if (p.isOnline()) { p.sendMessage(jm); } diff --git a/src/main/java/fr/xephi/authme/process/logout/AsynchronousLogout.java b/src/main/java/fr/xephi/authme/process/logout/AsynchronousLogout.java index 6cb7c77f..e3ff2f76 100644 --- a/src/main/java/fr/xephi/authme/process/logout/AsynchronousLogout.java +++ b/src/main/java/fr/xephi/authme/process/logout/AsynchronousLogout.java @@ -8,6 +8,7 @@ import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.process.AsynchronousProcess; import fr.xephi.authme.process.ProcessService; import fr.xephi.authme.process.SyncProcessManager; +import fr.xephi.authme.util.BukkitService; import fr.xephi.authme.util.Utils; import fr.xephi.authme.util.Utils.GroupType; import org.bukkit.entity.Player; @@ -31,6 +32,9 @@ public class AsynchronousLogout implements AsynchronousProcess { @Inject private SyncProcessManager syncProcessManager; + @Inject + private BukkitService bukkitService; + AsynchronousLogout() { } public void logout(final Player player) { @@ -49,7 +53,7 @@ public class AsynchronousLogout implements AsynchronousProcess { playerCache.removePlayer(name); database.setUnlogged(name); - service.scheduleSyncDelayedTask(new Runnable() { + bukkitService.scheduleSyncDelayedTask(new Runnable() { @Override public void run() { Utils.teleportToSpawn(player); diff --git a/src/main/java/fr/xephi/authme/process/logout/ProcessSynchronousPlayerLogout.java b/src/main/java/fr/xephi/authme/process/logout/ProcessSynchronousPlayerLogout.java index 7f781be2..60f0391f 100644 --- a/src/main/java/fr/xephi/authme/process/logout/ProcessSynchronousPlayerLogout.java +++ b/src/main/java/fr/xephi/authme/process/logout/ProcessSynchronousPlayerLogout.java @@ -14,6 +14,8 @@ import fr.xephi.authme.settings.properties.RegistrationSettings; import fr.xephi.authme.settings.properties.RestrictionSettings; import fr.xephi.authme.task.MessageTask; import fr.xephi.authme.task.TimeoutTask; +import fr.xephi.authme.util.BukkitService; + import org.bukkit.Bukkit; import org.bukkit.entity.Player; import org.bukkit.potion.PotionEffect; @@ -36,6 +38,9 @@ public class ProcessSynchronousPlayerLogout implements SynchronousProcess { @Inject private LimboCache limboCache; + @Inject + private BukkitService bukkitService; + ProcessSynchronousPlayerLogout() { } private void sendBungeeMessage(Player player) { @@ -66,10 +71,10 @@ public class ProcessSynchronousPlayerLogout implements SynchronousProcess { int timeOut = service.getProperty(RestrictionSettings.TIMEOUT) * TICKS_PER_SECOND; int interval = service.getProperty(RegistrationSettings.MESSAGE_INTERVAL); if (timeOut != 0) { - BukkitTask id = service.runTaskLater(new TimeoutTask(plugin, name, player), timeOut); + BukkitTask id = bukkitService.runTaskLater(new TimeoutTask(plugin, name, player), timeOut); limboCache.getLimboPlayer(name).setTimeoutTask(id); } - BukkitTask msgT = service.runTask(new MessageTask(service.getBukkitService(), plugin.getMessages(), + BukkitTask msgT = bukkitService.runTask(new MessageTask(bukkitService, plugin.getMessages(), name, MessageKey.LOGIN_MESSAGE, interval)); limboCache.getLimboPlayer(name).setMessageTask(msgT); if (player.isInsideVehicle() && player.getVehicle() != null) { diff --git a/src/main/java/fr/xephi/authme/process/register/ProcessSyncEmailRegister.java b/src/main/java/fr/xephi/authme/process/register/ProcessSyncEmailRegister.java index 89400471..cb3234ac 100644 --- a/src/main/java/fr/xephi/authme/process/register/ProcessSyncEmailRegister.java +++ b/src/main/java/fr/xephi/authme/process/register/ProcessSyncEmailRegister.java @@ -12,6 +12,7 @@ import fr.xephi.authme.settings.properties.RestrictionSettings; import fr.xephi.authme.settings.properties.SecuritySettings; import fr.xephi.authme.task.MessageTask; import fr.xephi.authme.task.TimeoutTask; +import fr.xephi.authme.util.BukkitService; import fr.xephi.authme.util.Utils; import org.bukkit.entity.Player; import org.bukkit.scheduler.BukkitTask; @@ -29,6 +30,9 @@ public class ProcessSyncEmailRegister implements SynchronousProcess { @Inject private LimboCache limboCache; + @Inject + private BukkitService bukkitService; + public ProcessSyncEmailRegister() { } public void processEmailRegister(Player player) { @@ -43,11 +47,11 @@ public class ProcessSyncEmailRegister implements SynchronousProcess { if (limbo != null) { if (time != 0) { - BukkitTask id = service.runTaskLater(new TimeoutTask(service.getAuthMe(), name, player), time); + BukkitTask id = bukkitService.runTaskLater(new TimeoutTask(service.getAuthMe(), name, player), time); limbo.setTimeoutTask(id); } - BukkitTask messageTask = service.runTask(new MessageTask( - service.getBukkitService(), name, service.retrieveMessage(MessageKey.LOGIN_MESSAGE), msgInterval)); + BukkitTask messageTask = bukkitService.runTask(new MessageTask( + bukkitService, name, service.retrieveMessage(MessageKey.LOGIN_MESSAGE), msgInterval)); limbo.setMessageTask(messageTask); } diff --git a/src/main/java/fr/xephi/authme/process/register/ProcessSyncPasswordRegister.java b/src/main/java/fr/xephi/authme/process/register/ProcessSyncPasswordRegister.java index b46807a2..98aa4609 100644 --- a/src/main/java/fr/xephi/authme/process/register/ProcessSyncPasswordRegister.java +++ b/src/main/java/fr/xephi/authme/process/register/ProcessSyncPasswordRegister.java @@ -19,6 +19,7 @@ import fr.xephi.authme.settings.properties.RestrictionSettings; import fr.xephi.authme.settings.properties.SecuritySettings; import fr.xephi.authme.task.MessageTask; import fr.xephi.authme.task.TimeoutTask; +import fr.xephi.authme.util.BukkitService; import fr.xephi.authme.util.Utils; import org.bukkit.Bukkit; import org.bukkit.entity.Player; @@ -40,6 +41,9 @@ public class ProcessSyncPasswordRegister implements SynchronousProcess { @Inject private ProcessService service; + @Inject + private BukkitService bukkitService; + ProcessSyncPasswordRegister() { } private void sendBungeeMessage(Player player) { @@ -75,10 +79,10 @@ public class ProcessSyncPasswordRegister implements SynchronousProcess { int interval = service.getProperty(RegistrationSettings.MESSAGE_INTERVAL); BukkitTask task; if (delay != 0) { - task = service.runTaskLater(new TimeoutTask(service.getAuthMe(), name, player), delay); + task = bukkitService.runTaskLater(new TimeoutTask(service.getAuthMe(), name, player), delay); cache.getLimboPlayer(name).setTimeoutTask(task); } - task = service.runTask(new MessageTask(service.getBukkitService(), plugin.getMessages(), + task = bukkitService.runTask(new MessageTask(bukkitService, plugin.getMessages(), name, MessageKey.LOGIN_MESSAGE, interval)); cache.getLimboPlayer(name).setMessageTask(task); if (player.isInsideVehicle() && player.getVehicle() != null) { diff --git a/src/main/java/fr/xephi/authme/process/unregister/AsynchronousUnregister.java b/src/main/java/fr/xephi/authme/process/unregister/AsynchronousUnregister.java index bf3dbb30..60c45f23 100644 --- a/src/main/java/fr/xephi/authme/process/unregister/AsynchronousUnregister.java +++ b/src/main/java/fr/xephi/authme/process/unregister/AsynchronousUnregister.java @@ -16,6 +16,7 @@ import fr.xephi.authme.settings.properties.RegistrationSettings; import fr.xephi.authme.settings.properties.RestrictionSettings; import fr.xephi.authme.task.MessageTask; import fr.xephi.authme.task.TimeoutTask; +import fr.xephi.authme.util.BukkitService; import fr.xephi.authme.util.Utils; import fr.xephi.authme.util.Utils.GroupType; import org.bukkit.entity.Player; @@ -47,6 +48,9 @@ public class AsynchronousUnregister implements AsynchronousProcess { @Inject private LimboCache limboCache; + @Inject + private BukkitService bukkitService; + AsynchronousUnregister() { } public void unregister(Player player, String password, boolean force) { @@ -69,10 +73,10 @@ public class AsynchronousUnregister implements AsynchronousProcess { LimboPlayer limboPlayer = limboCache.getLimboPlayer(name); int interval = service.getProperty(RegistrationSettings.MESSAGE_INTERVAL); if (timeOut != 0) { - BukkitTask id = service.runTaskLater(new TimeoutTask(plugin, name, player), timeOut); + BukkitTask id = bukkitService.runTaskLater(new TimeoutTask(plugin, name, player), timeOut); limboPlayer.setTimeoutTask(id); } - limboPlayer.setMessageTask(service.runTask(new MessageTask(service.getBukkitService(), + limboPlayer.setMessageTask(bukkitService.runTask(new MessageTask(bukkitService, plugin.getMessages(), name, MessageKey.REGISTER_MESSAGE, interval))); service.send(player, MessageKey.UNREGISTERED_SUCCESS); ConsoleLogger.info(player.getDisplayName() + " unregistered himself"); From 2a3ed384d592e6e9896b4821cf5cf5d35c5dd8f8 Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Tue, 24 May 2016 16:51:03 +0200 Subject: [PATCH 097/200] Fix #720 invalid HU lang file --- src/main/resources/messages/messages_hu.yml | 42 +++++++++++---------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/src/main/resources/messages/messages_hu.yml b/src/main/resources/messages/messages_hu.yml index 68494794..9f40be09 100644 --- a/src/main/resources/messages/messages_hu.yml +++ b/src/main/resources/messages/messages_hu.yml @@ -1,45 +1,45 @@ -reg_only: Csak regisztrált játékosoknak! Jelentkezni a mail@email.com e-mail címen lehet +reg_only: '&4Csak regisztrált játékosok tudnak csatlakozni a szerverhez! Kérlek kattints a http://example.com weboldalra és regisztráld magad!' usage_unreg: '&cHasználat: "/unregister "' registered: '&aSikeres regisztráció.' -user_regged: '&cJátékosnév már regisztrálva' +user_regged: '&cEz a játékosnév már regisztrálva van!' login_msg: '&cKérlek jelentkezz be: "/login "' not_logged_in: '&cNem vagy bejelentkezve!' logout: '&cSikeresen kijelentkeztél!' usage_log: '&cBejelentkezés: "/login "' -unknown_user: '&cA kért felhasználó nem telálható az adatbázisban!' -reg_voluntarily: Regisztrálhatod magad a szerveren a következö parancsal "/register " +unknown_user: '&cA kért felhasználó nem található az adatbázisban!' +reg_voluntarily: Regisztrálhatod magad a szerveren a következö parancsal "/register " reg_disabled: '&cRegisztráció letiltva!' no_perm: '&cNincs jogod ehhez!' -usage_reg: '&cHasználat: /register ""' +usage_reg: '&cHasználat: "/register "' password_error_nick: '&cNem használhatod a felhasználóneved jelszónak, kérlek válassz másikat...' password_error_unsafe: '&cA választott jelszó nem biztonságos, kérlek válassz másikat...' unregistered: '&cRegisztráció sikeresen törölve!' same_nick: 'Ezzel a játékosnévvel már játszanak a szerveren.' -valid_session: '&2A hálózati kapcsolat újraépítése megtörtént.' +valid_session: '&2A megadott időkereten belül csatlakoztál vissza így a rendszer automatikusan beléptetett.' pwd_changed: '&cJelszó cserélve!' reload: 'Beálítások és adatbázis újratöltve!' timeout: 'Bejelentkezési időtúllépés!' error: 'Hiba lépett fel! Lépj kapcsolatba a tulajjal!' logged_in: '&cMár be vagy jelentkezve!' login: '&aSikeresen beléptél!' -wrong_pwd: '&4Hibás jelszó! &rÚj jelszó igénylése --> &4IP &rcímed és a választott jelszót küld el: foxika69@gmail.com' +wrong_pwd: '&4Hibás jelszó!' user_unknown: '&cEz a felhasználó nincs regisztrálva!' -reg_msg: '&cKérlek Regisztrálj: "/register "' +reg_msg: '&cKérlek Regisztrálj: "/register "' reg_email_msg: '&cKérlek regisztrálj: "/register "' -unsafe_spawn: 'A kilépési helyzeted nem biztonságos, teleportálás a Spawnra.' -max_reg: 'Csak egy karakterrel registrálhatsz!' +unsafe_spawn: 'A kilépési helyzeted nem biztonságos, teleportálás a kezdő pozícióra.' +max_reg: '&cElérted a maximálisan beregisztrálható karakterek számát. (%reg_count/%max_acc %reg_names)!' password_error: 'A két jelszó nem egyezik!' -invalid_session: '&cAz IP címed megváltozott és a hálózati kapcsolatod lejárt. Kapcsolódj újra.' +invalid_session: '&cAz IP címed megváltozott, ezért a visszacsatlakozási időkereted lejárt.' pass_len: 'A jelszavad nem éri el a minimális hosszúságot!' -vb_nonActiv: '&cA felhasználód aktiválása még nem történt meg, ellenőrizd a leveleid!' +vb_nonActiv: '&cA felhasználód aktiválása még nem történt meg, ellenőrizd a megadott emailed!' usage_changepassword: 'Használat: "/changepassword <új Jelszó>"' name_len: '&4A felhasználó neved túl hosszú, vagy túl rövid! Válassz másikat!' regex: '&4A felhasználóneved nem használható karaktereket tartalmaz. Elfogadott karakterek: REG_EX' add_email: '&3Kérlek add hozzá a felhasználódhoz az email címedet "/email add "' recovery_email: '&3Ha elfelejtetted a jelszavad, használd az: "/email recovery "' -usage_captcha: '&3A bejelentkezéshez CAPTCHA szükséges, kérem használd a következő parancsot "/captcha "' -wrong_captcha: '&cHibás captcha, kérlek írd be a következő parancsot "/captcha THE_CAPTCHA" a csevegőbe!' -valid_captcha: '&2Captcha sikeresen feloldva!' +usage_captcha: '&3A bejelentkezéshez CAPTCHA szükséges, kérlek használd a következő parancsot "/captcha "' +wrong_captcha: '&cHibás CAPTCHA, kérlek írd be a következő parancsot "/captcha THE_CAPTCHA"!' +valid_captcha: '&2CAPTCHA sikeresen feloldva!' kick_forvip: '&3VIP játékos csatlakozott a szerverhez!' kick_fullserver: '&4A szerver megtelt, próbálj csatlakozni később!' usage_email_add: '&cHasználat: "/email add "' @@ -52,11 +52,13 @@ email_added: '&2Az email címed rögzítése sikeresen megtörtént!' email_confirm: '&cKérlek ellenőrízd az email címedet!' email_changed: '&2Az email cím cseréje sikeresen megtörtént!' email_send: '&2A jelszó visszaállításhoz szükséges emailt elküldtük! Ellenőrízd a leveleidet!' -email_exists: '&cA visszaállító emailt elküldtük! Hiba esetén újkérheted az alábbi parancs segítségével:' +email_exists: '&cA visszaállító emailt elküldtük! Hiba esetén újra kérheted az alábbi parancs segítségével:' +email_already_used: '&4Ez az email cím már használatban van!' country_banned: '&4Az országod tiltólistán van ezen a szerveren!' -antibot_auto_enabled: '&4[AntiBot] Az AntiBot védelem bekapcsolt a nagy számú hálózati kapcsolat miatt!' -antibot_auto_disabled: '&2[AntiBot] Az AntiBot kikapcsol %m múlva!' -kick_antibot: 'Az AntiBot védelem bekapcsolva! Kérünk várj pár másodpercet a csatlakozáshoz.' -two_factor_create: '&2A te titkos kódod a következő: %code' +antibot_auto_enabled: '&4[AntiBot] Az AntiBot védelem bekapcsolt, mert a megszabott időn belül több felhasználó csatlakozott!' +antibot_auto_disabled: '&2[AntiBot] Az AntiBot kikapcsol %m perc múlva!' +kick_antibot: 'Az AntiBot védelem bekapcsolva! Kérlek várj pár percet mielőtt csatlakozol.' +not_owner_error: 'Ez nem a te felhasználód. Kérlek válassz másik nevet!' invalid_name_case: '%valid a felhasználó neved nem? Akkor ne %invalid névvel próbálj feljönni.' +two_factor_create: '&2A te titkos kódod a következő: %code. Vagy skenneld be a következő oldalról: %url' denied_chat: '&cAmíg nem vagy bejelentkezve, nem használhatod a csevegőt!' From a7d3e60abeed013fa6de8ea82e9ac1574cdaf2ca Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Tue, 24 May 2016 17:00:23 +0200 Subject: [PATCH 098/200] Remove unused imports --- src/main/java/fr/xephi/authme/process/ProcessService.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/main/java/fr/xephi/authme/process/ProcessService.java b/src/main/java/fr/xephi/authme/process/ProcessService.java index d1e4c7d1..e179111d 100644 --- a/src/main/java/fr/xephi/authme/process/ProcessService.java +++ b/src/main/java/fr/xephi/authme/process/ProcessService.java @@ -6,15 +6,11 @@ import fr.xephi.authme.output.Messages; import fr.xephi.authme.permission.PermissionNode; import fr.xephi.authme.settings.NewSetting; import fr.xephi.authme.settings.domain.Property; -import fr.xephi.authme.util.BukkitService; import fr.xephi.authme.util.ValidationService; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.bukkit.event.Event; -import org.bukkit.scheduler.BukkitTask; - import javax.inject.Inject; -import java.util.Collection; /** * Service for asynchronous and synchronous processes. From 8de1897412af9a99898c01f71144f296c92cc9da Mon Sep 17 00:00:00 2001 From: ljacqu Date: Tue, 24 May 2016 18:25:38 +0200 Subject: [PATCH 099/200] Minor - Use explicit dependencies in ProcessService - Replace AuthMe in favor of PermissionsManager - Create missing tests for ProcessService --- .../xephi/authme/process/ProcessService.java | 22 +++---- .../register/ProcessSyncEmailRegister.java | 6 +- .../register/ProcessSyncPasswordRegister.java | 2 +- .../authme/process/ProcessServiceTest.java | 62 ++++++++++++------- 4 files changed, 55 insertions(+), 37 deletions(-) diff --git a/src/main/java/fr/xephi/authme/process/ProcessService.java b/src/main/java/fr/xephi/authme/process/ProcessService.java index e179111d..753573a9 100644 --- a/src/main/java/fr/xephi/authme/process/ProcessService.java +++ b/src/main/java/fr/xephi/authme/process/ProcessService.java @@ -1,15 +1,17 @@ package fr.xephi.authme.process; -import fr.xephi.authme.AuthMe; import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.output.Messages; import fr.xephi.authme.permission.PermissionNode; +import fr.xephi.authme.permission.PermissionsManager; import fr.xephi.authme.settings.NewSetting; import fr.xephi.authme.settings.domain.Property; import fr.xephi.authme.util.ValidationService; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.bukkit.event.Event; +import org.bukkit.plugin.PluginManager; + import javax.inject.Inject; /** @@ -24,11 +26,14 @@ public class ProcessService { private Messages messages; @Inject - private AuthMe plugin; + private PluginManager pluginManager; @Inject private ValidationService validationService; + @Inject + private PermissionsManager permissionsManager; + /** * Retrieve a property's value. * @@ -96,16 +101,7 @@ public class ProcessService { * @param event the event to emit */ public void callEvent(Event event) { - plugin.getServer().getPluginManager().callEvent(event); - } - - /** - * Return the plugin instance. - * - * @return AuthMe instance - */ - public AuthMe getAuthMe() { - return plugin; + pluginManager.callEvent(event); } /** @@ -128,7 +124,7 @@ public class ProcessService { } public boolean hasPermission(Player player, PermissionNode node) { - return plugin.getPermissionsManager().hasPermission(player, node); + return permissionsManager.hasPermission(player, node); } } diff --git a/src/main/java/fr/xephi/authme/process/register/ProcessSyncEmailRegister.java b/src/main/java/fr/xephi/authme/process/register/ProcessSyncEmailRegister.java index cb3234ac..88397e9d 100644 --- a/src/main/java/fr/xephi/authme/process/register/ProcessSyncEmailRegister.java +++ b/src/main/java/fr/xephi/authme/process/register/ProcessSyncEmailRegister.java @@ -1,5 +1,6 @@ package fr.xephi.authme.process.register; +import fr.xephi.authme.AuthMe; import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.cache.limbo.LimboCache; import fr.xephi.authme.cache.limbo.LimboPlayer; @@ -33,6 +34,9 @@ public class ProcessSyncEmailRegister implements SynchronousProcess { @Inject private BukkitService bukkitService; + @Inject + private AuthMe authMe; + public ProcessSyncEmailRegister() { } public void processEmailRegister(Player player) { @@ -47,7 +51,7 @@ public class ProcessSyncEmailRegister implements SynchronousProcess { if (limbo != null) { if (time != 0) { - BukkitTask id = bukkitService.runTaskLater(new TimeoutTask(service.getAuthMe(), name, player), time); + BukkitTask id = bukkitService.runTaskLater(new TimeoutTask(authMe, name, player), time); limbo.setTimeoutTask(id); } BukkitTask messageTask = bukkitService.runTask(new MessageTask( diff --git a/src/main/java/fr/xephi/authme/process/register/ProcessSyncPasswordRegister.java b/src/main/java/fr/xephi/authme/process/register/ProcessSyncPasswordRegister.java index 98aa4609..26bf174c 100644 --- a/src/main/java/fr/xephi/authme/process/register/ProcessSyncPasswordRegister.java +++ b/src/main/java/fr/xephi/authme/process/register/ProcessSyncPasswordRegister.java @@ -79,7 +79,7 @@ public class ProcessSyncPasswordRegister implements SynchronousProcess { int interval = service.getProperty(RegistrationSettings.MESSAGE_INTERVAL); BukkitTask task; if (delay != 0) { - task = bukkitService.runTaskLater(new TimeoutTask(service.getAuthMe(), name, player), delay); + task = bukkitService.runTaskLater(new TimeoutTask(plugin, name, player), delay); cache.getLimboPlayer(name).setTimeoutTask(task); } task = bukkitService.runTask(new MessageTask(bukkitService, plugin.getMessages(), diff --git a/src/test/java/fr/xephi/authme/process/ProcessServiceTest.java b/src/test/java/fr/xephi/authme/process/ProcessServiceTest.java index f35babb5..cff6a6c9 100644 --- a/src/test/java/fr/xephi/authme/process/ProcessServiceTest.java +++ b/src/test/java/fr/xephi/authme/process/ProcessServiceTest.java @@ -1,16 +1,17 @@ package fr.xephi.authme.process; -import fr.xephi.authme.AuthMe; -import fr.xephi.authme.datasource.DataSource; -import fr.xephi.authme.hooks.PluginHooks; import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.output.Messages; +import fr.xephi.authme.permission.PermissionNode; +import fr.xephi.authme.permission.PermissionsManager; +import fr.xephi.authme.permission.PlayerPermission; import fr.xephi.authme.settings.NewSetting; -import fr.xephi.authme.settings.SpawnLoader; import fr.xephi.authme.settings.properties.SecuritySettings; -import fr.xephi.authme.util.BukkitService; import fr.xephi.authme.util.ValidationService; import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.bukkit.event.Event; +import org.bukkit.plugin.PluginManager; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; @@ -31,22 +32,21 @@ public class ProcessServiceTest { @InjectMocks private ProcessService processService; + @Mock private ValidationService validationService; + @Mock private NewSetting settings; + @Mock private Messages messages; + @Mock - private AuthMe authMe; + private PluginManager pluginManager; + @Mock - private DataSource dataSource; - @Mock - private SpawnLoader spawnLoader; - @Mock - private PluginHooks pluginHooks; - @Mock - private BukkitService bukkitService; + private PermissionsManager permissionsManager; @Test public void shouldGetProperty() { @@ -127,15 +127,6 @@ public class ProcessServiceTest { verify(messages).retrieveSingle(key); } - @Test - public void shouldReturnAuthMeInstance() { - // given / when - AuthMe result = processService.getAuthMe(); - - // then - assertThat(result, equalTo(authMe)); - } - @Test public void shouldValidatePassword() { // given @@ -180,4 +171,31 @@ public class ProcessServiceTest { assertThat(result, equalTo(true)); verify(validationService).isEmailFreeForRegistration(email, sender); } + + @Test + public void shouldEmitEvent() { + // given + Event event = mock(Event.class); + + // when + processService.callEvent(event); + + // then + verify(pluginManager).callEvent(event); + } + + @Test + public void shouldCheckPermission() { + // given + Player player = mock(Player.class); + PermissionNode permission = PlayerPermission.CHANGE_PASSWORD; + given(permissionsManager.hasPermission(player, permission)).willReturn(true); + + // when + boolean result = processService.hasPermission(player, permission); + + // then + verify(permissionsManager).hasPermission(player, permission); + assertThat(result, equalTo(true)); + } } From e54cd526de475c79eee5482595de95a945b5b92e Mon Sep 17 00:00:00 2001 From: ljacqu Date: Tue, 24 May 2016 18:37:58 +0200 Subject: [PATCH 100/200] Add verification remarks in message files - Result from automatic task which checks message files for missing messages or tags --- src/main/resources/messages/messages_bg.yml | 5 ++++- src/main/resources/messages/messages_br.yml | 3 +++ src/main/resources/messages/messages_cz.yml | 5 ++++- src/main/resources/messages/messages_de.yml | 3 +++ src/main/resources/messages/messages_es.yml | 5 ++++- src/main/resources/messages/messages_eu.yml | 11 +++++++---- src/main/resources/messages/messages_fi.yml | 11 +++++++---- src/main/resources/messages/messages_fr.yml | 3 +++ src/main/resources/messages/messages_gl.yml | 5 ++++- src/main/resources/messages/messages_hu.yml | 1 + src/main/resources/messages/messages_id.yml | 5 ++++- src/main/resources/messages/messages_it.yml | 2 ++ src/main/resources/messages/messages_ko.yml | 5 ++++- src/main/resources/messages/messages_lt.yml | 9 ++++++--- src/main/resources/messages/messages_nl.yml | 5 ++++- src/main/resources/messages/messages_pl.yml | 11 +++++++---- src/main/resources/messages/messages_pt.yml | 5 ++++- src/main/resources/messages/messages_ru.yml | 5 ++++- src/main/resources/messages/messages_sk.yml | 9 ++++++--- src/main/resources/messages/messages_tr.yml | 5 ++++- src/main/resources/messages/messages_uk.yml | 5 ++++- src/main/resources/messages/messages_vn.yml | 5 ++++- src/main/resources/messages/messages_zhcn.yml | 5 ++++- src/main/resources/messages/messages_zhhk.yml | 5 ++++- src/main/resources/messages/messages_zhtw.yml | 5 ++++- 25 files changed, 105 insertions(+), 33 deletions(-) diff --git a/src/main/resources/messages/messages_bg.yml b/src/main/resources/messages/messages_bg.yml index 94e32f38..a7ffcf54 100644 --- a/src/main/resources/messages/messages_bg.yml +++ b/src/main/resources/messages/messages_bg.yml @@ -12,6 +12,7 @@ login: '&cВход успешен!' vb_nonActiv: '&fТвоята регистрация не е активирана, моля провери своя Имейл!' user_regged: '&cПотребителското име е заето!' usage_reg: '&cКоманда: /register парола парола' +# TODO max_reg: Missing tags %reg_count, %reg_names, %max_acc max_reg: '&fТи достигна максималния брой регистрации!' no_perm: '&cНямаш Достъп!' error: '&fПолучи се грешка; Моля свържете се с админ' @@ -57,8 +58,10 @@ antibot_auto_disabled: '[AuthMe] AntiBotMod автоматично изключ # TODO password_error_nick: '&cYou can''t use your name as password, please choose another one...' # TODO password_error_unsafe: '&cThe chosen password isn''t safe, please choose another one...' # TODO kick_antibot: 'AntiBot protection mode is enabled! You have to wait some minutes before joining the server.' +# TODO denied_chat: '&cIn order to be able to chat you must be authenticated!' # TODO email_exists: '&cA recovery email was already sent! You can discard it and send a new one using the command below:' +# TODO same_ip_online: 'A player with the same IP is already in game!' # TODO invalid_name_case: 'You should join using username %valid, not %invalid.' # TODO invalid_session: '&cYour IP has been changed and your session data has expired!' # TODO two_factor_create: '&2Your secret code is %code. You can scan it from here %url' -# TODO not_owner_error: 'You are not the owner of this account. Please try another name!' \ No newline at end of file +# TODO not_owner_error: 'You are not the owner of this account. Please choose another name!' \ No newline at end of file diff --git a/src/main/resources/messages/messages_br.yml b/src/main/resources/messages/messages_br.yml index 24662777..31df2dd1 100644 --- a/src/main/resources/messages/messages_br.yml +++ b/src/main/resources/messages/messages_br.yml @@ -14,6 +14,7 @@ login: '&2Logado com sucesso!' vb_nonActiv: '&cSua conta não foi ativada ainda, olhe seu email!' user_regged: '&cVocê já registrou esse nick!' usage_reg: '&cUse: /register ' +# TODO max_reg: Missing tags %reg_count, %reg_names, %max_acc max_reg: '&cVocê excedeu a quantidade de registros por ip!' no_perm: '&4Sem permissão!' error: '&4Um erro ocoreu, contate um administrador!' @@ -63,3 +64,5 @@ two_factor_create: '&2Seu código secreto é %code. Você pode escanear ele daqu email_already_used: '&4Este endereço de email já está em uso' not_owner_error: 'Você não é o dono desta conta. Por favor, tente outro nome!' invalid_name_case: 'Você deve entrar usando %valid, não %invalid.' +# TODO denied_chat: '&cIn order to be able to chat you must be authenticated!' +# TODO same_ip_online: 'A player with the same IP is already in game!' \ No newline at end of file diff --git a/src/main/resources/messages/messages_cz.yml b/src/main/resources/messages/messages_cz.yml index 580a12bc..037a1b05 100644 --- a/src/main/resources/messages/messages_cz.yml +++ b/src/main/resources/messages/messages_cz.yml @@ -26,6 +26,7 @@ reload: '&cZnovu nacteni nastaveni AuthMe probehlo uspesne.' timeout: '&cCas pro prihlaseni vyprsel!' unsafe_spawn: '&cTvoje pozice pri odpojeni byla nebezpecna, teleportuji na spawn!' invalid_session: '&cChybna data pri cteni pockejte do vyprseni.' +# TODO max_reg: Missing tags %reg_count, %reg_names, %max_acc max_reg: '&cJiz jsi prekrocil(a) limit pro pocet uctu z jedne IP.' password_error: '&cHesla se neshoduji!' pass_len: '&cTvoje heslo nedosahuje minimalni delky (4).' @@ -57,7 +58,9 @@ antibot_auto_disabled: '[AuthMe] AntiBotMod automaticky ukoncen po %m minutach, # TODO password_error_nick: '&cYou can''t use your name as password, please choose another one...' # TODO password_error_unsafe: '&cThe chosen password isn''t safe, please choose another one...' # TODO kick_antibot: 'AntiBot protection mode is enabled! You have to wait some minutes before joining the server.' +# TODO denied_chat: '&cIn order to be able to chat you must be authenticated!' # TODO email_exists: '&cA recovery email was already sent! You can discard it and send a new one using the command below:' +# TODO same_ip_online: 'A player with the same IP is already in game!' # TODO invalid_name_case: 'You should join using username %valid, not %invalid.' # TODO two_factor_create: '&2Your secret code is %code. You can scan it from here %url' -# TODO not_owner_error: 'You are not the owner of this account. Please try another name!' \ No newline at end of file +# TODO not_owner_error: 'You are not the owner of this account. Please choose another name!' \ No newline at end of file diff --git a/src/main/resources/messages/messages_de.yml b/src/main/resources/messages/messages_de.yml index 4cc59867..4f5a638a 100644 --- a/src/main/resources/messages/messages_de.yml +++ b/src/main/resources/messages/messages_de.yml @@ -11,6 +11,7 @@ login: '&2Erfolgreich eingeloggt!' vb_nonActiv: '&cDein Account wurde noch nicht aktiviert. Bitte prüfe deine E-Mails!' user_regged: '&cDieser Benutzername ist schon vergeben' usage_reg: '&cBenutze: /register ' +# TODO max_reg: Missing tag %reg_names max_reg: '&cDu hast die maximale Anzahl an Accounts erreicht (%max_acc/%reg_count).' no_perm: '&4Du hast keine Rechte, um diese Aktion auszuführen!' error: '&4Ein Fehler ist aufgetreten. Bitte kontaktiere einen Administrator.' @@ -61,3 +62,5 @@ two_factor_create: '&2Dein geheimer Code ist %code. Du kannst ihn hier abfragen: email_already_used: '&4Diese E-Mail-Adresse wird bereits genutzt.' invalid_name_case: 'Dein registrierter Benutzername ist &2%valid&f - nicht &4%invalid&f.' not_owner_error: 'Du bist nicht der Besitzer dieses Accounts. Bitte wähle einen anderen Namen!' +denied_chat: '&cDu musst eingeloggt sein, um chatten zu können!' +same_ip_online: 'Ein Spieler mit derselben IP ist bereits online!' diff --git a/src/main/resources/messages/messages_es.yml b/src/main/resources/messages/messages_es.yml index df415952..bebcd95a 100644 --- a/src/main/resources/messages/messages_es.yml +++ b/src/main/resources/messages/messages_es.yml @@ -12,6 +12,7 @@ login: '&c¡Sesión iniciada!' vb_nonActiv: '&fTu cuenta no está activada aún, ¡revisa tu correo!' user_regged: '&cUsuario ya registrado' usage_reg: '&cUso: /register Contraseña ConfirmarContraseña' +# TODO max_reg: Missing tags %reg_count, %reg_names, %max_acc max_reg: '&fHas excedido la cantidad máxima de registros para tu cuenta' no_perm: '&cNo tienes permiso' error: '&fHa ocurrido un error. Por favor contacta al administrador.' @@ -58,7 +59,9 @@ antibot_auto_disabled: '[AuthMe] AntiBotMod desactivado automáticamente luego d # TODO password_error_nick: '&cYou can''t use your name as password, please choose another one...' # TODO password_error_unsafe: '&cThe chosen password isn''t safe, please choose another one...' # TODO kick_antibot: 'AntiBot protection mode is enabled! You have to wait some minutes before joining the server.' +# TODO denied_chat: '&cIn order to be able to chat you must be authenticated!' # TODO email_exists: '&cA recovery email was already sent! You can discard it and send a new one using the command below:' +# TODO same_ip_online: 'A player with the same IP is already in game!' # TODO invalid_name_case: 'You should join using username %valid, not %invalid.' # TODO two_factor_create: '&2Your secret code is %code. You can scan it from here %url' -# TODO not_owner_error: 'You are not the owner of this account. Please try another name!' \ No newline at end of file +# TODO not_owner_error: 'You are not the owner of this account. Please choose another name!' \ No newline at end of file diff --git a/src/main/resources/messages/messages_eu.yml b/src/main/resources/messages/messages_eu.yml index b490aeeb..70a2ab20 100644 --- a/src/main/resources/messages/messages_eu.yml +++ b/src/main/resources/messages/messages_eu.yml @@ -11,6 +11,7 @@ login: '&cOngi etorri!' vb_nonActiv: '&fZure kontua aktibatu gabe dago, konfirmatu zure emaila!' user_regged: '&cErabiltzailea dagoeneko erregistratua' usage_reg: '&cErabili: /register pasahitza pasahitza' +# TODO max_reg: Missing tags %reg_count, %reg_names, %max_acc max_reg: '&fKontuko 2 erabiltzaile bakarrik izan ditzakezu' no_perm: '&cBaimenik ez' error: '&fErrorea; Mesedez jarri kontaktuan administratzaile batekin' @@ -51,13 +52,15 @@ country_banned: '[AuthMe] Zure herrialdea blokeatuta dago zerbitzari honetan' # TODO password_error_nick: '&cYou can''t use your name as password, please choose another one...' # TODO password_error_unsafe: '&cThe chosen password isn''t safe, please choose another one...' # TODO kick_antibot: 'AntiBot protection mode is enabled! You have to wait some minutes before joining the server.' -# TODO email_exists: '&cA recovery email was already sent! You can discard it and send a new one using the command below:' # TODO invalid_name_case: 'You should join using username %valid, not %invalid.' -# TODO antibot_auto_enabled: '&4[AntiBotService] AntiBot enabled due to the huge number of connections!' # TODO invalid_session: '&cYour IP has been changed and your session data has expired!' -# TODO wrong_captcha: '&cWrong captcha, please type "/captcha THE_CAPTCHA" into the chat!' # TODO usage_captcha: '&3To login you have to solve a captcha code, please use the command "/captcha "' # TODO antibot_auto_disabled: '&2[AntiBotService] AntiBot disabled disabled after %m minutes!' +# TODO denied_chat: '&cIn order to be able to chat you must be authenticated!' +# TODO same_ip_online: 'A player with the same IP is already in game!' +# TODO email_exists: '&cA recovery email was already sent! You can discard it and send a new one using the command below:' +# TODO antibot_auto_enabled: '&4[AntiBotService] AntiBot enabled due to the huge number of connections!' +# TODO wrong_captcha: '&cWrong captcha, please type "/captcha THE_CAPTCHA" into the chat!' # TODO valid_captcha: '&2Captcha code solved correctly!' # TODO two_factor_create: '&2Your secret code is %code. You can scan it from here %url' -# TODO not_owner_error: 'You are not the owner of this account. Please try another name!' \ No newline at end of file +# TODO not_owner_error: 'You are not the owner of this account. Please choose another name!' \ No newline at end of file diff --git a/src/main/resources/messages/messages_fi.yml b/src/main/resources/messages/messages_fi.yml index 80e6cd4f..b93101b1 100644 --- a/src/main/resources/messages/messages_fi.yml +++ b/src/main/resources/messages/messages_fi.yml @@ -11,6 +11,7 @@ login: '&cKirjauduit onnistuneesti' vb_nonActiv: '&fKäyttäjäsi ei ole vahvistettu!' user_regged: '&cPelaaja on jo rekisteröity' usage_reg: '&cKäyttötapa: /register salasana salasana' +# TODO max_reg: Missing tags %reg_count, %reg_names, %max_acc max_reg: '&fSinulla ei ole oikeuksia tehdä enempää pelaajatilejä!' no_perm: '&cEi oikeuksia' error: '&fVirhe: Ota yhteys palveluntarjoojaan!' @@ -50,14 +51,16 @@ email_added: '[AuthMe] Sähköposti lisätty!' email_confirm: '[AuthMe] Vahvistuta sähköposti!' email_changed: '[AuthMe] Sähköposti vaihdettu!' email_send: '[AuthMe] Palautus sähköposti lähetetty!' -# TODO email_already_used: '&4The email address is already being used' # TODO password_error_nick: '&cYou can''t use your name as password, please choose another one...' +# TODO email_already_used: '&4The email address is already being used' # TODO password_error_unsafe: '&cThe chosen password isn''t safe, please choose another one...' # TODO kick_antibot: 'AntiBot protection mode is enabled! You have to wait some minutes before joining the server.' -# TODO email_exists: '&cA recovery email was already sent! You can discard it and send a new one using the command below:' # TODO invalid_name_case: 'You should join using username %valid, not %invalid.' +# TODO antibot_auto_disabled: '&2[AntiBotService] AntiBot disabled disabled after %m minutes!' +# TODO denied_chat: '&cIn order to be able to chat you must be authenticated!' +# TODO same_ip_online: 'A player with the same IP is already in game!' +# TODO email_exists: '&cA recovery email was already sent! You can discard it and send a new one using the command below:' # TODO country_banned: '&4Your country is banned from this server!' # TODO antibot_auto_enabled: '&4[AntiBotService] AntiBot enabled due to the huge number of connections!' -# TODO antibot_auto_disabled: '&2[AntiBotService] AntiBot disabled disabled after %m minutes!' # TODO two_factor_create: '&2Your secret code is %code. You can scan it from here %url' -# TODO not_owner_error: 'You are not the owner of this account. Please try another name!' \ No newline at end of file +# TODO not_owner_error: 'You are not the owner of this account. Please choose another name!' \ No newline at end of file diff --git a/src/main/resources/messages/messages_fr.yml b/src/main/resources/messages/messages_fr.yml index 53efa63a..78bb2bc5 100644 --- a/src/main/resources/messages/messages_fr.yml +++ b/src/main/resources/messages/messages_fr.yml @@ -12,6 +12,7 @@ login: '&cConnexion effectuée!' vb_nonActiv: '&fCe compte n''est pas actif, consultez vos emails!' user_regged: '&cCe nom est deja utilisé.' usage_reg: '&cUtilisez la commande /register motdepasse confirmermotdepasse' +# TODO max_reg: Missing tags %reg_count, %reg_names, %max_acc max_reg: '&fLimite d''enregistrement atteinte pour ce compte' no_perm: '&cVous n''avez pas la permission' password_error_nick: '&fYou can''t use your name as password' @@ -62,3 +63,5 @@ two_factor_create: '&2Votre code secret est %code. Vous pouvez le scanner depuis email_already_used: '&4L''adresse email a déjà été utilisée' not_owner_error: 'Vous n''êtes pas le propriétaire de ce compte. Veuillez utiliser un autre nom !' invalid_name_case: 'Veuillez vous connecter avec %valid et non pas avec %invalid.' +denied_chat: 'Vous devez être connecté pour pouvoir parler !' +same_ip_online: 'Un joueur avec la même adresse IP joue déjà !' diff --git a/src/main/resources/messages/messages_gl.yml b/src/main/resources/messages/messages_gl.yml index 1562e3a3..7294a5d3 100644 --- a/src/main/resources/messages/messages_gl.yml +++ b/src/main/resources/messages/messages_gl.yml @@ -12,6 +12,7 @@ login: '&cIdentificación con éxito!' vb_nonActiv: '&fA túa conta aínda non está activada, comproba a túa bandexa de correo!!' user_regged: '&cEse nome de usuario xa está rexistrado' usage_reg: '&cUso: /register contrasinal confirmarContrasinal' +# TODO max_reg: Missing tags %reg_count, %reg_names, %max_acc max_reg: '&fExcediches o máximo de rexistros para a túa Conta' no_perm: '&cNon tes o permiso' error: '&fOcurriu un erro; contacta cun administrador' @@ -59,7 +60,9 @@ antibot_auto_disabled: '[AuthMe] AntiBotMod desactivouse automáticamente despo # TODO password_error_nick: '&cYou can''t use your name as password, please choose another one...' # TODO password_error_unsafe: '&cThe chosen password isn''t safe, please choose another one...' # TODO kick_antibot: 'AntiBot protection mode is enabled! You have to wait some minutes before joining the server.' +# TODO denied_chat: '&cIn order to be able to chat you must be authenticated!' # TODO email_exists: '&cA recovery email was already sent! You can discard it and send a new one using the command below:' +# TODO same_ip_online: 'A player with the same IP is already in game!' # TODO invalid_name_case: 'You should join using username %valid, not %invalid.' # TODO two_factor_create: '&2Your secret code is %code. You can scan it from here %url' -# TODO not_owner_error: 'You are not the owner of this account. Please try another name!' \ No newline at end of file +# TODO not_owner_error: 'You are not the owner of this account. Please choose another name!' \ No newline at end of file diff --git a/src/main/resources/messages/messages_hu.yml b/src/main/resources/messages/messages_hu.yml index 9f40be09..bb26702b 100644 --- a/src/main/resources/messages/messages_hu.yml +++ b/src/main/resources/messages/messages_hu.yml @@ -62,3 +62,4 @@ not_owner_error: 'Ez nem a te felhasználód. Kérlek válassz másik nevet!' invalid_name_case: '%valid a felhasználó neved nem? Akkor ne %invalid névvel próbálj feljönni.' two_factor_create: '&2A te titkos kódod a következő: %code. Vagy skenneld be a következő oldalról: %url' denied_chat: '&cAmíg nem vagy bejelentkezve, nem használhatod a csevegőt!' +# TODO same_ip_online: 'A player with the same IP is already in game!' \ No newline at end of file diff --git a/src/main/resources/messages/messages_id.yml b/src/main/resources/messages/messages_id.yml index 24e6a35b..f6c17995 100644 --- a/src/main/resources/messages/messages_id.yml +++ b/src/main/resources/messages/messages_id.yml @@ -10,6 +10,7 @@ valid_session: '&2Otomatis login, karena sesi masih terhubung.' login: '&2Login berhasil!' vb_nonActiv: '&cAkunmu belum diaktifkan, silahkan periksa email kamu!' user_regged: '&cKamu telah mendaftarkan username ini!' +# TODO max_reg: Missing tags %reg_count, %reg_names, %max_acc max_reg: '&Kamu telah mencapai batas maksimum pendaftaran di server ini!' no_perm: '&4Kamu tidak mempunyai izin melakukan ini!' error: '&4Terjadi kesalahan tak dikenal, silahkan hubungi Administrator!' @@ -55,9 +56,11 @@ antibot_auto_enabled: '&4[AntiBotService] AntiBot diaktifkan dikarenakan banyak antibot_auto_disabled: '&2[AntiBotService] AntiBot dimatikan setelah %m menit!' # TODO email_already_used: '&4The email address is already being used' # TODO kick_antibot: 'AntiBot protection mode is enabled! You have to wait some minutes before joining the server.' +# TODO denied_chat: '&cIn order to be able to chat you must be authenticated!' +# TODO same_ip_online: 'A player with the same IP is already in game!' # TODO invalid_name_case: 'You should join using username %valid, not %invalid.' # TODO country_banned: '&4Your country is banned from this server!' # TODO usage_unreg: '&cUsage: /unregister ' # TODO two_factor_create: '&2Your secret code is %code. You can scan it from here %url' -# TODO not_owner_error: 'You are not the owner of this account. Please try another name!' +# TODO not_owner_error: 'You are not the owner of this account. Please choose another name!' # TODO usage_reg: '&cUsage: /register ' \ No newline at end of file diff --git a/src/main/resources/messages/messages_it.yml b/src/main/resources/messages/messages_it.yml index 6361a4c9..5101b820 100644 --- a/src/main/resources/messages/messages_it.yml +++ b/src/main/resources/messages/messages_it.yml @@ -62,3 +62,5 @@ email_already_used: '&4L''indirizzo email inserito è già in uso' two_factor_create: '&2Il tuo codice segreto è: &f%code&n&2Puoi anche scannerizzare il codice QR da qui: &f%url' not_owner_error: 'Non sei il proprietario di questo account. Per favore scegli un altro nome!' invalid_name_case: 'Dovresti entrare con questo nome utente: "%valid", al posto di: "%invalid".' +# TODO denied_chat: '&cIn order to be able to chat you must be authenticated!' +# TODO same_ip_online: 'A player with the same IP is already in game!' \ No newline at end of file diff --git a/src/main/resources/messages/messages_ko.yml b/src/main/resources/messages/messages_ko.yml index ab122309..6a9abd44 100644 --- a/src/main/resources/messages/messages_ko.yml +++ b/src/main/resources/messages/messages_ko.yml @@ -14,6 +14,7 @@ login: '&c성공적인 접속입니다!' vb_nonActiv: '&f당신의 계정은 아직 활성화되어있지 않습니다, 당신의 이메일을 확인해보세요!' user_regged: '&c사용자이름은 이미 가입했습니다' usage_reg: '&c사용법: /register 비밀번호 비밀번호확인' +# TODO max_reg: Missing tags %reg_count, %reg_names, %max_acc max_reg: '&f당신은 가입할 수 있는 계정의 최대 한도를 초과했습니다' no_perm: '&c권한이 없습니다' error: '&f오류가 발생했습니다; 관리자에게 문의해주세요' @@ -61,6 +62,8 @@ antibot_auto_disabled: '[AuthMe] 봇차단모드가 %m 분 후에 자동적으 # TODO password_error_nick: '&cYou can''t use your name as password, please choose another one...' # TODO password_error_unsafe: '&cThe chosen password isn''t safe, please choose another one...' # TODO kick_antibot: 'AntiBot protection mode is enabled! You have to wait some minutes before joining the server.' +# TODO denied_chat: '&cIn order to be able to chat you must be authenticated!' +# TODO same_ip_online: 'A player with the same IP is already in game!' # TODO invalid_name_case: 'You should join using username %valid, not %invalid.' # TODO two_factor_create: '&2Your secret code is %code. You can scan it from here %url' -# TODO not_owner_error: 'You are not the owner of this account. Please try another name!' \ No newline at end of file +# TODO not_owner_error: 'You are not the owner of this account. Please choose another name!' \ No newline at end of file diff --git a/src/main/resources/messages/messages_lt.yml b/src/main/resources/messages/messages_lt.yml index fce26d1b..f197cfa1 100644 --- a/src/main/resources/messages/messages_lt.yml +++ b/src/main/resources/messages/messages_lt.yml @@ -11,6 +11,7 @@ login: '&aSekmingai prisijungete' vb_nonActiv: '&aJusu vartotojas nera patvirtintas, patikrinkite el.pasta.' user_regged: '&cVartotojo vardas jau uzregistruotas' usage_reg: '&eNaudojimas: /register slaptazodis pakartotiSlaptazodi' +# TODO max_reg: Missing tags %reg_count, %reg_names, %max_acc max_reg: '&cJus pasiekete maksimalu registraciju skaiciu.' no_perm: '&cNera leidimo' error: '&cAtsirado klaida, praneskite adminstratoriui.' @@ -48,16 +49,18 @@ kick_fullserver: '&cServeris yra pilnas, Atsiprasome.' # TODO kick_antibot: 'AntiBot protection mode is enabled! You have to wait some minutes before joining the server.' # TODO invalid_name_case: 'You should join using username %valid, not %invalid.' # TODO email_send: '&2Recovery email sent successfully! Please check your email inbox!' -# TODO usage_email_recovery: '&cUsage: /email recovery ' # TODO email_confirm: '&cPlease confirm your email address!' -# TODO old_email_invalid: '&cInvalid old email, try again!' +# TODO usage_email_recovery: '&cUsage: /email recovery ' # TODO email_changed: '&2Email address changed correctly!' +# TODO old_email_invalid: '&cInvalid old email, try again!' # TODO antibot_auto_disabled: '&2[AntiBotService] AntiBot disabled disabled after %m minutes!' # TODO email_added: '&2Email address successfully added to your account!' +# TODO denied_chat: '&cIn order to be able to chat you must be authenticated!' # TODO email_exists: '&cA recovery email was already sent! You can discard it and send a new one using the command below:' +# TODO same_ip_online: 'A player with the same IP is already in game!' # TODO country_banned: '&4Your country is banned from this server!' # TODO antibot_auto_enabled: '&4[AntiBotService] AntiBot enabled due to the huge number of connections!' # TODO usage_email_add: '&cUsage: /email add ' # TODO email_invalid: '&cInvalid email address, try again!' # TODO two_factor_create: '&2Your secret code is %code. You can scan it from here %url' -# TODO not_owner_error: 'You are not the owner of this account. Please try another name!' \ No newline at end of file +# TODO not_owner_error: 'You are not the owner of this account. Please choose another name!' \ No newline at end of file diff --git a/src/main/resources/messages/messages_nl.yml b/src/main/resources/messages/messages_nl.yml index a3159e48..6c7e9465 100644 --- a/src/main/resources/messages/messages_nl.yml +++ b/src/main/resources/messages/messages_nl.yml @@ -13,6 +13,7 @@ password_error_unsafe: '&fJe kunt geen onveilige wachtwoorden gebruiken' vb_nonActiv: Je accound is nog niet geactiveerd, controleer je mailbox! user_regged: '&cGebruikersnaam is al geregistreerd' usage_reg: '&cGebruik: /register ' +# TODO max_reg: Missing tags %reg_count, %reg_names, %max_acc max_reg: Je hebt de maximale registraties van jouw account overschreden. no_perm: '&cGeen toegang!' error: 'Error: neem contact op met een administrator!' @@ -58,7 +59,9 @@ kick_antibot: 'AntiBot is aangezet! Wacht alsjeblieft enkele minuten voor je met # TODO two_factor_create: Missing tag %url two_factor_create: '&2Je geheime code is %code' # TODO email_already_used: '&4The email address is already being used' +# TODO denied_chat: '&cIn order to be able to chat you must be authenticated!' # TODO email_exists: '&cA recovery email was already sent! You can discard it and send a new one using the command below:' +# TODO same_ip_online: 'A player with the same IP is already in game!' # TODO invalid_name_case: 'You should join using username %valid, not %invalid.' # TODO reg_email_msg: '&3Please, register to the server with the command "/register "' -# TODO not_owner_error: 'You are not the owner of this account. Please try another name!' \ No newline at end of file +# TODO not_owner_error: 'You are not the owner of this account. Please choose another name!' \ No newline at end of file diff --git a/src/main/resources/messages/messages_pl.yml b/src/main/resources/messages/messages_pl.yml index c6cba56f..578fede7 100644 --- a/src/main/resources/messages/messages_pl.yml +++ b/src/main/resources/messages/messages_pl.yml @@ -26,6 +26,7 @@ error: '&fBlad prosimy napisac do aministracji' unknown_user: '&fUzytkownika nie ma w bazie danych' unsafe_spawn: '&fTwoje pozycja jest niebezpieczna. Zostaniesz przeniesiony na bezpieczny spawn.' invalid_session: '&fSesja zakonczona!' +# TODO max_reg: Missing tags %reg_count, %reg_names, %max_acc max_reg: '&fPrzekroczyles limit zarejestrowanych kont na serwerze.' password_error: '&fHaslo niepoprawne!' pass_len: '&fTwoje haslo jest za krotkie lub za dlugie! Sprobuj ponownie...' @@ -50,14 +51,16 @@ email_added: '[AuthMe] Email dodany!' email_confirm: '[AuthMe] Potwierdz swoj email!' email_changed: '[AuthMe] Email zmieniony!' email_send: '[AuthMe] Email z odzyskaniem wyslany!' -# TODO email_already_used: '&4The email address is already being used' # TODO password_error_nick: '&cYou can''t use your name as password, please choose another one...' +# TODO email_already_used: '&4The email address is already being used' # TODO password_error_unsafe: '&cThe chosen password isn''t safe, please choose another one...' # TODO kick_antibot: 'AntiBot protection mode is enabled! You have to wait some minutes before joining the server.' -# TODO email_exists: '&cA recovery email was already sent! You can discard it and send a new one using the command below:' # TODO invalid_name_case: 'You should join using username %valid, not %invalid.' +# TODO antibot_auto_disabled: '&2[AntiBotService] AntiBot disabled disabled after %m minutes!' +# TODO denied_chat: '&cIn order to be able to chat you must be authenticated!' +# TODO same_ip_online: 'A player with the same IP is already in game!' +# TODO email_exists: '&cA recovery email was already sent! You can discard it and send a new one using the command below:' # TODO country_banned: '&4Your country is banned from this server!' # TODO antibot_auto_enabled: '&4[AntiBotService] AntiBot enabled due to the huge number of connections!' -# TODO antibot_auto_disabled: '&2[AntiBotService] AntiBot disabled disabled after %m minutes!' # TODO two_factor_create: '&2Your secret code is %code. You can scan it from here %url' -# TODO not_owner_error: 'You are not the owner of this account. Please try another name!' \ No newline at end of file +# TODO not_owner_error: 'You are not the owner of this account. Please choose another name!' \ No newline at end of file diff --git a/src/main/resources/messages/messages_pt.yml b/src/main/resources/messages/messages_pt.yml index 64428a52..7a8fa465 100644 --- a/src/main/resources/messages/messages_pt.yml +++ b/src/main/resources/messages/messages_pt.yml @@ -11,6 +11,7 @@ login: '&cAutenticado com sucesso!' vb_nonActiv: '&fA sua conta não foi ainda activada, verifique o seu email onde irá receber indicações para activação de conta. ' user_regged: '&cUtilizador já registado' usage_reg: '&cUse: /register seu@email.com seu@email.com' +# TODO max_reg: Missing tags %reg_count, %reg_names, %max_acc max_reg: '&cAtingiu o numero máximo de registos permitidos' no_perm: '&cSem Permissões' error: '&fOcorreu um erro; Por favor contacte um admin' @@ -58,7 +59,9 @@ antibot_auto_disabled: '[AuthMe] AntiBotMod desactivado automaticamente após %m # TODO password_error_nick: '&cYou can''t use your name as password, please choose another one...' # TODO password_error_unsafe: '&cThe chosen password isn''t safe, please choose another one...' # TODO kick_antibot: 'AntiBot protection mode is enabled! You have to wait some minutes before joining the server.' +# TODO denied_chat: '&cIn order to be able to chat you must be authenticated!' # TODO email_exists: '&cA recovery email was already sent! You can discard it and send a new one using the command below:' +# TODO same_ip_online: 'A player with the same IP is already in game!' # TODO invalid_name_case: 'You should join using username %valid, not %invalid.' # TODO two_factor_create: '&2Your secret code is %code. You can scan it from here %url' -# TODO not_owner_error: 'You are not the owner of this account. Please try another name!' \ No newline at end of file +# TODO not_owner_error: 'You are not the owner of this account. Please choose another name!' \ No newline at end of file diff --git a/src/main/resources/messages/messages_ru.yml b/src/main/resources/messages/messages_ru.yml index 2b9dd640..b77b126a 100644 --- a/src/main/resources/messages/messages_ru.yml +++ b/src/main/resources/messages/messages_ru.yml @@ -11,6 +11,7 @@ login: '&a&lВы успешно вошли!' vb_nonActiv: '&6Ваш аккаунт еще не активирован! Проверьте вашу почту!' user_regged: '&c&lТакой игрок уже зарегистрирован' usage_reg: '&c&lИспользование: &e&l/reg ПАРОЛЬ ПОВТОР_ПАРОЛЯ' +# TODO max_reg: Missing tags %reg_count, %reg_names, %max_acc max_reg: '&c&lВы превысили макс количество регистраций на ваш IP' no_perm: '&c&lНедостаточно прав' error: '&c&lПроизошла ошибка. Свяжитесь с администратором' @@ -57,7 +58,9 @@ antibot_auto_enabled: '&a[AuthMe] AntiBot-режим автоматически antibot_auto_disabled: '&a[AuthMe] AntiBot-режим автоматичски отключен после %m мин. Надеюсь атака закончилась' # TODO email_already_used: '&4The email address is already being used' # TODO kick_antibot: 'AntiBot protection mode is enabled! You have to wait some minutes before joining the server.' +# TODO denied_chat: '&cIn order to be able to chat you must be authenticated!' # TODO email_exists: '&cA recovery email was already sent! You can discard it and send a new one using the command below:' +# TODO same_ip_online: 'A player with the same IP is already in game!' # TODO invalid_name_case: 'You should join using username %valid, not %invalid.' # TODO two_factor_create: '&2Your secret code is %code. You can scan it from here %url' -# TODO not_owner_error: 'You are not the owner of this account. Please try another name!' \ No newline at end of file +# TODO not_owner_error: 'You are not the owner of this account. Please choose another name!' \ No newline at end of file diff --git a/src/main/resources/messages/messages_sk.yml b/src/main/resources/messages/messages_sk.yml index 77433f62..ab26f0c6 100644 --- a/src/main/resources/messages/messages_sk.yml +++ b/src/main/resources/messages/messages_sk.yml @@ -29,6 +29,7 @@ error: '&fNastala chyba; Kontaktujte administrátora' unknown_user: '&fHrac nie je v databázi' unsafe_spawn: '&fTvoj pozícia bol nebezpecná, teleportujem hraca na spawn' invalid_session: '&fZapamätane casove data nie su doveryhodne. Cakaj na ukoncenie spojenia' +# TODO max_reg: Missing tags %reg_count, %reg_names, %max_acc max_reg: '&fDosiahol si maximum registrovanych uctov.' password_error: '&fHeslá sa nezhodujú' pass_len: '&fHeslo je velmi kratke alebo dlhe' @@ -38,9 +39,9 @@ name_len: '&cTvoje meno je velmi krátke alebo dlhé' regex: '&cTvoje meno obsahuje zakázané znaky. Povolené znaky: REG_EX' add_email: '&cPridaj svoj e-mail príkazom "/email add email zopakujEmail"' recovery_email: '&cZabudol si heslo? Pouzi príkaz /email recovery ' +# TODO email_already_used: '&4The email address is already being used' # TODO usage_email_change: '&cUsage: /email change ' # TODO password_error_nick: '&cYou can''t use your name as password, please choose another one...' -# TODO email_already_used: '&4The email address is already being used' # TODO new_email_invalid: '&cInvalid new email, try again!' # TODO kick_antibot: 'AntiBot protection mode is enabled! You have to wait some minutes before joining the server.' # TODO old_email_invalid: '&cInvalid old email, try again!' @@ -54,13 +55,15 @@ recovery_email: '&cZabudol si heslo? Pouzi príkaz /email recovery ' # TODO password_error_unsafe: '&cThe chosen password isn''t safe, please choose another one...' # TODO invalid_name_case: 'You should join using username %valid, not %invalid.' # TODO email_send: '&2Recovery email sent successfully! Please check your email inbox!' -# TODO usage_email_recovery: '&cUsage: /email recovery ' # TODO usage_captcha: '&3To login you have to solve a captcha code, please use the command "/captcha "' +# TODO usage_email_recovery: '&cUsage: /email recovery ' # TODO email_confirm: '&cPlease confirm your email address!' # TODO kick_fullserver: '&4The server is full, try again later!' # TODO email_added: '&2Email address successfully added to your account!' +# TODO denied_chat: '&cIn order to be able to chat you must be authenticated!' +# TODO same_ip_online: 'A player with the same IP is already in game!' # TODO email_exists: '&cA recovery email was already sent! You can discard it and send a new one using the command below:' # TODO kick_forvip: '&3A VIP player has joined the server when it was full!' # TODO email_invalid: '&cInvalid email address, try again!' # TODO antibot_auto_enabled: '&4[AntiBotService] AntiBot enabled due to the huge number of connections!' -# TODO not_owner_error: 'You are not the owner of this account. Please try another name!' \ No newline at end of file +# TODO not_owner_error: 'You are not the owner of this account. Please choose another name!' \ No newline at end of file diff --git a/src/main/resources/messages/messages_tr.yml b/src/main/resources/messages/messages_tr.yml index 0e118de0..d6fc40f4 100644 --- a/src/main/resources/messages/messages_tr.yml +++ b/src/main/resources/messages/messages_tr.yml @@ -11,6 +11,7 @@ login: '&cBasarili giris!' vb_nonActiv: '&fHesabin aktiflestirilmedi! Emailini kontrol et' user_regged: '&cKullanici zaten oyunda' usage_reg: '&cKullanimi: /register sifre sifretekrar' +# TODO max_reg: Missing tags %reg_count, %reg_names, %max_acc max_reg: '&fMaximim kayit limitini astin!' no_perm: '&cYetkin yok' error: '&fBir hata olustu; Lutfen adminle iletisime gec' @@ -57,7 +58,9 @@ antibot_auto_disabled: '[AuthMe] AntiBotMode %m dakika sonra otomatik olarak isg # TODO password_error_nick: '&cYou can''t use your name as password, please choose another one...' # TODO password_error_unsafe: '&cThe chosen password isn''t safe, please choose another one...' # TODO kick_antibot: 'AntiBot protection mode is enabled! You have to wait some minutes before joining the server.' +# TODO denied_chat: '&cIn order to be able to chat you must be authenticated!' # TODO email_exists: '&cA recovery email was already sent! You can discard it and send a new one using the command below:' +# TODO same_ip_online: 'A player with the same IP is already in game!' # TODO invalid_name_case: 'You should join using username %valid, not %invalid.' # TODO two_factor_create: '&2Your secret code is %code. You can scan it from here %url' -# TODO not_owner_error: 'You are not the owner of this account. Please try another name!' \ No newline at end of file +# TODO not_owner_error: 'You are not the owner of this account. Please choose another name!' \ No newline at end of file diff --git a/src/main/resources/messages/messages_uk.yml b/src/main/resources/messages/messages_uk.yml index df5710f9..28846301 100644 --- a/src/main/resources/messages/messages_uk.yml +++ b/src/main/resources/messages/messages_uk.yml @@ -11,6 +11,7 @@ login: '&2Успішна авторизація!' vb_nonActiv: '&fВаш акаунт не активований. Перевірте свою електронну адресу!' user_regged: '&cТакий користувач вже зареєстрований' usage_reg: '&cВикористовуйте: /reg Пароль Повтор_Пароля' +# TODO max_reg: Missing tags %reg_count, %reg_names, %max_acc max_reg: '&fВи перевищили максимальне число реєстрацій на ваш IP' no_perm: '&cУ Вас недостатньо прав' error: '&fЩось пішло не так; Будь ласка зв`яжіться з адміністратором' @@ -58,7 +59,9 @@ antibot_auto_enabled: '[AuthMe] AntiBotMod автоматично увімкне antibot_auto_disabled: '[AuthMe] AntiBotMod автоматично вимкнувся, сподіваємось атака зупинена' # TODO email_already_used: '&4The email address is already being used' # TODO kick_antibot: 'AntiBot protection mode is enabled! You have to wait some minutes before joining the server.' +# TODO denied_chat: '&cIn order to be able to chat you must be authenticated!' # TODO email_exists: '&cA recovery email was already sent! You can discard it and send a new one using the command below:' +# TODO same_ip_online: 'A player with the same IP is already in game!' # TODO invalid_name_case: 'You should join using username %valid, not %invalid.' # TODO two_factor_create: '&2Your secret code is %code. You can scan it from here %url' -# TODO not_owner_error: 'You are not the owner of this account. Please try another name!' \ No newline at end of file +# TODO not_owner_error: 'You are not the owner of this account. Please choose another name!' \ No newline at end of file diff --git a/src/main/resources/messages/messages_vn.yml b/src/main/resources/messages/messages_vn.yml index 262b7b73..6d8ebbd4 100644 --- a/src/main/resources/messages/messages_vn.yml +++ b/src/main/resources/messages/messages_vn.yml @@ -11,6 +11,7 @@ login: '&cĐăng nhập thành công!' vb_nonActiv: '&fTài khoản của bạn chưa được kích hoạt, kiểm tra email!' user_regged: '&cTên đăng nhập này đã được đăng kí' usage_reg: '&eSử dụng: /register mật-khẩu nhập-lại-mật-khẩu' +# TODO max_reg: Missing tags %reg_count, %reg_names, %max_acc max_reg: '&fSố lượng tài khoản ở IP của bạn trong server này đã quá giới hạn cho phép' no_perm: '&cKhông có quyền' error: '&fCó lỗi xảy ra; Báo lại cho người điều hành server' @@ -58,7 +59,9 @@ antibot_auto_disabled: '[AuthMe] AntiBot tự huỷ kích hoạt sau %m phút, h # TODO password_error_nick: '&cYou can''t use your name as password, please choose another one...' # TODO password_error_unsafe: '&cThe chosen password isn''t safe, please choose another one...' # TODO kick_antibot: 'AntiBot protection mode is enabled! You have to wait some minutes before joining the server.' +# TODO denied_chat: '&cIn order to be able to chat you must be authenticated!' # TODO email_exists: '&cA recovery email was already sent! You can discard it and send a new one using the command below:' +# TODO same_ip_online: 'A player with the same IP is already in game!' # TODO invalid_name_case: 'You should join using username %valid, not %invalid.' # TODO two_factor_create: '&2Your secret code is %code. You can scan it from here %url' -# TODO not_owner_error: 'You are not the owner of this account. Please try another name!' \ No newline at end of file +# TODO not_owner_error: 'You are not the owner of this account. Please choose another name!' \ No newline at end of file diff --git a/src/main/resources/messages/messages_zhcn.yml b/src/main/resources/messages/messages_zhcn.yml index fbf89b5e..3118184d 100644 --- a/src/main/resources/messages/messages_zhcn.yml +++ b/src/main/resources/messages/messages_zhcn.yml @@ -15,6 +15,7 @@ login: '&8[&6玩家系统&8] &c已成功登录!' vb_nonActiv: '&8[&6玩家系统&8] &f你的帐号还未激活,请查看你的邮箱!' user_regged: '&8[&6玩家系统&8] &c此用户已经在此服务器注册过' usage_reg: '&8[&6玩家系统&8] &c正确用法:“/register <密码> <再输入一次以确定密码>”' +# TODO max_reg: Missing tags %reg_count, %reg_names, %max_acc max_reg: '&8[&6玩家系统&8] &f你不允许再为你的IP在服务器注册更多用户了!' no_perm: '&8[&6玩家系统&8] &c没有权限' error: '&8[&6玩家系统&8] &f发现错误,请联系管理员' @@ -63,4 +64,6 @@ email_already_used: '&8[&6玩家系统&8] &4邮箱已被使用' email_exists: '&8[&6玩家系统&8] &c恢复邮件已发送 ! 你可以丢弃它然後使用以下的指令来发送新的邮件:' two_factor_create: '&8[&6玩家系统&8] &2你的代码是 %code,你可以使用 %url 来扫描' not_owner_error: '&8[&6玩家系统&8] &4警告! &c你并不是此帐户持有人,请立即登出。 ' -invalid_name_case: '&8[&6玩家系统&8] &c你应该使用「%valid」而并非「%invalid」登入游戏。 ' \ No newline at end of file +invalid_name_case: '&8[&6玩家系统&8] &c你应该使用「%valid」而并非「%invalid」登入游戏。 ' +# TODO denied_chat: '&cIn order to be able to chat you must be authenticated!' +# TODO same_ip_online: 'A player with the same IP is already in game!' \ No newline at end of file diff --git a/src/main/resources/messages/messages_zhhk.yml b/src/main/resources/messages/messages_zhhk.yml index 3dc579c7..209962cf 100644 --- a/src/main/resources/messages/messages_zhhk.yml +++ b/src/main/resources/messages/messages_zhhk.yml @@ -15,6 +15,7 @@ login: '&8[&6用戶系統&8] &c你成功登入了。' vb_nonActiv: '&8[&6用戶系統&8] &f你的帳戶還沒有經過電郵驗證 !' user_regged: '&8[&6用戶系統&8] &c此用戶名已經註冊過了。' usage_reg: '&8[&6用戶系統&8] &f用法: 《 /register <密碼> <重覆密碼> 》' +# TODO max_reg: Missing tags %reg_count, %reg_names, %max_acc max_reg: '&8[&6用戶系統&8] &f你的IP地址已達到註冊數上限。' no_perm: '&8[&6用戶系統&8] &b嗯~你想幹甚麼?' error: '&8[&6用戶系統&8] &f發生錯誤,請與管理員聯絡。' @@ -63,4 +64,6 @@ email_already_used: '&8[&6用戶系統&8] &4這個電郵地址已被使用。' email_exists: '&8[&6用戶系統&8] &c訊息已發送!如果你收不到該封電郵,可以使用以下指令進行重寄:' two_factor_create: '&8[&6用戶系統 - 兩步驗證碼&8] &b你的登入金鑰為&9「%c%code&9」&b,掃描連結為:&c %url' not_owner_error: '&8[&6用戶系統&8] &4警告!&c你並不是此帳戶持有人,請立即登出。' -invalid_name_case: '&8[&6用戶系統&8] &4警告!&c你應該使用「%valid」而並非「%invalid」登入遊戲。' \ No newline at end of file +invalid_name_case: '&8[&6用戶系統&8] &4警告!&c你應該使用「%valid」而並非「%invalid」登入遊戲。' +# TODO denied_chat: '&cIn order to be able to chat you must be authenticated!' +# TODO same_ip_online: 'A player with the same IP is already in game!' \ No newline at end of file diff --git a/src/main/resources/messages/messages_zhtw.yml b/src/main/resources/messages/messages_zhtw.yml index 9b0d536c..6fe136f0 100644 --- a/src/main/resources/messages/messages_zhtw.yml +++ b/src/main/resources/messages/messages_zhtw.yml @@ -15,6 +15,7 @@ login: '&b【AuthMe】&6密碼正確,你已成功登入!' vb_nonActiv: '&b【AuthMe】&6你的帳號還沒有經過驗證! 檢查看看你的電子信箱 (Email) 吧!' user_regged: '&b【AuthMe】&6這個帳號已經被註冊過了!' usage_reg: '&b【AuthMe】&6用法: &c"/register <密碼> <確認密碼>"' +# TODO max_reg: Missing tags %reg_count, %reg_names, %max_acc max_reg: '&b【AuthMe】&6你的 IP 位置所註冊的帳號數量已經達到最大。' no_perm: '&b【AuthMe】&6你沒有使用該指令的權限。' error: '&b【AuthMe】&6發生錯誤,請聯繫管理員' @@ -63,4 +64,6 @@ email_already_used: '&b【AuthMe】&4這個電郵地址已被使用。' email_exists: '&b【AuthMe】&6這個帳戶已經有設定電子郵件了' two_factor_create: '&b【AuthMe - 兩步驗證碼】&b你的登入金鑰為&9「%c%code&9」&b,掃描連結為:&c %url' not_owner_error: '&b【AuthMe】&4警告!&c你並不是此帳戶持有人,請立即登出。' -invalid_name_case: '&b【AuthMe】&4警告!&c你應該使用「%valid」而並非「%invalid」登入遊戲。' \ No newline at end of file +invalid_name_case: '&b【AuthMe】&4警告!&c你應該使用「%valid」而並非「%invalid」登入遊戲。' +# TODO denied_chat: '&cIn order to be able to chat you must be authenticated!' +# TODO same_ip_online: 'A player with the same IP is already in game!' \ No newline at end of file From 6341d00df076b6604ae22833b04a15c84a46d942 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Tue, 24 May 2016 18:59:01 +0200 Subject: [PATCH 101/200] Code householding - remove legacy settings and TODO messages - Remove fields in legacy settings that are now unused - Remove various irrelevant todo messages - Reduce visibility on some elements --- .../authme/command/help/HelpProvider.java | 1 - .../process/login/ProcessSyncPlayerLogin.java | 1 - .../register/ProcessSyncEmailRegister.java | 2 +- .../authme/security/PasswordSecurity.java | 2 +- .../fr/xephi/authme/settings/Settings.java | 73 ++++++++----------- .../fr/xephi/authme/settings/SpawnLoader.java | 1 - 6 files changed, 32 insertions(+), 48 deletions(-) diff --git a/src/main/java/fr/xephi/authme/command/help/HelpProvider.java b/src/main/java/fr/xephi/authme/command/help/HelpProvider.java index 8a2774a5..0acf62f8 100644 --- a/src/main/java/fr/xephi/authme/command/help/HelpProvider.java +++ b/src/main/java/fr/xephi/authme/command/help/HelpProvider.java @@ -120,7 +120,6 @@ public class HelpProvider implements SettingsDependent { } private static void printAlternatives(CommandDescription command, List correctLabels, List lines) { - // TODO ljacqu 20151219: Need to show alternatives for base labels too? E.g. /r for /register if (command.getLabels().size() <= 1 || correctLabels.size() <= 1) { return; } diff --git a/src/main/java/fr/xephi/authme/process/login/ProcessSyncPlayerLogin.java b/src/main/java/fr/xephi/authme/process/login/ProcessSyncPlayerLogin.java index 574245e6..78df4473 100644 --- a/src/main/java/fr/xephi/authme/process/login/ProcessSyncPlayerLogin.java +++ b/src/main/java/fr/xephi/authme/process/login/ProcessSyncPlayerLogin.java @@ -52,7 +52,6 @@ public class ProcessSyncPlayerLogin implements SynchronousProcess { private BukkitService bukkitService; @Inject - // TODO ljacqu 20160520: Need to check whether we want to inject PluginManager, or some intermediate service private PluginManager pluginManager; private final boolean restoreCollisions = MethodUtils diff --git a/src/main/java/fr/xephi/authme/process/register/ProcessSyncEmailRegister.java b/src/main/java/fr/xephi/authme/process/register/ProcessSyncEmailRegister.java index 88397e9d..84d767ae 100644 --- a/src/main/java/fr/xephi/authme/process/register/ProcessSyncEmailRegister.java +++ b/src/main/java/fr/xephi/authme/process/register/ProcessSyncEmailRegister.java @@ -37,7 +37,7 @@ public class ProcessSyncEmailRegister implements SynchronousProcess { @Inject private AuthMe authMe; - public ProcessSyncEmailRegister() { } + ProcessSyncEmailRegister() { } public void processEmailRegister(Player player) { final String name = player.getName().toLowerCase(); diff --git a/src/main/java/fr/xephi/authme/security/PasswordSecurity.java b/src/main/java/fr/xephi/authme/security/PasswordSecurity.java index 18620426..6e6eac83 100644 --- a/src/main/java/fr/xephi/authme/security/PasswordSecurity.java +++ b/src/main/java/fr/xephi/authme/security/PasswordSecurity.java @@ -151,7 +151,7 @@ public class PasswordSecurity implements Reloadable { * * @return The associated encryption method, or null if CUSTOM / deprecated */ - public EncryptionMethod initializeEncryptionMethod(HashAlgorithm algorithm) { + private EncryptionMethod initializeEncryptionMethod(HashAlgorithm algorithm) { if (HashAlgorithm.CUSTOM.equals(algorithm) || HashAlgorithm.PLAINTEXT.equals(algorithm)) { return null; } diff --git a/src/main/java/fr/xephi/authme/settings/Settings.java b/src/main/java/fr/xephi/authme/settings/Settings.java index e66fa2e0..af021556 100644 --- a/src/main/java/fr/xephi/authme/settings/Settings.java +++ b/src/main/java/fr/xephi/authme/settings/Settings.java @@ -1,7 +1,6 @@ package fr.xephi.authme.settings; import fr.xephi.authme.AuthMe; -import fr.xephi.authme.security.HashAlgorithm; import fr.xephi.authme.settings.domain.Property; import fr.xephi.authme.settings.properties.HooksSettings; import fr.xephi.authme.settings.properties.PluginSettings; @@ -11,37 +10,45 @@ import fr.xephi.authme.settings.properties.SecuritySettings; import org.bukkit.configuration.file.FileConfiguration; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; -import java.util.regex.Pattern; /** * Old settings manager. See {@link NewSetting} for the new manager. */ public final class Settings { - public static List allowCommands; public static List getUnrestrictedName; public static List getForcedWorlds; - public static List countries; - public static List countriesBlacklist; - public static HashAlgorithm getPasswordHash; - public static Pattern nickPattern; - public static boolean isPermissionCheckEnabled, - isForcedRegistrationEnabled, isTeleportToSpawnEnabled, - isSessionsEnabled, isAllowRestrictedIp, - isForceSingleSessionEnabled, isForceSpawnLocOnJoinEnabled, - isSaveQuitLocationEnabled, protectInventoryBeforeLogInEnabled, - isStopEnabled, reloadSupport, rakamakUseIp, - removePassword, multiverse, bungee, - enableProtection, forceRegLogin, noTeleport, - allowAllCommandsIfRegIsOptional, isRemoveSpeedEnabled; - public static String getNickRegex, getUnloggedinGroup, - unRegisteredGroup, backupWindowsPath, getRegisteredGroup, - rakamakUsers, rakamakUsersIp, defaultWorld, crazyloginFileName; - public static int getSessionTimeout, getMaxNickLength, getMinNickLength, - getNonActivatedGroup, maxLoginTry, captchaLength, getMaxLoginPerIp; - protected static FileConfiguration configFile; + public static boolean isPermissionCheckEnabled; + public static boolean isForcedRegistrationEnabled; + public static boolean isTeleportToSpawnEnabled; + public static boolean isSessionsEnabled; + public static boolean isAllowRestrictedIp; + public static boolean isForceSpawnLocOnJoinEnabled; + public static boolean isSaveQuitLocationEnabled; + public static boolean protectInventoryBeforeLogInEnabled; + public static boolean isStopEnabled; + public static boolean reloadSupport; + public static boolean rakamakUseIp; + public static boolean removePassword; + public static boolean multiverse; + public static boolean bungee; + public static boolean forceRegLogin; + public static boolean noTeleport; + public static boolean isRemoveSpeedEnabled; + public static String getUnloggedinGroup; + public static String unRegisteredGroup; + public static String getRegisteredGroup; + public static String rakamakUsers; + public static String rakamakUsersIp; + public static String defaultWorld; + public static String crazyloginFileName; + public static int getSessionTimeout; + public static int getNonActivatedGroup; + public static int maxLoginTry; + public static int captchaLength; + public static int getMaxLoginPerIp; + private static FileConfiguration configFile; /** * Constructor for Settings. @@ -59,16 +66,10 @@ public final class Settings { isTeleportToSpawnEnabled = load(RestrictionSettings.TELEPORT_UNAUTHED_TO_SPAWN); isSessionsEnabled = load(PluginSettings.SESSIONS_ENABLED); getSessionTimeout = configFile.getInt("settings.sessions.timeout", 10); - getMaxNickLength = configFile.getInt("settings.restrictions.maxNicknameLength", 20); - getMinNickLength = configFile.getInt("settings.restrictions.minNicknameLength", 3); - getNickRegex = load(RestrictionSettings.ALLOWED_NICKNAME_CHARACTERS); - nickPattern = Pattern.compile(getNickRegex); isAllowRestrictedIp = load(RestrictionSettings.ENABLE_RESTRICTED_USERS); isRemoveSpeedEnabled = load(RestrictionSettings.REMOVE_SPEED); - isForceSingleSessionEnabled = load(RestrictionSettings.FORCE_SINGLE_SESSION); isForceSpawnLocOnJoinEnabled = load(RestrictionSettings.FORCE_SPAWN_LOCATION_AFTER_LOGIN); isSaveQuitLocationEnabled = configFile.getBoolean("settings.restrictions.SaveQuitLocation", false); - getPasswordHash = load(SecuritySettings.PASSWORD_HASH); getUnloggedinGroup = load(SecuritySettings.UNLOGGEDIN_GROUP); getNonActivatedGroup = configFile.getInt("ExternalBoardOptions.nonActivedUserGroup", -1); unRegisteredGroup = configFile.getString("GroupOptions.UnregisteredPlayerGroup", ""); @@ -80,20 +81,9 @@ public final class Settings { getRegisteredGroup = configFile.getString("GroupOptions.RegisteredPlayerGroup", ""); protectInventoryBeforeLogInEnabled = load(RestrictionSettings.PROTECT_INVENTORY_BEFORE_LOGIN); - backupWindowsPath = configFile.getString("BackupSystem.MysqlWindowsPath", "C:\\Program Files\\MySQL\\MySQL Server 5.1\\"); isStopEnabled = configFile.getBoolean("Security.SQLProblem.stopServer", true); reloadSupport = configFile.getBoolean("Security.ReloadCommand.useReloadCommandSupport", true); - allowAllCommandsIfRegIsOptional = load(RestrictionSettings.ALLOW_ALL_COMMANDS_IF_REGISTRATION_IS_OPTIONAL); - allowCommands = new ArrayList<>(); - allowCommands.addAll(Arrays.asList("/login", "/l", "/register", "/reg", "/email", "/captcha")); - for (String cmd : configFile.getStringList("settings.restrictions.allowCommands")) { - cmd = cmd.toLowerCase(); - if (!allowCommands.contains(cmd)) { - allowCommands.add(cmd); - } - } - rakamakUsers = configFile.getString("Converter.Rakamak.fileName", "users.rak"); rakamakUsersIp = configFile.getString("Converter.Rakamak.ipFileName", "UsersIp.rak"); rakamakUseIp = configFile.getBoolean("Converter.Rakamak.useIp", false); @@ -104,9 +94,6 @@ public final class Settings { bungee = load(HooksSettings.BUNGEECORD); getForcedWorlds = load(RestrictionSettings.FORCE_SPAWN_ON_WORLDS); defaultWorld = configFile.getString("Purge.defaultWorld", "world"); - enableProtection = configFile.getBoolean("Protection.enableProtection", false); - countries = configFile.getStringList("Protection.countries"); - countriesBlacklist = configFile.getStringList("Protection.countriesBlacklist"); forceRegLogin = load(RegistrationSettings.FORCE_LOGIN_AFTER_REGISTER); getMaxLoginPerIp = load(RestrictionSettings.MAX_LOGIN_PER_IP); noTeleport = load(RestrictionSettings.NO_TELEPORT); diff --git a/src/main/java/fr/xephi/authme/settings/SpawnLoader.java b/src/main/java/fr/xephi/authme/settings/SpawnLoader.java index b2d366ae..012eb053 100644 --- a/src/main/java/fr/xephi/authme/settings/SpawnLoader.java +++ b/src/main/java/fr/xephi/authme/settings/SpawnLoader.java @@ -201,7 +201,6 @@ public class SpawnLoader implements SettingsDependent { } private boolean saveAuthMeConfig() { - // TODO ljacqu 20160312: Investigate whether this utility should be put in a Utils class try { authMeConfiguration.save(authMeConfigurationFile); return true; From 3643b8879819211b586f9260b11d14fe6ef1460f Mon Sep 17 00:00:00 2001 From: ljacqu Date: Wed, 25 May 2016 17:37:15 +0200 Subject: [PATCH 102/200] #712 Add changes to fr messages by Jeje2201 --- src/main/resources/messages/messages_fr.yml | 68 ++++++++++----------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/src/main/resources/messages/messages_fr.yml b/src/main/resources/messages/messages_fr.yml index 78bb2bc5..6c7e45b9 100644 --- a/src/main/resources/messages/messages_fr.yml +++ b/src/main/resources/messages/messages_fr.yml @@ -1,66 +1,66 @@ # Traduction par: André -unknown_user: '&fUtilisateur non enregistré' -unsafe_spawn: '&fTéléportation dans un endroit sûr' -not_logged_in: '&cNon connecté!' +unknown_user: '&fUtilisateur non enregistré.' +unsafe_spawn: '&fTéléportation dans un endroit sûr.' +not_logged_in: '&cNon connecté !' reg_voluntarily: '&fVous venez d''arriver? Faites un "/register motdepasse confirmermotdepasse"' usage_log: '&cUtilisez: /login motdepasse' -wrong_pwd: '&cMauvais MotdePasse' -unregistered: '&cCe compte a été supprimé!' -reg_disabled: '&cL''enregistrement est désactivé' -valid_session: '&cVous êtes authentifié' -login: '&cConnexion effectuée!' -vb_nonActiv: '&fCe compte n''est pas actif, consultez vos emails!' -user_regged: '&cCe nom est deja utilisé.' +wrong_pwd: '&cMauvais mot de passe.' +unregistered: '&cCe compte a été supprimé !' +reg_disabled: '&cL''enregistrement est désactivé.' +valid_session: '&aVous êtes authentifié !' +login: '&aConnexion effectuée !' +vb_nonActiv: '&fCe compte n''est pas actif, consultez vos emails !' +user_regged: '&cCe nom est déjà utilisé.' usage_reg: '&cUtilisez la commande /register motdepasse confirmermotdepasse' -# TODO max_reg: Missing tags %reg_count, %reg_names, %max_acc +# TODO max_reg: Missing tags %reg_count, %max_acc, %reg_names max_reg: '&fLimite d''enregistrement atteinte pour ce compte' -no_perm: '&cVous n''avez pas la permission' -password_error_nick: '&fYou can''t use your name as password' -password_error_unsafe: '&fYou can''t use unsafe passwords' -error: '&fUne erreur est apparue, veuillez contacter un administrateur' +no_perm: '&cVous n''avez pas la permission.' +password_error_nick: '&fTu ne peux pas utiliser ton pseudo comme mot de passe.' +password_error_unsafe: '&fCe mot de passe n''est pas accepté, choisis en un autre.' +error: '&fUne erreur est apparue, veuillez contacter un administrateur.' login_msg: '&cPour vous connecter, utilisez: /login motdepasse' reg_msg: '&cPour vous inscrire, utilisez "/register motdepasse confirmermotdepasse"' reg_email_msg: '&cPour vous inscrire, utilisez "/register "' usage_unreg: '&cPour supprimer ce compte, utilisez: /unregister password' -pwd_changed: '&cMotdePasse changé avec succès!' -user_unknown: '&c Ce compte n''est pas enregistré' -password_error: '&fCe mot de passe est incorrect' -invalid_session: '&fSession invalide, relancez le jeu ou attendez la fin de la session' +pwd_changed: '&aMot de passe changé avec succès !' +user_unknown: '&cCe compte n''est pas enregistré.' +password_error: '&cCe mot de passe est incorrect.' +invalid_session: '&fSession invalide, relancez le jeu ou attendez la fin de la session.' reg_only: '&fSeul les joueurs enregistrés sont admis! Visitez http://example.com' -logged_in: '&cVous êtes déjà connecté!' -logout: '&cVous avez été déconnecté!' +logged_in: '&cVous êtes déjà connecté !' +logout: '&cVous avez été déconnecté !' same_nick: '&fUne personne ayant ce même pseudo joue déjà.' -registered: '&cEnregistrement réussi avec succès!' +registered: '&aEnregistrement réussi avec succès !' pass_len: '&fVotre mot de passe n''est pas assez long.' reload: '&fConfiguration et BDD relancé avec succès' timeout: '&fVous avez été expulsé car vous êtes trop lent pour vous enregistrer !' usage_changepassword: '&fPour changer de mot de passe, utilisez: /changepassword ancienmdp nouveaumdp' -name_len: '&cVotre pseudo est trop long ou trop court' +name_len: '&cVotre pseudo est trop long ou trop court.' regex: '&cCaractères autorisés: REG_EX' add_email: '&cMerci d''ajouter votre email : /email add yourEmail confirmEmail' -recovery_email: '&cVous avez oublié votre MotdePasse? Utilisez /email recovery ' +recovery_email: '&cVous avez oublié votre MotdePasse ? Utilisez /email recovery ' usage_captcha: '&cTrop de tentatives de connexion échouées, utilisez: /captcha ' wrong_captcha: '&cCaptcha incorrect, écrivez de nouveau : /captcha THE_CAPTCHA' -valid_captcha: '&cLe Captcha est valide, merci!' -kick_forvip: '&cUn joueur VIP a rejoint le serveur plein!' -kick_fullserver: '&cLe serveur est actuellement plein, désolé!' +valid_captcha: '&aLe Captcha est valide, merci !' +kick_forvip: '&cUn joueur VIP a rejoint le serveur plein !' +kick_fullserver: '&cLe serveur est actuellement plein, désolé !' usage_email_add: '&fUsage: /email add ' usage_email_change: '&fUsage: /email change ' usage_email_recovery: '&fUsage: /email recovery ' -new_email_invalid: '[AuthMe] Nouvel email invalide!' -old_email_invalid: '[AuthMe] Ancien email invalide!' +new_email_invalid: '[AuthMe] Nouvel email invalide !' +old_email_invalid: '[AuthMe] Ancien email invalide !' email_invalid: '[AuthMe] Email invalide' email_added: '[AuthMe] Email ajouté !' email_confirm: '[AuthMe] Confirmez votre email !' email_changed: '[AuthMe] Email changé !' -email_send: '[AuthMe] Email de récupération envoyé!' -country_banned: 'Votre pays est banni de ce serveur' -antibot_auto_enabled: '[AuthMe] AntiBotMod a été activé automatiquement à cause de nombreuses connexions!' -antibot_auto_disabled: '[AuthMe] AntiBotMod a été désactivé automatiquement après %m minutes, espérons que l''invasion soit arrêtée!' +email_send: '[AuthMe] Email de récupération envoyé !' +country_banned: 'Votre pays est banni de ce serveur.' +antibot_auto_enabled: '[AuthMe] AntiBotMod a été activé automatiquement à cause de nombreuses connexions !' +antibot_auto_disabled: '[AuthMe] AntiBotMod a été désactivé automatiquement après %m minutes, espérons que l''invasion soit arrêtée !' kick_antibot: 'AntiBotMod est activé ! Veuillez attendre quelques minutes avant de joindre le serveur.' email_exists: '&cUn email de restauration a déjà été envoyé ! Vous pouvez le jeter et vous en faire envoyez un nouveau en utilisant :' two_factor_create: '&2Votre code secret est %code. Vous pouvez le scanner depuis %url' -email_already_used: '&4L''adresse email a déjà été utilisée' +email_already_used: '&4L''adresse email a déjà été utilisée.' not_owner_error: 'Vous n''êtes pas le propriétaire de ce compte. Veuillez utiliser un autre nom !' invalid_name_case: 'Veuillez vous connecter avec %valid et non pas avec %invalid.' denied_chat: 'Vous devez être connecté pour pouvoir parler !' From 67511e3b45023e8d102e5360834a3d45372513a0 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Wed, 25 May 2016 19:24:12 +0200 Subject: [PATCH 103/200] Create task which draws AuthMe dependency graph --- .../tools/dependencygraph/DrawDependency.java | 173 ++++++++++++++++++ .../graph_stripped4times_20160525.dot | 32 ++++ .../graph_stripped4times_20160525.png | Bin 0 -> 127561 bytes 3 files changed, 205 insertions(+) create mode 100644 src/test/java/tools/dependencygraph/DrawDependency.java create mode 100644 src/test/java/tools/dependencygraph/graph_stripped4times_20160525.dot create mode 100644 src/test/java/tools/dependencygraph/graph_stripped4times_20160525.png diff --git a/src/test/java/tools/dependencygraph/DrawDependency.java b/src/test/java/tools/dependencygraph/DrawDependency.java new file mode 100644 index 00000000..6da9e7cb --- /dev/null +++ b/src/test/java/tools/dependencygraph/DrawDependency.java @@ -0,0 +1,173 @@ +package tools.dependencygraph; + +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.Multimap; +import fr.xephi.authme.initialization.ConstructorInjection; +import fr.xephi.authme.initialization.FieldInjection; +import fr.xephi.authme.initialization.Injection; +import tools.utils.ToolTask; +import tools.utils.ToolsConstants; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Scanner; + +/** + * Creates a DOT file of the dependencies in AuthMe classes. + */ +public class DrawDependency implements ToolTask { + + private static final String DOT_FILE = ToolsConstants.TOOLS_SOURCE_ROOT + "dependencygraph/graph.dot"; + // Package root + private static final String ROOT_PACKAGE = "fr.xephi.authme"; + + // Map with the graph's nodes: value is one of the key's dependencies + private Multimap, String> foundDependencies = ArrayListMultimap.create(); + + @Override + public String getTaskName() { + return "drawDependencyGraph"; + } + + @Override + public void execute(Scanner scanner) { + // Gather all connections + readAndProcessFiles(new File(ToolsConstants.MAIN_SOURCE_ROOT)); + + // Prompt user for simplification of graph + System.out.println("Do you want to remove classes that are not used as dependency elsewhere?"); + System.out.println("Specify the number of times to do this: [0=keep all]"); + int stripVerticesCount; + try { + stripVerticesCount = Integer.valueOf(scanner.nextLine()); + } catch (NumberFormatException e) { + stripVerticesCount = 0; + } + + // Perform simplification as per user's wish + for (int i = 0; i < stripVerticesCount; ++i) { + stripNodesWithNoOutgoingEdges(); + } + + // Create dot file content + final String pattern = "\t\"%s\" -> \"%s\";"; + String dotFile = ""; + for (Map.Entry, String> entry : foundDependencies.entries()) { + dotFile += "\n" + String.format(pattern, entry.getValue(), entry.getKey().getSimpleName()); + } + + // Write dot file + try { + Files.write(Paths.get(DOT_FILE), ("digraph G {\n" + dotFile + "\n}").getBytes()); + } catch (IOException e) { + throw new IllegalStateException(e); + } + System.out.println("Graph file written"); + System.out.format("Run 'dot -Tpng %s -o graph.png' to generate image (requires GraphViz)%n", DOT_FILE); + } + + /** + * Recursively reads the given directory and processes the files. + * + * @param dir the directory to read + */ + private void readAndProcessFiles(File dir) { + File[] files = dir.listFiles(); + if (files == null) { + throw new IllegalStateException("Cannot read folder '" + dir + "'"); + } + for (File file : files) { + if (file.isDirectory()) { + readAndProcessFiles(file); + } else if (file.isFile()) { + Class clazz = loadClass(file); + if (clazz != null) { + processClass(clazz); + } + } + } + } + + private void processClass(Class clazz) { + List dependencies = getDependencies(clazz); + if (dependencies != null) { + foundDependencies.putAll(clazz, dependencies); + } + } + + // Load Class object for the class in the given file + private static Class loadClass(File file) { + final String fileName = file.getPath().replace(File.separator, "."); + if (!fileName.endsWith(".java")) { + return null; + } + final String className = fileName + .substring(fileName.indexOf(ROOT_PACKAGE), fileName.length() - ".java".length()); + try { + return DrawDependency.class.getClassLoader().loadClass(className); + } catch (ClassNotFoundException e) { + throw new IllegalStateException(e); + } + } + + private static List getDependencies(Class clazz) { + Injection injection = ConstructorInjection.provide(clazz).get(); + if (injection != null) { + return formatInjectionDependencies(injection); + } + injection = FieldInjection.provide(clazz).get(); + return injection == null ? null : formatInjectionDependencies(injection); + } + + /** + * Formats the dependencies returned by the given injection appropriately: + * if the dependency has an annotation, the annotation will be returned; + * otherwise the class name. + * + * @param injection the injection whose dependencies should be formatted + * @return list of dependencies in a friendly format + */ + private static List formatInjectionDependencies(Injection injection) { + Class[] dependencies = injection.getDependencies(); + Class[] annotations = injection.getDependencyAnnotations(); + + List result = new ArrayList<>(dependencies.length); + for (int i = 0; i < dependencies.length; ++i) { + if (annotations[i] != null) { + result.add("@" + annotations[i].getSimpleName()); + } else { + result.add(dependencies[i].getSimpleName()); + } + } + return result; + } + + /** + * Removes all vertices in the graph that have no outgoing edges, i.e. all classes + * in the graph that only receive dependencies but are not used as a dependency anywhere. + * This process can be repeated multiple times. + */ + private void stripNodesWithNoOutgoingEdges() { + Map dependencies = new HashMap<>(); + for (Map.Entry, String> entry : foundDependencies.entries()) { + final String className = entry.getKey().getSimpleName(); + dependencies.putIfAbsent(className, Boolean.FALSE); + dependencies.put(entry.getValue(), Boolean.TRUE); + } + + Iterator> it = foundDependencies.keys().iterator(); + while (it.hasNext()) { + Class clazz = it.next(); + if (Boolean.FALSE.equals(dependencies.get(clazz.getSimpleName()))) { + it.remove(); + } + } + } +} diff --git a/src/test/java/tools/dependencygraph/graph_stripped4times_20160525.dot b/src/test/java/tools/dependencygraph/graph_stripped4times_20160525.dot new file mode 100644 index 00000000..139c2bfb --- /dev/null +++ b/src/test/java/tools/dependencygraph/graph_stripped4times_20160525.dot @@ -0,0 +1,32 @@ +digraph G { + + "ProcessService" -> "ProcessSyncEmailRegister"; + "LimboCache" -> "ProcessSyncEmailRegister"; + "BukkitService" -> "ProcessSyncEmailRegister"; + "AuthMe" -> "ProcessSyncEmailRegister"; + "AuthMe" -> "ProcessSyncPlayerLogin"; + "ProcessService" -> "ProcessSyncPlayerLogin"; + "LimboCache" -> "ProcessSyncPlayerLogin"; + "DataSource" -> "ProcessSyncPlayerLogin"; + "BukkitService" -> "ProcessSyncPlayerLogin"; + "PluginManager" -> "ProcessSyncPlayerLogin"; + "AuthMe" -> "ProcessSyncPasswordRegister"; + "ProcessService" -> "ProcessSyncPasswordRegister"; + "BukkitService" -> "ProcessSyncPasswordRegister"; + "AuthMe" -> "ProcessSynchronousPlayerLogout"; + "ProcessService" -> "ProcessSynchronousPlayerLogout"; + "LimboCache" -> "ProcessSynchronousPlayerLogout"; + "BukkitService" -> "ProcessSynchronousPlayerLogout"; + "AuthMe" -> "BukkitService"; + "NewSetting" -> "ValidationService"; + "DataSource" -> "ValidationService"; + "PermissionsManager" -> "ValidationService"; + "Server" -> "PermissionsManager"; + "PluginManager" -> "PermissionsManager"; + "PluginManager" -> "PluginHooks"; + "NewSetting" -> "ProcessService"; + "Messages" -> "ProcessService"; + "PluginManager" -> "ProcessService"; + "ValidationService" -> "ProcessService"; + "PermissionsManager" -> "ProcessService"; +} \ No newline at end of file diff --git a/src/test/java/tools/dependencygraph/graph_stripped4times_20160525.png b/src/test/java/tools/dependencygraph/graph_stripped4times_20160525.png new file mode 100644 index 0000000000000000000000000000000000000000..537d608e72212eaa78ab461de3a439f978b03897 GIT binary patch literal 127561 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%0DtPF70yxOiQsq6su$*Ul%0h%i0fg*@9I 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 YqQl(`MbW~_RQS(cZ9}a>4XdF42Uki)-~a#s literal 0 HcmV?d00001 From 41bad6189eb777cbf2950afd62619449f9fac42f Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Wed, 25 May 2016 19:52:21 +0200 Subject: [PATCH 104/200] Update messages_tr.yml #621 --- src/main/resources/messages/messages_tr.yml | 129 ++++++++++---------- 1 file changed, 63 insertions(+), 66 deletions(-) diff --git a/src/main/resources/messages/messages_tr.yml b/src/main/resources/messages/messages_tr.yml index d6fc40f4..c8f8135b 100644 --- a/src/main/resources/messages/messages_tr.yml +++ b/src/main/resources/messages/messages_tr.yml @@ -1,66 +1,63 @@ -unknown_user: '&fKullanici veritabanina ekli degil' -unsafe_spawn: '&fDogdugunuz konum guvenli degildi, lobiye isinlaniyorsunuz...' -not_logged_in: '&cGiris Yapmadin!' -reg_voluntarily: '&fKullanici adinla kayit olabilirsin! Komut: "/register sifren sifrentekrar"' -usage_log: '&cKullanimi: /login sifren' -wrong_pwd: '&cYanlis sifre' -unregistered: '&cSunucudan kaydiniz basariyla silindi!' -reg_disabled: '&cKayit deaktif' -valid_session: '&cOturum Acma' -login: '&cBasarili giris!' -vb_nonActiv: '&fHesabin aktiflestirilmedi! Emailini kontrol et' -user_regged: '&cKullanici zaten oyunda' -usage_reg: '&cKullanimi: /register sifre sifretekrar' -# TODO max_reg: Missing tags %reg_count, %reg_names, %max_acc -max_reg: '&fMaximim kayit limitini astin!' -no_perm: '&cYetkin yok' -error: '&fBir hata olustu; Lutfen adminle iletisime gec' -login_msg: '&cGiris Yapin : "/login sifre"' -reg_msg: '&cLutfen kaydolmak icin : "/register sifre sifretekrar"' -reg_email_msg: '&cLutfen Kaydolmak icin : "/register "' -usage_unreg: '&cKullanimi: /unregister sifren' -pwd_changed: '&cSifreniz degisti!' -user_unknown: '&cBu kullaniciyla kaydolunmamis!' -password_error: '&fSifren eslesmiyor' -invalid_session: '&fOturum veritabanlari uyusmuyor lutfen sonunu bekleyin' -reg_only: '&fSadece kayitli uyeler girebilir ! Kayit olmak icin www.orneksite.com adresini ziyaret ediniz !' -logged_in: '&cZaten Giris Yapilmis!' -logout: '&cBasarili cikis' -same_nick: '&fAyni kullanici oyunda' -registered: '&cBasarili kayit!' -pass_len: '&fSifren cok uzun ya da kisa olmamali ' -reload: '&fKonfigurasyon dosyasi ve veritabani yüklendi' -timeout: '&fZaman Asimi' -usage_changepassword: '&fkullanimi: /changepassword eskisifre yenisifre' -name_len: '&cKullanici adin cok kisa ya da cok uzun' -regex: '&cKullanici adin ozel karakterler iceriyor. Uygun karakterler: REG_EX' -add_email: '&cLutfen emailini ekle : /email add ' -recovery_email: '&cSifreni mi unuttun? Degistirmek icin : /email recovery ' -usage_captcha: '&cBir captcha yazman lazim , yazmak icin: /captcha ' -wrong_captcha: '&cYanlis Captcha, kullanmak icin : /captcha THE_CAPTCHA' -valid_captcha: '&cCaptcha gecerli !' -kick_forvip: '&cSenin yerine bir VIP kullanıcı girdi!' -kick_fullserver: '&cServer suanda dolu gozukuyor, Uzgunum!' -usage_email_add: '&fKullanimi: /email add ' -usage_email_change: '&fKullanimi: /email change ' -usage_email_recovery: '&fKullanimi: /email recovery ' -new_email_invalid: '[AuthMe] Yeni eposta gecersiz!' -old_email_invalid: '[AuthMe] Eski eposta gecersiz!' -email_invalid: '[AuthMe] Gecersiz Eposta' -email_added: '[AuthMe] Eposta Eklendi !' -email_confirm: '[AuthMe] Epostani Dogrula !' -email_changed: '[AuthMe] Eposta Degistirildi !' -email_send: '[AuthMe] Kurtarma postasi gonderildi !' -country_banned: 'Ulken bu serverdan banlandi !' -antibot_auto_enabled: '[AuthMe] AntiBotMode otomatik olarak etkinlestirildi!' -antibot_auto_disabled: '[AuthMe] AntiBotMode %m dakika sonra otomatik olarak isgal yuzundan devredisi birakildi' -# TODO email_already_used: '&4The email address is already being used' -# TODO password_error_nick: '&cYou can''t use your name as password, please choose another one...' -# TODO password_error_unsafe: '&cThe chosen password isn''t safe, please choose another one...' -# TODO kick_antibot: 'AntiBot protection mode is enabled! You have to wait some minutes before joining the server.' -# TODO denied_chat: '&cIn order to be able to chat you must be authenticated!' -# TODO email_exists: '&cA recovery email was already sent! You can discard it and send a new one using the command below:' -# TODO same_ip_online: 'A player with the same IP is already in game!' -# TODO invalid_name_case: 'You should join using username %valid, not %invalid.' -# TODO two_factor_create: '&2Your secret code is %code. You can scan it from here %url' -# TODO not_owner_error: 'You are not the owner of this account. Please choose another name!' \ No newline at end of file +kick_antibot: 'AntiBot koruma modu aktif! Birkac dakika sonra tekrar girmeyi deneyin.' +unknown_user: '&cVeritabaninda kullanici bulunamadi!' +unsafe_spawn: '&cOyundan ciktigin yer guvenli degil, baslangic noktasina isinlaniyorsun.' +not_logged_in: '&cGiris yapmadin!' +reg_voluntarily: 'Kendini sunucuya kaydetmek icin komut kullan, "/register "' +usage_log: '&cKullanim: /login ' +wrong_pwd: '&cYanlis sifre!' +unregistered: '&cKayit basariyla kaldirildi!' +reg_disabled: '&cOyun icin kayit olma kapatildi!' +valid_session: '&2Oturum icin yeniden giris gerekiyor.' +login: '&2Giris basarili!' +vb_nonActiv: '&cHeabiniz henuz aktif edilmemis, e-postanizi kontrol edin!' +user_regged: '&cSenin adinda daha once birisi kaydolmus!' +usage_reg: '&cKullanim: /register ' +max_reg: '&cSen maksimum kayit sinirini astin (%reg_count/%max_acc %reg_names)!' +no_perm: '&4Bunu yapmak icin iznin yok!' +error: '&4Beklenmedik bir hata olustu, yetkili ile iletisime gecin!' +login_msg: '&cLutfen giris komutunu kullanin "/login "' +reg_msg: '&3Lutfen kayit komutunu kullanin "/register "' +reg_email_msg: '&3Lutfen kayit komutunu kullanin "/register "' +usage_unreg: '&cKullanim: /unregister ' +pwd_changed: '&2Sifre basariyla degistirildi!' +user_unknown: '&cBu oyuncu kayitli degil!' +password_error: '&cSifre eslesmiyor, tekrar deneyin!' +password_error_nick: '&cSifrenize adinizi koyamazsiniz, lutfen farkli bir sifre secin...' +password_error_unsafe: '&cSectiginiz sifre guvenli degil, lutfen farkli bir sifre secin...' +invalid_session: '&cIP adresin degistirildi ve oturum suren doldu!' +reg_only: '&4Sunucuya kayit sadece internet uzerinden yapilmakta! Lutfen http://ornek.com sitesini kayit icin ziyaret edin!' +logged_in: '&cZaten giris yaptin!' +logout: '&2Basariyla cikis yaptin!' +same_nick: '&4Senin isminde bir oyuncu suncuda bulunmakta!' +registered: '&2Basariyla kaydoldun!' +pass_len: '&cSenin sifren ya cok kisa yada cok uzun! Lutfen farkli birsey dene!' +reload: '&2Ayarlar ve veritabani yenilendi!' +timeout: '&4Giris izni icin verilen zaman suresini astigin icin sunucudan atildin, tekrar deneyin!' +usage_changepassword: '&cKullanim: /changepassword ' +name_len: '&4Senin ismin ya cok kisa yada cok uzun!' +regex: '&4Senin isminde uygunsuz karakterler bulunmakta. Izin verilen karakterler: REG_EX' +add_email: '&3Lutfen hesabinize eposta adresinizi komut ile ekleyin "/email add "' +recovery_email: '&3Sifreni mi unuttun ? Komut kullanarak ogrenebilirsin "/email recovery "' +usage_captcha: '&3Giris yapmak icin guvenlik kodunu komut yazarak girin "/captcha "' +wrong_captcha: '&cYanlis guvenlik kodu, kullanim sekli "/captcha THE_CAPTCHA" sohbete yazin!' +valid_captcha: '&2Guvenlik kodu dogrulandi!' +kick_forvip: '&3Bir VIP oyuna giris yaptigi icin atildin!' +kick_fullserver: '&4Sunucu suanda dolu, daha sonra tekrar deneyin!' +usage_email_add: '&cKullanim: /email add ' +usage_email_change: '&cKullanim: /email change ' +usage_email_recovery: '&cKullanim: /email recovery ' +new_email_invalid: '&cGecersiz yeni eposta, tekrar deneyin!' +old_email_invalid: '&cGecersiz eski eposta, tekrar deneyin!' +email_invalid: '&cGecersiz eposta, tekrar deneyin!' +email_added: '&2Eposta basariyla kullaniciniza eklendi!' +email_confirm: '&cLutfen tekrar epostanizi giriniz!' +email_changed: '&2Epostaniz basariyla degistirildi!' +email_send: '&2Sifreniz epostaniza gonderildi! Lutfen eposta kutunuzu kontrol edin!' +email_exists: '&cSifreniz zaten epostanize gonderildi! Bunu iptal etmek veya yeni bir sifre gondermek icin assagidaki komutu kullanabilirsin:' +country_banned: '&4Senin bolgen sunucudan yasaklandi!' +antibot_auto_enabled: '&4[AntiBotServis] Saldiri oldugu icin AntiBot aktif edildi!' +antibot_auto_disabled: '&2[AntiBotServis] AntiBot, %m dakika sonra deaktif edilecek!' +email_already_used: '&4Eposta adresi zaten kullaniliyor.' +two_factor_create: '&2Gizli kodunuz %code. Buradan test edebilirsin, %url' +not_owner_error: 'Bu hesabin sahibi degilsin. Lutfen farkli bir isim sec!' +invalid_name_case: 'Oyuna %valid isminde katilmalisin. %invalid ismini kullanarak katilamazsin.' From 466fb8cab99d2dd115c5afada39b3e4ed33d9cfc Mon Sep 17 00:00:00 2001 From: games647 Date: Thu, 26 May 2016 10:52:49 +0200 Subject: [PATCH 105/200] Run explicit non-thread-safe setGameMode sync --- .../fr/xephi/authme/process/join/AsynchronousJoin.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java b/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java index 245380ed..478dcda6 100644 --- a/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java +++ b/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java @@ -89,7 +89,12 @@ public class AsynchronousJoin implements AsynchronousProcess { if (service.getProperty(RestrictionSettings.FORCE_SURVIVAL_MODE) && !service.hasPermission(player, PlayerStatePermission.BYPASS_FORCE_SURVIVAL)) { - player.setGameMode(GameMode.SURVIVAL); + bukkitService.runTask(new Runnable() { + @Override + public void run() { + player.setGameMode(GameMode.SURVIVAL); + } + }); } if (service.getProperty(HooksSettings.DISABLE_SOCIAL_SPY)) { From 87331d116c88e6d161578caeeeeaf2866e654f97 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Fri, 27 May 2016 23:00:44 +0200 Subject: [PATCH 106/200] Revise converter architecture + add integration test for CrazyLogin converter --- .../executable/authme/ConverterCommand.java | 66 ++++++------- .../fr/xephi/authme/converter/Converter.java | 13 ++- .../authme/converter/CrazyLoginConverter.java | 54 +++++------ .../authme/converter/ForceFlatToSqlite.java | 4 +- .../authme/converter/RakamakConverter.java | 24 +++-- .../authme/converter/RoyalAuthConverter.java | 9 +- .../xephi/authme/converter/SqliteToSql.java | 29 +++--- .../authme/converter/vAuthConverter.java | 9 +- .../authme/converter/xAuthConverter.java | 9 +- .../xephi/authme/util/MigrationService.java | 2 +- .../converter/CrazyLoginConverterTest.java | 95 +++++++++++++++++++ .../converter/ForceFlatToSqliteTest.java | 2 +- src/test/resources/converter/crazylogin.db | 3 + 13 files changed, 217 insertions(+), 102 deletions(-) create mode 100644 src/test/java/fr/xephi/authme/converter/CrazyLoginConverterTest.java create mode 100644 src/test/resources/converter/crazylogin.db diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/ConverterCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/ConverterCommand.java index 26a1d7ee..4a02cfdc 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/ConverterCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/ConverterCommand.java @@ -10,6 +10,7 @@ import fr.xephi.authme.converter.RoyalAuthConverter; import fr.xephi.authme.converter.SqliteToSql; import fr.xephi.authme.converter.vAuthConverter; import fr.xephi.authme.converter.xAuthConverter; +import fr.xephi.authme.initialization.AuthMeServiceInitializer; import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.util.BukkitService; import org.bukkit.command.CommandSender; @@ -17,6 +18,9 @@ import org.bukkit.command.CommandSender; import javax.inject.Inject; import java.util.List; +/** + * Converter command: launches conversion based on its parameters. + */ public class ConverterCommand implements ExecutableCommand { @Inject @@ -25,8 +29,11 @@ public class ConverterCommand implements ExecutableCommand { @Inject private BukkitService bukkitService; + @Inject + private AuthMeServiceInitializer initializer; + @Override - public void executeCommand(CommandSender sender, List arguments, CommandService commandService) { + public void executeCommand(final CommandSender sender, List arguments, CommandService commandService) { // Get the conversion job String job = arguments.get(0); @@ -38,49 +45,34 @@ public class ConverterCommand implements ExecutableCommand { } // Get the proper converter instance - Converter converter = null; - switch (jobType) { - case XAUTH: - converter = new xAuthConverter(authMe, sender); - break; - case CRAZYLOGIN: - converter = new CrazyLoginConverter(authMe, sender); - break; - case RAKAMAK: - converter = new RakamakConverter(authMe, sender); - break; - case ROYALAUTH: - converter = new RoyalAuthConverter(authMe); - break; - case VAUTH: - converter = new vAuthConverter(authMe, sender); - break; - case SQLITETOSQL: - converter = new SqliteToSql(authMe, sender, commandService.getSettings()); - break; - default: - break; - } + final Converter converter = initializer.newInstance(jobType.getConverterClass()); // Run the convert job - bukkitService.runTaskAsynchronously(converter); + bukkitService.runTaskAsynchronously(new Runnable() { + @Override + public void run() { + converter.execute(sender); + } + }); // Show a status message sender.sendMessage("[AuthMe] Successfully converted from " + jobType.getName()); } - public enum ConvertType { - XAUTH("xauth"), - CRAZYLOGIN("crazylogin"), - RAKAMAK("rakamak"), - ROYALAUTH("royalauth"), - VAUTH("vauth"), - SQLITETOSQL("sqlitetosql"); + private enum ConvertType { + XAUTH("xauth", xAuthConverter.class), + CRAZYLOGIN("crazylogin", CrazyLoginConverter.class), + RAKAMAK("rakamak", RakamakConverter.class), + ROYALAUTH("royalauth", RoyalAuthConverter.class), + VAUTH("vauth", vAuthConverter.class), + SQLITETOSQL("sqlitetosql", SqliteToSql.class); - final String name; + private final String name; + private final Class converterClass; - ConvertType(String name) { + ConvertType(String name, Class converterClass) { this.name = name; + this.converterClass = converterClass; } public static ConvertType fromName(String name) { @@ -92,8 +84,12 @@ public class ConverterCommand implements ExecutableCommand { return null; } - String getName() { + public String getName() { return this.name; } + + public Class getConverterClass() { + return converterClass; + } } } diff --git a/src/main/java/fr/xephi/authme/converter/Converter.java b/src/main/java/fr/xephi/authme/converter/Converter.java index 9068f02f..28f18fd9 100644 --- a/src/main/java/fr/xephi/authme/converter/Converter.java +++ b/src/main/java/fr/xephi/authme/converter/Converter.java @@ -1,7 +1,16 @@ package fr.xephi.authme.converter; +import org.bukkit.command.CommandSender; + /** - * Common supertype for AuthMe converters. + * Interface for AuthMe converters. */ -public interface Converter extends Runnable { +public interface Converter { + + /** + * Execute the conversion. + * + * @param sender the sender who initialized the conversion + */ + void execute(CommandSender sender); } diff --git a/src/main/java/fr/xephi/authme/converter/CrazyLoginConverter.java b/src/main/java/fr/xephi/authme/converter/CrazyLoginConverter.java index c617ccad..5a8c547a 100644 --- a/src/main/java/fr/xephi/authme/converter/CrazyLoginConverter.java +++ b/src/main/java/fr/xephi/authme/converter/CrazyLoginConverter.java @@ -1,47 +1,46 @@ package fr.xephi.authme.converter; -import fr.xephi.authme.AuthMe; import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.cache.auth.PlayerAuth; import fr.xephi.authme.datasource.DataSource; -import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.initialization.DataFolder; +import fr.xephi.authme.settings.NewSetting; +import fr.xephi.authme.settings.properties.ConverterSettings; import org.bukkit.command.CommandSender; +import javax.inject.Inject; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.IOException; /** - * @author Xephi59 + * Converter for CrazyLogin to AuthMe. */ public class CrazyLoginConverter implements Converter { private final DataSource database; - private final CommandSender sender; + private final NewSetting settings; + private final File dataFolder; - /** - * Constructor for CrazyLoginConverter. - * - * @param instance AuthMe - * @param sender CommandSender - */ - public CrazyLoginConverter(AuthMe instance, CommandSender sender) { - this.database = instance.getDataSource(); - this.sender = sender; + @Inject + CrazyLoginConverter(@DataFolder File dataFolder, DataSource dataSource, NewSetting settings) { + this.dataFolder = dataFolder; + this.database = dataSource; + this.settings = settings; } @Override - public void run() { - String fileName = Settings.crazyloginFileName; - try { - File source = new File(AuthMe.getInstance().getDataFolder() + File.separator + fileName); - if (!source.exists()) { - sender.sendMessage("Error while trying to import data, please put " + fileName + " in AuthMe folder!"); - return; - } - String line; - BufferedReader users = new BufferedReader(new FileReader(source)); + public void execute(CommandSender sender) { + String fileName = settings.getProperty(ConverterSettings.CRAZYLOGIN_FILE_NAME); + File source = new File(dataFolder, fileName); + if (!source.exists()) { + sender.sendMessage("CrazyLogin file not found, please put " + fileName + " in AuthMe folder!"); + return; + } + + String line; + try (BufferedReader users = new BufferedReader(new FileReader(source))) { while ((line = users.readLine()) != null) { if (line.contains("|")) { String[] args = line.split("\\|"); @@ -49,22 +48,21 @@ public class CrazyLoginConverter implements Converter { continue; } String playerName = args[0]; - String psw = args[1]; - if (psw != null) { + String password = args[1]; + if (password != null) { PlayerAuth auth = PlayerAuth.builder() .name(playerName.toLowerCase()) .realName(playerName) - .password(psw, null) + .password(password, null) .build(); database.saveAuth(auth); } } } - users.close(); ConsoleLogger.info("CrazyLogin database has been imported correctly"); } catch (IOException ex) { - ConsoleLogger.showError(ex.getMessage()); ConsoleLogger.showError("Can't open the crazylogin database file! Does it exist?"); + ConsoleLogger.logException("Encountered", ex); } } diff --git a/src/main/java/fr/xephi/authme/converter/ForceFlatToSqlite.java b/src/main/java/fr/xephi/authme/converter/ForceFlatToSqlite.java index 7919f6a0..f06382a6 100644 --- a/src/main/java/fr/xephi/authme/converter/ForceFlatToSqlite.java +++ b/src/main/java/fr/xephi/authme/converter/ForceFlatToSqlite.java @@ -5,6 +5,7 @@ import fr.xephi.authme.cache.auth.PlayerAuth; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.datasource.FlatFile; import fr.xephi.authme.util.StringUtils; +import org.bukkit.command.CommandSender; import java.util.ArrayList; import java.util.List; @@ -32,7 +33,8 @@ public class ForceFlatToSqlite implements Converter { * Perform the conversion. */ @Override - public void run() { + // Note ljacqu 20160527: CommandSender is null here; it is only present because of the interface it implements + public void execute(CommandSender sender) { List skippedPlayers = new ArrayList<>(); for (PlayerAuth auth : source.getAllAuths()) { if (destination.isAuthAvailable(auth.getNickname())) { diff --git a/src/main/java/fr/xephi/authme/converter/RakamakConverter.java b/src/main/java/fr/xephi/authme/converter/RakamakConverter.java index f1acf300..de6ea77b 100644 --- a/src/main/java/fr/xephi/authme/converter/RakamakConverter.java +++ b/src/main/java/fr/xephi/authme/converter/RakamakConverter.java @@ -4,11 +4,14 @@ import fr.xephi.authme.AuthMe; import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.cache.auth.PlayerAuth; import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.initialization.DataFolder; import fr.xephi.authme.security.PasswordSecurity; import fr.xephi.authme.security.crypts.HashedPassword; -import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.NewSetting; +import fr.xephi.authme.settings.properties.ConverterSettings; import org.bukkit.command.CommandSender; +import javax.inject.Inject; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; @@ -23,22 +26,23 @@ public class RakamakConverter implements Converter { private final AuthMe instance; private final DataSource database; - private final CommandSender sender; + private final NewSetting settings; private final File pluginFolder; - public RakamakConverter(AuthMe instance, CommandSender sender) { + @Inject + RakamakConverter(@DataFolder File dataFolder, AuthMe instance, DataSource dataSource, NewSetting settings) { this.instance = instance; - this.database = instance.getDataSource(); - this.sender = sender; - pluginFolder = instance.getDataFolder(); + this.database = dataSource; + this.settings = settings; + this.pluginFolder = dataFolder; } @Override // TODO ljacqu 20151229: Restructure this into smaller portions - public void run() { - boolean useIP = Settings.rakamakUseIp; - String fileName = Settings.rakamakUsers; - String ipFileName = Settings.rakamakUsersIp; + public void execute(CommandSender sender) { + boolean useIP = settings.getProperty(ConverterSettings.RAKAMAK_USE_IP); + String fileName = settings.getProperty(ConverterSettings.RAKAMAK_FILE_NAME); + String ipFileName = settings.getProperty(ConverterSettings.RAKAMAK_IP_FILE_NAME); File source = new File(pluginFolder, fileName); File ipfiles = new File(pluginFolder, ipFileName); HashMap playerIP = new HashMap<>(); diff --git a/src/main/java/fr/xephi/authme/converter/RoyalAuthConverter.java b/src/main/java/fr/xephi/authme/converter/RoyalAuthConverter.java index b79657e2..49cde259 100644 --- a/src/main/java/fr/xephi/authme/converter/RoyalAuthConverter.java +++ b/src/main/java/fr/xephi/authme/converter/RoyalAuthConverter.java @@ -5,9 +5,11 @@ import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.cache.auth.PlayerAuth; import fr.xephi.authme.datasource.DataSource; import org.bukkit.OfflinePlayer; +import org.bukkit.command.CommandSender; import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.configuration.file.YamlConfiguration; +import javax.inject.Inject; import java.io.File; import static fr.xephi.authme.util.StringUtils.makePath; @@ -19,13 +21,14 @@ public class RoyalAuthConverter implements Converter { private final AuthMe plugin; private final DataSource dataSource; - public RoyalAuthConverter(AuthMe plugin) { + @Inject + RoyalAuthConverter(AuthMe plugin, DataSource dataSource) { this.plugin = plugin; - this.dataSource = plugin.getDataSource(); + this.dataSource = dataSource; } @Override - public void run() { + public void execute(CommandSender sender) { for (OfflinePlayer player : plugin.getServer().getOfflinePlayers()) { try { String name = player.getName().toLowerCase(); diff --git a/src/main/java/fr/xephi/authme/converter/SqliteToSql.java b/src/main/java/fr/xephi/authme/converter/SqliteToSql.java index 0791613e..61168480 100644 --- a/src/main/java/fr/xephi/authme/converter/SqliteToSql.java +++ b/src/main/java/fr/xephi/authme/converter/SqliteToSql.java @@ -1,40 +1,43 @@ package fr.xephi.authme.converter; -import fr.xephi.authme.settings.NewSetting; -import org.bukkit.command.CommandSender; - -import fr.xephi.authme.AuthMe; import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.cache.auth.PlayerAuth; +import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.datasource.DataSourceType; import fr.xephi.authme.datasource.SQLite; import fr.xephi.authme.output.MessageKey; +import fr.xephi.authme.output.Messages; +import fr.xephi.authme.settings.NewSetting; +import org.bukkit.command.CommandSender; + +import javax.inject.Inject; public class SqliteToSql implements Converter { - private final AuthMe plugin; - private final CommandSender sender; private final NewSetting settings; + private final DataSource dataSource; + private final Messages messages; - public SqliteToSql(AuthMe plugin, CommandSender sender, NewSetting settings) { - this.plugin = plugin; - this.sender = sender; + @Inject + SqliteToSql(NewSetting settings, DataSource dataSource, Messages messages) { this.settings = settings; + this.dataSource = dataSource; + this.messages = messages; } @Override - public void run() { - if (plugin.getDataSource().getType() != DataSourceType.MYSQL) { + public void execute(CommandSender sender) { + if (dataSource.getType() != DataSourceType.MYSQL) { sender.sendMessage("Please configure your mySQL connection and re-run this command"); return; } try { SQLite data = new SQLite(settings); for (PlayerAuth auth : data.getAllAuths()) { - plugin.getDataSource().saveAuth(auth); + dataSource.saveAuth(auth); } } catch (Exception e) { - plugin.getMessages().send(sender, MessageKey.ERROR); + messages.send(sender, MessageKey.ERROR); ConsoleLogger.logException("Problem during SQLite to SQL conversion:", e); } } diff --git a/src/main/java/fr/xephi/authme/converter/vAuthConverter.java b/src/main/java/fr/xephi/authme/converter/vAuthConverter.java index 2e3d9746..7c6f3ea5 100644 --- a/src/main/java/fr/xephi/authme/converter/vAuthConverter.java +++ b/src/main/java/fr/xephi/authme/converter/vAuthConverter.java @@ -4,18 +4,19 @@ import fr.xephi.authme.AuthMe; import fr.xephi.authme.ConsoleLogger; import org.bukkit.command.CommandSender; +import javax.inject.Inject; + public class vAuthConverter implements Converter { private final AuthMe plugin; - private final CommandSender sender; - public vAuthConverter(AuthMe plugin, CommandSender sender) { + @Inject + vAuthConverter(AuthMe plugin) { this.plugin = plugin; - this.sender = sender; } @Override - public void run() { + public void execute(CommandSender sender) { try { new vAuthFileReader(plugin).convert(); } catch (Exception e) { diff --git a/src/main/java/fr/xephi/authme/converter/xAuthConverter.java b/src/main/java/fr/xephi/authme/converter/xAuthConverter.java index 696f86fd..6fdddbf7 100644 --- a/src/main/java/fr/xephi/authme/converter/xAuthConverter.java +++ b/src/main/java/fr/xephi/authme/converter/xAuthConverter.java @@ -3,18 +3,19 @@ package fr.xephi.authme.converter; import fr.xephi.authme.AuthMe; import org.bukkit.command.CommandSender; +import javax.inject.Inject; + public class xAuthConverter implements Converter { private final AuthMe plugin; - private final CommandSender sender; - public xAuthConverter(AuthMe plugin, CommandSender sender) { + @Inject + xAuthConverter(AuthMe plugin) { this.plugin = plugin; - this.sender = sender; } @Override - public void run() { + public void execute(CommandSender sender) { try { Class.forName("de.luricos.bukkit.xAuth.xAuth"); xAuthToFlat converter = new xAuthToFlat(plugin, sender); diff --git a/src/main/java/fr/xephi/authme/util/MigrationService.java b/src/main/java/fr/xephi/authme/util/MigrationService.java index cd9bb382..868b7ff8 100644 --- a/src/main/java/fr/xephi/authme/util/MigrationService.java +++ b/src/main/java/fr/xephi/authme/util/MigrationService.java @@ -69,7 +69,7 @@ public final class MigrationService { try { SQLite sqlite = new SQLite(settings); ForceFlatToSqlite converter = new ForceFlatToSqlite(flatFile, sqlite); - converter.run(); + converter.execute(null); settings.setProperty(DatabaseSettings.BACKEND, DataSourceType.SQLITE); settings.save(); return sqlite; diff --git a/src/test/java/fr/xephi/authme/converter/CrazyLoginConverterTest.java b/src/test/java/fr/xephi/authme/converter/CrazyLoginConverterTest.java new file mode 100644 index 00000000..e64fb97c --- /dev/null +++ b/src/test/java/fr/xephi/authme/converter/CrazyLoginConverterTest.java @@ -0,0 +1,95 @@ +package fr.xephi.authme.converter; + +import fr.xephi.authme.TestHelper; +import fr.xephi.authme.cache.auth.PlayerAuth; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.settings.NewSetting; +import fr.xephi.authme.settings.properties.ConverterSettings; +import org.bukkit.command.CommandSender; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import java.io.File; +import java.util.List; + +import static fr.xephi.authme.AuthMeMatchers.equalToHash; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Matchers.argThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; + +/** + * Test for {@link CrazyLoginConverter}. + */ +@RunWith(MockitoJUnitRunner.class) +public class CrazyLoginConverterTest { + + private CrazyLoginConverter crazyLoginConverter; + + @Mock + private DataSource dataSource; + + @Mock + private NewSetting settings; + + private File dataFolder = TestHelper.getJarFile("/converter/"); + + @BeforeClass + public static void initializeLogger() { + TestHelper.setupLogger(); + } + + @Before + public void instantiateConverter() { + crazyLoginConverter = new CrazyLoginConverter(dataFolder, dataSource, settings); + } + + @Test + public void shouldImportUsers() { + // given + given(settings.getProperty(ConverterSettings.CRAZYLOGIN_FILE_NAME)).willReturn("crazylogin.db"); + CommandSender sender = mock(CommandSender.class); + + // when + crazyLoginConverter.execute(sender); + + // then + ArgumentCaptor authCaptor = ArgumentCaptor.forClass(PlayerAuth.class); + verify(dataSource, times(2)).saveAuth(authCaptor.capture()); + List savedAuths = authCaptor.getAllValues(); + assertNameAndRealName(savedAuths.get(0), "qotato", "qotaTo"); + assertThat(savedAuths.get(0).getPassword(), equalToHash("8267663ab198a96437b9f455429a2c1b6c943111613c217bf2703c14d08a309d34e510ddb5549507b1500759dbcf9d4a99bc765ff37b32bd31adbb1e92e74ac5")); + assertNameAndRealName(savedAuths.get(1), "bobby", "Bobby"); + assertThat(savedAuths.get(1).getPassword(), equalToHash("ad50dbc841e6321210530801f5219a5ffb4c7c41f11878d465374a4b8db2965c50f69b6098918a58e4adea312e3633c7724b15e24a217009e6fa2b3c299d55f2")); + } + + @Test + public void shouldStopForNonExistentFile() { + // given + given(settings.getProperty(ConverterSettings.CRAZYLOGIN_FILE_NAME)).willReturn("invalid-file"); + CommandSender sender = mock(CommandSender.class); + + // when + crazyLoginConverter.execute(sender); + + // then + verifyZeroInteractions(dataSource); + verify(sender).sendMessage(argThat(containsString("file not found"))); + } + + private static void assertNameAndRealName(PlayerAuth auth, String name, String realName) { + assertThat(auth.getNickname(), equalTo(name)); + assertThat(auth.getRealName(), equalTo(realName)); + } + +} diff --git a/src/test/java/fr/xephi/authme/converter/ForceFlatToSqliteTest.java b/src/test/java/fr/xephi/authme/converter/ForceFlatToSqliteTest.java index 6f6082ce..49b2a75c 100644 --- a/src/test/java/fr/xephi/authme/converter/ForceFlatToSqliteTest.java +++ b/src/test/java/fr/xephi/authme/converter/ForceFlatToSqliteTest.java @@ -54,7 +54,7 @@ public class ForceFlatToSqliteTest { ForceFlatToSqlite converter = new ForceFlatToSqlite(flatFile, dataSource); // when - converter.run(); + converter.execute(null); // then ArgumentCaptor authCaptor = ArgumentCaptor.forClass(PlayerAuth.class); diff --git a/src/test/resources/converter/crazylogin.db b/src/test/resources/converter/crazylogin.db new file mode 100644 index 00000000..5498db81 --- /dev/null +++ b/src/test/resources/converter/crazylogin.db @@ -0,0 +1,3 @@ +name|password|ips|lastAction|loginFails|passwordExpired +qotaTo|8267663ab198a96437b9f455429a2c1b6c943111613c217bf2703c14d08a309d34e510ddb5549507b1500759dbcf9d4a99bc765ff37b32bd31adbb1e92e74ac5|127.0.0.1|2016-05-27 21:45:57|0|false +Bobby|ad50dbc841e6321210530801f5219a5ffb4c7c41f11878d465374a4b8db2965c50f69b6098918a58e4adea312e3633c7724b15e24a217009e6fa2b3c299d55f2|127.0.0.1|2016-05-27 21:45:40|0|false From cb1085461295689b0f09a276f00914e3b49e59ef Mon Sep 17 00:00:00 2001 From: ljacqu Date: Fri, 27 May 2016 23:05:03 +0200 Subject: [PATCH 107/200] Minor - fix failing build (cannot use method from Java 8) --- src/test/java/tools/dependencygraph/DrawDependency.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/tools/dependencygraph/DrawDependency.java b/src/test/java/tools/dependencygraph/DrawDependency.java index 6da9e7cb..b1fe0243 100644 --- a/src/test/java/tools/dependencygraph/DrawDependency.java +++ b/src/test/java/tools/dependencygraph/DrawDependency.java @@ -158,7 +158,7 @@ public class DrawDependency implements ToolTask { Map dependencies = new HashMap<>(); for (Map.Entry, String> entry : foundDependencies.entries()) { final String className = entry.getKey().getSimpleName(); - dependencies.putIfAbsent(className, Boolean.FALSE); + dependencies.put(className, Boolean.FALSE); dependencies.put(entry.getValue(), Boolean.TRUE); } From eee06dad50de165adabfe8abb812571f0b60f5cb Mon Sep 17 00:00:00 2001 From: ljacqu Date: Fri, 27 May 2016 23:25:11 +0200 Subject: [PATCH 108/200] Dependency graph - allow to summarize dependencies by super type --- .../tools/dependencygraph/DrawDependency.java | 37 ++++++++++++++++--- 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/src/test/java/tools/dependencygraph/DrawDependency.java b/src/test/java/tools/dependencygraph/DrawDependency.java index b1fe0243..706a734e 100644 --- a/src/test/java/tools/dependencygraph/DrawDependency.java +++ b/src/test/java/tools/dependencygraph/DrawDependency.java @@ -1,10 +1,16 @@ package tools.dependencygraph; -import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.ImmutableList; import com.google.common.collect.Multimap; +import fr.xephi.authme.command.ExecutableCommand; +import fr.xephi.authme.converter.Converter; import fr.xephi.authme.initialization.ConstructorInjection; import fr.xephi.authme.initialization.FieldInjection; import fr.xephi.authme.initialization.Injection; +import fr.xephi.authme.process.AsynchronousProcess; +import fr.xephi.authme.process.SynchronousProcess; +import fr.xephi.authme.security.crypts.EncryptionMethod; import tools.utils.ToolTask; import tools.utils.ToolsConstants; @@ -28,8 +34,12 @@ public class DrawDependency implements ToolTask { // Package root private static final String ROOT_PACKAGE = "fr.xephi.authme"; + private static final List> SUPER_TYPES = ImmutableList.of(ExecutableCommand.class, + SynchronousProcess.class, AsynchronousProcess.class, EncryptionMethod.class, Converter.class); + + private boolean mapToSupertype; // Map with the graph's nodes: value is one of the key's dependencies - private Multimap, String> foundDependencies = ArrayListMultimap.create(); + private Multimap, String> foundDependencies = HashMultimap.create(); @Override public String getTaskName() { @@ -38,6 +48,9 @@ public class DrawDependency implements ToolTask { @Override public void execute(Scanner scanner) { + System.out.println("Summarize classes to their generic super type where applicable?"); + mapToSupertype = "y".equalsIgnoreCase(scanner.nextLine()); + // Gather all connections readAndProcessFiles(new File(ToolsConstants.MAIN_SOURCE_ROOT)); @@ -98,10 +111,22 @@ public class DrawDependency implements ToolTask { private void processClass(Class clazz) { List dependencies = getDependencies(clazz); if (dependencies != null) { - foundDependencies.putAll(clazz, dependencies); + foundDependencies.putAll(mapToSuper(clazz), dependencies); } } + private Class mapToSuper(Class clazz) { + if (!mapToSupertype || clazz == null) { + return clazz; + } + for (Class parent : SUPER_TYPES) { + if (parent.isAssignableFrom(clazz)) { + return parent; + } + } + return clazz; + } + // Load Class object for the class in the given file private static Class loadClass(File file) { final String fileName = file.getPath().replace(File.separator, "."); @@ -117,7 +142,7 @@ public class DrawDependency implements ToolTask { } } - private static List getDependencies(Class clazz) { + private List getDependencies(Class clazz) { Injection injection = ConstructorInjection.provide(clazz).get(); if (injection != null) { return formatInjectionDependencies(injection); @@ -134,7 +159,7 @@ public class DrawDependency implements ToolTask { * @param injection the injection whose dependencies should be formatted * @return list of dependencies in a friendly format */ - private static List formatInjectionDependencies(Injection injection) { + private List formatInjectionDependencies(Injection injection) { Class[] dependencies = injection.getDependencies(); Class[] annotations = injection.getDependencyAnnotations(); @@ -143,7 +168,7 @@ public class DrawDependency implements ToolTask { if (annotations[i] != null) { result.add("@" + annotations[i].getSimpleName()); } else { - result.add(dependencies[i].getSimpleName()); + result.add(mapToSuper(dependencies[i]).getSimpleName()); } } return result; From b48e080324a72978a4cf2f3857c78d9e443113aa Mon Sep 17 00:00:00 2001 From: ljacqu Date: Sat, 28 May 2016 07:56:26 +0200 Subject: [PATCH 109/200] Minor code householding - Fix Javadoc errors reported during Jenkins build - Use Guava methods in NewSetting where possible --- .../xephi/authme/command/CommandHandler.java | 6 ++-- .../fr/xephi/authme/events/LoginEvent.java | 3 +- .../authme/permission/PermissionsManager.java | 1 + .../fr/xephi/authme/settings/NewSetting.java | 32 +++++++++---------- 4 files changed, 20 insertions(+), 22 deletions(-) diff --git a/src/main/java/fr/xephi/authme/command/CommandHandler.java b/src/main/java/fr/xephi/authme/command/CommandHandler.java index 2fdd722d..dfff17d1 100644 --- a/src/main/java/fr/xephi/authme/command/CommandHandler.java +++ b/src/main/java/fr/xephi/authme/command/CommandHandler.java @@ -26,10 +26,8 @@ public class CommandHandler { private final CommandService commandService; private final PermissionsManager permissionsManager; - /** - * Create a command handler. - * - * @param commandService The CommandService instance + /* + * Constructor. */ @Inject public CommandHandler(CommandService commandService, PermissionsManager permissionsManager) { diff --git a/src/main/java/fr/xephi/authme/events/LoginEvent.java b/src/main/java/fr/xephi/authme/events/LoginEvent.java index d36dcc1a..eb38d626 100644 --- a/src/main/java/fr/xephi/authme/events/LoginEvent.java +++ b/src/main/java/fr/xephi/authme/events/LoginEvent.java @@ -31,8 +31,9 @@ public class LoginEvent extends CustomEvent { } /** + * Ensures compatibility with plugins like GuiRules. * - * @return + * @return true * @deprecated this will always return true because this event is only called if it was successful */ @Deprecated diff --git a/src/main/java/fr/xephi/authme/permission/PermissionsManager.java b/src/main/java/fr/xephi/authme/permission/PermissionsManager.java index 2c94d6f6..14234e1f 100644 --- a/src/main/java/fr/xephi/authme/permission/PermissionsManager.java +++ b/src/main/java/fr/xephi/authme/permission/PermissionsManager.java @@ -69,6 +69,7 @@ public class PermissionsManager implements PermissionsService { * Constructor. * * @param server Server instance + * @param pluginManager Bukkit plugin manager */ @Inject public PermissionsManager(Server server, PluginManager pluginManager) { diff --git a/src/main/java/fr/xephi/authme/settings/NewSetting.java b/src/main/java/fr/xephi/authme/settings/NewSetting.java index 89717a8c..90719daa 100644 --- a/src/main/java/fr/xephi/authme/settings/NewSetting.java +++ b/src/main/java/fr/xephi/authme/settings/NewSetting.java @@ -1,6 +1,8 @@ package fr.xephi.authme.settings; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Joiner; +import com.google.common.base.Strings; import com.google.common.io.Files; import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.settings.domain.Property; @@ -121,10 +123,20 @@ public class NewSetting { return "/messages/messages_en.yml"; } + /** + * Return the text to use in email registrations. + * + * @return The email message + */ public String getEmailMessage() { return emailMessage; } + /** + * Return the lines to output after an in-game registration. + * + * @return The welcome message + */ public List getWelcomeMessage() { return welcomeMessage; } @@ -206,7 +218,7 @@ public class NewSetting { private String toYaml(Property property, int indent, Yaml simpleYaml, Yaml singleQuoteYaml) { String representation = property.toYaml(configuration, simpleYaml, singleQuoteYaml); - return join("\n" + indent(indent), representation.split("\\n")); + return Joiner.on("\n" + indent(indent)).join(representation.split("\\n")); } private File buildMessagesFile() { @@ -251,7 +263,7 @@ public class NewSetting { final Charset charset = Charset.forName("UTF-8"); if (copyFileFromResource(emailFile, "email.html")) { try { - return StringUtils.join("\n", Files.readLines(emailFile, charset)); + return Files.toString(emailFile, charset); } catch (IOException e) { ConsoleLogger.logException("Failed to read file '" + emailFile.getPath() + "':", e); } @@ -269,23 +281,9 @@ public class NewSetting { return new Yaml(options); } - private static String join(String delimiter, String[] items) { - StringBuilder sb = new StringBuilder(); - String delim = ""; - for (String item : items) { - sb.append(delim).append(item); - delim = delimiter; - } - return sb.toString(); - } - private static String indent(int level) { // We use an indentation of 4 spaces - StringBuilder sb = new StringBuilder(level * 4); - for (int i = 0; i < level; ++i) { - sb.append(" "); - } - return sb.toString(); + return Strings.repeat(" ", level * 4); } } From 25f5fdb45cdac96cfcabbed8855158c901188c9a Mon Sep 17 00:00:00 2001 From: ljacqu Date: Sat, 28 May 2016 22:00:50 +0200 Subject: [PATCH 110/200] Code householding (PlayerListener / Settings) - Use field on PlayerListener for storing nickname pattern -> repeatedly parsing pattern is expensive - Remove unused legacy setting fields - ForceFlatToSqlite cannot be run from converter command -> remove Converter interface to create more natural method signatures --- .../authme/converter/ForceFlatToSqlite.java | 7 +-- .../authme/listener/AuthMePlayerListener.java | 44 ++++++++++++------- .../authme/process/join/AsynchronousJoin.java | 3 -- .../fr/xephi/authme/settings/Settings.java | 7 --- .../xephi/authme/util/MigrationService.java | 2 +- .../converter/ForceFlatToSqliteTest.java | 2 +- .../listener/ListenerConsistencyTest.java | 6 ++- 7 files changed, 37 insertions(+), 34 deletions(-) diff --git a/src/main/java/fr/xephi/authme/converter/ForceFlatToSqlite.java b/src/main/java/fr/xephi/authme/converter/ForceFlatToSqlite.java index f06382a6..82b2540d 100644 --- a/src/main/java/fr/xephi/authme/converter/ForceFlatToSqlite.java +++ b/src/main/java/fr/xephi/authme/converter/ForceFlatToSqlite.java @@ -5,7 +5,6 @@ import fr.xephi.authme.cache.auth.PlayerAuth; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.datasource.FlatFile; import fr.xephi.authme.util.StringUtils; -import org.bukkit.command.CommandSender; import java.util.ArrayList; import java.util.List; @@ -13,7 +12,7 @@ import java.util.List; /** * Mandatory migration from the deprecated flat file datasource to SQLite. */ -public class ForceFlatToSqlite implements Converter { +public class ForceFlatToSqlite { private final DataSource source; private final DataSource destination; @@ -32,9 +31,7 @@ public class ForceFlatToSqlite implements Converter { /** * Perform the conversion. */ - @Override - // Note ljacqu 20160527: CommandSender is null here; it is only present because of the interface it implements - public void execute(CommandSender sender) { + public void run() { List skippedPlayers = new ArrayList<>(); for (PlayerAuth auth : source.getAllAuths()) { if (destination.isAuthAvailable(auth.getNickname())) { diff --git a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java index d2c8ad51..307c9d31 100644 --- a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java +++ b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java @@ -11,6 +11,7 @@ import fr.xephi.authme.cache.auth.PlayerCache; import fr.xephi.authme.cache.limbo.LimboCache; import fr.xephi.authme.cache.limbo.LimboPlayer; import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.initialization.Reloadable; import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.output.Messages; import fr.xephi.authme.permission.PermissionsManager; @@ -53,8 +54,8 @@ import org.bukkit.event.player.PlayerQuitEvent; import org.bukkit.event.player.PlayerRespawnEvent; import org.bukkit.event.player.PlayerShearEntityEvent; +import javax.annotation.PostConstruct; import javax.inject.Inject; - import java.util.Iterator; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -68,10 +69,9 @@ import static fr.xephi.authme.settings.properties.RestrictionSettings.ALLOW_UNAU /** * Listener class for player events. */ -public class AuthMePlayerListener implements Listener { +public class AuthMePlayerListener implements Listener, Reloadable { public static final ConcurrentHashMap joinMessage = new ConcurrentHashMap<>(); - public static final ConcurrentHashMap causeByAuthMe = new ConcurrentHashMap<>(); @Inject private AuthMe plugin; @@ -91,6 +91,10 @@ public class AuthMePlayerListener implements Listener { private SpawnLoader spawnLoader; @Inject private ValidationService validationService; + @Inject + private PermissionsManager permissionsManager; + + private Pattern nicknamePattern; private void sendLoginOrRegisterMessage(final Player player) { bukkitService.runTaskAsynchronously(new Runnable() { @@ -244,6 +248,7 @@ public class AuthMePlayerListener implements Listener { }); } + // Note ljacqu 20160528: AsyncPlayerPreLoginEvent is not fired by all servers in offline mode @EventHandler(priority = EventPriority.HIGHEST) public void onPreLogin(AsyncPlayerPreLoginEvent event) { PlayerAuth auth = dataSource.getAuth(event.getName()); @@ -306,11 +311,8 @@ public class AuthMePlayerListener implements Listener { return; } - // Get the permissions manager - PermissionsManager permsMan = plugin.getPermissionsManager(); - if (event.getResult() == PlayerLoginEvent.Result.KICK_FULL) { - if (permsMan.hasPermission(player, PlayerStatePermission.IS_VIP)) { + if (permissionsManager.hasPermission(player, PlayerStatePermission.IS_VIP)) { int playersOnline = bukkitService.getOnlinePlayers().size(); if (playersOnline > plugin.getServer().getMaxPlayers()) { event.allow(); @@ -358,10 +360,9 @@ public class AuthMePlayerListener implements Listener { return; } - String nickRegEx = settings.getProperty(RestrictionSettings.ALLOWED_NICKNAME_CHARACTERS); - Pattern nickPattern = Pattern.compile(nickRegEx); - if (name.equalsIgnoreCase("Player") || !nickPattern.matcher(player.getName()).matches()) { - event.setKickMessage(m.retrieveSingle(MessageKey.INVALID_NAME_CHARACTERS).replace("REG_EX", nickRegEx)); + if (name.equalsIgnoreCase("Player") || !nicknamePattern.matcher(player.getName()).matches()) { + event.setKickMessage(m.retrieveSingle(MessageKey.INVALID_NAME_CHARACTERS) + .replace("REG_EX", nicknamePattern.pattern())); event.setResult(PlayerLoginEvent.Result.KICK_OTHER); return; } @@ -388,7 +389,7 @@ public class AuthMePlayerListener implements Listener { } if (antiBot.antibotKicked.contains(player.getName())) { - return; + return; } management.performQuit(player, false); @@ -408,11 +409,9 @@ public class AuthMePlayerListener implements Listener { return; } - if (antiBot.antibotKicked.contains(player.getName())) { - return; + if (!antiBot.antibotKicked.contains(player.getName())) { + plugin.getManagement().performQuit(player, true); } - - plugin.getManagement().performQuit(player, true); } @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) @@ -548,4 +547,17 @@ public class AuthMePlayerListener implements Listener { event.setCancelled(true); } } + + @PostConstruct + @Override + public void reload() { + String nickRegEx = settings.getProperty(RestrictionSettings.ALLOWED_NICKNAME_CHARACTERS); + try { + nicknamePattern = Pattern.compile(nickRegEx); + } catch (Exception e) { + nicknamePattern = Pattern.compile(".*?"); + ConsoleLogger.showError("Nickname pattern is not a valid regular expression! " + + "Fallback to allowing all nicknames"); + } + } } diff --git a/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java b/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java index 478dcda6..59892c58 100644 --- a/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java +++ b/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java @@ -11,7 +11,6 @@ import fr.xephi.authme.events.FirstSpawnTeleportEvent; import fr.xephi.authme.events.ProtectInventoryEvent; import fr.xephi.authme.events.SpawnTeleportEvent; import fr.xephi.authme.hooks.PluginHooks; -import fr.xephi.authme.listener.AuthMePlayerListener; import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.permission.PlayerStatePermission; import fr.xephi.authme.process.AsynchronousProcess; @@ -105,7 +104,6 @@ public class AsynchronousJoin implements AsynchronousProcess { bukkitService.scheduleSyncDelayedTask(new Runnable() { @Override public void run() { - AuthMePlayerListener.causeByAuthMe.putIfAbsent(name, true); player.kickPlayer(service.retrieveSingleMessage(MessageKey.NOT_OWNER_ERROR)); if (service.getProperty(RestrictionSettings.BAN_UNKNOWN_IP)) { plugin.getServer().banIP(ip); @@ -124,7 +122,6 @@ public class AsynchronousJoin implements AsynchronousProcess { bukkitService.scheduleSyncDelayedTask(new Runnable() { @Override public void run() { - // TODO: Messages entry player.kickPlayer(service.retrieveSingleMessage(MessageKey.SAME_IP_ONLINE)); } }); diff --git a/src/main/java/fr/xephi/authme/settings/Settings.java b/src/main/java/fr/xephi/authme/settings/Settings.java index af021556..9684d752 100644 --- a/src/main/java/fr/xephi/authme/settings/Settings.java +++ b/src/main/java/fr/xephi/authme/settings/Settings.java @@ -29,7 +29,6 @@ public final class Settings { public static boolean protectInventoryBeforeLogInEnabled; public static boolean isStopEnabled; public static boolean reloadSupport; - public static boolean rakamakUseIp; public static boolean removePassword; public static boolean multiverse; public static boolean bungee; @@ -39,8 +38,6 @@ public final class Settings { public static String getUnloggedinGroup; public static String unRegisteredGroup; public static String getRegisteredGroup; - public static String rakamakUsers; - public static String rakamakUsersIp; public static String defaultWorld; public static String crazyloginFileName; public static int getSessionTimeout; @@ -83,10 +80,6 @@ public final class Settings { protectInventoryBeforeLogInEnabled = load(RestrictionSettings.PROTECT_INVENTORY_BEFORE_LOGIN); isStopEnabled = configFile.getBoolean("Security.SQLProblem.stopServer", true); reloadSupport = configFile.getBoolean("Security.ReloadCommand.useReloadCommandSupport", true); - - rakamakUsers = configFile.getString("Converter.Rakamak.fileName", "users.rak"); - rakamakUsersIp = configFile.getString("Converter.Rakamak.ipFileName", "UsersIp.rak"); - rakamakUseIp = configFile.getBoolean("Converter.Rakamak.useIp", false); removePassword = configFile.getBoolean("Security.console.removePassword", true); maxLoginTry = configFile.getInt("Security.captcha.maxLoginTry", 5); captchaLength = configFile.getInt("Security.captcha.captchaLength", 5); diff --git a/src/main/java/fr/xephi/authme/util/MigrationService.java b/src/main/java/fr/xephi/authme/util/MigrationService.java index 868b7ff8..cd9bb382 100644 --- a/src/main/java/fr/xephi/authme/util/MigrationService.java +++ b/src/main/java/fr/xephi/authme/util/MigrationService.java @@ -69,7 +69,7 @@ public final class MigrationService { try { SQLite sqlite = new SQLite(settings); ForceFlatToSqlite converter = new ForceFlatToSqlite(flatFile, sqlite); - converter.execute(null); + converter.run(); settings.setProperty(DatabaseSettings.BACKEND, DataSourceType.SQLITE); settings.save(); return sqlite; diff --git a/src/test/java/fr/xephi/authme/converter/ForceFlatToSqliteTest.java b/src/test/java/fr/xephi/authme/converter/ForceFlatToSqliteTest.java index 49b2a75c..6f6082ce 100644 --- a/src/test/java/fr/xephi/authme/converter/ForceFlatToSqliteTest.java +++ b/src/test/java/fr/xephi/authme/converter/ForceFlatToSqliteTest.java @@ -54,7 +54,7 @@ public class ForceFlatToSqliteTest { ForceFlatToSqlite converter = new ForceFlatToSqlite(flatFile, dataSource); // when - converter.execute(null); + converter.run(); // then ArgumentCaptor authCaptor = ArgumentCaptor.forClass(PlayerAuth.class); diff --git a/src/test/java/fr/xephi/authme/listener/ListenerConsistencyTest.java b/src/test/java/fr/xephi/authme/listener/ListenerConsistencyTest.java index 5fe3ce26..4749dac4 100644 --- a/src/test/java/fr/xephi/authme/listener/ListenerConsistencyTest.java +++ b/src/test/java/fr/xephi/authme/listener/ListenerConsistencyTest.java @@ -106,7 +106,11 @@ public final class ListenerConsistencyTest { // Exclude any methods with "$" in it: jacoco creates a "$jacocoInit" method we want to ignore, and // methods like "access$000" are created by the compiler when a private member is being accessed by an inner // class, which is not of interest for us - return !Modifier.isPrivate(method.getModifiers()) && !method.getName().contains("$"); + if (Modifier.isPrivate(method.getModifiers()) || method.getName().contains("$")) { + return false; + } + // Skip reload() method (implementation of Reloadable interface) + return !"reload".equals(method.getName()) || method.getParameterTypes().length > 0; } } From a854d4e0e00c57df7248dcdb85b1bb1cec71ed46 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Sat, 28 May 2016 22:29:49 +0200 Subject: [PATCH 111/200] Register entity listener again --- src/main/java/fr/xephi/authme/AuthMe.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/fr/xephi/authme/AuthMe.java b/src/main/java/fr/xephi/authme/AuthMe.java index 4553b207..05023a28 100644 --- a/src/main/java/fr/xephi/authme/AuthMe.java +++ b/src/main/java/fr/xephi/authme/AuthMe.java @@ -20,6 +20,7 @@ import fr.xephi.authme.initialization.AuthMeServiceInitializer; import fr.xephi.authme.initialization.DataFolder; import fr.xephi.authme.initialization.MetricsStarter; import fr.xephi.authme.listener.AuthMeBlockListener; +import fr.xephi.authme.listener.AuthMeEntityListener; import fr.xephi.authme.listener.AuthMeInventoryPacketAdapter; import fr.xephi.authme.listener.AuthMePlayerListener; import fr.xephi.authme.listener.AuthMePlayerListener16; @@ -357,7 +358,7 @@ public class AuthMe extends JavaPlugin { // Register event listeners pluginManager.registerEvents(initializer.get(AuthMePlayerListener.class), this); pluginManager.registerEvents(initializer.get(AuthMeBlockListener.class), this); - pluginManager.registerEvents(initializer.get(AuthMePlayerListener.class), this); + pluginManager.registerEvents(initializer.get(AuthMeEntityListener.class), this); pluginManager.registerEvents(initializer.get(AuthMeServerListener.class), this); // Try to register 1.6 player listeners From 2c92a8b52f9ac2c21792a47d831201b157a17033 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Sat, 28 May 2016 22:32:45 +0200 Subject: [PATCH 112/200] Move logic for join events in its own listener --- src/main/java/fr/xephi/authme/AuthMe.java | 13 +- .../listener/AuthMePlayerJoinListener.java | 233 ++++++++++++++++++ .../authme/listener/AuthMePlayerListener.java | 175 +------------ .../listener/ListenerConsistencyTest.java | 8 +- 4 files changed, 240 insertions(+), 189 deletions(-) create mode 100644 src/main/java/fr/xephi/authme/listener/AuthMePlayerJoinListener.java diff --git a/src/main/java/fr/xephi/authme/AuthMe.java b/src/main/java/fr/xephi/authme/AuthMe.java index 05023a28..98f6cca2 100644 --- a/src/main/java/fr/xephi/authme/AuthMe.java +++ b/src/main/java/fr/xephi/authme/AuthMe.java @@ -22,6 +22,7 @@ import fr.xephi.authme.initialization.MetricsStarter; import fr.xephi.authme.listener.AuthMeBlockListener; import fr.xephi.authme.listener.AuthMeEntityListener; import fr.xephi.authme.listener.AuthMeInventoryPacketAdapter; +import fr.xephi.authme.listener.AuthMePlayerJoinListener; import fr.xephi.authme.listener.AuthMePlayerListener; import fr.xephi.authme.listener.AuthMePlayerListener16; import fr.xephi.authme.listener.AuthMePlayerListener18; @@ -34,7 +35,6 @@ import fr.xephi.authme.output.Log4JFilter; import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.output.Messages; import fr.xephi.authme.permission.PermissionsManager; -import fr.xephi.authme.permission.PlayerStatePermission; import fr.xephi.authme.process.Management; import fr.xephi.authme.security.PasswordSecurity; import fr.xephi.authme.security.crypts.SHA256; @@ -360,6 +360,7 @@ public class AuthMe extends JavaPlugin { pluginManager.registerEvents(initializer.get(AuthMeBlockListener.class), this); pluginManager.registerEvents(initializer.get(AuthMeEntityListener.class), this); pluginManager.registerEvents(initializer.get(AuthMeServerListener.class), this); + pluginManager.registerEvents(initializer.get(AuthMePlayerJoinListener.class), this); // Try to register 1.6 player listeners try { @@ -650,16 +651,6 @@ public class AuthMe extends JavaPlugin { return pluginHooks != null && pluginHooks.isNpc(player) || player.hasMetadata("NPC"); } - // Select the player to kick when a vip player joins the server when full - public Player generateKickPlayer(Collection collection) { - for (Player player : collection) { - if (!getPermissionsManager().hasPermission(player, PlayerStatePermission.IS_VIP)) { - return player; - } - } - return null; - } - // Purge inactive players from the database, as defined in the configuration private void runAutoPurge() { if (!newSettings.getProperty(PurgeSettings.USE_AUTO_PURGE) || autoPurging) { diff --git a/src/main/java/fr/xephi/authme/listener/AuthMePlayerJoinListener.java b/src/main/java/fr/xephi/authme/listener/AuthMePlayerJoinListener.java new file mode 100644 index 00000000..c83d77a5 --- /dev/null +++ b/src/main/java/fr/xephi/authme/listener/AuthMePlayerJoinListener.java @@ -0,0 +1,233 @@ +package fr.xephi.authme.listener; + +import com.google.common.io.ByteArrayDataOutput; +import com.google.common.io.ByteStreams; +import fr.xephi.authme.AntiBot; +import fr.xephi.authme.AuthMe; +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.cache.auth.PlayerAuth; +import fr.xephi.authme.cache.auth.PlayerCache; +import fr.xephi.authme.cache.limbo.LimboCache; +import fr.xephi.authme.cache.limbo.LimboPlayer; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.initialization.Reloadable; +import fr.xephi.authme.output.MessageKey; +import fr.xephi.authme.output.Messages; +import fr.xephi.authme.permission.PermissionsManager; +import fr.xephi.authme.permission.PlayerStatePermission; +import fr.xephi.authme.process.Management; +import fr.xephi.authme.settings.NewSetting; +import fr.xephi.authme.settings.properties.HooksSettings; +import fr.xephi.authme.settings.properties.ProtectionSettings; +import fr.xephi.authme.settings.properties.RegistrationSettings; +import fr.xephi.authme.settings.properties.RestrictionSettings; +import fr.xephi.authme.util.BukkitService; +import fr.xephi.authme.util.Utils; +import fr.xephi.authme.util.ValidationService; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.player.AsyncPlayerPreLoginEvent; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerLoginEvent; + +import javax.annotation.PostConstruct; +import javax.inject.Inject; +import java.util.Collection; +import java.util.regex.Pattern; + +/** + * Listener for player join events. + */ +public class AuthMePlayerJoinListener implements Listener, Reloadable { + + @Inject + private BukkitService bukkitService; + @Inject + private DataSource dataSource; + @Inject + private AntiBot antiBot; + @Inject + private Management management; + @Inject + private NewSetting settings; + @Inject + private Messages m; + @Inject + private PermissionsManager permissionsManager; + @Inject + private ValidationService validationService; + @Inject + private AuthMe plugin; + + private Pattern nicknamePattern; + + @EventHandler(priority = EventPriority.LOW) + public void onPlayerJoin(PlayerJoinEvent event) { + final Player player = event.getPlayer(); + if (player == null) { + return; + } + + // Schedule login task so works after the prelogin + // (Fix found by Koolaid5000) + bukkitService.runTask(new Runnable() { + @Override + public void run() { + management.performJoin(player); + } + }); + } + + // Note ljacqu 20160528: AsyncPlayerPreLoginEvent is not fired by all servers in offline mode + @EventHandler(priority = EventPriority.HIGHEST) + public void onPreLogin(AsyncPlayerPreLoginEvent event) { + PlayerAuth auth = dataSource.getAuth(event.getName()); + if (auth == null && antiBot.getAntiBotStatus() == AntiBot.AntiBotStatus.ACTIVE) { + event.setKickMessage(m.retrieveSingle(MessageKey.KICK_ANTIBOT)); + event.setLoginResult(AsyncPlayerPreLoginEvent.Result.KICK_OTHER); + antiBot.antibotKicked.addIfAbsent(event.getName()); + return; + } + if (auth == null && settings.getProperty(RestrictionSettings.KICK_NON_REGISTERED)) { + event.setKickMessage(m.retrieveSingle(MessageKey.MUST_REGISTER_MESSAGE)); + event.setLoginResult(AsyncPlayerPreLoginEvent.Result.KICK_OTHER); + return; + } + final String name = event.getName().toLowerCase(); + if (name.length() > settings.getProperty(RestrictionSettings.MAX_NICKNAME_LENGTH) || name.length() < settings.getProperty(RestrictionSettings.MIN_NICKNAME_LENGTH)) { + event.setKickMessage(m.retrieveSingle(MessageKey.INVALID_NAME_LENGTH)); + event.setLoginResult(AsyncPlayerPreLoginEvent.Result.KICK_OTHER); + return; + } + if (settings.getProperty(RegistrationSettings.PREVENT_OTHER_CASE) && auth != null && auth.getRealName() != null) { + String realName = auth.getRealName(); + if (!realName.isEmpty() && !"Player".equals(realName) && !realName.equals(event.getName())) { + event.setLoginResult(AsyncPlayerPreLoginEvent.Result.KICK_OTHER); + event.setKickMessage(m.retrieveSingle(MessageKey.INVALID_NAME_CASE, realName, event.getName())); + return; + } + if (realName.isEmpty() || "Player".equals(realName)) { + dataSource.updateRealName(event.getName().toLowerCase(), event.getName()); + } + } + + if (auth == null && settings.getProperty(ProtectionSettings.ENABLE_PROTECTION)) { + String playerIp = event.getAddress().getHostAddress(); + if (!validationService.isCountryAdmitted(playerIp)) { + event.setLoginResult(AsyncPlayerPreLoginEvent.Result.KICK_OTHER); + event.setKickMessage(m.retrieveSingle(MessageKey.COUNTRY_BANNED_ERROR)); + return; + } + } + + final Player player = bukkitService.getPlayerExact(name); + // Check if forceSingleSession is set to true, so kick player that has + // joined with same nick of online player + if (player != null && settings.getProperty(RestrictionSettings.FORCE_SINGLE_SESSION)) { + event.setLoginResult(AsyncPlayerPreLoginEvent.Result.KICK_OTHER); + event.setKickMessage(m.retrieveSingle(MessageKey.USERNAME_ALREADY_ONLINE_ERROR)); + LimboPlayer limbo = LimboCache.getInstance().getLimboPlayer(name); + if (limbo != null && PlayerCache.getInstance().isAuthenticated(name)) { + Utils.addNormal(player, limbo.getGroup()); + LimboCache.getInstance().deleteLimboPlayer(name); + } + } + } + + @EventHandler(priority = EventPriority.HIGHEST) + public void onPlayerLogin(PlayerLoginEvent event) { + final Player player = event.getPlayer(); + if (player == null || Utils.isUnrestricted(player)) { + return; + } + + if (event.getResult() == PlayerLoginEvent.Result.KICK_FULL) { + if (permissionsManager.hasPermission(player, PlayerStatePermission.IS_VIP)) { + int playersOnline = bukkitService.getOnlinePlayers().size(); + if (playersOnline > plugin.getServer().getMaxPlayers()) { + event.allow(); + } else { + Player pl = generateKickPlayer(bukkitService.getOnlinePlayers()); + if (pl != null) { + pl.kickPlayer(m.retrieveSingle(MessageKey.KICK_FOR_VIP)); + event.allow(); + } else { + ConsoleLogger.info("The player " + event.getPlayer().getName() + " tried to join, but the server was full"); + event.setKickMessage(m.retrieveSingle(MessageKey.KICK_FULL_SERVER)); + event.setResult(PlayerLoginEvent.Result.KICK_FULL); + } + } + } else { + event.setKickMessage(m.retrieveSingle(MessageKey.KICK_FULL_SERVER)); + event.setResult(PlayerLoginEvent.Result.KICK_FULL); + return; + } + } + + if (event.getResult() != PlayerLoginEvent.Result.ALLOWED) { + return; + } + + final String name = player.getName().toLowerCase(); + boolean isAuthAvailable = dataSource.isAuthAvailable(name); + + if (antiBot.getAntiBotStatus() == AntiBot.AntiBotStatus.ACTIVE && !isAuthAvailable) { + event.setKickMessage(m.retrieveSingle(MessageKey.KICK_ANTIBOT)); + event.setResult(PlayerLoginEvent.Result.KICK_OTHER); + antiBot.antibotKicked.addIfAbsent(player.getName()); + return; + } + + if (settings.getProperty(RestrictionSettings.KICK_NON_REGISTERED) && !isAuthAvailable) { + event.setKickMessage(m.retrieveSingle(MessageKey.MUST_REGISTER_MESSAGE)); + event.setResult(PlayerLoginEvent.Result.KICK_OTHER); + return; + } + + if (name.length() > settings.getProperty(RestrictionSettings.MAX_NICKNAME_LENGTH) || name.length() < settings.getProperty(RestrictionSettings.MIN_NICKNAME_LENGTH)) { + event.setKickMessage(m.retrieveSingle(MessageKey.INVALID_NAME_LENGTH)); + event.setResult(PlayerLoginEvent.Result.KICK_OTHER); + return; + } + + if (name.equalsIgnoreCase("Player") || !nicknamePattern.matcher(player.getName()).matches()) { + event.setKickMessage(m.retrieveSingle(MessageKey.INVALID_NAME_CHARACTERS) + .replace("REG_EX", nicknamePattern.pattern())); + event.setResult(PlayerLoginEvent.Result.KICK_OTHER); + return; + } + + antiBot.checkAntiBot(player); + + if (settings.getProperty(HooksSettings.BUNGEECORD)) { + ByteArrayDataOutput out = ByteStreams.newDataOutput(); + out.writeUTF("IP"); + player.sendPluginMessage(plugin, "BungeeCord", out.toByteArray()); + } + } + + @PostConstruct + @Override + public void reload() { + String nickRegEx = settings.getProperty(RestrictionSettings.ALLOWED_NICKNAME_CHARACTERS); + try { + nicknamePattern = Pattern.compile(nickRegEx); + } catch (Exception e) { + nicknamePattern = Pattern.compile(".*?"); + ConsoleLogger.showError("Nickname pattern is not a valid regular expression! " + + "Fallback to allowing all nicknames"); + } + } + + // Select the player to kick when a vip player joins the server when full + private Player generateKickPlayer(Collection collection) { + for (Player player : collection) { + if (!permissionsManager.hasPermission(player, PlayerStatePermission.IS_VIP)) { + return player; + } + } + return null; + } +} diff --git a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java index 307c9d31..a928b915 100644 --- a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java +++ b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java @@ -1,26 +1,16 @@ package fr.xephi.authme.listener; -import com.google.common.io.ByteArrayDataOutput; -import com.google.common.io.ByteStreams; import fr.xephi.authme.AntiBot; -import fr.xephi.authme.AntiBot.AntiBotStatus; import fr.xephi.authme.AuthMe; -import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.cache.auth.PlayerAuth; -import fr.xephi.authme.cache.auth.PlayerCache; -import fr.xephi.authme.cache.limbo.LimboCache; -import fr.xephi.authme.cache.limbo.LimboPlayer; import fr.xephi.authme.datasource.DataSource; -import fr.xephi.authme.initialization.Reloadable; import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.output.Messages; import fr.xephi.authme.permission.PermissionsManager; -import fr.xephi.authme.permission.PlayerStatePermission; import fr.xephi.authme.process.Management; import fr.xephi.authme.settings.NewSetting; import fr.xephi.authme.settings.SpawnLoader; import fr.xephi.authme.settings.properties.HooksSettings; -import fr.xephi.authme.settings.properties.ProtectionSettings; import fr.xephi.authme.settings.properties.RegistrationSettings; import fr.xephi.authme.settings.properties.RestrictionSettings; import fr.xephi.authme.util.BukkitService; @@ -37,7 +27,6 @@ import org.bukkit.event.entity.EntityDamageByEntityEvent; import org.bukkit.event.inventory.InventoryClickEvent; import org.bukkit.event.inventory.InventoryOpenEvent; import org.bukkit.event.player.AsyncPlayerChatEvent; -import org.bukkit.event.player.AsyncPlayerPreLoginEvent; import org.bukkit.event.player.PlayerBedEnterEvent; import org.bukkit.event.player.PlayerCommandPreprocessEvent; import org.bukkit.event.player.PlayerDropItemEvent; @@ -47,19 +36,16 @@ import org.bukkit.event.player.PlayerInteractEvent; import org.bukkit.event.player.PlayerItemConsumeEvent; import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.event.player.PlayerKickEvent; -import org.bukkit.event.player.PlayerLoginEvent; import org.bukkit.event.player.PlayerMoveEvent; import org.bukkit.event.player.PlayerPickupItemEvent; import org.bukkit.event.player.PlayerQuitEvent; import org.bukkit.event.player.PlayerRespawnEvent; import org.bukkit.event.player.PlayerShearEntityEvent; -import javax.annotation.PostConstruct; import javax.inject.Inject; import java.util.Iterator; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; -import java.util.regex.Pattern; import static fr.xephi.authme.listener.ListenerService.shouldCancelEvent; import static fr.xephi.authme.settings.properties.RestrictionSettings.ALLOWED_MOVEMENT_RADIUS; @@ -69,7 +55,7 @@ import static fr.xephi.authme.settings.properties.RestrictionSettings.ALLOW_UNAU /** * Listener class for player events. */ -public class AuthMePlayerListener implements Listener, Reloadable { +public class AuthMePlayerListener implements Listener { public static final ConcurrentHashMap joinMessage = new ConcurrentHashMap<>(); @@ -94,8 +80,6 @@ public class AuthMePlayerListener implements Listener, Reloadable { @Inject private PermissionsManager permissionsManager; - private Pattern nicknamePattern; - private void sendLoginOrRegisterMessage(final Player player) { bukkitService.runTaskAsynchronously(new Runnable() { @Override @@ -231,151 +215,6 @@ public class AuthMePlayerListener implements Listener, Reloadable { } } - @EventHandler(priority = EventPriority.LOW) - public void onPlayerJoin(PlayerJoinEvent event) { - final Player player = event.getPlayer(); - if (player == null) { - return; - } - - // Schedule login task so works after the prelogin - // (Fix found by Koolaid5000) - bukkitService.runTask(new Runnable() { - @Override - public void run() { - management.performJoin(player); - } - }); - } - - // Note ljacqu 20160528: AsyncPlayerPreLoginEvent is not fired by all servers in offline mode - @EventHandler(priority = EventPriority.HIGHEST) - public void onPreLogin(AsyncPlayerPreLoginEvent event) { - PlayerAuth auth = dataSource.getAuth(event.getName()); - if (auth == null && antiBot.getAntiBotStatus() == AntiBotStatus.ACTIVE) { - event.setKickMessage(m.retrieveSingle(MessageKey.KICK_ANTIBOT)); - event.setLoginResult(AsyncPlayerPreLoginEvent.Result.KICK_OTHER); - antiBot.antibotKicked.addIfAbsent(event.getName()); - return; - } - if (auth == null && settings.getProperty(RestrictionSettings.KICK_NON_REGISTERED)) { - event.setKickMessage(m.retrieveSingle(MessageKey.MUST_REGISTER_MESSAGE)); - event.setLoginResult(AsyncPlayerPreLoginEvent.Result.KICK_OTHER); - return; - } - final String name = event.getName().toLowerCase(); - if (name.length() > settings.getProperty(RestrictionSettings.MAX_NICKNAME_LENGTH) || name.length() < settings.getProperty(RestrictionSettings.MIN_NICKNAME_LENGTH)) { - event.setKickMessage(m.retrieveSingle(MessageKey.INVALID_NAME_LENGTH)); - event.setLoginResult(AsyncPlayerPreLoginEvent.Result.KICK_OTHER); - return; - } - if (settings.getProperty(RegistrationSettings.PREVENT_OTHER_CASE) && auth != null && auth.getRealName() != null) { - String realName = auth.getRealName(); - if (!realName.isEmpty() && !"Player".equals(realName) && !realName.equals(event.getName())) { - event.setLoginResult(AsyncPlayerPreLoginEvent.Result.KICK_OTHER); - event.setKickMessage(m.retrieveSingle(MessageKey.INVALID_NAME_CASE, realName, event.getName())); - return; - } - if (realName.isEmpty() || "Player".equals(realName)) { - dataSource.updateRealName(event.getName().toLowerCase(), event.getName()); - } - } - - if (auth == null && settings.getProperty(ProtectionSettings.ENABLE_PROTECTION)) { - String playerIp = event.getAddress().getHostAddress(); - if (!validationService.isCountryAdmitted(playerIp)) { - event.setLoginResult(AsyncPlayerPreLoginEvent.Result.KICK_OTHER); - event.setKickMessage(m.retrieveSingle(MessageKey.COUNTRY_BANNED_ERROR)); - return; - } - } - - final Player player = bukkitService.getPlayerExact(name); - // Check if forceSingleSession is set to true, so kick player that has - // joined with same nick of online player - if (player != null && settings.getProperty(RestrictionSettings.FORCE_SINGLE_SESSION)) { - event.setLoginResult(AsyncPlayerPreLoginEvent.Result.KICK_OTHER); - event.setKickMessage(m.retrieveSingle(MessageKey.USERNAME_ALREADY_ONLINE_ERROR)); - LimboPlayer limbo = LimboCache.getInstance().getLimboPlayer(name); - if (limbo != null && PlayerCache.getInstance().isAuthenticated(name)) { - Utils.addNormal(player, limbo.getGroup()); - LimboCache.getInstance().deleteLimboPlayer(name); - } - } - } - - @EventHandler(priority = EventPriority.HIGHEST) - public void onPlayerLogin(PlayerLoginEvent event) { - final Player player = event.getPlayer(); - if (player == null || Utils.isUnrestricted(player)) { - return; - } - - if (event.getResult() == PlayerLoginEvent.Result.KICK_FULL) { - if (permissionsManager.hasPermission(player, PlayerStatePermission.IS_VIP)) { - int playersOnline = bukkitService.getOnlinePlayers().size(); - if (playersOnline > plugin.getServer().getMaxPlayers()) { - event.allow(); - } else { - Player pl = plugin.generateKickPlayer(bukkitService.getOnlinePlayers()); - if (pl != null) { - pl.kickPlayer(m.retrieveSingle(MessageKey.KICK_FOR_VIP)); - event.allow(); - } else { - ConsoleLogger.info("The player " + event.getPlayer().getName() + " tried to join, but the server was full"); - event.setKickMessage(m.retrieveSingle(MessageKey.KICK_FULL_SERVER)); - event.setResult(PlayerLoginEvent.Result.KICK_FULL); - } - } - } else { - event.setKickMessage(m.retrieveSingle(MessageKey.KICK_FULL_SERVER)); - event.setResult(PlayerLoginEvent.Result.KICK_FULL); - return; - } - } - - if (event.getResult() != PlayerLoginEvent.Result.ALLOWED) { - return; - } - - final String name = player.getName().toLowerCase(); - boolean isAuthAvailable = dataSource.isAuthAvailable(name); - - if (antiBot.getAntiBotStatus() == AntiBotStatus.ACTIVE && !isAuthAvailable) { - event.setKickMessage(m.retrieveSingle(MessageKey.KICK_ANTIBOT)); - event.setResult(PlayerLoginEvent.Result.KICK_OTHER); - antiBot.antibotKicked.addIfAbsent(player.getName()); - return; - } - - if (settings.getProperty(RestrictionSettings.KICK_NON_REGISTERED) && !isAuthAvailable) { - event.setKickMessage(m.retrieveSingle(MessageKey.MUST_REGISTER_MESSAGE)); - event.setResult(PlayerLoginEvent.Result.KICK_OTHER); - return; - } - - if (name.length() > settings.getProperty(RestrictionSettings.MAX_NICKNAME_LENGTH) || name.length() < settings.getProperty(RestrictionSettings.MIN_NICKNAME_LENGTH)) { - event.setKickMessage(m.retrieveSingle(MessageKey.INVALID_NAME_LENGTH)); - event.setResult(PlayerLoginEvent.Result.KICK_OTHER); - return; - } - - if (name.equalsIgnoreCase("Player") || !nicknamePattern.matcher(player.getName()).matches()) { - event.setKickMessage(m.retrieveSingle(MessageKey.INVALID_NAME_CHARACTERS) - .replace("REG_EX", nicknamePattern.pattern())); - event.setResult(PlayerLoginEvent.Result.KICK_OTHER); - return; - } - - antiBot.checkAntiBot(player); - - if (settings.getProperty(HooksSettings.BUNGEECORD)) { - ByteArrayDataOutput out = ByteStreams.newDataOutput(); - out.writeUTF("IP"); - player.sendPluginMessage(plugin, "BungeeCord", out.toByteArray()); - } - } - @EventHandler(priority = EventPriority.HIGHEST) public void onPlayerQuit(PlayerQuitEvent event) { Player player = event.getPlayer(); @@ -548,16 +387,4 @@ public class AuthMePlayerListener implements Listener, Reloadable { } } - @PostConstruct - @Override - public void reload() { - String nickRegEx = settings.getProperty(RestrictionSettings.ALLOWED_NICKNAME_CHARACTERS); - try { - nicknamePattern = Pattern.compile(nickRegEx); - } catch (Exception e) { - nicknamePattern = Pattern.compile(".*?"); - ConsoleLogger.showError("Nickname pattern is not a valid regular expression! " - + "Fallback to allowing all nicknames"); - } - } } diff --git a/src/test/java/fr/xephi/authme/listener/ListenerConsistencyTest.java b/src/test/java/fr/xephi/authme/listener/ListenerConsistencyTest.java index 4749dac4..d3503d4d 100644 --- a/src/test/java/fr/xephi/authme/listener/ListenerConsistencyTest.java +++ b/src/test/java/fr/xephi/authme/listener/ListenerConsistencyTest.java @@ -21,11 +21,11 @@ import static org.junit.Assert.fail; public final class ListenerConsistencyTest { private static final Class[] LISTENERS = { AuthMeBlockListener.class, AuthMeEntityListener.class, - AuthMePlayerListener.class, AuthMePlayerListener16.class, AuthMePlayerListener18.class, - AuthMeServerListener.class }; + AuthMePlayerJoinListener.class, AuthMePlayerListener.class, AuthMePlayerListener16.class, + AuthMePlayerListener18.class, AuthMeServerListener.class }; - private static final Set CANCELED_EXCEPTIONS = Sets.newHashSet("AuthMePlayerListener#onPlayerJoin", - "AuthMePlayerListener#onPreLogin", "AuthMePlayerListener#onPlayerLogin", + private static final Set CANCELED_EXCEPTIONS = Sets.newHashSet("AuthMePlayerJoinListener#onPlayerJoin", + "AuthMePlayerJoinListener#onPreLogin", "AuthMePlayerJoinListener#onPlayerLogin", "AuthMePlayerListener#onPlayerQuit", "AuthMeServerListener#onPluginDisable", "AuthMeServerListener#onServerPing", "AuthMeServerListener#onPluginEnable", "AuthMePlayerListener#onJoinMessage"); From 39d8a88142af971a7eecdc7019d1c2895dd8eefe Mon Sep 17 00:00:00 2001 From: ljacqu Date: Sun, 29 May 2016 11:45:12 +0200 Subject: [PATCH 113/200] Structure checks done in PlayerJoinListener as individual methods --- src/main/java/fr/xephi/authme/AntiBot.java | 11 +- .../listener/AuthMePlayerJoinListener.java | 312 +++++++++++------- .../authme/listener/AuthMePlayerListener.java | 17 +- .../java/fr/xephi/authme/AntiBotTest.java | 4 +- 4 files changed, 216 insertions(+), 128 deletions(-) diff --git a/src/main/java/fr/xephi/authme/AntiBot.java b/src/main/java/fr/xephi/authme/AntiBot.java index 071842bc..d85cc784 100644 --- a/src/main/java/fr/xephi/authme/AntiBot.java +++ b/src/main/java/fr/xephi/authme/AntiBot.java @@ -29,8 +29,8 @@ public class AntiBot { private AntiBotStatus antiBotStatus = AntiBotStatus.DISABLED; @Inject - public AntiBot(NewSetting settings, Messages messages, PermissionsManager permissionsManager, - BukkitService bukkitService) { + AntiBot(NewSetting settings, Messages messages, PermissionsManager permissionsManager, + BukkitService bukkitService) { this.settings = settings; this.messages = messages; this.permissionsManager = permissionsManager; @@ -86,7 +86,12 @@ public class AntiBot { }, duration * TICKS_PER_MINUTE); } - public void checkAntiBot(final Player player) { + /** + * Handles a player joining the server and checks if AntiBot needs to be activated. + * + * @param player the player who joined the server + */ + public void handlePlayerJoin(final Player player) { if (antiBotStatus == AntiBotStatus.ACTIVE || antiBotStatus == AntiBotStatus.DISABLED) { return; } diff --git a/src/main/java/fr/xephi/authme/listener/AuthMePlayerJoinListener.java b/src/main/java/fr/xephi/authme/listener/AuthMePlayerJoinListener.java index c83d77a5..5343f214 100644 --- a/src/main/java/fr/xephi/authme/listener/AuthMePlayerJoinListener.java +++ b/src/main/java/fr/xephi/authme/listener/AuthMePlayerJoinListener.java @@ -22,6 +22,7 @@ import fr.xephi.authme.settings.properties.ProtectionSettings; import fr.xephi.authme.settings.properties.RegistrationSettings; import fr.xephi.authme.settings.properties.RestrictionSettings; import fr.xephi.authme.util.BukkitService; +import fr.xephi.authme.util.StringUtils; import fr.xephi.authme.util.Utils; import fr.xephi.authme.util.ValidationService; import org.bukkit.entity.Player; @@ -60,79 +61,42 @@ public class AuthMePlayerJoinListener implements Listener, Reloadable { private ValidationService validationService; @Inject private AuthMe plugin; + @Inject + private LimboCache limboCache; private Pattern nicknamePattern; @EventHandler(priority = EventPriority.LOW) public void onPlayerJoin(PlayerJoinEvent event) { final Player player = event.getPlayer(); - if (player == null) { - return; + if (player != null) { + // Schedule login task so works after the prelogin + // (Fix found by Koolaid5000) + bukkitService.runTask(new Runnable() { + @Override + public void run() { + management.performJoin(player); + } + }); } - - // Schedule login task so works after the prelogin - // (Fix found by Koolaid5000) - bukkitService.runTask(new Runnable() { - @Override - public void run() { - management.performJoin(player); - } - }); } // Note ljacqu 20160528: AsyncPlayerPreLoginEvent is not fired by all servers in offline mode + // e.g. CraftBukkit does not. So we need to run crucial things in onPlayerLogin, too @EventHandler(priority = EventPriority.HIGHEST) public void onPreLogin(AsyncPlayerPreLoginEvent event) { - PlayerAuth auth = dataSource.getAuth(event.getName()); - if (auth == null && antiBot.getAntiBotStatus() == AntiBot.AntiBotStatus.ACTIVE) { - event.setKickMessage(m.retrieveSingle(MessageKey.KICK_ANTIBOT)); - event.setLoginResult(AsyncPlayerPreLoginEvent.Result.KICK_OTHER); - antiBot.antibotKicked.addIfAbsent(event.getName()); - return; - } - if (auth == null && settings.getProperty(RestrictionSettings.KICK_NON_REGISTERED)) { - event.setKickMessage(m.retrieveSingle(MessageKey.MUST_REGISTER_MESSAGE)); - event.setLoginResult(AsyncPlayerPreLoginEvent.Result.KICK_OTHER); - return; - } final String name = event.getName().toLowerCase(); - if (name.length() > settings.getProperty(RestrictionSettings.MAX_NICKNAME_LENGTH) || name.length() < settings.getProperty(RestrictionSettings.MIN_NICKNAME_LENGTH)) { - event.setKickMessage(m.retrieveSingle(MessageKey.INVALID_NAME_LENGTH)); - event.setLoginResult(AsyncPlayerPreLoginEvent.Result.KICK_OTHER); - return; - } - if (settings.getProperty(RegistrationSettings.PREVENT_OTHER_CASE) && auth != null && auth.getRealName() != null) { - String realName = auth.getRealName(); - if (!realName.isEmpty() && !"Player".equals(realName) && !realName.equals(event.getName())) { - event.setLoginResult(AsyncPlayerPreLoginEvent.Result.KICK_OTHER); - event.setKickMessage(m.retrieveSingle(MessageKey.INVALID_NAME_CASE, realName, event.getName())); - return; - } - if (realName.isEmpty() || "Player".equals(realName)) { - dataSource.updateRealName(event.getName().toLowerCase(), event.getName()); - } - } + final boolean isAuthAvailable = dataSource.isAuthAvailable(event.getName()); - if (auth == null && settings.getProperty(ProtectionSettings.ENABLE_PROTECTION)) { - String playerIp = event.getAddress().getHostAddress(); - if (!validationService.isCountryAdmitted(playerIp)) { - event.setLoginResult(AsyncPlayerPreLoginEvent.Result.KICK_OTHER); - event.setKickMessage(m.retrieveSingle(MessageKey.COUNTRY_BANNED_ERROR)); - return; - } - } - - final Player player = bukkitService.getPlayerExact(name); - // Check if forceSingleSession is set to true, so kick player that has - // joined with same nick of online player - if (player != null && settings.getProperty(RestrictionSettings.FORCE_SINGLE_SESSION)) { + try { + // Potential performance improvement: make checkAntiBot not require `isAuthAvailable` info and use + // "checkKickNonRegistered" as last -> no need to query the DB before checking antibot / name + checkAntibot(name, isAuthAvailable); + checkKickNonRegistered(isAuthAvailable); + checkIsValidName(name); + } catch (VerificationFailedException e) { + event.setKickMessage(m.retrieveSingle(e.getReason(), e.getArgs())); event.setLoginResult(AsyncPlayerPreLoginEvent.Result.KICK_OTHER); - event.setKickMessage(m.retrieveSingle(MessageKey.USERNAME_ALREADY_ONLINE_ERROR)); - LimboPlayer limbo = LimboCache.getInstance().getLimboPlayer(name); - if (limbo != null && PlayerCache.getInstance().isAuthenticated(name)) { - Utils.addNormal(player, limbo.getGroup()); - LimboCache.getInstance().deleteLimboPlayer(name); - } } } @@ -141,65 +105,30 @@ public class AuthMePlayerJoinListener implements Listener, Reloadable { final Player player = event.getPlayer(); if (player == null || Utils.isUnrestricted(player)) { return; - } - - if (event.getResult() == PlayerLoginEvent.Result.KICK_FULL) { - if (permissionsManager.hasPermission(player, PlayerStatePermission.IS_VIP)) { - int playersOnline = bukkitService.getOnlinePlayers().size(); - if (playersOnline > plugin.getServer().getMaxPlayers()) { - event.allow(); - } else { - Player pl = generateKickPlayer(bukkitService.getOnlinePlayers()); - if (pl != null) { - pl.kickPlayer(m.retrieveSingle(MessageKey.KICK_FOR_VIP)); - event.allow(); - } else { - ConsoleLogger.info("The player " + event.getPlayer().getName() + " tried to join, but the server was full"); - event.setKickMessage(m.retrieveSingle(MessageKey.KICK_FULL_SERVER)); - event.setResult(PlayerLoginEvent.Result.KICK_FULL); - } - } - } else { - event.setKickMessage(m.retrieveSingle(MessageKey.KICK_FULL_SERVER)); - event.setResult(PlayerLoginEvent.Result.KICK_FULL); - return; - } - } - - if (event.getResult() != PlayerLoginEvent.Result.ALLOWED) { + } else if (refusePlayerForFullServer(event)) { + return; + } else if (event.getResult() != PlayerLoginEvent.Result.ALLOWED) { return; } final String name = player.getName().toLowerCase(); - boolean isAuthAvailable = dataSource.isAuthAvailable(name); + final PlayerAuth auth = dataSource.getAuth(player.getName()); + final boolean isAuthAvailable = auth != null; - if (antiBot.getAntiBotStatus() == AntiBot.AntiBotStatus.ACTIVE && !isAuthAvailable) { - event.setKickMessage(m.retrieveSingle(MessageKey.KICK_ANTIBOT)); - event.setResult(PlayerLoginEvent.Result.KICK_OTHER); - antiBot.antibotKicked.addIfAbsent(player.getName()); - return; - } - - if (settings.getProperty(RestrictionSettings.KICK_NON_REGISTERED) && !isAuthAvailable) { - event.setKickMessage(m.retrieveSingle(MessageKey.MUST_REGISTER_MESSAGE)); + try { + checkAntibot(name, isAuthAvailable); + checkKickNonRegistered(isAuthAvailable); + checkIsValidName(name); + checkNameCasing(player, auth); + checkSingleSession(player); + checkPlayerCountry(isAuthAvailable, event); + } catch (VerificationFailedException e) { + event.setKickMessage(m.retrieveSingle(e.getReason(), e.getArgs())); event.setResult(PlayerLoginEvent.Result.KICK_OTHER); return; } - if (name.length() > settings.getProperty(RestrictionSettings.MAX_NICKNAME_LENGTH) || name.length() < settings.getProperty(RestrictionSettings.MIN_NICKNAME_LENGTH)) { - event.setKickMessage(m.retrieveSingle(MessageKey.INVALID_NAME_LENGTH)); - event.setResult(PlayerLoginEvent.Result.KICK_OTHER); - return; - } - - if (name.equalsIgnoreCase("Player") || !nicknamePattern.matcher(player.getName()).matches()) { - event.setKickMessage(m.retrieveSingle(MessageKey.INVALID_NAME_CHARACTERS) - .replace("REG_EX", nicknamePattern.pattern())); - event.setResult(PlayerLoginEvent.Result.KICK_OTHER); - return; - } - - antiBot.checkAntiBot(player); + antiBot.handlePlayerJoin(player); if (settings.getProperty(HooksSettings.BUNGEECORD)) { ByteArrayDataOutput out = ByteStreams.newDataOutput(); @@ -221,13 +150,174 @@ public class AuthMePlayerJoinListener implements Listener, Reloadable { } } - // Select the player to kick when a vip player joins the server when full - private Player generateKickPlayer(Collection collection) { - for (Player player : collection) { + /** + * Selects a non-VIP player to kick when a VIP player joins the server when full. + * + * @param onlinePlayers list of online players + * @return the player to kick, or null if none applicable + */ + private Player generateKickPlayer(Collection onlinePlayers) { + for (Player player : onlinePlayers) { if (!permissionsManager.hasPermission(player, PlayerStatePermission.IS_VIP)) { return player; } } return null; } + + /** + * Checks if Antibot is enabled. + * + * @param playerName the name of the player (lowercase) + * @param isAuthAvailable whether or not the player is registered + */ + private void checkAntibot(String playerName, boolean isAuthAvailable) throws VerificationFailedException { + if (antiBot.getAntiBotStatus() == AntiBot.AntiBotStatus.ACTIVE && !isAuthAvailable) { + antiBot.antibotKicked.addIfAbsent(playerName); + throw new VerificationFailedException(MessageKey.KICK_ANTIBOT); + } + } + + /** + * Checks whether non-registered players should be kicked, and if so, whether the player should be kicked. + * + * @param isAuthAvailable whether or not the player is registered + */ + private void checkKickNonRegistered(boolean isAuthAvailable) throws VerificationFailedException { + if (!isAuthAvailable && settings.getProperty(RestrictionSettings.KICK_NON_REGISTERED)) { + throw new VerificationFailedException(MessageKey.MUST_REGISTER_MESSAGE); + } + } + + /** + * Checks that the name adheres to the configured username restrictions. + * + * @param name the name to verify + */ + private void checkIsValidName(String name) throws VerificationFailedException { + if (name.length() > settings.getProperty(RestrictionSettings.MAX_NICKNAME_LENGTH) + || name.length() < settings.getProperty(RestrictionSettings.MIN_NICKNAME_LENGTH)) { + throw new VerificationFailedException(MessageKey.INVALID_NAME_LENGTH); + } + if (!nicknamePattern.matcher(name).matches()) { + throw new VerificationFailedException(MessageKey.INVALID_NAME_CHARACTERS, nicknamePattern.pattern()); + } + } + + /** + * Handles the case of a full server and verifies if the user's connection should really be refused + * by adjusting the event object accordingly. Attempts to kick a non-VIP player to make room if the + * joining player is a VIP. + * + * @param event the login event to verify + * @return true if the player's connection should be refused (i.e. the event does not need to be processed + * further), false if the player is not refused + */ + private boolean refusePlayerForFullServer(PlayerLoginEvent event) { + final Player player = event.getPlayer(); + if (event.getResult() != PlayerLoginEvent.Result.KICK_FULL) { + // Server is not full, no need to do anything + return false; + } else if (!permissionsManager.hasPermission(player, PlayerStatePermission.IS_VIP)) { + // Server is full and player is NOT VIP; set kick message and proceed with kick + event.setKickMessage(m.retrieveSingle(MessageKey.KICK_FULL_SERVER)); + return true; + } + + // Server is full and player is VIP; attempt to kick a non-VIP player to make room + Collection onlinePlayers = bukkitService.getOnlinePlayers(); + if (onlinePlayers.size() < plugin.getServer().getMaxPlayers()) { + event.allow(); + return false; + } + Player nonVipPlayer = generateKickPlayer(onlinePlayers); + if (nonVipPlayer != null) { + nonVipPlayer.kickPlayer(m.retrieveSingle(MessageKey.KICK_FOR_VIP)); + event.allow(); + return false; + } else { + ConsoleLogger.info("VIP player " + player.getName() + " tried to join, but the server was full"); + event.setKickMessage(m.retrieveSingle(MessageKey.KICK_FULL_SERVER)); + return true; + } + } + + /** + * Checks that the casing in the username corresponds to the one in the database, if so configured. + * + * @param player the player to verify + * @param auth the auth object associated with the player + */ + private void checkNameCasing(Player player, PlayerAuth auth) throws VerificationFailedException { + if (auth != null && settings.getProperty(RegistrationSettings.PREVENT_OTHER_CASE)) { + String realName = auth.getRealName(); // might be null or "Player" + String connectingName = player.getName(); + + if (StringUtils.isEmpty(realName) || "Player".equals(realName)) { + dataSource.updateRealName(connectingName.toLowerCase(), connectingName); + } else if (!realName.equals(connectingName)) { + throw new VerificationFailedException(MessageKey.INVALID_NAME_CASE, realName, connectingName); + } + } + } + + /** + * Checks that the player's country is admitted if he is not registered. + * + * @param isAuthAvailable whether or not the user is registered + * @param event the login event of the player + */ + private void checkPlayerCountry(boolean isAuthAvailable, + PlayerLoginEvent event) throws VerificationFailedException { + if (!isAuthAvailable && settings.getProperty(ProtectionSettings.ENABLE_PROTECTION)) { + String playerIp = event.getAddress().getHostAddress(); + if (!validationService.isCountryAdmitted(playerIp)) { + throw new VerificationFailedException(MessageKey.COUNTRY_BANNED_ERROR); + } + } + } + + /** + * Checks if a player with the same name (case-insensitive) is already playing and refuses the + * connection if so configured. + * + * @param player the player to verify + */ + private void checkSingleSession(Player player) throws VerificationFailedException { + if (!settings.getProperty(RestrictionSettings.FORCE_SINGLE_SESSION)) { + return; + } + + Player onlinePlayer = bukkitService.getPlayerExact(player.getName()); + if (onlinePlayer != null) { + String name = player.getName().toLowerCase(); + LimboPlayer limbo = limboCache.getLimboPlayer(name); + if (limbo != null && PlayerCache.getInstance().isAuthenticated(name)) { + Utils.addNormal(player, limbo.getGroup()); + limboCache.deleteLimboPlayer(name); + } + throw new VerificationFailedException(MessageKey.USERNAME_ALREADY_ONLINE_ERROR); + } + } + + /** + * Exception thrown when a verification has failed and the player should be kicked. + */ + private static final class VerificationFailedException extends Exception { + private final MessageKey reason; + private final String[] args; + + public VerificationFailedException(MessageKey reason, String... args) { + this.reason = reason; + this.args = args; + } + + public MessageKey getReason() { + return reason; + } + + public String[] getArgs() { + return args; + } + } } diff --git a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java index a928b915..ae3b49c4 100644 --- a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java +++ b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java @@ -1,12 +1,11 @@ package fr.xephi.authme.listener; import fr.xephi.authme.AntiBot; -import fr.xephi.authme.AuthMe; import fr.xephi.authme.cache.auth.PlayerAuth; import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.hooks.PluginHooks; import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.output.Messages; -import fr.xephi.authme.permission.PermissionsManager; import fr.xephi.authme.process.Management; import fr.xephi.authme.settings.NewSetting; import fr.xephi.authme.settings.SpawnLoader; @@ -15,8 +14,6 @@ import fr.xephi.authme.settings.properties.RegistrationSettings; import fr.xephi.authme.settings.properties.RestrictionSettings; import fr.xephi.authme.util.BukkitService; import fr.xephi.authme.util.Utils; -import fr.xephi.authme.util.ValidationService; -import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; @@ -59,8 +56,6 @@ public class AuthMePlayerListener implements Listener { public static final ConcurrentHashMap joinMessage = new ConcurrentHashMap<>(); - @Inject - private AuthMe plugin; @Inject private NewSetting settings; @Inject @@ -76,9 +71,7 @@ public class AuthMePlayerListener implements Listener { @Inject private SpawnLoader spawnLoader; @Inject - private ValidationService validationService; - @Inject - private PermissionsManager permissionsManager; + private PluginHooks pluginHooks; private void sendLoginOrRegisterMessage(final Player player) { bukkitService.runTaskAsynchronously(new Runnable() { @@ -249,7 +242,7 @@ public class AuthMePlayerListener implements Listener { } if (!antiBot.antibotKicked.contains(player.getName())) { - plugin.getManagement().performQuit(player, true); + management.performQuit(player, true); } } @@ -287,7 +280,7 @@ public class AuthMePlayerListener implements Listener { * @note little hack cause InventoryOpenEvent cannot be cancelled for * real, cause no packet is send to server by client for the main inv */ - Bukkit.getScheduler().scheduleSyncDelayedTask(plugin, new Runnable() { + bukkitService.scheduleSyncDelayedTask(new Runnable() { @Override public void run() { player.closeInventory(); @@ -307,7 +300,7 @@ public class AuthMePlayerListener implements Listener { if (Utils.checkAuth(player)) { return; } - if (plugin.getPluginHooks().isNpc(player)) { + if (pluginHooks.isNpc(player)) { return; } event.setCancelled(true); diff --git a/src/test/java/fr/xephi/authme/AntiBotTest.java b/src/test/java/fr/xephi/authme/AntiBotTest.java index 9950963a..ddf41831 100644 --- a/src/test/java/fr/xephi/authme/AntiBotTest.java +++ b/src/test/java/fr/xephi/authme/AntiBotTest.java @@ -164,7 +164,7 @@ public class AntiBotTest { AntiBot antiBot = createListeningAntiBot(); // when - antiBot.checkAntiBot(player); + antiBot.handlePlayerJoin(player); // then @SuppressWarnings("unchecked") @@ -194,7 +194,7 @@ public class AntiBotTest { AntiBot antiBot = createListeningAntiBot(); // when - antiBot.checkAntiBot(player); + antiBot.handlePlayerJoin(player); // then @SuppressWarnings("rawtypes") From 64aacb12dbd7d16707ecdf92c5330a0d23a90d40 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Sun, 29 May 2016 15:00:16 +0200 Subject: [PATCH 114/200] Unit test verifications done on joining - Write unit tests for checks done when a player joins - Move join event handler methods back to PlayerListener; move join check logic to new separate class --- src/main/java/fr/xephi/authme/AuthMe.java | 2 - .../authme/listener/AuthMePlayerListener.java | 80 ++++ .../listener/FailedVerificationException.java | 32 ++ ...rJoinListener.java => OnJoinVerifier.java} | 394 ++++++---------- .../listener/ListenerConsistencyTest.java | 8 +- .../authme/listener/OnJoinVerifierTest.java | 419 ++++++++++++++++++ 6 files changed, 682 insertions(+), 253 deletions(-) create mode 100644 src/main/java/fr/xephi/authme/listener/FailedVerificationException.java rename src/main/java/fr/xephi/authme/listener/{AuthMePlayerJoinListener.java => OnJoinVerifier.java} (56%) create mode 100644 src/test/java/fr/xephi/authme/listener/OnJoinVerifierTest.java diff --git a/src/main/java/fr/xephi/authme/AuthMe.java b/src/main/java/fr/xephi/authme/AuthMe.java index 98f6cca2..9c2124d7 100644 --- a/src/main/java/fr/xephi/authme/AuthMe.java +++ b/src/main/java/fr/xephi/authme/AuthMe.java @@ -22,7 +22,6 @@ import fr.xephi.authme.initialization.MetricsStarter; import fr.xephi.authme.listener.AuthMeBlockListener; import fr.xephi.authme.listener.AuthMeEntityListener; import fr.xephi.authme.listener.AuthMeInventoryPacketAdapter; -import fr.xephi.authme.listener.AuthMePlayerJoinListener; import fr.xephi.authme.listener.AuthMePlayerListener; import fr.xephi.authme.listener.AuthMePlayerListener16; import fr.xephi.authme.listener.AuthMePlayerListener18; @@ -360,7 +359,6 @@ public class AuthMe extends JavaPlugin { pluginManager.registerEvents(initializer.get(AuthMeBlockListener.class), this); pluginManager.registerEvents(initializer.get(AuthMeEntityListener.class), this); pluginManager.registerEvents(initializer.get(AuthMeServerListener.class), this); - pluginManager.registerEvents(initializer.get(AuthMePlayerJoinListener.class), this); // Try to register 1.6 player listeners try { diff --git a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java index ae3b49c4..ce882bcb 100644 --- a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java +++ b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java @@ -1,6 +1,9 @@ package fr.xephi.authme.listener; +import com.google.common.io.ByteArrayDataOutput; +import com.google.common.io.ByteStreams; import fr.xephi.authme.AntiBot; +import fr.xephi.authme.AuthMe; import fr.xephi.authme.cache.auth.PlayerAuth; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.hooks.PluginHooks; @@ -24,6 +27,7 @@ import org.bukkit.event.entity.EntityDamageByEntityEvent; import org.bukkit.event.inventory.InventoryClickEvent; import org.bukkit.event.inventory.InventoryOpenEvent; import org.bukkit.event.player.AsyncPlayerChatEvent; +import org.bukkit.event.player.AsyncPlayerPreLoginEvent; import org.bukkit.event.player.PlayerBedEnterEvent; import org.bukkit.event.player.PlayerCommandPreprocessEvent; import org.bukkit.event.player.PlayerDropItemEvent; @@ -33,6 +37,7 @@ import org.bukkit.event.player.PlayerInteractEvent; import org.bukkit.event.player.PlayerItemConsumeEvent; import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.event.player.PlayerKickEvent; +import org.bukkit.event.player.PlayerLoginEvent; import org.bukkit.event.player.PlayerMoveEvent; import org.bukkit.event.player.PlayerPickupItemEvent; import org.bukkit.event.player.PlayerQuitEvent; @@ -72,6 +77,10 @@ public class AuthMePlayerListener implements Listener { private SpawnLoader spawnLoader; @Inject private PluginHooks pluginHooks; + @Inject + private OnJoinVerifier onJoinVerifier; + @Inject + private AuthMe plugin; private void sendLoginOrRegisterMessage(final Player player) { bukkitService.runTaskAsynchronously(new Runnable() { @@ -208,6 +217,77 @@ public class AuthMePlayerListener implements Listener { } } + @EventHandler(priority = EventPriority.LOW) + public void onPlayerJoin(PlayerJoinEvent event) { + final Player player = event.getPlayer(); + if (player != null) { + // Schedule login task so works after the prelogin + // (Fix found by Koolaid5000) + bukkitService.runTask(new Runnable() { + @Override + public void run() { + management.performJoin(player); + } + }); + } + } + + // Note ljacqu 20160528: AsyncPlayerPreLoginEvent is not fired by all servers in offline mode + // e.g. CraftBukkit does not. So we need to run crucial things in onPlayerLogin, too + @EventHandler(priority = EventPriority.HIGHEST) + public void onPreLogin(AsyncPlayerPreLoginEvent event) { + final String name = event.getName().toLowerCase(); + final boolean isAuthAvailable = dataSource.isAuthAvailable(event.getName()); + + try { + // Potential performance improvement: make checkAntiBot not require `isAuthAvailable` info and use + // "checkKickNonRegistered" as last -> no need to query the DB before checking antibot / name + onJoinVerifier.checkAntibot(name, isAuthAvailable); + onJoinVerifier.checkKickNonRegistered(isAuthAvailable); + onJoinVerifier.checkIsValidName(name); + } catch (FailedVerificationException e) { + event.setKickMessage(m.retrieveSingle(e.getReason(), e.getArgs())); + event.setLoginResult(AsyncPlayerPreLoginEvent.Result.KICK_OTHER); + } + } + + @EventHandler(priority = EventPriority.HIGHEST) + public void onPlayerLogin(PlayerLoginEvent event) { + final Player player = event.getPlayer(); + if (player == null || Utils.isUnrestricted(player)) { + return; + } else if (onJoinVerifier.refusePlayerForFullServer(event)) { + return; + } else if (event.getResult() != PlayerLoginEvent.Result.ALLOWED) { + return; + } + + final String name = player.getName().toLowerCase(); + final PlayerAuth auth = dataSource.getAuth(player.getName()); + final boolean isAuthAvailable = auth != null; + + try { + onJoinVerifier.checkAntibot(name, isAuthAvailable); + onJoinVerifier.checkKickNonRegistered(isAuthAvailable); + onJoinVerifier.checkIsValidName(name); + onJoinVerifier.checkNameCasing(player, auth); + onJoinVerifier.checkSingleSession(player); + onJoinVerifier.checkPlayerCountry(isAuthAvailable, event); + } catch (FailedVerificationException e) { + event.setKickMessage(m.retrieveSingle(e.getReason(), e.getArgs())); + event.setResult(PlayerLoginEvent.Result.KICK_OTHER); + return; + } + + antiBot.handlePlayerJoin(player); + + if (settings.getProperty(HooksSettings.BUNGEECORD)) { + ByteArrayDataOutput out = ByteStreams.newDataOutput(); + out.writeUTF("IP"); + player.sendPluginMessage(plugin, "BungeeCord", out.toByteArray()); + } + } + @EventHandler(priority = EventPriority.HIGHEST) public void onPlayerQuit(PlayerQuitEvent event) { Player player = event.getPlayer(); diff --git a/src/main/java/fr/xephi/authme/listener/FailedVerificationException.java b/src/main/java/fr/xephi/authme/listener/FailedVerificationException.java new file mode 100644 index 00000000..e560ca7f --- /dev/null +++ b/src/main/java/fr/xephi/authme/listener/FailedVerificationException.java @@ -0,0 +1,32 @@ +package fr.xephi.authme.listener; + +import fr.xephi.authme.output.MessageKey; +import fr.xephi.authme.util.StringUtils; + +/** + * Exception thrown when a verification has failed. + */ +public class FailedVerificationException extends Exception { + + private final MessageKey reason; + private final String[] args; + + public FailedVerificationException(MessageKey reason, String... args) { + this.reason = reason; + this.args = args; + } + + public MessageKey getReason() { + return reason; + } + + public String[] getArgs() { + return args; + } + + @Override + public String toString() { + return getClass().getSimpleName() + ": reason=" + (reason == null ? "null" : reason) + + ";args=" + (args == null ? "null" : StringUtils.join(", ", args)); + } +} diff --git a/src/main/java/fr/xephi/authme/listener/AuthMePlayerJoinListener.java b/src/main/java/fr/xephi/authme/listener/OnJoinVerifier.java similarity index 56% rename from src/main/java/fr/xephi/authme/listener/AuthMePlayerJoinListener.java rename to src/main/java/fr/xephi/authme/listener/OnJoinVerifier.java index 5343f214..64f8c3f7 100644 --- a/src/main/java/fr/xephi/authme/listener/AuthMePlayerJoinListener.java +++ b/src/main/java/fr/xephi/authme/listener/OnJoinVerifier.java @@ -1,9 +1,6 @@ package fr.xephi.authme.listener; -import com.google.common.io.ByteArrayDataOutput; -import com.google.common.io.ByteStreams; import fr.xephi.authme.AntiBot; -import fr.xephi.authme.AuthMe; import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.cache.auth.PlayerAuth; import fr.xephi.authme.cache.auth.PlayerCache; @@ -15,9 +12,7 @@ import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.output.Messages; import fr.xephi.authme.permission.PermissionsManager; import fr.xephi.authme.permission.PlayerStatePermission; -import fr.xephi.authme.process.Management; import fr.xephi.authme.settings.NewSetting; -import fr.xephi.authme.settings.properties.HooksSettings; import fr.xephi.authme.settings.properties.ProtectionSettings; import fr.xephi.authme.settings.properties.RegistrationSettings; import fr.xephi.authme.settings.properties.RestrictionSettings; @@ -25,12 +20,8 @@ import fr.xephi.authme.util.BukkitService; import fr.xephi.authme.util.StringUtils; import fr.xephi.authme.util.Utils; import fr.xephi.authme.util.ValidationService; +import org.bukkit.Server; import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.Listener; -import org.bukkit.event.player.AsyncPlayerPreLoginEvent; -import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.event.player.PlayerLoginEvent; import javax.annotation.PostConstruct; @@ -39,103 +30,33 @@ import java.util.Collection; import java.util.regex.Pattern; /** - * Listener for player join events. + * Service for performing various verifications when a player joins. */ -public class AuthMePlayerJoinListener implements Listener, Reloadable { +class OnJoinVerifier implements Reloadable { - @Inject - private BukkitService bukkitService; - @Inject - private DataSource dataSource; - @Inject - private AntiBot antiBot; - @Inject - private Management management; @Inject private NewSetting settings; @Inject - private Messages m; + private DataSource dataSource; + @Inject + private Messages messages; @Inject private PermissionsManager permissionsManager; @Inject + private AntiBot antiBot; + @Inject private ValidationService validationService; @Inject - private AuthMe plugin; + private BukkitService bukkitService; @Inject private LimboCache limboCache; + @Inject + private Server server; private Pattern nicknamePattern; - @EventHandler(priority = EventPriority.LOW) - public void onPlayerJoin(PlayerJoinEvent event) { - final Player player = event.getPlayer(); - if (player != null) { - // Schedule login task so works after the prelogin - // (Fix found by Koolaid5000) - bukkitService.runTask(new Runnable() { - @Override - public void run() { - management.performJoin(player); - } - }); - } - } + OnJoinVerifier() { } - // Note ljacqu 20160528: AsyncPlayerPreLoginEvent is not fired by all servers in offline mode - // e.g. CraftBukkit does not. So we need to run crucial things in onPlayerLogin, too - @EventHandler(priority = EventPriority.HIGHEST) - public void onPreLogin(AsyncPlayerPreLoginEvent event) { - final String name = event.getName().toLowerCase(); - final boolean isAuthAvailable = dataSource.isAuthAvailable(event.getName()); - - try { - // Potential performance improvement: make checkAntiBot not require `isAuthAvailable` info and use - // "checkKickNonRegistered" as last -> no need to query the DB before checking antibot / name - checkAntibot(name, isAuthAvailable); - checkKickNonRegistered(isAuthAvailable); - checkIsValidName(name); - } catch (VerificationFailedException e) { - event.setKickMessage(m.retrieveSingle(e.getReason(), e.getArgs())); - event.setLoginResult(AsyncPlayerPreLoginEvent.Result.KICK_OTHER); - } - } - - @EventHandler(priority = EventPriority.HIGHEST) - public void onPlayerLogin(PlayerLoginEvent event) { - final Player player = event.getPlayer(); - if (player == null || Utils.isUnrestricted(player)) { - return; - } else if (refusePlayerForFullServer(event)) { - return; - } else if (event.getResult() != PlayerLoginEvent.Result.ALLOWED) { - return; - } - - final String name = player.getName().toLowerCase(); - final PlayerAuth auth = dataSource.getAuth(player.getName()); - final boolean isAuthAvailable = auth != null; - - try { - checkAntibot(name, isAuthAvailable); - checkKickNonRegistered(isAuthAvailable); - checkIsValidName(name); - checkNameCasing(player, auth); - checkSingleSession(player); - checkPlayerCountry(isAuthAvailable, event); - } catch (VerificationFailedException e) { - event.setKickMessage(m.retrieveSingle(e.getReason(), e.getArgs())); - event.setResult(PlayerLoginEvent.Result.KICK_OTHER); - return; - } - - antiBot.handlePlayerJoin(player); - - if (settings.getProperty(HooksSettings.BUNGEECORD)) { - ByteArrayDataOutput out = ByteStreams.newDataOutput(); - out.writeUTF("IP"); - player.sendPluginMessage(plugin, "BungeeCord", out.toByteArray()); - } - } @PostConstruct @Override @@ -150,6 +71,141 @@ public class AuthMePlayerJoinListener implements Listener, Reloadable { } } + /** + * Checks if Antibot is enabled. + * + * @param playerName the name of the player (lowercase) + * @param isAuthAvailable whether or not the player is registered + */ + public void checkAntibot(String playerName, boolean isAuthAvailable) throws FailedVerificationException { + if (antiBot.getAntiBotStatus() == AntiBot.AntiBotStatus.ACTIVE && !isAuthAvailable) { + antiBot.antibotKicked.addIfAbsent(playerName); + throw new FailedVerificationException(MessageKey.KICK_ANTIBOT); + } + } + + /** + * Checks whether non-registered players should be kicked, and if so, whether the player should be kicked. + * + * @param isAuthAvailable whether or not the player is registered + */ + public void checkKickNonRegistered(boolean isAuthAvailable) throws FailedVerificationException { + if (!isAuthAvailable && settings.getProperty(RestrictionSettings.KICK_NON_REGISTERED)) { + throw new FailedVerificationException(MessageKey.MUST_REGISTER_MESSAGE); + } + } + + /** + * Checks that the name adheres to the configured username restrictions. + * + * @param name the name to verify + */ + public void checkIsValidName(String name) throws FailedVerificationException { + if (name.length() > settings.getProperty(RestrictionSettings.MAX_NICKNAME_LENGTH) + || name.length() < settings.getProperty(RestrictionSettings.MIN_NICKNAME_LENGTH)) { + throw new FailedVerificationException(MessageKey.INVALID_NAME_LENGTH); + } + if (!nicknamePattern.matcher(name).matches()) { + throw new FailedVerificationException(MessageKey.INVALID_NAME_CHARACTERS, nicknamePattern.pattern()); + } + } + + /** + * Handles the case of a full server and verifies if the user's connection should really be refused + * by adjusting the event object accordingly. Attempts to kick a non-VIP player to make room if the + * joining player is a VIP. + * + * @param event the login event to verify + * @return true if the player's connection should be refused (i.e. the event does not need to be processed + * further), false if the player is not refused + */ + public boolean refusePlayerForFullServer(PlayerLoginEvent event) { + final Player player = event.getPlayer(); + if (event.getResult() != PlayerLoginEvent.Result.KICK_FULL) { + // Server is not full, no need to do anything + return false; + } else if (!permissionsManager.hasPermission(player, PlayerStatePermission.IS_VIP)) { + // Server is full and player is NOT VIP; set kick message and proceed with kick + event.setKickMessage(messages.retrieveSingle(MessageKey.KICK_FULL_SERVER)); + return true; + } + + // Server is full and player is VIP; attempt to kick a non-VIP player to make room + Collection onlinePlayers = bukkitService.getOnlinePlayers(); + if (onlinePlayers.size() < server.getMaxPlayers()) { + event.allow(); + return false; + } + Player nonVipPlayer = generateKickPlayer(onlinePlayers); + if (nonVipPlayer != null) { + nonVipPlayer.kickPlayer(messages.retrieveSingle(MessageKey.KICK_FOR_VIP)); + event.allow(); + return false; + } else { + ConsoleLogger.info("VIP player " + player.getName() + " tried to join, but the server was full"); + event.setKickMessage(messages.retrieveSingle(MessageKey.KICK_FULL_SERVER)); + return true; + } + } + + /** + * Checks that the casing in the username corresponds to the one in the database, if so configured. + * + * @param player the player to verify + * @param auth the auth object associated with the player + */ + public void checkNameCasing(Player player, PlayerAuth auth) throws FailedVerificationException { + if (auth != null && settings.getProperty(RegistrationSettings.PREVENT_OTHER_CASE)) { + String realName = auth.getRealName(); // might be null or "Player" + String connectingName = player.getName(); + + if (StringUtils.isEmpty(realName) || "Player".equals(realName)) { + dataSource.updateRealName(connectingName.toLowerCase(), connectingName); + } else if (!realName.equals(connectingName)) { + throw new FailedVerificationException(MessageKey.INVALID_NAME_CASE, realName, connectingName); + } + } + } + + /** + * Checks that the player's country is admitted if he is not registered. + * + * @param isAuthAvailable whether or not the user is registered + * @param event the login event of the player + */ + public void checkPlayerCountry(boolean isAuthAvailable, + PlayerLoginEvent event) throws FailedVerificationException { + if (!isAuthAvailable && settings.getProperty(ProtectionSettings.ENABLE_PROTECTION)) { + String playerIp = event.getAddress().getHostAddress(); + if (!validationService.isCountryAdmitted(playerIp)) { + throw new FailedVerificationException(MessageKey.COUNTRY_BANNED_ERROR); + } + } + } + + /** + * Checks if a player with the same name (case-insensitive) is already playing and refuses the + * connection if so configured. + * + * @param player the player to verify + */ + public void checkSingleSession(Player player) throws FailedVerificationException { + if (!settings.getProperty(RestrictionSettings.FORCE_SINGLE_SESSION)) { + return; + } + + Player onlinePlayer = bukkitService.getPlayerExact(player.getName()); + if (onlinePlayer != null) { + String name = player.getName().toLowerCase(); + LimboPlayer limbo = limboCache.getLimboPlayer(name); + if (limbo != null && PlayerCache.getInstance().isAuthenticated(name)) { + Utils.addNormal(player, limbo.getGroup()); + limboCache.deleteLimboPlayer(name); + } + throw new FailedVerificationException(MessageKey.USERNAME_ALREADY_ONLINE_ERROR); + } + } + /** * Selects a non-VIP player to kick when a VIP player joins the server when full. * @@ -164,160 +220,4 @@ public class AuthMePlayerJoinListener implements Listener, Reloadable { } return null; } - - /** - * Checks if Antibot is enabled. - * - * @param playerName the name of the player (lowercase) - * @param isAuthAvailable whether or not the player is registered - */ - private void checkAntibot(String playerName, boolean isAuthAvailable) throws VerificationFailedException { - if (antiBot.getAntiBotStatus() == AntiBot.AntiBotStatus.ACTIVE && !isAuthAvailable) { - antiBot.antibotKicked.addIfAbsent(playerName); - throw new VerificationFailedException(MessageKey.KICK_ANTIBOT); - } - } - - /** - * Checks whether non-registered players should be kicked, and if so, whether the player should be kicked. - * - * @param isAuthAvailable whether or not the player is registered - */ - private void checkKickNonRegistered(boolean isAuthAvailable) throws VerificationFailedException { - if (!isAuthAvailable && settings.getProperty(RestrictionSettings.KICK_NON_REGISTERED)) { - throw new VerificationFailedException(MessageKey.MUST_REGISTER_MESSAGE); - } - } - - /** - * Checks that the name adheres to the configured username restrictions. - * - * @param name the name to verify - */ - private void checkIsValidName(String name) throws VerificationFailedException { - if (name.length() > settings.getProperty(RestrictionSettings.MAX_NICKNAME_LENGTH) - || name.length() < settings.getProperty(RestrictionSettings.MIN_NICKNAME_LENGTH)) { - throw new VerificationFailedException(MessageKey.INVALID_NAME_LENGTH); - } - if (!nicknamePattern.matcher(name).matches()) { - throw new VerificationFailedException(MessageKey.INVALID_NAME_CHARACTERS, nicknamePattern.pattern()); - } - } - - /** - * Handles the case of a full server and verifies if the user's connection should really be refused - * by adjusting the event object accordingly. Attempts to kick a non-VIP player to make room if the - * joining player is a VIP. - * - * @param event the login event to verify - * @return true if the player's connection should be refused (i.e. the event does not need to be processed - * further), false if the player is not refused - */ - private boolean refusePlayerForFullServer(PlayerLoginEvent event) { - final Player player = event.getPlayer(); - if (event.getResult() != PlayerLoginEvent.Result.KICK_FULL) { - // Server is not full, no need to do anything - return false; - } else if (!permissionsManager.hasPermission(player, PlayerStatePermission.IS_VIP)) { - // Server is full and player is NOT VIP; set kick message and proceed with kick - event.setKickMessage(m.retrieveSingle(MessageKey.KICK_FULL_SERVER)); - return true; - } - - // Server is full and player is VIP; attempt to kick a non-VIP player to make room - Collection onlinePlayers = bukkitService.getOnlinePlayers(); - if (onlinePlayers.size() < plugin.getServer().getMaxPlayers()) { - event.allow(); - return false; - } - Player nonVipPlayer = generateKickPlayer(onlinePlayers); - if (nonVipPlayer != null) { - nonVipPlayer.kickPlayer(m.retrieveSingle(MessageKey.KICK_FOR_VIP)); - event.allow(); - return false; - } else { - ConsoleLogger.info("VIP player " + player.getName() + " tried to join, but the server was full"); - event.setKickMessage(m.retrieveSingle(MessageKey.KICK_FULL_SERVER)); - return true; - } - } - - /** - * Checks that the casing in the username corresponds to the one in the database, if so configured. - * - * @param player the player to verify - * @param auth the auth object associated with the player - */ - private void checkNameCasing(Player player, PlayerAuth auth) throws VerificationFailedException { - if (auth != null && settings.getProperty(RegistrationSettings.PREVENT_OTHER_CASE)) { - String realName = auth.getRealName(); // might be null or "Player" - String connectingName = player.getName(); - - if (StringUtils.isEmpty(realName) || "Player".equals(realName)) { - dataSource.updateRealName(connectingName.toLowerCase(), connectingName); - } else if (!realName.equals(connectingName)) { - throw new VerificationFailedException(MessageKey.INVALID_NAME_CASE, realName, connectingName); - } - } - } - - /** - * Checks that the player's country is admitted if he is not registered. - * - * @param isAuthAvailable whether or not the user is registered - * @param event the login event of the player - */ - private void checkPlayerCountry(boolean isAuthAvailable, - PlayerLoginEvent event) throws VerificationFailedException { - if (!isAuthAvailable && settings.getProperty(ProtectionSettings.ENABLE_PROTECTION)) { - String playerIp = event.getAddress().getHostAddress(); - if (!validationService.isCountryAdmitted(playerIp)) { - throw new VerificationFailedException(MessageKey.COUNTRY_BANNED_ERROR); - } - } - } - - /** - * Checks if a player with the same name (case-insensitive) is already playing and refuses the - * connection if so configured. - * - * @param player the player to verify - */ - private void checkSingleSession(Player player) throws VerificationFailedException { - if (!settings.getProperty(RestrictionSettings.FORCE_SINGLE_SESSION)) { - return; - } - - Player onlinePlayer = bukkitService.getPlayerExact(player.getName()); - if (onlinePlayer != null) { - String name = player.getName().toLowerCase(); - LimboPlayer limbo = limboCache.getLimboPlayer(name); - if (limbo != null && PlayerCache.getInstance().isAuthenticated(name)) { - Utils.addNormal(player, limbo.getGroup()); - limboCache.deleteLimboPlayer(name); - } - throw new VerificationFailedException(MessageKey.USERNAME_ALREADY_ONLINE_ERROR); - } - } - - /** - * Exception thrown when a verification has failed and the player should be kicked. - */ - private static final class VerificationFailedException extends Exception { - private final MessageKey reason; - private final String[] args; - - public VerificationFailedException(MessageKey reason, String... args) { - this.reason = reason; - this.args = args; - } - - public MessageKey getReason() { - return reason; - } - - public String[] getArgs() { - return args; - } - } } diff --git a/src/test/java/fr/xephi/authme/listener/ListenerConsistencyTest.java b/src/test/java/fr/xephi/authme/listener/ListenerConsistencyTest.java index d3503d4d..4749dac4 100644 --- a/src/test/java/fr/xephi/authme/listener/ListenerConsistencyTest.java +++ b/src/test/java/fr/xephi/authme/listener/ListenerConsistencyTest.java @@ -21,11 +21,11 @@ import static org.junit.Assert.fail; public final class ListenerConsistencyTest { private static final Class[] LISTENERS = { AuthMeBlockListener.class, AuthMeEntityListener.class, - AuthMePlayerJoinListener.class, AuthMePlayerListener.class, AuthMePlayerListener16.class, - AuthMePlayerListener18.class, AuthMeServerListener.class }; + AuthMePlayerListener.class, AuthMePlayerListener16.class, AuthMePlayerListener18.class, + AuthMeServerListener.class }; - private static final Set CANCELED_EXCEPTIONS = Sets.newHashSet("AuthMePlayerJoinListener#onPlayerJoin", - "AuthMePlayerJoinListener#onPreLogin", "AuthMePlayerJoinListener#onPlayerLogin", + private static final Set CANCELED_EXCEPTIONS = Sets.newHashSet("AuthMePlayerListener#onPlayerJoin", + "AuthMePlayerListener#onPreLogin", "AuthMePlayerListener#onPlayerLogin", "AuthMePlayerListener#onPlayerQuit", "AuthMeServerListener#onPluginDisable", "AuthMeServerListener#onServerPing", "AuthMeServerListener#onPluginEnable", "AuthMePlayerListener#onJoinMessage"); diff --git a/src/test/java/fr/xephi/authme/listener/OnJoinVerifierTest.java b/src/test/java/fr/xephi/authme/listener/OnJoinVerifierTest.java new file mode 100644 index 00000000..4f003249 --- /dev/null +++ b/src/test/java/fr/xephi/authme/listener/OnJoinVerifierTest.java @@ -0,0 +1,419 @@ +package fr.xephi.authme.listener; + +import fr.xephi.authme.AntiBot; +import fr.xephi.authme.TestHelper; +import fr.xephi.authme.cache.auth.PlayerAuth; +import fr.xephi.authme.cache.limbo.LimboCache; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.output.MessageKey; +import fr.xephi.authme.output.Messages; +import fr.xephi.authme.permission.PermissionsManager; +import fr.xephi.authme.permission.PlayerStatePermission; +import fr.xephi.authme.settings.NewSetting; +import fr.xephi.authme.settings.properties.RegistrationSettings; +import fr.xephi.authme.settings.properties.RestrictionSettings; +import fr.xephi.authme.util.BukkitService; +import fr.xephi.authme.util.StringUtils; +import fr.xephi.authme.util.ValidationService; +import org.bukkit.Server; +import org.bukkit.entity.Player; +import org.bukkit.event.player.PlayerLoginEvent; +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.TypeSafeMatcher; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +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.verifyNoMoreInteractions; +import static org.mockito.Mockito.verifyZeroInteractions; + +/** + * Test for {@link OnJoinVerifier}. + */ +@RunWith(MockitoJUnitRunner.class) +public class OnJoinVerifierTest { + + @InjectMocks + private OnJoinVerifier onJoinVerifier; + + @Mock + private NewSetting settings; + @Mock + private DataSource dataSource; + @Mock + private Messages messages; + @Mock + private PermissionsManager permissionsManager; + @Mock + private AntiBot antiBot; + @Mock + private ValidationService validationService; + @Mock + private BukkitService bukkitService; + @Mock + private LimboCache limboCache; + @Mock + private Server server; + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @BeforeClass + public static void setUpLogger() { + TestHelper.setupLogger(); + } + + @Test + public void shouldNotDoAnythingForNormalEvent() { + // given + PlayerLoginEvent event = mock(PlayerLoginEvent.class); + given(event.getResult()).willReturn(PlayerLoginEvent.Result.ALLOWED); + + // when + boolean result = onJoinVerifier.refusePlayerForFullServer(event); + + // then + assertThat(result, equalTo(false)); + verify(event).getResult(); + verifyNoMoreInteractions(event); + verifyZeroInteractions(bukkitService); + verifyZeroInteractions(dataSource); + verifyZeroInteractions(permissionsManager); + } + + @Test + public void shouldRefuseNonVipPlayerForFullServer() { + // given + Player player = mock(Player.class); + PlayerLoginEvent event = new PlayerLoginEvent(player, "hostname", null); + event.setResult(PlayerLoginEvent.Result.KICK_FULL); + given(permissionsManager.hasPermission(player, PlayerStatePermission.IS_VIP)).willReturn(false); + String serverFullMessage = "server is full"; + given(messages.retrieveSingle(MessageKey.KICK_FULL_SERVER)).willReturn(serverFullMessage); + + // when + boolean result = onJoinVerifier.refusePlayerForFullServer(event); + + // then + assertThat(result, equalTo(true)); + assertThat(event.getResult(), equalTo(PlayerLoginEvent.Result.KICK_FULL)); + assertThat(event.getKickMessage(), equalTo(serverFullMessage)); + verifyZeroInteractions(bukkitService); + verifyZeroInteractions(dataSource); + } + + @Test + public void shouldKickNonVipForJoiningVipPlayer() { + // given + Player player = mock(Player.class); + PlayerLoginEvent event = new PlayerLoginEvent(player, "hostname", null); + event.setResult(PlayerLoginEvent.Result.KICK_FULL); + given(permissionsManager.hasPermission(player, PlayerStatePermission.IS_VIP)).willReturn(true); + List onlinePlayers = Arrays.asList(mock(Player.class), mock(Player.class)); + given(permissionsManager.hasPermission(onlinePlayers.get(0), PlayerStatePermission.IS_VIP)).willReturn(true); + given(permissionsManager.hasPermission(onlinePlayers.get(1), PlayerStatePermission.IS_VIP)).willReturn(false); + returnOnlineListFromBukkitServer(onlinePlayers); + given(server.getMaxPlayers()).willReturn(onlinePlayers.size()); + given(messages.retrieveSingle(MessageKey.KICK_FOR_VIP)).willReturn("kick for vip"); + + // when + boolean result = onJoinVerifier.refusePlayerForFullServer(event); + + // then + assertThat(result, equalTo(false)); + assertThat(event.getResult(), equalTo(PlayerLoginEvent.Result.ALLOWED)); + // First player is VIP, so expect no interactions there and second player to have been kicked + verifyZeroInteractions(onlinePlayers.get(0)); + verify(onlinePlayers.get(1)).kickPlayer("kick for vip"); + } + + @Test + public void shouldKickVipPlayerIfNoPlayerCanBeKicked() { + // given + Player player = mock(Player.class); + PlayerLoginEvent event = new PlayerLoginEvent(player, "hostname", null); + event.setResult(PlayerLoginEvent.Result.KICK_FULL); + given(permissionsManager.hasPermission(player, PlayerStatePermission.IS_VIP)).willReturn(true); + List onlinePlayers = Collections.singletonList(mock(Player.class)); + given(permissionsManager.hasPermission(onlinePlayers.get(0), PlayerStatePermission.IS_VIP)).willReturn(true); + returnOnlineListFromBukkitServer(onlinePlayers); + given(server.getMaxPlayers()).willReturn(onlinePlayers.size()); + given(messages.retrieveSingle(MessageKey.KICK_FULL_SERVER)).willReturn("kick full server"); + + // when + boolean result = onJoinVerifier.refusePlayerForFullServer(event); + + // then + assertThat(result, equalTo(true)); + assertThat(event.getResult(), equalTo(PlayerLoginEvent.Result.KICK_FULL)); + assertThat(event.getKickMessage(), equalTo("kick full server")); + verifyZeroInteractions(onlinePlayers.get(0)); + } + + @Test + public void shouldKickNonRegistered() throws FailedVerificationException { + // given + given(settings.getProperty(RestrictionSettings.KICK_NON_REGISTERED)).willReturn(true); + + // expect + expectValidationExceptionWith(MessageKey.MUST_REGISTER_MESSAGE); + + // when + onJoinVerifier.checkKickNonRegistered(false); + } + + @Test + public void shouldNotKickRegisteredPlayer() throws FailedVerificationException { + // given + given(settings.getProperty(RestrictionSettings.KICK_NON_REGISTERED)).willReturn(true); + + // when + onJoinVerifier.checkKickNonRegistered(true); + } + + @Test + public void shouldNotKickUnregisteredPlayer() throws FailedVerificationException { + // given + given(settings.getProperty(RestrictionSettings.KICK_NON_REGISTERED)).willReturn(false); + + // when + onJoinVerifier.checkKickNonRegistered(false); + } + + @Test + public void shouldAllowValidName() throws FailedVerificationException { + // given + given(settings.getProperty(RestrictionSettings.MIN_NICKNAME_LENGTH)).willReturn(4); + given(settings.getProperty(RestrictionSettings.MAX_NICKNAME_LENGTH)).willReturn(8); + given(settings.getProperty(RestrictionSettings.ALLOWED_NICKNAME_CHARACTERS)).willReturn("[a-zA-Z0-9]+"); + onJoinVerifier.reload(); // @PostConstruct method + + // when + onJoinVerifier.checkIsValidName("Bobby5"); + } + + @Test + public void shouldRejectTooLongName() throws FailedVerificationException { + // given + given(settings.getProperty(RestrictionSettings.MIN_NICKNAME_LENGTH)).willReturn(4); + given(settings.getProperty(RestrictionSettings.MAX_NICKNAME_LENGTH)).willReturn(8); + given(settings.getProperty(RestrictionSettings.ALLOWED_NICKNAME_CHARACTERS)).willReturn("[a-zA-Z0-9]+"); + onJoinVerifier.reload(); // @PostConstruct method + + // expect + expectValidationExceptionWith(MessageKey.INVALID_NAME_LENGTH); + + // when + onJoinVerifier.checkIsValidName("longerthaneight"); + } + + @Test + public void shouldRejectTooShortName() throws FailedVerificationException { + // given + given(settings.getProperty(RestrictionSettings.MIN_NICKNAME_LENGTH)).willReturn(4); + given(settings.getProperty(RestrictionSettings.MAX_NICKNAME_LENGTH)).willReturn(8); + given(settings.getProperty(RestrictionSettings.ALLOWED_NICKNAME_CHARACTERS)).willReturn("[a-zA-Z0-9]+"); + onJoinVerifier.reload(); // @PostConstruct method + + // expect + expectValidationExceptionWith(MessageKey.INVALID_NAME_LENGTH); + + // when + onJoinVerifier.checkIsValidName("abc"); + } + + @Test + public void shouldRejectNameWithInvalidCharacters() throws FailedVerificationException { + // given + given(settings.getProperty(RestrictionSettings.MIN_NICKNAME_LENGTH)).willReturn(4); + given(settings.getProperty(RestrictionSettings.MAX_NICKNAME_LENGTH)).willReturn(8); + given(settings.getProperty(RestrictionSettings.ALLOWED_NICKNAME_CHARACTERS)).willReturn("[a-zA-Z0-9]+"); + onJoinVerifier.reload(); // @PostConstruct method + + // expect + expectValidationExceptionWith(MessageKey.INVALID_NAME_CHARACTERS, "[a-zA-Z0-9]+"); + + // when + onJoinVerifier.checkIsValidName("Tester!"); + } + + @Test + public void shouldAllowProperlyCasedName() throws FailedVerificationException { + // given + Player player = newPlayerWithName("Bobby"); + PlayerAuth auth = PlayerAuth.builder().name("bobby").realName("Bobby").build(); + given(settings.getProperty(RegistrationSettings.PREVENT_OTHER_CASE)).willReturn(true); + + // when + onJoinVerifier.checkNameCasing(player, auth); + + // then + verifyZeroInteractions(dataSource); + } + + @Test + public void shouldRejectNameWithWrongCasing() throws FailedVerificationException { + // given + Player player = newPlayerWithName("Tester"); + PlayerAuth auth = PlayerAuth.builder().name("tester").realName("testeR").build(); + given(settings.getProperty(RegistrationSettings.PREVENT_OTHER_CASE)).willReturn(true); + + // expect + expectValidationExceptionWith(MessageKey.INVALID_NAME_CASE, "testeR", "Tester"); + + // when / then + onJoinVerifier.checkNameCasing(player, auth); + verifyZeroInteractions(dataSource); + } + + @Test + public void shouldUpdateMissingRealName() throws FailedVerificationException { + // given + Player player = newPlayerWithName("Authme"); + PlayerAuth auth = PlayerAuth.builder().name("authme").realName("").build(); + given(settings.getProperty(RegistrationSettings.PREVENT_OTHER_CASE)).willReturn(true); + + // when + onJoinVerifier.checkNameCasing(player, auth); + + // then + verify(dataSource).updateRealName("authme", "Authme"); + } + + @Test + public void shouldUpdateDefaultRealName() throws FailedVerificationException { + // given + Player player = newPlayerWithName("SOMEONE"); + PlayerAuth auth = PlayerAuth.builder().name("someone").realName("Player").build(); + given(settings.getProperty(RegistrationSettings.PREVENT_OTHER_CASE)).willReturn(true); + + // when + onJoinVerifier.checkNameCasing(player, auth); + + // then + verify(dataSource).updateRealName("someone", "SOMEONE"); + } + + @Test + public void shouldAcceptCasingMismatchForDisabledSetting() throws FailedVerificationException { + // given + Player player = newPlayerWithName("Test"); + PlayerAuth auth = PlayerAuth.builder().name("test").realName("TEST").build(); + given(settings.getProperty(RegistrationSettings.PREVENT_OTHER_CASE)).willReturn(false); + + // when + onJoinVerifier.checkNameCasing(player, auth); + + // then + verifyZeroInteractions(dataSource); + } + + @Test + public void shouldAcceptNameForUnregisteredAccount() throws FailedVerificationException { + // given + Player player = newPlayerWithName("MyPlayer"); + PlayerAuth auth = null; + given(settings.getProperty(RegistrationSettings.PREVENT_OTHER_CASE)).willReturn(true); + + // when + onJoinVerifier.checkNameCasing(player, auth); + + // then + verifyZeroInteractions(dataSource); + } + + @Test + public void shouldAcceptNameThatIsNotOnline() throws FailedVerificationException { + // given + Player player = newPlayerWithName("bobby"); + given(settings.getProperty(RestrictionSettings.FORCE_SINGLE_SESSION)).willReturn(true); + given(bukkitService.getPlayerExact("bobby")).willReturn(null); + + // when + onJoinVerifier.checkSingleSession(player); + + // then + verifyZeroInteractions(limboCache); + } + + @Test + public void shouldRejectNameAlreadyOnline() throws FailedVerificationException { + // given + Player player = newPlayerWithName("Charlie"); + Player onlinePlayer = newPlayerWithName("charlie"); + given(bukkitService.getPlayerExact("Charlie")).willReturn(onlinePlayer); + given(settings.getProperty(RestrictionSettings.FORCE_SINGLE_SESSION)).willReturn(true); + + // expect + expectValidationExceptionWith(MessageKey.USERNAME_ALREADY_ONLINE_ERROR); + + // when / then + onJoinVerifier.checkSingleSession(player); + verify(limboCache).getLimboPlayer("charlie"); + } + + @Test + public void shouldAcceptAlreadyOnlineNameForDisabledSetting() throws FailedVerificationException { + // given + Player player = newPlayerWithName("Felipe"); + given(settings.getProperty(RestrictionSettings.FORCE_SINGLE_SESSION)).willReturn(false); + + // when + onJoinVerifier.checkSingleSession(player); + + // then + verifyZeroInteractions(bukkitService); + verifyZeroInteractions(limboCache); + } + + private static Player newPlayerWithName(String name) { + Player player = mock(Player.class); + given(player.getName()).willReturn(name); + return player; + } + + @SuppressWarnings("unchecked") + private void returnOnlineListFromBukkitServer(Collection onlineList) { + // Note ljacqu 20160529: The compiler gets lost in generics because Collection is returned + // from getOnlinePlayers(). We need to uncheck onlineList to a simple Collection or it will refuse to compile. + given(bukkitService.getOnlinePlayers()).willReturn((Collection) onlineList); + } + + private void expectValidationExceptionWith(MessageKey messageKey, String... args) { + //expectedException.expect(FailedVerificationException.class); + expectedException.expect(exceptionWithData(messageKey, args)); + } + + private static Matcher exceptionWithData(final MessageKey messageKey, + final String... args) { + return new TypeSafeMatcher() { + @Override + protected boolean matchesSafely(FailedVerificationException item) { + return messageKey.equals(item.getReason()) && Arrays.equals(args, item.getArgs()); + } + + @Override + public void describeTo(Description description) { + description.appendValue("VerificationFailedException: reason=" + messageKey + ";args=" + + (args == null ? "null" : StringUtils.join(", ", args))); + } + }; + } + +} From 52c0c7dd640217572d32587237abedb9cc35fca3 Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Sun, 29 May 2016 16:34:03 +0200 Subject: [PATCH 115/200] playerlistener cleanup --- .../executable/authme/ConverterCommand.java | 4 -- .../authme/listener/AuthMePlayerListener.java | 39 ++++--------------- .../listener/FailedVerificationException.java | 1 + .../fr/xephi/authme/output/MessageKey.java | 2 + .../properties/RestrictionSettings.java | 6 --- src/main/resources/messages/messages_en.yml | 1 + .../authme/listener/OnJoinVerifierTest.java | 2 +- 7 files changed, 13 insertions(+), 42 deletions(-) diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/ConverterCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/ConverterCommand.java index 4a02cfdc..5c4800ed 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/ConverterCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/ConverterCommand.java @@ -1,6 +1,5 @@ package fr.xephi.authme.command.executable.authme; -import fr.xephi.authme.AuthMe; import fr.xephi.authme.command.CommandService; import fr.xephi.authme.command.ExecutableCommand; import fr.xephi.authme.converter.Converter; @@ -23,9 +22,6 @@ import java.util.List; */ public class ConverterCommand implements ExecutableCommand { - @Inject - private AuthMe authMe; - @Inject private BukkitService bukkitService; diff --git a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java index ce882bcb..9e471b85 100644 --- a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java +++ b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java @@ -51,7 +51,6 @@ import java.util.concurrent.ConcurrentHashMap; import static fr.xephi.authme.listener.ListenerService.shouldCancelEvent; import static fr.xephi.authme.settings.properties.RestrictionSettings.ALLOWED_MOVEMENT_RADIUS; -import static fr.xephi.authme.settings.properties.RestrictionSettings.ALLOW_ALL_COMMANDS_IF_REGISTRATION_IS_OPTIONAL; import static fr.xephi.authme.settings.properties.RestrictionSettings.ALLOW_UNAUTHED_MOVEMENT; /** @@ -82,41 +81,21 @@ public class AuthMePlayerListener implements Listener { @Inject private AuthMe plugin; - private void sendLoginOrRegisterMessage(final Player player) { - bukkitService.runTaskAsynchronously(new Runnable() { - @Override - public void run() { - if (dataSource.isAuthAvailable(player.getName().toLowerCase())) { - m.send(player, MessageKey.LOGIN_MESSAGE); - } else { - if (settings.getProperty(RegistrationSettings.USE_EMAIL_REGISTRATION)) { - m.send(player, MessageKey.REGISTER_EMAIL_MESSAGE); - } else { - m.send(player, MessageKey.REGISTER_MESSAGE); - } - } - } - }); - } - @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) public void onPlayerCommandPreprocess(PlayerCommandPreprocessEvent event) { String cmd = event.getMessage().split(" ")[0].toLowerCase(); - if (settings.getProperty(HooksSettings.USE_ESSENTIALS_MOTD) && "/motd".equals(cmd)) { - return; - } - if (!settings.getProperty(RegistrationSettings.FORCE) - && settings.getProperty(ALLOW_ALL_COMMANDS_IF_REGISTRATION_IS_OPTIONAL)) { + if (settings.getProperty(HooksSettings.USE_ESSENTIALS_MOTD) && cmd.equals("/motd")) { return; } if (settings.getProperty(RestrictionSettings.ALLOW_COMMANDS).contains(cmd)) { return; } - if (Utils.checkAuth(event.getPlayer())) { + final Player player = event.getPlayer(); + if (!shouldCancelEvent(player)) { return; } event.setCancelled(true); - sendLoginOrRegisterMessage(event.getPlayer()); + m.send(player, MessageKey.DENIED_COMMAND); } @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) @@ -128,12 +107,7 @@ public class AuthMePlayerListener implements Listener { final Player player = event.getPlayer(); if (shouldCancelEvent(player)) { event.setCancelled(true); - bukkitService.runTaskAsynchronously(new Runnable() { - @Override - public void run() { - m.send(player, MessageKey.DENIED_CHAT); - } - }); + m.send(player, MessageKey.DENIED_CHAT); } else if (settings.getProperty(RestrictionSettings.HIDE_CHAT)) { Set recipients = event.getRecipients(); Iterator iter = recipients.iterator(); @@ -143,6 +117,9 @@ public class AuthMePlayerListener implements Listener { iter.remove(); } } + if (recipients.size() == 0) { + event.setCancelled(true); + } } } diff --git a/src/main/java/fr/xephi/authme/listener/FailedVerificationException.java b/src/main/java/fr/xephi/authme/listener/FailedVerificationException.java index e560ca7f..31957cdb 100644 --- a/src/main/java/fr/xephi/authme/listener/FailedVerificationException.java +++ b/src/main/java/fr/xephi/authme/listener/FailedVerificationException.java @@ -6,6 +6,7 @@ import fr.xephi.authme.util.StringUtils; /** * Exception thrown when a verification has failed. */ +@SuppressWarnings("serial") public class FailedVerificationException extends Exception { private final MessageKey reason; diff --git a/src/main/java/fr/xephi/authme/output/MessageKey.java b/src/main/java/fr/xephi/authme/output/MessageKey.java index 0f052c27..eb7a5238 100644 --- a/src/main/java/fr/xephi/authme/output/MessageKey.java +++ b/src/main/java/fr/xephi/authme/output/MessageKey.java @@ -5,6 +5,8 @@ package fr.xephi.authme.output; */ public enum MessageKey { + DENIED_COMMAND("denied_command"), + SAME_IP_ONLINE("same_ip_online"), DENIED_CHAT("denied_chat"), diff --git a/src/main/java/fr/xephi/authme/settings/properties/RestrictionSettings.java b/src/main/java/fr/xephi/authme/settings/properties/RestrictionSettings.java index 96c790e2..926ddac7 100644 --- a/src/main/java/fr/xephi/authme/settings/properties/RestrictionSettings.java +++ b/src/main/java/fr/xephi/authme/settings/properties/RestrictionSettings.java @@ -22,12 +22,6 @@ public class RestrictionSettings implements SettingsClass { public static final Property HIDE_CHAT = newProperty("settings.restrictions.hideChat", false); - @Comment({ - "Allow unlogged users to use all the commands if registration is not forced!", - "WARNING: use this only if you need it!"}) - public static final Property ALLOW_ALL_COMMANDS_IF_REGISTRATION_IS_OPTIONAL = - newProperty("settings.restrictions.allowAllCommandsIfRegistrationIsOptional", false); - @Comment("Allowed commands for unauthenticated players") public static final Property> ALLOW_COMMANDS = newListProperty("settings.restrictions.allowCommands", diff --git a/src/main/resources/messages/messages_en.yml b/src/main/resources/messages/messages_en.yml index 2623ac0b..75a4fcea 100644 --- a/src/main/resources/messages/messages_en.yml +++ b/src/main/resources/messages/messages_en.yml @@ -1,3 +1,4 @@ +denied_command: '&cIn order to be able to use this command you must be authenticated!' same_ip_online: 'A player with the same IP is already in game!' denied_chat: '&cIn order to be able to chat you must be authenticated!' kick_antibot: 'AntiBot protection mode is enabled! You have to wait some minutes before joining the server.' diff --git a/src/test/java/fr/xephi/authme/listener/OnJoinVerifierTest.java b/src/test/java/fr/xephi/authme/listener/OnJoinVerifierTest.java index 4f003249..eb7e1511 100644 --- a/src/test/java/fr/xephi/authme/listener/OnJoinVerifierTest.java +++ b/src/test/java/fr/xephi/authme/listener/OnJoinVerifierTest.java @@ -388,7 +388,7 @@ public class OnJoinVerifierTest { return player; } - @SuppressWarnings("unchecked") + @SuppressWarnings({ "unchecked", "rawtypes" }) private void returnOnlineListFromBukkitServer(Collection onlineList) { // Note ljacqu 20160529: The compiler gets lost in generics because Collection is returned // from getOnlinePlayers(). We need to uncheck onlineList to a simple Collection or it will refuse to compile. From f5b7246d1d6a66578032aaef33c6c8604e431d86 Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Sun, 29 May 2016 17:10:33 +0200 Subject: [PATCH 116/200] remove useless stuff from the player listener --- .../authme/listener/AuthMePlayerListener.java | 47 ++++++------------- 1 file changed, 14 insertions(+), 33 deletions(-) diff --git a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java index 9e471b85..fa3b5215 100644 --- a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java +++ b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java @@ -133,18 +133,21 @@ public class AuthMePlayerListener implements Listener { * Limit player X and Z movements to 1 block * Deny player Y+ movements (allows falling) */ - if (event.getFrom().getBlockX() == event.getTo().getBlockX() - && event.getFrom().getBlockZ() == event.getTo().getBlockZ() - && event.getFrom().getY() - event.getTo().getY() >= 0) { + Location from = event.getFrom(); + Location to = event.getTo(); + if (from.getBlockX() == to.getBlockX() + && from.getBlockZ() == to.getBlockZ() + && from.getY() - to.getY() >= 0) { return; } Player player = event.getPlayer(); - if (Utils.checkAuth(player)) { + if (!shouldCancelEvent(player)) { return; } if (!settings.getProperty(RestrictionSettings.ALLOW_UNAUTHED_MOVEMENT)) { + // "cancel" the event event.setTo(event.getFrom()); if (settings.getProperty(RestrictionSettings.REMOVE_SPEED)) { player.setFlySpeed(0.0f); @@ -231,7 +234,7 @@ public class AuthMePlayerListener implements Listener { @EventHandler(priority = EventPriority.HIGHEST) public void onPlayerLogin(PlayerLoginEvent event) { final Player player = event.getPlayer(); - if (player == null || Utils.isUnrestricted(player)) { + if (Utils.isUnrestricted(player)) { return; } else if (onJoinVerifier.refusePlayerForFullServer(event)) { return; @@ -241,7 +244,7 @@ public class AuthMePlayerListener implements Listener { final String name = player.getName().toLowerCase(); final PlayerAuth auth = dataSource.getAuth(player.getName()); - final boolean isAuthAvailable = auth != null; + final boolean isAuthAvailable = (auth != null); try { onJoinVerifier.checkAntibot(name, isAuthAvailable); @@ -257,22 +260,12 @@ public class AuthMePlayerListener implements Listener { } antiBot.handlePlayerJoin(player); - - if (settings.getProperty(HooksSettings.BUNGEECORD)) { - ByteArrayDataOutput out = ByteStreams.newDataOutput(); - out.writeUTF("IP"); - player.sendPluginMessage(plugin, "BungeeCord", out.toByteArray()); - } } @EventHandler(priority = EventPriority.HIGHEST) public void onPlayerQuit(PlayerQuitEvent event) { Player player = event.getPlayer(); - if (player == null) { - return; - } - if (settings.getProperty(RegistrationSettings.REMOVE_LEAVE_MESSAGE)) { event.setQuitMessage(null); } @@ -288,16 +281,6 @@ public class AuthMePlayerListener implements Listener { public void onPlayerKick(PlayerKickEvent event) { Player player = event.getPlayer(); - if (player == null) { - return; - } - - if (!settings.getProperty(RestrictionSettings.FORCE_SINGLE_SESSION) - && event.getReason().equals(m.retrieveSingle(MessageKey.USERNAME_ALREADY_ONLINE_ERROR))) { - event.setCancelled(true); - return; - } - if (!antiBot.antibotKicked.contains(player.getName())) { management.performQuit(player, true); } @@ -328,7 +311,7 @@ public class AuthMePlayerListener implements Listener { public void onPlayerInventoryOpen(InventoryOpenEvent event) { final Player player = (Player) event.getPlayer(); - if (!ListenerService.shouldCancelEvent(player)) { + if (!shouldCancelEvent(player)) { return; } event.setCancelled(true); @@ -354,10 +337,7 @@ public class AuthMePlayerListener implements Listener { return; } Player player = (Player) event.getWhoClicked(); - if (Utils.checkAuth(player)) { - return; - } - if (pluginHooks.isNpc(player)) { + if (!shouldCancelEvent(player)) { return; } event.setCancelled(true); @@ -365,7 +345,7 @@ public class AuthMePlayerListener implements Listener { @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) public void onPlayerHitPlayerEvent(EntityDamageByEntityEvent event) { - if (ListenerService.shouldCancelEvent(event)) { + if (shouldCancelEvent(event)) { event.setCancelled(true); } } @@ -394,11 +374,12 @@ public class AuthMePlayerListener implements Listener { @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) public void onSignChange(SignChangeEvent event) { Player player = event.getPlayer(); - if (ListenerService.shouldCancelEvent(player)) { + if (shouldCancelEvent(player)) { event.setCancelled(true); } } + // TODO: check this, why do we need to save the quit loc? @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST) public void onPlayerRespawn(PlayerRespawnEvent event) { if (settings.getProperty(RestrictionSettings.NO_TELEPORT)) { From be6ed07802e0b64d3c357c2e4b749b19d7ed9cb5 Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Sun, 29 May 2016 17:36:36 +0200 Subject: [PATCH 117/200] Fix #568 --- .../xephi/authme/listener/AuthMeEntityListener.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/main/java/fr/xephi/authme/listener/AuthMeEntityListener.java b/src/main/java/fr/xephi/authme/listener/AuthMeEntityListener.java index 897607ee..1c6522c9 100644 --- a/src/main/java/fr/xephi/authme/listener/AuthMeEntityListener.java +++ b/src/main/java/fr/xephi/authme/listener/AuthMeEntityListener.java @@ -16,6 +16,9 @@ import org.bukkit.event.entity.FoodLevelChangeEvent; import org.bukkit.event.entity.ProjectileLaunchEvent; import org.bukkit.projectiles.ProjectileSource; +import fr.xephi.authme.ConsoleLogger; + +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import static fr.xephi.authme.listener.ListenerService.shouldCancelEvent; @@ -87,7 +90,7 @@ public class AuthMeEntityListener implements Listener { } } - // TODO #568: Need to check this, player can't throw snowball but the item is taken. + // In old versions of the Bukkit API getShooter() returns a Player Object instead of a ProjectileSource @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) public void onProjectileLaunch(ProjectileLaunchEvent event) { if (event.getEntity() == null) { @@ -103,14 +106,14 @@ public class AuthMeEntityListener implements Listener { } player = (Player) shooter; } else { - // TODO #568 20151220: Invoking getShooter() with null but method isn't static try { if (getShooter == null) { getShooter = Projectile.class.getMethod("getShooter"); } - Object obj = getShooter.invoke(null); + Object obj = getShooter.invoke(projectile); player = (Player) obj; - } catch (Exception ignored) { + } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) { + ConsoleLogger.logException("Error getting shooter", e); } } From 10e5ae08e2345e37b0f4c39e9025002c47ada573 Mon Sep 17 00:00:00 2001 From: DNx5 Date: Sun, 29 May 2016 23:15:00 +0700 Subject: [PATCH 118/200] remove write log for info message. In case we don't have better log writing method yet. --- src/main/java/fr/xephi/authme/ConsoleLogger.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/main/java/fr/xephi/authme/ConsoleLogger.java b/src/main/java/fr/xephi/authme/ConsoleLogger.java index ebec0291..1fab749f 100644 --- a/src/main/java/fr/xephi/authme/ConsoleLogger.java +++ b/src/main/java/fr/xephi/authme/ConsoleLogger.java @@ -49,9 +49,6 @@ public final class ConsoleLogger { */ public static void info(String message) { logger.info(message); - if (useLogging) { - writeLog(message); - } } public static void debug(String message) { From bf91e7754e30d6b577c28c0be9f48e760b9411c5 Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Sun, 29 May 2016 21:43:28 +0200 Subject: [PATCH 119/200] Remove unused imports --- .../fr/xephi/authme/listener/AuthMePlayerListener.java | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java index fa3b5215..a974421a 100644 --- a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java +++ b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java @@ -1,12 +1,8 @@ package fr.xephi.authme.listener; -import com.google.common.io.ByteArrayDataOutput; -import com.google.common.io.ByteStreams; import fr.xephi.authme.AntiBot; -import fr.xephi.authme.AuthMe; import fr.xephi.authme.cache.auth.PlayerAuth; import fr.xephi.authme.datasource.DataSource; -import fr.xephi.authme.hooks.PluginHooks; import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.output.Messages; import fr.xephi.authme.process.Management; @@ -75,11 +71,7 @@ public class AuthMePlayerListener implements Listener { @Inject private SpawnLoader spawnLoader; @Inject - private PluginHooks pluginHooks; - @Inject private OnJoinVerifier onJoinVerifier; - @Inject - private AuthMe plugin; @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) public void onPlayerCommandPreprocess(PlayerCommandPreprocessEvent event) { @@ -379,7 +371,7 @@ public class AuthMePlayerListener implements Listener { } } - // TODO: check this, why do we need to save the quit loc? + // TODO: check this, why do we need to update the quit loc? -sgdc3 @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST) public void onPlayerRespawn(PlayerRespawnEvent event) { if (settings.getProperty(RestrictionSettings.NO_TELEPORT)) { From 30aace06d4a32f717fb2330999af5e4b65c9112a Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Sun, 29 May 2016 21:47:54 +0200 Subject: [PATCH 120/200] Remove unknown config entry --- src/main/resources/config.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 3cb5f23f..abf71daf 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -69,9 +69,6 @@ settings: allowChat: false # Can not authenticated players see the chat log? hideChat: false - # WARNING: use this only if you need it! - # Allow unlogged users to use all the commands if registration is not forced! - allowAllCommandsIfRegistrationIsOptional: false # Commands allowed when a player is not authenticated allowCommands: - /login From 428b27943bb163916afeb6ca7ad855de2bd68507 Mon Sep 17 00:00:00 2001 From: EbonJaguar Date: Sun, 29 May 2016 18:29:19 -0400 Subject: [PATCH 121/200] Add isPermissionsSystem method to PermissionsSystemType enum - fixes #612 --- .../authme/permission/PermissionsManager.java | 22 ++++++++++--------- .../permission/PermissionsSystemType.java | 10 +++++++++ 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/main/java/fr/xephi/authme/permission/PermissionsManager.java b/src/main/java/fr/xephi/authme/permission/PermissionsManager.java index 14234e1f..1a5fd929 100644 --- a/src/main/java/fr/xephi/authme/permission/PermissionsManager.java +++ b/src/main/java/fr/xephi/authme/permission/PermissionsManager.java @@ -224,11 +224,12 @@ public class PermissionsManager implements PermissionsService { String pluginName = plugin.getName(); // Check if any known permissions system is enabling - if (pluginName.equals("PermissionsEx") || pluginName.equals("PermissionsBukkit") || - pluginName.equals("bPermissions") || pluginName.equals("GroupManager") || - pluginName.equals("zPermissions") || pluginName.equals("Vault")) { - ConsoleLogger.info(pluginName + " plugin enabled, dynamically updating permissions hooks!"); - setup(); + for (PermissionsSystemType permissionsSystemType : PermissionsSystemType.values()) { + if (permissionsSystemType.isPermissionSystem(pluginName)) { + ConsoleLogger.info(pluginName + " plugin enabled, dynamically updating permissions hooks!"); + setup(); + break; + } } } @@ -243,11 +244,12 @@ public class PermissionsManager implements PermissionsService { String pluginName = plugin.getName(); // Is the WorldGuard plugin disabled - if (pluginName.equals("PermissionsEx") || pluginName.equals("PermissionsBukkit") || - pluginName.equals("bPermissions") || pluginName.equals("GroupManager") || - pluginName.equals("zPermissions") || pluginName.equals("Vault")) { - ConsoleLogger.info(pluginName + " plugin disabled, updating hooks!"); - setup(); + for (PermissionsSystemType permissionsSystemType : PermissionsSystemType.values()) { + if (permissionsSystemType.isPermissionSystem(pluginName)) { + ConsoleLogger.info(pluginName + " plugin disabled, updating hooks!"); + setup(); + break; + } } } diff --git a/src/main/java/fr/xephi/authme/permission/PermissionsSystemType.java b/src/main/java/fr/xephi/authme/permission/PermissionsSystemType.java index 2167fde6..a65ac50c 100644 --- a/src/main/java/fr/xephi/authme/permission/PermissionsSystemType.java +++ b/src/main/java/fr/xephi/authme/permission/PermissionsSystemType.java @@ -83,4 +83,14 @@ public enum PermissionsSystemType { public String toString() { return getName(); } + + /** + * Check if a given plugin is a permissions system. + * + * @param name The name of the plugin to check. + * @return If the plugin is a valid permissions system. + */ + public boolean isPermissionSystem(String name) { + return name.equals(pluginName); + } } From 9b1ee86b2fb737bfd3c27b327d8c85d26fc27f3f Mon Sep 17 00:00:00 2001 From: ljacqu Date: Mon, 30 May 2016 12:18:55 +0200 Subject: [PATCH 122/200] Trivial code householding - Replace `if (!x) ... else ...` with `if(x) ... else ...` - Avoid throwing RuntimeException; use children --- .../executable/authme/AccountsCommand.java | 30 +++++++++---------- .../authme/ChangePasswordAdminCommand.java | 6 ++-- .../executable/authme/FirstSpawnCommand.java | 6 ++-- .../executable/authme/SpawnCommand.java | 6 ++-- .../AuthMeServiceInitializer.java | 6 ++-- .../process/login/AsynchronousLogin.java | 6 ++-- .../xephi/authme/security/crypts/PHPBB.java | 6 +--- .../authme/settings/domain/EnumProperty.java | 2 +- .../fr/xephi/authme/util/BukkitService.java | 2 +- .../fr/xephi/authme/ReflectionTestUtils.java | 8 ++--- .../command/CommandInitializerTest.java | 30 +++++++++---------- .../authme/command/CommandMapperTest.java | 15 +++++----- .../authme/command/TestCommandsUtil.java | 6 ++-- .../authme/security/PasswordSecurityTest.java | 4 +-- .../crypts/AbstractEncryptionMethodTest.java | 2 +- src/test/java/tools/ToolsRunner.java | 6 ++-- .../tools/messages/VerifyMessagesTask.java | 4 +-- src/test/java/tools/utils/FileUtils.java | 8 ++--- src/test/java/tools/utils/ToolsConstants.java | 6 ++-- 19 files changed, 77 insertions(+), 82 deletions(-) diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/AccountsCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/AccountsCommand.java index 5c3c227e..b2cd359e 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/AccountsCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/AccountsCommand.java @@ -29,7 +29,21 @@ public class AccountsCommand implements ExecutableCommand { final String playerName = arguments.isEmpty() ? sender.getName() : arguments.get(0); // Assumption: a player name cannot contain '.' - if (!playerName.contains(".")) { + if (playerName.contains(".")) { + bukkitService.runTaskAsynchronously(new Runnable() { + @Override + public void run() { + List accountList = dataSource.getAllAuthsByIp(playerName); + if (accountList.isEmpty()) { + sender.sendMessage("[AuthMe] This IP does not exist in the database."); + } else if (accountList.size() == 1) { + sender.sendMessage("[AuthMe] " + playerName + " is a single account player"); + } else { + outputAccountsList(sender, playerName, accountList); + } + } + }); + } else { bukkitService.runTaskAsynchronously(new Runnable() { @Override public void run() { @@ -49,20 +63,6 @@ public class AccountsCommand implements ExecutableCommand { } } }); - } else { - bukkitService.runTaskAsynchronously(new Runnable() { - @Override - public void run() { - List accountList = dataSource.getAllAuthsByIp(playerName); - if (accountList.isEmpty()) { - sender.sendMessage("[AuthMe] This IP does not exist in the database."); - } else if (accountList.size() == 1) { - sender.sendMessage("[AuthMe] " + playerName + " is a single account player"); - } else { - outputAccountsList(sender, playerName, accountList); - } - } - }); } } 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 2e353f15..d2242347 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 @@ -66,11 +66,11 @@ public class ChangePasswordAdminCommand implements ExecutableCommand { HashedPassword hashedPassword = passwordSecurity.computeHash(playerPass, playerNameLowerCase); auth.setPassword(hashedPassword); - if (!dataSource.updatePassword(auth)) { - commandService.send(sender, MessageKey.ERROR); - } else { + if (dataSource.updatePassword(auth)) { commandService.send(sender, MessageKey.PASSWORD_CHANGED_SUCCESS); ConsoleLogger.info(playerNameLowerCase + "'s password changed"); + } else { + commandService.send(sender, MessageKey.ERROR); } } diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/FirstSpawnCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/FirstSpawnCommand.java index 9cb3ca9b..52793e10 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/FirstSpawnCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/FirstSpawnCommand.java @@ -18,10 +18,10 @@ public class FirstSpawnCommand extends PlayerCommand { @Override public void runCommand(Player player, List arguments, CommandService commandService) { - if (spawnLoader.getFirstSpawn() != null) { - player.teleport(spawnLoader.getFirstSpawn()); - } else { + if (spawnLoader.getFirstSpawn() == null) { player.sendMessage("[AuthMe] First spawn has failed, please try to define the first spawn"); + } else { + player.teleport(spawnLoader.getFirstSpawn()); } } } diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/SpawnCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/SpawnCommand.java index 8c142d25..c7f584f7 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/SpawnCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/SpawnCommand.java @@ -15,10 +15,10 @@ public class SpawnCommand extends PlayerCommand { @Override public void runCommand(Player player, List arguments, CommandService commandService) { - if (spawnLoader.getSpawn() != null) { - player.teleport(spawnLoader.getSpawn()); - } else { + if (spawnLoader.getSpawn() == null) { player.sendMessage("[AuthMe] Spawn has failed, please try to define the spawn"); + } else { + player.teleport(spawnLoader.getSpawn()); } } } diff --git a/src/main/java/fr/xephi/authme/initialization/AuthMeServiceInitializer.java b/src/main/java/fr/xephi/authme/initialization/AuthMeServiceInitializer.java index 9bc8525c..9213772f 100644 --- a/src/main/java/fr/xephi/authme/initialization/AuthMeServiceInitializer.java +++ b/src/main/java/fr/xephi/authme/initialization/AuthMeServiceInitializer.java @@ -181,15 +181,15 @@ public class AuthMeServiceInitializer { Class[] annotations = injection.getDependencyAnnotations(); Object[] values = new Object[dependencies.length]; for (int i = 0; i < dependencies.length; ++i) { - if (annotations[i] != null) { + if (annotations[i] == null) { + values[i] = get(dependencies[i], traversedClasses); + } else { Object value = objects.get(annotations[i]); if (value == null) { throw new IllegalStateException("Value for field with @" + annotations[i].getSimpleName() + " must be registered beforehand"); } values[i] = value; - } else { - values[i] = get(dependencies[i], traversedClasses); } } return values; diff --git a/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java b/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java index d829a6ce..a86ac93e 100644 --- a/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java +++ b/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java @@ -68,12 +68,12 @@ public class AsynchronousLogin implements AsynchronousProcess { private boolean needsCaptcha(Player player) { final String name = player.getName().toLowerCase(); if (service.getProperty(SecuritySettings.USE_CAPTCHA)) { - if (!plugin.captcha.containsKey(name)) { - plugin.captcha.putIfAbsent(name, 1); - } else { + if (plugin.captcha.containsKey(name)) { int i = plugin.captcha.get(name) + 1; plugin.captcha.remove(name); plugin.captcha.putIfAbsent(name, i); + } else { + plugin.captcha.putIfAbsent(name, 1); } if (plugin.captcha.containsKey(name) && plugin.captcha.get(name) > Settings.maxLoginTry) { plugin.cap.putIfAbsent(name, RandomString.generate(Settings.captchaLength)); diff --git a/src/main/java/fr/xephi/authme/security/crypts/PHPBB.java b/src/main/java/fr/xephi/authme/security/crypts/PHPBB.java index c07db986..074143fd 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/PHPBB.java +++ b/src/main/java/fr/xephi/authme/security/crypts/PHPBB.java @@ -1,7 +1,3 @@ -/* - * To change this template, choose Tools | Templates and open the template in - * the editor. - */ package fr.xephi.authme.security.crypts; import fr.xephi.authme.security.HashUtils; @@ -24,7 +20,7 @@ public class PHPBB extends HexSaltedMethod { byte[] hash = md5er.digest(bytes); return bytes2hex(hash); } catch (UnsupportedEncodingException e) { - throw new RuntimeException(e); + throw new UnsupportedOperationException(e); } } diff --git a/src/main/java/fr/xephi/authme/settings/domain/EnumProperty.java b/src/main/java/fr/xephi/authme/settings/domain/EnumProperty.java index 14d09329..3e3d3e3b 100644 --- a/src/main/java/fr/xephi/authme/settings/domain/EnumProperty.java +++ b/src/main/java/fr/xephi/authme/settings/domain/EnumProperty.java @@ -24,7 +24,7 @@ class EnumProperty> extends Property { return getDefaultValue(); } E mappedValue = mapToEnum(textValue); - return mappedValue != null ? mappedValue : getDefaultValue(); + return mappedValue == null ? getDefaultValue() : mappedValue; } @Override diff --git a/src/main/java/fr/xephi/authme/util/BukkitService.java b/src/main/java/fr/xephi/authme/util/BukkitService.java index 592caa22..5837b942 100644 --- a/src/main/java/fr/xephi/authme/util/BukkitService.java +++ b/src/main/java/fr/xephi/authme/util/BukkitService.java @@ -158,7 +158,7 @@ public class BukkitService { } else if (obj instanceof Player[]) { return Arrays.asList((Player[]) obj); } else { - String type = (obj != null) ? obj.getClass().getName() : "null"; + String type = (obj == null) ? "null" : obj.getClass().getName(); ConsoleLogger.showError("Unknown list of online players of type " + type); } } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { diff --git a/src/test/java/fr/xephi/authme/ReflectionTestUtils.java b/src/test/java/fr/xephi/authme/ReflectionTestUtils.java index 33579e80..397181bb 100644 --- a/src/test/java/fr/xephi/authme/ReflectionTestUtils.java +++ b/src/test/java/fr/xephi/authme/ReflectionTestUtils.java @@ -27,7 +27,7 @@ public final class ReflectionTestUtils { Field field = getField(clazz, instance, fieldName); field.set(instance, value); } catch (IllegalAccessException e) { - throw new RuntimeException( + throw new UnsupportedOperationException( format("Could not set value to field '%s' for instance '%s' of class '%s'", fieldName, instance, clazz.getName()), e); } @@ -39,7 +39,7 @@ public final class ReflectionTestUtils { field.setAccessible(true); return field; } catch (NoSuchFieldException e) { - throw new RuntimeException(format("Could not get field '%s' for instance '%s' of class '%s'", + throw new UnsupportedOperationException(format("Could not get field '%s' for instance '%s' of class '%s'", fieldName, instance, clazz.getName()), e); } } @@ -50,7 +50,7 @@ public final class ReflectionTestUtils { try { return field.get(instance); } catch (IllegalAccessException e) { - throw new RuntimeException("Could not get value of field '" + fieldName + "'"); + throw new UnsupportedOperationException("Could not get value of field '" + fieldName + "'"); } } @@ -69,7 +69,7 @@ public final class ReflectionTestUtils { method.setAccessible(true); return method; } catch (NoSuchMethodException e) { - throw new RuntimeException("Could not retrieve method '" + methodName + "' from class '" + throw new UnsupportedOperationException("Could not retrieve method '" + methodName + "' from class '" + clazz.getName() + "'"); } } diff --git a/src/test/java/fr/xephi/authme/command/CommandInitializerTest.java b/src/test/java/fr/xephi/authme/command/CommandInitializerTest.java index 786ea477..1af9687b 100644 --- a/src/test/java/fr/xephi/authme/command/CommandInitializerTest.java +++ b/src/test/java/fr/xephi/authme/command/CommandInitializerTest.java @@ -22,6 +22,7 @@ import static fr.xephi.authme.permission.DefaultPermission.OP_ONLY; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertSame; import static org.junit.Assert.assertThat; import static org.junit.Assert.fail; import static org.mockito.Matchers.any; @@ -187,12 +188,12 @@ public class CommandInitializerTest { assertThat(command.getExecutableCommand(), not(nullValue())); ExecutableCommand commandExec = command.getExecutableCommand(); ExecutableCommand storedExec = implementations.get(command.getExecutableCommand().getClass()); - if (storedExec != null) { - assertThat("has same implementation of '" + storedExec.getClass().getName() + "' for command with " - + "parent " + (command.getParent() == null ? "null" : command.getParent().getLabels()), - storedExec == commandExec, equalTo(true)); - } else { + if (storedExec == null) { implementations.put(commandExec.getClass(), commandExec); + } else { + assertSame("has same implementation of '" + storedExec.getClass().getName() + "' for command with " + + "parent " + (command.getParent() == null ? "null" : command.getParent().getLabels()), + storedExec, commandExec); } } }; @@ -211,7 +212,7 @@ public class CommandInitializerTest { for (CommandArgumentDescription argument : command.getArguments()) { if (argument.isOptional()) { encounteredOptionalArg = true; - } else if (!argument.isOptional() && encounteredOptionalArg) { + } else if (encounteredOptionalArg) { fail("Mandatory arguments should come before optional ones for command with labels '" + command.getLabels() + "'"); } @@ -256,11 +257,10 @@ public class CommandInitializerTest { @Override public void accept(CommandDescription command, int depth) { CommandPermissions permissions = command.getCommandPermissions(); - if (permissions != null && OP_ONLY.equals(permissions.getDefaultPermission())) { - if (!hasAdminNode(permissions)) { - fail("The command with labels " + command.getLabels() + " has OP_ONLY default " - + "permission but no permission node on admin level"); - } + if (permissions != null && OP_ONLY.equals(permissions.getDefaultPermission()) + && !hasAdminNode(permissions)) { + fail("The command with labels " + command.getLabels() + " has OP_ONLY default " + + "permission but no permission node on admin level"); } } @@ -298,13 +298,13 @@ public class CommandInitializerTest { Map, Integer> collection) { final Class clazz = command.getExecutableCommand().getClass(); Integer existingCount = collection.get(clazz); - if (existingCount != null) { + if (existingCount == null) { + collection.put(clazz, argCount); + } else { String commandDescription = "Command with label '" + command.getLabels().get(0) + "' and parent '" - + (command.getParent() != null ? command.getLabels().get(0) : "null") + "' "; + + (command.getParent() == null ? "null" : command.getLabels().get(0)) + "' "; assertThat(commandDescription + "should point to " + clazz + " with arguments consistent to others", argCount, equalTo(existingCount)); - } else { - collection.put(clazz, argCount); } } }; diff --git a/src/test/java/fr/xephi/authme/command/CommandMapperTest.java b/src/test/java/fr/xephi/authme/command/CommandMapperTest.java index ebcd0b64..b5789bbb 100644 --- a/src/test/java/fr/xephi/authme/command/CommandMapperTest.java +++ b/src/test/java/fr/xephi/authme/command/CommandMapperTest.java @@ -6,7 +6,6 @@ import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; -import java.util.Arrays; import java.util.List; import java.util.Set; @@ -52,7 +51,7 @@ public class CommandMapperTest { @Test public void shouldMapPartsToLoginChildCommand() { // given - List parts = Arrays.asList("authme", "login", "test1"); + List parts = asList("authme", "login", "test1"); CommandSender sender = mock(CommandSender.class); given(permissionsManager.hasPermission(eq(sender), any(CommandDescription.class))).willReturn(true); @@ -71,7 +70,7 @@ public class CommandMapperTest { @Test public void shouldMapPartsToCommandWithNoCaseSensitivity() { // given - List parts = Arrays.asList("Authme", "REG", "arg1", "arg2"); + List parts = asList("Authme", "REG", "arg1", "arg2"); CommandSender sender = mock(CommandSender.class); given(permissionsManager.hasPermission(eq(sender), any(CommandDescription.class))).willReturn(true); @@ -89,7 +88,7 @@ public class CommandMapperTest { @Test public void shouldRejectCommandWithTooManyArguments() { // given - List parts = Arrays.asList("authme", "register", "pass123", "pass123", "pass123"); + List parts = asList("authme", "register", "pass123", "pass123", "pass123"); CommandSender sender = mock(CommandSender.class); given(permissionsManager.hasPermission(eq(sender), any(CommandDescription.class))).willReturn(true); @@ -107,7 +106,7 @@ public class CommandMapperTest { @Test public void shouldRejectCommandWithTooFewArguments() { // given - List parts = Arrays.asList("authme", "Reg"); + List parts = asList("authme", "Reg"); CommandSender sender = mock(CommandSender.class); given(permissionsManager.hasPermission(eq(sender), any(CommandDescription.class))).willReturn(true); @@ -125,7 +124,7 @@ public class CommandMapperTest { @Test public void shouldSuggestCommandWithSimilarLabel() { // given - List parts = Arrays.asList("authme", "reh", "pass123", "pass123"); + List parts = asList("authme", "reh", "pass123", "pass123"); CommandSender sender = mock(CommandSender.class); given(permissionsManager.hasPermission(eq(sender), any(CommandDescription.class))).willReturn(true); @@ -144,7 +143,7 @@ public class CommandMapperTest { @Test public void shouldSuggestMostSimilarCommand() { // given - List parts = Arrays.asList("authme", "asdfawetawty4asdca"); + List parts = asList("authme", "asdfawetawty4asdca"); CommandSender sender = mock(CommandSender.class); given(permissionsManager.hasPermission(eq(sender), any(CommandDescription.class))).willReturn(true); @@ -259,7 +258,7 @@ public class CommandMapperTest { @Test public void shouldRecognizeMissingPermissionForCommand() { // given - List parts = Arrays.asList("authme", "login", "test1"); + List parts = asList("authme", "login", "test1"); CommandSender sender = mock(CommandSender.class); given(permissionsManager.hasPermission(eq(sender), any(CommandDescription.class))).willReturn(false); diff --git a/src/test/java/fr/xephi/authme/command/TestCommandsUtil.java b/src/test/java/fr/xephi/authme/command/TestCommandsUtil.java index 3beb7693..6f7857b0 100644 --- a/src/test/java/fr/xephi/authme/command/TestCommandsUtil.java +++ b/src/test/java/fr/xephi/authme/command/TestCommandsUtil.java @@ -85,11 +85,11 @@ public final class TestCommandsUtil { private static CommandDescription createCommand(PermissionNode permission, CommandDescription parent, List labels, CommandArgumentDescription... arguments) { PermissionNode[] notNullPermission; - if (permission != null) { + if (permission == null) { + notNullPermission = new PermissionNode[0]; + } else { notNullPermission = new PermissionNode[1]; notNullPermission[0] = permission; - } else { - notNullPermission = new PermissionNode[0]; } CommandDescription.CommandBuilder command = CommandDescription.builder() diff --git a/src/test/java/fr/xephi/authme/security/PasswordSecurityTest.java b/src/test/java/fr/xephi/authme/security/PasswordSecurityTest.java index f3d577dc..e98f2377 100644 --- a/src/test/java/fr/xephi/authme/security/PasswordSecurityTest.java +++ b/src/test/java/fr/xephi/authme/security/PasswordSecurityTest.java @@ -73,13 +73,13 @@ public class PasswordSecurityTest { Object[] arguments = invocation.getArguments(); if (arguments[0] instanceof PasswordEncryptionEvent) { PasswordEncryptionEvent event = (PasswordEncryptionEvent) arguments[0]; - caughtClassInEvent = event.getMethod() != null ? event.getMethod().getClass() : null; + caughtClassInEvent = event.getMethod() == null ? null : event.getMethod().getClass(); event.setMethod(method); } return null; } }).when(pluginManager).callEvent(any(Event.class)); - initializer = new AuthMeServiceInitializer(new String[]{}); + initializer = new AuthMeServiceInitializer(); initializer.register(NewSetting.class, settings); initializer.register(DataSource.class, dataSource); initializer.register(PluginManager.class, pluginManager); diff --git a/src/test/java/fr/xephi/authme/security/crypts/AbstractEncryptionMethodTest.java b/src/test/java/fr/xephi/authme/security/crypts/AbstractEncryptionMethodTest.java index 73e52fa3..45489710 100644 --- a/src/test/java/fr/xephi/authme/security/crypts/AbstractEncryptionMethodTest.java +++ b/src/test/java/fr/xephi/authme/security/crypts/AbstractEncryptionMethodTest.java @@ -152,7 +152,7 @@ public abstract class AbstractEncryptionMethodTest { * * @param method The method to create a test class for */ - static void generateTest(EncryptionMethod method) { + protected static void generateTest(EncryptionMethod method) { String className = method.getClass().getSimpleName(); // Create javadoc and "public class extends" and the constructor call "super(new Class()," System.out.println("/**\n * Test for {@link " + className + "}.\n */"); diff --git a/src/test/java/tools/ToolsRunner.java b/src/test/java/tools/ToolsRunner.java index f8e74aea..b387f32f 100644 --- a/src/test/java/tools/ToolsRunner.java +++ b/src/test/java/tools/ToolsRunner.java @@ -62,10 +62,10 @@ public final class ToolsRunner { ToolTask task = tasks.get(taskName); if (task == null) { System.out.format("Unknown task '%s'%n", taskName); - } else if (!(task instanceof AutoToolTask)) { - System.out.format("Task '%s' cannot be run on command line%n", taskName); - } else { + } else if (task instanceof AutoToolTask) { ((AutoToolTask) task).executeDefault(); + } else { + System.out.format("Task '%s' cannot be run on command line%n", taskName); } } } diff --git a/src/test/java/tools/messages/VerifyMessagesTask.java b/src/test/java/tools/messages/VerifyMessagesTask.java index c4415e03..de888be7 100644 --- a/src/test/java/tools/messages/VerifyMessagesTask.java +++ b/src/test/java/tools/messages/VerifyMessagesTask.java @@ -133,7 +133,7 @@ public final class VerifyMessagesTask implements ToolTask { File folder = new File(MESSAGES_FOLDER); File[] files = folder.listFiles(); if (files == null) { - throw new RuntimeException("Could not read files from folder '" + folder.getName() + "'"); + throw new IllegalStateException("Could not read files from folder '" + folder.getName() + "'"); } List messageFiles = new ArrayList<>(); @@ -143,7 +143,7 @@ public final class VerifyMessagesTask implements ToolTask { } } if (messageFiles.isEmpty()) { - throw new RuntimeException("Error getting message files: list of files is empty"); + throw new IllegalStateException("Error getting message files: list of files is empty"); } return messageFiles; } diff --git a/src/test/java/tools/utils/FileUtils.java b/src/test/java/tools/utils/FileUtils.java index dbf40037..acc260de 100644 --- a/src/test/java/tools/utils/FileUtils.java +++ b/src/test/java/tools/utils/FileUtils.java @@ -27,7 +27,7 @@ public final class FileUtils { try { Files.write(Paths.get(outputFile), contents.getBytes()); } catch (IOException e) { - throw new RuntimeException("Failed to write to file '" + outputFile + "'", e); + throw new UnsupportedOperationException("Failed to write to file '" + outputFile + "'", e); } } @@ -35,7 +35,7 @@ public final class FileUtils { try { Files.write(Paths.get(outputFile), contents.getBytes(), StandardOpenOption.APPEND); } catch (IOException e) { - throw new RuntimeException("Failed to append to file '" + outputFile + "'", e); + throw new UnsupportedOperationException("Failed to append to file '" + outputFile + "'", e); } } @@ -43,7 +43,7 @@ public final class FileUtils { try { return new String(Files.readAllBytes(Paths.get(file)), CHARSET); } catch (IOException e) { - throw new RuntimeException("Could not read from file '" + file + "'", e); + throw new UnsupportedOperationException("Could not read from file '" + file + "'", e); } } @@ -51,7 +51,7 @@ public final class FileUtils { try { return Files.readAllLines(Paths.get(file), CHARSET); } catch (IOException e) { - throw new RuntimeException("Could not read from file '" + file + "'", e); + throw new UnsupportedOperationException("Could not read from file '" + file + "'", e); } } diff --git a/src/test/java/tools/utils/ToolsConstants.java b/src/test/java/tools/utils/ToolsConstants.java index c94a22b1..6708fd77 100644 --- a/src/test/java/tools/utils/ToolsConstants.java +++ b/src/test/java/tools/utils/ToolsConstants.java @@ -5,9 +5,6 @@ package tools.utils; */ public final class ToolsConstants { - private ToolsConstants() { - } - public static final String MAIN_SOURCE_ROOT = "src/main/java/"; public static final String MAIN_RESOURCES_ROOT = "src/main/resources/"; @@ -21,4 +18,7 @@ public final class ToolsConstants { public static final String DOCS_FOLDER_URL = "https://github.com/AuthMe-Team/AuthMeReloaded/tree/master/docs/"; + private ToolsConstants() { + } + } From f94f4643cf3cd2e27189e985660798fbe40d3d75 Mon Sep 17 00:00:00 2001 From: EbonJaguar Date: Mon, 30 May 2016 11:08:01 -0400 Subject: [PATCH 123/200] Add a lowercase String list property - fixes #602 --- .../authme/settings/domain/Property.java | 38 ++++++++++++++++++- .../properties/RestrictionSettings.java | 7 ++-- .../settings/properties/SecuritySettings.java | 3 +- .../authme/settings/domain/PropertyTest.java | 28 ++++++++++++++ 4 files changed, 71 insertions(+), 5 deletions(-) diff --git a/src/main/java/fr/xephi/authme/settings/domain/Property.java b/src/main/java/fr/xephi/authme/settings/domain/Property.java index 1d5100af..a92d3222 100644 --- a/src/main/java/fr/xephi/authme/settings/domain/Property.java +++ b/src/main/java/fr/xephi/authme/settings/domain/Property.java @@ -3,6 +3,7 @@ package fr.xephi.authme.settings.domain; import org.bukkit.configuration.file.FileConfiguration; import org.yaml.snakeyaml.Yaml; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Objects; @@ -33,6 +34,17 @@ public abstract class Property { return new StringListProperty(path, defaultValues); } + /** + * Create a new String list property where all values are lowercase. + * + * @param path The property's path + * @param defaultValues The items in the default list + * @return The created list property + */ + public static Property> newLowercaseListProperty(String path, String... defaultValues) { + return new LowercaseStringListProperty(path, defaultValues); + } + /** * Create a new enum property. * @@ -165,7 +177,7 @@ public abstract class Property { /** * String list property. */ - private static final class StringListProperty extends Property> { + private static class StringListProperty extends Property> { public StringListProperty(String path, String[] defaultValues) { super(path, Arrays.asList(defaultValues)); @@ -196,4 +208,28 @@ public abstract class Property { } } + /** + * Lowercase String list property. + */ + private static final class LowercaseStringListProperty extends StringListProperty { + + public LowercaseStringListProperty(String path, String[] defaultValues) { + super(path, defaultValues); + } + + @Override + public List getFromFile(FileConfiguration configuration) { + if (!configuration.isList(getPath())) { + return getDefaultValue(); + } + + // make sure all elements are lowercase + List lowercaseList = new ArrayList<>(); + for (String element : configuration.getStringList(getPath())) { + lowercaseList.add(element.toLowerCase()); + } + + return lowercaseList; + } + } } diff --git a/src/main/java/fr/xephi/authme/settings/properties/RestrictionSettings.java b/src/main/java/fr/xephi/authme/settings/properties/RestrictionSettings.java index 96c790e2..7a9fd7cd 100644 --- a/src/main/java/fr/xephi/authme/settings/properties/RestrictionSettings.java +++ b/src/main/java/fr/xephi/authme/settings/properties/RestrictionSettings.java @@ -7,6 +7,7 @@ import fr.xephi.authme.settings.domain.SettingsClass; import java.util.List; import static fr.xephi.authme.settings.domain.Property.newListProperty; +import static fr.xephi.authme.settings.domain.Property.newLowercaseListProperty; import static fr.xephi.authme.settings.domain.Property.newProperty; public class RestrictionSettings implements SettingsClass { @@ -30,7 +31,7 @@ public class RestrictionSettings implements SettingsClass { @Comment("Allowed commands for unauthenticated players") public static final Property> ALLOW_COMMANDS = - newListProperty("settings.restrictions.allowCommands", + newLowercaseListProperty("settings.restrictions.allowCommands", "/login", "/register", "/l", "/reg", "/email", "/captcha"); @Comment({ @@ -87,7 +88,7 @@ public class RestrictionSettings implements SettingsClass { " AllowedRestrictedUser:", " - playername;127.0.0.1"}) public static final Property> ALLOWED_RESTRICTED_USERS = - newListProperty("settings.restrictions.AllowedRestrictedUser"); + newLowercaseListProperty("settings.restrictions.AllowedRestrictedUser"); @Comment("Should unregistered players be kicked immediately?") public static final Property KICK_NON_REGISTERED = @@ -194,7 +195,7 @@ public class RestrictionSettings implements SettingsClass { "It is case-sensitive!" }) public static final Property> UNRESTRICTED_NAMES = - newListProperty("settings.unrestrictions.UnrestrictedName"); + newLowercaseListProperty("settings.unrestrictions.UnrestrictedName"); private RestrictionSettings() { diff --git a/src/main/java/fr/xephi/authme/settings/properties/SecuritySettings.java b/src/main/java/fr/xephi/authme/settings/properties/SecuritySettings.java index 9a52ca82..f865793b 100644 --- a/src/main/java/fr/xephi/authme/settings/properties/SecuritySettings.java +++ b/src/main/java/fr/xephi/authme/settings/properties/SecuritySettings.java @@ -8,6 +8,7 @@ import fr.xephi.authme.settings.domain.SettingsClass; import java.util.List; import static fr.xephi.authme.settings.domain.Property.newListProperty; +import static fr.xephi.authme.settings.domain.Property.newLowercaseListProperty; import static fr.xephi.authme.settings.domain.Property.newProperty; public class SecuritySettings implements SettingsClass { @@ -98,7 +99,7 @@ public class SecuritySettings implements SettingsClass { "- '123456'", "- 'password'"}) public static final Property> UNSAFE_PASSWORDS = - newListProperty("settings.security.unsafePasswords", "123456", "password", "qwerty", "12345", "54321"); + newLowercaseListProperty("settings.security.unsafePasswords", "123456", "password", "qwerty", "12345", "54321"); private SecuritySettings() { } diff --git a/src/test/java/fr/xephi/authme/settings/domain/PropertyTest.java b/src/test/java/fr/xephi/authme/settings/domain/PropertyTest.java index ff84c393..3ea8ff0e 100644 --- a/src/test/java/fr/xephi/authme/settings/domain/PropertyTest.java +++ b/src/test/java/fr/xephi/authme/settings/domain/PropertyTest.java @@ -38,6 +38,9 @@ public class PropertyTest { 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); + when(configuration.isList("lowercaselist.path.test")).thenReturn(true); + when(configuration.getStringList("lowercaselist.path.test")).thenReturn(Arrays.asList("test1", "Test2", "3rd test")); + when(configuration.isList("lowercaselist.path.wrong")).thenReturn(false); } /* Boolean */ @@ -141,4 +144,29 @@ public class PropertyTest { assertThat(result, contains("default", "list", "elements")); } + /* Lowercase String list */ + @Test + public void shouldGetLowercaseStringListValue() { + // given + Property> property = Property.newLowercaseListProperty("lowercaselist.path.test", "1", "b"); + + // when + List result = property.getFromFile(configuration); + + // then + assertThat(result, contains("test1", "test2", "3rd test")); + } + + @Test + public void shouldGetLowercaseStringListDefault() { + // given + Property> property = + Property.newLowercaseListProperty("lowercaselist.path.wrong", "default", "list", "elements"); + + // when + List result = property.getFromFile(configuration); + + // then + assertThat(result, contains("default", "list", "elements")); + } } From cc67624a468c442bb5ec59bac9e2ce72709cdf15 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Mon, 30 May 2016 17:09:10 +0200 Subject: [PATCH 124/200] Throwing snowball still possible when unlogged --- .../authme/listener/AuthMeEntityListener.java | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/main/java/fr/xephi/authme/listener/AuthMeEntityListener.java b/src/main/java/fr/xephi/authme/listener/AuthMeEntityListener.java index 1c6522c9..f2686f6a 100644 --- a/src/main/java/fr/xephi/authme/listener/AuthMeEntityListener.java +++ b/src/main/java/fr/xephi/authme/listener/AuthMeEntityListener.java @@ -25,14 +25,15 @@ import static fr.xephi.authme.listener.ListenerService.shouldCancelEvent; public class AuthMeEntityListener implements Listener { - private static Method getShooter; - private static boolean shooterIsProjectileSource; + private Method getShooter; + private boolean shooterIsProjectileSource; public AuthMeEntityListener() { try { - Method m = Projectile.class.getDeclaredMethod("getShooter"); - shooterIsProjectileSource = m.getReturnType() != LivingEntity.class; - } catch (Exception ignored) { + getShooter = Projectile.class.getDeclaredMethod("getShooter"); + shooterIsProjectileSource = getShooter.getReturnType() != LivingEntity.class; + } catch (NoSuchMethodException | SecurityException e) { + ConsoleLogger.logException("Cannot load getShooter() method on Projectile class", e); } } @@ -90,7 +91,7 @@ public class AuthMeEntityListener implements Listener { } } - // In old versions of the Bukkit API getShooter() returns a Player Object instead of a ProjectileSource + // TODO #733: Player can't throw snowball but the item is taken. @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) public void onProjectileLaunch(ProjectileLaunchEvent event) { if (event.getEntity() == null) { @@ -99,6 +100,7 @@ public class AuthMeEntityListener implements Listener { Player player = null; Projectile projectile = event.getEntity(); + // In old versions of the Bukkit API getShooter() returns a Player object instead of a ProjectileSource if (shooterIsProjectileSource) { ProjectileSource shooter = projectile.getShooter(); if (shooter == null || !(shooter instanceof Player)) { From 18ff5d53316a0a2053641a63d9494d8554efc51d Mon Sep 17 00:00:00 2001 From: ljacqu Date: Mon, 30 May 2016 20:02:07 +0200 Subject: [PATCH 125/200] Remove todo comment associated to #602 --- src/main/java/fr/xephi/authme/util/ValidationService.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/fr/xephi/authme/util/ValidationService.java b/src/main/java/fr/xephi/authme/util/ValidationService.java index 999e2d1c..40f8fd1c 100644 --- a/src/main/java/fr/xephi/authme/util/ValidationService.java +++ b/src/main/java/fr/xephi/authme/util/ValidationService.java @@ -49,8 +49,6 @@ public class ValidationService { || password.length() > settings.getProperty(SecuritySettings.MAX_PASSWORD_LENGTH)) { return MessageKey.INVALID_PASSWORD_LENGTH; } else if (settings.getProperty(SecuritySettings.UNSAFE_PASSWORDS).contains(passLow)) { - // TODO #602 20160312: The UNSAFE_PASSWORDS should be all lowercase - // -> introduce a lowercase String list property type return MessageKey.PASSWORD_UNSAFE_ERROR; } return null; From 3ad00a45f9a13251dd3a1b70e18f87fc041aca34 Mon Sep 17 00:00:00 2001 From: EbonJaguar Date: Mon, 30 May 2016 16:47:48 -0400 Subject: [PATCH 126/200] Move default permissions out of Commands and into PermissionNode - fixes #606 --- .../authme/command/CommandDescription.java | 33 ++++++----- .../authme/command/CommandInitializer.java | 56 +++++++++---------- .../authme/command/CommandPermissions.java | 52 ----------------- .../authme/command/help/HelpProvider.java | 19 +++---- .../authme/permission/AdminPermission.java | 52 ++++++++++------- .../authme/permission/PermissionNode.java | 6 ++ .../authme/permission/PermissionsManager.java | 7 +-- .../authme/permission/PlayerPermission.java | 35 ++++++++---- .../permission/PlayerStatePermission.java | 21 +++++-- .../command/CommandInitializerTest.java | 15 ++--- .../authme/command/TestCommandsUtil.java | 10 +--- .../authme/command/help/HelpProviderTest.java | 7 ++- .../tools/commands/CommandPageCreater.java | 14 ++--- 13 files changed, 150 insertions(+), 177 deletions(-) delete mode 100644 src/main/java/fr/xephi/authme/command/CommandPermissions.java diff --git a/src/main/java/fr/xephi/authme/command/CommandDescription.java b/src/main/java/fr/xephi/authme/command/CommandDescription.java index 22a3f05c..595f513d 100644 --- a/src/main/java/fr/xephi/authme/command/CommandDescription.java +++ b/src/main/java/fr/xephi/authme/command/CommandDescription.java @@ -52,9 +52,9 @@ public class CommandDescription { */ private List arguments; /** - * Command permissions required to execute this command. + * Permission node required to execute this command. */ - private CommandPermissions permissions; + private PermissionNode permission; /** * Private constructor. Use {@link CommandDescription#builder()} to create instances of this class. @@ -74,7 +74,7 @@ public class CommandDescription { * @param executableCommand The executable command, or null. * @param parent Parent command. * @param arguments Command arguments. - * @param permissions The permissions required to execute this command. + * @param permission The permission node required to execute this command. * * @return The created instance * @see CommandDescription#builder() @@ -82,7 +82,7 @@ public class CommandDescription { private static CommandDescription createInstance(List labels, String description, String detailedDescription, ExecutableCommand executableCommand, CommandDescription parent, List arguments, - CommandPermissions permissions) { + PermissionNode permission) { CommandDescription instance = new CommandDescription(); instance.labels = labels; instance.description = description; @@ -90,7 +90,7 @@ public class CommandDescription { instance.executableCommand = executableCommand; instance.parent = parent; instance.arguments = arguments; - instance.permissions = permissions; + instance.permission = permission; if (parent != null) { parent.addChild(instance); @@ -196,12 +196,12 @@ public class CommandDescription { } /** - * Return the permissions required to execute the command. + * Return the permission node required to execute the command. * - * @return The command permissions, or null if none are required to execute the command. + * @return The permission node, or null if none are required to execute the command. */ - public CommandPermissions getCommandPermissions() { - return permissions; + public PermissionNode getPermission() { + return permission; } /** @@ -223,7 +223,7 @@ public class CommandDescription { private ExecutableCommand executableCommand; private CommandDescription parent; private List arguments = new ArrayList<>(); - private CommandPermissions permissions; + private PermissionNode permission; /** * Build a CommandDescription from the builder or throw an exception if a mandatory @@ -239,7 +239,7 @@ public class CommandDescription { // parents and permissions may be null; arguments may be empty return createInstance(labels, description, detailedDescription, executableCommand, - parent, arguments, permissions); + parent, arguments, permission); } public CommandBuilder labels(List labels) { @@ -286,9 +286,14 @@ public class CommandDescription { return this; } - public CommandBuilder permissions(DefaultPermission defaultPermission, - PermissionNode... permissionNodes) { - this.permissions = new CommandPermissions(asList(permissionNodes), defaultPermission); + /** + * Add a permission node that the a user must have to execute the command. + * + * @param permission The PermissionNode to add + * @return The builder + */ + public CommandBuilder permission(PermissionNode permission) { + this.permission = permission; return this; } } diff --git a/src/main/java/fr/xephi/authme/command/CommandInitializer.java b/src/main/java/fr/xephi/authme/command/CommandInitializer.java index ca4cec3b..162d0289 100644 --- a/src/main/java/fr/xephi/authme/command/CommandInitializer.java +++ b/src/main/java/fr/xephi/authme/command/CommandInitializer.java @@ -82,7 +82,7 @@ public class CommandInitializer { .detailedDescription("Register the specified player with the specified password.") .withArgument("player", "Player name", false) .withArgument("password", "Password", false) - .permissions(OP_ONLY, AdminPermission.REGISTER) + .permission(AdminPermission.REGISTER) .executableCommand(initializer.newInstance(RegisterAdminCommand.class)) .build(); @@ -93,7 +93,7 @@ public class CommandInitializer { .description("Unregister a player") .detailedDescription("Unregister the specified player.") .withArgument("player", "Player name", false) - .permissions(OP_ONLY, AdminPermission.UNREGISTER) + .permission(AdminPermission.UNREGISTER) .executableCommand(initializer.newInstance(UnregisterAdminCommand.class)) .build(); @@ -104,7 +104,7 @@ public class CommandInitializer { .description("Enforce login player") .detailedDescription("Enforce the specified player to login.") .withArgument("player", "Online player name", true) - .permissions(OP_ONLY, AdminPermission.FORCE_LOGIN) + .permission(AdminPermission.FORCE_LOGIN) .executableCommand(initializer.newInstance(ForceLoginCommand.class)) .build(); @@ -116,7 +116,7 @@ public class CommandInitializer { .detailedDescription("Change the password of a player.") .withArgument("player", "Player name", false) .withArgument("pwd", "New password", false) - .permissions(OP_ONLY, AdminPermission.CHANGE_PASSWORD) + .permission(AdminPermission.CHANGE_PASSWORD) .executableCommand(initializer.newInstance(ChangePasswordAdminCommand.class)) .build(); @@ -127,7 +127,7 @@ public class CommandInitializer { .description("Player's last login") .detailedDescription("View the date of the specified players last login.") .withArgument("player", "Player name", true) - .permissions(OP_ONLY, AdminPermission.LAST_LOGIN) + .permission(AdminPermission.LAST_LOGIN) .executableCommand(initializer.newInstance(LastLoginCommand.class)) .build(); @@ -138,7 +138,7 @@ public class CommandInitializer { .description("Display player accounts") .detailedDescription("Display all accounts of a player by his player name or IP.") .withArgument("player", "Player name or IP", true) - .permissions(OP_ONLY, AdminPermission.ACCOUNTS) + .permission(AdminPermission.ACCOUNTS) .executableCommand(initializer.newInstance(AccountsCommand.class)) .build(); @@ -149,7 +149,7 @@ public class CommandInitializer { .description("Display player's email") .detailedDescription("Display the email address of the specified player if set.") .withArgument("player", "Player name", true) - .permissions(OP_ONLY, AdminPermission.GET_EMAIL) + .permission(AdminPermission.GET_EMAIL) .executableCommand(initializer.newInstance(GetEmailCommand.class)) .build(); @@ -161,7 +161,7 @@ public class CommandInitializer { .detailedDescription("Change the email address of the specified player.") .withArgument("player", "Player name", false) .withArgument("email", "Player email", false) - .permissions(OP_ONLY, AdminPermission.CHANGE_EMAIL) + .permission(AdminPermission.CHANGE_EMAIL) .executableCommand(initializer.newInstance(SetEmailCommand.class)) .build(); @@ -172,7 +172,7 @@ public class CommandInitializer { .description("Get player's IP") .detailedDescription("Get the IP address of the specified online player.") .withArgument("player", "Player name", false) - .permissions(OP_ONLY, AdminPermission.GET_IP) + .permission(AdminPermission.GET_IP) .executableCommand(initializer.newInstance(GetIpCommand.class)) .build(); @@ -182,7 +182,7 @@ public class CommandInitializer { .labels("spawn", "home") .description("Teleport to spawn") .detailedDescription("Teleport to the spawn.") - .permissions(OP_ONLY, AdminPermission.SPAWN) + .permission(AdminPermission.SPAWN) .executableCommand(initializer.newInstance(SpawnCommand.class)) .build(); @@ -192,7 +192,7 @@ public class CommandInitializer { .labels("setspawn", "chgspawn") .description("Change the spawn") .detailedDescription("Change the player's spawn to your current position.") - .permissions(OP_ONLY, AdminPermission.SET_SPAWN) + .permission(AdminPermission.SET_SPAWN) .executableCommand(initializer.newInstance(SetSpawnCommand.class)) .build(); @@ -202,7 +202,7 @@ public class CommandInitializer { .labels("firstspawn", "firsthome") .description("Teleport to first spawn") .detailedDescription("Teleport to the first spawn.") - .permissions(OP_ONLY, AdminPermission.FIRST_SPAWN) + .permission(AdminPermission.FIRST_SPAWN) .executableCommand(initializer.newInstance(FirstSpawnCommand.class)) .build(); @@ -212,7 +212,7 @@ public class CommandInitializer { .labels("setfirstspawn", "chgfirstspawn") .description("Change the first spawn") .detailedDescription("Change the first player's spawn to your current position.") - .permissions(OP_ONLY, AdminPermission.SET_FIRST_SPAWN) + .permission(AdminPermission.SET_FIRST_SPAWN) .executableCommand(initializer.newInstance(SetFirstSpawnCommand.class)) .build(); @@ -223,7 +223,7 @@ public class CommandInitializer { .description("Purge old data") .detailedDescription("Purge old AuthMeReloaded data longer than the specified amount of days ago.") .withArgument("days", "Number of days", false) - .permissions(OP_ONLY, AdminPermission.PURGE) + .permission(AdminPermission.PURGE) .executableCommand(initializer.newInstance(PurgeCommand.class)) .build(); @@ -235,7 +235,7 @@ public class CommandInitializer { .description("Purge player's last position") .detailedDescription("Purge the last know position of the specified player or all of them.") .withArgument("player/*", "Player name or * for all players", false) - .permissions(OP_ONLY, AdminPermission.PURGE_LAST_POSITION) + .permission(AdminPermission.PURGE_LAST_POSITION) .executableCommand(initializer.newInstance(PurgeLastPositionCommand.class)) .build(); @@ -245,7 +245,7 @@ public class CommandInitializer { .labels("purgebannedplayers", "purgebannedplayer", "deletebannedplayers", "deletebannedplayer") .description("Purge banned players data") .detailedDescription("Purge all AuthMeReloaded data for banned players.") - .permissions(OP_ONLY, AdminPermission.PURGE_BANNED_PLAYERS) + .permission(AdminPermission.PURGE_BANNED_PLAYERS) .executableCommand(initializer.newInstance(PurgeBannedPlayersCommand.class)) .build(); @@ -256,7 +256,7 @@ public class CommandInitializer { .description("Switch AntiBot mode") .detailedDescription("Switch or toggle the AntiBot mode to the specified state.") .withArgument("mode", "ON / OFF", true) - .permissions(OP_ONLY, AdminPermission.SWITCH_ANTIBOT) + .permission(AdminPermission.SWITCH_ANTIBOT) .executableCommand(initializer.newInstance(SwitchAntiBotCommand.class)) .build(); @@ -266,7 +266,7 @@ public class CommandInitializer { .labels("reload", "rld") .description("Reload plugin") .detailedDescription("Reload the AuthMeReloaded plugin.") - .permissions(OP_ONLY, AdminPermission.RELOAD) + .permission(AdminPermission.RELOAD) .executableCommand(initializer.newInstance(ReloadCommand.class)) .build(); @@ -287,7 +287,7 @@ public class CommandInitializer { .detailedDescription("Converter command for AuthMeReloaded.") .withArgument("job", "Conversion job: xauth / crazylogin / rakamak / " + "royalauth / vauth / sqlitetosql", false) - .permissions(OP_ONLY, AdminPermission.CONVERTER) + .permission(AdminPermission.CONVERTER) .executableCommand(initializer.newInstance(ConverterCommand.class)) .build(); @@ -298,7 +298,7 @@ public class CommandInitializer { .description("Login command") .detailedDescription("Command to log in using AuthMeReloaded.") .withArgument("password", "Login password", false) - .permissions(ALLOWED, PlayerPermission.LOGIN) + .permission(PlayerPermission.LOGIN) .executableCommand(initializer.newInstance(LoginCommand.class)) .build(); @@ -308,7 +308,7 @@ public class CommandInitializer { .labels("logout") .description("Logout command") .detailedDescription("Command to logout using AuthMeReloaded.") - .permissions(ALLOWED, PlayerPermission.LOGOUT) + .permission(PlayerPermission.LOGOUT) .executableCommand(initializer.newInstance(LogoutCommand.class)) .build(); @@ -320,7 +320,7 @@ public class CommandInitializer { .detailedDescription("Command to register using AuthMeReloaded.") .withArgument("password", "Password", true) .withArgument("verifyPassword", "Verify password", true) - .permissions(ALLOWED, PlayerPermission.REGISTER) + .permission(PlayerPermission.REGISTER) .executableCommand(initializer.newInstance(RegisterCommand.class)) .build(); @@ -331,7 +331,7 @@ public class CommandInitializer { .description("Unregistration Command") .detailedDescription("Command to unregister using AuthMeReloaded.") .withArgument("password", "Password", false) - .permissions(ALLOWED, PlayerPermission.UNREGISTER) + .permission(PlayerPermission.UNREGISTER) .executableCommand(initializer.newInstance(UnregisterCommand.class)) .build(); @@ -343,7 +343,7 @@ public class CommandInitializer { .detailedDescription("Command to change your password using AuthMeReloaded.") .withArgument("oldPassword", "Old Password", false) .withArgument("newPassword", "New Password.", false) - .permissions(ALLOWED, PlayerPermission.CHANGE_PASSWORD) + .permission(PlayerPermission.CHANGE_PASSWORD) .executableCommand(initializer.newInstance(ChangePasswordCommand.class)) .build(); @@ -364,7 +364,7 @@ public class CommandInitializer { .detailedDescription("Add a new email address to your account.") .withArgument("email", "Email address", false) .withArgument("verifyEmail", "Email address verification", false) - .permissions(ALLOWED, PlayerPermission.ADD_EMAIL) + .permission(PlayerPermission.ADD_EMAIL) .executableCommand(initializer.newInstance(AddEmailCommand.class)) .build(); @@ -376,7 +376,7 @@ public class CommandInitializer { .detailedDescription("Change an email address of your account.") .withArgument("oldEmail", "Old email address", false) .withArgument("newEmail", "New email address", false) - .permissions(ALLOWED, PlayerPermission.CHANGE_EMAIL) + .permission(PlayerPermission.CHANGE_EMAIL) .executableCommand(initializer.newInstance(ChangeEmailCommand.class)) .build(); @@ -388,7 +388,7 @@ public class CommandInitializer { .detailedDescription("Recover your account using an Email address by sending a mail containing " + "a new password.") .withArgument("email", "Email address", false) - .permissions(ALLOWED, PlayerPermission.RECOVER_EMAIL) + .permission(PlayerPermission.RECOVER_EMAIL) .executableCommand(initializer.newInstance(RecoverEmailCommand.class)) .build(); @@ -399,7 +399,7 @@ public class CommandInitializer { .description("Captcha Command") .detailedDescription("Captcha command for AuthMeReloaded.") .withArgument("captcha", "The Captcha", false) - .permissions(ALLOWED, PlayerPermission.CAPTCHA) + .permission(PlayerPermission.CAPTCHA) .executableCommand(initializer.newInstance(CaptchaCommand.class)) .build(); diff --git a/src/main/java/fr/xephi/authme/command/CommandPermissions.java b/src/main/java/fr/xephi/authme/command/CommandPermissions.java deleted file mode 100644 index 0082b858..00000000 --- a/src/main/java/fr/xephi/authme/command/CommandPermissions.java +++ /dev/null @@ -1,52 +0,0 @@ -package fr.xephi.authme.command; - -import fr.xephi.authme.permission.DefaultPermission; -import fr.xephi.authme.permission.PermissionNode; - -import java.util.List; - -/** - */ -public class CommandPermissions { - - /** - * Defines the permission nodes required to have permission to execute this command. - */ - private List permissionNodes; - /** - * Defines the default permission if the permission nodes couldn't be used. - */ - private DefaultPermission defaultPermission; - - /** - * Constructor. - * - * @param permissionNodes The permission nodes required to execute a command. - * @param defaultPermission The default permission if the permission nodes couldn't be used. - */ - public CommandPermissions(List permissionNodes, DefaultPermission defaultPermission) { - this.permissionNodes = permissionNodes; - this.defaultPermission = defaultPermission; - } - - /** - * Get the permission nodes required to execute this command. - * - * @return The permission nodes required to execute this command. - */ - public List getPermissionNodes() { - return this.permissionNodes; - } - - - /** - * Get the default permission if the permission nodes couldn't be used. - * - * @return The default permission. - */ - public DefaultPermission getDefaultPermission() { - return this.defaultPermission; - } - - -} diff --git a/src/main/java/fr/xephi/authme/command/help/HelpProvider.java b/src/main/java/fr/xephi/authme/command/help/HelpProvider.java index 0acf62f8..1d8b6bdb 100644 --- a/src/main/java/fr/xephi/authme/command/help/HelpProvider.java +++ b/src/main/java/fr/xephi/authme/command/help/HelpProvider.java @@ -4,7 +4,6 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; import fr.xephi.authme.command.CommandArgumentDescription; import fr.xephi.authme.command.CommandDescription; -import fr.xephi.authme.command.CommandPermissions; import fr.xephi.authme.command.CommandUtils; import fr.xephi.authme.command.FoundCommandResult; import fr.xephi.authme.initialization.SettingsDependent; @@ -13,7 +12,6 @@ import fr.xephi.authme.permission.PermissionNode; import fr.xephi.authme.permission.PermissionsManager; import fr.xephi.authme.settings.NewSetting; import fr.xephi.authme.settings.properties.PluginSettings; -import fr.xephi.authme.util.CollectionUtils; import org.bukkit.ChatColor; import org.bukkit.command.CommandSender; @@ -139,22 +137,19 @@ public class HelpProvider implements SettingsDependent { private static void printPermissions(CommandDescription command, CommandSender sender, PermissionsManager permissionsManager, List lines) { - CommandPermissions permissions = command.getCommandPermissions(); - // TODO ljacqu 20151224: Isn't it possible to have a default permission but no permission nodes? - if (permissions == null || CollectionUtils.isEmpty(permissions.getPermissionNodes())) { + PermissionNode permission = command.getPermission(); + if (permission == null) { return; } lines.add(ChatColor.GOLD + "Permissions:"); - for (PermissionNode node : permissions.getPermissionNodes()) { - boolean hasPermission = permissionsManager.hasPermission(sender, node); - final String nodePermsString = "" + ChatColor.GRAY + ChatColor.ITALIC - + (hasPermission ? " (You have permission)" : " (No permission)"); - lines.add(" " + ChatColor.YELLOW + ChatColor.ITALIC + node.getNode() + nodePermsString); - } + boolean hasPermission = permissionsManager.hasPermission(sender, permission); + final String nodePermsString = "" + ChatColor.GRAY + ChatColor.ITALIC + + (hasPermission ? " (You have permission)" : " (No permission)"); + lines.add(" " + ChatColor.YELLOW + ChatColor.ITALIC + permission.getNode() + nodePermsString); // Addendum to the line to specify whether the sender has permission or not when default is OP_ONLY - final DefaultPermission defaultPermission = permissions.getDefaultPermission(); + final DefaultPermission defaultPermission = permission.getDefaultPermission(); String addendum = ""; if (DefaultPermission.OP_ONLY.equals(defaultPermission)) { addendum = PermissionsManager.evaluateDefaultPermission(defaultPermission, sender) diff --git a/src/main/java/fr/xephi/authme/permission/AdminPermission.java b/src/main/java/fr/xephi/authme/permission/AdminPermission.java index 6dedf827..043ba6dc 100644 --- a/src/main/java/fr/xephi/authme/permission/AdminPermission.java +++ b/src/main/java/fr/xephi/authme/permission/AdminPermission.java @@ -8,115 +8,121 @@ public enum AdminPermission implements PermissionNode { /** * Administrator command to register a new user. */ - REGISTER("authme.admin.register"), + REGISTER("authme.admin.register", DefaultPermission.OP_ONLY), /** * Administrator command to unregister an existing user. */ - UNREGISTER("authme.admin.unregister"), + UNREGISTER("authme.admin.unregister", DefaultPermission.OP_ONLY), /** * Administrator command to force-login an existing user. */ - FORCE_LOGIN("authme.admin.forcelogin"), + FORCE_LOGIN("authme.admin.forcelogin", DefaultPermission.OP_ONLY), /** * Administrator command to change the password of a user. */ - CHANGE_PASSWORD("authme.admin.changepassword"), + CHANGE_PASSWORD("authme.admin.changepassword", DefaultPermission.OP_ONLY), /** * Administrator command to see the last login date and time of a user. */ - LAST_LOGIN("authme.admin.lastlogin"), + LAST_LOGIN("authme.admin.lastlogin", DefaultPermission.OP_ONLY), /** * Administrator command to see all accounts associated with a user. */ - ACCOUNTS("authme.admin.accounts"), + ACCOUNTS("authme.admin.accounts", DefaultPermission.OP_ONLY), /** * Administrator command to get the email address of a user, if set. */ - GET_EMAIL("authme.admin.getemail"), + GET_EMAIL("authme.admin.getemail", DefaultPermission.OP_ONLY), /** * Administrator command to set or change the email address of a user. */ - CHANGE_EMAIL("authme.admin.changemail"), + CHANGE_EMAIL("authme.admin.changemail", DefaultPermission.OP_ONLY), /** * Administrator command to get the last known IP of a user. */ - GET_IP("authme.admin.getip"), + GET_IP("authme.admin.getip", DefaultPermission.OP_ONLY), /** * Administrator command to teleport to the AuthMe spawn. */ - SPAWN("authme.admin.spawn"), + SPAWN("authme.admin.spawn", DefaultPermission.OP_ONLY), /** * Administrator command to set the AuthMe spawn. */ - SET_SPAWN("authme.admin.setspawn"), + SET_SPAWN("authme.admin.setspawn", DefaultPermission.OP_ONLY), /** * Administrator command to teleport to the first AuthMe spawn. */ - FIRST_SPAWN("authme.admin.firstspawn"), + FIRST_SPAWN("authme.admin.firstspawn", DefaultPermission.OP_ONLY), /** * Administrator command to set the first AuthMe spawn. */ - SET_FIRST_SPAWN("authme.admin.setfirstspawn"), + SET_FIRST_SPAWN("authme.admin.setfirstspawn", DefaultPermission.OP_ONLY), /** * Administrator command to purge old user data. */ - PURGE("authme.admin.purge"), + PURGE("authme.admin.purge", DefaultPermission.OP_ONLY), /** * Administrator command to purge the last position of a user. */ - PURGE_LAST_POSITION("authme.admin.purgelastpos"), + PURGE_LAST_POSITION("authme.admin.purgelastpos", DefaultPermission.OP_ONLY), /** * Administrator command to purge all data associated with banned players. */ - PURGE_BANNED_PLAYERS("authme.admin.purgebannedplayers"), + PURGE_BANNED_PLAYERS("authme.admin.purgebannedplayers", DefaultPermission.OP_ONLY), /** * Administrator command to toggle the AntiBot protection status. */ - SWITCH_ANTIBOT("authme.admin.switchantibot"), + SWITCH_ANTIBOT("authme.admin.switchantibot", DefaultPermission.OP_ONLY), /** * Administrator command to convert old or other data to AuthMe data. */ - CONVERTER("authme.admin.converter"), + CONVERTER("authme.admin.converter", DefaultPermission.OP_ONLY), /** * Administrator command to reload the plugin configuration. */ - RELOAD("authme.admin.reload"), + RELOAD("authme.admin.reload", DefaultPermission.OP_ONLY), /** * Permission to see the other accounts of the players that log in. */ - SEE_OTHER_ACCOUNTS("authme.admin.seeotheraccounts"); + SEE_OTHER_ACCOUNTS("authme.admin.seeotheraccounts", DefaultPermission.OP_ONLY); /** * The permission node. */ private String node; + /** + * The default permission level + */ + private DefaultPermission defaultPermission; + /** * Constructor. * * @param node Permission node. */ - AdminPermission(String node) { + AdminPermission(String node, DefaultPermission defaultPermission) { this.node = node; + this.defaultPermission = defaultPermission; } @Override @@ -124,4 +130,8 @@ public enum AdminPermission implements PermissionNode { return node; } + @Override + public DefaultPermission getDefaultPermission() { + return defaultPermission; + } } diff --git a/src/main/java/fr/xephi/authme/permission/PermissionNode.java b/src/main/java/fr/xephi/authme/permission/PermissionNode.java index 04f9c80c..189f97b5 100644 --- a/src/main/java/fr/xephi/authme/permission/PermissionNode.java +++ b/src/main/java/fr/xephi/authme/permission/PermissionNode.java @@ -12,4 +12,10 @@ public interface PermissionNode { */ String getNode(); + /** + * Return the default permission for this node, e.g. "OP_ONLY" + * + * @return The default level of permission + */ + DefaultPermission getDefaultPermission(); } diff --git a/src/main/java/fr/xephi/authme/permission/PermissionsManager.java b/src/main/java/fr/xephi/authme/permission/PermissionsManager.java index 1a5fd929..358bc555 100644 --- a/src/main/java/fr/xephi/authme/permission/PermissionsManager.java +++ b/src/main/java/fr/xephi/authme/permission/PermissionsManager.java @@ -285,15 +285,14 @@ public class PermissionsManager implements PermissionsService { } public boolean hasPermission(CommandSender sender, CommandDescription command) { - if (command.getCommandPermissions() == null - || CollectionUtils.isEmpty(command.getCommandPermissions().getPermissionNodes())) { + if (command.getPermission() == null) { return true; } - DefaultPermission defaultPermission = command.getCommandPermissions().getDefaultPermission(); + DefaultPermission defaultPermission = command.getPermission().getDefaultPermission(); boolean def = evaluateDefaultPermission(defaultPermission, sender); return (sender instanceof Player) - ? hasPermission((Player) sender, command.getCommandPermissions().getPermissionNodes(), def) + ? hasPermission((Player) sender, command.getPermission().getNode(), def) : def; } diff --git a/src/main/java/fr/xephi/authme/permission/PlayerPermission.java b/src/main/java/fr/xephi/authme/permission/PlayerPermission.java index 210cbce9..e2932e93 100644 --- a/src/main/java/fr/xephi/authme/permission/PlayerPermission.java +++ b/src/main/java/fr/xephi/authme/permission/PlayerPermission.java @@ -8,70 +8,76 @@ public enum PlayerPermission implements PermissionNode { /** * Command permission to login. */ - LOGIN("authme.player.login"), + LOGIN("authme.player.login", DefaultPermission.ALLOWED), /** * Command permission to logout. */ - LOGOUT("authme.player.logout"), + LOGOUT("authme.player.logout", DefaultPermission.ALLOWED), /** * Command permission to register. */ - REGISTER("authme.player.register"), + REGISTER("authme.player.register", DefaultPermission.ALLOWED), /** * Command permission to unregister. */ - UNREGISTER("authme.player.unregister"), + UNREGISTER("authme.player.unregister", DefaultPermission.ALLOWED), /** * Command permission to change the password. */ - CHANGE_PASSWORD("authme.player.changepassword"), + CHANGE_PASSWORD("authme.player.changepassword", DefaultPermission.ALLOWED), /** * Command permission to add an email address. */ - ADD_EMAIL("authme.player.email.add"), + ADD_EMAIL("authme.player.email.add", DefaultPermission.ALLOWED), /** * Command permission to change the email address. */ - CHANGE_EMAIL("authme.player.email.change"), + CHANGE_EMAIL("authme.player.email.change", DefaultPermission.ALLOWED), /** * Command permission to recover an account using it's email address. */ - RECOVER_EMAIL("authme.player.email.recover"), + RECOVER_EMAIL("authme.player.email.recover", DefaultPermission.ALLOWED), /** * Command permission to use captcha. */ - CAPTCHA("authme.player.captcha"), + CAPTCHA("authme.player.captcha", DefaultPermission.ALLOWED), /** * Permission for users a login can be forced to. */ - CAN_LOGIN_BE_FORCED("authme.player.canbeforced"), + CAN_LOGIN_BE_FORCED("authme.player.canbeforced", DefaultPermission.ALLOWED), /** * Permission to use to see own other accounts. */ - SEE_OWN_ACCOUNTS("authme.player.seeownaccounts"); + SEE_OWN_ACCOUNTS("authme.player.seeownaccounts", DefaultPermission.ALLOWED); /** * The permission node. */ private String node; + /** + * The default permission level + */ + private DefaultPermission defaultPermission; + /** * Constructor. * * @param node Permission node. */ - PlayerPermission(String node) { + PlayerPermission(String node, DefaultPermission defaultPermission) { this.node = node; + this.defaultPermission = defaultPermission; } @Override @@ -79,4 +85,9 @@ public enum PlayerPermission implements PermissionNode { return node; } + @Override + public DefaultPermission getDefaultPermission() { + return defaultPermission; + } + } diff --git a/src/main/java/fr/xephi/authme/permission/PlayerStatePermission.java b/src/main/java/fr/xephi/authme/permission/PlayerStatePermission.java index b91a1d4b..fd023935 100644 --- a/src/main/java/fr/xephi/authme/permission/PlayerStatePermission.java +++ b/src/main/java/fr/xephi/authme/permission/PlayerStatePermission.java @@ -9,39 +9,50 @@ public enum PlayerStatePermission implements PermissionNode { /** * Permission node to bypass AntiBot protection. */ - BYPASS_ANTIBOT("authme.bypassantibot"), + BYPASS_ANTIBOT("authme.bypassantibot", DefaultPermission.OP_ONLY), /** * Permission for users to bypass force-survival mode. */ - BYPASS_FORCE_SURVIVAL("authme.bypassforcesurvival"), + BYPASS_FORCE_SURVIVAL("authme.bypassforcesurvival", DefaultPermission.OP_ONLY), /** * Permission node to identify VIP users. */ - IS_VIP("authme.vip"), + IS_VIP("authme.vip", DefaultPermission.OP_ONLY), /** * Permission to be able to register multiple accounts. */ - ALLOW_MULTIPLE_ACCOUNTS("authme.allowmultipleaccounts"); + ALLOW_MULTIPLE_ACCOUNTS("authme.allowmultipleaccounts", DefaultPermission.OP_ONLY); /** * The permission node. */ private String node; + /** + * The default permission level + */ + private DefaultPermission defaultPermission; + /** * Constructor. * * @param node Permission node. */ - PlayerStatePermission(String node) { + PlayerStatePermission(String node, DefaultPermission defaultPermission) { this.node = node; + this.defaultPermission = defaultPermission; } @Override public String getNode() { return node; } + + @Override + public DefaultPermission getDefaultPermission() { + return defaultPermission; + } } diff --git a/src/test/java/fr/xephi/authme/command/CommandInitializerTest.java b/src/test/java/fr/xephi/authme/command/CommandInitializerTest.java index 1af9687b..81f4741a 100644 --- a/src/test/java/fr/xephi/authme/command/CommandInitializerTest.java +++ b/src/test/java/fr/xephi/authme/command/CommandInitializerTest.java @@ -256,21 +256,16 @@ public class CommandInitializerTest { BiConsumer adminPermissionChecker = new BiConsumer() { @Override public void accept(CommandDescription command, int depth) { - CommandPermissions permissions = command.getCommandPermissions(); - if (permissions != null && OP_ONLY.equals(permissions.getDefaultPermission()) - && !hasAdminNode(permissions)) { + PermissionNode permission = command.getPermission(); + if (permission != null && OP_ONLY.equals(permission.getDefaultPermission()) + && !hasAdminNode(permission)) { fail("The command with labels " + command.getLabels() + " has OP_ONLY default " + "permission but no permission node on admin level"); } } - private boolean hasAdminNode(CommandPermissions permissions) { - for (PermissionNode node : permissions.getPermissionNodes()) { - if (node instanceof AdminPermission) { - return true; - } - } - return false; + private boolean hasAdminNode(PermissionNode permission) { + return permission instanceof AdminPermission; } }; diff --git a/src/test/java/fr/xephi/authme/command/TestCommandsUtil.java b/src/test/java/fr/xephi/authme/command/TestCommandsUtil.java index 6f7857b0..2fc4c215 100644 --- a/src/test/java/fr/xephi/authme/command/TestCommandsUtil.java +++ b/src/test/java/fr/xephi/authme/command/TestCommandsUtil.java @@ -84,18 +84,10 @@ public final class TestCommandsUtil { /** Shortcut command to initialize a new test command. */ private static CommandDescription createCommand(PermissionNode permission, CommandDescription parent, List labels, CommandArgumentDescription... arguments) { - PermissionNode[] notNullPermission; - if (permission == null) { - notNullPermission = new PermissionNode[0]; - } else { - notNullPermission = new PermissionNode[1]; - notNullPermission[0] = permission; - } - CommandDescription.CommandBuilder command = CommandDescription.builder() .labels(labels) .parent(parent) - .permissions(DefaultPermission.OP_ONLY, notNullPermission) + .permission(permission) .description(labels.get(0) + " cmd") .detailedDescription("'" + labels.get(0) + "' test command") .executableCommand(mock(ExecutableCommand.class)); diff --git a/src/test/java/fr/xephi/authme/command/help/HelpProviderTest.java b/src/test/java/fr/xephi/authme/command/help/HelpProviderTest.java index e7d85f14..7c6cf74d 100644 --- a/src/test/java/fr/xephi/authme/command/help/HelpProviderTest.java +++ b/src/test/java/fr/xephi/authme/command/help/HelpProviderTest.java @@ -12,6 +12,7 @@ import org.bukkit.ChatColor; import org.bukkit.command.CommandSender; import org.junit.Before; import org.junit.BeforeClass; +import org.junit.Ignore; import org.junit.Test; import java.util.Arrays; @@ -124,6 +125,8 @@ public class HelpProviderTest { } @Test + @Ignore + // TODO Gnat008 20160530: Update tests for new PermissionNode setup public void shouldShowAndEvaluatePermissions() { // given CommandDescription command = getCommandWithLabel(commands, "authme", "login"); @@ -145,6 +148,8 @@ public class HelpProviderTest { } @Test + @Ignore + // TODO Gnat008 20160530: Update tests for new PermissionNode setup public void shouldShowAndEvaluateForbiddenPermissions() { // given CommandDescription command = getCommandWithLabel(commands, "authme", "login"); @@ -182,7 +187,7 @@ public class HelpProviderTest { public void shouldNotShowAnythingForNullPermissionsOnCommand() { // given CommandDescription command = mock(CommandDescription.class); - given(command.getCommandPermissions()).willReturn(null); + given(command.getPermission()).willReturn(null); given(command.getLabels()).willReturn(Collections.singletonList("test")); FoundCommandResult result = newFoundResult(command, Collections.singletonList("test")); diff --git a/src/test/java/tools/commands/CommandPageCreater.java b/src/test/java/tools/commands/CommandPageCreater.java index 74bc74a9..b8478205 100644 --- a/src/test/java/tools/commands/CommandPageCreater.java +++ b/src/test/java/tools/commands/CommandPageCreater.java @@ -3,7 +3,6 @@ package tools.commands; import fr.xephi.authme.command.CommandArgumentDescription; import fr.xephi.authme.command.CommandDescription; import fr.xephi.authme.command.CommandInitializer; -import fr.xephi.authme.command.CommandPermissions; import fr.xephi.authme.command.CommandUtils; import fr.xephi.authme.command.ExecutableCommand; import fr.xephi.authme.initialization.AuthMeServiceInitializer; @@ -58,7 +57,7 @@ public class CommandPageCreater implements AutoToolTask { .put("command", CommandUtils.constructCommandPath(command)) .put("description", command.getDetailedDescription()) .put("arguments", formatArguments(command.getArguments())) - .put("permissions", formatPermissions(command.getCommandPermissions())); + .put("permissions", formatPermissions(command.getPermission())); commandTags.add(tags); if (!command.getChildren().isEmpty()) { @@ -67,15 +66,12 @@ public class CommandPageCreater implements AutoToolTask { } } - private static String formatPermissions(CommandPermissions permissions) { - if (permissions == null) { + private static String formatPermissions(PermissionNode permission) { + if (permission == null) { return ""; + } else { + return permission.getNode(); } - String result = ""; - for (PermissionNode node : permissions.getPermissionNodes()) { - result += node.getNode() + " "; - } - return result.trim(); } private static String formatArguments(Iterable arguments) { From 30b72bec4c9ba14a016037c125dd5f2b87fa64b1 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Mon, 30 May 2016 23:49:59 +0200 Subject: [PATCH 127/200] #604 Fix HelpProvider tests --- .../authme/command/CommandDescription.java | 3 +-- .../authme/command/TestCommandsUtil.java | 6 ++--- .../authme/command/help/HelpProviderTest.java | 23 ++++++++----------- 3 files changed, 13 insertions(+), 19 deletions(-) diff --git a/src/main/java/fr/xephi/authme/command/CommandDescription.java b/src/main/java/fr/xephi/authme/command/CommandDescription.java index 595f513d..7ff0e739 100644 --- a/src/main/java/fr/xephi/authme/command/CommandDescription.java +++ b/src/main/java/fr/xephi/authme/command/CommandDescription.java @@ -1,6 +1,5 @@ package fr.xephi.authme.command; -import fr.xephi.authme.permission.DefaultPermission; import fr.xephi.authme.permission.PermissionNode; import fr.xephi.authme.util.CollectionUtils; import fr.xephi.authme.util.StringUtils; @@ -287,7 +286,7 @@ public class CommandDescription { } /** - * Add a permission node that the a user must have to execute the command. + * Add a permission node that a user must have to execute the command. * * @param permission The PermissionNode to add * @return The builder diff --git a/src/test/java/fr/xephi/authme/command/TestCommandsUtil.java b/src/test/java/fr/xephi/authme/command/TestCommandsUtil.java index 2fc4c215..65fb74e2 100644 --- a/src/test/java/fr/xephi/authme/command/TestCommandsUtil.java +++ b/src/test/java/fr/xephi/authme/command/TestCommandsUtil.java @@ -1,7 +1,7 @@ package fr.xephi.authme.command; import fr.xephi.authme.command.executable.HelpCommand; -import fr.xephi.authme.permission.DefaultPermission; +import fr.xephi.authme.permission.AdminPermission; import fr.xephi.authme.permission.PermissionNode; import fr.xephi.authme.permission.PlayerPermission; @@ -43,8 +43,8 @@ public final class TestCommandsUtil { .description("test").detailedDescription("Test.").withArgument("Query", "", false).build(); // Register /unregister , alias: /unreg - CommandDescription unregisterBase = createCommand(null, null, asList("unregister", "unreg"), - newArgument("player", false)); + CommandDescription unregisterBase = createCommand(AdminPermission.UNREGISTER, null, + asList("unregister", "unreg"), newArgument("player", false)); return newHashSet(authMeBase, emailBase, unregisterBase); } diff --git a/src/test/java/fr/xephi/authme/command/help/HelpProviderTest.java b/src/test/java/fr/xephi/authme/command/help/HelpProviderTest.java index 7c6cf74d..1c877b73 100644 --- a/src/test/java/fr/xephi/authme/command/help/HelpProviderTest.java +++ b/src/test/java/fr/xephi/authme/command/help/HelpProviderTest.java @@ -4,15 +4,14 @@ import fr.xephi.authme.command.CommandDescription; import fr.xephi.authme.command.FoundCommandResult; import fr.xephi.authme.command.FoundResultStatus; import fr.xephi.authme.command.TestCommandsUtil; +import fr.xephi.authme.permission.AdminPermission; import fr.xephi.authme.permission.PermissionsManager; -import fr.xephi.authme.permission.PlayerPermission; import fr.xephi.authme.settings.NewSetting; import fr.xephi.authme.settings.properties.PluginSettings; import org.bukkit.ChatColor; import org.bukkit.command.CommandSender; import org.junit.Before; import org.junit.BeforeClass; -import org.junit.Ignore; import org.junit.Test; import java.util.Arrays; @@ -125,14 +124,12 @@ public class HelpProviderTest { } @Test - @Ignore - // TODO Gnat008 20160530: Update tests for new PermissionNode setup public void shouldShowAndEvaluatePermissions() { // given - CommandDescription command = getCommandWithLabel(commands, "authme", "login"); - FoundCommandResult result = newFoundResult(command, Collections.singletonList("authme")); + CommandDescription command = getCommandWithLabel(commands, "unregister"); + FoundCommandResult result = newFoundResult(command, Collections.singletonList("unreg")); given(sender.isOp()).willReturn(true); - given(permissionsManager.hasPermission(sender, PlayerPermission.LOGIN)).willReturn(true); + given(permissionsManager.hasPermission(sender, AdminPermission.UNREGISTER)).willReturn(true); given(permissionsManager.hasPermission(sender, command)).willReturn(true); // when @@ -142,20 +139,18 @@ public class HelpProviderTest { assertThat(lines, hasSize(5)); assertThat(removeColors(lines.get(1)), containsString("Permissions:")); assertThat(removeColors(lines.get(2)), - containsString(PlayerPermission.LOGIN.getNode() + " (You have permission)")); + containsString(AdminPermission.UNREGISTER.getNode() + " (You have permission)")); assertThat(removeColors(lines.get(3)), containsString("Default: OP's only (You have permission)")); assertThat(removeColors(lines.get(4)), containsString("Result: You have permission")); } @Test - @Ignore - // TODO Gnat008 20160530: Update tests for new PermissionNode setup public void shouldShowAndEvaluateForbiddenPermissions() { // given - CommandDescription command = getCommandWithLabel(commands, "authme", "login"); - FoundCommandResult result = newFoundResult(command, Collections.singletonList("authme")); + CommandDescription command = getCommandWithLabel(commands, "unregister"); + FoundCommandResult result = newFoundResult(command, Collections.singletonList("unregister")); given(sender.isOp()).willReturn(false); - given(permissionsManager.hasPermission(sender, PlayerPermission.LOGIN)).willReturn(false); + given(permissionsManager.hasPermission(sender, AdminPermission.UNREGISTER)).willReturn(false); given(permissionsManager.hasPermission(sender, command)).willReturn(false); // when @@ -165,7 +160,7 @@ public class HelpProviderTest { assertThat(lines, hasSize(5)); assertThat(removeColors(lines.get(1)), containsString("Permissions:")); assertThat(removeColors(lines.get(2)), - containsString(PlayerPermission.LOGIN.getNode() + " (No permission)")); + containsString(AdminPermission.UNREGISTER.getNode() + " (No permission)")); assertThat(removeColors(lines.get(3)), containsString("Default: OP's only (No permission)")); assertThat(removeColors(lines.get(4)), containsString("Result: No permission")); } From 506f32243b816b55ae33bc26c6c45443fb1de06e Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Tue, 31 May 2016 00:09:19 +0200 Subject: [PATCH 128/200] Fix pom typo + update h2 test dependency --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index dc62638d..9b2f05cd 100644 --- a/pom.xml +++ b/pom.xml @@ -123,7 +123,7 @@ maven-surefire-plugin 2.19.1 - -Dfile.encoding=${project.build.sourceEncoding} ${argLine} + -Dfile.encoding=${project.build.sourceEncoding} @{argLine} @@ -839,7 +839,7 @@ com.h2database h2 - 1.4.191 + 1.4.192 test From be4b3a86050a8f99305fd3c90b3d210ff929c691 Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Tue, 31 May 2016 10:29:35 +0200 Subject: [PATCH 129/200] Update messages_hu.yml --- src/main/resources/messages/messages_hu.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/resources/messages/messages_hu.yml b/src/main/resources/messages/messages_hu.yml index bb26702b..53c5b52d 100644 --- a/src/main/resources/messages/messages_hu.yml +++ b/src/main/resources/messages/messages_hu.yml @@ -62,4 +62,5 @@ not_owner_error: 'Ez nem a te felhasználód. Kérlek válassz másik nevet!' invalid_name_case: '%valid a felhasználó neved nem? Akkor ne %invalid névvel próbálj feljönni.' two_factor_create: '&2A te titkos kódod a következő: %code. Vagy skenneld be a következő oldalról: %url' denied_chat: '&cAmíg nem vagy bejelentkezve, nem használhatod a csevegőt!' -# TODO same_ip_online: 'A player with the same IP is already in game!' \ No newline at end of file +denied_command: '&cAmíg nem vagy bejelentkezve, nem használhatod ezt a parancsot!' +same_ip_online: 'Már valaki csatlakozott a szerverhez ezzel az IP címmel!' From 097755892487c28be796d68e639f1370c1397f7a Mon Sep 17 00:00:00 2001 From: ljacqu Date: Tue, 31 May 2016 11:13:42 +0200 Subject: [PATCH 130/200] #736 Remove use of service getters and deprecate them --- src/main/java/fr/xephi/authme/AuthMe.java | 46 +++++++++++++------ src/main/java/fr/xephi/authme/api/NewAPI.java | 19 ++------ .../changepassword/ChangePasswordCommand.java | 8 +++- .../authme/converter/RakamakConverter.java | 9 ++-- .../xephi/authme/hooks/BungeeCordMessage.java | 35 +++++++------- .../process/login/AsynchronousLogin.java | 8 +++- .../process/register/AsyncRegister.java | 12 +++-- .../fr/xephi/authme/settings/SpawnLoader.java | 8 +++- .../xephi/authme/task/ChangePasswordTask.java | 7 +-- .../authme/settings/SpawnLoaderTest.java | 7 ++- 10 files changed, 96 insertions(+), 63 deletions(-) diff --git a/src/main/java/fr/xephi/authme/AuthMe.java b/src/main/java/fr/xephi/authme/AuthMe.java index 9c2124d7..eaefea8d 100644 --- a/src/main/java/fr/xephi/authme/AuthMe.java +++ b/src/main/java/fr/xephi/authme/AuthMe.java @@ -297,7 +297,7 @@ public class AuthMe extends JavaPlugin { // Set up the BungeeCord hook - setupBungeeCordHook(newSettings); + setupBungeeCordHook(newSettings, initializer); // Reload support hook reloadSupportHook(); @@ -396,11 +396,11 @@ public class AuthMe extends JavaPlugin { /** * Set up the BungeeCord hook. */ - private void setupBungeeCordHook(NewSetting settings) { + private void setupBungeeCordHook(NewSetting settings, AuthMeServiceInitializer initializer) { if (settings.getProperty(HooksSettings.BUNGEECORD)) { Bukkit.getMessenger().registerOutgoingPluginChannel(this, "BungeeCord"); Bukkit.getMessenger().registerIncomingPluginChannel( - this, "BungeeCord", new BungeeCordMessage(this)); + this, "BungeeCord", initializer.get(BungeeCordMessage.class)); } } @@ -750,37 +750,57 @@ public class AuthMe extends JavaPlugin { return commandHandler.processCommand(sender, commandLabel, args); } + public void notifyAutoPurgeEnd() { + this.autoPurging = false; + } + + + // ------------- + // Service getters (deprecated) + // Use @Inject fields instead + // ------------- /** - * Get the permissions manager instance. - * - * @return Permissions Manager instance. + * @return permission manager + * @deprecated should be used in API classes only (temporarily) */ + @Deprecated public PermissionsManager getPermissionsManager() { return this.permsMan; } /** - * Return the management instance. - * - * @return management The Management + * @return process manager + * @deprecated should be used in API classes only (temporarily) */ + @Deprecated public Management getManagement() { return management; } + /** + * @return the datasource + * @deprecated should be used in API classes only (temporarily) + */ + @Deprecated public DataSource getDataSource() { return database; } + /** + * @return password manager + * @deprecated should be used in API classes only (temporarily) + */ + @Deprecated public PasswordSecurity getPasswordSecurity() { return passwordSecurity; } + /** + * @return plugin hooks + * @deprecated should be used in API classes only (temporarily) + */ + @Deprecated public PluginHooks getPluginHooks() { return pluginHooks; } - - public void notifyAutoPurgeEnd() { - this.autoPurging = false; - } } diff --git a/src/main/java/fr/xephi/authme/api/NewAPI.java b/src/main/java/fr/xephi/authme/api/NewAPI.java index a0def639..5e0063e9 100644 --- a/src/main/java/fr/xephi/authme/api/NewAPI.java +++ b/src/main/java/fr/xephi/authme/api/NewAPI.java @@ -1,16 +1,14 @@ package fr.xephi.authme.api; -import org.bukkit.Bukkit; -import org.bukkit.Location; -import org.bukkit.Server; -import org.bukkit.entity.Player; -import org.bukkit.plugin.Plugin; - import fr.xephi.authme.AuthMe; import fr.xephi.authme.cache.auth.PlayerAuth; import fr.xephi.authme.cache.auth.PlayerCache; import fr.xephi.authme.security.crypts.HashedPassword; import fr.xephi.authme.util.Utils; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; import javax.inject.Inject; @@ -35,15 +33,6 @@ public class NewAPI { this.plugin = plugin; } - /** - * Constructor for NewAPI. - * - * @param server The server instance - */ - public NewAPI(Server server) { - this.plugin = (AuthMe) server.getPluginManager().getPlugin("AuthMe"); - } - /** * Get the API object for AuthMe. * 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 310976fc..01aea862 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,6 +5,7 @@ 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.security.PasswordSecurity; import fr.xephi.authme.task.ChangePasswordTask; import fr.xephi.authme.util.BukkitService; import org.bukkit.entity.Player; @@ -23,6 +24,10 @@ public class ChangePasswordCommand extends PlayerCommand { @Inject private BukkitService bukkitService; + @Inject + // TODO ljacqu 20160531: Remove this once change password task runs as a process (via Management) + private PasswordSecurity passwordSecurity; + @Override public void runCommand(Player player, List arguments, CommandService commandService) { String oldPassword = arguments.get(0); @@ -43,6 +48,7 @@ public class ChangePasswordCommand extends PlayerCommand { AuthMe plugin = AuthMe.getInstance(); // TODO ljacqu 20160117: Call async task via Management - bukkitService.runTaskAsynchronously(new ChangePasswordTask(plugin, player, oldPassword, newPassword)); + bukkitService.runTaskAsynchronously( + new ChangePasswordTask(plugin, player, oldPassword, newPassword, passwordSecurity)); } } diff --git a/src/main/java/fr/xephi/authme/converter/RakamakConverter.java b/src/main/java/fr/xephi/authme/converter/RakamakConverter.java index de6ea77b..e568bb8b 100644 --- a/src/main/java/fr/xephi/authme/converter/RakamakConverter.java +++ b/src/main/java/fr/xephi/authme/converter/RakamakConverter.java @@ -1,6 +1,5 @@ package fr.xephi.authme.converter; -import fr.xephi.authme.AuthMe; import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.cache.auth.PlayerAuth; import fr.xephi.authme.datasource.DataSource; @@ -24,17 +23,18 @@ import java.util.Map.Entry; */ public class RakamakConverter implements Converter { - private final AuthMe instance; private final DataSource database; private final NewSetting settings; private final File pluginFolder; + private final PasswordSecurity passwordSecurity; @Inject - RakamakConverter(@DataFolder File dataFolder, AuthMe instance, DataSource dataSource, NewSetting settings) { - this.instance = instance; + RakamakConverter(@DataFolder File dataFolder, DataSource dataSource, NewSetting settings, + PasswordSecurity passwordSecurity) { this.database = dataSource; this.settings = settings; this.pluginFolder = dataFolder; + this.passwordSecurity = passwordSecurity; } @Override @@ -64,7 +64,6 @@ public class RakamakConverter implements Converter { ipFile.close(); users = new BufferedReader(new FileReader(source)); - PasswordSecurity passwordSecurity = instance.getPasswordSecurity(); while ((line = users.readLine()) != null) { if (line.contains("=")) { String[] arguments = line.split("="); diff --git a/src/main/java/fr/xephi/authme/hooks/BungeeCordMessage.java b/src/main/java/fr/xephi/authme/hooks/BungeeCordMessage.java index ed089b8a..26d5847a 100644 --- a/src/main/java/fr/xephi/authme/hooks/BungeeCordMessage.java +++ b/src/main/java/fr/xephi/authme/hooks/BungeeCordMessage.java @@ -2,29 +2,31 @@ package fr.xephi.authme.hooks; import com.google.common.io.ByteArrayDataInput; import com.google.common.io.ByteStreams; -import fr.xephi.authme.AuthMe; import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.cache.auth.PlayerAuth; import fr.xephi.authme.cache.auth.PlayerCache; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.security.crypts.HashedPassword; +import fr.xephi.authme.util.BukkitService; import org.bukkit.entity.Player; import org.bukkit.plugin.messaging.PluginMessageListener; -/** - */ +import javax.inject.Inject; + + public class BungeeCordMessage implements PluginMessageListener { - private final AuthMe plugin; + @Inject + private DataSource dataSource; + + @Inject + private BukkitService bukkitService; + + @Inject + private PlayerCache playerCache; + + BungeeCordMessage() { } - /** - * Constructor for BungeeCordMessage. - * - * @param plugin The plugin instance - */ - public BungeeCordMessage(AuthMe plugin) { - this.plugin = plugin; - } @Override public void onPluginMessageReceived(String channel, Player player, byte[] message) { @@ -38,8 +40,7 @@ public class BungeeCordMessage implements PluginMessageListener { final String[] args = str.split(";"); final String act = args[0]; final String name = args[1]; - final DataSource dataSource = plugin.getDataSource(); - plugin.getServer().getScheduler().runTaskAsynchronously(plugin, new Runnable() { + bukkitService.runTaskAsynchronously(new Runnable() { @Override public void run() { PlayerAuth auth = dataSource.getAuth(name); @@ -47,12 +48,12 @@ public class BungeeCordMessage implements PluginMessageListener { return; } if ("login".equals(act)) { - PlayerCache.getInstance().updatePlayer(auth); + playerCache.updatePlayer(auth); dataSource.setLogged(name); ConsoleLogger.info("Player " + auth.getNickname() + " has logged in from one of your server!"); } else if ("logout".equals(act)) { - PlayerCache.getInstance().removePlayer(name); + playerCache.removePlayer(name); dataSource.setUnlogged(name); ConsoleLogger.info("Player " + auth.getNickname() + " has logged out from one of your server!"); @@ -63,7 +64,7 @@ public class BungeeCordMessage implements PluginMessageListener { final String password = args[2]; final String salt = args.length >= 4 ? args[3] : null; auth.setPassword(new HashedPassword(password, salt)); - PlayerCache.getInstance().updatePlayer(auth); + playerCache.updatePlayer(auth); dataSource.updatePassword(auth); } diff --git a/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java b/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java index a86ac93e..473c8142 100644 --- a/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java +++ b/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java @@ -16,6 +16,7 @@ import fr.xephi.authme.permission.PlayerStatePermission; import fr.xephi.authme.process.AsynchronousProcess; import fr.xephi.authme.process.ProcessService; import fr.xephi.authme.process.SyncProcessManager; +import fr.xephi.authme.security.PasswordSecurity; import fr.xephi.authme.security.RandomString; import fr.xephi.authme.settings.Settings; import fr.xephi.authme.settings.properties.DatabaseSettings; @@ -62,6 +63,9 @@ public class AsynchronousLogin implements AsynchronousProcess { @Inject private BukkitService bukkitService; + @Inject + private PasswordSecurity passwordSecurity; + AsynchronousLogin() { } @@ -150,8 +154,8 @@ public class AsynchronousLogin implements AsynchronousProcess { } String email = pAuth.getEmail(); - boolean passwordVerified = forceLogin || plugin.getPasswordSecurity() - .comparePassword(password, pAuth.getPassword(), player.getName()); + boolean passwordVerified = forceLogin || passwordSecurity.comparePassword( + password, pAuth.getPassword(), player.getName()); final String name = player.getName().toLowerCase(); if (passwordVerified && player.isOnline()) { diff --git a/src/main/java/fr/xephi/authme/process/register/AsyncRegister.java b/src/main/java/fr/xephi/authme/process/register/AsyncRegister.java index fbc6c86d..2d47b5ba 100644 --- a/src/main/java/fr/xephi/authme/process/register/AsyncRegister.java +++ b/src/main/java/fr/xephi/authme/process/register/AsyncRegister.java @@ -5,7 +5,7 @@ import fr.xephi.authme.cache.auth.PlayerAuth; import fr.xephi.authme.cache.auth.PlayerCache; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.output.MessageKey; -import fr.xephi.authme.permission.PlayerStatePermission; +import fr.xephi.authme.permission.PermissionsManager; import fr.xephi.authme.process.AsynchronousProcess; import fr.xephi.authme.process.ProcessService; import fr.xephi.authme.process.SyncProcessManager; @@ -26,6 +26,8 @@ import org.bukkit.entity.Player; import javax.inject.Inject; import java.util.List; +import static fr.xephi.authme.permission.PlayerStatePermission.ALLOW_MULTIPLE_ACCOUNTS; + public class AsyncRegister implements AsynchronousProcess { @Inject @@ -46,6 +48,9 @@ public class AsyncRegister implements AsynchronousProcess { @Inject private SyncProcessManager syncProcessManager; + @Inject + private PermissionsManager permissionsManager; + AsyncRegister() { } private boolean preRegisterCheck(Player player, String password) { @@ -78,7 +83,7 @@ public class AsyncRegister implements AsynchronousProcess { if (maxRegPerIp > 0 && !"127.0.0.1".equalsIgnoreCase(ip) && !"localhost".equalsIgnoreCase(ip) - && !plugin.getPermissionsManager().hasPermission(player, PlayerStatePermission.ALLOW_MULTIPLE_ACCOUNTS)) { + && !permissionsManager.hasPermission(player, ALLOW_MULTIPLE_ACCOUNTS)) { List otherAccounts = database.getAllAuthsByIp(ip); if (otherAccounts.size() >= maxRegPerIp) { service.send(player, MessageKey.MAX_REGISTER_EXCEEDED, Integer.toString(maxRegPerIp), @@ -102,8 +107,7 @@ public class AsyncRegister implements AsynchronousProcess { private void emailRegister(Player player, String password, String email) { final String name = player.getName().toLowerCase(); final int maxRegPerEmail = service.getProperty(EmailSettings.MAX_REG_PER_EMAIL); - if (maxRegPerEmail > 0 - && !plugin.getPermissionsManager().hasPermission(player, PlayerStatePermission.ALLOW_MULTIPLE_ACCOUNTS)) { + if (maxRegPerEmail > 0 && !permissionsManager.hasPermission(player, ALLOW_MULTIPLE_ACCOUNTS)) { int otherAccounts = database.countAuthsByEmail(email); if (otherAccounts >= maxRegPerEmail) { service.send(player, MessageKey.MAX_REGISTER_EXCEEDED, Integer.toString(maxRegPerEmail), diff --git a/src/main/java/fr/xephi/authme/settings/SpawnLoader.java b/src/main/java/fr/xephi/authme/settings/SpawnLoader.java index 012eb053..f2a4cc5c 100644 --- a/src/main/java/fr/xephi/authme/settings/SpawnLoader.java +++ b/src/main/java/fr/xephi/authme/settings/SpawnLoader.java @@ -3,6 +3,7 @@ package fr.xephi.authme.settings; import fr.xephi.authme.AuthMe; import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.cache.auth.PlayerCache; +import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.hooks.PluginHooks; import fr.xephi.authme.initialization.DataFolder; import fr.xephi.authme.initialization.SettingsDependent; @@ -32,6 +33,7 @@ public class SpawnLoader implements SettingsDependent { private final File authMeConfigurationFile; private final PluginHooks pluginHooks; + private final DataSource dataSource; private FileConfiguration authMeConfiguration; private String[] spawnPriority; private Location essentialsSpawn; @@ -44,12 +46,14 @@ public class SpawnLoader implements SettingsDependent { * @param pluginHooks The plugin hooks instance */ @Inject - public SpawnLoader(@DataFolder File pluginFolder, NewSetting settings, PluginHooks pluginHooks) { + public SpawnLoader(@DataFolder File pluginFolder, NewSetting settings, PluginHooks pluginHooks, + DataSource dataSource) { File spawnFile = new File(pluginFolder, "spawn.yml"); // TODO ljacqu 20160312: Check if resource could be copied and handle the case if not FileUtils.copyFileFromResource(spawnFile, "spawn.yml"); this.authMeConfigurationFile = new File(pluginFolder, "spawn.yml"); this.pluginHooks = pluginHooks; + this.dataSource = dataSource; loadSettings(settings); } @@ -166,7 +170,7 @@ public class SpawnLoader implements SettingsDependent { if (PlayerCache.getInstance().isAuthenticated(playerNameLower)) { spawnLoc = getSpawn(); } else if (getFirstSpawn() != null && (!player.hasPlayedBefore() || - !plugin.getDataSource().isAuthAvailable(playerNameLower))) { + !dataSource.isAuthAvailable(playerNameLower))) { spawnLoc = getFirstSpawn(); } else { spawnLoc = getSpawn(); diff --git a/src/main/java/fr/xephi/authme/task/ChangePasswordTask.java b/src/main/java/fr/xephi/authme/task/ChangePasswordTask.java index c5ff5b95..1fbd8c14 100644 --- a/src/main/java/fr/xephi/authme/task/ChangePasswordTask.java +++ b/src/main/java/fr/xephi/authme/task/ChangePasswordTask.java @@ -19,19 +19,20 @@ public class ChangePasswordTask implements Runnable { private final Player player; private final String oldPassword; private final String newPassword; + private final PasswordSecurity passwordSecurity; - public ChangePasswordTask(AuthMe plugin, Player player, String oldPassword, String newPassword) { + public ChangePasswordTask(AuthMe plugin, Player player, String oldPassword, String newPassword, + PasswordSecurity passwordSecurity) { this.plugin = plugin; this.player = player; this.oldPassword = oldPassword; this.newPassword = newPassword; + this.passwordSecurity = passwordSecurity; } @Override public void run() { Messages m = plugin.getMessages(); - PasswordSecurity passwordSecurity = plugin.getPasswordSecurity(); - final String name = player.getName().toLowerCase(); PlayerAuth auth = PlayerCache.getInstance().getAuth(name); if (passwordSecurity.comparePassword(oldPassword, auth.getPassword(), player.getName())) { diff --git a/src/test/java/fr/xephi/authme/settings/SpawnLoaderTest.java b/src/test/java/fr/xephi/authme/settings/SpawnLoaderTest.java index 8bd9da4b..7bd0d40e 100644 --- a/src/test/java/fr/xephi/authme/settings/SpawnLoaderTest.java +++ b/src/test/java/fr/xephi/authme/settings/SpawnLoaderTest.java @@ -2,6 +2,7 @@ package fr.xephi.authme.settings; import com.google.common.io.Files; import fr.xephi.authme.TestHelper; +import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.hooks.PluginHooks; import fr.xephi.authme.settings.properties.RestrictionSettings; import org.bukkit.Location; @@ -11,6 +12,8 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.mockito.runners.MockitoJUnitRunner; import java.io.File; import java.io.IOException; @@ -23,6 +26,7 @@ import static org.mockito.Mockito.mock; /** * Test for {@link SpawnLoader}. */ +@RunWith(MockitoJUnitRunner.class) public class SpawnLoaderTest { @Rule @@ -48,7 +52,8 @@ public class SpawnLoaderTest { @Test public void shouldSetSpawn() { // given - SpawnLoader spawnLoader = new SpawnLoader(testFolder, settings, mock(PluginHooks.class)); + SpawnLoader spawnLoader = + new SpawnLoader(testFolder, settings, mock(PluginHooks.class), mock(DataSource.class)); World world = mock(World.class); given(world.getName()).willReturn("new_world"); Location newSpawn = new Location(world, 123, 45.0, -67.89); From 5c690d722aa5adf9bc644055574d86c987832238 Mon Sep 17 00:00:00 2001 From: DNx5 Date: Wed, 1 Jun 2016 05:23:40 +0700 Subject: [PATCH 131/200] - do the check for old GeoLite data correctly. --- .../java/fr/xephi/authme/util/GeoLiteAPI.java | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/main/java/fr/xephi/authme/util/GeoLiteAPI.java b/src/main/java/fr/xephi/authme/util/GeoLiteAPI.java index 83594086..49a57cf3 100644 --- a/src/main/java/fr/xephi/authme/util/GeoLiteAPI.java +++ b/src/main/java/fr/xephi/authme/util/GeoLiteAPI.java @@ -39,18 +39,21 @@ public class GeoLiteAPI { } final File pluginFolder = AuthMe.getInstance().getDataFolder(); final File data = new File(pluginFolder, "GeoIP.dat"); - boolean dataIsOld = (System.currentTimeMillis() - data.lastModified()) > TimeUnit.DAYS.toMillis(30); - if (dataIsOld && !data.delete()) { - ConsoleLogger.showError("Failed to delete GeoLiteAPI database"); - } if (data.exists()) { - try { - lookupService = new LookupService(data); - ConsoleLogger.info(LICENSE); - return true; - } catch (IOException e) { - ConsoleLogger.logException("Failed to load GeoLiteAPI database", e); - return false; + boolean dataIsOld = (System.currentTimeMillis() - data.lastModified()) > TimeUnit.DAYS.toMillis(30); + if (!dataIsOld) { + try { + lookupService = new LookupService(data); + ConsoleLogger.info(LICENSE); + return true; + } catch (IOException e) { + ConsoleLogger.logException("Failed to load GeoLiteAPI database", e); + return false; + } + } else { + if (!data.delete()) { + ConsoleLogger.showError("Failed to delete GeoLiteAPI database"); + } } } // Ok, let's try to download the data file! From 3d1f735c1bced7f822733d8a4c0468a0d2fee3ea Mon Sep 17 00:00:00 2001 From: DNx5 Date: Wed, 1 Jun 2016 06:12:22 +0700 Subject: [PATCH 132/200] Use FileWriter to write the messages. --- src/main/java/fr/xephi/authme/AuthMe.java | 1 + .../java/fr/xephi/authme/ConsoleLogger.java | 39 ++++++++++++++++--- .../executable/authme/ReloadCommand.java | 2 +- 3 files changed, 35 insertions(+), 7 deletions(-) diff --git a/src/main/java/fr/xephi/authme/AuthMe.java b/src/main/java/fr/xephi/authme/AuthMe.java index 05023a28..f8d2cc57 100644 --- a/src/main/java/fr/xephi/authme/AuthMe.java +++ b/src/main/java/fr/xephi/authme/AuthMe.java @@ -503,6 +503,7 @@ public class AuthMe extends JavaPlugin { // Disabled correctly ConsoleLogger.info("AuthMe " + this.getDescription().getVersion() + " disabled!"); + ConsoleLogger.close(); } // Stop/unload the server/plugin as defined in the configuration diff --git a/src/main/java/fr/xephi/authme/ConsoleLogger.java b/src/main/java/fr/xephi/authme/ConsoleLogger.java index 1fab749f..29b8c5f6 100644 --- a/src/main/java/fr/xephi/authme/ConsoleLogger.java +++ b/src/main/java/fr/xephi/authme/ConsoleLogger.java @@ -6,9 +6,8 @@ import fr.xephi.authme.settings.properties.SecuritySettings; import fr.xephi.authme.util.StringUtils; import java.io.File; +import java.io.FileWriter; import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.StandardOpenOption; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Date; @@ -25,6 +24,7 @@ public final class ConsoleLogger { private static boolean enableDebug = false; private static boolean useLogging = false; private static File logFile; + private static FileWriter fileWriter; private ConsoleLogger() { } @@ -40,6 +40,16 @@ public final class ConsoleLogger { public static void setLoggingOptions(NewSetting settings) { ConsoleLogger.useLogging = settings.getProperty(SecuritySettings.USE_LOGGING); ConsoleLogger.enableDebug = !settings.getProperty(SecuritySettings.REMOVE_SPAM_FROM_CONSOLE); + if (useLogging) { + if (fileWriter == null) { + try { + fileWriter = new FileWriter(logFile, true); + } catch (IOException ignored) { + } + } + } else { + close(); + } } /** @@ -49,6 +59,10 @@ public final class ConsoleLogger { */ public static void info(String message) { logger.info(message); + if (useLogging) { + writeLog(message); + } + } public static void debug(String message) { @@ -83,9 +97,11 @@ public final class ConsoleLogger { dateTime = DATE_FORMAT.format(new Date()); } try { - Files.write(logFile.toPath(), (dateTime + ": " + message + NEW_LINE).getBytes(), - StandardOpenOption.APPEND, - StandardOpenOption.CREATE); + fileWriter.write(dateTime); + fileWriter.write(": "); + fileWriter.write(message); + fileWriter.write(NEW_LINE); + fileWriter.flush(); } catch (IOException ignored) { } } @@ -105,10 +121,21 @@ public final class ConsoleLogger { * Logs a Throwable with the provided message and saves the stack trace to the log file. * * @param message The message to accompany the exception - * @param th The Throwable to log + * @param th The Throwable to log */ public static void logException(String message, Throwable th) { showError(message + " " + StringUtils.formatException(th)); writeStackTrace(th); } + + public static void close() { + if (fileWriter != null) { + try { + fileWriter.flush(); + fileWriter.close(); + fileWriter = null; + } catch (IOException ignored) { + } + } + } } diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/ReloadCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/ReloadCommand.java index 48a43f0a..1698e316 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/ReloadCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/ReloadCommand.java @@ -35,12 +35,12 @@ public class ReloadCommand implements ExecutableCommand { public void executeCommand(CommandSender sender, List arguments, CommandService commandService) { try { settings.reload(); + ConsoleLogger.setLoggingOptions(settings); // We do not change database type for consistency issues, but we'll output a note in the logs if (!settings.getProperty(DatabaseSettings.BACKEND).equals(dataSource.getType())) { ConsoleLogger.info("Note: cannot change database type during /authme reload"); sender.sendMessage("Note: cannot change database type during /authme reload"); } - ConsoleLogger.setLoggingOptions(settings); initializer.performReloadOnServices(); commandService.send(sender, MessageKey.CONFIG_RELOAD_SUCCESS); } catch (Exception e) { From 58a6b6060fe027e00bf424e1b00cbd02d90676bf Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Wed, 1 Jun 2016 11:40:50 +0200 Subject: [PATCH 133/200] Log an error if the plugin is unable to create the log file. --- src/main/java/fr/xephi/authme/ConsoleLogger.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/fr/xephi/authme/ConsoleLogger.java b/src/main/java/fr/xephi/authme/ConsoleLogger.java index 29b8c5f6..ff8a067e 100644 --- a/src/main/java/fr/xephi/authme/ConsoleLogger.java +++ b/src/main/java/fr/xephi/authme/ConsoleLogger.java @@ -44,7 +44,8 @@ public final class ConsoleLogger { if (fileWriter == null) { try { fileWriter = new FileWriter(logFile, true); - } catch (IOException ignored) { + } catch (IOException e) { + ConsoleLogger.showError("Failed to create the log file:" + e); } } } else { From e59bbbf10e65cb43178e4a0a48af3f9d01639583 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Wed, 1 Jun 2016 14:06:36 +0200 Subject: [PATCH 134/200] #738 Add check that shooter is of type Player in ProjectileLaunchEvent - Use reflection the opposite way: if it is an old version (getShooter() returns LivingEntity), use reflection; otherwise, call the method directly - Add missing instanceof Player check --- .../authme/listener/AuthMeEntityListener.java | 28 +++++++------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/src/main/java/fr/xephi/authme/listener/AuthMeEntityListener.java b/src/main/java/fr/xephi/authme/listener/AuthMeEntityListener.java index f2686f6a..61641679 100644 --- a/src/main/java/fr/xephi/authme/listener/AuthMeEntityListener.java +++ b/src/main/java/fr/xephi/authme/listener/AuthMeEntityListener.java @@ -1,5 +1,6 @@ package fr.xephi.authme.listener; +import fr.xephi.authme.ConsoleLogger; import org.bukkit.entity.LivingEntity; import org.bukkit.entity.Player; import org.bukkit.entity.Projectile; @@ -14,9 +15,6 @@ import org.bukkit.event.entity.EntityShootBowEvent; import org.bukkit.event.entity.EntityTargetEvent; import org.bukkit.event.entity.FoodLevelChangeEvent; import org.bukkit.event.entity.ProjectileLaunchEvent; -import org.bukkit.projectiles.ProjectileSource; - -import fr.xephi.authme.ConsoleLogger; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -26,12 +24,12 @@ import static fr.xephi.authme.listener.ListenerService.shouldCancelEvent; public class AuthMeEntityListener implements Listener { private Method getShooter; - private boolean shooterIsProjectileSource; + private boolean shooterIsLivingEntity; public AuthMeEntityListener() { try { getShooter = Projectile.class.getDeclaredMethod("getShooter"); - shooterIsProjectileSource = getShooter.getReturnType() != LivingEntity.class; + shooterIsLivingEntity = getShooter.getReturnType() == LivingEntity.class; } catch (NoSuchMethodException | SecurityException e) { ConsoleLogger.logException("Cannot load getShooter() method on Projectile class", e); } @@ -98,28 +96,22 @@ public class AuthMeEntityListener implements Listener { return; } - Player player = null; Projectile projectile = event.getEntity(); - // In old versions of the Bukkit API getShooter() returns a Player object instead of a ProjectileSource - if (shooterIsProjectileSource) { - ProjectileSource shooter = projectile.getShooter(); - if (shooter == null || !(shooter instanceof Player)) { - return; - } - player = (Player) shooter; - } else { + // In the Bukkit API prior to 1.7, getShooter() returns a LivingEntity instead of a ProjectileSource + Object shooterRaw = null; + if (shooterIsLivingEntity) { try { if (getShooter == null) { getShooter = Projectile.class.getMethod("getShooter"); } - Object obj = getShooter.invoke(projectile); - player = (Player) obj; + shooterRaw = getShooter.invoke(projectile); } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) { ConsoleLogger.logException("Error getting shooter", e); } + } else { + shooterRaw = projectile.getShooter(); } - - if (ListenerService.shouldCancelEvent(player)) { + if (shooterRaw instanceof Player && shouldCancelEvent((Player) shooterRaw)) { event.setCancelled(true); } } From 351fe3aa5a99f46c70b76093139fbba245f3aa38 Mon Sep 17 00:00:00 2001 From: games647 Date: Wed, 1 Jun 2016 18:03:54 +0200 Subject: [PATCH 135/200] Add thread name and stacktrace to the debug logging (Related to #419) It's only visible to user who enabled debug logging in their spigot configuration. --- src/main/java/fr/xephi/authme/ConsoleLogger.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/main/java/fr/xephi/authme/ConsoleLogger.java b/src/main/java/fr/xephi/authme/ConsoleLogger.java index ff8a067e..fc208742 100644 --- a/src/main/java/fr/xephi/authme/ConsoleLogger.java +++ b/src/main/java/fr/xephi/authme/ConsoleLogger.java @@ -11,6 +11,7 @@ import java.io.IOException; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Date; +import java.util.logging.Level; import java.util.logging.Logger; /** @@ -68,9 +69,15 @@ public final class ConsoleLogger { public static void debug(String message) { if (enableDebug) { - logger.fine(message); + if (logger.isLoggable(Level.FINE)) { + //creating and filling an exception is a expensive call + logger.log(Level.FINE, message + ' ' + Thread.currentThread().getName(), new Exception()); + } else { + logger.log(Level.FINE, "{0} {1}", new Object[]{message, Thread.currentThread().getName()}); + } + if (useLogging) { - writeLog("Debug: " + message); + writeLog("Debug: " + Thread.currentThread().getName() + ':' + message); } } } From 71a9abdad997844546404c755be48ccc80a752f8 Mon Sep 17 00:00:00 2001 From: games647 Date: Wed, 1 Jun 2016 18:28:22 +0200 Subject: [PATCH 136/200] Remove redundant isLoggable check. It's always level ALL Looks like the java logger does not reflect the log4j level settings :( --- src/main/java/fr/xephi/authme/ConsoleLogger.java | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/main/java/fr/xephi/authme/ConsoleLogger.java b/src/main/java/fr/xephi/authme/ConsoleLogger.java index fc208742..ef9561a9 100644 --- a/src/main/java/fr/xephi/authme/ConsoleLogger.java +++ b/src/main/java/fr/xephi/authme/ConsoleLogger.java @@ -69,12 +69,10 @@ public final class ConsoleLogger { public static void debug(String message) { if (enableDebug) { - if (logger.isLoggable(Level.FINE)) { - //creating and filling an exception is a expensive call - logger.log(Level.FINE, message + ' ' + Thread.currentThread().getName(), new Exception()); - } else { - logger.log(Level.FINE, "{0} {1}", new Object[]{message, Thread.currentThread().getName()}); - } + //creating and filling an exception is a expensive call + //->so it should be removed as soon #418 is fixed + //logger.isLoggable does not work because the plugin logger is always ALL + logger.log(Level.FINE, message + ' ' + Thread.currentThread().getName(), new Exception()); if (useLogging) { writeLog("Debug: " + Thread.currentThread().getName() + ':' + message); From 1cbd11a7531a59ccdc80ae3defc79665c5ebbc14 Mon Sep 17 00:00:00 2001 From: games647 Date: Wed, 1 Jun 2016 18:30:46 +0200 Subject: [PATCH 137/200] Fix the issue reference for removing the exception stacktrace logging --- src/main/java/fr/xephi/authme/ConsoleLogger.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/fr/xephi/authme/ConsoleLogger.java b/src/main/java/fr/xephi/authme/ConsoleLogger.java index ef9561a9..26a25704 100644 --- a/src/main/java/fr/xephi/authme/ConsoleLogger.java +++ b/src/main/java/fr/xephi/authme/ConsoleLogger.java @@ -70,7 +70,7 @@ public final class ConsoleLogger { public static void debug(String message) { if (enableDebug) { //creating and filling an exception is a expensive call - //->so it should be removed as soon #418 is fixed + //->so it should be removed as soon #419 is fixed //logger.isLoggable does not work because the plugin logger is always ALL logger.log(Level.FINE, message + ' ' + Thread.currentThread().getName(), new Exception()); From e99d9414b868882cbad36f6db8517213a81064f3 Mon Sep 17 00:00:00 2001 From: EbonJaguar Date: Wed, 1 Jun 2016 14:00:27 -0400 Subject: [PATCH 138/200] Add and implement handlers for each individual permissions plugin, instead of doing everything in the PermissionsManager - see #314 --- .../authme/permission/PermissionsManager.java | 339 ++---------------- .../authme/permission/PermissionsService.java | 39 -- .../handlers/BPermissionsHandler.java | 69 ++++ .../handlers/GroupManagerHandler.java | 79 ++++ .../handlers/PermissionHandler.java | 98 +++++ .../handlers/PermissionsBukkitHandler.java | 68 ++++ .../handlers/PermissionsExHandler.java | 89 +++++ .../permission/handlers/VaultHandler.java | 67 ++++ .../handlers/ZPermissionsHandler.java | 72 ++++ 9 files changed, 580 insertions(+), 340 deletions(-) delete mode 100644 src/main/java/fr/xephi/authme/permission/PermissionsService.java create mode 100644 src/main/java/fr/xephi/authme/permission/handlers/BPermissionsHandler.java create mode 100644 src/main/java/fr/xephi/authme/permission/handlers/GroupManagerHandler.java create mode 100644 src/main/java/fr/xephi/authme/permission/handlers/PermissionHandler.java create mode 100644 src/main/java/fr/xephi/authme/permission/handlers/PermissionsBukkitHandler.java create mode 100644 src/main/java/fr/xephi/authme/permission/handlers/PermissionsExHandler.java create mode 100644 src/main/java/fr/xephi/authme/permission/handlers/VaultHandler.java create mode 100644 src/main/java/fr/xephi/authme/permission/handlers/ZPermissionsHandler.java diff --git a/src/main/java/fr/xephi/authme/permission/PermissionsManager.java b/src/main/java/fr/xephi/authme/permission/PermissionsManager.java index 358bc555..d3718f5f 100644 --- a/src/main/java/fr/xephi/authme/permission/PermissionsManager.java +++ b/src/main/java/fr/xephi/authme/permission/PermissionsManager.java @@ -1,13 +1,16 @@ package fr.xephi.authme.permission; -import de.bananaco.bpermissions.api.ApiLayer; -import de.bananaco.bpermissions.api.CalculableType; import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.command.CommandDescription; -import fr.xephi.authme.util.CollectionUtils; +import fr.xephi.authme.permission.handlers.BPermissionsHandler; +import fr.xephi.authme.permission.handlers.GroupManagerHandler; +import fr.xephi.authme.permission.handlers.PermissionHandler; +import fr.xephi.authme.permission.handlers.PermissionsBukkitHandler; +import fr.xephi.authme.permission.handlers.PermissionsExHandler; +import fr.xephi.authme.permission.handlers.VaultHandler; +import fr.xephi.authme.permission.handlers.ZPermissionsHandler; import net.milkbowl.vault.permission.Permission; import org.anjocaido.groupmanager.GroupManager; -import org.anjocaido.groupmanager.permissions.AnjoPermissionsHandler; import org.bukkit.Bukkit; import org.bukkit.Server; import org.bukkit.command.CommandSender; @@ -18,15 +21,12 @@ import org.bukkit.plugin.Plugin; import org.bukkit.plugin.PluginManager; import org.bukkit.plugin.RegisteredServiceProvider; import org.tyrannyofheaven.bukkit.zPermissions.ZPermissionsService; -import ru.tehkode.permissions.PermissionUser; import ru.tehkode.permissions.bukkit.PermissionsEx; import javax.annotation.PostConstruct; import javax.inject.Inject; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; -import java.util.Map; /** *

@@ -40,12 +40,8 @@ import java.util.Map; * @author Tim Visée, http://timvisee.com * @version 0.3 */ -public class PermissionsManager implements PermissionsService { +public class PermissionsManager { - /** - * Vault instance. - */ - public Permission vaultPerms = null; /** * Server instance. */ @@ -57,13 +53,10 @@ public class PermissionsManager implements PermissionsService { */ private PermissionsSystemType permsType = null; /** - * Essentials group manager instance. + * The permission handler that is currently in use. + * Null if no permission system is hooked. */ - private GroupManager groupManagerPerms; - /** - * zPermissions service instance. - */ - private ZPermissionsService zPermissionsService; + private PermissionHandler handler = null; /** * Constructor. @@ -83,7 +76,7 @@ public class PermissionsManager implements PermissionsService { * @return False if there isn't any permissions system used. */ public boolean isEnabled() { - return permsType != null; + return handler != null; } /** @@ -132,21 +125,23 @@ public class PermissionsManager implements PermissionsService { continue; } + handler = new PermissionsExHandler(PermissionsEx.getPermissionManager()); break; case ESSENTIALS_GROUP_MANAGER: // Set the plugin instance - groupManagerPerms = (GroupManager) plugin; + handler = new GroupManagerHandler((GroupManager) plugin); break; case Z_PERMISSIONS: // Set the zPermissions service and make sure it's valid - zPermissionsService = Bukkit.getServicesManager().load(ZPermissionsService.class); + ZPermissionsService zPermissionsService = Bukkit.getServicesManager().load(ZPermissionsService.class); if (zPermissionsService == null) { ConsoleLogger.info("Failed to hook into " + type.getName() + "!"); continue; } + handler = new ZPermissionsHandler(zPermissionsService); break; case VAULT: @@ -158,12 +153,21 @@ public class PermissionsManager implements PermissionsService { } // Get the Vault provider and make sure it's valid - vaultPerms = permissionProvider.getProvider(); + Permission vaultPerms = permissionProvider.getProvider(); if (vaultPerms == null) { ConsoleLogger.info("Not using " + type.getName() + " because it's disabled!"); continue; } + handler = new VaultHandler(vaultPerms); + break; + + case B_PERMISSIONS: + handler = new BPermissionsHandler(); + break; + + case PERMISSIONS_BUKKIT: + handler = new PermissionsBukkitHandler(); break; default: @@ -272,7 +276,7 @@ public class PermissionsManager implements PermissionsService { } Player player = (Player) sender; - return hasPermission(player, permissionNode.getNode(), def); + return hasPermission(player, permissionNode, def); } public boolean hasPermission(Player player, Iterable nodes, boolean def) { @@ -292,7 +296,7 @@ public class PermissionsManager implements PermissionsService { DefaultPermission defaultPermission = command.getPermission().getDefaultPermission(); boolean def = evaluateDefaultPermission(defaultPermission, sender); return (sender instanceof Player) - ? hasPermission((Player) sender, command.getPermission().getNode(), def) + ? hasPermission((Player) sender, command.getPermission(), def) : def; } @@ -314,51 +318,17 @@ public class PermissionsManager implements PermissionsService { * Check if a player has permission. * * @param player The player. - * @param permsNode The permission node. + * @param node The permission node. * @param def Default returned if no permissions system is used. * * @return True if the player has permission. */ - private boolean hasPermission(Player player, String permsNode, boolean def) { + private boolean hasPermission(Player player, PermissionNode node, boolean def) { // If no permissions system is used, return the default value if (!isEnabled()) return def; - switch (this.permsType) { - case PERMISSIONS_EX: - // Permissions Ex - PermissionUser user = PermissionsEx.getUser(player); - return user.has(permsNode); - - case PERMISSIONS_BUKKIT: - // Permissions Bukkit - return player.hasPermission(permsNode); - - case B_PERMISSIONS: - // bPermissions - return ApiLayer.hasPermission(player.getWorld().getName(), CalculableType.USER, player.getName(), permsNode); - - case ESSENTIALS_GROUP_MANAGER: - // Essentials Group Manager - final AnjoPermissionsHandler handler = groupManagerPerms.getWorldsHolder().getWorldPermissions(player); - return handler != null && handler.has(player, permsNode); - - case Z_PERMISSIONS: - // zPermissions - Map perms = zPermissionsService.getPlayerPermissions(player.getWorld().getName(), null, player.getName()); - if (perms.containsKey(permsNode)) - return perms.get(permsNode); - else - return def; - - case VAULT: - // Vault - return vaultPerms.has(player, permsNode); - - default: - // Not hooked into any permissions system, return default - return def; - } + return handler.hasPermission(player, node, def); } /** @@ -372,22 +342,7 @@ public class PermissionsManager implements PermissionsService { if (!isEnabled()) return false; - switch (this.permsType) { - case PERMISSIONS_EX: - case PERMISSIONS_BUKKIT: - case B_PERMISSIONS: - case ESSENTIALS_GROUP_MANAGER: - case Z_PERMISSIONS: - return true; - - case VAULT: - // Vault - return vaultPerms.hasGroupSupport(); - - default: - // Not hooked into any permissions system, return false - return false; - } + return handler.hasGroupSupport(); } /** @@ -397,46 +352,12 @@ public class PermissionsManager implements PermissionsService { * * @return Permission groups, or an empty list if this feature is not supported. */ - @SuppressWarnings({"unchecked", "rawtypes", "deprecation"}) public List getGroups(Player player) { // If no permissions system is used, return an empty list if (!isEnabled()) return new ArrayList<>(); - switch (this.permsType) { - case PERMISSIONS_EX: - // Permissions Ex - PermissionUser user = PermissionsEx.getUser(player); - return user.getParentIdentifiers(null); - - case PERMISSIONS_BUKKIT: - // Permissions Bukkit - // FIXME: Add support for this! - return new ArrayList<>(); - - case B_PERMISSIONS: - // bPermissions - return Arrays.asList(ApiLayer.getGroups(player.getWorld().getName(), CalculableType.USER, player.getName())); - - case ESSENTIALS_GROUP_MANAGER: - // Essentials Group Manager - final AnjoPermissionsHandler handler = groupManagerPerms.getWorldsHolder().getWorldPermissions(player); - if (handler == null) - return new ArrayList<>(); - return Arrays.asList(handler.getGroups(player.getName())); - - case Z_PERMISSIONS: - //zPermissions - return new ArrayList(zPermissionsService.getPlayerGroups(player.getName())); - - case VAULT: - // Vault - return Arrays.asList(vaultPerms.getPlayerGroups(player)); - - default: - // Not hooked into any permissions system, return an empty list - return new ArrayList<>(); - } + return handler.getGroups(player); } /** @@ -451,39 +372,7 @@ public class PermissionsManager implements PermissionsService { if (!isEnabled()) return null; - switch (this.permsType) { - case PERMISSIONS_EX: - case PERMISSIONS_BUKKIT: - case B_PERMISSIONS: - // Get the groups of the player - List groups = getGroups(player); - - // Make sure there is any group available, or return null - if (groups.size() == 0) - return null; - - // Return the first group - return groups.get(0); - - case ESSENTIALS_GROUP_MANAGER: - // Essentials Group Manager - final AnjoPermissionsHandler handler = groupManagerPerms.getWorldsHolder().getWorldPermissions(player); - if (handler == null) - return null; - return handler.getGroup(player.getName()); - - case Z_PERMISSIONS: - //zPermissions - return zPermissionsService.getPlayerPrimaryGroup(player.getName()); - - case VAULT: - // Vault - return vaultPerms.getPrimaryGroup(player); - - default: - // Not hooked into any permissions system, return null - return null; - } + return handler.getPrimaryGroup(player); } /** @@ -500,40 +389,7 @@ public class PermissionsManager implements PermissionsService { if (!isEnabled()) return false; - switch (this.permsType) { - case PERMISSIONS_EX: - // Permissions Ex - PermissionUser user = PermissionsEx.getUser(player); - return user.inGroup(groupName); - - case PERMISSIONS_BUKKIT: - case Z_PERMISSIONS: - // Get the current list of groups - List groupNames = getGroups(player); - - // Check whether the list contains the group name, return the result - for (String entry : groupNames) - if (entry.equals(groupName)) - return true; - return false; - - case B_PERMISSIONS: - // bPermissions - return ApiLayer.hasGroup(player.getWorld().getName(), CalculableType.USER, player.getName(), groupName); - - case ESSENTIALS_GROUP_MANAGER: - // Essentials Group Manager - final AnjoPermissionsHandler handler = groupManagerPerms.getWorldsHolder().getWorldPermissions(player); - return handler != null && handler.inGroup(player.getName(), groupName); - - case VAULT: - // Vault - return vaultPerms.playerInGroup(player, groupName); - - default: - // Not hooked into any permissions system, return an empty list - return false; - } + return handler.isInGroup(player, groupName); } /** @@ -550,47 +406,7 @@ public class PermissionsManager implements PermissionsService { if (!isEnabled()) return false; - // Set the group the proper way - switch (this.permsType) { - case PERMISSIONS_EX: - // Permissions Ex - if(!PermissionsEx.getPermissionManager().getGroupNames().contains(groupName)) { - ConsoleLogger.showError("The plugin tried to set " + player + "'s group to " + groupName + ", but it doesn't exist!"); - return false; - } - PermissionUser user = PermissionsEx.getUser(player); - user.addGroup(groupName); - return true; - - case PERMISSIONS_BUKKIT: - // Permissions Bukkit - // Add the group to the user using a command - return Bukkit.dispatchCommand(Bukkit.getConsoleSender(), "permissions player addgroup " + player.getName() + " " + groupName); - - case B_PERMISSIONS: - // bPermissions - ApiLayer.addGroup(player.getWorld().getName(), CalculableType.USER, player.getName(), groupName); - return true; - - case ESSENTIALS_GROUP_MANAGER: - // Essentials Group Manager - // Add the group to the user using a command - return Bukkit.dispatchCommand(Bukkit.getConsoleSender(), "manuaddsub " + player.getName() + " " + groupName); - - case Z_PERMISSIONS: - // zPermissions - // Add the group to the user using a command - return Bukkit.dispatchCommand(Bukkit.getConsoleSender(), "permissions player " + player.getName() + " addgroup " + groupName); - - case VAULT: - // Vault - vaultPerms.playerAddGroup(player, groupName); - return true; - - default: - // Not hooked into any permissions system, return false - return false; - } + return handler.addToGroup(player, groupName); } /** @@ -631,43 +447,7 @@ public class PermissionsManager implements PermissionsService { if (!isEnabled()) return false; - // Set the group the proper way - switch (this.permsType) { - case PERMISSIONS_EX: - // Permissions Ex - PermissionUser user = PermissionsEx.getUser(player); - user.removeGroup(groupName); - return true; - - case PERMISSIONS_BUKKIT: - // Permissions Bukkit - // Remove the group to the user using a command - return Bukkit.dispatchCommand(Bukkit.getConsoleSender(), "permissions player removegroup " + player.getName() + " " + groupName); - - case B_PERMISSIONS: - // bPermissions - ApiLayer.removeGroup(player.getWorld().getName(), CalculableType.USER, player.getName(), groupName); - return true; - - case ESSENTIALS_GROUP_MANAGER: - // Essentials Group Manager - // Remove the group to the user using a command - return Bukkit.dispatchCommand(Bukkit.getConsoleSender(), "manudelsub " + player.getName() + " " + groupName); - - case Z_PERMISSIONS: - // zPermissions - // Remove the group to the user using a command - return Bukkit.dispatchCommand(Bukkit.getConsoleSender(), "permissions player " + player.getName() + " removegroup " + groupName); - - case VAULT: - // Vault - vaultPerms.playerRemoveGroup(player, groupName); - return true; - - default: - // Not hooked into any permissions system, return false - return false; - } + return handler.removeFromGroup(player, groupName); } /** @@ -709,50 +489,7 @@ public class PermissionsManager implements PermissionsService { if (!isEnabled()) return false; - // Create a list of group names - List groupNames = new ArrayList<>(); - groupNames.add(groupName); - - // Set the group the proper way - switch (this.permsType) { - case PERMISSIONS_EX: - // Permissions Ex - PermissionUser user = PermissionsEx.getUser(player); - user.setParentsIdentifier(groupNames); - return true; - - case PERMISSIONS_BUKKIT: - // Permissions Bukkit - // Set the user's group using a command - return Bukkit.dispatchCommand(Bukkit.getConsoleSender(), "permissions player setgroup " + player.getName() + " " + groupName); - - case B_PERMISSIONS: - // bPermissions - ApiLayer.setGroup(player.getWorld().getName(), CalculableType.USER, player.getName(), groupName); - return true; - - case ESSENTIALS_GROUP_MANAGER: - // Essentials Group Manager - // Clear the list of groups, add the player to the specified group afterwards using a command - removeAllGroups(player); - return Bukkit.dispatchCommand(Bukkit.getConsoleSender(), "manuadd " + player.getName() + " " + groupName); - - case Z_PERMISSIONS: - //zPermissions - // Set the players group through the plugin commands - return Bukkit.dispatchCommand(Bukkit.getConsoleSender(), "permissions player " + player.getName() + " setgroup " + groupName); - - case VAULT: - // Vault - // Remove all current groups, add the player to the specified group afterwards - removeAllGroups(player); - vaultPerms.playerAddGroup(player, groupName); - return true; - - default: - // Not hooked into any permissions system, return false - return false; - } + return handler.setGroup(player, groupName); } /** diff --git a/src/main/java/fr/xephi/authme/permission/PermissionsService.java b/src/main/java/fr/xephi/authme/permission/PermissionsService.java deleted file mode 100644 index 9de4f68e..00000000 --- a/src/main/java/fr/xephi/authme/permission/PermissionsService.java +++ /dev/null @@ -1,39 +0,0 @@ -package fr.xephi.authme.permission; - -import fr.xephi.authme.command.CommandDescription; -import org.bukkit.command.CommandSender; - -/** - * Interface for dealing with permissions. - */ -public interface PermissionsService { - - /** - * Check if the player has the given permission. - * - * @param sender The command sender - * @param permission The permission node to check - * @param def Default returned if no permissions system is used - * - * @return True if the player has permission - */ - boolean hasPermission(CommandSender sender, PermissionNode permission, boolean def); - - /** - * Check if the player has the permissions for the given command. - * - * @param sender The command sender - * @param command The command whose permissions should be checked - * - * @return True if the player may execute the command - */ - boolean hasPermission(CommandSender sender, CommandDescription command); - - /** - * Return the permission system the service is working with. - * - * @return The permission system AuthMe is hooked into - */ - PermissionsSystemType getSystem(); - -} diff --git a/src/main/java/fr/xephi/authme/permission/handlers/BPermissionsHandler.java b/src/main/java/fr/xephi/authme/permission/handlers/BPermissionsHandler.java new file mode 100644 index 00000000..62291f2a --- /dev/null +++ b/src/main/java/fr/xephi/authme/permission/handlers/BPermissionsHandler.java @@ -0,0 +1,69 @@ +package fr.xephi.authme.permission.handlers; + +import de.bananaco.bpermissions.api.ApiLayer; +import de.bananaco.bpermissions.api.CalculableType; +import fr.xephi.authme.permission.PermissionNode; +import fr.xephi.authme.permission.PermissionsSystemType; +import org.bukkit.entity.Player; + +import java.util.Arrays; +import java.util.List; + +public class BPermissionsHandler implements PermissionHandler { + + @Override + public boolean addToGroup(Player player, String group) { + ApiLayer.addGroup(player.getWorld().getName(), CalculableType.USER, player.getName(), group); + return true; + } + + @Override + public boolean hasGroupSupport() { + return true; + } + + @Override + public boolean hasPermission(Player player, PermissionNode node, boolean def) { + return ApiLayer.hasPermission(player.getWorld().getName(), CalculableType.USER, player.getName(), node.getNode()); + } + + @Override + public boolean isInGroup(Player player, String group) { + return ApiLayer.hasGroup(player.getWorld().getName(), CalculableType.USER, player.getName(), group); + } + + @Override + public boolean removeFromGroup(Player player, String group) { + ApiLayer.removeGroup(player.getWorld().getName(), CalculableType.USER, player.getName(), group); + return true; + } + + @Override + public boolean setGroup(Player player, String group) { + ApiLayer.setGroup(player.getWorld().getName(), CalculableType.USER, player.getName(), group); + return true; + } + + @Override + public List getGroups(Player player) { + return Arrays.asList(ApiLayer.getGroups(player.getWorld().getName(), CalculableType.USER, player.getName())); + } + + @Override + public String getPrimaryGroup(Player player) { + // Get the groups of the player + List groups = getGroups(player); + + // Make sure there is any group available, or return null + if (groups.size() == 0) + return null; + + // Return the first group + return groups.get(0); + } + + @Override + public PermissionsSystemType getPermissionSystem() { + return PermissionsSystemType.B_PERMISSIONS; + } +} diff --git a/src/main/java/fr/xephi/authme/permission/handlers/GroupManagerHandler.java b/src/main/java/fr/xephi/authme/permission/handlers/GroupManagerHandler.java new file mode 100644 index 00000000..8d2b0b8c --- /dev/null +++ b/src/main/java/fr/xephi/authme/permission/handlers/GroupManagerHandler.java @@ -0,0 +1,79 @@ +package fr.xephi.authme.permission.handlers; + +import fr.xephi.authme.permission.PermissionNode; +import fr.xephi.authme.permission.PermissionsSystemType; +import org.anjocaido.groupmanager.GroupManager; +import org.anjocaido.groupmanager.permissions.AnjoPermissionsHandler; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class GroupManagerHandler implements PermissionHandler { + + private GroupManager groupManager; + + public GroupManagerHandler(GroupManager groupManager) { + this.groupManager = groupManager; + } + + @Override + public boolean addToGroup(Player player, String group) { + return Bukkit.dispatchCommand(Bukkit.getConsoleSender(), "manuaddsub " + player.getName() + " " + group); + } + + @Override + public boolean hasGroupSupport() { + return true; + } + + @Override + public boolean hasPermission(Player player, PermissionNode node, boolean def) { + final AnjoPermissionsHandler handler = groupManager.getWorldsHolder().getWorldPermissions(player); + return handler != null && handler.has(player, node.getNode()); + } + + @Override + public boolean isInGroup(Player player, String group) { + final AnjoPermissionsHandler handler = groupManager.getWorldsHolder().getWorldPermissions(player); + return handler != null && handler.inGroup(player.getName(), group); + } + + @Override + public boolean removeFromGroup(Player player, String group) { + return Bukkit.dispatchCommand(Bukkit.getConsoleSender(), "manudelsub " + player.getName() + " " + group); + } + + @Override + public boolean setGroup(Player player, String group) { + final AnjoPermissionsHandler handler = groupManager.getWorldsHolder().getWorldPermissions(player); + for (String groupName : handler.getGroups(player.getName())) { + removeFromGroup(player, groupName); + } + + return Bukkit.dispatchCommand(Bukkit.getConsoleSender(), "manuadd " + player.getName() + " " + group); + } + + @Override + public List getGroups(Player player) { + final AnjoPermissionsHandler handler = groupManager.getWorldsHolder().getWorldPermissions(player); + if (handler == null) + return new ArrayList<>(); + return Arrays.asList(handler.getGroups(player.getName())); + } + + @Override + public String getPrimaryGroup(Player player) { + final AnjoPermissionsHandler handler = groupManager.getWorldsHolder().getWorldPermissions(player); + if (handler == null) + return null; + return handler.getGroup(player.getName()); + } + + @Override + public PermissionsSystemType getPermissionSystem() { + return PermissionsSystemType.ESSENTIALS_GROUP_MANAGER; + } +} diff --git a/src/main/java/fr/xephi/authme/permission/handlers/PermissionHandler.java b/src/main/java/fr/xephi/authme/permission/handlers/PermissionHandler.java new file mode 100644 index 00000000..5f1b4c10 --- /dev/null +++ b/src/main/java/fr/xephi/authme/permission/handlers/PermissionHandler.java @@ -0,0 +1,98 @@ +package fr.xephi.authme.permission.handlers; + +import fr.xephi.authme.permission.PermissionNode; +import fr.xephi.authme.permission.PermissionsSystemType; +import org.bukkit.entity.Player; + +import java.util.List; +public interface PermissionHandler { + + /** + * Add the permission group of a player, if supported. + * + * @param player The player + * @param group The name of the group. + * + * @return True if succeed, false otherwise. + * False is also returned if this feature isn't supported for the current permissions system. + */ + boolean addToGroup(Player player, String group); + + /** + * Check whether the current permissions system has group support. + * If no permissions system is hooked, false will be returned. + * + * @return True if the current permissions system supports groups, false otherwise. + */ + boolean hasGroupSupport(); + + /** + * Check if a player has permission. + * + * @param player The player. + * @param node The permission node. + * @param def Default returned if no permissions system is used. + * + * @return True if the player has permission. + */ + boolean hasPermission(Player player, PermissionNode node, boolean def); + + /** + * Check whether the player is in the specified group. + * + * @param player The player. + * @param group The group name. + * + * @return True if the player is in the specified group, false otherwise. + * False is also returned if groups aren't supported by the used permissions system. + */ + boolean isInGroup(Player player, String group); + + /** + * Remove the permission group of a player, if supported. + * + * @param player The player + * @param group The name of the group. + * + * @return True if succeed, false otherwise. + * False is also returned if this feature isn't supported for the current permissions system. + */ + boolean removeFromGroup(Player player, String group); + + /** + * Set the permission group of a player, if supported. + * This clears the current groups of the player. + * + * @param player The player + * @param group The name of the group. + * + * @return True if succeed, false otherwise. + * False is also returned if this feature isn't supported for the current permissions system. + */ + boolean setGroup(Player player, String group); + + /** + * Get the permission groups of a player, if available. + * + * @param player The player. + * + * @return Permission groups, or an empty list if this feature is not supported. + */ + List getGroups(Player player); + + /** + * Get the primary group of a player, if available. + * + * @param player The player. + * + * @return The name of the primary permission group. Or null. + */ + String getPrimaryGroup(Player player); + + /** + * Get the permission system that is being used. + * + * @return The permission system. + */ + PermissionsSystemType getPermissionSystem(); +} diff --git a/src/main/java/fr/xephi/authme/permission/handlers/PermissionsBukkitHandler.java b/src/main/java/fr/xephi/authme/permission/handlers/PermissionsBukkitHandler.java new file mode 100644 index 00000000..e28ded0f --- /dev/null +++ b/src/main/java/fr/xephi/authme/permission/handlers/PermissionsBukkitHandler.java @@ -0,0 +1,68 @@ +package fr.xephi.authme.permission.handlers; + +import fr.xephi.authme.permission.PermissionNode; +import fr.xephi.authme.permission.PermissionsSystemType; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.util.ArrayList; +import java.util.List; + +public class PermissionsBukkitHandler implements PermissionHandler { + + @Override + public boolean addToGroup(Player player, String group) { + return Bukkit.dispatchCommand(Bukkit.getConsoleSender(), "permissions player addgroup " + player.getName() + " " + group); + } + + @Override + public boolean hasGroupSupport() { + return true; + } + + @Override + public boolean hasPermission(Player player, PermissionNode node, boolean def) { + return player.hasPermission(node.getNode()); + } + + @Override + public boolean isInGroup(Player player, String group) { + List groupNames = getGroups(player); + + return groupNames.contains(group); + } + + @Override + public boolean removeFromGroup(Player player, String group) { + return Bukkit.dispatchCommand(Bukkit.getConsoleSender(), "permissions player removegroup " + player.getName() + " " + group); + } + + @Override + public boolean setGroup(Player player, String group) { + return Bukkit.dispatchCommand(Bukkit.getConsoleSender(), "permissions player setgroup " + player.getName() + " " + group); + } + + @Override + public List getGroups(Player player) { + // FIXME Gnat008 20160601: Add support for this + return new ArrayList<>(); + } + + @Override + public String getPrimaryGroup(Player player) { + // Get the groups of the player + List groups = getGroups(player); + + // Make sure there is any group available, or return null + if (groups.size() == 0) + return null; + + // Return the first group + return groups.get(0); + } + + @Override + public PermissionsSystemType getPermissionSystem() { + return PermissionsSystemType.PERMISSIONS_BUKKIT; + } +} diff --git a/src/main/java/fr/xephi/authme/permission/handlers/PermissionsExHandler.java b/src/main/java/fr/xephi/authme/permission/handlers/PermissionsExHandler.java new file mode 100644 index 00000000..3d03b65b --- /dev/null +++ b/src/main/java/fr/xephi/authme/permission/handlers/PermissionsExHandler.java @@ -0,0 +1,89 @@ +package fr.xephi.authme.permission.handlers; + +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.permission.PermissionNode; +import fr.xephi.authme.permission.PermissionsSystemType; +import org.bukkit.entity.Player; +import ru.tehkode.permissions.PermissionManager; +import ru.tehkode.permissions.PermissionUser; +import ru.tehkode.permissions.bukkit.PermissionsEx; + +import java.util.ArrayList; +import java.util.List; + +public class PermissionsExHandler implements PermissionHandler { + + private PermissionManager permissionManager; + + public PermissionsExHandler(PermissionManager permissionManager) { + this.permissionManager = permissionManager; + } + + @Override + public boolean addToGroup(Player player, String group) { + if(!PermissionsEx.getPermissionManager().getGroupNames().contains(group)) { + ConsoleLogger.showError("The plugin tried to set " + player + "'s group to '" + group + "', but it doesn't exist!"); + return false; + } + + PermissionUser user = PermissionsEx.getUser(player); + user.addGroup(group); + return true; + } + + @Override + public boolean hasGroupSupport() { + return true; + } + + @Override + public boolean hasPermission(Player player, PermissionNode node, boolean def) { + PermissionUser user = permissionManager.getUser(player); + return user.has(node.getNode()); + } + + @Override + public boolean isInGroup(Player player, String group) { + PermissionUser user = permissionManager.getUser(player); + return user.inGroup(group); + } + + @Override + public boolean removeFromGroup(Player player, String group) { + PermissionUser user = permissionManager.getUser(player); + user.removeGroup(group); + return true; + } + + @Override + public boolean setGroup(Player player, String group) { + List groups = new ArrayList<>(); + groups.add(group); + + PermissionUser user = permissionManager.getUser(player); + user.setParentsIdentifier(groups); + return true; + } + + @Override + public List getGroups(Player player) { + PermissionUser user = permissionManager.getUser(player); + return user.getParentIdentifiers(null); + } + + @Override + public String getPrimaryGroup(Player player) { + PermissionUser user = permissionManager.getUser(player); + + List groups = user.getParentIdentifiers(null); + if (groups.size() == 0) + return null; + + return groups.get(0); + } + + @Override + public PermissionsSystemType getPermissionSystem() { + return PermissionsSystemType.PERMISSIONS_EX; + } +} diff --git a/src/main/java/fr/xephi/authme/permission/handlers/VaultHandler.java b/src/main/java/fr/xephi/authme/permission/handlers/VaultHandler.java new file mode 100644 index 00000000..4ad03839 --- /dev/null +++ b/src/main/java/fr/xephi/authme/permission/handlers/VaultHandler.java @@ -0,0 +1,67 @@ +package fr.xephi.authme.permission.handlers; + +import fr.xephi.authme.permission.PermissionNode; +import fr.xephi.authme.permission.PermissionsSystemType; +import net.milkbowl.vault.permission.Permission; +import org.bukkit.entity.Player; + +import java.util.Arrays; +import java.util.List; + +public class VaultHandler implements PermissionHandler { + + private Permission vaultProvider; + + public VaultHandler(Permission vaultProvider) { + this.vaultProvider = vaultProvider; + } + + @Override + public boolean addToGroup(Player player, String group) { + return vaultProvider.playerAddGroup(player, group); + } + + @Override + public boolean hasGroupSupport() { + return vaultProvider.hasGroupSupport(); + } + + @Override + public boolean hasPermission(Player player, PermissionNode node, boolean def) { + return vaultProvider.has(player, node.getNode()); + } + + @Override + public boolean isInGroup(Player player, String group) { + return vaultProvider.playerInGroup(player, group); + } + + @Override + public boolean removeFromGroup(Player player, String group) { + return vaultProvider.playerRemoveGroup(player, group); + } + + @Override + public boolean setGroup(Player player, String group) { + for (String groupName : getGroups(player)) { + removeFromGroup(player, groupName); + } + + return vaultProvider.playerAddGroup(player, group); + } + + @Override + public List getGroups(Player player) { + return Arrays.asList(vaultProvider.getPlayerGroups(player)); + } + + @Override + public String getPrimaryGroup(Player player) { + return vaultProvider.getPrimaryGroup(player); + } + + @Override + public PermissionsSystemType getPermissionSystem() { + return PermissionsSystemType.VAULT; + } +} diff --git a/src/main/java/fr/xephi/authme/permission/handlers/ZPermissionsHandler.java b/src/main/java/fr/xephi/authme/permission/handlers/ZPermissionsHandler.java new file mode 100644 index 00000000..3afdbce2 --- /dev/null +++ b/src/main/java/fr/xephi/authme/permission/handlers/ZPermissionsHandler.java @@ -0,0 +1,72 @@ +package fr.xephi.authme.permission.handlers; + +import fr.xephi.authme.permission.PermissionNode; +import fr.xephi.authme.permission.PermissionsSystemType; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.tyrannyofheaven.bukkit.zPermissions.ZPermissionsService; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class ZPermissionsHandler implements PermissionHandler { + + private ZPermissionsService zPermissionsService; + + public ZPermissionsHandler(ZPermissionsService zPermissionsService) { + this.zPermissionsService = zPermissionsService; + } + + @Override + public boolean addToGroup(Player player, String group) { + return Bukkit.dispatchCommand(Bukkit.getConsoleSender(), "permissions player " + player.getName() + " addgroup " + group); + } + + @Override + public boolean hasGroupSupport() { + return true; + } + + @Override + public boolean hasPermission(Player player, PermissionNode node, boolean def) { + Map perms = zPermissionsService.getPlayerPermissions(player.getWorld().getName(), null, player.getName()); + if (perms.containsKey(node.getNode())) + return perms.get(node.getNode()); + else + return def; + } + + @Override + public boolean isInGroup(Player player, String group) { + return getGroups(player).contains(group); + } + + @Override + public boolean removeFromGroup(Player player, String group) { + return Bukkit.dispatchCommand(Bukkit.getConsoleSender(), "permissions player " + player.getName() + " removegroup " + group); + } + + @Override + public boolean setGroup(Player player, String group) { + return Bukkit.dispatchCommand(Bukkit.getConsoleSender(), "permissions player " + player.getName() + " setgroup " + group); + } + + @Override + @SuppressWarnings("unchecked") + public List getGroups(Player player) { + // TODO Gnat008 20160631: Use UUID not name? + return new ArrayList(zPermissionsService.getPlayerGroups(player.getName())); + } + + @Override + public String getPrimaryGroup(Player player) { + // TODO Gnat008 20160631: Use UUID not name? + return zPermissionsService.getPlayerPrimaryGroup(player.getName()); + } + + @Override + public PermissionsSystemType getPermissionSystem() { + return PermissionsSystemType.Z_PERMISSIONS; + } +} From fdb9227ec12a78888e2ccb13f290cf7baf08280c Mon Sep 17 00:00:00 2001 From: EbonJaguar Date: Wed, 1 Jun 2016 14:09:30 -0400 Subject: [PATCH 139/200] Properly unhook the current system when unhook() is called --- src/main/java/fr/xephi/authme/permission/PermissionsManager.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/fr/xephi/authme/permission/PermissionsManager.java b/src/main/java/fr/xephi/authme/permission/PermissionsManager.java index d3718f5f..a6566f1a 100644 --- a/src/main/java/fr/xephi/authme/permission/PermissionsManager.java +++ b/src/main/java/fr/xephi/authme/permission/PermissionsManager.java @@ -198,6 +198,7 @@ public class PermissionsManager { public void unhook() { // Reset the current used permissions system this.permsType = null; + this.handler = null; // Print a status message to the console ConsoleLogger.info("Unhooked from Permissions!"); From 95343e366b973d2a714bd1a038a133042f5da67b Mon Sep 17 00:00:00 2001 From: ljacqu Date: Wed, 1 Jun 2016 21:56:57 +0200 Subject: [PATCH 140/200] #314 Evaluate default permission on enum, remove default from handler interface - Evaluate permission for DefaultPermission on the enum itself - Remove boolean default from PermissionHandler for hasPermission() - Remove some unused / intermediary hasPermission() flavors in PermissionsManager --- .../authme/command/help/HelpProvider.java | 4 +- .../authme/permission/DefaultPermission.java | 45 ++++++++++++++----- .../authme/permission/PermissionsManager.java | 31 ++----------- .../handlers/BPermissionsHandler.java | 2 +- .../handlers/GroupManagerHandler.java | 2 +- .../handlers/PermissionHandler.java | 3 +- .../handlers/PermissionsBukkitHandler.java | 2 +- .../handlers/PermissionsExHandler.java | 4 +- .../permission/handlers/VaultHandler.java | 2 +- .../handlers/ZPermissionsHandler.java | 4 +- 10 files changed, 50 insertions(+), 49 deletions(-) diff --git a/src/main/java/fr/xephi/authme/command/help/HelpProvider.java b/src/main/java/fr/xephi/authme/command/help/HelpProvider.java index 1d8b6bdb..cadfd641 100644 --- a/src/main/java/fr/xephi/authme/command/help/HelpProvider.java +++ b/src/main/java/fr/xephi/authme/command/help/HelpProvider.java @@ -48,7 +48,7 @@ public class HelpProvider implements SettingsDependent { private String helpHeader; @Inject - public HelpProvider(PermissionsManager permissionsManager, NewSetting settings) { + HelpProvider(PermissionsManager permissionsManager, NewSetting settings) { this.permissionsManager = permissionsManager; loadSettings(settings); } @@ -152,7 +152,7 @@ public class HelpProvider implements SettingsDependent { final DefaultPermission defaultPermission = permission.getDefaultPermission(); String addendum = ""; if (DefaultPermission.OP_ONLY.equals(defaultPermission)) { - addendum = PermissionsManager.evaluateDefaultPermission(defaultPermission, sender) + addendum = defaultPermission.evaluate(sender) ? " (You have permission)" : " (No permission)"; } diff --git a/src/main/java/fr/xephi/authme/permission/DefaultPermission.java b/src/main/java/fr/xephi/authme/permission/DefaultPermission.java index 9ad8aff1..3acbfa8c 100644 --- a/src/main/java/fr/xephi/authme/permission/DefaultPermission.java +++ b/src/main/java/fr/xephi/authme/permission/DefaultPermission.java @@ -1,18 +1,35 @@ package fr.xephi.authme.permission; +import org.bukkit.command.CommandSender; + /** - * The default permission for a command if there is no support for permission nodes. + * The default permission to fall back to if there is no support for permission nodes. */ public enum DefaultPermission { - /** No one can execute the command. */ - NOT_ALLOWED("No permission"), + /** No one has permission. */ + NOT_ALLOWED("No permission") { + @Override + public boolean evaluate(CommandSender sender) { + return false; + } + }, - /** Only players with the OP status may execute the command. */ - OP_ONLY("OP's only"), + /** Only players with OP status have permission. */ + OP_ONLY("OP's only") { + @Override + public boolean evaluate(CommandSender sender) { + return sender.isOp(); + } + }, - /** The command can be executed by anyone. */ - ALLOWED("Everyone allowed"); + /** Everyone is granted permission. */ + ALLOWED("Everyone allowed") { + @Override + public boolean evaluate(CommandSender sender) { + return true; + } + }; /** Textual representation of the default permission. */ private final String title; @@ -26,9 +43,17 @@ public enum DefaultPermission { } /** - * Return the textual representation. - * - * @return The textual representation + * Evaluates whether permission is granted to the sender or not. + * + * @param sender the sender to process + * @return true if the sender has permission, false otherwise + */ + public abstract boolean evaluate(CommandSender sender); + + /** + * Return the textual representation. + * + * @return the textual representation */ public String getTitle() { return title; diff --git a/src/main/java/fr/xephi/authme/permission/PermissionsManager.java b/src/main/java/fr/xephi/authme/permission/PermissionsManager.java index a6566f1a..072e82c5 100644 --- a/src/main/java/fr/xephi/authme/permission/PermissionsManager.java +++ b/src/main/java/fr/xephi/authme/permission/PermissionsManager.java @@ -224,7 +224,7 @@ public class PermissionsManager { * @param event Event instance. */ public void onPluginEnable(PluginEnableEvent event) { - // Get the plugin and it's name + // Get the plugin and its name Plugin plugin = event.getPlugin(); String pluginName = plugin.getName(); @@ -280,41 +280,18 @@ public class PermissionsManager { return hasPermission(player, permissionNode, def); } - public boolean hasPermission(Player player, Iterable nodes, boolean def) { - for (PermissionNode node : nodes) { - if (!hasPermission(player, node, def)) { - return false; - } - } - return true; - } - public boolean hasPermission(CommandSender sender, CommandDescription command) { if (command.getPermission() == null) { return true; } DefaultPermission defaultPermission = command.getPermission().getDefaultPermission(); - boolean def = evaluateDefaultPermission(defaultPermission, sender); + boolean def = defaultPermission.evaluate(sender); return (sender instanceof Player) ? hasPermission((Player) sender, command.getPermission(), def) : def; } - public static boolean evaluateDefaultPermission(DefaultPermission defaultPermission, CommandSender sender) { - switch (defaultPermission) { - case ALLOWED: - return true; - - case OP_ONLY: - return sender.isOp(); - - case NOT_ALLOWED: - default: - return false; - } - } - /** * Check if a player has permission. * @@ -329,7 +306,7 @@ public class PermissionsManager { if (!isEnabled()) return def; - return handler.hasPermission(player, node, def); + return handler.hasPermission(player, node); } /** @@ -530,7 +507,7 @@ public class PermissionsManager { /** * Remove all groups of the specified player, if supported. * Systems like Essentials GroupManager don't allow all groups to be removed from a player, thus the user will stay - * in it's primary group. All the subgroups are removed just fine. + * in its primary group. All the subgroups are removed just fine. * * @param player The player to remove all groups from. * diff --git a/src/main/java/fr/xephi/authme/permission/handlers/BPermissionsHandler.java b/src/main/java/fr/xephi/authme/permission/handlers/BPermissionsHandler.java index 62291f2a..45592323 100644 --- a/src/main/java/fr/xephi/authme/permission/handlers/BPermissionsHandler.java +++ b/src/main/java/fr/xephi/authme/permission/handlers/BPermissionsHandler.java @@ -23,7 +23,7 @@ public class BPermissionsHandler implements PermissionHandler { } @Override - public boolean hasPermission(Player player, PermissionNode node, boolean def) { + public boolean hasPermission(Player player, PermissionNode node) { return ApiLayer.hasPermission(player.getWorld().getName(), CalculableType.USER, player.getName(), node.getNode()); } diff --git a/src/main/java/fr/xephi/authme/permission/handlers/GroupManagerHandler.java b/src/main/java/fr/xephi/authme/permission/handlers/GroupManagerHandler.java index 8d2b0b8c..fe0b0236 100644 --- a/src/main/java/fr/xephi/authme/permission/handlers/GroupManagerHandler.java +++ b/src/main/java/fr/xephi/authme/permission/handlers/GroupManagerHandler.java @@ -30,7 +30,7 @@ public class GroupManagerHandler implements PermissionHandler { } @Override - public boolean hasPermission(Player player, PermissionNode node, boolean def) { + public boolean hasPermission(Player player, PermissionNode node) { final AnjoPermissionsHandler handler = groupManager.getWorldsHolder().getWorldPermissions(player); return handler != null && handler.has(player, node.getNode()); } diff --git a/src/main/java/fr/xephi/authme/permission/handlers/PermissionHandler.java b/src/main/java/fr/xephi/authme/permission/handlers/PermissionHandler.java index 5f1b4c10..f222dac2 100644 --- a/src/main/java/fr/xephi/authme/permission/handlers/PermissionHandler.java +++ b/src/main/java/fr/xephi/authme/permission/handlers/PermissionHandler.java @@ -31,11 +31,10 @@ public interface PermissionHandler { * * @param player The player. * @param node The permission node. - * @param def Default returned if no permissions system is used. * * @return True if the player has permission. */ - boolean hasPermission(Player player, PermissionNode node, boolean def); + boolean hasPermission(Player player, PermissionNode node); /** * Check whether the player is in the specified group. diff --git a/src/main/java/fr/xephi/authme/permission/handlers/PermissionsBukkitHandler.java b/src/main/java/fr/xephi/authme/permission/handlers/PermissionsBukkitHandler.java index e28ded0f..fb1edcae 100644 --- a/src/main/java/fr/xephi/authme/permission/handlers/PermissionsBukkitHandler.java +++ b/src/main/java/fr/xephi/authme/permission/handlers/PermissionsBukkitHandler.java @@ -21,7 +21,7 @@ public class PermissionsBukkitHandler implements PermissionHandler { } @Override - public boolean hasPermission(Player player, PermissionNode node, boolean def) { + public boolean hasPermission(Player player, PermissionNode node) { return player.hasPermission(node.getNode()); } diff --git a/src/main/java/fr/xephi/authme/permission/handlers/PermissionsExHandler.java b/src/main/java/fr/xephi/authme/permission/handlers/PermissionsExHandler.java index 3d03b65b..b49e4b37 100644 --- a/src/main/java/fr/xephi/authme/permission/handlers/PermissionsExHandler.java +++ b/src/main/java/fr/xephi/authme/permission/handlers/PermissionsExHandler.java @@ -21,7 +21,7 @@ public class PermissionsExHandler implements PermissionHandler { @Override public boolean addToGroup(Player player, String group) { - if(!PermissionsEx.getPermissionManager().getGroupNames().contains(group)) { + if (!PermissionsEx.getPermissionManager().getGroupNames().contains(group)) { ConsoleLogger.showError("The plugin tried to set " + player + "'s group to '" + group + "', but it doesn't exist!"); return false; } @@ -37,7 +37,7 @@ public class PermissionsExHandler implements PermissionHandler { } @Override - public boolean hasPermission(Player player, PermissionNode node, boolean def) { + public boolean hasPermission(Player player, PermissionNode node) { PermissionUser user = permissionManager.getUser(player); return user.has(node.getNode()); } diff --git a/src/main/java/fr/xephi/authme/permission/handlers/VaultHandler.java b/src/main/java/fr/xephi/authme/permission/handlers/VaultHandler.java index 4ad03839..7b99b5ed 100644 --- a/src/main/java/fr/xephi/authme/permission/handlers/VaultHandler.java +++ b/src/main/java/fr/xephi/authme/permission/handlers/VaultHandler.java @@ -27,7 +27,7 @@ public class VaultHandler implements PermissionHandler { } @Override - public boolean hasPermission(Player player, PermissionNode node, boolean def) { + public boolean hasPermission(Player player, PermissionNode node) { return vaultProvider.has(player, node.getNode()); } diff --git a/src/main/java/fr/xephi/authme/permission/handlers/ZPermissionsHandler.java b/src/main/java/fr/xephi/authme/permission/handlers/ZPermissionsHandler.java index 3afdbce2..18e433ba 100644 --- a/src/main/java/fr/xephi/authme/permission/handlers/ZPermissionsHandler.java +++ b/src/main/java/fr/xephi/authme/permission/handlers/ZPermissionsHandler.java @@ -29,12 +29,12 @@ public class ZPermissionsHandler implements PermissionHandler { } @Override - public boolean hasPermission(Player player, PermissionNode node, boolean def) { + public boolean hasPermission(Player player, PermissionNode node) { Map perms = zPermissionsService.getPlayerPermissions(player.getWorld().getName(), null, player.getName()); if (perms.containsKey(node.getNode())) return perms.get(node.getNode()); else - return def; + return node.getDefaultPermission().evaluate(player); } @Override From 38db2ef0bd96fad41b43e288f915e68381a65afc Mon Sep 17 00:00:00 2001 From: ljacqu Date: Wed, 1 Jun 2016 23:24:48 +0200 Subject: [PATCH 141/200] Minor code householding - Add removed property to migration service - Log exception with appropriate logger method --- src/main/java/fr/xephi/authme/ConsoleLogger.java | 4 ++-- src/main/java/fr/xephi/authme/command/CommandInitializer.java | 3 --- .../fr/xephi/authme/settings/SettingsMigrationService.java | 2 +- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/main/java/fr/xephi/authme/ConsoleLogger.java b/src/main/java/fr/xephi/authme/ConsoleLogger.java index 26a25704..284a7442 100644 --- a/src/main/java/fr/xephi/authme/ConsoleLogger.java +++ b/src/main/java/fr/xephi/authme/ConsoleLogger.java @@ -46,7 +46,7 @@ public final class ConsoleLogger { try { fileWriter = new FileWriter(logFile, true); } catch (IOException e) { - ConsoleLogger.showError("Failed to create the log file:" + e); + ConsoleLogger.logException("Failed to create the log file:", e); } } } else { @@ -70,7 +70,7 @@ public final class ConsoleLogger { public static void debug(String message) { if (enableDebug) { //creating and filling an exception is a expensive call - //->so it should be removed as soon #419 is fixed + //TODO #419 20160601: ->so it should be removed as soon #419 is fixed //logger.isLoggable does not work because the plugin logger is always ALL logger.log(Level.FINE, message + ' ' + Thread.currentThread().getName(), new Exception()); diff --git a/src/main/java/fr/xephi/authme/command/CommandInitializer.java b/src/main/java/fr/xephi/authme/command/CommandInitializer.java index 162d0289..59ab9c51 100644 --- a/src/main/java/fr/xephi/authme/command/CommandInitializer.java +++ b/src/main/java/fr/xephi/authme/command/CommandInitializer.java @@ -43,9 +43,6 @@ import java.util.Collection; import java.util.List; import java.util.Set; -import static fr.xephi.authme.permission.DefaultPermission.ALLOWED; -import static fr.xephi.authme.permission.DefaultPermission.OP_ONLY; - /** * Initializes all available AuthMe commands. */ diff --git a/src/main/java/fr/xephi/authme/settings/SettingsMigrationService.java b/src/main/java/fr/xephi/authme/settings/SettingsMigrationService.java index 7b024113..fdafea6f 100644 --- a/src/main/java/fr/xephi/authme/settings/SettingsMigrationService.java +++ b/src/main/java/fr/xephi/authme/settings/SettingsMigrationService.java @@ -64,7 +64,7 @@ public class SettingsMigrationService { String[] deprecatedProperties = { "Converter.Rakamak.newPasswordHash", "Hooks.chestshop", "Hooks.legacyChestshop", "Hooks.notifications", "Passpartu", "Performances", "settings.restrictions.enablePasswordVerifier", "Xenoforo.predefinedSalt", - "VeryGames"}; + "VeryGames", "settings.restrictions.allowAllCommandsIfRegistrationIsOptional"}; for (String deprecatedPath : deprecatedProperties) { if (configuration.contains(deprecatedPath)) { return true; From 408e8dd0ddd56792f4a2003ccc62f1a6c4951760 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Thu, 2 Jun 2016 00:03:02 +0200 Subject: [PATCH 142/200] #739 Create unit tests for PermissionsManager#hasPermission --- .../permission/PermissionsManagerTest.java | 165 ++++++++++++++++++ .../authme/permission/TestPermissions.java | 33 ++++ 2 files changed, 198 insertions(+) create mode 100644 src/test/java/fr/xephi/authme/permission/PermissionsManagerTest.java create mode 100644 src/test/java/fr/xephi/authme/permission/TestPermissions.java diff --git a/src/test/java/fr/xephi/authme/permission/PermissionsManagerTest.java b/src/test/java/fr/xephi/authme/permission/PermissionsManagerTest.java new file mode 100644 index 00000000..225fc899 --- /dev/null +++ b/src/test/java/fr/xephi/authme/permission/PermissionsManagerTest.java @@ -0,0 +1,165 @@ +package fr.xephi.authme.permission; + +import org.bukkit.Server; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.bukkit.plugin.PluginManager; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +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 PermissionsManager}. + */ +// TODO #739: Unignore tests after they pass +@RunWith(MockitoJUnitRunner.class) +public class PermissionsManagerTest { + + @InjectMocks + private PermissionsManager permissionsManager; + + @Mock + private Server server; + + @Mock + private PluginManager pluginManager; + + @Test + public void shouldUseDefaultPermissionForCommandSender() { + // given + PermissionNode node = TestPermissions.LOGIN; + CommandSender sender = mock(CommandSender.class); + + // when + boolean result = permissionsManager.hasPermission(sender, node); + + // then + assertThat(result, equalTo(false)); + } + + @Test + public void shouldGrantToOpCommandSender() { + // given + PermissionNode node = TestPermissions.DELETE_USER; + CommandSender sender = mock(CommandSender.class); + given(sender.isOp()).willReturn(true); + + // when + boolean result = permissionsManager.hasPermission(sender, node); + + // then + assertThat(result, equalTo(true)); + } + + @Test + @Ignore + // TODO ljacqu 20160601: This test should pass - tested permission node has DefaultPermission.NOT_ALLOWED + public void shouldDenyPermissionEvenForOpCommandSender() { + // given + PermissionNode node = TestPermissions.WORLD_DOMINATION; + CommandSender sender = mock(CommandSender.class); + given(sender.isOp()).willReturn(true); + + // when + boolean result = permissionsManager.hasPermission(sender, node); + + // then + assertThat(result, equalTo(false)); + } + + @Test + @Ignore + // TODO ljacqu 20160601: This test MUST pass! -> tested node has DefaultPermission.ALLOW + public void shouldAllowForNonOpPlayer() { + // given + PermissionNode node = TestPermissions.LOGIN; + Player player = mock(Player.class); + + // when + boolean result = permissionsManager.hasPermission(player, node); + + // then + assertThat(result, equalTo(true)); + } + + @Test + public void shouldDenyForNonOpPlayer() { + // given + PermissionNode node = TestPermissions.DELETE_USER; + Player player = mock(Player.class); + + // when + boolean result = permissionsManager.hasPermission(player, node); + + // then + assertThat(result, equalTo(false)); + } + + @Test + public void shouldAllowForOpPlayer() { + // given + PermissionNode node = TestPermissions.DELETE_USER; + Player player = mock(Player.class); + given(player.isOp()).willReturn(true); + + // when + boolean result = permissionsManager.hasPermission(player, node); + + // then + assertThat(result, equalTo(true)); + } + + @Test + @Ignore + // TODO ljacqu 20160601: This should pass -> tested node has DefaultPermission.NOT_ALLOWED so result should be false + public void shouldDenyEvenForOpPlayer() { + // given + PermissionNode node = TestPermissions.WORLD_DOMINATION; + Player player = mock(Player.class); + given(player.isOp()).willReturn(true); + + // when + boolean result = permissionsManager.hasPermission(player, node); + + // then + assertThat(result, equalTo(false)); + } + + @Test + @Ignore + // TODO ljacqu 20160601: This must pass. null permission => true + public void shouldHandleNullPermissionForCommandSender() { + // given + PermissionNode node = null; + CommandSender sender = mock(CommandSender.class); + + // when + boolean result = permissionsManager.hasPermission(sender, node); + + // then + assertThat(result, equalTo(true)); + } + + @Test + @Ignore + // TODO ljacqu 20160601: This must pass. null permission => true + public void shouldHandleNullPermissionForPlayer() { + // given + PermissionNode node = null; + Player player = mock(Player.class); + + // when + boolean result = permissionsManager.hasPermission(player, node); + + // then + assertThat(result, equalTo(true)); + } +} diff --git a/src/test/java/fr/xephi/authme/permission/TestPermissions.java b/src/test/java/fr/xephi/authme/permission/TestPermissions.java new file mode 100644 index 00000000..89460771 --- /dev/null +++ b/src/test/java/fr/xephi/authme/permission/TestPermissions.java @@ -0,0 +1,33 @@ +package fr.xephi.authme.permission; + +/** + * Sample permission nodes for testing. + */ +public enum TestPermissions implements PermissionNode { + + LOGIN("authme.login", DefaultPermission.ALLOWED), + + DELETE_USER("authme.admin.delete", DefaultPermission.OP_ONLY), + + WORLD_DOMINATION("global.own", DefaultPermission.NOT_ALLOWED); + + + private final String node; + private final DefaultPermission defaultPermission; + + TestPermissions(String node, DefaultPermission defaultPermission) { + this.node = node; + this.defaultPermission = defaultPermission; + } + + @Override + public String getNode() { + return node; + } + + @Override + public DefaultPermission getDefaultPermission() { + return defaultPermission; + } + +} From e06c5e7309a190e96b0632ae47bb60c19684b946 Mon Sep 17 00:00:00 2001 From: DNx Date: Thu, 2 Jun 2016 05:23:16 +0700 Subject: [PATCH 143/200] Fix NPE from ConsoleLogger if plugin could not load configuration. --- src/main/java/fr/xephi/authme/AuthMe.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/fr/xephi/authme/AuthMe.java b/src/main/java/fr/xephi/authme/AuthMe.java index fe75c530..225f8115 100644 --- a/src/main/java/fr/xephi/authme/AuthMe.java +++ b/src/main/java/fr/xephi/authme/AuthMe.java @@ -215,7 +215,7 @@ public class AuthMe extends JavaPlugin { // Load settings and custom configurations, if it fails, stop the server due to security reasons. newSettings = createNewSetting(); if (newSettings == null) { - ConsoleLogger.showError("Could not load configuration. Aborting."); + getLogger().warning("Could not load configuration. Aborting."); getServer().shutdown(); return; } From 2581b95afbf46189c6bd3a7c912da9ab0b4bacb5 Mon Sep 17 00:00:00 2001 From: DNx Date: Thu, 2 Jun 2016 11:07:37 +0700 Subject: [PATCH 144/200] Fix NPE on server stop #740 - not the best, because we will miss that 3 log inside authme.log. --- src/main/java/fr/xephi/authme/AuthMe.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/fr/xephi/authme/AuthMe.java b/src/main/java/fr/xephi/authme/AuthMe.java index 225f8115..cf7289c0 100644 --- a/src/main/java/fr/xephi/authme/AuthMe.java +++ b/src/main/java/fr/xephi/authme/AuthMe.java @@ -473,13 +473,13 @@ public class AuthMe extends JavaPlugin { pendingTasks.add(pendingTask.getTaskId()); } } - ConsoleLogger.info("Waiting for " + pendingTasks.size() + " tasks to finish"); + getLogger().info("Waiting for " + pendingTasks.size() + " tasks to finish"); int progress = 0; for (int taskId : pendingTasks) { int maxTries = 5; while (getServer().getScheduler().isCurrentlyRunning(taskId)) { if (maxTries <= 0) { - ConsoleLogger.info("Async task " + taskId + " times out after to many tries"); + getLogger().info("Async task " + taskId + " times out after to many tries"); break; } try { @@ -490,7 +490,7 @@ public class AuthMe extends JavaPlugin { } progress++; - ConsoleLogger.info("Progress: " + progress + " / " + pendingTasks.size()); + getLogger().info("Progress: " + progress + " / " + pendingTasks.size()); } if (database != null) { database.close(); From e75cff5fb8bdbbda226d0decd2f2b7cf5edab092 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Thu, 2 Jun 2016 12:46:54 +0200 Subject: [PATCH 145/200] Remove static injections in ListenerService - Get other classes via Inject annotation - Remove no longer needed Utils methods (relates to #736) - Create tests for ListenerService and AuthMeBlockListener - Performance improvement: keep unrestricted names as Set instead of List for faster contains() method --- src/main/java/fr/xephi/authme/api/API.java | 2 +- .../authme/listener/AuthMeBlockListener.java | 9 +- .../authme/listener/AuthMeEntityListener.java | 26 +- .../authme/listener/AuthMePlayerListener.java | 42 +-- .../listener/AuthMePlayerListener16.java | 7 +- .../listener/AuthMePlayerListener18.java | 7 +- .../authme/listener/ListenerService.java | 93 +++++-- src/main/java/fr/xephi/authme/util/Utils.java | 24 +- .../listener/AuthMeBlockListenerTest.java | 95 +++++++ .../authme/listener/ListenerServiceTest.java | 254 ++++++++++++++++++ .../authme/listener/OnJoinVerifierTest.java | 1 - .../authme/settings/SpawnLoaderTest.java | 3 - 12 files changed, 480 insertions(+), 83 deletions(-) create mode 100644 src/test/java/fr/xephi/authme/listener/AuthMeBlockListenerTest.java create mode 100644 src/test/java/fr/xephi/authme/listener/ListenerServiceTest.java diff --git a/src/main/java/fr/xephi/authme/api/API.java b/src/main/java/fr/xephi/authme/api/API.java index 26f5ea8f..ca75cee1 100644 --- a/src/main/java/fr/xephi/authme/api/API.java +++ b/src/main/java/fr/xephi/authme/api/API.java @@ -179,7 +179,7 @@ public class API { */ @Deprecated public boolean isNPC(Player player) { - return Utils.isNPC(player); + return instance.getPluginHooks().isNpc(player); } } diff --git a/src/main/java/fr/xephi/authme/listener/AuthMeBlockListener.java b/src/main/java/fr/xephi/authme/listener/AuthMeBlockListener.java index 9ffdd483..7fe23dd9 100644 --- a/src/main/java/fr/xephi/authme/listener/AuthMeBlockListener.java +++ b/src/main/java/fr/xephi/authme/listener/AuthMeBlockListener.java @@ -5,18 +5,23 @@ import org.bukkit.event.Listener; import org.bukkit.event.block.BlockBreakEvent; import org.bukkit.event.block.BlockPlaceEvent; +import javax.inject.Inject; + public class AuthMeBlockListener implements Listener { + @Inject + private ListenerService listenerService; + @EventHandler(ignoreCancelled = true) public void onBlockPlace(BlockPlaceEvent event) { - if (ListenerService.shouldCancelEvent(event.getPlayer())) { + if (listenerService.shouldCancelEvent(event.getPlayer())) { event.setCancelled(true); } } @EventHandler(ignoreCancelled = true) public void onBlockBreak(BlockBreakEvent event) { - if (ListenerService.shouldCancelEvent(event.getPlayer())) { + if (listenerService.shouldCancelEvent(event.getPlayer())) { event.setCancelled(true); } } diff --git a/src/main/java/fr/xephi/authme/listener/AuthMeEntityListener.java b/src/main/java/fr/xephi/authme/listener/AuthMeEntityListener.java index 61641679..ce8afa40 100644 --- a/src/main/java/fr/xephi/authme/listener/AuthMeEntityListener.java +++ b/src/main/java/fr/xephi/authme/listener/AuthMeEntityListener.java @@ -16,17 +16,19 @@ import org.bukkit.event.entity.EntityTargetEvent; import org.bukkit.event.entity.FoodLevelChangeEvent; import org.bukkit.event.entity.ProjectileLaunchEvent; +import javax.inject.Inject; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -import static fr.xephi.authme.listener.ListenerService.shouldCancelEvent; - public class AuthMeEntityListener implements Listener { + private final ListenerService listenerService; private Method getShooter; private boolean shooterIsLivingEntity; - public AuthMeEntityListener() { + @Inject + AuthMeEntityListener(ListenerService listenerService) { + this.listenerService = listenerService; try { getShooter = Projectile.class.getDeclaredMethod("getShooter"); shooterIsLivingEntity = getShooter.getReturnType() == LivingEntity.class; @@ -38,7 +40,7 @@ public class AuthMeEntityListener implements Listener { // Note #360: npc status can be used to bypass security!!! @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) public void onEntityDamage(EntityDamageEvent event) { - if (shouldCancelEvent(event)) { + if (listenerService.shouldCancelEvent(event)) { event.getEntity().setFireTicks(0); event.setDamage(0); event.setCancelled(true); @@ -47,7 +49,7 @@ public class AuthMeEntityListener implements Listener { @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) public void onEntityTarget(EntityTargetEvent event) { - if (shouldCancelEvent(event)) { + if (listenerService.shouldCancelEvent(event)) { event.setTarget(null); event.setCancelled(true); } @@ -55,21 +57,21 @@ public class AuthMeEntityListener implements Listener { @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) public void onDamage(EntityDamageByEntityEvent event) { - if (shouldCancelEvent(event)) { + if (listenerService.shouldCancelEvent(event)) { event.setCancelled(true); } } @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) public void onFoodLevelChange(FoodLevelChangeEvent event) { - if (shouldCancelEvent(event)) { + if (listenerService.shouldCancelEvent(event)) { event.setCancelled(true); } } @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) public void entityRegainHealthEvent(EntityRegainHealthEvent event) { - if (shouldCancelEvent(event)) { + if (listenerService.shouldCancelEvent(event)) { event.setAmount(0); event.setCancelled(true); } @@ -77,14 +79,14 @@ public class AuthMeEntityListener implements Listener { @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST) public void onEntityInteract(EntityInteractEvent event) { - if (shouldCancelEvent(event)) { + if (listenerService.shouldCancelEvent(event)) { event.setCancelled(true); } } @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) public void onLowestEntityInteract(EntityInteractEvent event) { - if (shouldCancelEvent(event)) { + if (listenerService.shouldCancelEvent(event)) { event.setCancelled(true); } } @@ -111,14 +113,14 @@ public class AuthMeEntityListener implements Listener { } else { shooterRaw = projectile.getShooter(); } - if (shooterRaw instanceof Player && shouldCancelEvent((Player) shooterRaw)) { + if (shooterRaw instanceof Player && listenerService.shouldCancelEvent((Player) shooterRaw)) { event.setCancelled(true); } } @EventHandler(ignoreCancelled = true, priority = EventPriority.NORMAL) public void onShoot(EntityShootBowEvent event) { - if (shouldCancelEvent(event)) { + if (listenerService.shouldCancelEvent(event)) { event.setCancelled(true); } } diff --git a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java index a974421a..67e57ee8 100644 --- a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java +++ b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java @@ -45,7 +45,6 @@ import java.util.Iterator; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; -import static fr.xephi.authme.listener.ListenerService.shouldCancelEvent; import static fr.xephi.authme.settings.properties.RestrictionSettings.ALLOWED_MOVEMENT_RADIUS; import static fr.xephi.authme.settings.properties.RestrictionSettings.ALLOW_UNAUTHED_MOVEMENT; @@ -72,6 +71,8 @@ public class AuthMePlayerListener implements Listener { private SpawnLoader spawnLoader; @Inject private OnJoinVerifier onJoinVerifier; + @Inject + private ListenerService listenerService; @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) public void onPlayerCommandPreprocess(PlayerCommandPreprocessEvent event) { @@ -83,11 +84,10 @@ public class AuthMePlayerListener implements Listener { return; } final Player player = event.getPlayer(); - if (!shouldCancelEvent(player)) { - return; + if (listenerService.shouldCancelEvent(player)) { + event.setCancelled(true); + m.send(player, MessageKey.DENIED_COMMAND); } - event.setCancelled(true); - m.send(player, MessageKey.DENIED_COMMAND); } @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) @@ -97,7 +97,7 @@ public class AuthMePlayerListener implements Listener { } final Player player = event.getPlayer(); - if (shouldCancelEvent(player)) { + if (listenerService.shouldCancelEvent(player)) { event.setCancelled(true); m.send(player, MessageKey.DENIED_CHAT); } else if (settings.getProperty(RestrictionSettings.HIDE_CHAT)) { @@ -105,7 +105,7 @@ public class AuthMePlayerListener implements Listener { Iterator iter = recipients.iterator(); while (iter.hasNext()) { Player p = iter.next(); - if (shouldCancelEvent(p)) { + if (listenerService.shouldCancelEvent(p)) { iter.remove(); } } @@ -134,7 +134,7 @@ public class AuthMePlayerListener implements Listener { } Player player = event.getPlayer(); - if (!shouldCancelEvent(player)) { + if (!listenerService.shouldCancelEvent(player)) { return; } @@ -280,21 +280,21 @@ public class AuthMePlayerListener implements Listener { @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) public void onPlayerPickupItem(PlayerPickupItemEvent event) { - if (shouldCancelEvent(event)) { + if (listenerService.shouldCancelEvent(event)) { event.setCancelled(true); } } @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) public void onPlayerInteract(PlayerInteractEvent event) { - if (shouldCancelEvent(event)) { + if (listenerService.shouldCancelEvent(event)) { event.setCancelled(true); } } @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) public void onPlayerConsumeItem(PlayerItemConsumeEvent event) { - if (shouldCancelEvent(event)) { + if (listenerService.shouldCancelEvent(event)) { event.setCancelled(true); } } @@ -303,7 +303,7 @@ public class AuthMePlayerListener implements Listener { public void onPlayerInventoryOpen(InventoryOpenEvent event) { final Player player = (Player) event.getPlayer(); - if (!shouldCancelEvent(player)) { + if (!listenerService.shouldCancelEvent(player)) { return; } event.setCancelled(true); @@ -329,7 +329,7 @@ public class AuthMePlayerListener implements Listener { return; } Player player = (Player) event.getWhoClicked(); - if (!shouldCancelEvent(player)) { + if (!listenerService.shouldCancelEvent(player)) { return; } event.setCancelled(true); @@ -337,28 +337,28 @@ public class AuthMePlayerListener implements Listener { @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) public void onPlayerHitPlayerEvent(EntityDamageByEntityEvent event) { - if (shouldCancelEvent(event)) { + if (listenerService.shouldCancelEvent(event)) { event.setCancelled(true); } } @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) public void onPlayerInteractEntity(PlayerInteractEntityEvent event) { - if (shouldCancelEvent(event)) { + if (listenerService.shouldCancelEvent(event)) { event.setCancelled(true); } } @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) public void onPlayerDropItem(PlayerDropItemEvent event) { - if (shouldCancelEvent(event)) { + if (listenerService.shouldCancelEvent(event)) { event.setCancelled(true); } } @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) public void onPlayerBedEnter(PlayerBedEnterEvent event) { - if (shouldCancelEvent(event)) { + if (listenerService.shouldCancelEvent(event)) { event.setCancelled(true); } } @@ -366,7 +366,7 @@ public class AuthMePlayerListener implements Listener { @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) public void onSignChange(SignChangeEvent event) { Player player = event.getPlayer(); - if (shouldCancelEvent(player)) { + if (listenerService.shouldCancelEvent(player)) { event.setCancelled(true); } } @@ -377,7 +377,7 @@ public class AuthMePlayerListener implements Listener { if (settings.getProperty(RestrictionSettings.NO_TELEPORT)) { return; } - if (!shouldCancelEvent(event)) { + if (!listenerService.shouldCancelEvent(event)) { return; } Player player = event.getPlayer(); @@ -398,14 +398,14 @@ public class AuthMePlayerListener implements Listener { @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) public void onPlayerShear(PlayerShearEntityEvent event) { - if (shouldCancelEvent(event)) { + if (listenerService.shouldCancelEvent(event)) { event.setCancelled(true); } } @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) public void onPlayerFish(PlayerFishEvent event) { - if (shouldCancelEvent(event)) { + if (listenerService.shouldCancelEvent(event)) { event.setCancelled(true); } } diff --git a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener16.java b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener16.java index ee0e581c..871757f4 100644 --- a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener16.java +++ b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener16.java @@ -5,14 +5,19 @@ import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; import org.bukkit.event.player.PlayerEditBookEvent; +import javax.inject.Inject; + /** * Listener of player events for events introduced in Minecraft 1.6. */ public class AuthMePlayerListener16 implements Listener { + @Inject + private ListenerService listenerService; + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) public void onPlayerEditBook(PlayerEditBookEvent event) { - if (ListenerService.shouldCancelEvent(event)) { + if (listenerService.shouldCancelEvent(event)) { event.setCancelled(true); } } diff --git a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener18.java b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener18.java index 560f6e8b..b6cbf2a7 100644 --- a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener18.java +++ b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener18.java @@ -5,14 +5,19 @@ import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; import org.bukkit.event.player.PlayerInteractAtEntityEvent; +import javax.inject.Inject; + /** * Listener of player events for events introduced in Minecraft 1.8. */ public class AuthMePlayerListener18 implements Listener { + @Inject + private ListenerService listenerService; + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) public void onPlayerInteractAtEntity(PlayerInteractAtEntityEvent event) { - if (ListenerService.shouldCancelEvent(event)) { + if (listenerService.shouldCancelEvent(event)) { event.setCancelled(true); } } diff --git a/src/main/java/fr/xephi/authme/listener/ListenerService.java b/src/main/java/fr/xephi/authme/listener/ListenerService.java index 449dec67..c5de1bc3 100644 --- a/src/main/java/fr/xephi/authme/listener/ListenerService.java +++ b/src/main/java/fr/xephi/authme/listener/ListenerService.java @@ -1,26 +1,48 @@ package fr.xephi.authme.listener; -import fr.xephi.authme.util.Utils; +import fr.xephi.authme.cache.auth.PlayerCache; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.hooks.PluginHooks; +import fr.xephi.authme.initialization.SettingsDependent; +import fr.xephi.authme.settings.NewSetting; +import fr.xephi.authme.settings.properties.RegistrationSettings; +import fr.xephi.authme.settings.properties.RestrictionSettings; import org.bukkit.entity.Entity; import org.bukkit.entity.Player; import org.bukkit.event.entity.EntityEvent; import org.bukkit.event.player.PlayerEvent; -/** - * Service class for the AuthMe listeners. - */ -final class ListenerService { +import javax.inject.Inject; +import java.util.HashSet; +import java.util.Set; - private ListenerService() { +/** + * Service class for the AuthMe listeners to determine whether an event should be canceled. + */ +class ListenerService implements SettingsDependent { + + private final DataSource dataSource; + private final PluginHooks pluginHooks; + private final PlayerCache playerCache; + + private boolean isRegistrationForced; + private Set unrestrictedNames; + + @Inject + ListenerService(NewSetting settings, DataSource dataSource, PluginHooks pluginHooks, PlayerCache playerCache) { + this.dataSource = dataSource; + this.pluginHooks = pluginHooks; + this.playerCache = playerCache; + loadSettings(settings); } /** - * Return whether an event should be canceled (for unauthenticated, non-NPC players). + * Returns whether an event should be canceled (for unauthenticated, non-NPC players). * - * @param event The event to process - * @return True if the event should be canceled, false otherwise + * @param event the event to process + * @return true if the event should be canceled, false otherwise */ - public static boolean shouldCancelEvent(EntityEvent event) { + public boolean shouldCancelEvent(EntityEvent event) { Entity entity = event.getEntity(); if (entity == null || !(entity instanceof Player)) { return false; @@ -31,24 +53,57 @@ final class ListenerService { } /** - * Return whether an event should be canceled (for unauthenticated, non-NPC players). + * Returns whether an event should be canceled (for unauthenticated, non-NPC players). * - * @param event The event to process - * @return True if the event should be canceled, false otherwise + * @param event the event to process + * @return true if the event should be canceled, false otherwise */ - public static boolean shouldCancelEvent(PlayerEvent event) { + public boolean shouldCancelEvent(PlayerEvent event) { Player player = event.getPlayer(); return shouldCancelEvent(player); } /** - * Return, based on the player associated with the event, whether or not the event should be canceled. + * Returns, based on the player associated with the event, whether or not the event should be canceled. * - * @param player The player to verify - * @return True if the associated event should be canceled, false otherwise + * @param player the player to verify + * @return true if the associated event should be canceled, false otherwise */ - public static boolean shouldCancelEvent(Player player) { - return player != null && !Utils.checkAuth(player) && !Utils.isNPC(player); + public boolean shouldCancelEvent(Player player) { + return player != null && !checkAuth(player.getName()) && !pluginHooks.isNpc(player); } + @Override + public void loadSettings(NewSetting settings) { + isRegistrationForced = settings.getProperty(RegistrationSettings.FORCE); + // Keep unrestricted names as Set for more efficient contains() + unrestrictedNames = new HashSet<>(settings.getProperty(RestrictionSettings.UNRESTRICTED_NAMES)); + } + + /** + * Checks whether the player is allowed to perform actions (i.e. whether he is logged in + * or if other settings permit playing). + * + * @param name the name of the player to verify + * @return true if the player may play, false otherwise + */ + private boolean checkAuth(String name) { + if (isUnrestricted(name) || playerCache.isAuthenticated(name)) { + return true; + } + if (!isRegistrationForced && !dataSource.isAuthAvailable(name)) { + return true; + } + return false; + } + + /** + * Checks if the name is unrestricted according to the configured settings. + * + * @param name the name to verify + * @return true if unrestricted, false otherwise + */ + private boolean isUnrestricted(String name) { + return unrestrictedNames.contains(name.toLowerCase()); + } } diff --git a/src/main/java/fr/xephi/authme/util/Utils.java b/src/main/java/fr/xephi/authme/util/Utils.java index 5d8c08ee..32cbeb9a 100644 --- a/src/main/java/fr/xephi/authme/util/Utils.java +++ b/src/main/java/fr/xephi/authme/util/Utils.java @@ -2,7 +2,6 @@ package fr.xephi.authme.util; import fr.xephi.authme.AuthMe; import fr.xephi.authme.ConsoleLogger; -import fr.xephi.authme.cache.auth.PlayerCache; import fr.xephi.authme.cache.limbo.LimboCache; import fr.xephi.authme.cache.limbo.LimboPlayer; import fr.xephi.authme.events.AuthMeTeleportEvent; @@ -118,23 +117,9 @@ public final class Utils { return permsMan.addGroup(player, group); } - // TODO: Move to a Manager - public static boolean checkAuth(Player player) { - if (player == null || Utils.isUnrestricted(player)) { - return true; - } - - if (PlayerCache.getInstance().isAuthenticated(player.getName())) { - return true; - } - - if (!Settings.isForcedRegistrationEnabled && !plugin.getDataSource().isAuthAvailable(player.getName())) { - return true; - } - return false; - } - public static boolean isUnrestricted(Player player) { + // TODO ljacqu 20160602: Checking for Settings.isAllowRestrictedIp is wrong! Nothing in the config suggests + // that this setting has anything to do with unrestricted names return Settings.isAllowRestrictedIp && Settings.getUnrestrictedName.contains(player.getName().toLowerCase()); } @@ -165,11 +150,6 @@ public final class Utils { }); } - @Deprecated - public static boolean isNPC(Player player) { - return plugin.getPluginHooks().isNpc(player); - } - public static void teleportToSpawn(Player player) { if (Settings.isTeleportToSpawnEnabled && !Settings.noTeleport) { Location spawn = plugin.getSpawnLocation(player); diff --git a/src/test/java/fr/xephi/authme/listener/AuthMeBlockListenerTest.java b/src/test/java/fr/xephi/authme/listener/AuthMeBlockListenerTest.java new file mode 100644 index 00000000..e96a3022 --- /dev/null +++ b/src/test/java/fr/xephi/authme/listener/AuthMeBlockListenerTest.java @@ -0,0 +1,95 @@ +package fr.xephi.authme.listener; + +import org.bukkit.entity.Player; +import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.event.block.BlockPlaceEvent; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +/** + * Test for {@link AuthMeBlockListener}. + */ +@RunWith(MockitoJUnitRunner.class) +public class AuthMeBlockListenerTest { + + @InjectMocks + private AuthMeBlockListener listener; + + @Mock + private ListenerService listenerService; + + @Test + public void shouldAllowPlaceEvent() { + // given + Player player = mock(Player.class); + BlockPlaceEvent event = mock(BlockPlaceEvent.class); + given(event.getPlayer()).willReturn(player); + given(listenerService.shouldCancelEvent(player)).willReturn(false); + + // when + listener.onBlockPlace(event); + + // then + verify(event).getPlayer(); + verifyNoMoreInteractions(event); + } + + @Test + public void shouldDenyPlaceEvent() { + // given + Player player = mock(Player.class); + BlockPlaceEvent event = mock(BlockPlaceEvent.class); + given(event.getPlayer()).willReturn(player); + given(listenerService.shouldCancelEvent(player)).willReturn(true); + + // when + listener.onBlockPlace(event); + + // then + verify(event).setCancelled(true); + verify(event).getPlayer(); + verifyNoMoreInteractions(event); + } + + @Test + public void shouldAllowBreakEvent() { + // given + Player player = mock(Player.class); + BlockBreakEvent event = mock(BlockBreakEvent.class); + given(event.getPlayer()).willReturn(player); + given(listenerService.shouldCancelEvent(player)).willReturn(false); + + // when + listener.onBlockBreak(event); + + // then + verify(event).getPlayer(); + verifyNoMoreInteractions(event); + } + + @Test + public void shouldDenyBreakEvent() { + // given + Player player = mock(Player.class); + BlockBreakEvent event = mock(BlockBreakEvent.class); + given(event.getPlayer()).willReturn(player); + given(listenerService.shouldCancelEvent(player)).willReturn(true); + + // when + listener.onBlockBreak(event); + + // then + verify(event).setCancelled(true); + verify(event).getPlayer(); + verifyNoMoreInteractions(event); + } + +} diff --git a/src/test/java/fr/xephi/authme/listener/ListenerServiceTest.java b/src/test/java/fr/xephi/authme/listener/ListenerServiceTest.java new file mode 100644 index 00000000..c9413e17 --- /dev/null +++ b/src/test/java/fr/xephi/authme/listener/ListenerServiceTest.java @@ -0,0 +1,254 @@ +package fr.xephi.authme.listener; + +import fr.xephi.authme.cache.auth.PlayerCache; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.hooks.PluginHooks; +import fr.xephi.authme.settings.NewSetting; +import fr.xephi.authme.settings.properties.RegistrationSettings; +import fr.xephi.authme.settings.properties.RestrictionSettings; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import org.bukkit.event.HandlerList; +import org.bukkit.event.entity.EntityEvent; +import org.bukkit.event.player.PlayerEvent; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; + +import java.lang.reflect.Method; +import java.util.Arrays; + +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.verifyZeroInteractions; + +/** + * Test for {@link ListenerService}. + */ +@RunWith(MockitoJUnitRunner.class) +public class ListenerServiceTest { + + private ListenerService listenerService; + + @Mock + private NewSetting settings; + + @Mock + private DataSource dataSource; + + @Mock + private PluginHooks pluginHooks; + + @Mock + private PlayerCache playerCache; + + @Before + public void initializeTestSetup() { + given(settings.getProperty(RegistrationSettings.FORCE)).willReturn(true); + given(settings.getProperty(RestrictionSettings.UNRESTRICTED_NAMES)).willReturn( + Arrays.asList("npc1", "npc2", "npc3")); + + // Note ljacqu 20160602: We use a hacky way to avoid having to instantiate the service in each test: + // the listenerService test is initialized as a mock that will answer to any method invocation by creating an + // actual service object (with the @Mock fields) and then invoking the method on that actual service. + // As long as there is no interaction with listenerService all of the mock setups will have effect. + listenerService = mock(ListenerService.class, new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Exception { + Method method = invocation.getMethod(); + ListenerService service = new ListenerService(settings, dataSource, pluginHooks, playerCache); + return method.invoke(service, invocation.getArguments()); + } + }); + } + + @Test + public void shouldHandleEventWithNullEntity() { + // given + EntityEvent event = mock(EntityEvent.class); + given(event.getEntity()).willReturn(null); + + // when + boolean result = listenerService.shouldCancelEvent(event); + + // then + assertThat(result, equalTo(false)); + } + + @Test + public void shouldHandleEntityEventWithNonPlayerEntity() { + // given + EntityEvent event = mock(EntityEvent.class); + given(event.getEntity()).willReturn(mock(Entity.class)); + + // when + boolean result = listenerService.shouldCancelEvent(event); + + // then + assertThat(result, equalTo(false)); + } + + @Test + public void shouldAllowAuthenticatedPlayer() { + // given + String playerName = "Bobby"; + Player player = mockPlayerWithName(playerName); + given(playerCache.isAuthenticated(playerName)).willReturn(true); + EntityEvent event = mock(EntityEvent.class); + given(event.getEntity()).willReturn(player); + + // when + boolean result = listenerService.shouldCancelEvent(event); + + // then + assertThat(result, equalTo(false)); + verify(playerCache).isAuthenticated(playerName); + verifyZeroInteractions(dataSource); + } + + @Test + public void shouldDenyUnLoggedPlayer() { + // given + String playerName = "Tester"; + Player player = mockPlayerWithName(playerName); + given(playerCache.isAuthenticated(playerName)).willReturn(false); + EntityEvent event = mock(EntityEvent.class); + given(event.getEntity()).willReturn(player); + + // when + boolean result = listenerService.shouldCancelEvent(event); + + // then + assertThat(result, equalTo(true)); + verify(playerCache).isAuthenticated(playerName); + // makes sure the setting is checked first = avoid unnecessary DB operation + verifyZeroInteractions(dataSource); + } + + @Test + public void shouldAllowUnloggedPlayerForOptionalRegistration() { + // given + String playerName = "myPlayer1"; + Player player = mockPlayerWithName(playerName); + given(playerCache.isAuthenticated(playerName)).willReturn(false); + given(settings.getProperty(RegistrationSettings.FORCE)).willReturn(false); + EntityEvent event = mock(EntityEvent.class); + given(event.getEntity()).willReturn(player); + + // when + boolean result = listenerService.shouldCancelEvent(event); + + // then + assertThat(result, equalTo(false)); + verify(playerCache).isAuthenticated(playerName); + verify(dataSource).isAuthAvailable(playerName); + } + + @Test + public void shouldAllowUnrestrictedName() { + // given + String playerName = "Npc2"; + Player player = mockPlayerWithName(playerName); + EntityEvent event = mock(EntityEvent.class); + given(event.getEntity()).willReturn(player); + + // when + boolean result = listenerService.shouldCancelEvent(event); + + // then + assertThat(result, equalTo(false)); + verifyZeroInteractions(dataSource); + } + + @Test + public void shouldAllowNpcPlayer() { + // given + String playerName = "other_npc"; + Player player = mockPlayerWithName(playerName); + EntityEvent event = mock(EntityEvent.class); + given(event.getEntity()).willReturn(player); + given(pluginHooks.isNpc(player)).willReturn(true); + + // when + boolean result = listenerService.shouldCancelEvent(event); + + // then + assertThat(result, equalTo(false)); + verify(pluginHooks).isNpc(player); + } + + @Test + // This simply forwards to shouldCancelEvent(Player), so the rest is already tested + public void shouldHandlePlayerEvent() { + // given + String playerName = "example"; + Player player = mockPlayerWithName(playerName); + PlayerEvent event = new TestPlayerEvent(player); + given(playerCache.isAuthenticated(playerName)).willReturn(true); + + // when + boolean result = listenerService.shouldCancelEvent(event); + + // then + assertThat(result, equalTo(false)); + verify(playerCache).isAuthenticated(playerName); + verifyZeroInteractions(dataSource); + } + + @Test + public void shouldHandlePlayerEventWithNullPlayer() { + // given + PlayerEvent event = new TestPlayerEvent(null); + + // when + boolean result = listenerService.shouldCancelEvent(event); + + // then + assertThat(result, equalTo(false)); + } + + @Test + // The previous tests verify most of shouldCancelEvent(Player) + public void shouldVerifyBasedOnPlayer() { + // given + String playerName = "player"; + Player player = mockPlayerWithName(playerName); + + // when + boolean result = listenerService.shouldCancelEvent(player); + + // then + assertThat(result, equalTo(true)); + verify(playerCache).isAuthenticated(playerName); + verifyZeroInteractions(dataSource); + verify(pluginHooks).isNpc(player); + } + + private static Player mockPlayerWithName(String name) { + Player player = mock(Player.class); + given(player.getName()).willReturn(name); + return player; + } + + /** + * Test implementation of {@link PlayerEvent} (necessary because + * {@link PlayerEvent#getPlayer()} is declared final). + */ + private static final class TestPlayerEvent extends PlayerEvent { + public TestPlayerEvent(Player player) { + super(player); + } + + @Override + public HandlerList getHandlers() { + return null; + } + } +} diff --git a/src/test/java/fr/xephi/authme/listener/OnJoinVerifierTest.java b/src/test/java/fr/xephi/authme/listener/OnJoinVerifierTest.java index eb7e1511..1cf9d0c1 100644 --- a/src/test/java/fr/xephi/authme/listener/OnJoinVerifierTest.java +++ b/src/test/java/fr/xephi/authme/listener/OnJoinVerifierTest.java @@ -396,7 +396,6 @@ public class OnJoinVerifierTest { } private void expectValidationExceptionWith(MessageKey messageKey, String... args) { - //expectedException.expect(FailedVerificationException.class); expectedException.expect(exceptionWithData(messageKey, args)); } diff --git a/src/test/java/fr/xephi/authme/settings/SpawnLoaderTest.java b/src/test/java/fr/xephi/authme/settings/SpawnLoaderTest.java index 7bd0d40e..06088e19 100644 --- a/src/test/java/fr/xephi/authme/settings/SpawnLoaderTest.java +++ b/src/test/java/fr/xephi/authme/settings/SpawnLoaderTest.java @@ -12,8 +12,6 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; -import org.junit.runner.RunWith; -import org.mockito.runners.MockitoJUnitRunner; import java.io.File; import java.io.IOException; @@ -26,7 +24,6 @@ import static org.mockito.Mockito.mock; /** * Test for {@link SpawnLoader}. */ -@RunWith(MockitoJUnitRunner.class) public class SpawnLoaderTest { @Rule From 9b5009eb8c60a97657b4d985af09b03cfe983d26 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Thu, 2 Jun 2016 15:49:21 +0200 Subject: [PATCH 146/200] #742 Create test that plugin.yml corresponds to command definitions - Create test - Fix definitions to correspond --- .../authme/command/CommandInitializer.java | 8 +- src/main/resources/plugin.yml | 2 + .../command/CommandConsistencyTest.java | 98 +++++++++++++++++++ 3 files changed, 104 insertions(+), 4 deletions(-) create mode 100644 src/test/java/fr/xephi/authme/command/CommandConsistencyTest.java diff --git a/src/main/java/fr/xephi/authme/command/CommandInitializer.java b/src/main/java/fr/xephi/authme/command/CommandInitializer.java index 59ab9c51..7f6b2e26 100644 --- a/src/main/java/fr/xephi/authme/command/CommandInitializer.java +++ b/src/main/java/fr/xephi/authme/command/CommandInitializer.java @@ -291,7 +291,7 @@ public class CommandInitializer { // Register the base login command final CommandDescription LOGIN_BASE = CommandDescription.builder() .parent(null) - .labels("login", "l") + .labels("login", "l", "log") .description("Login command") .detailedDescription("Command to log in using AuthMeReloaded.") .withArgument("password", "Login password", false) @@ -324,7 +324,7 @@ public class CommandInitializer { // Register the base unregister command CommandDescription UNREGISTER_BASE = CommandDescription.builder() .parent(null) - .labels("unreg", "unregister") + .labels("unregister", "unreg") .description("Unregistration Command") .detailedDescription("Command to unregister using AuthMeReloaded.") .withArgument("password", "Password", false) @@ -347,7 +347,7 @@ public class CommandInitializer { // Register the base Email command CommandDescription EMAIL_BASE = CommandDescription.builder() .parent(null) - .labels("email", "mail") + .labels("email") .description("Email command") .detailedDescription("The AuthMeReloaded Email command base.") .executableCommand(initializer.newInstance(EmailBaseCommand.class)) @@ -392,7 +392,7 @@ public class CommandInitializer { // Register the base captcha command CommandDescription CAPTCHA_BASE = CommandDescription.builder() .parent(null) - .labels("captcha", "capt") + .labels("captcha") .description("Captcha Command") .detailedDescription("Captcha command for AuthMeReloaded.") .withArgument("captcha", "The Captcha", false) diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 63a9d81c..ef48d2e2 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -28,12 +28,14 @@ commands: changepassword: description: Change password of an account usage: /changepassword + aliases: [cp,changepass] logout: description: Logout usage: /logout unregister: description: Unregister your account usage: /unregister + aliases: [unreg] email: description: Add Email or recover password usage: '/email add your@email.com your@email.com|change oldEmail newEmail|recovery your@email.com' diff --git a/src/test/java/fr/xephi/authme/command/CommandConsistencyTest.java b/src/test/java/fr/xephi/authme/command/CommandConsistencyTest.java new file mode 100644 index 00000000..94a723b1 --- /dev/null +++ b/src/test/java/fr/xephi/authme/command/CommandConsistencyTest.java @@ -0,0 +1,98 @@ +package fr.xephi.authme.command; + + +import fr.xephi.authme.initialization.AuthMeServiceInitializer; +import org.bukkit.configuration.MemorySection; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; +import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static fr.xephi.authme.TestHelper.getJarFile; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; + +/** + * Checks that the commands declared in plugin.yml correspond + * to the ones built by the {@link CommandInitializer}. + */ +public class CommandConsistencyTest { + + @Test + public void shouldHaveEqualDefinitions() { + // given + Collection> initializedCommands = initializeCommands(); + Map> pluginFileLabels = getLabelsFromPluginFile(); + + // when / then + assertThat("number of base commands are equal in plugin.yml and CommandInitializer", + initializedCommands.size(), equalTo(pluginFileLabels.size())); + for (List commandLabels : initializedCommands) { + List pluginYmlLabels = pluginFileLabels.get(commandLabels.get(0)); + // NB: the first label in CommandDescription needs to correspond to the key in plugin.yml + assertThat("plugin.yml contains definition for command '" + commandLabels.get(0) + "'", + pluginYmlLabels, not(nullValue())); + assertThat("plugin.yml and CommandDescription have same alternative labels for /" + commandLabels.get(0), + pluginYmlLabels, containsInAnyOrder(commandLabels.subList(1, commandLabels.size()).toArray())); + } + } + + /** + * Gets the command definitions from CommandInitializer and returns the + * labels of all base commands. + * + * @return collection of all base command labels + */ + private static Collection> initializeCommands() { + AuthMeServiceInitializer injector = mock(AuthMeServiceInitializer.class); + given(injector.newInstance(any(Class.class))).willAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + return mock((Class) invocation.getArguments()[0]); + } + }); + CommandInitializer initializer = new CommandInitializer(injector); + Collection> commandLabels = new ArrayList<>(); + for (CommandDescription baseCommand : initializer.getCommands()) { + commandLabels.add(baseCommand.getLabels()); + } + return commandLabels; + } + + /** + * Reads plugin.yml and returns the defined commands by main label and aliases. + * + * @return collection of all labels and their aliases + */ + @SuppressWarnings("unchecked") + private static Map> getLabelsFromPluginFile() { + FileConfiguration pluginFile = YamlConfiguration.loadConfiguration(getJarFile("/plugin.yml")); + MemorySection commandList = (MemorySection) pluginFile.get("commands"); + Map commandDefinitions = commandList.getValues(false); + + Map> commandLabels = new HashMap<>(); + for (Map.Entry commandDefinition : commandDefinitions.entrySet()) { + MemorySection definition = (MemorySection) commandDefinition.getValue(); + List alternativeLabels = definition.get("aliases") == null + ? Collections.EMPTY_LIST + : (List) definition.get("aliases"); + commandLabels.put(commandDefinition.getKey(), alternativeLabels); + } + return commandLabels; + } + +} From c3d07cb9a42757a58f1f4659642464c7ae88bf8f Mon Sep 17 00:00:00 2001 From: EbonJaguar Date: Thu, 2 Jun 2016 02:49:10 +0200 Subject: [PATCH 147/200] #739 Cleanup on PermissionsManager (cherry picked from commit d9ad12b) --- .../authme/listener/AuthMeServerListener.java | 14 +++--- .../authme/permission/PermissionsManager.java | 43 +++---------------- 2 files changed, 15 insertions(+), 42 deletions(-) diff --git a/src/main/java/fr/xephi/authme/listener/AuthMeServerListener.java b/src/main/java/fr/xephi/authme/listener/AuthMeServerListener.java index 0e3162b8..b627ed6d 100644 --- a/src/main/java/fr/xephi/authme/listener/AuthMeServerListener.java +++ b/src/main/java/fr/xephi/authme/listener/AuthMeServerListener.java @@ -55,10 +55,11 @@ public class AuthMeServerListener implements Listener { return; } - // Call the onPluginDisable method in the permissions manager - permissionsManager.onPluginDisable(event); - final String pluginName = event.getPlugin().getName(); + + // Call the onPluginDisable method in the permissions manager + permissionsManager.onPluginDisable(pluginName); + if ("Essentials".equalsIgnoreCase(pluginName)) { pluginHooks.unhookEssentials(); ConsoleLogger.info("Essentials has been disabled: unhooking"); @@ -88,10 +89,11 @@ public class AuthMeServerListener implements Listener { return; } - // Call the onPluginEnable method in the permissions manager - permissionsManager.onPluginEnable(event); - final String pluginName = event.getPlugin().getName(); + + // Call the onPluginEnable method in the permissions manager + permissionsManager.onPluginEnable(pluginName); + if ("Essentials".equalsIgnoreCase(pluginName)) { pluginHooks.tryHookToEssentials(); } else if ("Multiverse-Core".equalsIgnoreCase(pluginName)) { diff --git a/src/main/java/fr/xephi/authme/permission/PermissionsManager.java b/src/main/java/fr/xephi/authme/permission/PermissionsManager.java index 072e82c5..8094a6b4 100644 --- a/src/main/java/fr/xephi/authme/permission/PermissionsManager.java +++ b/src/main/java/fr/xephi/authme/permission/PermissionsManager.java @@ -47,11 +47,7 @@ public class PermissionsManager { */ private final Server server; private final PluginManager pluginManager; - /** - * Type of permissions system that is currently used. - * Null if no permissions system is hooked and/or used. - */ - private PermissionsSystemType permsType = null; + /** * The permission handler that is currently in use. * Null if no permission system is hooked. @@ -79,15 +75,6 @@ public class PermissionsManager { return handler != null; } - /** - * Return the permissions system where the permissions manager is currently hooked into. - * - * @return The name of the permissions system used. - */ - public PermissionsSystemType getSystem() { - return permsType; - } - /** * Setup and hook into the permissions systems. */ @@ -96,9 +83,6 @@ public class PermissionsManager { // Force-unhook from current hooked permissions systems unhook(); - // Reset used permissions system type flag - permsType = null; - // Loop through all the available permissions system types for (PermissionsSystemType type : PermissionsSystemType.values()) { // Try to find and hook the current plugin if available, print an error if failed @@ -173,9 +157,6 @@ public class PermissionsManager { default: } - // Set the hooked permissions system type - this.permsType = type; - // Show a success message ConsoleLogger.info("Hooked into " + type.getName() + "!"); @@ -197,7 +178,6 @@ public class PermissionsManager { */ public void unhook() { // Reset the current used permissions system - this.permsType = null; this.handler = null; // Print a status message to the console @@ -209,25 +189,20 @@ public class PermissionsManager { * * @return True on success, false on failure. */ - public boolean reload() { + public void reload() { // Unhook all permission plugins unhook(); // Set up the permissions manager again, return the result setup(); - return true; } /** * Method called when a plugin is being enabled. * - * @param event Event instance. + * @param pluginName The name of the plugin being enabled. */ - public void onPluginEnable(PluginEnableEvent event) { - // Get the plugin and its name - Plugin plugin = event.getPlugin(); - String pluginName = plugin.getName(); - + public void onPluginEnable(String pluginName) { // Check if any known permissions system is enabling for (PermissionsSystemType permissionsSystemType : PermissionsSystemType.values()) { if (permissionsSystemType.isPermissionSystem(pluginName)) { @@ -241,14 +216,10 @@ public class PermissionsManager { /** * Method called when a plugin is being disabled. * - * @param event Event instance. + * @param pluginName The name of the plugin being disabled. */ - public void onPluginDisable(PluginDisableEvent event) { - // Get the plugin instance and name - Plugin plugin = event.getPlugin(); - String pluginName = plugin.getName(); - - // Is the WorldGuard plugin disabled + public void onPluginDisable(String pluginName) { + // Check if any known permission system is being disabled for (PermissionsSystemType permissionsSystemType : PermissionsSystemType.values()) { if (permissionsSystemType.isPermissionSystem(pluginName)) { ConsoleLogger.info(pluginName + " plugin disabled, updating hooks!"); From 73272b5931dc99a1344a4f3a682e4eee894c6ff1 Mon Sep 17 00:00:00 2001 From: Gnat008 Date: Thu, 2 Jun 2016 22:35:07 +0200 Subject: [PATCH 148/200] Remove all but one hasPermission() method in the PermissionsManager #739 (cherry picked from commit 65f3347) --- .../xephi/authme/command/CommandHandler.java | 2 +- .../xephi/authme/command/CommandMapper.java | 2 +- .../authme/command/help/HelpProvider.java | 2 +- .../authme/permission/PermissionsManager.java | 43 ++++--------------- .../authme/command/CommandHandlerTest.java | 4 +- .../authme/command/CommandMapperTest.java | 27 ++++++------ .../authme/command/help/HelpProviderTest.java | 4 +- 7 files changed, 30 insertions(+), 54 deletions(-) diff --git a/src/main/java/fr/xephi/authme/command/CommandHandler.java b/src/main/java/fr/xephi/authme/command/CommandHandler.java index dfff17d1..bca1ae04 100644 --- a/src/main/java/fr/xephi/authme/command/CommandHandler.java +++ b/src/main/java/fr/xephi/authme/command/CommandHandler.java @@ -127,7 +127,7 @@ public class CommandHandler { private void sendImproperArgumentsMessage(CommandSender sender, FoundCommandResult result) { CommandDescription command = result.getCommandDescription(); - if (!permissionsManager.hasPermission(sender, command)) { + if (!permissionsManager.hasPermission(sender, command.getPermission())) { sendPermissionDeniedError(sender); return; } diff --git a/src/main/java/fr/xephi/authme/command/CommandMapper.java b/src/main/java/fr/xephi/authme/command/CommandMapper.java index 2bb597a0..755cea0d 100644 --- a/src/main/java/fr/xephi/authme/command/CommandMapper.java +++ b/src/main/java/fr/xephi/authme/command/CommandMapper.java @@ -154,7 +154,7 @@ public class CommandMapper { } private FoundResultStatus getPermissionAwareStatus(CommandSender sender, CommandDescription command) { - if (sender != null && !permissionsManager.hasPermission(sender, command)) { + if (sender != null && !permissionsManager.hasPermission(sender, command.getPermission())) { return FoundResultStatus.NO_PERMISSION; } return FoundResultStatus.SUCCESS; diff --git a/src/main/java/fr/xephi/authme/command/help/HelpProvider.java b/src/main/java/fr/xephi/authme/command/help/HelpProvider.java index cadfd641..7257b4d8 100644 --- a/src/main/java/fr/xephi/authme/command/help/HelpProvider.java +++ b/src/main/java/fr/xephi/authme/command/help/HelpProvider.java @@ -160,7 +160,7 @@ public class HelpProvider implements SettingsDependent { + defaultPermission.getTitle() + addendum); // Evaluate if the sender has permission to the command - if (permissionsManager.hasPermission(sender, command)) { + if (permissionsManager.hasPermission(sender, command.getPermission())) { lines.add(ChatColor.GOLD + " Result: " + ChatColor.GREEN + ChatColor.ITALIC + "You have permission"); } else { lines.add(ChatColor.GOLD + " Result: " + ChatColor.DARK_RED + ChatColor.ITALIC + "No permission"); diff --git a/src/main/java/fr/xephi/authme/permission/PermissionsManager.java b/src/main/java/fr/xephi/authme/permission/PermissionsManager.java index 8094a6b4..8a392207 100644 --- a/src/main/java/fr/xephi/authme/permission/PermissionsManager.java +++ b/src/main/java/fr/xephi/authme/permission/PermissionsManager.java @@ -239,45 +239,18 @@ public class PermissionsManager { * @return True if the sender has the permission, false otherwise. */ public boolean hasPermission(CommandSender sender, PermissionNode permissionNode) { - return hasPermission(sender, permissionNode, sender.isOp()); - } - - public boolean hasPermission(CommandSender sender, PermissionNode permissionNode, boolean def) { - if (!(sender instanceof Player)) { - return def; - } - - Player player = (Player) sender; - return hasPermission(player, permissionNode, def); - } - - public boolean hasPermission(CommandSender sender, CommandDescription command) { - if (command.getPermission() == null) { + // Check if the permission node is null + if (permissionNode == null) { return true; } - DefaultPermission defaultPermission = command.getPermission().getDefaultPermission(); - boolean def = defaultPermission.evaluate(sender); - return (sender instanceof Player) - ? hasPermission((Player) sender, command.getPermission(), def) - : def; - } + // Return if the player is an Op if sender is console or no permission system in use + if (!(sender instanceof Player) || !isEnabled()) { + return sender.isOp(); + } - /** - * Check if a player has permission. - * - * @param player The player. - * @param node The permission node. - * @param def Default returned if no permissions system is used. - * - * @return True if the player has permission. - */ - private boolean hasPermission(Player player, PermissionNode node, boolean def) { - // If no permissions system is used, return the default value - if (!isEnabled()) - return def; - - return handler.hasPermission(player, node); + Player player = (Player) sender; + return handler.hasPermission(player, permissionNode); } /** diff --git a/src/test/java/fr/xephi/authme/command/CommandHandlerTest.java b/src/test/java/fr/xephi/authme/command/CommandHandlerTest.java index 914fe195..3e97627e 100644 --- a/src/test/java/fr/xephi/authme/command/CommandHandlerTest.java +++ b/src/test/java/fr/xephi/authme/command/CommandHandlerTest.java @@ -112,7 +112,7 @@ public class CommandHandlerTest { 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)); - given(permissionsManager.hasPermission(sender, command)).willReturn(true); + given(permissionsManager.hasPermission(sender, command.getPermission())).willReturn(true); // when handler.processCommand(sender, bukkitLabel, bukkitArgs); @@ -136,7 +136,7 @@ public class CommandHandlerTest { 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)); - given(permissionsManager.hasPermission(sender, command)).willReturn(false); + given(permissionsManager.hasPermission(sender, command.getPermission())).willReturn(false); // when handler.processCommand(sender, bukkitLabel, bukkitArgs); diff --git a/src/test/java/fr/xephi/authme/command/CommandMapperTest.java b/src/test/java/fr/xephi/authme/command/CommandMapperTest.java index b5789bbb..ecf43f72 100644 --- a/src/test/java/fr/xephi/authme/command/CommandMapperTest.java +++ b/src/test/java/fr/xephi/authme/command/CommandMapperTest.java @@ -4,6 +4,7 @@ import fr.xephi.authme.permission.PermissionsManager; import org.bukkit.command.CommandSender; import org.junit.Before; import org.junit.BeforeClass; +import org.junit.Ignore; import org.junit.Test; import java.util.List; @@ -26,6 +27,8 @@ import static org.mockito.Mockito.mock; /** * Test for {@link CommandMapper}. */ +@Ignore +// TODO Gnat008 20160602: Adjust matcher for null permission public class CommandMapperTest { private static Set commands; @@ -53,7 +56,7 @@ public class CommandMapperTest { // given List parts = asList("authme", "login", "test1"); CommandSender sender = mock(CommandSender.class); - given(permissionsManager.hasPermission(eq(sender), any(CommandDescription.class))).willReturn(true); + given(permissionsManager.hasPermission(eq(sender), any(CommandDescription.class).getPermission())).willReturn(true); // when FoundCommandResult result = mapper.mapPartsToCommand(sender, parts); @@ -72,7 +75,7 @@ public class CommandMapperTest { // given List parts = asList("Authme", "REG", "arg1", "arg2"); CommandSender sender = mock(CommandSender.class); - given(permissionsManager.hasPermission(eq(sender), any(CommandDescription.class))).willReturn(true); + given(permissionsManager.hasPermission(eq(sender), any(CommandDescription.class).getPermission())).willReturn(true); // when FoundCommandResult result = mapper.mapPartsToCommand(sender, parts); @@ -90,7 +93,7 @@ public class CommandMapperTest { // given List parts = asList("authme", "register", "pass123", "pass123", "pass123"); CommandSender sender = mock(CommandSender.class); - given(permissionsManager.hasPermission(eq(sender), any(CommandDescription.class))).willReturn(true); + given(permissionsManager.hasPermission(eq(sender), any(CommandDescription.class).getPermission())).willReturn(true); // when FoundCommandResult result = mapper.mapPartsToCommand(sender, parts); @@ -108,7 +111,7 @@ public class CommandMapperTest { // given List parts = asList("authme", "Reg"); CommandSender sender = mock(CommandSender.class); - given(permissionsManager.hasPermission(eq(sender), any(CommandDescription.class))).willReturn(true); + given(permissionsManager.hasPermission(eq(sender), any(CommandDescription.class).getPermission())).willReturn(true); // when FoundCommandResult result = mapper.mapPartsToCommand(sender, parts); @@ -126,7 +129,7 @@ public class CommandMapperTest { // given List parts = asList("authme", "reh", "pass123", "pass123"); CommandSender sender = mock(CommandSender.class); - given(permissionsManager.hasPermission(eq(sender), any(CommandDescription.class))).willReturn(true); + given(permissionsManager.hasPermission(eq(sender), any(CommandDescription.class).getPermission())).willReturn(true); // when FoundCommandResult result = mapper.mapPartsToCommand(sender, parts); @@ -145,7 +148,7 @@ public class CommandMapperTest { // given List parts = asList("authme", "asdfawetawty4asdca"); CommandSender sender = mock(CommandSender.class); - given(permissionsManager.hasPermission(eq(sender), any(CommandDescription.class))).willReturn(true); + given(permissionsManager.hasPermission(eq(sender), any(CommandDescription.class).getPermission())).willReturn(true); // when FoundCommandResult result = mapper.mapPartsToCommand(sender, parts); @@ -163,7 +166,7 @@ public class CommandMapperTest { // given List parts = singletonList("unregister"); CommandSender sender = mock(CommandSender.class); - given(permissionsManager.hasPermission(eq(sender), any(CommandDescription.class))).willReturn(true); + given(permissionsManager.hasPermission(eq(sender), any(CommandDescription.class).getPermission())).willReturn(true); // when FoundCommandResult result = mapper.mapPartsToCommand(sender, parts); @@ -181,7 +184,7 @@ public class CommandMapperTest { // given List parts = asList("bogus", "label1", "arg1"); CommandSender sender = mock(CommandSender.class); - given(permissionsManager.hasPermission(eq(sender), any(CommandDescription.class))).willReturn(true); + given(permissionsManager.hasPermission(eq(sender), any(CommandDescription.class).getPermission())).willReturn(true); // when FoundCommandResult result = mapper.mapPartsToCommand(sender, parts); @@ -206,7 +209,7 @@ public class CommandMapperTest { // given List parts = asList("Unreg", "player1"); CommandSender sender = mock(CommandSender.class); - given(permissionsManager.hasPermission(eq(sender), any(CommandDescription.class))).willReturn(true); + given(permissionsManager.hasPermission(eq(sender), any(CommandDescription.class).getPermission())).willReturn(true); // when FoundCommandResult result = mapper.mapPartsToCommand(sender, parts); @@ -224,7 +227,7 @@ public class CommandMapperTest { // given List parts = asList("unregistER", "player1", "wrongArg"); CommandSender sender = mock(CommandSender.class); - given(permissionsManager.hasPermission(eq(sender), any(CommandDescription.class))).willReturn(true); + given(permissionsManager.hasPermission(eq(sender), any(CommandDescription.class).getPermission())).willReturn(true); // when FoundCommandResult result = mapper.mapPartsToCommand(sender, parts); @@ -242,7 +245,7 @@ public class CommandMapperTest { // given List parts = asList("email", "helptest", "arg1"); CommandSender sender = mock(CommandSender.class); - given(permissionsManager.hasPermission(eq(sender), any(CommandDescription.class))).willReturn(true); + given(permissionsManager.hasPermission(eq(sender), any(CommandDescription.class).getPermission())).willReturn(true); // when FoundCommandResult result = mapper.mapPartsToCommand(sender, parts); @@ -260,7 +263,7 @@ public class CommandMapperTest { // given List parts = asList("authme", "login", "test1"); CommandSender sender = mock(CommandSender.class); - given(permissionsManager.hasPermission(eq(sender), any(CommandDescription.class))).willReturn(false); + given(permissionsManager.hasPermission(eq(sender), any(CommandDescription.class).getPermission())).willReturn(false); // when FoundCommandResult result = mapper.mapPartsToCommand(sender, parts); diff --git a/src/test/java/fr/xephi/authme/command/help/HelpProviderTest.java b/src/test/java/fr/xephi/authme/command/help/HelpProviderTest.java index 1c877b73..b6f3c88a 100644 --- a/src/test/java/fr/xephi/authme/command/help/HelpProviderTest.java +++ b/src/test/java/fr/xephi/authme/command/help/HelpProviderTest.java @@ -130,7 +130,7 @@ public class HelpProviderTest { FoundCommandResult result = newFoundResult(command, Collections.singletonList("unreg")); given(sender.isOp()).willReturn(true); given(permissionsManager.hasPermission(sender, AdminPermission.UNREGISTER)).willReturn(true); - given(permissionsManager.hasPermission(sender, command)).willReturn(true); + given(permissionsManager.hasPermission(sender, command.getPermission())).willReturn(true); // when List lines = helpProvider.printHelp(sender, result, HIDE_COMMAND | SHOW_PERMISSIONS); @@ -151,7 +151,7 @@ public class HelpProviderTest { FoundCommandResult result = newFoundResult(command, Collections.singletonList("unregister")); given(sender.isOp()).willReturn(false); given(permissionsManager.hasPermission(sender, AdminPermission.UNREGISTER)).willReturn(false); - given(permissionsManager.hasPermission(sender, command)).willReturn(false); + given(permissionsManager.hasPermission(sender, command.getPermission())).willReturn(false); // when List lines = helpProvider.printHelp(sender, result, HIDE_COMMAND | SHOW_PERMISSIONS); From 6d2597a9802b3ddf15d563465195c1df7863e8b5 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Thu, 2 Jun 2016 23:08:13 +0200 Subject: [PATCH 149/200] #739 Fix command unit tests; adjust default handling (cherry picked from commit 61c119b) --- .../authme/permission/PermissionsManager.java | 7 +--- .../authme/command/CommandMapperTest.java | 32 +++++++++---------- .../authme/command/help/HelpProviderTest.java | 4 +-- .../permission/PermissionsManagerTest.java | 14 +------- 4 files changed, 19 insertions(+), 38 deletions(-) diff --git a/src/main/java/fr/xephi/authme/permission/PermissionsManager.java b/src/main/java/fr/xephi/authme/permission/PermissionsManager.java index 8a392207..d8bc5337 100644 --- a/src/main/java/fr/xephi/authme/permission/PermissionsManager.java +++ b/src/main/java/fr/xephi/authme/permission/PermissionsManager.java @@ -1,7 +1,6 @@ package fr.xephi.authme.permission; import fr.xephi.authme.ConsoleLogger; -import fr.xephi.authme.command.CommandDescription; import fr.xephi.authme.permission.handlers.BPermissionsHandler; import fr.xephi.authme.permission.handlers.GroupManagerHandler; import fr.xephi.authme.permission.handlers.PermissionHandler; @@ -15,8 +14,6 @@ import org.bukkit.Bukkit; import org.bukkit.Server; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; -import org.bukkit.event.server.PluginDisableEvent; -import org.bukkit.event.server.PluginEnableEvent; import org.bukkit.plugin.Plugin; import org.bukkit.plugin.PluginManager; import org.bukkit.plugin.RegisteredServiceProvider; @@ -186,8 +183,6 @@ public class PermissionsManager { /** * Reload the permissions manager, and re-hook all permission plugins. - * - * @return True on success, false on failure. */ public void reload() { // Unhook all permission plugins @@ -246,7 +241,7 @@ public class PermissionsManager { // Return if the player is an Op if sender is console or no permission system in use if (!(sender instanceof Player) || !isEnabled()) { - return sender.isOp(); + return permissionNode.getDefaultPermission().evaluate(sender); } Player player = (Player) sender; diff --git a/src/test/java/fr/xephi/authme/command/CommandMapperTest.java b/src/test/java/fr/xephi/authme/command/CommandMapperTest.java index ecf43f72..96eab841 100644 --- a/src/test/java/fr/xephi/authme/command/CommandMapperTest.java +++ b/src/test/java/fr/xephi/authme/command/CommandMapperTest.java @@ -1,10 +1,10 @@ package fr.xephi.authme.command; +import fr.xephi.authme.permission.PermissionNode; import fr.xephi.authme.permission.PermissionsManager; import org.bukkit.command.CommandSender; import org.junit.Before; import org.junit.BeforeClass; -import org.junit.Ignore; import org.junit.Test; import java.util.List; @@ -27,13 +27,11 @@ import static org.mockito.Mockito.mock; /** * Test for {@link CommandMapper}. */ -@Ignore -// TODO Gnat008 20160602: Adjust matcher for null permission public class CommandMapperTest { private static Set commands; - private static CommandMapper mapper; - private static PermissionsManager permissionsManager; + private CommandMapper mapper; + private PermissionsManager permissionsManager; @BeforeClass public static void setUpCommandHandler() { @@ -56,7 +54,7 @@ public class CommandMapperTest { // given List parts = asList("authme", "login", "test1"); CommandSender sender = mock(CommandSender.class); - given(permissionsManager.hasPermission(eq(sender), any(CommandDescription.class).getPermission())).willReturn(true); + given(permissionsManager.hasPermission(eq(sender), any(PermissionNode.class))).willReturn(true); // when FoundCommandResult result = mapper.mapPartsToCommand(sender, parts); @@ -75,7 +73,7 @@ public class CommandMapperTest { // given List parts = asList("Authme", "REG", "arg1", "arg2"); CommandSender sender = mock(CommandSender.class); - given(permissionsManager.hasPermission(eq(sender), any(CommandDescription.class).getPermission())).willReturn(true); + given(permissionsManager.hasPermission(eq(sender), any(PermissionNode.class))).willReturn(true); // when FoundCommandResult result = mapper.mapPartsToCommand(sender, parts); @@ -93,7 +91,7 @@ public class CommandMapperTest { // given List parts = asList("authme", "register", "pass123", "pass123", "pass123"); CommandSender sender = mock(CommandSender.class); - given(permissionsManager.hasPermission(eq(sender), any(CommandDescription.class).getPermission())).willReturn(true); + given(permissionsManager.hasPermission(eq(sender), any(PermissionNode.class))).willReturn(true); // when FoundCommandResult result = mapper.mapPartsToCommand(sender, parts); @@ -111,7 +109,7 @@ public class CommandMapperTest { // given List parts = asList("authme", "Reg"); CommandSender sender = mock(CommandSender.class); - given(permissionsManager.hasPermission(eq(sender), any(CommandDescription.class).getPermission())).willReturn(true); + given(permissionsManager.hasPermission(eq(sender), any(PermissionNode.class))).willReturn(true); // when FoundCommandResult result = mapper.mapPartsToCommand(sender, parts); @@ -129,7 +127,7 @@ public class CommandMapperTest { // given List parts = asList("authme", "reh", "pass123", "pass123"); CommandSender sender = mock(CommandSender.class); - given(permissionsManager.hasPermission(eq(sender), any(CommandDescription.class).getPermission())).willReturn(true); + given(permissionsManager.hasPermission(eq(sender), any(PermissionNode.class))).willReturn(true); // when FoundCommandResult result = mapper.mapPartsToCommand(sender, parts); @@ -148,7 +146,7 @@ public class CommandMapperTest { // given List parts = asList("authme", "asdfawetawty4asdca"); CommandSender sender = mock(CommandSender.class); - given(permissionsManager.hasPermission(eq(sender), any(CommandDescription.class).getPermission())).willReturn(true); + given(permissionsManager.hasPermission(eq(sender), any(PermissionNode.class))).willReturn(true); // when FoundCommandResult result = mapper.mapPartsToCommand(sender, parts); @@ -166,7 +164,7 @@ public class CommandMapperTest { // given List parts = singletonList("unregister"); CommandSender sender = mock(CommandSender.class); - given(permissionsManager.hasPermission(eq(sender), any(CommandDescription.class).getPermission())).willReturn(true); + given(permissionsManager.hasPermission(eq(sender), any(PermissionNode.class))).willReturn(true); // when FoundCommandResult result = mapper.mapPartsToCommand(sender, parts); @@ -184,7 +182,7 @@ public class CommandMapperTest { // given List parts = asList("bogus", "label1", "arg1"); CommandSender sender = mock(CommandSender.class); - given(permissionsManager.hasPermission(eq(sender), any(CommandDescription.class).getPermission())).willReturn(true); + given(permissionsManager.hasPermission(eq(sender), any(PermissionNode.class))).willReturn(true); // when FoundCommandResult result = mapper.mapPartsToCommand(sender, parts); @@ -209,7 +207,7 @@ public class CommandMapperTest { // given List parts = asList("Unreg", "player1"); CommandSender sender = mock(CommandSender.class); - given(permissionsManager.hasPermission(eq(sender), any(CommandDescription.class).getPermission())).willReturn(true); + given(permissionsManager.hasPermission(eq(sender), any(PermissionNode.class))).willReturn(true); // when FoundCommandResult result = mapper.mapPartsToCommand(sender, parts); @@ -227,7 +225,7 @@ public class CommandMapperTest { // given List parts = asList("unregistER", "player1", "wrongArg"); CommandSender sender = mock(CommandSender.class); - given(permissionsManager.hasPermission(eq(sender), any(CommandDescription.class).getPermission())).willReturn(true); + given(permissionsManager.hasPermission(eq(sender), any(PermissionNode.class))).willReturn(true); // when FoundCommandResult result = mapper.mapPartsToCommand(sender, parts); @@ -245,7 +243,7 @@ public class CommandMapperTest { // given List parts = asList("email", "helptest", "arg1"); CommandSender sender = mock(CommandSender.class); - given(permissionsManager.hasPermission(eq(sender), any(CommandDescription.class).getPermission())).willReturn(true); + given(permissionsManager.hasPermission(eq(sender), any(PermissionNode.class))).willReturn(true); // when FoundCommandResult result = mapper.mapPartsToCommand(sender, parts); @@ -263,7 +261,7 @@ public class CommandMapperTest { // given List parts = asList("authme", "login", "test1"); CommandSender sender = mock(CommandSender.class); - given(permissionsManager.hasPermission(eq(sender), any(CommandDescription.class).getPermission())).willReturn(false); + given(permissionsManager.hasPermission(eq(sender), any(PermissionNode.class))).willReturn(false); // when FoundCommandResult result = mapper.mapPartsToCommand(sender, parts); diff --git a/src/test/java/fr/xephi/authme/command/help/HelpProviderTest.java b/src/test/java/fr/xephi/authme/command/help/HelpProviderTest.java index b6f3c88a..f40350e7 100644 --- a/src/test/java/fr/xephi/authme/command/help/HelpProviderTest.java +++ b/src/test/java/fr/xephi/authme/command/help/HelpProviderTest.java @@ -255,7 +255,7 @@ public class HelpProviderTest { public void shouldHandleUnboundFoundCommandResult() { // given FoundCommandResult result = new FoundCommandResult(null, Arrays.asList("authme", "test"), - Collections. emptyList(), 0.0, FoundResultStatus.UNKNOWN_LABEL); + Collections.emptyList(), 0.0, FoundResultStatus.UNKNOWN_LABEL); // when List lines = helpProvider.printHelp(sender, result, ALL_OPTIONS); @@ -319,7 +319,7 @@ public class HelpProviderTest { * @return The generated FoundCommandResult object */ private static FoundCommandResult newFoundResult(CommandDescription command, List labels) { - return new FoundCommandResult(command, labels, Collections. emptyList(), 0.0, FoundResultStatus.SUCCESS); + return new FoundCommandResult(command, labels, Collections.emptyList(), 0.0, FoundResultStatus.SUCCESS); } private static String removeColors(String str) { diff --git a/src/test/java/fr/xephi/authme/permission/PermissionsManagerTest.java b/src/test/java/fr/xephi/authme/permission/PermissionsManagerTest.java index 225fc899..8ede82b6 100644 --- a/src/test/java/fr/xephi/authme/permission/PermissionsManagerTest.java +++ b/src/test/java/fr/xephi/authme/permission/PermissionsManagerTest.java @@ -4,7 +4,6 @@ import org.bukkit.Server; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.bukkit.plugin.PluginManager; -import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; @@ -19,7 +18,6 @@ import static org.mockito.Mockito.mock; /** * Test for {@link PermissionsManager}. */ -// TODO #739: Unignore tests after they pass @RunWith(MockitoJUnitRunner.class) public class PermissionsManagerTest { @@ -42,7 +40,7 @@ public class PermissionsManagerTest { boolean result = permissionsManager.hasPermission(sender, node); // then - assertThat(result, equalTo(false)); + assertThat(result, equalTo(true)); } @Test @@ -60,8 +58,6 @@ public class PermissionsManagerTest { } @Test - @Ignore - // TODO ljacqu 20160601: This test should pass - tested permission node has DefaultPermission.NOT_ALLOWED public void shouldDenyPermissionEvenForOpCommandSender() { // given PermissionNode node = TestPermissions.WORLD_DOMINATION; @@ -76,8 +72,6 @@ public class PermissionsManagerTest { } @Test - @Ignore - // TODO ljacqu 20160601: This test MUST pass! -> tested node has DefaultPermission.ALLOW public void shouldAllowForNonOpPlayer() { // given PermissionNode node = TestPermissions.LOGIN; @@ -118,8 +112,6 @@ public class PermissionsManagerTest { } @Test - @Ignore - // TODO ljacqu 20160601: This should pass -> tested node has DefaultPermission.NOT_ALLOWED so result should be false public void shouldDenyEvenForOpPlayer() { // given PermissionNode node = TestPermissions.WORLD_DOMINATION; @@ -134,8 +126,6 @@ public class PermissionsManagerTest { } @Test - @Ignore - // TODO ljacqu 20160601: This must pass. null permission => true public void shouldHandleNullPermissionForCommandSender() { // given PermissionNode node = null; @@ -149,8 +139,6 @@ public class PermissionsManagerTest { } @Test - @Ignore - // TODO ljacqu 20160601: This must pass. null permission => true public void shouldHandleNullPermissionForPlayer() { // given PermissionNode node = null; From eafb3b6653de45d88676f4811ed9a91bcd604537 Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Fri, 3 Jun 2016 00:07:25 +0200 Subject: [PATCH 150/200] cleanup --- .../xephi/authme/permission/handlers/ZPermissionsHandler.java | 3 +-- .../fr/xephi/authme/settings/properties/SecuritySettings.java | 1 - .../java/fr/xephi/authme/command/CommandConsistencyTest.java | 1 + .../java/fr/xephi/authme/listener/ListenerServiceTest.java | 1 + 4 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/fr/xephi/authme/permission/handlers/ZPermissionsHandler.java b/src/main/java/fr/xephi/authme/permission/handlers/ZPermissionsHandler.java index 18e433ba..fb2490f7 100644 --- a/src/main/java/fr/xephi/authme/permission/handlers/ZPermissionsHandler.java +++ b/src/main/java/fr/xephi/authme/permission/handlers/ZPermissionsHandler.java @@ -53,10 +53,9 @@ public class ZPermissionsHandler implements PermissionHandler { } @Override - @SuppressWarnings("unchecked") public List getGroups(Player player) { // TODO Gnat008 20160631: Use UUID not name? - return new ArrayList(zPermissionsService.getPlayerGroups(player.getName())); + return new ArrayList(zPermissionsService.getPlayerGroups(player.getName())); } @Override diff --git a/src/main/java/fr/xephi/authme/settings/properties/SecuritySettings.java b/src/main/java/fr/xephi/authme/settings/properties/SecuritySettings.java index f865793b..3419a05c 100644 --- a/src/main/java/fr/xephi/authme/settings/properties/SecuritySettings.java +++ b/src/main/java/fr/xephi/authme/settings/properties/SecuritySettings.java @@ -7,7 +7,6 @@ import fr.xephi.authme.settings.domain.SettingsClass; import java.util.List; -import static fr.xephi.authme.settings.domain.Property.newListProperty; import static fr.xephi.authme.settings.domain.Property.newLowercaseListProperty; import static fr.xephi.authme.settings.domain.Property.newProperty; diff --git a/src/test/java/fr/xephi/authme/command/CommandConsistencyTest.java b/src/test/java/fr/xephi/authme/command/CommandConsistencyTest.java index 94a723b1..bb297268 100644 --- a/src/test/java/fr/xephi/authme/command/CommandConsistencyTest.java +++ b/src/test/java/fr/xephi/authme/command/CommandConsistencyTest.java @@ -57,6 +57,7 @@ public class CommandConsistencyTest { * * @return collection of all base command labels */ + @SuppressWarnings("unchecked") private static Collection> initializeCommands() { AuthMeServiceInitializer injector = mock(AuthMeServiceInitializer.class); given(injector.newInstance(any(Class.class))).willAnswer(new Answer() { diff --git a/src/test/java/fr/xephi/authme/listener/ListenerServiceTest.java b/src/test/java/fr/xephi/authme/listener/ListenerServiceTest.java index c9413e17..6b87ffe1 100644 --- a/src/test/java/fr/xephi/authme/listener/ListenerServiceTest.java +++ b/src/test/java/fr/xephi/authme/listener/ListenerServiceTest.java @@ -49,6 +49,7 @@ public class ListenerServiceTest { @Mock private PlayerCache playerCache; + @SuppressWarnings("rawtypes") @Before public void initializeTestSetup() { given(settings.getProperty(RegistrationSettings.FORCE)).willReturn(true); From 6c0dec887a0ef72dc88b6634c43ecf3d396dab93 Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Fri, 3 Jun 2016 00:27:06 +0200 Subject: [PATCH 151/200] try to fix javadoc --- pom.xml | 3 --- src/main/java/fr/xephi/authme/settings/SpawnLoader.java | 1 + 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 9b2f05cd..edad3fc7 100644 --- a/pom.xml +++ b/pom.xml @@ -282,9 +282,6 @@ maven-javadoc-plugin 2.10.3 - UTF-8 - UTF-8 - true public false diff --git a/src/main/java/fr/xephi/authme/settings/SpawnLoader.java b/src/main/java/fr/xephi/authme/settings/SpawnLoader.java index f2a4cc5c..212e0235 100644 --- a/src/main/java/fr/xephi/authme/settings/SpawnLoader.java +++ b/src/main/java/fr/xephi/authme/settings/SpawnLoader.java @@ -44,6 +44,7 @@ public class SpawnLoader implements SettingsDependent { * @param pluginFolder The AuthMe data folder * @param settings The setting instance * @param pluginHooks The plugin hooks instance + * @param dataSource The plugin auth database instance */ @Inject public SpawnLoader(@DataFolder File pluginFolder, NewSetting settings, PluginHooks pluginHooks, From ca2e75651cc605fd598bc042942b918b4b6feb50 Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Fri, 3 Jun 2016 00:53:24 +0200 Subject: [PATCH 152/200] Fix #734 It should run before the password check --- .../java/fr/xephi/authme/events/AuthMeAsyncPreLoginEvent.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/fr/xephi/authme/events/AuthMeAsyncPreLoginEvent.java b/src/main/java/fr/xephi/authme/events/AuthMeAsyncPreLoginEvent.java index b2a5819a..843cfaf2 100644 --- a/src/main/java/fr/xephi/authme/events/AuthMeAsyncPreLoginEvent.java +++ b/src/main/java/fr/xephi/authme/events/AuthMeAsyncPreLoginEvent.java @@ -5,7 +5,8 @@ import org.bukkit.event.Event; import org.bukkit.event.HandlerList; /** - * This event is called when a player uses the /login command with correct credentials. + * This event is called when a player uses the login command, + * it's fired even when a user does a /login with invalid password. * {@link #setCanLogin(boolean) event.setCanLogin(false)} prevents the player from logging in. */ public class AuthMeAsyncPreLoginEvent extends CustomEvent { From 6549ebbf5ee3d31dc5be4f82247f1fbabc650fea Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Fri, 3 Jun 2016 01:18:54 +0200 Subject: [PATCH 153/200] Should fix #731 --- .../java/fr/xephi/authme/hooks/BungeeCordMessage.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/main/java/fr/xephi/authme/hooks/BungeeCordMessage.java b/src/main/java/fr/xephi/authme/hooks/BungeeCordMessage.java index 26d5847a..22a7ae08 100644 --- a/src/main/java/fr/xephi/authme/hooks/BungeeCordMessage.java +++ b/src/main/java/fr/xephi/authme/hooks/BungeeCordMessage.java @@ -2,6 +2,8 @@ package fr.xephi.authme.hooks; import com.google.common.io.ByteArrayDataInput; import com.google.common.io.ByteStreams; + +import fr.xephi.authme.AuthMe; import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.cache.auth.PlayerAuth; import fr.xephi.authme.cache.auth.PlayerCache; @@ -25,6 +27,9 @@ public class BungeeCordMessage implements PluginMessageListener { @Inject private PlayerCache playerCache; + @Inject + private AuthMe plugin; + BungeeCordMessage() { } @@ -50,6 +55,12 @@ public class BungeeCordMessage implements PluginMessageListener { if ("login".equals(act)) { playerCache.updatePlayer(auth); dataSource.setLogged(name); + //START 03062016 sgdc3: should fix #731 but we need to recode this mess + if (plugin.sessions.containsKey(name)) { + plugin.sessions.get(name).cancel(); + plugin.sessions.remove(name); + } + //END ConsoleLogger.info("Player " + auth.getNickname() + " has logged in from one of your server!"); } else if ("logout".equals(act)) { From 55f7e8097aefbb7bf46aca5302d4384cd9348b4f Mon Sep 17 00:00:00 2001 From: ljacqu Date: Fri, 3 Jun 2016 12:51:49 +0200 Subject: [PATCH 154/200] #743 Add proper error message for "invalid chars in password" - Change password validation to return a ValidationResult object for passing message arguments - Remove wrapping methods in ProcessService and CommandService and use ValidationService directly --- .../xephi/authme/command/CommandService.java | 12 ---- .../authme/ChangePasswordAdminCommand.java | 11 ++- .../authme/RegisterAdminCommand.java | 11 ++- .../changepassword/ChangePasswordCommand.java | 11 ++- .../fr/xephi/authme/output/MessageKey.java | 2 + .../handlers/PermissionHandler.java | 2 + .../xephi/authme/process/ProcessService.java | 11 --- .../process/register/AsyncRegister.java | 11 ++- .../xephi/authme/util/ValidationService.java | 67 ++++++++++++++++--- .../authme/command/CommandServiceTest.java | 15 ----- .../ChangePasswordAdminCommandTest.java | 25 +++++-- .../authme/RegisterAdminCommandTest.java | 34 ++++++---- .../ChangePasswordCommandTest.java | 15 +++-- .../authme/process/ProcessServiceTest.java | 15 ----- .../authme/util/ValidationServiceTest.java | 32 +++++---- 15 files changed, 163 insertions(+), 111 deletions(-) diff --git a/src/main/java/fr/xephi/authme/command/CommandService.java b/src/main/java/fr/xephi/authme/command/CommandService.java index c479f547..acf15056 100644 --- a/src/main/java/fr/xephi/authme/command/CommandService.java +++ b/src/main/java/fr/xephi/authme/command/CommandService.java @@ -104,18 +104,6 @@ public class CommandService { return settings; } - - /** - * Verifies whether a password is valid according to the plugin settings. - * - * @param password the password to verify - * @param username the username the password is associated with - * @return message key with the password error, or {@code null} if password is valid - */ - public MessageKey validatePassword(String password, String username) { - return validationService.validatePassword(password, username); - } - public boolean validateEmail(String email) { return validationService.validateEmail(email); } 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 d2242347..5f35b7df 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 @@ -10,6 +10,8 @@ import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.security.PasswordSecurity; import fr.xephi.authme.security.crypts.HashedPassword; import fr.xephi.authme.util.BukkitService; +import fr.xephi.authme.util.ValidationService; +import fr.xephi.authme.util.ValidationService.ValidationResult; import org.bukkit.command.CommandSender; import javax.inject.Inject; @@ -32,6 +34,9 @@ public class ChangePasswordAdminCommand implements ExecutableCommand { @Inject private BukkitService bukkitService; + @Inject + private ValidationService validationService; + @Override public void executeCommand(final CommandSender sender, List arguments, final CommandService commandService) { @@ -40,9 +45,9 @@ public class ChangePasswordAdminCommand implements ExecutableCommand { final String playerPass = arguments.get(1); // Validate the password - MessageKey passwordError = commandService.validatePassword(playerPass, playerName); - if (passwordError != null) { - commandService.send(sender, passwordError); + ValidationResult validationResult = validationService.validatePassword(playerPass, playerName); + if (validationResult.hasError()) { + commandService.send(sender, validationResult.getMessageKey(), validationResult.getArgs()); 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 1540e465..29218003 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 @@ -9,6 +9,8 @@ import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.security.PasswordSecurity; import fr.xephi.authme.security.crypts.HashedPassword; import fr.xephi.authme.util.BukkitService; +import fr.xephi.authme.util.ValidationService; +import fr.xephi.authme.util.ValidationService.ValidationResult; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; @@ -29,6 +31,9 @@ public class RegisterAdminCommand implements ExecutableCommand { @Inject private BukkitService bukkitService; + @Inject + private ValidationService validationService; + @Override public void executeCommand(final CommandSender sender, List arguments, final CommandService commandService) { @@ -38,9 +43,9 @@ public class RegisterAdminCommand implements ExecutableCommand { final String playerNameLowerCase = playerName.toLowerCase(); // Command logic - MessageKey passwordError = commandService.validatePassword(playerPass, playerName); - if (passwordError != null) { - commandService.send(sender, passwordError); + ValidationResult passwordValidation = validationService.validatePassword(playerPass, playerName); + if (passwordValidation.hasError()) { + commandService.send(sender, passwordValidation.getMessageKey(), passwordValidation.getArgs()); 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 01aea862..8ed7b637 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 @@ -8,6 +8,8 @@ import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.security.PasswordSecurity; import fr.xephi.authme.task.ChangePasswordTask; import fr.xephi.authme.util.BukkitService; +import fr.xephi.authme.util.ValidationService; +import fr.xephi.authme.util.ValidationService.ValidationResult; import org.bukkit.entity.Player; import javax.inject.Inject; @@ -24,6 +26,9 @@ public class ChangePasswordCommand extends PlayerCommand { @Inject private BukkitService bukkitService; + @Inject + private ValidationService validationService; + @Inject // TODO ljacqu 20160531: Remove this once change password task runs as a process (via Management) private PasswordSecurity passwordSecurity; @@ -40,9 +45,9 @@ public class ChangePasswordCommand extends PlayerCommand { } // Make sure the password is allowed - MessageKey passwordError = commandService.validatePassword(newPassword, name); - if (passwordError != null) { - commandService.send(player, passwordError); + ValidationResult passwordValidation = validationService.validatePassword(newPassword, name); + if (passwordValidation.hasError()) { + commandService.send(player, passwordValidation.getMessageKey(), passwordValidation.getArgs()); return; } diff --git a/src/main/java/fr/xephi/authme/output/MessageKey.java b/src/main/java/fr/xephi/authme/output/MessageKey.java index eb7a5238..9cfa960d 100644 --- a/src/main/java/fr/xephi/authme/output/MessageKey.java +++ b/src/main/java/fr/xephi/authme/output/MessageKey.java @@ -63,6 +63,8 @@ public enum MessageKey { PASSWORD_UNSAFE_ERROR("password_error_unsafe"), + PASSWORD_CHARACTERS_ERROR("password_error_chars", "REG_EX"), + SESSION_EXPIRED("invalid_session"), MUST_REGISTER_MESSAGE("reg_only"), diff --git a/src/main/java/fr/xephi/authme/permission/handlers/PermissionHandler.java b/src/main/java/fr/xephi/authme/permission/handlers/PermissionHandler.java index f222dac2..5d467cea 100644 --- a/src/main/java/fr/xephi/authme/permission/handlers/PermissionHandler.java +++ b/src/main/java/fr/xephi/authme/permission/handlers/PermissionHandler.java @@ -5,6 +5,8 @@ import fr.xephi.authme.permission.PermissionsSystemType; import org.bukkit.entity.Player; import java.util.List; + + public interface PermissionHandler { /** diff --git a/src/main/java/fr/xephi/authme/process/ProcessService.java b/src/main/java/fr/xephi/authme/process/ProcessService.java index 753573a9..f171e368 100644 --- a/src/main/java/fr/xephi/authme/process/ProcessService.java +++ b/src/main/java/fr/xephi/authme/process/ProcessService.java @@ -104,17 +104,6 @@ public class ProcessService { pluginManager.callEvent(event); } - /** - * Verifies whether a password is valid according to the plugin settings. - * - * @param password the password to verify - * @param username the username the password is associated with - * @return message key with the password error, or {@code null} if password is valid - */ - public MessageKey validatePassword(String password, String username) { - return validationService.validatePassword(password, username); - } - public boolean validateEmail(String email) { return validationService.validateEmail(email); } diff --git a/src/main/java/fr/xephi/authme/process/register/AsyncRegister.java b/src/main/java/fr/xephi/authme/process/register/AsyncRegister.java index 2d47b5ba..44f0fd0b 100644 --- a/src/main/java/fr/xephi/authme/process/register/AsyncRegister.java +++ b/src/main/java/fr/xephi/authme/process/register/AsyncRegister.java @@ -20,6 +20,8 @@ import fr.xephi.authme.settings.properties.RestrictionSettings; import fr.xephi.authme.settings.properties.SecuritySettings; import fr.xephi.authme.util.StringUtils; import fr.xephi.authme.util.Utils; +import fr.xephi.authme.util.ValidationService; +import fr.xephi.authme.util.ValidationService.ValidationResult; import org.bukkit.Bukkit; import org.bukkit.entity.Player; @@ -51,6 +53,9 @@ public class AsyncRegister implements AsynchronousProcess { @Inject private PermissionsManager permissionsManager; + @Inject + private ValidationService validationService; + AsyncRegister() { } private boolean preRegisterCheck(Player player, String password) { @@ -65,9 +70,9 @@ public class AsyncRegister implements AsynchronousProcess { //check the password safety only if it's not a automatically generated password if (service.getProperty(SecuritySettings.PASSWORD_HASH) != HashAlgorithm.TWO_FACTOR) { - MessageKey passwordError = service.validatePassword(password, player.getName()); - if (passwordError != null) { - service.send(player, passwordError); + ValidationResult passwordValidation = validationService.validatePassword(password, player.getName()); + if (passwordValidation.hasError()) { + service.send(player, passwordValidation.getMessageKey(), passwordValidation.getArgs()); return false; } } diff --git a/src/main/java/fr/xephi/authme/util/ValidationService.java b/src/main/java/fr/xephi/authme/util/ValidationService.java index 40f8fd1c..8e72f852 100644 --- a/src/main/java/fr/xephi/authme/util/ValidationService.java +++ b/src/main/java/fr/xephi/authme/util/ValidationService.java @@ -1,6 +1,7 @@ package fr.xephi.authme.util; import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.initialization.Reloadable; import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.permission.PermissionsManager; import fr.xephi.authme.permission.PlayerStatePermission; @@ -15,21 +16,29 @@ import org.bukkit.command.CommandSender; import javax.inject.Inject; import java.util.Collection; import java.util.List; +import java.util.regex.Pattern; /** * Validation service. */ -public class ValidationService { +public class ValidationService implements Reloadable { private final NewSetting settings; private final DataSource dataSource; private final PermissionsManager permissionsManager; + private Pattern passwordRegex; @Inject public ValidationService(NewSetting settings, DataSource dataSource, PermissionsManager permissionsManager) { this.settings = settings; this.dataSource = dataSource; this.permissionsManager = permissionsManager; + reload(); + } + + @Override + public void reload() { + passwordRegex = Pattern.compile(settings.getProperty(RestrictionSettings.ALLOWED_PASSWORD_REGEX)); } /** @@ -37,21 +46,21 @@ public class ValidationService { * * @param password the password to verify * @param username the username the password is associated with - * @return message key with the password error, or {@code null} if password is valid + * @return the validation result */ - public MessageKey validatePassword(String password, String username) { + public ValidationResult validatePassword(String password, String username) { String passLow = password.toLowerCase(); - if (!passLow.matches(settings.getProperty(RestrictionSettings.ALLOWED_PASSWORD_REGEX))) { - return MessageKey.PASSWORD_MATCH_ERROR; + if (!passwordRegex.matcher(passLow).matches()) { + return new ValidationResult(MessageKey.PASSWORD_CHARACTERS_ERROR, passwordRegex.pattern()); } else if (passLow.equalsIgnoreCase(username)) { - return MessageKey.PASSWORD_IS_USERNAME_ERROR; + return new ValidationResult(MessageKey.PASSWORD_IS_USERNAME_ERROR); } else if (password.length() < settings.getProperty(SecuritySettings.MIN_PASSWORD_LENGTH) || password.length() > settings.getProperty(SecuritySettings.MAX_PASSWORD_LENGTH)) { - return MessageKey.INVALID_PASSWORD_LENGTH; + return new ValidationResult(MessageKey.INVALID_PASSWORD_LENGTH); } else if (settings.getProperty(SecuritySettings.UNSAFE_PASSWORDS).contains(passLow)) { - return MessageKey.PASSWORD_UNSAFE_ERROR; + return new ValidationResult(MessageKey.PASSWORD_UNSAFE_ERROR); } - return null; + return new ValidationResult(); } /** @@ -130,4 +139,44 @@ public class ValidationService { } return false; } + + public static final class ValidationResult { + private final MessageKey messageKey; + private final String[] args; + + /** + * Constructor for a successful validation. + */ + public ValidationResult() { + this.messageKey = null; + this.args = null; + } + + /** + * Constructor for a failed validation. + * + * @param messageKey message key of the validation error + * @param args arguments for the message key + */ + public ValidationResult(MessageKey messageKey, String... args) { + this.messageKey = messageKey; + this.args = args; + } + + /** + * Returns whether an error was found during the validation, i.e. whether the validation failed. + * + * @return true if there is an error, false if the validation was successful + */ + public boolean hasError() { + return messageKey != null; + } + + public MessageKey getMessageKey() { + return messageKey; + } + public String[] getArgs() { + return args; + } + } } diff --git a/src/test/java/fr/xephi/authme/command/CommandServiceTest.java b/src/test/java/fr/xephi/authme/command/CommandServiceTest.java index c07d44d5..334fc416 100644 --- a/src/test/java/fr/xephi/authme/command/CommandServiceTest.java +++ b/src/test/java/fr/xephi/authme/command/CommandServiceTest.java @@ -141,21 +141,6 @@ public class CommandServiceTest { assertThat(result, equalTo(settings)); } - @Test - public void shouldValidatePassword() { - // given - String user = "asdf"; - String password = "mySecret55"; - given(validationService.validatePassword(password, user)).willReturn(MessageKey.INVALID_PASSWORD_LENGTH); - - // when - MessageKey result = commandService.validatePassword(password, user); - - // then - assertThat(result, equalTo(MessageKey.INVALID_PASSWORD_LENGTH)); - verify(validationService).validatePassword(password, user); - } - @Test public void shouldValidateEmail() { // given diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/ChangePasswordAdminCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/ChangePasswordAdminCommandTest.java index 64059b2d..5ab9ba0d 100644 --- a/src/test/java/fr/xephi/authme/command/executable/authme/ChangePasswordAdminCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/authme/ChangePasswordAdminCommandTest.java @@ -9,6 +9,8 @@ import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.security.PasswordSecurity; import fr.xephi.authme.security.crypts.HashedPassword; import fr.xephi.authme.util.BukkitService; +import fr.xephi.authme.util.ValidationService; +import fr.xephi.authme.util.ValidationService.ValidationResult; import org.bukkit.command.CommandSender; import org.junit.BeforeClass; import org.junit.Test; @@ -51,6 +53,9 @@ public class ChangePasswordAdminCommandTest { @Mock private BukkitService bukkitService; + @Mock + private ValidationService validationService; + @BeforeClass public static void setUpLogger() { TestHelper.setupLogger(); @@ -60,14 +65,15 @@ public class ChangePasswordAdminCommandTest { public void shouldRejectInvalidPassword() { // given CommandSender sender = mock(CommandSender.class); - given(service.validatePassword("Bobby", "bobby")).willReturn(MessageKey.PASSWORD_IS_USERNAME_ERROR); + given(validationService.validatePassword("Bobby", "bobby")).willReturn( + new ValidationResult(MessageKey.PASSWORD_IS_USERNAME_ERROR)); // when command.executeCommand(sender, Arrays.asList("bobby", "Bobby"), service); // then - verify(service).validatePassword("Bobby", "bobby"); - verify(service).send(sender, MessageKey.PASSWORD_IS_USERNAME_ERROR); + verify(validationService).validatePassword("Bobby", "bobby"); + verify(service).send(sender, MessageKey.PASSWORD_IS_USERNAME_ERROR, new String[0]); verifyZeroInteractions(dataSource); } @@ -76,11 +82,13 @@ public class ChangePasswordAdminCommandTest { // given CommandSender sender = mock(CommandSender.class); String player = "player"; + String password = "password"; given(playerCache.isAuthenticated(player)).willReturn(false); given(dataSource.getAuth(player)).willReturn(null); + given(validationService.validatePassword(password, player)).willReturn(new ValidationResult()); // when - command.executeCommand(sender, Arrays.asList(player, "password"), service); + command.executeCommand(sender, Arrays.asList(player, password), service); runInnerRunnable(bukkitService); // then @@ -102,13 +110,14 @@ public class ChangePasswordAdminCommandTest { HashedPassword hashedPassword = mock(HashedPassword.class); given(passwordSecurity.computeHash(password, player)).willReturn(hashedPassword); given(dataSource.updatePassword(auth)).willReturn(true); + given(validationService.validatePassword(password, player)).willReturn(new ValidationResult()); // when command.executeCommand(sender, Arrays.asList(player, password), service); runInnerRunnable(bukkitService); // then - verify(service).validatePassword(password, player); + verify(validationService).validatePassword(password, player); verify(service).send(sender, MessageKey.PASSWORD_CHANGED_SUCCESS); verify(passwordSecurity).computeHash(password, player); verify(auth).setPassword(hashedPassword); @@ -126,6 +135,7 @@ public class ChangePasswordAdminCommandTest { given(dataSource.isAuthAvailable(player)).willReturn(true); given(dataSource.getAuth(player)).willReturn(auth); given(dataSource.updatePassword(auth)).willReturn(true); + given(validationService.validatePassword(password, player)).willReturn(new ValidationResult()); HashedPassword hashedPassword = mock(HashedPassword.class); given(passwordSecurity.computeHash(password, player)).willReturn(hashedPassword); @@ -135,7 +145,7 @@ public class ChangePasswordAdminCommandTest { runInnerRunnable(bukkitService); // then - verify(service).validatePassword(password, player); + verify(validationService).validatePassword(password, player); verify(service).send(sender, MessageKey.PASSWORD_CHANGED_SUCCESS); verify(passwordSecurity).computeHash(password, player); verify(auth).setPassword(hashedPassword); @@ -151,6 +161,7 @@ public class ChangePasswordAdminCommandTest { PlayerAuth auth = mock(PlayerAuth.class); given(playerCache.isAuthenticated(player)).willReturn(true); given(playerCache.getAuth(player)).willReturn(auth); + given(validationService.validatePassword(password, player)).willReturn(new ValidationResult()); HashedPassword hashedPassword = mock(HashedPassword.class); given(passwordSecurity.computeHash(password, player)).willReturn(hashedPassword); @@ -161,7 +172,7 @@ public class ChangePasswordAdminCommandTest { runInnerRunnable(bukkitService); // then - verify(service).validatePassword(password, player); + verify(validationService).validatePassword(password, player); verify(service).send(sender, MessageKey.ERROR); verify(passwordSecurity).computeHash(password, player); verify(auth).setPassword(hashedPassword); diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/RegisterAdminCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/RegisterAdminCommandTest.java index 2459bc89..c82b7e0c 100644 --- a/src/test/java/fr/xephi/authme/command/executable/authme/RegisterAdminCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/authme/RegisterAdminCommandTest.java @@ -8,6 +8,8 @@ import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.security.PasswordSecurity; import fr.xephi.authme.security.crypts.HashedPassword; import fr.xephi.authme.util.BukkitService; +import fr.xephi.authme.util.ValidationService; +import fr.xephi.authme.util.ValidationService.ValidationResult; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.junit.BeforeClass; @@ -49,10 +51,10 @@ public class RegisterAdminCommandTest { private BukkitService bukkitService; @Mock - private CommandSender sender; + private CommandService commandService; @Mock - private CommandService commandService; + private ValidationService validationService; @BeforeClass public static void setUpLogger() { @@ -64,14 +66,16 @@ public class RegisterAdminCommandTest { // given String user = "tester"; String password = "myPassword"; - given(commandService.validatePassword(password, user)).willReturn(MessageKey.INVALID_PASSWORD_LENGTH); + given(validationService.validatePassword(password, user)) + .willReturn(new ValidationResult(MessageKey.INVALID_PASSWORD_LENGTH)); + CommandSender sender = mock(CommandSender.class); // when command.executeCommand(sender, Arrays.asList(user, password), commandService); // then - verify(commandService).validatePassword(password, user); - verify(commandService).send(sender, MessageKey.INVALID_PASSWORD_LENGTH); + verify(validationService).validatePassword(password, user); + verify(commandService).send(sender, MessageKey.INVALID_PASSWORD_LENGTH, new String[0]); verify(bukkitService, never()).runTaskAsynchronously(any(Runnable.class)); } @@ -80,15 +84,16 @@ public class RegisterAdminCommandTest { // given String user = "my_name55"; String password = "@some-pass@"; - given(commandService.validatePassword(password, user)).willReturn(null); + given(validationService.validatePassword(password, user)).willReturn(new ValidationResult()); given(dataSource.isAuthAvailable(user)).willReturn(true); + CommandSender sender = mock(CommandSender.class); // when command.executeCommand(sender, Arrays.asList(user, password), commandService); TestHelper.runInnerRunnable(bukkitService); // then - verify(commandService).validatePassword(password, user); + verify(validationService).validatePassword(password, user); verify(commandService).send(sender, MessageKey.NAME_ALREADY_REGISTERED); verify(dataSource, never()).saveAuth(any(PlayerAuth.class)); } @@ -98,18 +103,19 @@ public class RegisterAdminCommandTest { // given String user = "test-test"; String password = "afdjhfkt"; - given(commandService.validatePassword(password, user)).willReturn(null); + given(validationService.validatePassword(password, user)).willReturn(new ValidationResult()); given(dataSource.isAuthAvailable(user)).willReturn(false); given(dataSource.saveAuth(any(PlayerAuth.class))).willReturn(false); HashedPassword hashedPassword = new HashedPassword("235sdf4w5udsgf"); given(passwordSecurity.computeHash(password, user)).willReturn(hashedPassword); + CommandSender sender = mock(CommandSender.class); // when command.executeCommand(sender, Arrays.asList(user, password), commandService); TestHelper.runInnerRunnable(bukkitService); // then - verify(commandService).validatePassword(password, user); + verify(validationService).validatePassword(password, user); verify(commandService).send(sender, MessageKey.ERROR); ArgumentCaptor captor = ArgumentCaptor.forClass(PlayerAuth.class); verify(dataSource).saveAuth(captor.capture()); @@ -121,19 +127,20 @@ public class RegisterAdminCommandTest { // given String user = "someone"; String password = "Al1O3P49S5%"; - given(commandService.validatePassword(password, user)).willReturn(null); + given(validationService.validatePassword(password, user)).willReturn(new ValidationResult()); given(dataSource.isAuthAvailable(user)).willReturn(false); given(dataSource.saveAuth(any(PlayerAuth.class))).willReturn(true); HashedPassword hashedPassword = new HashedPassword("$aea2345EW235dfsa@#R%987048"); given(passwordSecurity.computeHash(password, user)).willReturn(hashedPassword); given(bukkitService.getPlayerExact(user)).willReturn(null); + CommandSender sender = mock(CommandSender.class); // when command.executeCommand(sender, Arrays.asList(user, password), commandService); TestHelper.runInnerRunnable(bukkitService); // then - verify(commandService).validatePassword(password, user); + verify(validationService).validatePassword(password, user); verify(commandService).send(sender, MessageKey.REGISTER_SUCCESS); ArgumentCaptor captor = ArgumentCaptor.forClass(PlayerAuth.class); verify(dataSource).saveAuth(captor.capture()); @@ -146,13 +153,14 @@ public class RegisterAdminCommandTest { // given String user = "someone"; String password = "Al1O3P49S5%"; - given(commandService.validatePassword(password, user)).willReturn(null); + given(validationService.validatePassword(password, user)).willReturn(new ValidationResult()); given(dataSource.isAuthAvailable(user)).willReturn(false); given(dataSource.saveAuth(any(PlayerAuth.class))).willReturn(true); HashedPassword hashedPassword = new HashedPassword("$aea2345EW235dfsa@#R%987048"); given(passwordSecurity.computeHash(password, user)).willReturn(hashedPassword); Player player = mock(Player.class); given(bukkitService.getPlayerExact(user)).willReturn(player); + CommandSender sender = mock(CommandSender.class); // when command.executeCommand(sender, Arrays.asList(user, password), commandService); @@ -160,7 +168,7 @@ public class RegisterAdminCommandTest { runSyncDelayedTask(bukkitService); // then - verify(commandService).validatePassword(password, user); + verify(validationService).validatePassword(password, user); verify(commandService).send(sender, MessageKey.REGISTER_SUCCESS); ArgumentCaptor captor = ArgumentCaptor.forClass(PlayerAuth.class); verify(dataSource).saveAuth(captor.capture()); 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 289af7c6..57b01006 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 @@ -8,6 +8,8 @@ import fr.xephi.authme.settings.properties.RestrictionSettings; import fr.xephi.authme.settings.properties.SecuritySettings; import fr.xephi.authme.task.ChangePasswordTask; import fr.xephi.authme.util.BukkitService; +import fr.xephi.authme.util.ValidationService; +import fr.xephi.authme.util.ValidationService.ValidationResult; import org.bukkit.command.BlockCommandSender; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; @@ -53,6 +55,9 @@ public class ChangePasswordCommandTest { @Mock private BukkitService bukkitService; + @Mock + private ValidationService validationService; + @Before public void setSettings() { when(commandService.getProperty(SecuritySettings.MIN_PASSWORD_LENGTH)).thenReturn(2); @@ -91,26 +96,28 @@ public class ChangePasswordCommandTest { // given CommandSender sender = initPlayerWithName("abc12", true); String password = "newPW"; - given(commandService.validatePassword(password, "abc12")).willReturn(MessageKey.INVALID_PASSWORD_LENGTH); + given(validationService.validatePassword(password, "abc12")) + .willReturn(new ValidationResult(MessageKey.INVALID_PASSWORD_LENGTH)); // when command.executeCommand(sender, Arrays.asList("tester", password), commandService); // then - verify(commandService).validatePassword(password, "abc12"); - verify(commandService).send(sender, MessageKey.INVALID_PASSWORD_LENGTH); + verify(validationService).validatePassword(password, "abc12"); + verify(commandService).send(sender, MessageKey.INVALID_PASSWORD_LENGTH, new String[0]); } @Test public void shouldForwardTheDataForValidPassword() { // given CommandSender sender = initPlayerWithName("parker", true); + given(validationService.validatePassword("abc123", "parker")).willReturn(new ValidationResult()); // when command.executeCommand(sender, Arrays.asList("abc123", "abc123"), commandService); // then - verify(commandService).validatePassword("abc123", "parker"); + verify(validationService).validatePassword("abc123", "parker"); verify(commandService, never()).send(eq(sender), any(MessageKey.class)); ArgumentCaptor taskCaptor = ArgumentCaptor.forClass(ChangePasswordTask.class); verify(bukkitService).runTaskAsynchronously(taskCaptor.capture()); diff --git a/src/test/java/fr/xephi/authme/process/ProcessServiceTest.java b/src/test/java/fr/xephi/authme/process/ProcessServiceTest.java index cff6a6c9..164494a9 100644 --- a/src/test/java/fr/xephi/authme/process/ProcessServiceTest.java +++ b/src/test/java/fr/xephi/authme/process/ProcessServiceTest.java @@ -127,21 +127,6 @@ public class ProcessServiceTest { verify(messages).retrieveSingle(key); } - @Test - public void shouldValidatePassword() { - // given - String user = "test-user"; - String password = "passw0rd"; - given(validationService.validatePassword(password, user)).willReturn(MessageKey.PASSWORD_MATCH_ERROR); - - // when - MessageKey result = processService.validatePassword(password, user); - - // then - assertThat(result, equalTo(MessageKey.PASSWORD_MATCH_ERROR)); - verify(validationService).validatePassword(password, user); - } - @Test public void shouldValidateEmail() { // given diff --git a/src/test/java/fr/xephi/authme/util/ValidationServiceTest.java b/src/test/java/fr/xephi/authme/util/ValidationServiceTest.java index ed784510..0d52f775 100644 --- a/src/test/java/fr/xephi/authme/util/ValidationServiceTest.java +++ b/src/test/java/fr/xephi/authme/util/ValidationServiceTest.java @@ -9,6 +9,7 @@ import fr.xephi.authme.settings.NewSetting; import fr.xephi.authme.settings.properties.EmailSettings; import fr.xephi.authme.settings.properties.RestrictionSettings; import fr.xephi.authme.settings.properties.SecuritySettings; +import fr.xephi.authme.util.ValidationService.ValidationResult; import org.bukkit.command.CommandSender; import org.junit.Before; import org.junit.Test; @@ -20,7 +21,6 @@ import java.util.Arrays; import java.util.Collections; import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.nullValue; import static org.junit.Assert.assertThat; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; @@ -53,56 +53,56 @@ public class ValidationServiceTest { @Test public void shouldRejectPasswordSameAsUsername() { // given/when - MessageKey error = validationService.validatePassword("bobby", "Bobby"); + ValidationResult error = validationService.validatePassword("bobby", "Bobby"); // then - assertThat(error, equalTo(MessageKey.PASSWORD_IS_USERNAME_ERROR)); + assertErrorEquals(error, MessageKey.PASSWORD_IS_USERNAME_ERROR); } @Test public void shouldRejectPasswordNotMatchingPattern() { // given/when // service mock returns pattern a-zA-Z -> numbers should not be accepted - MessageKey error = validationService.validatePassword("invalid1234", "myPlayer"); + ValidationResult error = validationService.validatePassword("invalid1234", "myPlayer"); // then - assertThat(error, equalTo(MessageKey.PASSWORD_MATCH_ERROR)); + assertErrorEquals(error, MessageKey.PASSWORD_CHARACTERS_ERROR, "[a-zA-Z]+"); } @Test public void shouldRejectTooShortPassword() { // given/when - MessageKey error = validationService.validatePassword("ab", "tester"); + ValidationResult error = validationService.validatePassword("ab", "tester"); // then - assertThat(error, equalTo(MessageKey.INVALID_PASSWORD_LENGTH)); + assertErrorEquals(error, MessageKey.INVALID_PASSWORD_LENGTH); } @Test public void shouldRejectTooLongPassword() { // given/when - MessageKey error = validationService.validatePassword(Strings.repeat("a", 30), "player"); + ValidationResult error = validationService.validatePassword(Strings.repeat("a", 30), "player"); // then - assertThat(error, equalTo(MessageKey.INVALID_PASSWORD_LENGTH)); + assertErrorEquals(error, MessageKey.INVALID_PASSWORD_LENGTH); } @Test public void shouldRejectUnsafePassword() { // given/when - MessageKey error = validationService.validatePassword("unsafe", "playertest"); + ValidationResult error = validationService.validatePassword("unsafe", "playertest"); // then - assertThat(error, equalTo(MessageKey.PASSWORD_UNSAFE_ERROR)); + assertErrorEquals(error, MessageKey.PASSWORD_UNSAFE_ERROR); } @Test public void shouldAcceptValidPassword() { // given/when - MessageKey error = validationService.validatePassword("safePass", "some_user"); + ValidationResult error = validationService.validatePassword("safePass", "some_user"); // then - assertThat(error, nullValue()); + assertThat(error.hasError(), equalTo(false)); } @Test @@ -230,4 +230,10 @@ public class ValidationServiceTest { // then assertThat(result, equalTo(true)); } + + private static void assertErrorEquals(ValidationResult validationResult, MessageKey messageKey, String... args) { + assertThat(validationResult.hasError(), equalTo(true)); + assertThat(validationResult.getMessageKey(), equalTo(messageKey)); + assertThat(validationResult.getArgs(), equalTo(args)); + } } From e8717ba07619afa8d24b27c5bd07af7ba4f407c3 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Fri, 3 Jun 2016 12:52:47 +0200 Subject: [PATCH 155/200] Update verification notes in messages files + add entry for #743 invalid chars in password --- src/main/resources/messages/messages_bg.yml | 16 +++--- src/main/resources/messages/messages_br.yml | 8 +-- src/main/resources/messages/messages_cz.yml | 16 +++--- src/main/resources/messages/messages_de.yml | 2 + src/main/resources/messages/messages_en.yml | 5 +- src/main/resources/messages/messages_es.yml | 16 +++--- src/main/resources/messages/messages_eu.yml | 24 +++++---- src/main/resources/messages/messages_fi.yml | 22 ++++---- src/main/resources/messages/messages_fr.yml | 2 + src/main/resources/messages/messages_gl.yml | 16 +++--- src/main/resources/messages/messages_hu.yml | 1 + src/main/resources/messages/messages_id.yml | 16 +++--- src/main/resources/messages/messages_it.yml | 6 ++- src/main/resources/messages/messages_ko.yml | 16 +++--- src/main/resources/messages/messages_lt.yml | 48 +++++++++--------- src/main/resources/messages/messages_nl.yml | 12 +++-- src/main/resources/messages/messages_pl.yml | 22 ++++---- src/main/resources/messages/messages_pt.yml | 16 +++--- src/main/resources/messages/messages_ru.yml | 19 ++++--- src/main/resources/messages/messages_sk.yml | 50 ++++++++++--------- src/main/resources/messages/messages_tr.yml | 5 ++ src/main/resources/messages/messages_uk.yml | 18 ++++--- src/main/resources/messages/messages_vn.yml | 16 +++--- src/main/resources/messages/messages_zhcn.yml | 8 +-- src/main/resources/messages/messages_zhhk.yml | 8 +-- src/main/resources/messages/messages_zhtw.yml | 8 +-- 26 files changed, 225 insertions(+), 171 deletions(-) diff --git a/src/main/resources/messages/messages_bg.yml b/src/main/resources/messages/messages_bg.yml index a7ffcf54..e174e7b9 100644 --- a/src/main/resources/messages/messages_bg.yml +++ b/src/main/resources/messages/messages_bg.yml @@ -12,7 +12,7 @@ login: '&cВход успешен!' vb_nonActiv: '&fТвоята регистрация не е активирана, моля провери своя Имейл!' user_regged: '&cПотребителското име е заето!' usage_reg: '&cКоманда: /register парола парола' -# TODO max_reg: Missing tags %reg_count, %reg_names, %max_acc +# TODO max_reg: Missing tags %reg_count, %max_acc, %reg_names max_reg: '&fТи достигна максималния брой регистрации!' no_perm: '&cНямаш Достъп!' error: '&fПолучи се грешка; Моля свържете се с админ' @@ -54,14 +54,16 @@ email_send: '[AuthMe] Изпраен е имейл !' country_banned: Твоята държава е забранена в този сървър! antibot_auto_enabled: '[AuthMe] AntiBotMod автоматично включен, открита е потенциална атака!' antibot_auto_disabled: '[AuthMe] AntiBotMod автоматично изключване след %m Минути.' +# TODO not_owner_error: 'You are not the owner of this account. Please choose another name!' +# TODO two_factor_create: '&2Your secret code is %code. You can scan it from here %url' # TODO email_already_used: '&4The email address is already being used' -# TODO password_error_nick: '&cYou can''t use your name as password, please choose another one...' +# TODO denied_command: '&cIn order to use this command you must be authenticated!' +# TODO same_ip_online: 'A player with the same IP is already in game!' +# TODO denied_chat: '&cIn order to chat you must be authenticated!' # TODO password_error_unsafe: '&cThe chosen password isn''t safe, please choose another one...' # TODO kick_antibot: 'AntiBot protection mode is enabled! You have to wait some minutes before joining the server.' -# TODO denied_chat: '&cIn order to be able to chat you must be authenticated!' # TODO email_exists: '&cA recovery email was already sent! You can discard it and send a new one using the command below:' -# TODO same_ip_online: 'A player with the same IP is already in game!' -# TODO invalid_name_case: 'You should join using username %valid, not %invalid.' +# TODO password_error_nick: '&cYou can''t use your name as password, please choose another one...' +# TODO password_error_chars: '&4Your password contains illegal characters. Allowed chars: REG_EX' # TODO invalid_session: '&cYour IP has been changed and your session data has expired!' -# TODO two_factor_create: '&2Your secret code is %code. You can scan it from here %url' -# TODO not_owner_error: 'You are not the owner of this account. Please choose another name!' \ No newline at end of file +# TODO invalid_name_case: 'You should join using username %valid, not %invalid.' \ No newline at end of file diff --git a/src/main/resources/messages/messages_br.yml b/src/main/resources/messages/messages_br.yml index 31df2dd1..4f1c2f11 100644 --- a/src/main/resources/messages/messages_br.yml +++ b/src/main/resources/messages/messages_br.yml @@ -14,7 +14,7 @@ login: '&2Logado com sucesso!' vb_nonActiv: '&cSua conta não foi ativada ainda, olhe seu email!' user_regged: '&cVocê já registrou esse nick!' usage_reg: '&cUse: /register ' -# TODO max_reg: Missing tags %reg_count, %reg_names, %max_acc +# TODO max_reg: Missing tags %reg_count, %max_acc, %reg_names max_reg: '&cVocê excedeu a quantidade de registros por ip!' no_perm: '&4Sem permissão!' error: '&4Um erro ocoreu, contate um administrador!' @@ -64,5 +64,7 @@ two_factor_create: '&2Seu código secreto é %code. Você pode escanear ele daqu email_already_used: '&4Este endereço de email já está em uso' not_owner_error: 'Você não é o dono desta conta. Por favor, tente outro nome!' invalid_name_case: 'Você deve entrar usando %valid, não %invalid.' -# TODO denied_chat: '&cIn order to be able to chat you must be authenticated!' -# TODO same_ip_online: 'A player with the same IP is already in game!' \ No newline at end of file +# TODO denied_command: '&cIn order to use this command you must be authenticated!' +# TODO same_ip_online: 'A player with the same IP is already in game!' +# TODO password_error_chars: '&4Your password contains illegal characters. Allowed chars: REG_EX' +# TODO denied_chat: '&cIn order to chat you must be authenticated!' \ No newline at end of file diff --git a/src/main/resources/messages/messages_cz.yml b/src/main/resources/messages/messages_cz.yml index 037a1b05..2937cef7 100644 --- a/src/main/resources/messages/messages_cz.yml +++ b/src/main/resources/messages/messages_cz.yml @@ -26,7 +26,7 @@ reload: '&cZnovu nacteni nastaveni AuthMe probehlo uspesne.' timeout: '&cCas pro prihlaseni vyprsel!' unsafe_spawn: '&cTvoje pozice pri odpojeni byla nebezpecna, teleportuji na spawn!' invalid_session: '&cChybna data pri cteni pockejte do vyprseni.' -# TODO max_reg: Missing tags %reg_count, %reg_names, %max_acc +# TODO max_reg: Missing tags %reg_count, %max_acc, %reg_names max_reg: '&cJiz jsi prekrocil(a) limit pro pocet uctu z jedne IP.' password_error: '&cHesla se neshoduji!' pass_len: '&cTvoje heslo nedosahuje minimalni delky (4).' @@ -54,13 +54,15 @@ email_send: '[AuthMe] Email pro obnoveni hesla odeslan!' country_banned: 'Vase zeme je na tomto serveru zakazana' antibot_auto_enabled: '[AuthMe] AntiBotMod automaticky spusten z duvodu masivnich pripojeni!' antibot_auto_disabled: '[AuthMe] AntiBotMod automaticky ukoncen po %m minutach, doufejme v konec invaze' -# TODO email_already_used: '&4The email address is already being used' # TODO password_error_nick: '&cYou can''t use your name as password, please choose another one...' +# TODO not_owner_error: 'You are not the owner of this account. Please choose another name!' +# TODO two_factor_create: '&2Your secret code is %code. You can scan it from here %url' +# TODO email_already_used: '&4The email address is already being used' +# TODO denied_command: '&cIn order to use this command you must be authenticated!' +# TODO same_ip_online: 'A player with the same IP is already in game!' +# TODO password_error_chars: '&4Your password contains illegal characters. Allowed chars: REG_EX' +# TODO denied_chat: '&cIn order to chat you must be authenticated!' # TODO password_error_unsafe: '&cThe chosen password isn''t safe, please choose another one...' # TODO kick_antibot: 'AntiBot protection mode is enabled! You have to wait some minutes before joining the server.' -# TODO denied_chat: '&cIn order to be able to chat you must be authenticated!' # TODO email_exists: '&cA recovery email was already sent! You can discard it and send a new one using the command below:' -# TODO same_ip_online: 'A player with the same IP is already in game!' -# TODO invalid_name_case: 'You should join using username %valid, not %invalid.' -# TODO two_factor_create: '&2Your secret code is %code. You can scan it from here %url' -# TODO not_owner_error: 'You are not the owner of this account. Please choose another name!' \ No newline at end of file +# TODO invalid_name_case: 'You should join using username %valid, not %invalid.' \ No newline at end of file diff --git a/src/main/resources/messages/messages_de.yml b/src/main/resources/messages/messages_de.yml index 4f5a638a..870bf6a9 100644 --- a/src/main/resources/messages/messages_de.yml +++ b/src/main/resources/messages/messages_de.yml @@ -24,6 +24,7 @@ user_unknown: '&cBenutzername nicht registriert!' password_error: '&cPasswörter stimmen nicht überein!' password_error_nick: '&cDu kannst deinen Namen nicht als Passwort verwenden!' password_error_unsafe: '&cPasswort unsicher! Bitte wähle ein anderes.' +password_error_chars: '&4Dein Passwort enthält unerlaubte Zeichen. Zulässige Zeichen: REG_EX' invalid_session: '&cUngültige Session. Bitte starte das Spiel neu oder warte, bis die Session abgelaufen ist.' reg_only: '&4Nur für registrierte Spieler! Bitte besuche http://example.com um dich zu registrieren.' logged_in: '&cBereits eingeloggt!' @@ -64,3 +65,4 @@ invalid_name_case: 'Dein registrierter Benutzername ist &2%valid&f - nicht &4%in not_owner_error: 'Du bist nicht der Besitzer dieses Accounts. Bitte wähle einen anderen Namen!' denied_chat: '&cDu musst eingeloggt sein, um chatten zu können!' same_ip_online: 'Ein Spieler mit derselben IP ist bereits online!' +# TODO denied_command: '&cIn order to use this command you must be authenticated!' \ No newline at end of file diff --git a/src/main/resources/messages/messages_en.yml b/src/main/resources/messages/messages_en.yml index 75a4fcea..1bb5d3ff 100644 --- a/src/main/resources/messages/messages_en.yml +++ b/src/main/resources/messages/messages_en.yml @@ -1,6 +1,6 @@ -denied_command: '&cIn order to be able to use this command you must be authenticated!' +denied_command: '&cIn order to use this command you must be authenticated!' same_ip_online: 'A player with the same IP is already in game!' -denied_chat: '&cIn order to be able to chat you must be authenticated!' +denied_chat: '&cIn order to chat you must be authenticated!' kick_antibot: 'AntiBot protection mode is enabled! You have to wait some minutes before joining the server.' unknown_user: '&cCan''t find the requested user in the database!' unsafe_spawn: '&cYour quit location was unsafe, you have been teleported to the world''s spawnpoint.' @@ -27,6 +27,7 @@ user_unknown: '&cThis user isn''t registered!' password_error: '&cPasswords didn''t match, check them again!' password_error_nick: '&cYou can''t use your name as password, please choose another one...' password_error_unsafe: '&cThe chosen password isn''t safe, please choose another one...' +password_error_chars: '&4Your password contains illegal characters. Allowed chars: REG_EX' invalid_session: '&cYour IP has been changed and your session data has expired!' reg_only: '&4Only registered users can join the server! Please visit http://example.com to register yourself!' logged_in: '&cYou''re already logged in!' diff --git a/src/main/resources/messages/messages_es.yml b/src/main/resources/messages/messages_es.yml index bebcd95a..da7b4beb 100644 --- a/src/main/resources/messages/messages_es.yml +++ b/src/main/resources/messages/messages_es.yml @@ -12,7 +12,7 @@ login: '&c¡Sesión iniciada!' vb_nonActiv: '&fTu cuenta no está activada aún, ¡revisa tu correo!' user_regged: '&cUsuario ya registrado' usage_reg: '&cUso: /register Contraseña ConfirmarContraseña' -# TODO max_reg: Missing tags %reg_count, %reg_names, %max_acc +# TODO max_reg: Missing tags %reg_count, %max_acc, %reg_names max_reg: '&fHas excedido la cantidad máxima de registros para tu cuenta' no_perm: '&cNo tienes permiso' error: '&fHa ocurrido un error. Por favor contacta al administrador.' @@ -23,6 +23,7 @@ usage_unreg: '&cUso: /unregister contraseña' pwd_changed: '&c¡Contraseña cambiada!' user_unknown: '&cUsuario no registrado' password_error: '&fLas contraseñas no son iguales' +password_error_chars: '&cTu contraseña tiene carácteres no admitidos, los cuales son: REG_EX' invalid_session: '&fLos datos de sesión no corresponden. Por favor espera a terminar la sesión.' reg_only: '&f¡Sólo para jugadores registrados! Por favor visita http://www.example.com/ para registrarte' logged_in: '&c¡Ya has iniciado sesión!' @@ -55,13 +56,14 @@ email_send: '[AuthMe] Correo de recuperación enviado !' country_banned: 'Tu país ha sido baneado de este servidor!' antibot_auto_enabled: '[AuthMe] AntiBotMod activado automáticamente debido a conexiones masivas!' antibot_auto_disabled: '[AuthMe] AntiBotMod desactivado automáticamente luego de %m minutos. Esperamos que haya terminado' -# TODO email_already_used: '&4The email address is already being used' # TODO password_error_nick: '&cYou can''t use your name as password, please choose another one...' +# TODO not_owner_error: 'You are not the owner of this account. Please choose another name!' +# TODO two_factor_create: '&2Your secret code is %code. You can scan it from here %url' +# TODO email_already_used: '&4The email address is already being used' +# TODO denied_command: '&cIn order to use this command you must be authenticated!' +# TODO same_ip_online: 'A player with the same IP is already in game!' +# TODO denied_chat: '&cIn order to chat you must be authenticated!' # TODO password_error_unsafe: '&cThe chosen password isn''t safe, please choose another one...' # TODO kick_antibot: 'AntiBot protection mode is enabled! You have to wait some minutes before joining the server.' -# TODO denied_chat: '&cIn order to be able to chat you must be authenticated!' # TODO email_exists: '&cA recovery email was already sent! You can discard it and send a new one using the command below:' -# TODO same_ip_online: 'A player with the same IP is already in game!' -# TODO invalid_name_case: 'You should join using username %valid, not %invalid.' -# TODO two_factor_create: '&2Your secret code is %code. You can scan it from here %url' -# TODO not_owner_error: 'You are not the owner of this account. Please choose another name!' \ No newline at end of file +# TODO invalid_name_case: 'You should join using username %valid, not %invalid.' \ No newline at end of file diff --git a/src/main/resources/messages/messages_eu.yml b/src/main/resources/messages/messages_eu.yml index 70a2ab20..d761ffec 100644 --- a/src/main/resources/messages/messages_eu.yml +++ b/src/main/resources/messages/messages_eu.yml @@ -11,7 +11,7 @@ login: '&cOngi etorri!' vb_nonActiv: '&fZure kontua aktibatu gabe dago, konfirmatu zure emaila!' user_regged: '&cErabiltzailea dagoeneko erregistratua' usage_reg: '&cErabili: /register pasahitza pasahitza' -# TODO max_reg: Missing tags %reg_count, %reg_names, %max_acc +# TODO max_reg: Missing tags %reg_count, %max_acc, %reg_names max_reg: '&fKontuko 2 erabiltzaile bakarrik izan ditzakezu' no_perm: '&cBaimenik ez' error: '&fErrorea; Mesedez jarri kontaktuan administratzaile batekin' @@ -48,19 +48,21 @@ email_confirm: '[AuthMe] Konfirmatu zure emaila !' email_changed: '[AuthMe] Emaila aldatua!' email_send: '[AuthMe] Berreskuratze emaila bidalita !' country_banned: '[AuthMe] Zure herrialdea blokeatuta dago zerbitzari honetan' +# TODO not_owner_error: 'You are not the owner of this account. Please choose another name!' +# TODO two_factor_create: '&2Your secret code is %code. You can scan it from here %url' +# TODO antibot_auto_enabled: '&4[AntiBotService] AntiBot enabled due to the huge number of connections!' # TODO email_already_used: '&4The email address is already being used' -# TODO password_error_nick: '&cYou can''t use your name as password, please choose another one...' +# TODO denied_command: '&cIn order to use this command you must be authenticated!' +# TODO same_ip_online: 'A player with the same IP is already in game!' +# TODO denied_chat: '&cIn order to chat you must be authenticated!' +# TODO wrong_captcha: '&cWrong captcha, please type "/captcha THE_CAPTCHA" into the chat!' # TODO password_error_unsafe: '&cThe chosen password isn''t safe, please choose another one...' # TODO kick_antibot: 'AntiBot protection mode is enabled! You have to wait some minutes before joining the server.' -# TODO invalid_name_case: 'You should join using username %valid, not %invalid.' +# TODO email_exists: '&cA recovery email was already sent! You can discard it and send a new one using the command below:' +# TODO password_error_nick: '&cYou can''t use your name as password, please choose another one...' +# TODO antibot_auto_disabled: '&2[AntiBotService] AntiBot disabled disabled after %m minutes!' +# TODO password_error_chars: '&4Your password contains illegal characters. Allowed chars: REG_EX' # TODO invalid_session: '&cYour IP has been changed and your session data has expired!' # TODO usage_captcha: '&3To login you have to solve a captcha code, please use the command "/captcha "' -# TODO antibot_auto_disabled: '&2[AntiBotService] AntiBot disabled disabled after %m minutes!' -# TODO denied_chat: '&cIn order to be able to chat you must be authenticated!' -# TODO same_ip_online: 'A player with the same IP is already in game!' -# TODO email_exists: '&cA recovery email was already sent! You can discard it and send a new one using the command below:' -# TODO antibot_auto_enabled: '&4[AntiBotService] AntiBot enabled due to the huge number of connections!' -# TODO wrong_captcha: '&cWrong captcha, please type "/captcha THE_CAPTCHA" into the chat!' # TODO valid_captcha: '&2Captcha code solved correctly!' -# TODO two_factor_create: '&2Your secret code is %code. You can scan it from here %url' -# TODO not_owner_error: 'You are not the owner of this account. Please choose another name!' \ No newline at end of file +# TODO invalid_name_case: 'You should join using username %valid, not %invalid.' \ No newline at end of file diff --git a/src/main/resources/messages/messages_fi.yml b/src/main/resources/messages/messages_fi.yml index b93101b1..2a4beacf 100644 --- a/src/main/resources/messages/messages_fi.yml +++ b/src/main/resources/messages/messages_fi.yml @@ -11,7 +11,7 @@ login: '&cKirjauduit onnistuneesti' vb_nonActiv: '&fKäyttäjäsi ei ole vahvistettu!' user_regged: '&cPelaaja on jo rekisteröity' usage_reg: '&cKäyttötapa: /register salasana salasana' -# TODO max_reg: Missing tags %reg_count, %reg_names, %max_acc +# TODO max_reg: Missing tags %reg_count, %max_acc, %reg_names max_reg: '&fSinulla ei ole oikeuksia tehdä enempää pelaajatilejä!' no_perm: '&cEi oikeuksia' error: '&fVirhe: Ota yhteys palveluntarjoojaan!' @@ -51,16 +51,18 @@ email_added: '[AuthMe] Sähköposti lisätty!' email_confirm: '[AuthMe] Vahvistuta sähköposti!' email_changed: '[AuthMe] Sähköposti vaihdettu!' email_send: '[AuthMe] Palautus sähköposti lähetetty!' -# TODO password_error_nick: '&cYou can''t use your name as password, please choose another one...' +# TODO not_owner_error: 'You are not the owner of this account. Please choose another name!' +# TODO two_factor_create: '&2Your secret code is %code. You can scan it from here %url' +# TODO antibot_auto_enabled: '&4[AntiBotService] AntiBot enabled due to the huge number of connections!' # TODO email_already_used: '&4The email address is already being used' +# TODO denied_command: '&cIn order to use this command you must be authenticated!' +# TODO same_ip_online: 'A player with the same IP is already in game!' +# TODO denied_chat: '&cIn order to chat you must be authenticated!' +# TODO country_banned: '&4Your country is banned from this server!' # TODO password_error_unsafe: '&cThe chosen password isn''t safe, please choose another one...' # TODO kick_antibot: 'AntiBot protection mode is enabled! You have to wait some minutes before joining the server.' -# TODO invalid_name_case: 'You should join using username %valid, not %invalid.' -# TODO antibot_auto_disabled: '&2[AntiBotService] AntiBot disabled disabled after %m minutes!' -# TODO denied_chat: '&cIn order to be able to chat you must be authenticated!' -# TODO same_ip_online: 'A player with the same IP is already in game!' # TODO email_exists: '&cA recovery email was already sent! You can discard it and send a new one using the command below:' -# TODO country_banned: '&4Your country is banned from this server!' -# TODO antibot_auto_enabled: '&4[AntiBotService] AntiBot enabled due to the huge number of connections!' -# TODO two_factor_create: '&2Your secret code is %code. You can scan it from here %url' -# TODO not_owner_error: 'You are not the owner of this account. Please choose another name!' \ No newline at end of file +# TODO password_error_nick: '&cYou can''t use your name as password, please choose another one...' +# TODO antibot_auto_disabled: '&2[AntiBotService] AntiBot disabled disabled after %m minutes!' +# TODO password_error_chars: '&4Your password contains illegal characters. Allowed chars: REG_EX' +# TODO invalid_name_case: 'You should join using username %valid, not %invalid.' \ No newline at end of file diff --git a/src/main/resources/messages/messages_fr.yml b/src/main/resources/messages/messages_fr.yml index 6c7e45b9..09691261 100644 --- a/src/main/resources/messages/messages_fr.yml +++ b/src/main/resources/messages/messages_fr.yml @@ -17,6 +17,7 @@ max_reg: '&fLimite d''enregistrement atteinte pour ce compte' no_perm: '&cVous n''avez pas la permission.' password_error_nick: '&fTu ne peux pas utiliser ton pseudo comme mot de passe.' password_error_unsafe: '&fCe mot de passe n''est pas accepté, choisis en un autre.' +password_error_chars: '&4Ton mot de passe contient des caractères non autorisés. Permis sont : REG_EX' error: '&fUne erreur est apparue, veuillez contacter un administrateur.' login_msg: '&cPour vous connecter, utilisez: /login motdepasse' reg_msg: '&cPour vous inscrire, utilisez "/register motdepasse confirmermotdepasse"' @@ -65,3 +66,4 @@ not_owner_error: 'Vous n''êtes pas le propriétaire de ce compte. Veuillez util invalid_name_case: 'Veuillez vous connecter avec %valid et non pas avec %invalid.' denied_chat: 'Vous devez être connecté pour pouvoir parler !' same_ip_online: 'Un joueur avec la même adresse IP joue déjà !' +# TODO denied_command: '&cIn order to use this command you must be authenticated!' \ No newline at end of file diff --git a/src/main/resources/messages/messages_gl.yml b/src/main/resources/messages/messages_gl.yml index 7294a5d3..01c14292 100644 --- a/src/main/resources/messages/messages_gl.yml +++ b/src/main/resources/messages/messages_gl.yml @@ -12,7 +12,7 @@ login: '&cIdentificación con éxito!' vb_nonActiv: '&fA túa conta aínda non está activada, comproba a túa bandexa de correo!!' user_regged: '&cEse nome de usuario xa está rexistrado' usage_reg: '&cUso: /register contrasinal confirmarContrasinal' -# TODO max_reg: Missing tags %reg_count, %reg_names, %max_acc +# TODO max_reg: Missing tags %reg_count, %max_acc, %reg_names max_reg: '&fExcediches o máximo de rexistros para a túa Conta' no_perm: '&cNon tes o permiso' error: '&fOcurriu un erro; contacta cun administrador' @@ -56,13 +56,15 @@ country_banned: 'O teu país está bloqueado neste servidor' antibot_auto_enabled: '[AuthMe] AntiBotMod conectouse automáticamente debido a conexións masivas!' antibot_auto_disabled: '[AuthMe] AntiBotMod desactivouse automáticamente despois de %m minutos, esperemos que a invasión se detivera' -# TODO email_already_used: '&4The email address is already being used' # TODO password_error_nick: '&cYou can''t use your name as password, please choose another one...' +# TODO not_owner_error: 'You are not the owner of this account. Please choose another name!' +# TODO two_factor_create: '&2Your secret code is %code. You can scan it from here %url' +# TODO email_already_used: '&4The email address is already being used' +# TODO denied_command: '&cIn order to use this command you must be authenticated!' +# TODO same_ip_online: 'A player with the same IP is already in game!' +# TODO password_error_chars: '&4Your password contains illegal characters. Allowed chars: REG_EX' +# TODO denied_chat: '&cIn order to chat you must be authenticated!' # TODO password_error_unsafe: '&cThe chosen password isn''t safe, please choose another one...' # TODO kick_antibot: 'AntiBot protection mode is enabled! You have to wait some minutes before joining the server.' -# TODO denied_chat: '&cIn order to be able to chat you must be authenticated!' # TODO email_exists: '&cA recovery email was already sent! You can discard it and send a new one using the command below:' -# TODO same_ip_online: 'A player with the same IP is already in game!' -# TODO invalid_name_case: 'You should join using username %valid, not %invalid.' -# TODO two_factor_create: '&2Your secret code is %code. You can scan it from here %url' -# TODO not_owner_error: 'You are not the owner of this account. Please choose another name!' \ No newline at end of file +# TODO invalid_name_case: 'You should join using username %valid, not %invalid.' \ No newline at end of file diff --git a/src/main/resources/messages/messages_hu.yml b/src/main/resources/messages/messages_hu.yml index 53c5b52d..326bd2bc 100644 --- a/src/main/resources/messages/messages_hu.yml +++ b/src/main/resources/messages/messages_hu.yml @@ -64,3 +64,4 @@ two_factor_create: '&2A te titkos kódod a következő: %code. Vagy skenneld be denied_chat: '&cAmíg nem vagy bejelentkezve, nem használhatod a csevegőt!' denied_command: '&cAmíg nem vagy bejelentkezve, nem használhatod ezt a parancsot!' same_ip_online: 'Már valaki csatlakozott a szerverhez ezzel az IP címmel!' +# TODO password_error_chars: '&4Your password contains illegal characters. Allowed chars: REG_EX' \ No newline at end of file diff --git a/src/main/resources/messages/messages_id.yml b/src/main/resources/messages/messages_id.yml index f6c17995..88b45933 100644 --- a/src/main/resources/messages/messages_id.yml +++ b/src/main/resources/messages/messages_id.yml @@ -10,7 +10,7 @@ valid_session: '&2Otomatis login, karena sesi masih terhubung.' login: '&2Login berhasil!' vb_nonActiv: '&cAkunmu belum diaktifkan, silahkan periksa email kamu!' user_regged: '&cKamu telah mendaftarkan username ini!' -# TODO max_reg: Missing tags %reg_count, %reg_names, %max_acc +# TODO max_reg: Missing tags %reg_count, %max_acc, %reg_names max_reg: '&Kamu telah mencapai batas maksimum pendaftaran di server ini!' no_perm: '&4Kamu tidak mempunyai izin melakukan ini!' error: '&4Terjadi kesalahan tak dikenal, silahkan hubungi Administrator!' @@ -54,13 +54,15 @@ email_send: '&2Email pemulihan akun telah dikirim! Silahkan periksa kotak masuk email_exists: '&cEmail pemulihan sudah dikirim! kamu bisa membatalkan dan mengirimkan yg baru dengan command dibawah:' antibot_auto_enabled: '&4[AntiBotService] AntiBot diaktifkan dikarenakan banyak koneksi yg diterima!' antibot_auto_disabled: '&2[AntiBotService] AntiBot dimatikan setelah %m menit!' +# TODO not_owner_error: 'You are not the owner of this account. Please choose another name!' +# TODO two_factor_create: '&2Your secret code is %code. You can scan it from here %url' # TODO email_already_used: '&4The email address is already being used' -# TODO kick_antibot: 'AntiBot protection mode is enabled! You have to wait some minutes before joining the server.' -# TODO denied_chat: '&cIn order to be able to chat you must be authenticated!' +# TODO denied_command: '&cIn order to use this command you must be authenticated!' # TODO same_ip_online: 'A player with the same IP is already in game!' -# TODO invalid_name_case: 'You should join using username %valid, not %invalid.' +# TODO password_error_chars: '&4Your password contains illegal characters. Allowed chars: REG_EX' +# TODO denied_chat: '&cIn order to chat you must be authenticated!' # TODO country_banned: '&4Your country is banned from this server!' # TODO usage_unreg: '&cUsage: /unregister ' -# TODO two_factor_create: '&2Your secret code is %code. You can scan it from here %url' -# TODO not_owner_error: 'You are not the owner of this account. Please choose another name!' -# TODO usage_reg: '&cUsage: /register ' \ No newline at end of file +# TODO kick_antibot: 'AntiBot protection mode is enabled! You have to wait some minutes before joining the server.' +# TODO usage_reg: '&cUsage: /register ' +# TODO invalid_name_case: 'You should join using username %valid, not %invalid.' \ No newline at end of file diff --git a/src/main/resources/messages/messages_it.yml b/src/main/resources/messages/messages_it.yml index 5101b820..e4b9b7ae 100644 --- a/src/main/resources/messages/messages_it.yml +++ b/src/main/resources/messages/messages_it.yml @@ -25,6 +25,7 @@ user_unknown: '&cL''utente non ha ancora eseguito la registrazione.' password_error: '&cLe password non corrispondono!' password_error_nick: '&cNon puoi usare il tuo nome utente come password, per favore scegline un''altra...' password_error_unsafe: '&cLa password che hai inserito non è sicura, per favore scegline un''altra...' +password_error_chars: '&4La tua password contiene caratteri non consentiti. I caratteri consentiti sono: REG_EX' invalid_session: '&cIl tuo indirizzo IP è cambiato e la tua sessione è stata terminata!' reg_only: '&4Puoi giocare in questo server solo dopo aver effettuato la registrazione attraverso il sito web! Per favore, vai su http://esempio.it per procedere!' logged_in: '&cHai già eseguito l''autenticazione, non è necessario eseguirla nuovamente!' @@ -62,5 +63,6 @@ email_already_used: '&4L''indirizzo email inserito è già in uso' two_factor_create: '&2Il tuo codice segreto è: &f%code&n&2Puoi anche scannerizzare il codice QR da qui: &f%url' not_owner_error: 'Non sei il proprietario di questo account. Per favore scegli un altro nome!' invalid_name_case: 'Dovresti entrare con questo nome utente: "%valid", al posto di: "%invalid".' -# TODO denied_chat: '&cIn order to be able to chat you must be authenticated!' -# TODO same_ip_online: 'A player with the same IP is already in game!' \ No newline at end of file +# TODO denied_command: '&cIn order to use this command you must be authenticated!' +# TODO same_ip_online: 'A player with the same IP is already in game!' +# TODO denied_chat: '&cIn order to chat you must be authenticated!' \ No newline at end of file diff --git a/src/main/resources/messages/messages_ko.yml b/src/main/resources/messages/messages_ko.yml index 6a9abd44..17dd47c9 100644 --- a/src/main/resources/messages/messages_ko.yml +++ b/src/main/resources/messages/messages_ko.yml @@ -14,7 +14,7 @@ login: '&c성공적인 접속입니다!' vb_nonActiv: '&f당신의 계정은 아직 활성화되어있지 않습니다, 당신의 이메일을 확인해보세요!' user_regged: '&c사용자이름은 이미 가입했습니다' usage_reg: '&c사용법: /register 비밀번호 비밀번호확인' -# TODO max_reg: Missing tags %reg_count, %reg_names, %max_acc +# TODO max_reg: Missing tags %reg_count, %max_acc, %reg_names max_reg: '&f당신은 가입할 수 있는 계정의 최대 한도를 초과했습니다' no_perm: '&c권한이 없습니다' error: '&f오류가 발생했습니다; 관리자에게 문의해주세요' @@ -58,12 +58,14 @@ email_exists: '[AuthMe] 당신의 계정에 이미 이메일이 존재합니다. country_banned: '당신의 국가는 이 서버에서 차단당했습니다' antibot_auto_enabled: '[AuthMe] 봇차단모드가 연결 개수 때문에 자동적으로 활성화됩니다!' antibot_auto_disabled: '[AuthMe] 봇차단모드가 %m 분 후에 자동적으로 비활성화됩니다' -# TODO email_already_used: '&4The email address is already being used' # TODO password_error_nick: '&cYou can''t use your name as password, please choose another one...' +# TODO not_owner_error: 'You are not the owner of this account. Please choose another name!' +# TODO two_factor_create: '&2Your secret code is %code. You can scan it from here %url' +# TODO email_already_used: '&4The email address is already being used' +# TODO denied_command: '&cIn order to use this command you must be authenticated!' +# TODO same_ip_online: 'A player with the same IP is already in game!' +# TODO password_error_chars: '&4Your password contains illegal characters. Allowed chars: REG_EX' +# TODO denied_chat: '&cIn order to chat you must be authenticated!' # TODO password_error_unsafe: '&cThe chosen password isn''t safe, please choose another one...' # TODO kick_antibot: 'AntiBot protection mode is enabled! You have to wait some minutes before joining the server.' -# TODO denied_chat: '&cIn order to be able to chat you must be authenticated!' -# TODO same_ip_online: 'A player with the same IP is already in game!' -# TODO invalid_name_case: 'You should join using username %valid, not %invalid.' -# TODO two_factor_create: '&2Your secret code is %code. You can scan it from here %url' -# TODO not_owner_error: 'You are not the owner of this account. Please choose another name!' \ No newline at end of file +# TODO invalid_name_case: 'You should join using username %valid, not %invalid.' \ No newline at end of file diff --git a/src/main/resources/messages/messages_lt.yml b/src/main/resources/messages/messages_lt.yml index f197cfa1..7c06429f 100644 --- a/src/main/resources/messages/messages_lt.yml +++ b/src/main/resources/messages/messages_lt.yml @@ -11,7 +11,7 @@ login: '&aSekmingai prisijungete' vb_nonActiv: '&aJusu vartotojas nera patvirtintas, patikrinkite el.pasta.' user_regged: '&cVartotojo vardas jau uzregistruotas' usage_reg: '&eNaudojimas: /register slaptazodis pakartotiSlaptazodi' -# TODO max_reg: Missing tags %reg_count, %reg_names, %max_acc +# TODO max_reg: Missing tags %reg_count, %max_acc, %reg_names max_reg: '&cJus pasiekete maksimalu registraciju skaiciu.' no_perm: '&cNera leidimo' error: '&cAtsirado klaida, praneskite adminstratoriui.' @@ -41,26 +41,28 @@ wrong_captcha: '&cNeteisinga Captcha, naudokite : /captcha THE_CAPTCHA' valid_captcha: '&cJusu captcha Teisinga!' kick_forvip: '&cA VIP prisijunge i pilna serveri!' kick_fullserver: '&cServeris yra pilnas, Atsiprasome.' -# TODO email_already_used: '&4The email address is already being used' -# TODO password_error_nick: '&cYou can''t use your name as password, please choose another one...' -# TODO usage_email_change: '&cUsage: /email change ' -# TODO password_error_unsafe: '&cThe chosen password isn''t safe, please choose another one...' -# TODO new_email_invalid: '&cInvalid new email, try again!' -# TODO kick_antibot: 'AntiBot protection mode is enabled! You have to wait some minutes before joining the server.' -# TODO invalid_name_case: 'You should join using username %valid, not %invalid.' -# TODO email_send: '&2Recovery email sent successfully! Please check your email inbox!' -# TODO email_confirm: '&cPlease confirm your email address!' -# TODO usage_email_recovery: '&cUsage: /email recovery ' -# TODO email_changed: '&2Email address changed correctly!' -# TODO old_email_invalid: '&cInvalid old email, try again!' -# TODO antibot_auto_disabled: '&2[AntiBotService] AntiBot disabled disabled after %m minutes!' -# TODO email_added: '&2Email address successfully added to your account!' -# TODO denied_chat: '&cIn order to be able to chat you must be authenticated!' -# TODO email_exists: '&cA recovery email was already sent! You can discard it and send a new one using the command below:' -# TODO same_ip_online: 'A player with the same IP is already in game!' -# TODO country_banned: '&4Your country is banned from this server!' -# TODO antibot_auto_enabled: '&4[AntiBotService] AntiBot enabled due to the huge number of connections!' -# TODO usage_email_add: '&cUsage: /email add ' -# TODO email_invalid: '&cInvalid email address, try again!' # TODO two_factor_create: '&2Your secret code is %code. You can scan it from here %url' -# TODO not_owner_error: 'You are not the owner of this account. Please choose another name!' \ No newline at end of file +# TODO email_changed: '&2Email address changed correctly!' +# TODO antibot_auto_enabled: '&4[AntiBotService] AntiBot enabled due to the huge number of connections!' +# TODO new_email_invalid: '&cInvalid new email, try again!' +# TODO email_already_used: '&4The email address is already being used' +# TODO denied_command: '&cIn order to use this command you must be authenticated!' +# TODO same_ip_online: 'A player with the same IP is already in game!' +# TODO denied_chat: '&cIn order to chat you must be authenticated!' +# TODO usage_email_recovery: '&cUsage: /email recovery ' +# TODO country_banned: '&4Your country is banned from this server!' +# TODO usage_email_add: '&cUsage: /email add ' +# TODO password_error_unsafe: '&cThe chosen password isn''t safe, please choose another one...' +# TODO email_invalid: '&cInvalid email address, try again!' +# TODO kick_antibot: 'AntiBot protection mode is enabled! You have to wait some minutes before joining the server.' +# TODO email_added: '&2Email address successfully added to your account!' +# TODO antibot_auto_disabled: '&2[AntiBotService] AntiBot disabled disabled after %m minutes!' +# TODO email_send: '&2Recovery email sent successfully! Please check your email inbox!' +# TODO password_error_chars: '&4Your password contains illegal characters. Allowed chars: REG_EX' +# TODO usage_email_change: '&cUsage: /email change ' +# TODO invalid_name_case: 'You should join using username %valid, not %invalid.' +# TODO not_owner_error: 'You are not the owner of this account. Please choose another name!' +# TODO email_confirm: '&cPlease confirm your email address!' +# TODO email_exists: '&cA recovery email was already sent! You can discard it and send a new one using the command below:' +# TODO password_error_nick: '&cYou can''t use your name as password, please choose another one...' +# TODO old_email_invalid: '&cInvalid old email, try again!' \ No newline at end of file diff --git a/src/main/resources/messages/messages_nl.yml b/src/main/resources/messages/messages_nl.yml index 6c7e9465..db7a9baf 100644 --- a/src/main/resources/messages/messages_nl.yml +++ b/src/main/resources/messages/messages_nl.yml @@ -13,7 +13,7 @@ password_error_unsafe: '&fJe kunt geen onveilige wachtwoorden gebruiken' vb_nonActiv: Je accound is nog niet geactiveerd, controleer je mailbox! user_regged: '&cGebruikersnaam is al geregistreerd' usage_reg: '&cGebruik: /register ' -# TODO max_reg: Missing tags %reg_count, %reg_names, %max_acc +# TODO max_reg: Missing tags %reg_count, %max_acc, %reg_names max_reg: Je hebt de maximale registraties van jouw account overschreden. no_perm: '&cGeen toegang!' error: 'Error: neem contact op met een administrator!' @@ -23,6 +23,7 @@ usage_unreg: '&cGebruik: /unregister password' pwd_changed: '&cWachtwoord aangepast!' user_unknown: '&cGebruikersnaam niet geregistreerd' password_error: Wachtwoord incorrect! +password_error_chars: '&cJouw wachtwoord bevat illegale tekens. Toegestaane karakters: REG_EX' invalid_session: Sessie beschadigd, wacht tot de sessie is verlopen en verbindt opnieuw. reg_only: Alleen voor geregistreerde spelers! Bezoek http://example.com om te registreren logged_in: '&cJe bent al ingelogd!' @@ -58,10 +59,11 @@ antibot_auto_disabled: '[AuthMe] AntiBotMod automatisch uitgezet na %m minuten, kick_antibot: 'AntiBot is aangezet! Wacht alsjeblieft enkele minuten voor je met de server verbindt.' # TODO two_factor_create: Missing tag %url two_factor_create: '&2Je geheime code is %code' +# TODO not_owner_error: 'You are not the owner of this account. Please choose another name!' # TODO email_already_used: '&4The email address is already being used' -# TODO denied_chat: '&cIn order to be able to chat you must be authenticated!' -# TODO email_exists: '&cA recovery email was already sent! You can discard it and send a new one using the command below:' +# TODO denied_command: '&cIn order to use this command you must be authenticated!' # TODO same_ip_online: 'A player with the same IP is already in game!' -# TODO invalid_name_case: 'You should join using username %valid, not %invalid.' # TODO reg_email_msg: '&3Please, register to the server with the command "/register "' -# TODO not_owner_error: 'You are not the owner of this account. Please choose another name!' \ No newline at end of file +# TODO denied_chat: '&cIn order to chat you must be authenticated!' +# TODO email_exists: '&cA recovery email was already sent! You can discard it and send a new one using the command below:' +# TODO invalid_name_case: 'You should join using username %valid, not %invalid.' \ No newline at end of file diff --git a/src/main/resources/messages/messages_pl.yml b/src/main/resources/messages/messages_pl.yml index 578fede7..7630ca94 100644 --- a/src/main/resources/messages/messages_pl.yml +++ b/src/main/resources/messages/messages_pl.yml @@ -26,7 +26,7 @@ error: '&fBlad prosimy napisac do aministracji' unknown_user: '&fUzytkownika nie ma w bazie danych' unsafe_spawn: '&fTwoje pozycja jest niebezpieczna. Zostaniesz przeniesiony na bezpieczny spawn.' invalid_session: '&fSesja zakonczona!' -# TODO max_reg: Missing tags %reg_count, %reg_names, %max_acc +# TODO max_reg: Missing tags %reg_count, %max_acc, %reg_names max_reg: '&fPrzekroczyles limit zarejestrowanych kont na serwerze.' password_error: '&fHaslo niepoprawne!' pass_len: '&fTwoje haslo jest za krotkie lub za dlugie! Sprobuj ponownie...' @@ -51,16 +51,18 @@ email_added: '[AuthMe] Email dodany!' email_confirm: '[AuthMe] Potwierdz swoj email!' email_changed: '[AuthMe] Email zmieniony!' email_send: '[AuthMe] Email z odzyskaniem wyslany!' -# TODO password_error_nick: '&cYou can''t use your name as password, please choose another one...' +# TODO not_owner_error: 'You are not the owner of this account. Please choose another name!' +# TODO two_factor_create: '&2Your secret code is %code. You can scan it from here %url' +# TODO antibot_auto_enabled: '&4[AntiBotService] AntiBot enabled due to the huge number of connections!' # TODO email_already_used: '&4The email address is already being used' +# TODO denied_command: '&cIn order to use this command you must be authenticated!' +# TODO same_ip_online: 'A player with the same IP is already in game!' +# TODO denied_chat: '&cIn order to chat you must be authenticated!' +# TODO country_banned: '&4Your country is banned from this server!' # TODO password_error_unsafe: '&cThe chosen password isn''t safe, please choose another one...' # TODO kick_antibot: 'AntiBot protection mode is enabled! You have to wait some minutes before joining the server.' -# TODO invalid_name_case: 'You should join using username %valid, not %invalid.' -# TODO antibot_auto_disabled: '&2[AntiBotService] AntiBot disabled disabled after %m minutes!' -# TODO denied_chat: '&cIn order to be able to chat you must be authenticated!' -# TODO same_ip_online: 'A player with the same IP is already in game!' # TODO email_exists: '&cA recovery email was already sent! You can discard it and send a new one using the command below:' -# TODO country_banned: '&4Your country is banned from this server!' -# TODO antibot_auto_enabled: '&4[AntiBotService] AntiBot enabled due to the huge number of connections!' -# TODO two_factor_create: '&2Your secret code is %code. You can scan it from here %url' -# TODO not_owner_error: 'You are not the owner of this account. Please choose another name!' \ No newline at end of file +# TODO password_error_nick: '&cYou can''t use your name as password, please choose another one...' +# TODO antibot_auto_disabled: '&2[AntiBotService] AntiBot disabled disabled after %m minutes!' +# TODO password_error_chars: '&4Your password contains illegal characters. Allowed chars: REG_EX' +# TODO invalid_name_case: 'You should join using username %valid, not %invalid.' \ No newline at end of file diff --git a/src/main/resources/messages/messages_pt.yml b/src/main/resources/messages/messages_pt.yml index 7a8fa465..d35ab6b9 100644 --- a/src/main/resources/messages/messages_pt.yml +++ b/src/main/resources/messages/messages_pt.yml @@ -11,7 +11,7 @@ login: '&cAutenticado com sucesso!' vb_nonActiv: '&fA sua conta não foi ainda activada, verifique o seu email onde irá receber indicações para activação de conta. ' user_regged: '&cUtilizador já registado' usage_reg: '&cUse: /register seu@email.com seu@email.com' -# TODO max_reg: Missing tags %reg_count, %reg_names, %max_acc +# TODO max_reg: Missing tags %reg_count, %max_acc, %reg_names max_reg: '&cAtingiu o numero máximo de registos permitidos' no_perm: '&cSem Permissões' error: '&fOcorreu um erro; Por favor contacte um admin' @@ -55,13 +55,15 @@ email_send: 'Nova palavra-passe enviada para o seu email!' country_banned: 'O seu país está banido deste servidor' antibot_auto_enabled: '[AuthMe] AntiBotMod activado automaticamente devido a um aumento anormal de tentativas de ligação!' antibot_auto_disabled: '[AuthMe] AntiBotMod desactivado automaticamente após %m minutos, esperamos que a invasão tenha parado' -# TODO email_already_used: '&4The email address is already being used' # TODO password_error_nick: '&cYou can''t use your name as password, please choose another one...' +# TODO not_owner_error: 'You are not the owner of this account. Please choose another name!' +# TODO two_factor_create: '&2Your secret code is %code. You can scan it from here %url' +# TODO email_already_used: '&4The email address is already being used' +# TODO denied_command: '&cIn order to use this command you must be authenticated!' +# TODO same_ip_online: 'A player with the same IP is already in game!' +# TODO password_error_chars: '&4Your password contains illegal characters. Allowed chars: REG_EX' +# TODO denied_chat: '&cIn order to chat you must be authenticated!' # TODO password_error_unsafe: '&cThe chosen password isn''t safe, please choose another one...' # TODO kick_antibot: 'AntiBot protection mode is enabled! You have to wait some minutes before joining the server.' -# TODO denied_chat: '&cIn order to be able to chat you must be authenticated!' # TODO email_exists: '&cA recovery email was already sent! You can discard it and send a new one using the command below:' -# TODO same_ip_online: 'A player with the same IP is already in game!' -# TODO invalid_name_case: 'You should join using username %valid, not %invalid.' -# TODO two_factor_create: '&2Your secret code is %code. You can scan it from here %url' -# TODO not_owner_error: 'You are not the owner of this account. Please choose another name!' \ No newline at end of file +# TODO invalid_name_case: 'You should join using username %valid, not %invalid.' \ No newline at end of file diff --git a/src/main/resources/messages/messages_ru.yml b/src/main/resources/messages/messages_ru.yml index b77b126a..b615d868 100644 --- a/src/main/resources/messages/messages_ru.yml +++ b/src/main/resources/messages/messages_ru.yml @@ -11,7 +11,7 @@ login: '&a&lВы успешно вошли!' vb_nonActiv: '&6Ваш аккаунт еще не активирован! Проверьте вашу почту!' user_regged: '&c&lТакой игрок уже зарегистрирован' usage_reg: '&c&lИспользование: &e&l/reg ПАРОЛЬ ПОВТОР_ПАРОЛЯ' -# TODO max_reg: Missing tags %reg_count, %reg_names, %max_acc +# TODO max_reg: Missing tags %reg_count, %max_acc, %reg_names max_reg: '&c&lВы превысили макс количество регистраций на ваш IP' no_perm: '&c&lНедостаточно прав' error: '&c&lПроизошла ошибка. Свяжитесь с администратором' @@ -19,6 +19,7 @@ login_msg: '&a&lАвторизация: &e&l/login ПАРОЛЬ' reg_msg: '&a&lРегистрация: &e&l/reg ПАРОЛЬ ПОВТОР_ПАРОЛЯ' password_error_nick: '&c&lВы не можете использовать ваш ник в роли пароля' password_error_unsafe: '&c&lВы не можете использовать небезопасный пароль' +regex: '&c&lВаш пароль содержит запрещенные символы. Разрешенные символы: REG_EX' reg_email_msg: '&c&lРегистрация: &e&l/reg EMAIL ПОВТОР_EMAIL' usage_unreg: '&c&lИспользование: &e&l/unregister ПАРОЛЬ' pwd_changed: '&2Пароль изменен!' @@ -56,11 +57,13 @@ email_send: '[AuthMe] Письмо с инструкциями для восст country_banned: 'Вход с IP-адресов вашей страны воспрещен на этом сервере' antibot_auto_enabled: '&a[AuthMe] AntiBot-режим автоматически включен из-за большого количества входов!' antibot_auto_disabled: '&a[AuthMe] AntiBot-режим автоматичски отключен после %m мин. Надеюсь атака закончилась' -# TODO email_already_used: '&4The email address is already being used' -# TODO kick_antibot: 'AntiBot protection mode is enabled! You have to wait some minutes before joining the server.' -# TODO denied_chat: '&cIn order to be able to chat you must be authenticated!' -# TODO email_exists: '&cA recovery email was already sent! You can discard it and send a new one using the command below:' -# TODO same_ip_online: 'A player with the same IP is already in game!' -# TODO invalid_name_case: 'You should join using username %valid, not %invalid.' +# TODO not_owner_error: 'You are not the owner of this account. Please choose another name!' # TODO two_factor_create: '&2Your secret code is %code. You can scan it from here %url' -# TODO not_owner_error: 'You are not the owner of this account. Please choose another name!' \ No newline at end of file +# TODO email_already_used: '&4The email address is already being used' +# TODO denied_command: '&cIn order to use this command you must be authenticated!' +# TODO same_ip_online: 'A player with the same IP is already in game!' +# TODO password_error_chars: '&4Your password contains illegal characters. Allowed chars: REG_EX' +# TODO denied_chat: '&cIn order to chat you must be authenticated!' +# TODO kick_antibot: 'AntiBot protection mode is enabled! You have to wait some minutes before joining the server.' +# TODO email_exists: '&cA recovery email was already sent! You can discard it and send a new one using the command below:' +# TODO invalid_name_case: 'You should join using username %valid, not %invalid.' \ No newline at end of file diff --git a/src/main/resources/messages/messages_sk.yml b/src/main/resources/messages/messages_sk.yml index ab26f0c6..56adb408 100644 --- a/src/main/resources/messages/messages_sk.yml +++ b/src/main/resources/messages/messages_sk.yml @@ -29,7 +29,7 @@ error: '&fNastala chyba; Kontaktujte administrátora' unknown_user: '&fHrac nie je v databázi' unsafe_spawn: '&fTvoj pozícia bol nebezpecná, teleportujem hraca na spawn' invalid_session: '&fZapamätane casove data nie su doveryhodne. Cakaj na ukoncenie spojenia' -# TODO max_reg: Missing tags %reg_count, %reg_names, %max_acc +# TODO max_reg: Missing tags %reg_count, %max_acc, %reg_names max_reg: '&fDosiahol si maximum registrovanych uctov.' password_error: '&fHeslá sa nezhodujú' pass_len: '&fHeslo je velmi kratke alebo dlhe' @@ -39,31 +39,33 @@ name_len: '&cTvoje meno je velmi krátke alebo dlhé' regex: '&cTvoje meno obsahuje zakázané znaky. Povolené znaky: REG_EX' add_email: '&cPridaj svoj e-mail príkazom "/email add email zopakujEmail"' recovery_email: '&cZabudol si heslo? Pouzi príkaz /email recovery ' -# TODO email_already_used: '&4The email address is already being used' -# TODO usage_email_change: '&cUsage: /email change ' -# TODO password_error_nick: '&cYou can''t use your name as password, please choose another one...' -# TODO new_email_invalid: '&cInvalid new email, try again!' -# TODO kick_antibot: 'AntiBot protection mode is enabled! You have to wait some minutes before joining the server.' -# TODO old_email_invalid: '&cInvalid old email, try again!' +# TODO two_factor_create: '&2Your secret code is %code. You can scan it from here %url' # TODO email_changed: '&2Email address changed correctly!' -# TODO antibot_auto_disabled: '&2[AntiBotService] AntiBot disabled disabled after %m minutes!' +# TODO antibot_auto_enabled: '&4[AntiBotService] AntiBot enabled due to the huge number of connections!' +# TODO new_email_invalid: '&cInvalid new email, try again!' +# TODO email_already_used: '&4The email address is already being used' +# TODO denied_command: '&cIn order to use this command you must be authenticated!' +# TODO same_ip_online: 'A player with the same IP is already in game!' +# TODO denied_chat: '&cIn order to chat you must be authenticated!' +# TODO kick_fullserver: '&4The server is full, try again later!' +# TODO usage_email_recovery: '&cUsage: /email recovery ' # TODO country_banned: '&4Your country is banned from this server!' # TODO usage_email_add: '&cUsage: /email add ' -# TODO wrong_captcha: '&cWrong captcha, please type "/captcha THE_CAPTCHA" into the chat!' -# TODO valid_captcha: '&2Captcha code solved correctly!' -# TODO two_factor_create: '&2Your secret code is %code. You can scan it from here %url' # TODO password_error_unsafe: '&cThe chosen password isn''t safe, please choose another one...' -# TODO invalid_name_case: 'You should join using username %valid, not %invalid.' -# TODO email_send: '&2Recovery email sent successfully! Please check your email inbox!' -# TODO usage_captcha: '&3To login you have to solve a captcha code, please use the command "/captcha "' -# TODO usage_email_recovery: '&cUsage: /email recovery ' -# TODO email_confirm: '&cPlease confirm your email address!' -# TODO kick_fullserver: '&4The server is full, try again later!' -# TODO email_added: '&2Email address successfully added to your account!' -# TODO denied_chat: '&cIn order to be able to chat you must be authenticated!' -# TODO same_ip_online: 'A player with the same IP is already in game!' -# TODO email_exists: '&cA recovery email was already sent! You can discard it and send a new one using the command below:' -# TODO kick_forvip: '&3A VIP player has joined the server when it was full!' # TODO email_invalid: '&cInvalid email address, try again!' -# TODO antibot_auto_enabled: '&4[AntiBotService] AntiBot enabled due to the huge number of connections!' -# TODO not_owner_error: 'You are not the owner of this account. Please choose another name!' \ No newline at end of file +# TODO kick_antibot: 'AntiBot protection mode is enabled! You have to wait some minutes before joining the server.' +# TODO email_added: '&2Email address successfully added to your account!' +# TODO antibot_auto_disabled: '&2[AntiBotService] AntiBot disabled disabled after %m minutes!' +# TODO email_send: '&2Recovery email sent successfully! Please check your email inbox!' +# TODO password_error_chars: '&4Your password contains illegal characters. Allowed chars: REG_EX' +# TODO kick_forvip: '&3A VIP player has joined the server when it was full!' +# TODO usage_email_change: '&cUsage: /email change ' +# TODO invalid_name_case: 'You should join using username %valid, not %invalid.' +# TODO not_owner_error: 'You are not the owner of this account. Please choose another name!' +# TODO email_confirm: '&cPlease confirm your email address!' +# TODO wrong_captcha: '&cWrong captcha, please type "/captcha THE_CAPTCHA" into the chat!' +# TODO email_exists: '&cA recovery email was already sent! You can discard it and send a new one using the command below:' +# TODO password_error_nick: '&cYou can''t use your name as password, please choose another one...' +# TODO old_email_invalid: '&cInvalid old email, try again!' +# TODO usage_captcha: '&3To login you have to solve a captcha code, please use the command "/captcha "' +# TODO valid_captcha: '&2Captcha code solved correctly!' \ No newline at end of file diff --git a/src/main/resources/messages/messages_tr.yml b/src/main/resources/messages/messages_tr.yml index c8f8135b..2f1992ef 100644 --- a/src/main/resources/messages/messages_tr.yml +++ b/src/main/resources/messages/messages_tr.yml @@ -38,6 +38,7 @@ name_len: '&4Senin ismin ya cok kisa yada cok uzun!' regex: '&4Senin isminde uygunsuz karakterler bulunmakta. Izin verilen karakterler: REG_EX' add_email: '&3Lutfen hesabinize eposta adresinizi komut ile ekleyin "/email add "' recovery_email: '&3Sifreni mi unuttun ? Komut kullanarak ogrenebilirsin "/email recovery "' +# TODO usage_captcha: Missing tag usage_captcha: '&3Giris yapmak icin guvenlik kodunu komut yazarak girin "/captcha "' wrong_captcha: '&cYanlis guvenlik kodu, kullanim sekli "/captcha THE_CAPTCHA" sohbete yazin!' valid_captcha: '&2Guvenlik kodu dogrulandi!' @@ -61,3 +62,7 @@ email_already_used: '&4Eposta adresi zaten kullaniliyor.' two_factor_create: '&2Gizli kodunuz %code. Buradan test edebilirsin, %url' not_owner_error: 'Bu hesabin sahibi degilsin. Lutfen farkli bir isim sec!' invalid_name_case: 'Oyuna %valid isminde katilmalisin. %invalid ismini kullanarak katilamazsin.' +# TODO denied_command: '&cIn order to use this command you must be authenticated!' +# TODO same_ip_online: 'A player with the same IP is already in game!' +# TODO password_error_chars: '&4Your password contains illegal characters. Allowed chars: REG_EX' +# TODO denied_chat: '&cIn order to chat you must be authenticated!' \ No newline at end of file diff --git a/src/main/resources/messages/messages_uk.yml b/src/main/resources/messages/messages_uk.yml index 28846301..6c84e72a 100644 --- a/src/main/resources/messages/messages_uk.yml +++ b/src/main/resources/messages/messages_uk.yml @@ -11,7 +11,7 @@ login: '&2Успішна авторизація!' vb_nonActiv: '&fВаш акаунт не активований. Перевірте свою електронну адресу!' user_regged: '&cТакий користувач вже зареєстрований' usage_reg: '&cВикористовуйте: /reg Пароль Повтор_Пароля' -# TODO max_reg: Missing tags %reg_count, %reg_names, %max_acc +# TODO max_reg: Missing tags %reg_count, %max_acc, %reg_names max_reg: '&fВи перевищили максимальне число реєстрацій на ваш IP' no_perm: '&cУ Вас недостатньо прав' error: '&fЩось пішло не так; Будь ласка зв`яжіться з адміністратором' @@ -57,11 +57,13 @@ country_banned: 'Сервер не доступний для вашої краї antibot_auto_enabled: '[AuthMe] AntiBotMod автоматично увімкнений (забагато одначасних з`єднань)!' # TODO antibot_auto_disabled: Missing tag %m antibot_auto_disabled: '[AuthMe] AntiBotMod автоматично вимкнувся, сподіваємось атака зупинена' -# TODO email_already_used: '&4The email address is already being used' -# TODO kick_antibot: 'AntiBot protection mode is enabled! You have to wait some minutes before joining the server.' -# TODO denied_chat: '&cIn order to be able to chat you must be authenticated!' -# TODO email_exists: '&cA recovery email was already sent! You can discard it and send a new one using the command below:' -# TODO same_ip_online: 'A player with the same IP is already in game!' -# TODO invalid_name_case: 'You should join using username %valid, not %invalid.' +# TODO not_owner_error: 'You are not the owner of this account. Please choose another name!' # TODO two_factor_create: '&2Your secret code is %code. You can scan it from here %url' -# TODO not_owner_error: 'You are not the owner of this account. Please choose another name!' \ No newline at end of file +# TODO email_already_used: '&4The email address is already being used' +# TODO denied_command: '&cIn order to use this command you must be authenticated!' +# TODO same_ip_online: 'A player with the same IP is already in game!' +# TODO password_error_chars: '&4Your password contains illegal characters. Allowed chars: REG_EX' +# TODO denied_chat: '&cIn order to chat you must be authenticated!' +# TODO kick_antibot: 'AntiBot protection mode is enabled! You have to wait some minutes before joining the server.' +# TODO email_exists: '&cA recovery email was already sent! You can discard it and send a new one using the command below:' +# TODO invalid_name_case: 'You should join using username %valid, not %invalid.' \ No newline at end of file diff --git a/src/main/resources/messages/messages_vn.yml b/src/main/resources/messages/messages_vn.yml index 6d8ebbd4..b65b32fe 100644 --- a/src/main/resources/messages/messages_vn.yml +++ b/src/main/resources/messages/messages_vn.yml @@ -11,7 +11,7 @@ login: '&cĐăng nhập thành công!' vb_nonActiv: '&fTài khoản của bạn chưa được kích hoạt, kiểm tra email!' user_regged: '&cTên đăng nhập này đã được đăng kí' usage_reg: '&eSử dụng: /register mật-khẩu nhập-lại-mật-khẩu' -# TODO max_reg: Missing tags %reg_count, %reg_names, %max_acc +# TODO max_reg: Missing tags %reg_count, %max_acc, %reg_names max_reg: '&fSố lượng tài khoản ở IP của bạn trong server này đã quá giới hạn cho phép' no_perm: '&cKhông có quyền' error: '&fCó lỗi xảy ra; Báo lại cho người điều hành server' @@ -55,13 +55,15 @@ email_send: '[AuthMe] Đã gửi email khôi phục mật khẩu tới bạn !' country_banned: 'Rất tiếc, quốc gia của bạn không được phép gia nhập server' antibot_auto_enabled: '[AuthMe] AntiBot đã được kích hoạt vì lượng người chơi kết nối vượt quá giới hạn!' antibot_auto_disabled: '[AuthMe] AntiBot tự huỷ kích hoạt sau %m phút, hi vọng lượng kết nối sẽ giảm bớt' -# TODO email_already_used: '&4The email address is already being used' # TODO password_error_nick: '&cYou can''t use your name as password, please choose another one...' +# TODO not_owner_error: 'You are not the owner of this account. Please choose another name!' +# TODO two_factor_create: '&2Your secret code is %code. You can scan it from here %url' +# TODO email_already_used: '&4The email address is already being used' +# TODO denied_command: '&cIn order to use this command you must be authenticated!' +# TODO same_ip_online: 'A player with the same IP is already in game!' +# TODO password_error_chars: '&4Your password contains illegal characters. Allowed chars: REG_EX' +# TODO denied_chat: '&cIn order to chat you must be authenticated!' # TODO password_error_unsafe: '&cThe chosen password isn''t safe, please choose another one...' # TODO kick_antibot: 'AntiBot protection mode is enabled! You have to wait some minutes before joining the server.' -# TODO denied_chat: '&cIn order to be able to chat you must be authenticated!' # TODO email_exists: '&cA recovery email was already sent! You can discard it and send a new one using the command below:' -# TODO same_ip_online: 'A player with the same IP is already in game!' -# TODO invalid_name_case: 'You should join using username %valid, not %invalid.' -# TODO two_factor_create: '&2Your secret code is %code. You can scan it from here %url' -# TODO not_owner_error: 'You are not the owner of this account. Please choose another name!' \ No newline at end of file +# TODO invalid_name_case: 'You should join using username %valid, not %invalid.' \ No newline at end of file diff --git a/src/main/resources/messages/messages_zhcn.yml b/src/main/resources/messages/messages_zhcn.yml index 3118184d..349787c5 100644 --- a/src/main/resources/messages/messages_zhcn.yml +++ b/src/main/resources/messages/messages_zhcn.yml @@ -15,7 +15,7 @@ login: '&8[&6玩家系统&8] &c已成功登录!' vb_nonActiv: '&8[&6玩家系统&8] &f你的帐号还未激活,请查看你的邮箱!' user_regged: '&8[&6玩家系统&8] &c此用户已经在此服务器注册过' usage_reg: '&8[&6玩家系统&8] &c正确用法:“/register <密码> <再输入一次以确定密码>”' -# TODO max_reg: Missing tags %reg_count, %reg_names, %max_acc +# TODO max_reg: Missing tags %reg_count, %max_acc, %reg_names max_reg: '&8[&6玩家系统&8] &f你不允许再为你的IP在服务器注册更多用户了!' no_perm: '&8[&6玩家系统&8] &c没有权限' error: '&8[&6玩家系统&8] &f发现错误,请联系管理员' @@ -65,5 +65,7 @@ email_exists: '&8[&6玩家系统&8] &c恢复邮件已发送 ! 你可以丢弃它 two_factor_create: '&8[&6玩家系统&8] &2你的代码是 %code,你可以使用 %url 来扫描' not_owner_error: '&8[&6玩家系统&8] &4警告! &c你并不是此帐户持有人,请立即登出。 ' invalid_name_case: '&8[&6玩家系统&8] &c你应该使用「%valid」而并非「%invalid」登入游戏。 ' -# TODO denied_chat: '&cIn order to be able to chat you must be authenticated!' -# TODO same_ip_online: 'A player with the same IP is already in game!' \ No newline at end of file +# TODO denied_command: '&cIn order to use this command you must be authenticated!' +# TODO same_ip_online: 'A player with the same IP is already in game!' +# TODO password_error_chars: '&4Your password contains illegal characters. Allowed chars: REG_EX' +# TODO denied_chat: '&cIn order to chat you must be authenticated!' \ No newline at end of file diff --git a/src/main/resources/messages/messages_zhhk.yml b/src/main/resources/messages/messages_zhhk.yml index 209962cf..7001604c 100644 --- a/src/main/resources/messages/messages_zhhk.yml +++ b/src/main/resources/messages/messages_zhhk.yml @@ -15,7 +15,7 @@ login: '&8[&6用戶系統&8] &c你成功登入了。' vb_nonActiv: '&8[&6用戶系統&8] &f你的帳戶還沒有經過電郵驗證 !' user_regged: '&8[&6用戶系統&8] &c此用戶名已經註冊過了。' usage_reg: '&8[&6用戶系統&8] &f用法: 《 /register <密碼> <重覆密碼> 》' -# TODO max_reg: Missing tags %reg_count, %reg_names, %max_acc +# TODO max_reg: Missing tags %reg_count, %max_acc, %reg_names max_reg: '&8[&6用戶系統&8] &f你的IP地址已達到註冊數上限。' no_perm: '&8[&6用戶系統&8] &b嗯~你想幹甚麼?' error: '&8[&6用戶系統&8] &f發生錯誤,請與管理員聯絡。' @@ -65,5 +65,7 @@ email_exists: '&8[&6用戶系統&8] &c訊息已發送!如果你收不到該封 two_factor_create: '&8[&6用戶系統 - 兩步驗證碼&8] &b你的登入金鑰為&9「%c%code&9」&b,掃描連結為:&c %url' not_owner_error: '&8[&6用戶系統&8] &4警告!&c你並不是此帳戶持有人,請立即登出。' invalid_name_case: '&8[&6用戶系統&8] &4警告!&c你應該使用「%valid」而並非「%invalid」登入遊戲。' -# TODO denied_chat: '&cIn order to be able to chat you must be authenticated!' -# TODO same_ip_online: 'A player with the same IP is already in game!' \ No newline at end of file +# TODO denied_command: '&cIn order to use this command you must be authenticated!' +# TODO same_ip_online: 'A player with the same IP is already in game!' +# TODO password_error_chars: '&4Your password contains illegal characters. Allowed chars: REG_EX' +# TODO denied_chat: '&cIn order to chat you must be authenticated!' \ No newline at end of file diff --git a/src/main/resources/messages/messages_zhtw.yml b/src/main/resources/messages/messages_zhtw.yml index 6fe136f0..f50208b0 100644 --- a/src/main/resources/messages/messages_zhtw.yml +++ b/src/main/resources/messages/messages_zhtw.yml @@ -15,7 +15,7 @@ login: '&b【AuthMe】&6密碼正確,你已成功登入!' vb_nonActiv: '&b【AuthMe】&6你的帳號還沒有經過驗證! 檢查看看你的電子信箱 (Email) 吧!' user_regged: '&b【AuthMe】&6這個帳號已經被註冊過了!' usage_reg: '&b【AuthMe】&6用法: &c"/register <密碼> <確認密碼>"' -# TODO max_reg: Missing tags %reg_count, %reg_names, %max_acc +# TODO max_reg: Missing tags %reg_count, %max_acc, %reg_names max_reg: '&b【AuthMe】&6你的 IP 位置所註冊的帳號數量已經達到最大。' no_perm: '&b【AuthMe】&6你沒有使用該指令的權限。' error: '&b【AuthMe】&6發生錯誤,請聯繫管理員' @@ -65,5 +65,7 @@ email_exists: '&b【AuthMe】&6這個帳戶已經有設定電子郵件了' two_factor_create: '&b【AuthMe - 兩步驗證碼】&b你的登入金鑰為&9「%c%code&9」&b,掃描連結為:&c %url' not_owner_error: '&b【AuthMe】&4警告!&c你並不是此帳戶持有人,請立即登出。' invalid_name_case: '&b【AuthMe】&4警告!&c你應該使用「%valid」而並非「%invalid」登入遊戲。' -# TODO denied_chat: '&cIn order to be able to chat you must be authenticated!' -# TODO same_ip_online: 'A player with the same IP is already in game!' \ No newline at end of file +# TODO denied_command: '&cIn order to use this command you must be authenticated!' +# TODO same_ip_online: 'A player with the same IP is already in game!' +# TODO password_error_chars: '&4Your password contains illegal characters. Allowed chars: REG_EX' +# TODO denied_chat: '&cIn order to chat you must be authenticated!' \ No newline at end of file From 1f2a823f99c085592629d47bc54256d00103885e Mon Sep 17 00:00:00 2001 From: ljacqu Date: Fri, 3 Jun 2016 13:36:33 +0200 Subject: [PATCH 156/200] Add tests for PluginHooks - Multiverse interactions --- .../xephi/authme/hooks/PluginHooksTest.java | 103 +++++++++++++++++- 1 file changed, 97 insertions(+), 6 deletions(-) diff --git a/src/test/java/fr/xephi/authme/hooks/PluginHooksTest.java b/src/test/java/fr/xephi/authme/hooks/PluginHooksTest.java index e8475d9c..eef56144 100644 --- a/src/test/java/fr/xephi/authme/hooks/PluginHooksTest.java +++ b/src/test/java/fr/xephi/authme/hooks/PluginHooksTest.java @@ -2,8 +2,13 @@ package fr.xephi.authme.hooks; import com.earth2me.essentials.Essentials; import com.earth2me.essentials.User; +import com.onarandombox.MultiverseCore.MultiverseCore; +import com.onarandombox.MultiverseCore.api.MVWorldManager; +import com.onarandombox.MultiverseCore.api.MultiverseWorld; import fr.xephi.authme.ReflectionTestUtils; import fr.xephi.authme.TestHelper; +import org.bukkit.Location; +import org.bukkit.World; import org.bukkit.entity.Player; import org.bukkit.plugin.Plugin; import org.bukkit.plugin.PluginManager; @@ -20,6 +25,7 @@ import static org.mockito.BDDMockito.given; import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; /** @@ -27,6 +33,11 @@ import static org.mockito.Mockito.verify; */ public class PluginHooksTest { + /** The plugin name of Essentials. */ + private static final String ESSENTIALS = "Essentials"; + /** The plugin name of Multiverse-Core. */ + private static final String MULTIVERSE = "Multiverse-Core"; + @BeforeClass public static void setLogger() { TestHelper.setupLogger(); @@ -37,7 +48,7 @@ public class PluginHooksTest { // given PluginManager pluginManager = mock(PluginManager.class); PluginHooks pluginHooks = new PluginHooks(pluginManager); - setPluginAvailable(pluginManager, "Essentials", Essentials.class); + setPluginAvailable(pluginManager, ESSENTIALS, Essentials.class); assertThat(pluginHooks.isEssentialsAvailable(), equalTo(false)); // when @@ -53,7 +64,7 @@ public class PluginHooksTest { public void shouldHookIntoEssentialsAtInitialization() { // given PluginManager pluginManager = mock(PluginManager.class); - setPluginAvailable(pluginManager, "Essentials", Essentials.class); + setPluginAvailable(pluginManager, ESSENTIALS, Essentials.class); // when PluginHooks pluginHooks = new PluginHooks(pluginManager); @@ -62,6 +73,19 @@ public class PluginHooksTest { assertThat(pluginHooks.isEssentialsAvailable(), equalTo(true)); } + @Test + public void shouldHookIntoMultiverseAtInitialization() { + // given + PluginManager pluginManager = mock(PluginManager.class); + setPluginAvailable(pluginManager, MULTIVERSE, MultiverseCore.class); + + // when + PluginHooks pluginHooks = new PluginHooks(pluginManager); + + // then + assertThat(pluginHooks.isMultiverseAvailable(), equalTo(true)); + } + @Test public void shouldReturnEssentialsDataFolder() { // given @@ -71,7 +95,7 @@ public class PluginHooksTest { ReflectionTestUtils.setField(JavaPlugin.class, ess, "dataFolder", essDataFolder); PluginManager pluginManager = mock(PluginManager.class); - setPluginAvailable(pluginManager, "Essentials", ess); + setPluginAvailable(pluginManager, ESSENTIALS, ess); PluginHooks pluginHooks = new PluginHooks(pluginManager); // when @@ -104,7 +128,7 @@ public class PluginHooksTest { given(ess.getUser(player)).willReturn(user); PluginManager pluginManager = mock(PluginManager.class); - setPluginAvailable(pluginManager, "Essentials", ess); + setPluginAvailable(pluginManager, ESSENTIALS, ess); PluginHooks pluginHooks = new PluginHooks(pluginManager); // when @@ -125,17 +149,20 @@ public class PluginHooksTest { } @Test - public void shouldUnhookEssentials() { + public void shouldUnhookEssentialsAndMultiverse() { // given PluginManager pluginManager = mock(PluginManager.class); - setPluginAvailable(pluginManager, "Essentials", Essentials.class); + setPluginAvailable(pluginManager, ESSENTIALS, Essentials.class); + setPluginAvailable(pluginManager, MULTIVERSE, MultiverseCore.class); PluginHooks pluginHooks = new PluginHooks(pluginManager); // when pluginHooks.unhookEssentials(); + pluginHooks.unhookMultiverse(); // then assertThat(pluginHooks.isEssentialsAvailable(), equalTo(false)); + assertThat(pluginHooks.isMultiverseAvailable(), equalTo(false)); } @Test @@ -154,6 +181,70 @@ public class PluginHooksTest { assertThat(pluginHooks.isCombatTagPlusAvailable(), equalTo(false)); } + @Test + public void shouldReturnNullForUnavailableMultiverse() { + // given + PluginManager pluginManager = mock(PluginManager.class); + PluginHooks pluginHooks = new PluginHooks(pluginManager); + World world = mock(World.class); + + // when + Location result = pluginHooks.getMultiverseSpawn(world); + + // then + assertThat(result, nullValue()); + } + + @Test + public void shouldGetMultiverseSpawn() { + // given + Location location = mock(Location.class); + MultiverseWorld multiverseWorld = mock(MultiverseWorld.class); + given(multiverseWorld.getSpawnLocation()).willReturn(location); + + World world = mock(World.class); + MVWorldManager mvWorldManager = mock(MVWorldManager.class); + given(mvWorldManager.isMVWorld(world)).willReturn(true); + given(mvWorldManager.getMVWorld(world)).willReturn(multiverseWorld); + MultiverseCore multiverse = mock(MultiverseCore.class); + given(multiverse.getMVWorldManager()).willReturn(mvWorldManager); + + PluginManager pluginManager = mock(PluginManager.class); + setPluginAvailable(pluginManager, MULTIVERSE, multiverse); + PluginHooks pluginHooks = new PluginHooks(pluginManager); + + // when + Location spawn = pluginHooks.getMultiverseSpawn(world); + + // then + assertThat(spawn, equalTo(location)); + verify(mvWorldManager).isMVWorld(world); + verify(mvWorldManager).getMVWorld(world); + verify(multiverseWorld).getSpawnLocation(); + } + + @Test + public void shouldReturnNullForNonMvWorld() { + // given + World world = mock(World.class); + MVWorldManager mvWorldManager = mock(MVWorldManager.class); + given(mvWorldManager.isMVWorld(world)).willReturn(false); + + PluginManager pluginManager = mock(PluginManager.class); + MultiverseCore multiverse = mock(MultiverseCore.class); + setPluginAvailable(pluginManager, MULTIVERSE, multiverse); + given(multiverse.getMVWorldManager()).willReturn(mvWorldManager); + PluginHooks pluginHooks = new PluginHooks(pluginManager); + + // when + Location spawn = pluginHooks.getMultiverseSpawn(world); + + // then + assertThat(spawn, nullValue()); + verify(mvWorldManager).isMVWorld(world); + verify(mvWorldManager, never()).getMVWorld(world); + } + private static void setPluginAvailable(PluginManager managerMock, String pluginName, Class pluginClass) { setPluginAvailable(managerMock, pluginName, mock(pluginClass)); From 12703d1613bd2601b2597c4ae44ae25fe5591044 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Fri, 3 Jun 2016 22:47:17 +0200 Subject: [PATCH 157/200] #601 Integrate plugin manager - Encapsulate captcha functionality into a class instead of two public fields on the AuthMe main class(!) - Let CaptchaManager worry about whether it is enabled or not -> no need to check on the outside - Implement full reloading support to enable/disable captchas + parameters - Add unit tests --- src/main/java/fr/xephi/authme/AuthMe.java | 5 +- .../fr/xephi/authme/cache/CaptchaManager.java | 118 +++++++++++------ .../executable/captcha/CaptchaCommand.java | 50 +++----- .../process/login/AsynchronousLogin.java | 36 ++---- .../settings/properties/SecuritySettings.java | 4 +- src/main/resources/config.yml | 4 +- .../authme/cache/CaptchaManagerTest.java | 100 ++++++++++++++- .../captcha/CaptchaCommandTest.java | 121 ++++++++++++++++++ 8 files changed, 335 insertions(+), 103 deletions(-) create mode 100644 src/test/java/fr/xephi/authme/command/executable/captcha/CaptchaCommandTest.java diff --git a/src/main/java/fr/xephi/authme/AuthMe.java b/src/main/java/fr/xephi/authme/AuthMe.java index cf7289c0..9f29ff86 100644 --- a/src/main/java/fr/xephi/authme/AuthMe.java +++ b/src/main/java/fr/xephi/authme/AuthMe.java @@ -101,13 +101,10 @@ public class AuthMe extends JavaPlugin { /* * Maps and stuff */ - // TODO #601: Integrate CaptchaManager public final ConcurrentHashMap sessions = new ConcurrentHashMap<>(); - public final ConcurrentHashMap captcha = new ConcurrentHashMap<>(); - public final ConcurrentHashMap cap = new ConcurrentHashMap<>(); /* - * Public Instances + * Public instances */ public NewAPI api; // TODO #655: Encapsulate mail diff --git a/src/main/java/fr/xephi/authme/cache/CaptchaManager.java b/src/main/java/fr/xephi/authme/cache/CaptchaManager.java index 342ad7c3..6d262f22 100644 --- a/src/main/java/fr/xephi/authme/cache/CaptchaManager.java +++ b/src/main/java/fr/xephi/authme/cache/CaptchaManager.java @@ -1,81 +1,127 @@ package fr.xephi.authme.cache; +import fr.xephi.authme.initialization.SettingsDependent; import fr.xephi.authme.security.RandomString; import fr.xephi.authme.settings.NewSetting; import fr.xephi.authme.settings.properties.SecuritySettings; +import javax.inject.Inject; import java.util.concurrent.ConcurrentHashMap; /** * Manager for the handling of captchas. */ -public class CaptchaManager { +public class CaptchaManager implements SettingsDependent { - private final int threshold; - private final int captchaLength; private final ConcurrentHashMap playerCounts; private final ConcurrentHashMap captchaCodes; - public CaptchaManager(NewSetting settings) { + private boolean isEnabled; + private int threshold; + private int captchaLength; + + @Inject + CaptchaManager(NewSetting settings) { this.playerCounts = new ConcurrentHashMap<>(); this.captchaCodes = new ConcurrentHashMap<>(); - this.threshold = settings.getProperty(SecuritySettings.MAX_LOGIN_TRIES_BEFORE_CAPTCHA); - this.captchaLength = settings.getProperty(SecuritySettings.CAPTCHA_LENGTH); + loadSettings(settings); } - public void increaseCount(String player) { - String playerLower = player.toLowerCase(); - Integer currentCount = playerCounts.get(playerLower); - if (currentCount == null) { - playerCounts.put(playerLower, 1); - } else { - playerCounts.put(playerLower, currentCount + 1); + /** + * Increases the failure count for the given player. + * + * @param name the player's name + */ + public void increaseCount(String name) { + if (isEnabled) { + String playerLower = name.toLowerCase(); + Integer currentCount = playerCounts.get(playerLower); + if (currentCount == null) { + playerCounts.put(playerLower, 1); + } else { + playerCounts.put(playerLower, currentCount + 1); + } } } /** - * Return whether the given player is required to solve a captcha. + * Returns whether the given player is required to solve a captcha. * - * @param player The player to verify - * @return True if the player has to solve a captcha, false otherwise + * @param name the name of the player to verify + * @return true if the player has to solve a captcha, false otherwise */ - public boolean isCaptchaRequired(String player) { - Integer count = playerCounts.get(player.toLowerCase()); - return count != null && count >= threshold; + public boolean isCaptchaRequired(String name) { + if (isEnabled) { + Integer count = playerCounts.get(name.toLowerCase()); + return count != null && count >= threshold; + } + return false; } /** - * Return the captcha code for the player. Creates one if none present, so call only after - * checking with {@link #isCaptchaRequired}. + * Returns the stored captcha code for the player. * - * @param player The player - * @return The code required for the player + * @param name the player's name + * @return the code the player is required to enter, or null if none registered */ - public String getCaptchaCode(String player) { - String code = captchaCodes.get(player.toLowerCase()); - if (code == null) { - code = RandomString.generate(captchaLength); - captchaCodes.put(player.toLowerCase(), code); - } + public String getCaptchaCode(String name) { + return captchaCodes.get(name.toLowerCase()); + } + + /** + * Returns the stored captcha for the player or generates and saves a new one. + * + * @param name the player's name + * @return the code the player is required to enter + */ + public String getCaptchaCodeOrGenerateNew(String name) { + String code = getCaptchaCode(name); + return code == null ? generateCode(name) : code; + } + + /** + * Generates a code for the player and returns it. + * + * @param name the name of the player to generate a code for + * @return the generated code + */ + public String generateCode(String name) { + String code = RandomString.generate(captchaLength); + captchaCodes.put(name.toLowerCase(), code); return code; } /** - * Return whether the supplied code is correct for the given player. + * Checks the given code against the existing one and resets the player's auth failure count upon success. * - * @param player The player to check - * @param code The supplied code - * @return True if the code matches or if no captcha is required for the player, false otherwise + * @param name the name of the player to check + * @param code the supplied code + * @return true if the code matches or if no captcha is required for the player, false otherwise */ - public boolean checkCode(String player, String code) { - String savedCode = captchaCodes.get(player.toLowerCase()); + public boolean checkCode(String name, String code) { + String savedCode = captchaCodes.get(name.toLowerCase()); if (savedCode == null) { return true; } else if (savedCode.equalsIgnoreCase(code)) { - captchaCodes.remove(player.toLowerCase()); + captchaCodes.remove(name.toLowerCase()); + playerCounts.remove(name.toLowerCase()); return true; } return false; } + public void resetCounts(String name) { + if (isEnabled) { + captchaCodes.remove(name.toLowerCase()); + playerCounts.remove(name.toLowerCase()); + } + } + + @Override + public void loadSettings(NewSetting settings) { + this.isEnabled = settings.getProperty(SecuritySettings.USE_CAPTCHA); + this.threshold = settings.getProperty(SecuritySettings.MAX_LOGIN_TRIES_BEFORE_CAPTCHA); + this.captchaLength = settings.getProperty(SecuritySettings.CAPTCHA_LENGTH); + } + } 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 ebb44f80..410a9d8f 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 @@ -1,12 +1,10 @@ package fr.xephi.authme.command.executable.captcha; -import fr.xephi.authme.AuthMe; +import fr.xephi.authme.cache.CaptchaManager; 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.security.RandomString; -import fr.xephi.authme.settings.properties.SecuritySettings; import org.bukkit.entity.Player; import javax.inject.Inject; @@ -15,46 +13,32 @@ import java.util.List; public class CaptchaCommand extends PlayerCommand { @Inject - private AuthMe plugin; + private PlayerCache playerCache; @Inject - private PlayerCache playerCache; + private CaptchaManager captchaManager; @Override public void runCommand(Player player, List arguments, CommandService commandService) { - final String playerNameLowerCase = player.getName().toLowerCase(); - final String captcha = arguments.get(0); + final String playerName = player.getName().toLowerCase(); - // Command logic - if (playerCache.isAuthenticated(playerNameLowerCase)) { + if (playerCache.isAuthenticated(playerName)) { commandService.send(player, MessageKey.ALREADY_LOGGED_IN_ERROR); - return; - } - - if (!commandService.getProperty(SecuritySettings.USE_CAPTCHA)) { + } else if (!captchaManager.isCaptchaRequired(playerName)) { commandService.send(player, MessageKey.USAGE_LOGIN); - return; + } else { + checkCaptcha(player, arguments.get(0), commandService); } + } - if (!plugin.cap.containsKey(playerNameLowerCase)) { - commandService.send(player, MessageKey.USAGE_LOGIN); - return; + private void checkCaptcha(Player player, String captchaCode, CommandService service) { + final boolean isCorrectCode = captchaManager.checkCode(player.getName(), captchaCode); + if (isCorrectCode) { + service.send(player, MessageKey.CAPTCHA_SUCCESS); + service.send(player, MessageKey.LOGIN_MESSAGE); + } else { + String newCode = captchaManager.generateCode(player.getName()); + service.send(player, MessageKey.CAPTCHA_WRONG_ERROR, newCode); } - - if (!captcha.equals(plugin.cap.get(playerNameLowerCase))) { - plugin.cap.remove(playerNameLowerCase); - 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; - } - - plugin.captcha.remove(playerNameLowerCase); - plugin.cap.remove(playerNameLowerCase); - - // Show a status message - commandService.send(player, MessageKey.CAPTCHA_SUCCESS); - commandService.send(player, MessageKey.LOGIN_MESSAGE); } } diff --git a/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java b/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java index 473c8142..593ee25b 100644 --- a/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java +++ b/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java @@ -2,6 +2,7 @@ package fr.xephi.authme.process.login; import fr.xephi.authme.AuthMe; import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.cache.CaptchaManager; import fr.xephi.authme.cache.auth.PlayerAuth; import fr.xephi.authme.cache.auth.PlayerCache; import fr.xephi.authme.cache.limbo.LimboCache; @@ -17,7 +18,6 @@ import fr.xephi.authme.process.AsynchronousProcess; import fr.xephi.authme.process.ProcessService; import fr.xephi.authme.process.SyncProcessManager; import fr.xephi.authme.security.PasswordSecurity; -import fr.xephi.authme.security.RandomString; import fr.xephi.authme.settings.Settings; import fr.xephi.authme.settings.properties.DatabaseSettings; import fr.xephi.authme.settings.properties.EmailSettings; @@ -66,24 +66,16 @@ public class AsynchronousLogin implements AsynchronousProcess { @Inject private PasswordSecurity passwordSecurity; + @Inject + private CaptchaManager captchaManager; AsynchronousLogin() { } + private boolean needsCaptcha(Player player) { - final String name = player.getName().toLowerCase(); - if (service.getProperty(SecuritySettings.USE_CAPTCHA)) { - if (plugin.captcha.containsKey(name)) { - int i = plugin.captcha.get(name) + 1; - plugin.captcha.remove(name); - plugin.captcha.putIfAbsent(name, i); - } else { - plugin.captcha.putIfAbsent(name, 1); - } - if (plugin.captcha.containsKey(name) && plugin.captcha.get(name) > Settings.maxLoginTry) { - plugin.cap.putIfAbsent(name, RandomString.generate(Settings.captchaLength)); - service.send(player, MessageKey.USAGE_CAPTCHA, plugin.cap.get(name)); - return true; - } + if (captchaManager.isCaptchaRequired(player.getName())) { + service.send(player, MessageKey.USAGE_CAPTCHA, captchaManager.getCaptchaCodeOrGenerateNew(player.getName())); + return true; } return false; } @@ -124,7 +116,7 @@ public class AsynchronousLogin implements AsynchronousProcess { } final String ip = Utils.getPlayerIp(player); - if (Settings.getMaxLoginPerIp > 0 + if (service.getProperty(RestrictionSettings.MAX_LOGIN_PER_IP) > 0 && !permissionsManager.hasPermission(player, PlayerStatePermission.ALLOW_MULTIPLE_ACCOUNTS) && !"127.0.0.1".equalsIgnoreCase(ip) && !"localhost".equalsIgnoreCase(ip)) { if (plugin.isLoggedIp(name, ip)) { @@ -168,16 +160,9 @@ public class AsynchronousLogin implements AsynchronousProcess { .build(); database.updateSession(auth); - if (service.getProperty(SecuritySettings.USE_CAPTCHA)) { - if (plugin.captcha.containsKey(name)) { - plugin.captcha.remove(name); - } - if (plugin.cap.containsKey(name)) { - plugin.cap.remove(name); - } - } - + captchaManager.resetCounts(name); player.setNoDamageTicks(0); + if (!forceLogin) service.send(player, MessageKey.LOGIN_SUCCESS); @@ -214,6 +199,7 @@ public class AsynchronousLogin implements AsynchronousProcess { if (!service.getProperty(SecuritySettings.REMOVE_SPAM_FROM_CONSOLE)) { ConsoleLogger.info(player.getName() + " used the wrong password"); } + captchaManager.increaseCount(name); if (service.getProperty(RestrictionSettings.KICK_ON_WRONG_PASSWORD)) { bukkitService.scheduleSyncDelayedTask(new Runnable() { @Override diff --git a/src/main/java/fr/xephi/authme/settings/properties/SecuritySettings.java b/src/main/java/fr/xephi/authme/settings/properties/SecuritySettings.java index 3419a05c..49398d69 100644 --- a/src/main/java/fr/xephi/authme/settings/properties/SecuritySettings.java +++ b/src/main/java/fr/xephi/authme/settings/properties/SecuritySettings.java @@ -34,11 +34,11 @@ public class SecuritySettings implements SettingsClass { public static final Property USE_LOGGING = newProperty("Security.console.logConsole", true); - @Comment("Player need to put a captcha when he fails too lot the password") + @Comment("Enable captcha when a player uses wrong password too many times") public static final Property USE_CAPTCHA = newProperty("Security.captcha.useCaptcha", false); - @Comment("Max allowed tries before request a captcha") + @Comment("Max allowed tries before a captcha is required") public static final Property MAX_LOGIN_TRIES_BEFORE_CAPTCHA = newProperty("Security.captcha.maxLoginTry", 5); diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index abf71daf..1a9582c2 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -321,9 +321,9 @@ Security: # Copy AuthMe log output in a separate file as well? logConsole: true captcha: - # Player need to put a captcha when he fails too lot the password + # Enable captcha when a player uses wrong password too many times useCaptcha: false - # Max allowed tries before request a captcha + # Max allowed tries before a captcha is required maxLoginTry: 5 # Captcha length captchaLength: 5 diff --git a/src/test/java/fr/xephi/authme/cache/CaptchaManagerTest.java b/src/test/java/fr/xephi/authme/cache/CaptchaManagerTest.java index 96489155..83fe4b34 100644 --- a/src/test/java/fr/xephi/authme/cache/CaptchaManagerTest.java +++ b/src/test/java/fr/xephi/authme/cache/CaptchaManagerTest.java @@ -1,10 +1,14 @@ package fr.xephi.authme.cache; +import fr.xephi.authme.ReflectionTestUtils; import fr.xephi.authme.settings.NewSetting; import fr.xephi.authme.settings.properties.SecuritySettings; import org.junit.Test; +import java.util.Map; + import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.nullValue; import static org.junit.Assert.assertThat; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; @@ -39,7 +43,7 @@ public class CaptchaManagerTest { String player = "Miner"; NewSetting settings = mockSettings(1, 4); CaptchaManager manager = new CaptchaManager(settings); - String captchaCode = manager.getCaptchaCode(player); + String captchaCode = manager.getCaptchaCodeOrGenerateNew(player); // when boolean badResult = manager.checkCode(player, "wrong_code"); @@ -53,11 +57,105 @@ public class CaptchaManagerTest { assertThat(manager.checkCode(player, "bogus"), equalTo(true)); } + /** + * Tests {@link CaptchaManager#getCaptchaCode} and {@link CaptchaManager#getCaptchaCodeOrGenerateNew}. + * The former method should never change the code (and so return {@code null} for no code) while the latter should + * generate a new code if no code is yet present. If a code is saved, it should never generate a new one. + */ + @Test + public void shouldHaveSameCodeAfterGeneration() { + // given + String player = "Tester"; + NewSetting settings = mockSettings(1, 5); + CaptchaManager manager = new CaptchaManager(settings); + + // when + String code1 = manager.getCaptchaCode(player); + String code2 = manager.getCaptchaCodeOrGenerateNew(player); + String code3 = manager.getCaptchaCode(player); + String code4 = manager.getCaptchaCodeOrGenerateNew(player); + String code5 = manager.getCaptchaCode(player); + + // then + assertThat(code1, nullValue()); + assertThat(code2.length(), equalTo(5)); + assertThat(code3, equalTo(code2)); + assertThat(code4, equalTo(code2)); + assertThat(code5, equalTo(code2)); + } + + @Test + public void shouldIncreaseAndResetCount() { + // given + String player = "plaYer"; + NewSetting settings = mockSettings(2, 3); + CaptchaManager manager = new CaptchaManager(settings); + + // when + manager.increaseCount(player); + manager.increaseCount(player); + + // then + assertThat(manager.isCaptchaRequired(player), equalTo(true)); + assertHasCount(manager, player, 2); + + // when 2 + manager.resetCounts(player); + + // then 2 + assertThat(manager.isCaptchaRequired(player), equalTo(false)); + assertHasCount(manager, player, null); + } + + @Test + public void shouldNotIncreaseCountForDisabledCaptcha() { + // given + String player = "someone_"; + NewSetting settings = mockSettings(1, 3); + given(settings.getProperty(SecuritySettings.USE_CAPTCHA)).willReturn(false); + CaptchaManager manager = new CaptchaManager(settings); + + // when + manager.increaseCount(player); + + // then + assertThat(manager.isCaptchaRequired(player), equalTo(false)); + assertHasCount(manager, player, null); + } + + @Test + public void shouldNotCheckCountIfCaptchaIsDisabled() { + // given + String player = "Robert001"; + NewSetting settings = mockSettings(1, 5); + CaptchaManager manager = new CaptchaManager(settings); + given(settings.getProperty(SecuritySettings.USE_CAPTCHA)).willReturn(false); + + // when + manager.increaseCount(player); + // assumptions + assertThat(manager.isCaptchaRequired(player), equalTo(true)); + assertHasCount(manager, player, 1); + // end assumptions + manager.loadSettings(settings); + boolean result = manager.isCaptchaRequired(player); + + // then + assertThat(result, equalTo(false)); + } private static NewSetting mockSettings(int maxTries, int captchaLength) { NewSetting settings = mock(NewSetting.class); + given(settings.getProperty(SecuritySettings.USE_CAPTCHA)).willReturn(true); given(settings.getProperty(SecuritySettings.MAX_LOGIN_TRIES_BEFORE_CAPTCHA)).willReturn(maxTries); given(settings.getProperty(SecuritySettings.CAPTCHA_LENGTH)).willReturn(captchaLength); return settings; } + + private static void assertHasCount(CaptchaManager manager, String player, Integer count) { + @SuppressWarnings("unchecked") + Map playerCounts = (Map) ReflectionTestUtils + .getFieldValue(CaptchaManager.class, manager, "playerCounts"); + assertThat(playerCounts.get(player.toLowerCase()), equalTo(count)); + } } 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 new file mode 100644 index 00000000..52529d19 --- /dev/null +++ b/src/test/java/fr/xephi/authme/command/executable/captcha/CaptchaCommandTest.java @@ -0,0 +1,121 @@ +package fr.xephi.authme.command.executable.captcha; + +import fr.xephi.authme.cache.CaptchaManager; +import fr.xephi.authme.cache.auth.PlayerCache; +import fr.xephi.authme.command.CommandService; +import fr.xephi.authme.output.MessageKey; +import org.bukkit.entity.Player; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import java.util.Collections; + +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +/** + * Test for {@link CaptchaCommand}. + */ +@RunWith(MockitoJUnitRunner.class) +public class CaptchaCommandTest { + + @InjectMocks + private CaptchaCommand command; + + @Mock + private CaptchaManager captchaManager; + + @Mock + private PlayerCache playerCache; + + @Mock + private CommandService commandService; + + @Test + public void shouldDetectIfPlayerIsLoggedIn() { + // given + String name = "creeper011"; + Player player = mockPlayerWithName(name); + given(playerCache.isAuthenticated(name)).willReturn(true); + + // when + command.executeCommand(player, Collections.singletonList("123"), commandService); + + // then + verify(commandService).send(player, MessageKey.ALREADY_LOGGED_IN_ERROR); + } + + @Test + public void shouldShowLoginUsageIfCaptchaIsNotRequired() { + // given + String name = "bobby"; + Player player = mockPlayerWithName(name); + given(playerCache.isAuthenticated(name)).willReturn(false); + given(captchaManager.isCaptchaRequired(name)).willReturn(false); + + // when + command.executeCommand(player, Collections.singletonList("1234"), commandService); + + // then + verify(commandService).send(player, MessageKey.USAGE_LOGIN); + verify(captchaManager).isCaptchaRequired(name); + verifyNoMoreInteractions(captchaManager); + } + + @Test + public void shouldHandleCorrectCaptchaInput() { + // given + String name = "smith"; + Player player = mockPlayerWithName(name); + given(playerCache.isAuthenticated(name)).willReturn(false); + given(captchaManager.isCaptchaRequired(name)).willReturn(true); + String captchaCode = "3991"; + given(captchaManager.checkCode(name, captchaCode)).willReturn(true); + + // when + command.executeCommand(player, Collections.singletonList(captchaCode), commandService); + + // then + verify(captchaManager).isCaptchaRequired(name); + verify(captchaManager).checkCode(name, captchaCode); + verifyNoMoreInteractions(captchaManager); + verify(commandService).send(player, MessageKey.CAPTCHA_SUCCESS); + verify(commandService).send(player, MessageKey.LOGIN_MESSAGE); + verifyNoMoreInteractions(commandService); + } + + @Test + public void shouldHandleWrongCaptchaInput() { + // given + String name = "smith"; + Player player = mockPlayerWithName(name); + given(playerCache.isAuthenticated(name)).willReturn(false); + given(captchaManager.isCaptchaRequired(name)).willReturn(true); + String captchaCode = "2468"; + given(captchaManager.checkCode(name, captchaCode)).willReturn(false); + String newCode = "1337"; + given(captchaManager.generateCode(name)).willReturn(newCode); + + // when + command.executeCommand(player, Collections.singletonList(captchaCode), commandService); + + // then + verify(captchaManager).isCaptchaRequired(name); + verify(captchaManager).checkCode(name, captchaCode); + verify(captchaManager).generateCode(name); + verifyNoMoreInteractions(captchaManager); + verify(commandService).send(player, MessageKey.CAPTCHA_WRONG_ERROR, newCode); + verifyNoMoreInteractions(commandService); + } + + private static Player mockPlayerWithName(String name) { + Player player = mock(Player.class); + given(player.getName()).willReturn(name); + return player; + } +} From 40ce01f65edfd66503ef5d34f0454104c2a9bfc2 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Sat, 4 Jun 2016 10:44:34 +0200 Subject: [PATCH 158/200] #601 Increase captcha count on login start - Increase login count at start of the login process (as done previously) and not only when login has failed AND player is still online - Add missing javadoc to CaptchaManager --- .../java/fr/xephi/authme/cache/CaptchaManager.java | 5 +++++ .../xephi/authme/process/login/AsynchronousLogin.java | 10 +++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/main/java/fr/xephi/authme/cache/CaptchaManager.java b/src/main/java/fr/xephi/authme/cache/CaptchaManager.java index 6d262f22..ec7deca9 100644 --- a/src/main/java/fr/xephi/authme/cache/CaptchaManager.java +++ b/src/main/java/fr/xephi/authme/cache/CaptchaManager.java @@ -110,6 +110,11 @@ public class CaptchaManager implements SettingsDependent { return false; } + /** + * Resets the login count of the given player to 0. + * + * @param name the player's name + */ public void resetCounts(String name) { if (isEnabled) { captchaCodes.remove(name.toLowerCase()); diff --git a/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java b/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java index 593ee25b..4116ffd0 100644 --- a/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java +++ b/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java @@ -73,9 +73,14 @@ public class AsynchronousLogin implements AsynchronousProcess { private boolean needsCaptcha(Player player) { - if (captchaManager.isCaptchaRequired(player.getName())) { - service.send(player, MessageKey.USAGE_CAPTCHA, captchaManager.getCaptchaCodeOrGenerateNew(player.getName())); + final String playerName = player.getName(); + if (captchaManager.isCaptchaRequired(playerName)) { + service.send(player, MessageKey.USAGE_CAPTCHA, captchaManager.getCaptchaCodeOrGenerateNew(playerName)); return true; + } else { + // Increase the count here before knowing the result of the login. + // If login is successful, we clear the count for the player + captchaManager.increaseCount(playerName); } return false; } @@ -199,7 +204,6 @@ public class AsynchronousLogin implements AsynchronousProcess { if (!service.getProperty(SecuritySettings.REMOVE_SPAM_FROM_CONSOLE)) { ConsoleLogger.info(player.getName() + " used the wrong password"); } - captchaManager.increaseCount(name); if (service.getProperty(RestrictionSettings.KICK_ON_WRONG_PASSWORD)) { bukkitService.scheduleSyncDelayedTask(new Runnable() { @Override From c6778b566d954bfcbb1ffb74146e3cf390f1d395 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Sat, 4 Jun 2016 11:02:15 +0200 Subject: [PATCH 159/200] #727 Remove CommandService from ExecutableCommand interface (work in progress) - Inject CommandService like other classes instead of passing it as method parameter - Not solved: cyclic dependency CommandInitializer > ExecutableCommand > CommandService > CommandInitializer... --- .../xephi/authme/command/CommandHandler.java | 17 ++--- .../authme/command/ExecutableCommand.java | 9 ++- .../xephi/authme/command/PlayerCommand.java | 23 ++++--- .../command/executable/HelpCommand.java | 7 ++- .../executable/authme/AccountsCommand.java | 6 +- .../executable/authme/AuthMeCommand.java | 3 +- .../authme/ChangePasswordAdminCommand.java | 6 +- .../executable/authme/ConverterCommand.java | 5 +- .../executable/authme/FirstSpawnCommand.java | 3 +- .../executable/authme/ForceLoginCommand.java | 3 +- .../executable/authme/GetEmailCommand.java | 5 +- .../executable/authme/GetIpCommand.java | 3 +- .../executable/authme/LastLoginCommand.java | 5 +- .../authme/PurgeBannedPlayersCommand.java | 13 ++-- .../executable/authme/PurgeCommand.java | 3 +- .../authme/PurgeLastPositionCommand.java | 5 +- .../authme/RegisterAdminCommand.java | 6 +- .../executable/authme/ReloadCommand.java | 5 +- .../executable/authme/SetEmailCommand.java | 6 +- .../authme/SetFirstSpawnCommand.java | 3 +- .../executable/authme/SetSpawnCommand.java | 3 +- .../executable/authme/SpawnCommand.java | 3 +- .../authme/SwitchAntiBotCommand.java | 5 +- .../authme/UnregisterAdminCommand.java | 14 +++-- .../executable/authme/VersionCommand.java | 5 +- .../executable/captcha/CaptchaCommand.java | 5 +- .../changepassword/ChangePasswordCommand.java | 5 +- .../executable/email/AddEmailCommand.java | 5 +- .../executable/email/ChangeEmailCommand.java | 3 +- .../executable/email/EmailBaseCommand.java | 6 +- .../executable/email/RecoverEmailCommand.java | 16 ++--- .../executable/login/LoginCommand.java | 3 +- .../executable/logout/LogoutCommand.java | 3 +- .../executable/register/RegisterCommand.java | 5 +- .../unregister/UnregisterCommand.java | 5 +- .../authme/command/CommandHandlerTest.java | 41 ++++++------ .../authme/command/CommandUtilsTest.java | 7 +++ .../authme/command/PlayerCommandTest.java | 10 +-- .../authme/AccountsCommandTest.java | 14 ++--- .../executable/authme/AuthMeCommandTest.java | 4 +- .../ChangePasswordAdminCommandTest.java | 10 +-- .../authme/FirstSpawnCommandTest.java | 8 +-- .../authme/ForceLoginCommandTest.java | 14 ++--- .../authme/GetEmailCommandTest.java | 9 ++- .../executable/authme/GetIpCommandTest.java | 12 ++-- .../authme/LastLoginCommandTest.java | 17 ++--- .../authme/PurgeLastPositionCommandTest.java | 13 ++-- .../authme/RegisterAdminCommandTest.java | 10 +-- .../executable/authme/ReloadCommandTest.java | 12 ++-- .../authme/SetFirstSpawnCommandTest.java | 7 +-- .../authme/SetSpawnCommandTest.java | 7 +-- .../executable/authme/SpawnCommandTest.java | 7 +-- .../authme/SwitchAntiBotCommandTest.java | 8 +-- .../captcha/CaptchaCommandTest.java | 8 +-- .../ChangePasswordCommandTest.java | 8 +-- .../executable/email/AddEmailCommandTest.java | 6 +- .../email/ChangeEmailCommandTest.java | 8 +-- .../executable/login/LoginCommandTest.java | 7 +-- .../executable/logout/LogoutCommandTest.java | 13 +--- .../register/RegisterCommandTest.java | 62 +++++++++++-------- .../command/help/CommandSyntaxHelperTest.java | 7 +++ 61 files changed, 292 insertions(+), 259 deletions(-) diff --git a/src/main/java/fr/xephi/authme/command/CommandHandler.java b/src/main/java/fr/xephi/authme/command/CommandHandler.java index bca1ae04..8f493acb 100644 --- a/src/main/java/fr/xephi/authme/command/CommandHandler.java +++ b/src/main/java/fr/xephi/authme/command/CommandHandler.java @@ -23,17 +23,12 @@ public class CommandHandler { */ private static final double SUGGEST_COMMAND_THRESHOLD = 0.75; - private final CommandService commandService; - private final PermissionsManager permissionsManager; - - /* - * Constructor. - */ @Inject - public CommandHandler(CommandService commandService, PermissionsManager permissionsManager) { - this.commandService = commandService; - this.permissionsManager = permissionsManager; - } + private CommandService commandService; + + @Inject + private PermissionsManager permissionsManager; + /** * Map a command that was invoked to the proper {@link CommandDescription} or return a useful error @@ -86,7 +81,7 @@ public class CommandHandler { private void executeCommand(CommandSender sender, FoundCommandResult result) { ExecutableCommand executableCommand = result.getCommandDescription().getExecutableCommand(); List arguments = result.getArguments(); - executableCommand.executeCommand(sender, arguments, commandService); + executableCommand.executeCommand(sender, arguments); } /** diff --git a/src/main/java/fr/xephi/authme/command/ExecutableCommand.java b/src/main/java/fr/xephi/authme/command/ExecutableCommand.java index 9567089f..ceda12ef 100644 --- a/src/main/java/fr/xephi/authme/command/ExecutableCommand.java +++ b/src/main/java/fr/xephi/authme/command/ExecutableCommand.java @@ -10,12 +10,11 @@ import java.util.List; public interface ExecutableCommand { /** - * Execute the command with the given arguments. + * Executes the command with the given arguments. * - * @param sender The command sender. - * @param arguments The arguments. - * @param commandService The command service. + * @param sender the command sender (initiator of the command) + * @param arguments the arguments */ - void executeCommand(CommandSender sender, List arguments, CommandService commandService); + void executeCommand(CommandSender sender, List arguments); } diff --git a/src/main/java/fr/xephi/authme/command/PlayerCommand.java b/src/main/java/fr/xephi/authme/command/PlayerCommand.java index 2d7aca0b..8ce6e05f 100644 --- a/src/main/java/fr/xephi/authme/command/PlayerCommand.java +++ b/src/main/java/fr/xephi/authme/command/PlayerCommand.java @@ -1,19 +1,19 @@ package fr.xephi.authme.command; -import java.util.List; - import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; +import java.util.List; + /** * Common base type for player-only commands, handling the verification that the command sender is indeed a player. */ public abstract class PlayerCommand implements ExecutableCommand { @Override - public void executeCommand(CommandSender sender, List arguments, CommandService commandService) { + public void executeCommand(CommandSender sender, List arguments) { if (sender instanceof Player) { - runCommand((Player) sender, arguments, commandService); + runCommand((Player) sender, arguments); } else { String alternative = getAlternativeCommand(); if (alternative != null) { @@ -25,19 +25,18 @@ public abstract class PlayerCommand implements ExecutableCommand { } /** - * Run the command with the given player and arguments. + * Runs the command with the given player and arguments. * - * @param player The player who initiated the command - * @param arguments The arguments supplied with the command - * @param commandService The command service + * @param player the player who initiated the command + * @param arguments the arguments supplied with the command */ - protected abstract void runCommand(Player player, List arguments, CommandService commandService); + protected abstract void runCommand(Player player, List arguments); /** - * Return an alternative command (textual representation) that is not restricted to players only. - * Example: {@code "authme register "} + * Returns an alternative command (textual representation) that is not restricted to players only. + * Example: {@code "/authme register "} * - * @return Alternative command not only for players, or null if not applicable + * @return Alternative command not restricted to players, or null if not applicable */ protected String getAlternativeCommand() { return null; diff --git a/src/main/java/fr/xephi/authme/command/executable/HelpCommand.java b/src/main/java/fr/xephi/authme/command/executable/HelpCommand.java index 89256535..e912fb12 100644 --- a/src/main/java/fr/xephi/authme/command/executable/HelpCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/HelpCommand.java @@ -9,6 +9,7 @@ import fr.xephi.authme.command.help.HelpProvider; import org.bukkit.ChatColor; import org.bukkit.command.CommandSender; +import javax.inject.Inject; import java.util.List; import static fr.xephi.authme.command.FoundResultStatus.MISSING_BASE_COMMAND; @@ -16,10 +17,14 @@ import static fr.xephi.authme.command.FoundResultStatus.UNKNOWN_LABEL; public class HelpCommand implements ExecutableCommand { + @Inject + private CommandService commandService; + + // Convention: arguments is not the actual invoked arguments but the command that was invoked, // e.g. "/authme help register" would typically be arguments = [register], but here we pass [authme, register] @Override - public void executeCommand(CommandSender sender, List arguments, CommandService commandService) { + public void executeCommand(CommandSender sender, List arguments) { FoundCommandResult result = commandService.mapPartsToCommand(sender, arguments); FoundResultStatus resultStatus = result.getResultStatus(); diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/AccountsCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/AccountsCommand.java index b2cd359e..3b446a7f 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/AccountsCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/AccountsCommand.java @@ -23,9 +23,11 @@ public class AccountsCommand implements ExecutableCommand { @Inject private BukkitService bukkitService; + @Inject + private CommandService commandService; + @Override - public void executeCommand(final CommandSender sender, List arguments, - final CommandService commandService) { + public void executeCommand(final CommandSender sender, List arguments) { final String playerName = arguments.isEmpty() ? sender.getName() : arguments.get(0); // Assumption: a player name cannot contain '.' diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/AuthMeCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/AuthMeCommand.java index 2c0ef601..2648177e 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/AuthMeCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/AuthMeCommand.java @@ -1,7 +1,6 @@ package fr.xephi.authme.command.executable.authme; import fr.xephi.authme.AuthMe; -import fr.xephi.authme.command.CommandService; import fr.xephi.authme.command.ExecutableCommand; import org.bukkit.ChatColor; import org.bukkit.command.CommandSender; @@ -14,7 +13,7 @@ import java.util.List; public class AuthMeCommand implements ExecutableCommand { @Override - public void executeCommand(CommandSender sender, List arguments, CommandService commandService) { + public void executeCommand(CommandSender sender, List arguments) { sender.sendMessage(ChatColor.GREEN + "This server is running " + AuthMe.getPluginName() + " v" + AuthMe.getPluginVersion() + " b" + AuthMe.getPluginBuildNumber()+ "! " + ChatColor.RED + "<3"); sender.sendMessage(ChatColor.YELLOW + "Use the command " + ChatColor.GOLD + "/authme help" + ChatColor.YELLOW 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 5f35b7df..729a4b4e 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 @@ -37,9 +37,11 @@ public class ChangePasswordAdminCommand implements ExecutableCommand { @Inject private ValidationService validationService; + @Inject + private CommandService commandService; + @Override - public void executeCommand(final CommandSender sender, List arguments, - final CommandService commandService) { + public void executeCommand(final CommandSender sender, List arguments) { // Get the player and password final String playerName = arguments.get(0); final String playerPass = arguments.get(1); diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/ConverterCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/ConverterCommand.java index 5c4800ed..4c2865b5 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/ConverterCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/ConverterCommand.java @@ -22,6 +22,9 @@ import java.util.List; */ public class ConverterCommand implements ExecutableCommand { + @Inject + private CommandService commandService; + @Inject private BukkitService bukkitService; @@ -29,7 +32,7 @@ public class ConverterCommand implements ExecutableCommand { private AuthMeServiceInitializer initializer; @Override - public void executeCommand(final CommandSender sender, List arguments, CommandService commandService) { + public void executeCommand(final CommandSender sender, List arguments) { // Get the conversion job String job = arguments.get(0); diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/FirstSpawnCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/FirstSpawnCommand.java index 52793e10..acfc59be 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/FirstSpawnCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/FirstSpawnCommand.java @@ -1,6 +1,5 @@ package fr.xephi.authme.command.executable.authme; -import fr.xephi.authme.command.CommandService; import fr.xephi.authme.command.PlayerCommand; import fr.xephi.authme.settings.SpawnLoader; import org.bukkit.entity.Player; @@ -17,7 +16,7 @@ public class FirstSpawnCommand extends PlayerCommand { private SpawnLoader spawnLoader; @Override - public void runCommand(Player player, List arguments, CommandService commandService) { + public void runCommand(Player player, List arguments) { if (spawnLoader.getFirstSpawn() == null) { player.sendMessage("[AuthMe] First spawn has failed, please try to define the first spawn"); } else { diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/ForceLoginCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/ForceLoginCommand.java index e9f8f777..706e313e 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/ForceLoginCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/ForceLoginCommand.java @@ -1,6 +1,5 @@ package fr.xephi.authme.command.executable.authme; -import fr.xephi.authme.command.CommandService; import fr.xephi.authme.command.ExecutableCommand; import fr.xephi.authme.permission.PermissionsManager; import fr.xephi.authme.process.Management; @@ -28,7 +27,7 @@ public class ForceLoginCommand implements ExecutableCommand { private BukkitService bukkitService; @Override - public void executeCommand(CommandSender sender, List arguments, CommandService commandService) { + public void executeCommand(CommandSender sender, List arguments) { // Get the player query String playerName = arguments.isEmpty() ? sender.getName() : arguments.get(0); diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/GetEmailCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/GetEmailCommand.java index ede9a424..07ee57d3 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/GetEmailCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/GetEmailCommand.java @@ -18,8 +18,11 @@ public class GetEmailCommand implements ExecutableCommand { @Inject private DataSource dataSource; + @Inject + private CommandService commandService; + @Override - public void executeCommand(CommandSender sender, List arguments, CommandService commandService) { + public void executeCommand(CommandSender sender, List arguments) { String playerName = arguments.isEmpty() ? sender.getName() : arguments.get(0); PlayerAuth auth = dataSource.getAuth(playerName); diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/GetIpCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/GetIpCommand.java index 749a88bf..773f01b4 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/GetIpCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/GetIpCommand.java @@ -1,6 +1,5 @@ package fr.xephi.authme.command.executable.authme; -import fr.xephi.authme.command.CommandService; import fr.xephi.authme.command.ExecutableCommand; import fr.xephi.authme.util.BukkitService; import org.bukkit.command.CommandSender; @@ -15,7 +14,7 @@ public class GetIpCommand implements ExecutableCommand { private BukkitService bukkitService; @Override - public void executeCommand(CommandSender sender, List arguments, CommandService commandService) { + public void executeCommand(CommandSender sender, List arguments) { // Get the player query String playerName = arguments.get(0); diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/LastLoginCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/LastLoginCommand.java index 4f0135a2..376499fb 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/LastLoginCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/LastLoginCommand.java @@ -19,8 +19,11 @@ public class LastLoginCommand implements ExecutableCommand { @Inject private DataSource dataSource; + @Inject + private CommandService commandService; + @Override - public void executeCommand(CommandSender sender, List arguments, CommandService commandService) { + public void executeCommand(CommandSender sender, List arguments) { // Get the player String playerName = (arguments.size() >= 1) ? arguments.get(0) : sender.getName(); diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/PurgeBannedPlayersCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/PurgeBannedPlayersCommand.java index 92a4b50c..c80a790d 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/PurgeBannedPlayersCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/PurgeBannedPlayersCommand.java @@ -1,22 +1,19 @@ package fr.xephi.authme.command.executable.authme; import fr.xephi.authme.AuthMe; -import fr.xephi.authme.command.CommandService; import fr.xephi.authme.command.ExecutableCommand; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.task.PurgeTask; import fr.xephi.authme.util.BukkitService; +import org.bukkit.ChatColor; +import org.bukkit.OfflinePlayer; +import org.bukkit.command.CommandSender; +import javax.inject.Inject; import java.util.HashSet; import java.util.List; import java.util.Set; -import javax.inject.Inject; -import org.bukkit.ChatColor; - -import org.bukkit.OfflinePlayer; -import org.bukkit.command.CommandSender; - /** * Command for purging data of banned players. Depending on the settings * it purges (deletes) data from third-party plugins as well. @@ -33,7 +30,7 @@ public class PurgeBannedPlayersCommand implements ExecutableCommand { private BukkitService bukkitService; @Override - public void executeCommand(CommandSender sender, List arguments, CommandService commandService) { + public void executeCommand(CommandSender sender, List arguments) { // Get the list of banned players Set namedBanned = new HashSet<>(); Set bannedPlayers = bukkitService.getBannedPlayers(); diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/PurgeCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/PurgeCommand.java index db91aad6..73e8e7c0 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/PurgeCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/PurgeCommand.java @@ -1,7 +1,6 @@ package fr.xephi.authme.command.executable.authme; import fr.xephi.authme.AuthMe; -import fr.xephi.authme.command.CommandService; import fr.xephi.authme.command.ExecutableCommand; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.task.PurgeTask; @@ -28,7 +27,7 @@ public class PurgeCommand implements ExecutableCommand { private AuthMe plugin; @Override - public void executeCommand(CommandSender sender, List arguments, CommandService commandService) { + public void executeCommand(CommandSender sender, List arguments) { // Get the days parameter String daysStr = arguments.get(0); diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/PurgeLastPositionCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/PurgeLastPositionCommand.java index 7c88df86..ca6a0200 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/PurgeLastPositionCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/PurgeLastPositionCommand.java @@ -18,8 +18,11 @@ public class PurgeLastPositionCommand implements ExecutableCommand { @Inject private DataSource dataSource; + @Inject + private CommandService commandService; + @Override - public void executeCommand(final CommandSender sender, List arguments, CommandService commandService) { + public void executeCommand(final CommandSender sender, List arguments) { String playerName = arguments.isEmpty() ? sender.getName() : arguments.get(0); if ("*".equals(playerName)) { 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 29218003..a4931e96 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 @@ -25,6 +25,9 @@ public class RegisterAdminCommand implements ExecutableCommand { @Inject private PasswordSecurity passwordSecurity; + @Inject + private CommandService commandService; + @Inject private DataSource dataSource; @@ -35,8 +38,7 @@ public class RegisterAdminCommand implements ExecutableCommand { private ValidationService validationService; @Override - public void executeCommand(final CommandSender sender, List arguments, - final CommandService commandService) { + public void executeCommand(final CommandSender sender, List arguments) { // Get the player name and password final String playerName = arguments.get(0); final String playerPass = arguments.get(1); diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/ReloadCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/ReloadCommand.java index 1698e316..73534902 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/ReloadCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/ReloadCommand.java @@ -31,8 +31,11 @@ public class ReloadCommand implements ExecutableCommand { @Inject private DataSource dataSource; + @Inject + private CommandService commandService; + @Override - public void executeCommand(CommandSender sender, List arguments, CommandService commandService) { + public void executeCommand(CommandSender sender, List arguments) { try { settings.reload(); ConsoleLogger.setLoggingOptions(settings); diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/SetEmailCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/SetEmailCommand.java index 35c0fa0d..76ef4959 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/SetEmailCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/SetEmailCommand.java @@ -20,6 +20,9 @@ public class SetEmailCommand implements ExecutableCommand { @Inject private DataSource dataSource; + @Inject + private CommandService commandService; + @Inject private PlayerCache playerCache; @@ -27,8 +30,7 @@ public class SetEmailCommand implements ExecutableCommand { private BukkitService bukkitService; @Override - public void executeCommand(final CommandSender sender, List arguments, - final CommandService commandService) { + public void executeCommand(final CommandSender sender, List arguments) { // Get the player name and email address final String playerName = arguments.get(0); final String playerEmail = arguments.get(1); diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/SetFirstSpawnCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/SetFirstSpawnCommand.java index 5d4b6995..899a1103 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/SetFirstSpawnCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/SetFirstSpawnCommand.java @@ -1,6 +1,5 @@ package fr.xephi.authme.command.executable.authme; -import fr.xephi.authme.command.CommandService; import fr.xephi.authme.command.PlayerCommand; import fr.xephi.authme.settings.SpawnLoader; import org.bukkit.entity.Player; @@ -14,7 +13,7 @@ public class SetFirstSpawnCommand extends PlayerCommand { private SpawnLoader spawnLoader; @Override - public void runCommand(Player player, List arguments, CommandService commandService) { + public void runCommand(Player player, List arguments) { if (spawnLoader.setFirstSpawn(player.getLocation())) { player.sendMessage("[AuthMe] Correctly defined new first spawn point"); } else { diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/SetSpawnCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/SetSpawnCommand.java index 18cdf13b..fc9a67b9 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/SetSpawnCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/SetSpawnCommand.java @@ -1,6 +1,5 @@ package fr.xephi.authme.command.executable.authme; -import fr.xephi.authme.command.CommandService; import fr.xephi.authme.command.PlayerCommand; import fr.xephi.authme.settings.SpawnLoader; import org.bukkit.entity.Player; @@ -14,7 +13,7 @@ public class SetSpawnCommand extends PlayerCommand { private SpawnLoader spawnLoader; @Override - public void runCommand(Player player, List arguments, CommandService commandService) { + public void runCommand(Player player, List arguments) { if (spawnLoader.setSpawn(player.getLocation())) { player.sendMessage("[AuthMe] Correctly defined new spawn point"); } else { diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/SpawnCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/SpawnCommand.java index c7f584f7..3c011e6d 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/SpawnCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/SpawnCommand.java @@ -1,6 +1,5 @@ package fr.xephi.authme.command.executable.authme; -import fr.xephi.authme.command.CommandService; import fr.xephi.authme.command.PlayerCommand; import fr.xephi.authme.settings.SpawnLoader; import org.bukkit.entity.Player; @@ -14,7 +13,7 @@ public class SpawnCommand extends PlayerCommand { private SpawnLoader spawnLoader; @Override - public void runCommand(Player player, List arguments, CommandService commandService) { + public void runCommand(Player player, List arguments) { if (spawnLoader.getSpawn() == null) { player.sendMessage("[AuthMe] Spawn has failed, please try to define the spawn"); } else { diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/SwitchAntiBotCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/SwitchAntiBotCommand.java index e996e6f0..37a709cf 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/SwitchAntiBotCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/SwitchAntiBotCommand.java @@ -20,8 +20,11 @@ public class SwitchAntiBotCommand implements ExecutableCommand { @Inject private AntiBot antiBot; + @Inject + private CommandService commandService; + @Override - public void executeCommand(final CommandSender sender, List arguments, CommandService commandService) { + public void executeCommand(final CommandSender sender, List arguments) { if (arguments.isEmpty()) { sender.sendMessage("[AuthMe] AntiBot status: " + antiBot.getAntiBotStatus().name()); return; diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/UnregisterAdminCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/UnregisterAdminCommand.java index d8fe66ad..d81b9baf 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/UnregisterAdminCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/UnregisterAdminCommand.java @@ -33,6 +33,9 @@ public class UnregisterAdminCommand implements ExecutableCommand { @Inject private DataSource dataSource; + @Inject + private CommandService commandService; + @Inject private PlayerCache playerCache; @@ -42,8 +45,11 @@ public class UnregisterAdminCommand implements ExecutableCommand { @Inject private BukkitService bukkitService; + @Inject + private LimboCache limboCache; + @Override - public void executeCommand(final CommandSender sender, List arguments, CommandService commandService) { + public void executeCommand(final CommandSender sender, List arguments) { // Get the player name String playerName = arguments.get(0); String playerNameLowerCase = playerName.toLowerCase(); @@ -88,14 +94,14 @@ public class UnregisterAdminCommand implements ExecutableCommand { final String playerNameLowerCase = target.getName().toLowerCase(); Utils.teleportToSpawn(target); - LimboCache.getInstance().addLimboPlayer(target); + limboCache.addLimboPlayer(target); int timeOut = service.getProperty(RestrictionSettings.TIMEOUT) * TICKS_PER_SECOND; int interval = service.getProperty(RegistrationSettings.MESSAGE_INTERVAL); if (timeOut != 0) { BukkitTask id = bukkitService.runTaskLater(new TimeoutTask(authMe, playerNameLowerCase, target), timeOut); - LimboCache.getInstance().getLimboPlayer(playerNameLowerCase).setTimeoutTask(id); + limboCache.getLimboPlayer(playerNameLowerCase).setTimeoutTask(id); } - LimboCache.getInstance().getLimboPlayer(playerNameLowerCase).setMessageTask( + limboCache.getLimboPlayer(playerNameLowerCase).setMessageTask( bukkitService.runTask(new MessageTask(bukkitService, authMe.getMessages(), playerNameLowerCase, MessageKey.REGISTER_MESSAGE, interval))); diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/VersionCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/VersionCommand.java index 825e0528..a7e6575e 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/VersionCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/VersionCommand.java @@ -19,8 +19,11 @@ public class VersionCommand implements ExecutableCommand { @Inject private BukkitService bukkitService; + @Inject + private CommandService commandService; + @Override - public void executeCommand(CommandSender sender, List arguments, CommandService commandService) { + public void executeCommand(CommandSender sender, List arguments) { // Show some version info sender.sendMessage(ChatColor.GOLD + "==========[ " + commandService.getProperty(HELP_HEADER) + " ABOUT ]=========="); 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 410a9d8f..0cae9aa0 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 @@ -18,8 +18,11 @@ public class CaptchaCommand extends PlayerCommand { @Inject private CaptchaManager captchaManager; + @Inject + private CommandService commandService; + @Override - public void runCommand(Player player, List arguments, CommandService commandService) { + public void runCommand(Player player, List arguments) { final String playerName = player.getName().toLowerCase(); if (playerCache.isAuthenticated(playerName)) { 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 8ed7b637..6f05d364 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 @@ -20,6 +20,9 @@ import java.util.List; */ public class ChangePasswordCommand extends PlayerCommand { + @Inject + private CommandService commandService; + @Inject private PlayerCache playerCache; @@ -34,7 +37,7 @@ public class ChangePasswordCommand extends PlayerCommand { private PasswordSecurity passwordSecurity; @Override - public void runCommand(Player player, List arguments, CommandService commandService) { + public void runCommand(Player player, List arguments) { String oldPassword = arguments.get(0); String newPassword = arguments.get(1); diff --git a/src/main/java/fr/xephi/authme/command/executable/email/AddEmailCommand.java b/src/main/java/fr/xephi/authme/command/executable/email/AddEmailCommand.java index 6d9b384d..842d866d 100644 --- a/src/main/java/fr/xephi/authme/command/executable/email/AddEmailCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/email/AddEmailCommand.java @@ -17,8 +17,11 @@ public class AddEmailCommand extends PlayerCommand { @Inject private Management management; + @Inject + private CommandService commandService; + @Override - public void runCommand(Player player, List arguments, CommandService commandService) { + public void runCommand(Player player, List arguments) { String email = arguments.get(0); String emailConfirmation = arguments.get(1); diff --git a/src/main/java/fr/xephi/authme/command/executable/email/ChangeEmailCommand.java b/src/main/java/fr/xephi/authme/command/executable/email/ChangeEmailCommand.java index 333f795e..1f9051e3 100644 --- a/src/main/java/fr/xephi/authme/command/executable/email/ChangeEmailCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/email/ChangeEmailCommand.java @@ -1,6 +1,5 @@ package fr.xephi.authme.command.executable.email; -import fr.xephi.authme.command.CommandService; import fr.xephi.authme.command.PlayerCommand; import fr.xephi.authme.process.Management; import org.bukkit.entity.Player; @@ -17,7 +16,7 @@ public class ChangeEmailCommand extends PlayerCommand { private Management management; @Override - public void runCommand(Player player, List arguments, CommandService commandService) { + public void runCommand(Player player, List arguments) { String playerMailOld = arguments.get(0); String playerMailNew = arguments.get(1); diff --git a/src/main/java/fr/xephi/authme/command/executable/email/EmailBaseCommand.java b/src/main/java/fr/xephi/authme/command/executable/email/EmailBaseCommand.java index 3a79843e..d75973f0 100644 --- a/src/main/java/fr/xephi/authme/command/executable/email/EmailBaseCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/email/EmailBaseCommand.java @@ -6,6 +6,7 @@ import fr.xephi.authme.command.FoundCommandResult; import fr.xephi.authme.command.help.HelpProvider; import org.bukkit.command.CommandSender; +import javax.inject.Inject; import java.util.Collections; import java.util.List; @@ -14,8 +15,11 @@ import java.util.List; */ public class EmailBaseCommand implements ExecutableCommand { + @Inject + private CommandService commandService; + @Override - public void executeCommand(CommandSender sender, List arguments, CommandService commandService) { + public void executeCommand(CommandSender sender, List arguments) { FoundCommandResult result = commandService.mapPartsToCommand(sender, Collections.singletonList("email")); commandService.outputHelp(sender, result, HelpProvider.SHOW_CHILDREN); } diff --git a/src/main/java/fr/xephi/authme/command/executable/email/RecoverEmailCommand.java b/src/main/java/fr/xephi/authme/command/executable/email/RecoverEmailCommand.java index cb9a4163..fac517ea 100644 --- a/src/main/java/fr/xephi/authme/command/executable/email/RecoverEmailCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/email/RecoverEmailCommand.java @@ -1,11 +1,5 @@ package fr.xephi.authme.command.executable.email; -import java.util.List; - -import javax.inject.Inject; - -import org.bukkit.entity.Player; - import fr.xephi.authme.AuthMe; import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.cache.auth.PlayerAuth; @@ -19,12 +13,19 @@ import fr.xephi.authme.security.RandomString; import fr.xephi.authme.security.crypts.HashedPassword; import fr.xephi.authme.settings.properties.EmailSettings; import fr.xephi.authme.util.StringUtils; +import org.bukkit.entity.Player; + +import javax.inject.Inject; +import java.util.List; public class RecoverEmailCommand extends PlayerCommand { @Inject private PasswordSecurity passwordSecurity; + @Inject + private CommandService commandService; + @Inject private DataSource dataSource; @@ -32,10 +33,11 @@ public class RecoverEmailCommand extends PlayerCommand { private PlayerCache playerCache; @Inject + // TODO #655: Remove injected AuthMe instance once Authme#mail is encapsulated private AuthMe plugin; @Override - public void runCommand(Player player, List arguments, CommandService commandService) { + public void runCommand(Player player, List arguments) { final String playerMail = arguments.get(0); final String playerName = player.getName(); diff --git a/src/main/java/fr/xephi/authme/command/executable/login/LoginCommand.java b/src/main/java/fr/xephi/authme/command/executable/login/LoginCommand.java index d10ef91a..8b4054ac 100644 --- a/src/main/java/fr/xephi/authme/command/executable/login/LoginCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/login/LoginCommand.java @@ -1,6 +1,5 @@ package fr.xephi.authme.command.executable.login; -import fr.xephi.authme.command.CommandService; import fr.xephi.authme.command.PlayerCommand; import fr.xephi.authme.process.Management; import org.bukkit.entity.Player; @@ -17,7 +16,7 @@ public class LoginCommand extends PlayerCommand { private Management management; @Override - public void runCommand(Player player, List arguments, CommandService commandService) { + public void runCommand(Player player, List arguments) { final String password = arguments.get(0); management.performLogin(player, password, false); } diff --git a/src/main/java/fr/xephi/authme/command/executable/logout/LogoutCommand.java b/src/main/java/fr/xephi/authme/command/executable/logout/LogoutCommand.java index 2236f125..83b17882 100644 --- a/src/main/java/fr/xephi/authme/command/executable/logout/LogoutCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/logout/LogoutCommand.java @@ -1,6 +1,5 @@ package fr.xephi.authme.command.executable.logout; -import fr.xephi.authme.command.CommandService; import fr.xephi.authme.command.PlayerCommand; import fr.xephi.authme.process.Management; import org.bukkit.entity.Player; @@ -17,7 +16,7 @@ public class LogoutCommand extends PlayerCommand { private Management management; @Override - public void runCommand(Player player, List arguments, CommandService commandService) { + public void runCommand(Player player, List arguments) { management.performLogout(player); } } 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 54f054e5..f9a6d165 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 @@ -24,8 +24,11 @@ public class RegisterCommand extends PlayerCommand { @Inject private Management management; + @Inject + private CommandService commandService; + @Override - public void runCommand(Player player, List arguments, CommandService commandService) { + public void runCommand(Player player, List arguments) { if (commandService.getProperty(SecuritySettings.PASSWORD_HASH) == HashAlgorithm.TWO_FACTOR) { //for two factor auth we don't need to check the usage management.performRegister(player, "", "", true); diff --git a/src/main/java/fr/xephi/authme/command/executable/unregister/UnregisterCommand.java b/src/main/java/fr/xephi/authme/command/executable/unregister/UnregisterCommand.java index 2d0a5bde..e289480e 100644 --- a/src/main/java/fr/xephi/authme/command/executable/unregister/UnregisterCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/unregister/UnregisterCommand.java @@ -15,8 +15,11 @@ public class UnregisterCommand extends PlayerCommand { @Inject private Management management; + @Inject + private CommandService commandService; + @Override - public void runCommand(Player player, List arguments, CommandService commandService) { + public void runCommand(Player player, List arguments) { String playerPass = arguments.get(0); final String playerNameLowerCase = player.getName().toLowerCase(); diff --git a/src/test/java/fr/xephi/authme/command/CommandHandlerTest.java b/src/test/java/fr/xephi/authme/command/CommandHandlerTest.java index 3e97627e..d604a5b6 100644 --- a/src/test/java/fr/xephi/authme/command/CommandHandlerTest.java +++ b/src/test/java/fr/xephi/authme/command/CommandHandlerTest.java @@ -47,7 +47,10 @@ public class CommandHandlerTest { private CommandHandler handler; @Mock - private CommandService serviceMock; + private CommandService commandService; + + @Mock + private CommandMapper commandMapper; @Mock private PermissionsManager permissionsManager; @@ -66,17 +69,17 @@ public class CommandHandlerTest { ExecutableCommand executableCommand = mock(ExecutableCommand.class); CommandDescription command = mock(CommandDescription.class); given(command.getExecutableCommand()).willReturn(executableCommand); - given(serviceMock.mapPartsToCommand(any(CommandSender.class), anyListOf(String.class))) + given(commandService.mapPartsToCommand(any(CommandSender.class), anyListOf(String.class))) .willReturn(new FoundCommandResult(command, asList("Authme", "Login"), asList("myPass"), 0.0, SUCCESS)); // when handler.processCommand(sender, bukkitLabel, bukkitArgs); // then - verify(serviceMock).mapPartsToCommand(eq(sender), captor.capture()); + verify(commandService).mapPartsToCommand(eq(sender), captor.capture()); assertThat(captor.getValue(), contains("Authme", "Login", "myPass")); - verify(executableCommand).executeCommand(eq(sender), captor.capture(), any(CommandService.class)); + verify(executableCommand).executeCommand(eq(sender), captor.capture()); assertThat(captor.getValue(), contains("myPass")); // Ensure that no error message was issued to the command sender @@ -90,14 +93,14 @@ public class CommandHandlerTest { String[] bukkitArgs = {"testPlayer"}; CommandSender sender = mock(CommandSender.class); CommandDescription command = mock(CommandDescription.class); - given(serviceMock.mapPartsToCommand(any(CommandSender.class), anyListOf(String.class))) + given(commandService.mapPartsToCommand(any(CommandSender.class), anyListOf(String.class))) .willReturn(new FoundCommandResult(command, asList("unreg"), asList("testPlayer"), 0.0, NO_PERMISSION)); // when handler.processCommand(sender, bukkitLabel, bukkitArgs); // then - verify(serviceMock).mapPartsToCommand(eq(sender), captor.capture()); + verify(commandService).mapPartsToCommand(eq(sender), captor.capture()); assertThat(captor.getValue(), contains("unreg", "testPlayer")); verify(command, never()).getExecutableCommand(); verify(sender).sendMessage(argThat(containsString("don't have permission"))); @@ -110,7 +113,7 @@ public class CommandHandlerTest { String[] bukkitArgs = {"testPlayer"}; CommandSender sender = mock(CommandSender.class); CommandDescription command = mock(CommandDescription.class); - given(serviceMock.mapPartsToCommand(any(CommandSender.class), anyListOf(String.class))).willReturn( + given(commandService.mapPartsToCommand(any(CommandSender.class), anyListOf(String.class))).willReturn( new FoundCommandResult(command, asList("unreg"), asList("testPlayer"), 0.0, INCORRECT_ARGUMENTS)); given(permissionsManager.hasPermission(sender, command.getPermission())).willReturn(true); @@ -118,7 +121,7 @@ public class CommandHandlerTest { handler.processCommand(sender, bukkitLabel, bukkitArgs); // then - verify(serviceMock).mapPartsToCommand(eq(sender), captor.capture()); + verify(commandService).mapPartsToCommand(eq(sender), captor.capture()); assertThat(captor.getValue(), contains("unreg", "testPlayer")); verify(command, never()).getExecutableCommand(); @@ -134,7 +137,7 @@ public class CommandHandlerTest { String[] bukkitArgs = {"testPlayer"}; CommandSender sender = mock(CommandSender.class); CommandDescription command = mock(CommandDescription.class); - given(serviceMock.mapPartsToCommand(any(CommandSender.class), anyListOf(String.class))).willReturn( + given(commandService.mapPartsToCommand(any(CommandSender.class), anyListOf(String.class))).willReturn( new FoundCommandResult(command, asList("unreg"), asList("testPlayer"), 0.0, INCORRECT_ARGUMENTS)); given(permissionsManager.hasPermission(sender, command.getPermission())).willReturn(false); @@ -142,7 +145,7 @@ public class CommandHandlerTest { handler.processCommand(sender, bukkitLabel, bukkitArgs); // then - verify(serviceMock).mapPartsToCommand(eq(sender), captor.capture()); + verify(commandService).mapPartsToCommand(eq(sender), captor.capture()); assertThat(captor.getValue(), contains("unreg", "testPlayer")); verify(command, never()).getExecutableCommand(); @@ -158,14 +161,14 @@ public class CommandHandlerTest { String[] bukkitArgs = {"testPlayer"}; CommandSender sender = mock(CommandSender.class); CommandDescription command = mock(CommandDescription.class); - given(serviceMock.mapPartsToCommand(any(CommandSender.class), anyListOf(String.class))).willReturn( + given(commandService.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()); + verify(commandService).mapPartsToCommand(eq(sender), captor.capture()); assertThat(captor.getValue(), contains("unreg", "testPlayer")); verify(command, never()).getExecutableCommand(); verify(sender).sendMessage(argThat(containsString("Failed to parse"))); @@ -179,14 +182,14 @@ public class CommandHandlerTest { 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( + given(commandService.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()); + verify(commandService).mapPartsToCommand(eq(sender), captor.capture()); assertThat(captor.getValue(), contains("unreg", "testPlayer")); verify(command, never()).getExecutableCommand(); @@ -207,14 +210,14 @@ public class CommandHandlerTest { 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( + given(commandService.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()); + verify(commandService).mapPartsToCommand(eq(sender), captor.capture()); assertThat(captor.getValue(), contains("unreg", "testPlayer")); verify(command, never()).getExecutableCommand(); @@ -235,17 +238,17 @@ public class CommandHandlerTest { ExecutableCommand executableCommand = mock(ExecutableCommand.class); CommandDescription command = mock(CommandDescription.class); given(command.getExecutableCommand()).willReturn(executableCommand); - given(serviceMock.mapPartsToCommand(eq(sender), anyListOf(String.class))) + given(commandService.mapPartsToCommand(eq(sender), anyListOf(String.class))) .willReturn(new FoundCommandResult(command, asList("AuthMe", "LOGIN"), asList("testArg"), 0.0, SUCCESS)); // when handler.processCommand(sender, bukkitLabel, bukkitArgs); // then - verify(serviceMock).mapPartsToCommand(eq(sender), captor.capture()); + verify(commandService).mapPartsToCommand(eq(sender), captor.capture()); assertThat(captor.getValue(), contains("AuthMe", "LOGIN", "testArg")); - verify(command.getExecutableCommand()).executeCommand(eq(sender), captor.capture(), eq(serviceMock)); + verify(command.getExecutableCommand()).executeCommand(eq(sender), captor.capture()); assertThat(captor.getValue(), contains("testArg")); verify(sender, never()).sendMessage(anyString()); diff --git a/src/test/java/fr/xephi/authme/command/CommandUtilsTest.java b/src/test/java/fr/xephi/authme/command/CommandUtilsTest.java index e3970fa6..a290967c 100644 --- a/src/test/java/fr/xephi/authme/command/CommandUtilsTest.java +++ b/src/test/java/fr/xephi/authme/command/CommandUtilsTest.java @@ -1,5 +1,6 @@ package fr.xephi.authme.command; +import fr.xephi.authme.TestHelper; import org.junit.Test; import java.util.Arrays; @@ -113,6 +114,12 @@ public class CommandUtilsTest { checkArgumentCount(command, 1, 3); } + @Test + public void shouldHaveHiddenConstructor() { + // given / when / then + TestHelper.validateHasOnlyPrivateEmptyConstructor(CommandUtils.class); + } + private static void checkArgumentCount(CommandDescription command, int expectedMin, int expectedMax) { assertThat(CommandUtils.getMinNumberOfArguments(command), equalTo(expectedMin)); diff --git a/src/test/java/fr/xephi/authme/command/PlayerCommandTest.java b/src/test/java/fr/xephi/authme/command/PlayerCommandTest.java index a4c02b93..f7641215 100644 --- a/src/test/java/fr/xephi/authme/command/PlayerCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/PlayerCommandTest.java @@ -27,7 +27,7 @@ public class PlayerCommandTest { PlayerCommandImpl command = new PlayerCommandImpl(); // when - command.executeCommand(sender, Collections.emptyList(), mock(CommandService.class)); + command.executeCommand(sender, Collections.emptyList()); // then verify(sender).sendMessage(argThat(containsString("only for players"))); @@ -42,7 +42,7 @@ public class PlayerCommandTest { PlayerCommandImpl command = new PlayerCommandImpl(); // when - command.executeCommand(player, arguments, service); + command.executeCommand(player, arguments); // then verify(player, times(1)).sendMessage("testarg2"); @@ -55,7 +55,7 @@ public class PlayerCommandTest { PlayerCommandWithAlt command = new PlayerCommandWithAlt(); // when - command.executeCommand(sender, Collections.emptyList(), mock(CommandService.class)); + command.executeCommand(sender, Collections.emptyList()); // then verify(sender, times(1)).sendMessage(argThat(containsString("use /authme test instead"))); @@ -64,14 +64,14 @@ public class PlayerCommandTest { private static class PlayerCommandImpl extends PlayerCommand { @Override - public void runCommand(Player player, List arguments, CommandService commandService) { + public void runCommand(Player player, List arguments) { player.sendMessage(arguments.get(1)); } } private static class PlayerCommandWithAlt extends PlayerCommand { @Override - public void runCommand(Player player, List arguments, CommandService commandService) { + public void runCommand(Player player, List arguments) { throw new IllegalStateException("Should not be called"); } @Override diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/AccountsCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/AccountsCommandTest.java index 404a2f58..df464811 100644 --- a/src/test/java/fr/xephi/authme/command/executable/authme/AccountsCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/authme/AccountsCommandTest.java @@ -53,7 +53,7 @@ public class AccountsCommandTest { given(dataSource.getAllAuthsByIp("123.45.67.89")).willReturn(Arrays.asList("Toaster", "Pester")); // when - command.executeCommand(sender, arguments, service); + command.executeCommand(sender, arguments); runInnerRunnable(bukkitService); // then @@ -69,7 +69,7 @@ public class AccountsCommandTest { given(dataSource.getAuth("someuser")).willReturn(null); // when - command.executeCommand(sender, arguments, service); + command.executeCommand(sender, arguments); runInnerRunnable(bukkitService); // then @@ -85,7 +85,7 @@ public class AccountsCommandTest { given(dataSource.getAllAuthsByIp(anyString())).willReturn(Collections.emptyList()); // when - command.executeCommand(sender, arguments, service); + command.executeCommand(sender, arguments); runInnerRunnable(bukkitService); // then @@ -101,7 +101,7 @@ public class AccountsCommandTest { given(dataSource.getAllAuthsByIp("56.78.90.123")).willReturn(Collections.singletonList("SomeUser")); // when - command.executeCommand(sender, arguments, service); + command.executeCommand(sender, arguments); runInnerRunnable(bukkitService); // then @@ -119,7 +119,7 @@ public class AccountsCommandTest { given(dataSource.getAllAuthsByIp("123.45.67.89")).willReturn(Collections.emptyList()); // when - command.executeCommand(sender, arguments, service); + command.executeCommand(sender, arguments); runInnerRunnable(bukkitService); // then @@ -134,7 +134,7 @@ public class AccountsCommandTest { given(dataSource.getAllAuthsByIp("24.24.48.48")).willReturn(Collections.singletonList("SomeUser")); // when - command.executeCommand(sender, arguments, service); + command.executeCommand(sender, arguments); runInnerRunnable(bukkitService); // then @@ -149,7 +149,7 @@ public class AccountsCommandTest { given(dataSource.getAllAuthsByIp("98.76.41.122")).willReturn(Arrays.asList("Tester", "Lester", "Taster")); // when - command.executeCommand(sender, arguments, service); + command.executeCommand(sender, arguments); runInnerRunnable(bukkitService); // then diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/AuthMeCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/AuthMeCommandTest.java index c1c0c6e3..f17f75fe 100644 --- a/src/test/java/fr/xephi/authme/command/executable/authme/AuthMeCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/authme/AuthMeCommandTest.java @@ -1,6 +1,5 @@ package fr.xephi.authme.command.executable.authme; -import fr.xephi.authme.command.CommandService; import fr.xephi.authme.command.ExecutableCommand; import org.bukkit.command.CommandSender; import org.junit.Test; @@ -24,10 +23,9 @@ public class AuthMeCommandTest { // given ExecutableCommand command = new AuthMeCommand(); CommandSender sender = mock(CommandSender.class); - CommandService service = mock(CommandService.class); // when - command.executeCommand(sender, Collections. emptyList(), service); + command.executeCommand(sender, Collections.emptyList()); // then ArgumentCaptor messagesCaptor = ArgumentCaptor.forClass(String.class); diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/ChangePasswordAdminCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/ChangePasswordAdminCommandTest.java index 5ab9ba0d..e4aea7ed 100644 --- a/src/test/java/fr/xephi/authme/command/executable/authme/ChangePasswordAdminCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/authme/ChangePasswordAdminCommandTest.java @@ -69,7 +69,7 @@ public class ChangePasswordAdminCommandTest { new ValidationResult(MessageKey.PASSWORD_IS_USERNAME_ERROR)); // when - command.executeCommand(sender, Arrays.asList("bobby", "Bobby"), service); + command.executeCommand(sender, Arrays.asList("bobby", "Bobby")); // then verify(validationService).validatePassword("Bobby", "bobby"); @@ -88,7 +88,7 @@ public class ChangePasswordAdminCommandTest { given(validationService.validatePassword(password, player)).willReturn(new ValidationResult()); // when - command.executeCommand(sender, Arrays.asList(player, password), service); + command.executeCommand(sender, Arrays.asList(player, password)); runInnerRunnable(bukkitService); // then @@ -113,7 +113,7 @@ public class ChangePasswordAdminCommandTest { given(validationService.validatePassword(password, player)).willReturn(new ValidationResult()); // when - command.executeCommand(sender, Arrays.asList(player, password), service); + command.executeCommand(sender, Arrays.asList(player, password)); runInnerRunnable(bukkitService); // then @@ -141,7 +141,7 @@ public class ChangePasswordAdminCommandTest { given(passwordSecurity.computeHash(password, player)).willReturn(hashedPassword); // when - command.executeCommand(sender, Arrays.asList(player, password), service); + command.executeCommand(sender, Arrays.asList(player, password)); runInnerRunnable(bukkitService); // then @@ -168,7 +168,7 @@ public class ChangePasswordAdminCommandTest { given(dataSource.updatePassword(auth)).willReturn(false); // when - command.executeCommand(sender, Arrays.asList(player, password), service); + command.executeCommand(sender, Arrays.asList(player, password)); runInnerRunnable(bukkitService); // then diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/FirstSpawnCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/FirstSpawnCommandTest.java index 42edaeb2..000c7922 100644 --- a/src/test/java/fr/xephi/authme/command/executable/authme/FirstSpawnCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/authme/FirstSpawnCommandTest.java @@ -1,6 +1,5 @@ package fr.xephi.authme.command.executable.authme; -import fr.xephi.authme.command.CommandService; import fr.xephi.authme.settings.SpawnLoader; import org.bukkit.Location; import org.bukkit.entity.Player; @@ -33,9 +32,6 @@ public class FirstSpawnCommandTest { @Mock private SpawnLoader spawnLoader; - @Mock - private CommandService service; - @Test public void shouldTeleportToFirstSpawn() { // given @@ -44,7 +40,7 @@ public class FirstSpawnCommandTest { Player player = mock(Player.class); // when - command.executeCommand(player, Collections.emptyList(), service); + command.executeCommand(player, Collections.emptyList()); // then verify(player).teleport(firstSpawn); @@ -58,7 +54,7 @@ public class FirstSpawnCommandTest { Player player = mock(Player.class); // when - command.executeCommand(player, Collections.emptyList(), service); + command.executeCommand(player, Collections.emptyList()); // then verify(player).sendMessage(argThat(containsString("spawn has failed"))); diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/ForceLoginCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/ForceLoginCommandTest.java index 8896be7c..9f89fe5a 100644 --- a/src/test/java/fr/xephi/authme/command/executable/authme/ForceLoginCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/authme/ForceLoginCommandTest.java @@ -1,6 +1,5 @@ package fr.xephi.authme.command.executable.authme; -import fr.xephi.authme.command.CommandService; import fr.xephi.authme.permission.PermissionsManager; import fr.xephi.authme.permission.PlayerPermission; import fr.xephi.authme.process.Management; @@ -43,9 +42,6 @@ public class ForceLoginCommandTest { @Mock private BukkitService bukkitService; - @Mock - private CommandService commandService; - @Test public void shouldRejectOfflinePlayer() { // given @@ -55,7 +51,7 @@ public class ForceLoginCommandTest { CommandSender sender = mock(CommandSender.class); // when - command.executeCommand(sender, Collections.singletonList(playerName), commandService); + command.executeCommand(sender, Collections.singletonList(playerName)); // then verify(bukkitService).getPlayerExact(playerName); @@ -71,7 +67,7 @@ public class ForceLoginCommandTest { CommandSender sender = mock(CommandSender.class); // when - command.executeCommand(sender, Collections.singletonList(playerName), commandService); + command.executeCommand(sender, Collections.singletonList(playerName)); // then verify(bukkitService).getPlayerExact(playerName); @@ -89,7 +85,7 @@ public class ForceLoginCommandTest { CommandSender sender = mock(CommandSender.class); // when - command.executeCommand(sender, Collections.singletonList(playerName), commandService); + command.executeCommand(sender, Collections.singletonList(playerName)); // then verify(bukkitService).getPlayerExact(playerName); @@ -107,7 +103,7 @@ public class ForceLoginCommandTest { CommandSender sender = mock(CommandSender.class); // when - command.executeCommand(sender, Collections.singletonList(playerName), commandService); + command.executeCommand(sender, Collections.singletonList(playerName)); // then verify(bukkitService).getPlayerExact(playerName); @@ -125,7 +121,7 @@ public class ForceLoginCommandTest { given(sender.getName()).willReturn(senderName); // when - command.executeCommand(sender, Collections.emptyList(), commandService); + command.executeCommand(sender, Collections.emptyList()); // then verify(bukkitService).getPlayerExact(senderName); diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/GetEmailCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/GetEmailCommandTest.java index f03ff426..6afdc329 100644 --- a/src/test/java/fr/xephi/authme/command/executable/authme/GetEmailCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/authme/GetEmailCommandTest.java @@ -31,9 +31,6 @@ public class GetEmailCommandTest { @Mock private DataSource dataSource; - @Mock - private CommandSender sender; - @Mock private CommandService service; @@ -42,9 +39,10 @@ public class GetEmailCommandTest { // given String user = "myTestUser"; given(dataSource.getAuth(user)).willReturn(null); + CommandSender sender = mock(CommandSender.class); // when - command.executeCommand(sender, Collections.singletonList(user), service); + command.executeCommand(sender, Collections.singletonList(user)); // then verify(service).send(sender, MessageKey.UNKNOWN_USER); @@ -58,9 +56,10 @@ public class GetEmailCommandTest { PlayerAuth auth = mock(PlayerAuth.class); given(auth.getEmail()).willReturn(email); given(dataSource.getAuth(user)).willReturn(auth); + CommandSender sender = mock(CommandSender.class); // when - command.executeCommand(sender, Collections.singletonList(user), service); + command.executeCommand(sender, Collections.singletonList(user)); // then verify(sender).sendMessage(argThat(containsString(email))); diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/GetIpCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/GetIpCommandTest.java index 8d0eadc5..d3d5c376 100644 --- a/src/test/java/fr/xephi/authme/command/executable/authme/GetIpCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/authme/GetIpCommandTest.java @@ -1,6 +1,5 @@ package fr.xephi.authme.command.executable.authme; -import fr.xephi.authme.command.CommandService; import fr.xephi.authme.util.BukkitService; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; @@ -31,22 +30,18 @@ public class GetIpCommandTest { @InjectMocks private GetIpCommand command; - @Mock - private CommandService commandService; - @Mock private BukkitService bukkitService; - @Mock - private CommandSender sender; @Test public void shouldGetIpOfPlayer() { // given given(bukkitService.getPlayerExact(anyString())).willReturn(null); + CommandSender sender = mock(CommandSender.class); // when - command.executeCommand(sender, Collections.singletonList("Testt"), commandService); + command.executeCommand(sender, Collections.singletonList("Testt")); // then verify(bukkitService).getPlayerExact("Testt"); @@ -60,9 +55,10 @@ public class GetIpCommandTest { String ip = "123.34.56.88"; Player player = mockPlayer(playerName, ip); given(bukkitService.getPlayerExact(playerName)).willReturn(player); + CommandSender sender = mock(CommandSender.class); // when - command.executeCommand(sender, Collections.singletonList(playerName), commandService); + command.executeCommand(sender, Collections.singletonList(playerName)); // then verify(bukkitService).getPlayerExact(playerName); diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/LastLoginCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/LastLoginCommandTest.java index 04ffabef..d42996ee 100644 --- a/src/test/java/fr/xephi/authme/command/executable/authme/LastLoginCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/authme/LastLoginCommandTest.java @@ -29,6 +29,9 @@ import static org.mockito.Mockito.verify; @RunWith(MockitoJUnitRunner.class) public class LastLoginCommandTest { + private static final long HOUR_IN_MSEC = 3600 * 1000; + private static final long DAY_IN_MSEC = 24 * HOUR_IN_MSEC; + @InjectMocks private LastLoginCommand command; @@ -38,20 +41,16 @@ public class LastLoginCommandTest { @Mock private CommandService service; - @Mock - private CommandSender sender; - - private static final long HOUR_IN_MSEC = 3600 * 1000; - private static final long DAY_IN_MSEC = 24 * HOUR_IN_MSEC; @Test public void shouldRejectNonExistentUser() { // given String player = "tester"; given(dataSource.getAuth(player)).willReturn(null); + CommandSender sender = mock(CommandSender.class); // when - command.executeCommand(sender, Collections.singletonList(player), service); + command.executeCommand(sender, Collections.singletonList(player)); // then verify(dataSource).getAuth(player); @@ -68,9 +67,10 @@ public class LastLoginCommandTest { given(auth.getLastLogin()).willReturn(lastLogin); given(auth.getIp()).willReturn("123.45.66.77"); given(dataSource.getAuth(player)).willReturn(auth); + CommandSender sender = mock(CommandSender.class); // when - command.executeCommand(sender, Collections.singletonList(player), service); + command.executeCommand(sender, Collections.singletonList(player)); // then verify(dataSource).getAuth(player); @@ -87,6 +87,7 @@ public class LastLoginCommandTest { public void shouldDisplayLastLoginOfCommandSender() { // given String name = "CommandSender"; + CommandSender sender = mock(CommandSender.class); given(sender.getName()).willReturn(name); long lastLogin = System.currentTimeMillis() - @@ -97,7 +98,7 @@ public class LastLoginCommandTest { given(dataSource.getAuth(name)).willReturn(auth); // when - command.executeCommand(sender, Collections.emptyList(), service); + command.executeCommand(sender, Collections.emptyList()); // then verify(dataSource).getAuth(name); diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/PurgeLastPositionCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/PurgeLastPositionCommandTest.java index 2c80a19d..e3a79db8 100644 --- a/src/test/java/fr/xephi/authme/command/executable/authme/PurgeLastPositionCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/authme/PurgeLastPositionCommandTest.java @@ -35,8 +35,6 @@ public class PurgeLastPositionCommandTest { @Mock private CommandService service; - @Mock - private CommandSender sender; @Test public void shouldPurgeLastPosOfUser() { @@ -44,9 +42,10 @@ public class PurgeLastPositionCommandTest { String player = "_Bobby"; PlayerAuth auth = mock(PlayerAuth.class); given(dataSource.getAuth(player)).willReturn(auth); + CommandSender sender = mock(CommandSender.class); // when - command.executeCommand(sender, Collections.singletonList(player), service); + command.executeCommand(sender, Collections.singletonList(player)); // then verify(dataSource).getAuth(player); @@ -64,7 +63,7 @@ public class PurgeLastPositionCommandTest { given(dataSource.getAuth(player)).willReturn(auth); // when - command.executeCommand(sender, Collections.emptyList(), service); + command.executeCommand(sender, Collections.emptyList()); // then verify(dataSource).getAuth(player); @@ -76,9 +75,10 @@ public class PurgeLastPositionCommandTest { public void shouldHandleNonExistentUser() { // given String name = "invalidPlayer"; + CommandSender sender = mock(CommandSender.class); // when - command.executeCommand(sender, Collections.singletonList(name), service); + command.executeCommand(sender, Collections.singletonList(name)); // then verify(dataSource).getAuth(name); @@ -92,9 +92,10 @@ public class PurgeLastPositionCommandTest { PlayerAuth auth2 = mock(PlayerAuth.class); PlayerAuth auth3 = mock(PlayerAuth.class); given(dataSource.getAllAuths()).willReturn(Arrays.asList(auth1, auth2, auth3)); + CommandSender sender = mock(CommandSender.class); // when - command.executeCommand(sender, Collections.singletonList("*"), service); + command.executeCommand(sender, Collections.singletonList("*")); // then verify(dataSource).getAllAuths(); diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/RegisterAdminCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/RegisterAdminCommandTest.java index c82b7e0c..d6d0924e 100644 --- a/src/test/java/fr/xephi/authme/command/executable/authme/RegisterAdminCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/authme/RegisterAdminCommandTest.java @@ -71,7 +71,7 @@ public class RegisterAdminCommandTest { CommandSender sender = mock(CommandSender.class); // when - command.executeCommand(sender, Arrays.asList(user, password), commandService); + command.executeCommand(sender, Arrays.asList(user, password)); // then verify(validationService).validatePassword(password, user); @@ -89,7 +89,7 @@ public class RegisterAdminCommandTest { CommandSender sender = mock(CommandSender.class); // when - command.executeCommand(sender, Arrays.asList(user, password), commandService); + command.executeCommand(sender, Arrays.asList(user, password)); TestHelper.runInnerRunnable(bukkitService); // then @@ -111,7 +111,7 @@ public class RegisterAdminCommandTest { CommandSender sender = mock(CommandSender.class); // when - command.executeCommand(sender, Arrays.asList(user, password), commandService); + command.executeCommand(sender, Arrays.asList(user, password)); TestHelper.runInnerRunnable(bukkitService); // then @@ -136,7 +136,7 @@ public class RegisterAdminCommandTest { CommandSender sender = mock(CommandSender.class); // when - command.executeCommand(sender, Arrays.asList(user, password), commandService); + command.executeCommand(sender, Arrays.asList(user, password)); TestHelper.runInnerRunnable(bukkitService); // then @@ -163,7 +163,7 @@ public class RegisterAdminCommandTest { CommandSender sender = mock(CommandSender.class); // when - command.executeCommand(sender, Arrays.asList(user, password), commandService); + command.executeCommand(sender, Arrays.asList(user, password)); TestHelper.runInnerRunnable(bukkitService); runSyncDelayedTask(bukkitService); diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/ReloadCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/ReloadCommandTest.java index d9c28b64..2371a500 100644 --- a/src/test/java/fr/xephi/authme/command/executable/authme/ReloadCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/authme/ReloadCommandTest.java @@ -50,6 +50,9 @@ public class ReloadCommandTest { @Mock private DataSource dataSource; + @Mock + private CommandService commandService; + @BeforeClass public static void setUpLogger() { TestHelper.setupLogger(); @@ -66,17 +69,16 @@ public class ReloadCommandTest { public void shouldReload() { // given CommandSender sender = mock(CommandSender.class); - CommandService service = mock(CommandService.class); given(settings.getProperty(DatabaseSettings.BACKEND)).willReturn(DataSourceType.MYSQL); given(dataSource.getType()).willReturn(DataSourceType.MYSQL); // when - command.executeCommand(sender, Collections.emptyList(), service); + command.executeCommand(sender, Collections.emptyList()); // then verify(settings).reload(); verify(initializer).performReloadOnServices(); - verify(service).send(sender, MessageKey.CONFIG_RELOAD_SUCCESS); + verify(commandService).send(sender, MessageKey.CONFIG_RELOAD_SUCCESS); } @Test @@ -89,7 +91,7 @@ public class ReloadCommandTest { given(dataSource.getType()).willReturn(DataSourceType.MYSQL); // when - command.executeCommand(sender, Collections.emptyList(), service); + command.executeCommand(sender, Collections.emptyList()); // then verify(settings).reload(); @@ -107,7 +109,7 @@ public class ReloadCommandTest { given(dataSource.getType()).willReturn(DataSourceType.SQLITE); // when - command.executeCommand(sender, Collections.emptyList(), service); + command.executeCommand(sender, Collections.emptyList()); // then verify(settings).reload(); diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/SetFirstSpawnCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/SetFirstSpawnCommandTest.java index 17163a3a..0974da72 100644 --- a/src/test/java/fr/xephi/authme/command/executable/authme/SetFirstSpawnCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/authme/SetFirstSpawnCommandTest.java @@ -1,6 +1,5 @@ package fr.xephi.authme.command.executable.authme; -import fr.xephi.authme.command.CommandService; import fr.xephi.authme.settings.SpawnLoader; import org.bukkit.Location; import org.bukkit.entity.Player; @@ -30,8 +29,6 @@ public class SetFirstSpawnCommandTest { @Mock private SpawnLoader spawnLoader; - @Mock - private CommandService service; @Test public void shouldSetFirstSpawn() { @@ -42,7 +39,7 @@ public class SetFirstSpawnCommandTest { given(spawnLoader.setFirstSpawn(location)).willReturn(true); // when - command.executeCommand(player, Collections.emptyList(), service); + command.executeCommand(player, Collections.emptyList()); // then verify(spawnLoader).setFirstSpawn(location); @@ -58,7 +55,7 @@ public class SetFirstSpawnCommandTest { given(spawnLoader.setFirstSpawn(location)).willReturn(false); // when - command.executeCommand(player, Collections.emptyList(), service); + command.executeCommand(player, Collections.emptyList()); // then verify(spawnLoader).setFirstSpawn(location); diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/SetSpawnCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/SetSpawnCommandTest.java index 77e916d9..8522f997 100644 --- a/src/test/java/fr/xephi/authme/command/executable/authme/SetSpawnCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/authme/SetSpawnCommandTest.java @@ -1,6 +1,5 @@ package fr.xephi.authme.command.executable.authme; -import fr.xephi.authme.command.CommandService; import fr.xephi.authme.settings.SpawnLoader; import org.bukkit.Location; import org.bukkit.entity.Player; @@ -30,8 +29,6 @@ public class SetSpawnCommandTest { @Mock private SpawnLoader spawnLoader; - @Mock - private CommandService service; @Test public void shouldSetSpawn() { @@ -42,7 +39,7 @@ public class SetSpawnCommandTest { given(spawnLoader.setSpawn(location)).willReturn(true); // when - command.executeCommand(player, Collections.emptyList(), service); + command.executeCommand(player, Collections.emptyList()); // then verify(spawnLoader).setSpawn(location); @@ -58,7 +55,7 @@ public class SetSpawnCommandTest { given(spawnLoader.setSpawn(location)).willReturn(false); // when - command.executeCommand(player, Collections.emptyList(), service); + command.executeCommand(player, Collections.emptyList()); // then verify(spawnLoader).setSpawn(location); diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/SpawnCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/SpawnCommandTest.java index 64d006e2..81a7f416 100644 --- a/src/test/java/fr/xephi/authme/command/executable/authme/SpawnCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/authme/SpawnCommandTest.java @@ -1,6 +1,5 @@ package fr.xephi.authme.command.executable.authme; -import fr.xephi.authme.command.CommandService; import fr.xephi.authme.settings.SpawnLoader; import org.bukkit.Location; import org.bukkit.entity.Player; @@ -33,8 +32,6 @@ public class SpawnCommandTest { @Mock private SpawnLoader spawnLoader; - @Mock - private CommandService service; @Test public void shouldTeleportToSpawn() { @@ -44,7 +41,7 @@ public class SpawnCommandTest { Player player = mock(Player.class); // when - command.executeCommand(player, Collections.emptyList(), service); + command.executeCommand(player, Collections.emptyList()); // then verify(player).teleport(spawn); @@ -58,7 +55,7 @@ public class SpawnCommandTest { Player player = mock(Player.class); // when - command.executeCommand(player, Collections.emptyList(), service); + command.executeCommand(player, Collections.emptyList()); // then verify(player).sendMessage(argThat(containsString("Spawn has failed"))); diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/SwitchAntiBotCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/SwitchAntiBotCommandTest.java index f8945a1c..cd704557 100644 --- a/src/test/java/fr/xephi/authme/command/executable/authme/SwitchAntiBotCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/authme/SwitchAntiBotCommandTest.java @@ -44,7 +44,7 @@ public class SwitchAntiBotCommandTest { CommandSender sender = mock(CommandSender.class); // when - command.executeCommand(sender, Collections.emptyList(), service); + command.executeCommand(sender, Collections.emptyList()); // then verify(sender).sendMessage(argThat(containsString("status: ACTIVE"))); @@ -56,7 +56,7 @@ public class SwitchAntiBotCommandTest { CommandSender sender = mock(CommandSender.class); // when - command.executeCommand(sender, Collections.singletonList("on"), service); + command.executeCommand(sender, Collections.singletonList("on")); // then verify(antiBot).overrideAntiBotStatus(true); @@ -69,7 +69,7 @@ public class SwitchAntiBotCommandTest { CommandSender sender = mock(CommandSender.class); // when - command.executeCommand(sender, Collections.singletonList("Off"), service); + command.executeCommand(sender, Collections.singletonList("Off")); // then verify(antiBot).overrideAntiBotStatus(false); @@ -84,7 +84,7 @@ public class SwitchAntiBotCommandTest { given(service.mapPartsToCommand(sender, asList("authme", "antibot"))).willReturn(foundCommandResult); // when - command.executeCommand(sender, Collections.singletonList("wrong"), service); + command.executeCommand(sender, Collections.singletonList("wrong")); // then verify(antiBot, never()).overrideAntiBotStatus(anyBoolean()); 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 52529d19..5de45e61 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 @@ -44,7 +44,7 @@ public class CaptchaCommandTest { given(playerCache.isAuthenticated(name)).willReturn(true); // when - command.executeCommand(player, Collections.singletonList("123"), commandService); + command.executeCommand(player, Collections.singletonList("123")); // then verify(commandService).send(player, MessageKey.ALREADY_LOGGED_IN_ERROR); @@ -59,7 +59,7 @@ public class CaptchaCommandTest { given(captchaManager.isCaptchaRequired(name)).willReturn(false); // when - command.executeCommand(player, Collections.singletonList("1234"), commandService); + command.executeCommand(player, Collections.singletonList("1234")); // then verify(commandService).send(player, MessageKey.USAGE_LOGIN); @@ -78,7 +78,7 @@ public class CaptchaCommandTest { given(captchaManager.checkCode(name, captchaCode)).willReturn(true); // when - command.executeCommand(player, Collections.singletonList(captchaCode), commandService); + command.executeCommand(player, Collections.singletonList(captchaCode)); // then verify(captchaManager).isCaptchaRequired(name); @@ -102,7 +102,7 @@ public class CaptchaCommandTest { given(captchaManager.generateCode(name)).willReturn(newCode); // when - command.executeCommand(player, Collections.singletonList(captchaCode), commandService); + command.executeCommand(player, Collections.singletonList(captchaCode)); // then verify(captchaManager).isCaptchaRequired(name); 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 57b01006..1c19b945 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 @@ -73,7 +73,7 @@ public class ChangePasswordCommandTest { CommandSender sender = mock(BlockCommandSender.class); // when - command.executeCommand(sender, new ArrayList(), commandService); + command.executeCommand(sender, new ArrayList()); // then verify(sender).sendMessage(argThat(containsString("only for players"))); @@ -85,7 +85,7 @@ public class ChangePasswordCommandTest { CommandSender sender = initPlayerWithName("name", false); // when - command.executeCommand(sender, Arrays.asList("pass", "pass"), commandService); + command.executeCommand(sender, Arrays.asList("pass", "pass")); // then verify(commandService).send(sender, MessageKey.NOT_LOGGED_IN); @@ -100,7 +100,7 @@ public class ChangePasswordCommandTest { .willReturn(new ValidationResult(MessageKey.INVALID_PASSWORD_LENGTH)); // when - command.executeCommand(sender, Arrays.asList("tester", password), commandService); + command.executeCommand(sender, Arrays.asList("tester", password)); // then verify(validationService).validatePassword(password, "abc12"); @@ -114,7 +114,7 @@ public class ChangePasswordCommandTest { given(validationService.validatePassword("abc123", "parker")).willReturn(new ValidationResult()); // when - command.executeCommand(sender, Arrays.asList("abc123", "abc123"), commandService); + command.executeCommand(sender, Arrays.asList("abc123", "abc123")); // then verify(validationService).validatePassword("abc123", "parker"); diff --git a/src/test/java/fr/xephi/authme/command/executable/email/AddEmailCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/email/AddEmailCommandTest.java index 9b91ee5b..143ac5fe 100644 --- a/src/test/java/fr/xephi/authme/command/executable/email/AddEmailCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/email/AddEmailCommandTest.java @@ -41,7 +41,7 @@ public class AddEmailCommandTest { CommandSender sender = mock(BlockCommandSender.class); // when - command.executeCommand(sender, new ArrayList(), commandService); + command.executeCommand(sender, new ArrayList()); // then verifyZeroInteractions(management); @@ -55,7 +55,7 @@ public class AddEmailCommandTest { given(commandService.validateEmail(email)).willReturn(true); // when - command.executeCommand(sender, Arrays.asList(email, email), commandService); + command.executeCommand(sender, Arrays.asList(email, email)); // then verify(management).performAddEmail(sender, email); @@ -69,7 +69,7 @@ public class AddEmailCommandTest { given(commandService.validateEmail(email)).willReturn(true); // when - command.executeCommand(sender, Arrays.asList(email, "wrongConf"), commandService); + command.executeCommand(sender, Arrays.asList(email, "wrongConf")); // then verifyZeroInteractions(management); diff --git a/src/test/java/fr/xephi/authme/command/executable/email/ChangeEmailCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/email/ChangeEmailCommandTest.java index d9f7e0cd..240ace31 100644 --- a/src/test/java/fr/xephi/authme/command/executable/email/ChangeEmailCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/email/ChangeEmailCommandTest.java @@ -1,6 +1,5 @@ package fr.xephi.authme.command.executable.email; -import fr.xephi.authme.command.CommandService; import fr.xephi.authme.process.Management; import org.bukkit.command.BlockCommandSender; import org.bukkit.command.CommandSender; @@ -30,9 +29,6 @@ public class ChangeEmailCommandTest { @Mock private Management management; - @Mock - private CommandService commandService; - @Test public void shouldRejectNonPlayerSender() { @@ -40,7 +36,7 @@ public class ChangeEmailCommandTest { CommandSender sender = mock(BlockCommandSender.class); // when - command.executeCommand(sender, new ArrayList(), commandService); + command.executeCommand(sender, new ArrayList()); // then verifyZeroInteractions(management); @@ -52,7 +48,7 @@ public class ChangeEmailCommandTest { Player sender = mock(Player.class); // when - command.executeCommand(sender, Arrays.asList("new.mail@example.org", "old_mail@example.org"), commandService); + command.executeCommand(sender, Arrays.asList("new.mail@example.org", "old_mail@example.org")); // then verify(management).performChangeEmail(sender, "new.mail@example.org", "old_mail@example.org"); diff --git a/src/test/java/fr/xephi/authme/command/executable/login/LoginCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/login/LoginCommandTest.java index 2eede4a1..21e938bf 100644 --- a/src/test/java/fr/xephi/authme/command/executable/login/LoginCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/login/LoginCommandTest.java @@ -1,6 +1,5 @@ package fr.xephi.authme.command.executable.login; -import fr.xephi.authme.command.CommandService; import fr.xephi.authme.process.Management; import org.bukkit.command.BlockCommandSender; import org.bukkit.command.CommandSender; @@ -33,8 +32,6 @@ public class LoginCommandTest { @Mock private Management management; - @Mock - private CommandService commandService; @Test public void shouldStopIfSenderIsNotAPlayer() { @@ -42,7 +39,7 @@ public class LoginCommandTest { CommandSender sender = mock(BlockCommandSender.class); // when - command.executeCommand(sender, new ArrayList(), commandService); + command.executeCommand(sender, new ArrayList()); // then verifyZeroInteractions(management); @@ -55,7 +52,7 @@ public class LoginCommandTest { Player sender = mock(Player.class); // when - command.executeCommand(sender, Collections.singletonList("password"), commandService); + command.executeCommand(sender, Collections.singletonList("password")); // then verify(management).performLogin(eq(sender), eq("password"), eq(false)); diff --git a/src/test/java/fr/xephi/authme/command/executable/logout/LogoutCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/logout/LogoutCommandTest.java index bca24ac2..7bc9eeb1 100644 --- a/src/test/java/fr/xephi/authme/command/executable/logout/LogoutCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/logout/LogoutCommandTest.java @@ -1,11 +1,9 @@ package fr.xephi.authme.command.executable.logout; -import fr.xephi.authme.command.CommandService; import fr.xephi.authme.process.Management; import org.bukkit.command.BlockCommandSender; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; -import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; @@ -33,13 +31,6 @@ public class LogoutCommandTest { @Mock private Management management; - @Mock - private CommandService commandService; - - @Before - public void initializeAuthMeMock() { - commandService = mock(CommandService.class); - } @Test public void shouldStopIfSenderIsNotAPlayer() { @@ -47,7 +38,7 @@ public class LogoutCommandTest { CommandSender sender = mock(BlockCommandSender.class); // when - command.executeCommand(sender, new ArrayList(), commandService); + command.executeCommand(sender, new ArrayList()); // then verifyZeroInteractions(management); @@ -60,7 +51,7 @@ public class LogoutCommandTest { Player sender = mock(Player.class); // when - command.executeCommand(sender, Collections.singletonList("password"), commandService); + command.executeCommand(sender, Collections.singletonList("password")); // then verify(management).performLogout(sender); diff --git a/src/test/java/fr/xephi/authme/command/executable/register/RegisterCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/register/RegisterCommandTest.java index f33ad87e..217b74be 100644 --- a/src/test/java/fr/xephi/authme/command/executable/register/RegisterCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/register/RegisterCommandTest.java @@ -49,8 +49,6 @@ public class RegisterCommandTest { @Mock private Management management; - @Mock - private Player sender; @BeforeClass public static void setup() { @@ -70,7 +68,7 @@ public class RegisterCommandTest { CommandSender sender = mock(BlockCommandSender.class); // when - command.executeCommand(sender, new ArrayList(), commandService); + command.executeCommand(sender, new ArrayList()); // then verify(sender).sendMessage(argThat(containsString("Player only!"))); @@ -81,21 +79,25 @@ public class RegisterCommandTest { public void shouldForwardToManagementForTwoFactor() { // given given(commandService.getProperty(SecuritySettings.PASSWORD_HASH)).willReturn(HashAlgorithm.TWO_FACTOR); + Player player = mock(Player.class); // when - command.executeCommand(sender, Collections.emptyList(), commandService); + command.executeCommand(player, Collections.emptyList()); // then - verify(management).performRegister(sender, "", "", true); + verify(management).performRegister(player, "", "", true); } @Test public void shouldReturnErrorForEmptyArguments() { - // given / when - command.executeCommand(sender, Collections.emptyList(), commandService); + // given + Player player = mock(Player.class); + + // when + command.executeCommand(player, Collections.emptyList()); // then - verify(commandService).send(sender, MessageKey.USAGE_REGISTER); + verify(commandService).send(player, MessageKey.USAGE_REGISTER); verifyZeroInteractions(management); } @@ -103,12 +105,13 @@ public class RegisterCommandTest { public void shouldReturnErrorForMissingConfirmation() { // given given(commandService.getProperty(RestrictionSettings.ENABLE_PASSWORD_CONFIRMATION)).willReturn(true); + Player player = mock(Player.class); // when - command.executeCommand(sender, Collections.singletonList("arrrr"), commandService); + command.executeCommand(player, Collections.singletonList("arrrr")); // then - verify(commandService).send(sender, MessageKey.USAGE_REGISTER); + verify(commandService).send(player, MessageKey.USAGE_REGISTER); verifyZeroInteractions(management); } @@ -117,12 +120,13 @@ public class RegisterCommandTest { // given given(commandService.getProperty(RegistrationSettings.USE_EMAIL_REGISTRATION)).willReturn(true); given(commandService.getProperty(RegistrationSettings.ENABLE_CONFIRM_EMAIL)).willReturn(true); + Player player = mock(Player.class); // when - command.executeCommand(sender, Collections.singletonList("test@example.org"), commandService); + command.executeCommand(player, Collections.singletonList("test@example.org")); // then - verify(commandService).send(sender, MessageKey.USAGE_REGISTER); + verify(commandService).send(player, MessageKey.USAGE_REGISTER); verifyZeroInteractions(management); } @@ -132,12 +136,13 @@ public class RegisterCommandTest { given(commandService.getProperty(RegistrationSettings.USE_EMAIL_REGISTRATION)).willReturn(true); given(commandService.getProperty(RegistrationSettings.ENABLE_CONFIRM_EMAIL)).willReturn(false); given(commandService.getProperty(EmailSettings.MAIL_ACCOUNT)).willReturn(""); + Player player = mock(Player.class); // when - command.executeCommand(sender, Collections.singletonList("myMail@example.tld"), commandService); + command.executeCommand(player, Collections.singletonList("myMail@example.tld")); // then - verify(sender).sendMessage(argThat(containsString("no email address"))); + verify(player).sendMessage(argThat(containsString("no email address"))); verifyZeroInteractions(management); } @@ -150,13 +155,14 @@ public class RegisterCommandTest { given(commandService.getProperty(RegistrationSettings.USE_EMAIL_REGISTRATION)).willReturn(true); given(commandService.getProperty(RegistrationSettings.ENABLE_CONFIRM_EMAIL)).willReturn(true); given(commandService.getProperty(EmailSettings.MAIL_ACCOUNT)).willReturn("server@example.com"); + Player player = mock(Player.class); // when - command.executeCommand(sender, Arrays.asList(playerMail, playerMail), commandService); + command.executeCommand(player, Arrays.asList(playerMail, playerMail)); // then verify(commandService).validateEmail(playerMail); - verify(commandService).send(sender, MessageKey.INVALID_EMAIL); + verify(commandService).send(player, MessageKey.INVALID_EMAIL); verifyZeroInteractions(management); } @@ -169,12 +175,13 @@ public class RegisterCommandTest { given(commandService.getProperty(RegistrationSettings.USE_EMAIL_REGISTRATION)).willReturn(true); given(commandService.getProperty(RegistrationSettings.ENABLE_CONFIRM_EMAIL)).willReturn(true); given(commandService.getProperty(EmailSettings.MAIL_ACCOUNT)).willReturn("server@example.com"); + Player player = mock(Player.class); // when - command.executeCommand(sender, Arrays.asList(playerMail, "invalid"), commandService); + command.executeCommand(player, Arrays.asList(playerMail, "invalid")); // then - verify(commandService).send(sender, MessageKey.USAGE_REGISTER); + verify(commandService).send(player, MessageKey.USAGE_REGISTER); verifyZeroInteractions(management); } @@ -189,35 +196,40 @@ public class RegisterCommandTest { given(commandService.getProperty(RegistrationSettings.USE_EMAIL_REGISTRATION)).willReturn(true); given(commandService.getProperty(RegistrationSettings.ENABLE_CONFIRM_EMAIL)).willReturn(true); given(commandService.getProperty(EmailSettings.MAIL_ACCOUNT)).willReturn("server@example.com"); + Player player = mock(Player.class); // when - command.executeCommand(sender, Arrays.asList(playerMail, playerMail), commandService); + command.executeCommand(player, Arrays.asList(playerMail, playerMail)); // then verify(commandService).validateEmail(playerMail); - verify(management).performRegister(eq(sender), argThat(stringWithLength(passLength)), eq(playerMail), eq(true)); + verify(management).performRegister(eq(player), argThat(stringWithLength(passLength)), eq(playerMail), eq(true)); } @Test public void shouldRejectInvalidPasswordConfirmation() { // given given(commandService.getProperty(RestrictionSettings.ENABLE_PASSWORD_CONFIRMATION)).willReturn(true); + Player player = mock(Player.class); // when - command.executeCommand(sender, Arrays.asList("myPass", "mypass"), commandService); + command.executeCommand(player, Arrays.asList("myPass", "mypass")); // then - verify(commandService).send(sender, MessageKey.PASSWORD_MATCH_ERROR); + verify(commandService).send(player, MessageKey.PASSWORD_MATCH_ERROR); verifyZeroInteractions(management); } @Test public void shouldPerformPasswordValidation() { - // given / when - command.executeCommand(sender, Collections.singletonList("myPass"), commandService); + // given + Player player = mock(Player.class); + + // when + command.executeCommand(player, Collections.singletonList("myPass")); // then - verify(management).performRegister(sender, "myPass", "", true); + verify(management).performRegister(player, "myPass", "", true); } diff --git a/src/test/java/fr/xephi/authme/command/help/CommandSyntaxHelperTest.java b/src/test/java/fr/xephi/authme/command/help/CommandSyntaxHelperTest.java index 608951de..77320d15 100644 --- a/src/test/java/fr/xephi/authme/command/help/CommandSyntaxHelperTest.java +++ b/src/test/java/fr/xephi/authme/command/help/CommandSyntaxHelperTest.java @@ -1,5 +1,6 @@ package fr.xephi.authme.command.help; +import fr.xephi.authme.TestHelper; import fr.xephi.authme.command.CommandDescription; import fr.xephi.authme.command.TestCommandsUtil; import org.bukkit.ChatColor; @@ -66,4 +67,10 @@ public class CommandSyntaxHelperTest { assertThat(result, equalTo(ChatColor.WHITE + "/email" + ChatColor.YELLOW + " [player]")); } + @Test + public void shouldHaveHiddenConstructor() { + // given / when / then + TestHelper.validateHasOnlyPrivateEmptyConstructor(CommandSyntaxHelper.class); + } + } From 26ac466035c42b1f8b459dc7ed2666c95111fcb4 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Sat, 4 Jun 2016 21:13:38 +0200 Subject: [PATCH 160/200] #727 Instantiate ExecutableCommand objects in CommandHandler - Change CommandDescription to contain a reference to ExecutableCommand class only - Instantiate the actual ExecutableCommand objects in CommandHandler --- .../authme/command/CommandDescription.java | 33 +++-- .../xephi/authme/command/CommandHandler.java | 41 +++++- .../authme/command/CommandInitializer.java | 82 ++++++----- .../xephi/authme/command/CommandMapper.java | 20 ++- .../xephi/authme/command/CommandService.java | 31 ----- .../command/executable/HelpCommand.java | 13 +- .../authme/SwitchAntiBotCommand.java | 11 +- .../executable/email/EmailBaseCommand.java | 11 +- .../authme/command/help/HelpProvider.java | 16 ++- .../command/CommandConsistencyTest.java | 16 +-- .../authme/command/CommandHandlerTest.java | 127 +++++++++++------- .../command/CommandInitializerTest.java | 49 +------ .../authme/command/CommandMapperTest.java | 15 +++ .../authme/command/CommandServiceTest.java | 44 ------ .../authme/command/CommandUtilsTest.java | 7 +- .../authme/command/TestCommandsUtil.java | 42 ++++-- .../authme/SwitchAntiBotCommandTest.java | 11 +- .../authme/command/help/HelpProviderTest.java | 51 +++++-- .../tools/commands/CommandPageCreater.java | 31 +---- 19 files changed, 320 insertions(+), 331 deletions(-) diff --git a/src/main/java/fr/xephi/authme/command/CommandDescription.java b/src/main/java/fr/xephi/authme/command/CommandDescription.java index 7ff0e739..115d0f23 100644 --- a/src/main/java/fr/xephi/authme/command/CommandDescription.java +++ b/src/main/java/fr/xephi/authme/command/CommandDescription.java @@ -13,7 +13,7 @@ import static java.util.Arrays.asList; /** * Command description – defines which labels ("names") will lead to a command and points to the * {@link ExecutableCommand} implementation that executes the logic of the command. - * + *

* CommandDescription instances are built hierarchically: they have one parent, or {@code null} for base commands * (main commands such as {@code /authme}), and may have multiple children extending the mapping of the parent: e.g. if * {@code /authme} has a child whose label is {@code "register"}, then {@code /authme register} is the command that @@ -37,7 +37,7 @@ public class CommandDescription { /** * The executable command instance described by this object. */ - private ExecutableCommand executableCommand; + private Class executableCommand; /** * The parent command. */ @@ -57,7 +57,7 @@ public class CommandDescription { /** * Private constructor. Use {@link CommandDescription#builder()} to create instances of this class. - *

+ *

* Note for developers: Instances should be created with {@link CommandDescription#createInstance} to be properly * registered in the command tree. */ @@ -67,21 +67,20 @@ public class CommandDescription { /** * Create an instance. * - * @param labels List of command labels. - * @param description Command description. - * @param detailedDescription Detailed comment description. - * @param executableCommand The executable command, or null. - * @param parent Parent command. - * @param arguments Command arguments. - * @param permission The permission node required to execute this command. + * @param labels command labels + * @param description description of the command + * @param detailedDescription detailed command description + * @param executableCommand class of the command implementation + * @param parent parent command + * @param arguments command arguments + * @param permission permission node required to execute this command * - * @return The created instance + * @return the created instance * @see CommandDescription#builder() */ private static CommandDescription createInstance(List labels, String description, - String detailedDescription, ExecutableCommand executableCommand, - CommandDescription parent, List arguments, - PermissionNode permission) { + String detailedDescription, Class executableCommand, CommandDescription parent, + List arguments, PermissionNode permission) { CommandDescription instance = new CommandDescription(); instance.labels = labels; instance.description = description; @@ -133,7 +132,7 @@ public class CommandDescription { * * @return The executable command object. */ - public ExecutableCommand getExecutableCommand() { + public Class getExecutableCommand() { return executableCommand; } @@ -219,7 +218,7 @@ public class CommandDescription { private List labels; private String description; private String detailedDescription; - private ExecutableCommand executableCommand; + private Class executableCommand; private CommandDescription parent; private List arguments = new ArrayList<>(); private PermissionNode permission; @@ -260,7 +259,7 @@ public class CommandDescription { return this; } - public CommandBuilder executableCommand(ExecutableCommand executableCommand) { + public CommandBuilder executableCommand(Class executableCommand) { this.executableCommand = executableCommand; return this; } diff --git a/src/main/java/fr/xephi/authme/command/CommandHandler.java b/src/main/java/fr/xephi/authme/command/CommandHandler.java index 8f493acb..0b76a822 100644 --- a/src/main/java/fr/xephi/authme/command/CommandHandler.java +++ b/src/main/java/fr/xephi/authme/command/CommandHandler.java @@ -2,6 +2,7 @@ package fr.xephi.authme.command; import fr.xephi.authme.AuthMe; import fr.xephi.authme.command.help.HelpProvider; +import fr.xephi.authme.initialization.AuthMeServiceInitializer; import fr.xephi.authme.permission.PermissionsManager; import fr.xephi.authme.util.StringUtils; import org.bukkit.ChatColor; @@ -9,7 +10,10 @@ import org.bukkit.command.CommandSender; import javax.inject.Inject; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Set; /** * The AuthMe command handler, responsible for mapping incoming commands to the correct {@link CommandDescription} @@ -23,12 +27,23 @@ public class CommandHandler { */ private static final double SUGGEST_COMMAND_THRESHOLD = 0.75; - @Inject - private CommandService commandService; + private final CommandMapper commandMapper; + private final PermissionsManager permissionsManager; + private final HelpProvider helpProvider; + + /** + * Map with ExecutableCommand children. The key is the type of the value. + */ + private Map, ExecutableCommand> commands = new HashMap<>(); @Inject - private PermissionsManager permissionsManager; - + public CommandHandler(AuthMeServiceInitializer initializer, CommandMapper commandMapper, + PermissionsManager permissionsManager, HelpProvider helpProvider) { + this.commandMapper = commandMapper; + this.permissionsManager = permissionsManager; + this.helpProvider = helpProvider; + initializeCommands(initializer, commandMapper.getCommandClasses()); + } /** * Map a command that was invoked to the proper {@link CommandDescription} or return a useful error @@ -45,7 +60,7 @@ public class CommandHandler { List parts = skipEmptyArguments(bukkitArgs); parts.add(0, bukkitCommandLabel); - FoundCommandResult result = commandService.mapPartsToCommand(sender, parts); + FoundCommandResult result = commandMapper.mapPartsToCommand(sender, parts); handleCommandResult(sender, result); return !FoundResultStatus.MISSING_BASE_COMMAND.equals(result.getResultStatus()); } @@ -72,6 +87,18 @@ public class CommandHandler { } } + /** + * Initializes all required ExecutableCommand objects. + * + * @param commandClasses the classes to instantiate + */ + private void initializeCommands(AuthMeServiceInitializer initializer, + Set> commandClasses) { + for (Class clazz : commandClasses) { + commands.put(clazz, initializer.newInstance(clazz)); + } + } + /** * Execute the command for the given command sender. * @@ -79,7 +106,7 @@ public class CommandHandler { * @param result The mapped result */ private void executeCommand(CommandSender sender, FoundCommandResult result) { - ExecutableCommand executableCommand = result.getCommandDescription().getExecutableCommand(); + ExecutableCommand executableCommand = commands.get(result.getCommandDescription().getExecutableCommand()); List arguments = result.getArguments(); executableCommand.executeCommand(sender, arguments); } @@ -129,7 +156,7 @@ public class CommandHandler { // Show the command argument help sender.sendMessage(ChatColor.DARK_RED + "Incorrect command arguments!"); - commandService.outputHelp(sender, result, HelpProvider.SHOW_ARGUMENTS); + helpProvider.outputHelp(sender, result, HelpProvider.SHOW_ARGUMENTS); List labels = result.getLabels(); String childLabel = labels.size() >= 2 ? labels.get(1) : ""; diff --git a/src/main/java/fr/xephi/authme/command/CommandInitializer.java b/src/main/java/fr/xephi/authme/command/CommandInitializer.java index 7f6b2e26..ce02b9a8 100644 --- a/src/main/java/fr/xephi/authme/command/CommandInitializer.java +++ b/src/main/java/fr/xephi/authme/command/CommandInitializer.java @@ -33,11 +33,9 @@ import fr.xephi.authme.command.executable.login.LoginCommand; import fr.xephi.authme.command.executable.logout.LogoutCommand; import fr.xephi.authme.command.executable.register.RegisterCommand; import fr.xephi.authme.command.executable.unregister.UnregisterCommand; -import fr.xephi.authme.initialization.AuthMeServiceInitializer; import fr.xephi.authme.permission.AdminPermission; import fr.xephi.authme.permission.PlayerPermission; -import javax.inject.Inject; import java.util.Arrays; import java.util.Collection; import java.util.List; @@ -48,16 +46,17 @@ import java.util.Set; */ public class CommandInitializer { - private AuthMeServiceInitializer initializer; - private Set commands; - @Inject - public CommandInitializer(AuthMeServiceInitializer initializer) { - this.initializer = initializer; + public CommandInitializer() { buildCommands(); } + /** + * Returns the description of all AuthMe commands. + * + * @return the command descriptions + */ public Set getCommands() { return commands; } @@ -68,7 +67,7 @@ public class CommandInitializer { .labels("authme") .description("Main command") .detailedDescription("The main AuthMeReloaded command. The root for all admin commands.") - .executableCommand(initializer.newInstance(AuthMeCommand.class)) + .executableCommand(AuthMeCommand.class) .build(); // Register the register command @@ -80,7 +79,7 @@ public class CommandInitializer { .withArgument("player", "Player name", false) .withArgument("password", "Password", false) .permission(AdminPermission.REGISTER) - .executableCommand(initializer.newInstance(RegisterAdminCommand.class)) + .executableCommand(RegisterAdminCommand.class) .build(); // Register the unregister command @@ -91,7 +90,7 @@ public class CommandInitializer { .detailedDescription("Unregister the specified player.") .withArgument("player", "Player name", false) .permission(AdminPermission.UNREGISTER) - .executableCommand(initializer.newInstance(UnregisterAdminCommand.class)) + .executableCommand(UnregisterAdminCommand.class) .build(); // Register the forcelogin command @@ -102,7 +101,7 @@ public class CommandInitializer { .detailedDescription("Enforce the specified player to login.") .withArgument("player", "Online player name", true) .permission(AdminPermission.FORCE_LOGIN) - .executableCommand(initializer.newInstance(ForceLoginCommand.class)) + .executableCommand(ForceLoginCommand.class) .build(); // Register the changepassword command @@ -114,7 +113,7 @@ public class CommandInitializer { .withArgument("player", "Player name", false) .withArgument("pwd", "New password", false) .permission(AdminPermission.CHANGE_PASSWORD) - .executableCommand(initializer.newInstance(ChangePasswordAdminCommand.class)) + .executableCommand(ChangePasswordAdminCommand.class) .build(); // Register the last login command @@ -125,7 +124,7 @@ public class CommandInitializer { .detailedDescription("View the date of the specified players last login.") .withArgument("player", "Player name", true) .permission(AdminPermission.LAST_LOGIN) - .executableCommand(initializer.newInstance(LastLoginCommand.class)) + .executableCommand(LastLoginCommand.class) .build(); // Register the accounts command @@ -136,7 +135,7 @@ public class CommandInitializer { .detailedDescription("Display all accounts of a player by his player name or IP.") .withArgument("player", "Player name or IP", true) .permission(AdminPermission.ACCOUNTS) - .executableCommand(initializer.newInstance(AccountsCommand.class)) + .executableCommand(AccountsCommand.class) .build(); // Register the getemail command @@ -147,7 +146,7 @@ public class CommandInitializer { .detailedDescription("Display the email address of the specified player if set.") .withArgument("player", "Player name", true) .permission(AdminPermission.GET_EMAIL) - .executableCommand(initializer.newInstance(GetEmailCommand.class)) + .executableCommand(GetEmailCommand.class) .build(); // Register the setemail command @@ -159,7 +158,7 @@ public class CommandInitializer { .withArgument("player", "Player name", false) .withArgument("email", "Player email", false) .permission(AdminPermission.CHANGE_EMAIL) - .executableCommand(initializer.newInstance(SetEmailCommand.class)) + .executableCommand(SetEmailCommand.class) .build(); // Register the getip command @@ -170,7 +169,7 @@ public class CommandInitializer { .detailedDescription("Get the IP address of the specified online player.") .withArgument("player", "Player name", false) .permission(AdminPermission.GET_IP) - .executableCommand(initializer.newInstance(GetIpCommand.class)) + .executableCommand(GetIpCommand.class) .build(); // Register the spawn command @@ -180,7 +179,7 @@ public class CommandInitializer { .description("Teleport to spawn") .detailedDescription("Teleport to the spawn.") .permission(AdminPermission.SPAWN) - .executableCommand(initializer.newInstance(SpawnCommand.class)) + .executableCommand(SpawnCommand.class) .build(); // Register the setspawn command @@ -190,7 +189,7 @@ public class CommandInitializer { .description("Change the spawn") .detailedDescription("Change the player's spawn to your current position.") .permission(AdminPermission.SET_SPAWN) - .executableCommand(initializer.newInstance(SetSpawnCommand.class)) + .executableCommand(SetSpawnCommand.class) .build(); // Register the firstspawn command @@ -200,7 +199,7 @@ public class CommandInitializer { .description("Teleport to first spawn") .detailedDescription("Teleport to the first spawn.") .permission(AdminPermission.FIRST_SPAWN) - .executableCommand(initializer.newInstance(FirstSpawnCommand.class)) + .executableCommand(FirstSpawnCommand.class) .build(); // Register the setfirstspawn command @@ -210,7 +209,7 @@ public class CommandInitializer { .description("Change the first spawn") .detailedDescription("Change the first player's spawn to your current position.") .permission(AdminPermission.SET_FIRST_SPAWN) - .executableCommand(initializer.newInstance(SetFirstSpawnCommand.class)) + .executableCommand(SetFirstSpawnCommand.class) .build(); // Register the purge command @@ -221,7 +220,7 @@ public class CommandInitializer { .detailedDescription("Purge old AuthMeReloaded data longer than the specified amount of days ago.") .withArgument("days", "Number of days", false) .permission(AdminPermission.PURGE) - .executableCommand(initializer.newInstance(PurgeCommand.class)) + .executableCommand(PurgeCommand.class) .build(); // Register the purgelastposition command @@ -233,7 +232,7 @@ public class CommandInitializer { .detailedDescription("Purge the last know position of the specified player or all of them.") .withArgument("player/*", "Player name or * for all players", false) .permission(AdminPermission.PURGE_LAST_POSITION) - .executableCommand(initializer.newInstance(PurgeLastPositionCommand.class)) + .executableCommand(PurgeLastPositionCommand.class) .build(); // Register the purgebannedplayers command @@ -243,7 +242,7 @@ public class CommandInitializer { .description("Purge banned players data") .detailedDescription("Purge all AuthMeReloaded data for banned players.") .permission(AdminPermission.PURGE_BANNED_PLAYERS) - .executableCommand(initializer.newInstance(PurgeBannedPlayersCommand.class)) + .executableCommand(PurgeBannedPlayersCommand.class) .build(); // Register the switchantibot command @@ -254,7 +253,7 @@ public class CommandInitializer { .detailedDescription("Switch or toggle the AntiBot mode to the specified state.") .withArgument("mode", "ON / OFF", true) .permission(AdminPermission.SWITCH_ANTIBOT) - .executableCommand(initializer.newInstance(SwitchAntiBotCommand.class)) + .executableCommand(SwitchAntiBotCommand.class) .build(); // Register the reload command @@ -264,7 +263,7 @@ public class CommandInitializer { .description("Reload plugin") .detailedDescription("Reload the AuthMeReloaded plugin.") .permission(AdminPermission.RELOAD) - .executableCommand(initializer.newInstance(ReloadCommand.class)) + .executableCommand(ReloadCommand.class) .build(); // Register the version command @@ -274,7 +273,7 @@ public class CommandInitializer { .description("Version info") .detailedDescription("Show detailed information about the installed AuthMeReloaded version, the " + "developers, contributors, and license.") - .executableCommand(initializer.newInstance(VersionCommand.class)) + .executableCommand(VersionCommand.class) .build(); CommandDescription.builder() @@ -285,7 +284,7 @@ public class CommandInitializer { .withArgument("job", "Conversion job: xauth / crazylogin / rakamak / " + "royalauth / vauth / sqlitetosql", false) .permission(AdminPermission.CONVERTER) - .executableCommand(initializer.newInstance(ConverterCommand.class)) + .executableCommand(ConverterCommand.class) .build(); // Register the base login command @@ -296,7 +295,7 @@ public class CommandInitializer { .detailedDescription("Command to log in using AuthMeReloaded.") .withArgument("password", "Login password", false) .permission(PlayerPermission.LOGIN) - .executableCommand(initializer.newInstance(LoginCommand.class)) + .executableCommand(LoginCommand.class) .build(); // Register the base logout command @@ -306,7 +305,7 @@ public class CommandInitializer { .description("Logout command") .detailedDescription("Command to logout using AuthMeReloaded.") .permission(PlayerPermission.LOGOUT) - .executableCommand(initializer.newInstance(LogoutCommand.class)) + .executableCommand(LogoutCommand.class) .build(); // Register the base register command @@ -318,7 +317,7 @@ public class CommandInitializer { .withArgument("password", "Password", true) .withArgument("verifyPassword", "Verify password", true) .permission(PlayerPermission.REGISTER) - .executableCommand(initializer.newInstance(RegisterCommand.class)) + .executableCommand(RegisterCommand.class) .build(); // Register the base unregister command @@ -329,7 +328,7 @@ public class CommandInitializer { .detailedDescription("Command to unregister using AuthMeReloaded.") .withArgument("password", "Password", false) .permission(PlayerPermission.UNREGISTER) - .executableCommand(initializer.newInstance(UnregisterCommand.class)) + .executableCommand(UnregisterCommand.class) .build(); // Register the base changepassword command @@ -341,7 +340,7 @@ public class CommandInitializer { .withArgument("oldPassword", "Old Password", false) .withArgument("newPassword", "New Password.", false) .permission(PlayerPermission.CHANGE_PASSWORD) - .executableCommand(initializer.newInstance(ChangePasswordCommand.class)) + .executableCommand(ChangePasswordCommand.class) .build(); // Register the base Email command @@ -350,7 +349,7 @@ public class CommandInitializer { .labels("email") .description("Email command") .detailedDescription("The AuthMeReloaded Email command base.") - .executableCommand(initializer.newInstance(EmailBaseCommand.class)) + .executableCommand(EmailBaseCommand.class) .build(); // Register the add command @@ -362,7 +361,7 @@ public class CommandInitializer { .withArgument("email", "Email address", false) .withArgument("verifyEmail", "Email address verification", false) .permission(PlayerPermission.ADD_EMAIL) - .executableCommand(initializer.newInstance(AddEmailCommand.class)) + .executableCommand(AddEmailCommand.class) .build(); // Register the change command @@ -374,7 +373,7 @@ public class CommandInitializer { .withArgument("oldEmail", "Old email address", false) .withArgument("newEmail", "New email address", false) .permission(PlayerPermission.CHANGE_EMAIL) - .executableCommand(initializer.newInstance(ChangeEmailCommand.class)) + .executableCommand(ChangeEmailCommand.class) .build(); // Register the recover command @@ -386,7 +385,7 @@ public class CommandInitializer { "a new password.") .withArgument("email", "Email address", false) .permission(PlayerPermission.RECOVER_EMAIL) - .executableCommand(initializer.newInstance(RecoverEmailCommand.class)) + .executableCommand(RecoverEmailCommand.class) .build(); // Register the base captcha command @@ -397,7 +396,7 @@ public class CommandInitializer { .detailedDescription("Captcha command for AuthMeReloaded.") .withArgument("captcha", "The Captcha", false) .permission(PlayerPermission.CAPTCHA) - .executableCommand(initializer.newInstance(CaptchaCommand.class)) + .executableCommand(CaptchaCommand.class) .build(); Set baseCommands = ImmutableSet.of( @@ -415,12 +414,11 @@ public class CommandInitializer { } /** - * Set the help command on all base commands, e.g. to register /authme help or /register help. + * Sets the help command on all base commands, e.g. to register /authme help or /register help. * - * @param commands The list of base commands to register a help child command on + * @param commands the list of base commands to register a help child command on */ private void setHelpOnAllBases(Collection commands) { - final HelpCommand helpCommandExecutable = initializer.newInstance(HelpCommand.class); final List helpCommandLabels = Arrays.asList("help", "hlp", "h", "sos", "?"); for (CommandDescription base : commands) { @@ -430,7 +428,7 @@ public class CommandInitializer { .description("View help") .detailedDescription("View detailed help for /" + base.getLabels().get(0) + " commands.") .withArgument("query", "The command or query to view help for.", true) - .executableCommand(helpCommandExecutable) + .executableCommand(HelpCommand.class) .build(); } } diff --git a/src/main/java/fr/xephi/authme/command/CommandMapper.java b/src/main/java/fr/xephi/authme/command/CommandMapper.java index 755cea0d..d0706c80 100644 --- a/src/main/java/fr/xephi/authme/command/CommandMapper.java +++ b/src/main/java/fr/xephi/authme/command/CommandMapper.java @@ -8,6 +8,7 @@ import org.bukkit.command.CommandSender; import javax.inject.Inject; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Set; @@ -69,6 +70,23 @@ public class CommandMapper { return getCommandWithSmallestDifference(base, parts); } + /** + * Return all {@link ExecutableCommand} classes referenced in {@link CommandDescription} objects. + * + * @return all classes + * @see CommandInitializer#getCommands + */ + public Set> getCommandClasses() { + Set> classes = new HashSet<>(50); + for (CommandDescription command : baseCommands) { + classes.add(command.getExecutableCommand()); + for (CommandDescription child : command.getChildren()) { + classes.add(child.getExecutableCommand()); + } + } + return classes; + } + private FoundCommandResult getCommandWithSmallestDifference(CommandDescription base, List parts) { // Return the base command with incorrect arg count error if we only have one part if (parts.size() <= 1) { @@ -141,7 +159,7 @@ public class CommandMapper { private static FoundCommandResult transformResultForHelp(FoundCommandResult result) { if (result.getCommandDescription() != null - && HELP_COMMAND_CLASS.isAssignableFrom(result.getCommandDescription().getExecutableCommand().getClass())) { + && HELP_COMMAND_CLASS.isAssignableFrom(result.getCommandDescription().getExecutableCommand())) { // For "/authme help register" we have labels = [authme, help] and arguments = [register] // But for the help command we want labels = [authme, help] and arguments = [authme, register], // so we can use the arguments as the labels to the command to show help for diff --git a/src/main/java/fr/xephi/authme/command/CommandService.java b/src/main/java/fr/xephi/authme/command/CommandService.java index acf15056..5efe79e8 100644 --- a/src/main/java/fr/xephi/authme/command/CommandService.java +++ b/src/main/java/fr/xephi/authme/command/CommandService.java @@ -1,6 +1,5 @@ package fr.xephi.authme.command; -import fr.xephi.authme.command.help.HelpProvider; import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.output.Messages; import fr.xephi.authme.settings.NewSetting; @@ -9,7 +8,6 @@ import fr.xephi.authme.util.ValidationService; import org.bukkit.command.CommandSender; import javax.inject.Inject; -import java.util.List; /** * Service for implementations of {@link ExecutableCommand} to execute some common tasks. @@ -20,10 +18,6 @@ public class CommandService { @Inject private Messages messages; @Inject - private HelpProvider helpProvider; - @Inject - private CommandMapper commandMapper; - @Inject private NewSetting settings; @Inject private ValidationService validationService; @@ -49,31 +43,6 @@ public class CommandService { messages.send(sender, messageKey, replacements); } - /** - * Map command parts to a command description. - * - * @param sender The command sender issuing the request (for permission check), or null to skip permissions - * @param commandParts The received command parts to map to a command - * @return The computed mapping result - */ - public FoundCommandResult mapPartsToCommand(CommandSender sender, List commandParts) { - return commandMapper.mapPartsToCommand(sender, commandParts); - } - - /** - * Output the help for a given command. - * - * @param sender The sender to output the help to - * @param result The result to output information about - * @param options Output options, see {@link HelpProvider} - */ - public void outputHelp(CommandSender sender, FoundCommandResult result, int options) { - List lines = helpProvider.printHelp(sender, result, options); - for (String line : lines) { - sender.sendMessage(line); - } - } - /** * Retrieve a message by its message key. * diff --git a/src/main/java/fr/xephi/authme/command/executable/HelpCommand.java b/src/main/java/fr/xephi/authme/command/executable/HelpCommand.java index e912fb12..2076bcbc 100644 --- a/src/main/java/fr/xephi/authme/command/executable/HelpCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/HelpCommand.java @@ -1,6 +1,6 @@ package fr.xephi.authme.command.executable; -import fr.xephi.authme.command.CommandService; +import fr.xephi.authme.command.CommandMapper; import fr.xephi.authme.command.CommandUtils; import fr.xephi.authme.command.ExecutableCommand; import fr.xephi.authme.command.FoundCommandResult; @@ -18,14 +18,17 @@ import static fr.xephi.authme.command.FoundResultStatus.UNKNOWN_LABEL; public class HelpCommand implements ExecutableCommand { @Inject - private CommandService commandService; + private CommandMapper commandMapper; + + @Inject + private HelpProvider helpProvider; // Convention: arguments is not the actual invoked arguments but the command that was invoked, // e.g. "/authme help register" would typically be arguments = [register], but here we pass [authme, register] @Override public void executeCommand(CommandSender sender, List arguments) { - FoundCommandResult result = commandService.mapPartsToCommand(sender, arguments); + FoundCommandResult result = commandMapper.mapPartsToCommand(sender, arguments); FoundResultStatus resultStatus = result.getResultStatus(); if (MISSING_BASE_COMMAND.equals(resultStatus)) { @@ -43,9 +46,9 @@ public class HelpCommand implements ExecutableCommand { int mappedCommandLevel = result.getCommandDescription().getLabelCount(); if (mappedCommandLevel == 1) { - commandService.outputHelp(sender, result, HelpProvider.SHOW_CHILDREN); + helpProvider.outputHelp(sender, result, HelpProvider.SHOW_CHILDREN); } else { - commandService.outputHelp(sender, result, HelpProvider.ALL_OPTIONS); + helpProvider.outputHelp(sender, result, HelpProvider.ALL_OPTIONS); } } diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/SwitchAntiBotCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/SwitchAntiBotCommand.java index 37a709cf..64a7241f 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/SwitchAntiBotCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/SwitchAntiBotCommand.java @@ -1,7 +1,7 @@ package fr.xephi.authme.command.executable.authme; import fr.xephi.authme.AntiBot; -import fr.xephi.authme.command.CommandService; +import fr.xephi.authme.command.CommandMapper; import fr.xephi.authme.command.ExecutableCommand; import fr.xephi.authme.command.FoundCommandResult; import fr.xephi.authme.command.help.HelpProvider; @@ -21,7 +21,10 @@ public class SwitchAntiBotCommand implements ExecutableCommand { private AntiBot antiBot; @Inject - private CommandService commandService; + private CommandMapper commandMapper; + + @Inject + private HelpProvider helpProvider; @Override public void executeCommand(final CommandSender sender, List arguments) { @@ -41,8 +44,8 @@ public class SwitchAntiBotCommand implements ExecutableCommand { sender.sendMessage("[AuthMe] AntiBot Manual Override: disabled!"); } else { sender.sendMessage(ChatColor.DARK_RED + "Invalid AntiBot mode!"); - FoundCommandResult result = commandService.mapPartsToCommand(sender, Arrays.asList("authme", "antibot")); - commandService.outputHelp(sender, result, HelpProvider.SHOW_ARGUMENTS); + FoundCommandResult result = commandMapper.mapPartsToCommand(sender, Arrays.asList("authme", "antibot")); + helpProvider.outputHelp(sender, result, HelpProvider.SHOW_ARGUMENTS); sender.sendMessage(ChatColor.GOLD + "Detailed help: " + ChatColor.WHITE + "/authme help antibot"); } } diff --git a/src/main/java/fr/xephi/authme/command/executable/email/EmailBaseCommand.java b/src/main/java/fr/xephi/authme/command/executable/email/EmailBaseCommand.java index d75973f0..0484e218 100644 --- a/src/main/java/fr/xephi/authme/command/executable/email/EmailBaseCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/email/EmailBaseCommand.java @@ -1,6 +1,6 @@ package fr.xephi.authme.command.executable.email; -import fr.xephi.authme.command.CommandService; +import fr.xephi.authme.command.CommandMapper; import fr.xephi.authme.command.ExecutableCommand; import fr.xephi.authme.command.FoundCommandResult; import fr.xephi.authme.command.help.HelpProvider; @@ -16,11 +16,14 @@ import java.util.List; public class EmailBaseCommand implements ExecutableCommand { @Inject - private CommandService commandService; + private CommandMapper commandMapper; + + @Inject + private HelpProvider helpProvider; @Override public void executeCommand(CommandSender sender, List arguments) { - FoundCommandResult result = commandService.mapPartsToCommand(sender, Collections.singletonList("email")); - commandService.outputHelp(sender, result, HelpProvider.SHOW_CHILDREN); + FoundCommandResult result = commandMapper.mapPartsToCommand(sender, Collections.singletonList("email")); + helpProvider.outputHelp(sender, result, HelpProvider.SHOW_CHILDREN); } } diff --git a/src/main/java/fr/xephi/authme/command/help/HelpProvider.java b/src/main/java/fr/xephi/authme/command/help/HelpProvider.java index 7257b4d8..4e07639d 100644 --- a/src/main/java/fr/xephi/authme/command/help/HelpProvider.java +++ b/src/main/java/fr/xephi/authme/command/help/HelpProvider.java @@ -53,7 +53,7 @@ public class HelpProvider implements SettingsDependent { loadSettings(settings); } - public List printHelp(CommandSender sender, FoundCommandResult result, int options) { + private List printHelp(CommandSender sender, FoundCommandResult result, int options) { if (result.getCommandDescription() == null) { return singletonList(ChatColor.DARK_RED + "Failed to retrieve any help information!"); } @@ -87,6 +87,20 @@ public class HelpProvider implements SettingsDependent { return lines; } + /** + * Output the help for a given command. + * + * @param sender The sender to output the help to + * @param result The result to output information about + * @param options Output options, see {@link HelpProvider} + */ + public void outputHelp(CommandSender sender, FoundCommandResult result, int options) { + List lines = printHelp(sender, result, options); + for (String line : lines) { + sender.sendMessage(line); + } + } + @Override public void loadSettings(NewSetting settings) { helpHeader = settings.getProperty(PluginSettings.HELP_HEADER); diff --git a/src/test/java/fr/xephi/authme/command/CommandConsistencyTest.java b/src/test/java/fr/xephi/authme/command/CommandConsistencyTest.java index bb297268..287b398a 100644 --- a/src/test/java/fr/xephi/authme/command/CommandConsistencyTest.java +++ b/src/test/java/fr/xephi/authme/command/CommandConsistencyTest.java @@ -1,13 +1,9 @@ package fr.xephi.authme.command; - -import fr.xephi.authme.initialization.AuthMeServiceInitializer; import org.bukkit.configuration.MemorySection; import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.configuration.file.YamlConfiguration; import org.junit.Test; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; import java.util.ArrayList; import java.util.Collection; @@ -22,9 +18,6 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.nullValue; import static org.junit.Assert.assertThat; -import static org.mockito.BDDMockito.given; -import static org.mockito.Matchers.any; -import static org.mockito.Mockito.mock; /** * Checks that the commands declared in plugin.yml correspond @@ -59,14 +52,7 @@ public class CommandConsistencyTest { */ @SuppressWarnings("unchecked") private static Collection> initializeCommands() { - AuthMeServiceInitializer injector = mock(AuthMeServiceInitializer.class); - given(injector.newInstance(any(Class.class))).willAnswer(new Answer() { - @Override - public Object answer(InvocationOnMock invocation) throws Throwable { - return mock((Class) invocation.getArguments()[0]); - } - }); - CommandInitializer initializer = new CommandInitializer(injector); + CommandInitializer initializer = new CommandInitializer(); Collection> commandLabels = new ArrayList<>(); for (CommandDescription baseCommand : initializer.getCommands()) { commandLabels.add(baseCommand.getLabels()); diff --git a/src/test/java/fr/xephi/authme/command/CommandHandlerTest.java b/src/test/java/fr/xephi/authme/command/CommandHandlerTest.java index d604a5b6..ff3fd5a7 100644 --- a/src/test/java/fr/xephi/authme/command/CommandHandlerTest.java +++ b/src/test/java/fr/xephi/authme/command/CommandHandlerTest.java @@ -1,17 +1,25 @@ package fr.xephi.authme.command; +import com.google.common.collect.Sets; +import fr.xephi.authme.command.TestCommandsUtil.TestLoginCommand; +import fr.xephi.authme.command.TestCommandsUtil.TestRegisterCommand; +import fr.xephi.authme.command.TestCommandsUtil.TestUnregisterCommand; +import fr.xephi.authme.command.help.HelpProvider; +import fr.xephi.authme.initialization.AuthMeServiceInitializer; import fr.xephi.authme.permission.PermissionsManager; import org.bukkit.command.CommandSender; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; -import org.mockito.Captor; -import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.invocation.InvocationOnMock; import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; import java.util.Collections; -import java.util.List; +import java.util.HashMap; +import java.util.Map; import static fr.xephi.authme.command.FoundResultStatus.INCORRECT_ARGUMENTS; import static fr.xephi.authme.command.FoundResultStatus.MISSING_BASE_COMMAND; @@ -19,7 +27,6 @@ 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.Matchers.contains; import static org.hamcrest.Matchers.containsString; import static org.junit.Assert.assertThat; import static org.mockito.BDDMockito.given; @@ -29,6 +36,7 @@ import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.argThat; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; @@ -43,20 +51,53 @@ import static org.mockito.Mockito.verify; @RunWith(MockitoJUnitRunner.class) public class CommandHandlerTest { - @InjectMocks private CommandHandler handler; @Mock - private CommandService commandService; - + private AuthMeServiceInitializer initializer; + @Mock + private CommandInitializer commandInitializer; @Mock private CommandMapper commandMapper; - @Mock private PermissionsManager permissionsManager; + @Mock + private HelpProvider helpProvider; - @Captor - private ArgumentCaptor> captor; + private Map, ExecutableCommand> mockedCommands = new HashMap<>(); + + @Before + @SuppressWarnings("unchecked") + public void initializeCommandMapper() { + given(commandMapper.getCommandClasses()).willReturn(Sets.newHashSet(ExecutableCommand.class, + TestLoginCommand.class, TestRegisterCommand.class, TestUnregisterCommand.class)); + setInjectorToMockExecutableCommandClasses(); + handler = new CommandHandler(initializer, commandMapper, permissionsManager, helpProvider); + } + + /** + * Makes the initializer return a mock when {@link AuthMeServiceInitializer#newInstance(Class)} is invoked + * with (a child of) ExecutableCommand.class. The mocks the initializer creates are stored in {@link #mockedCommands}. + *

+ * The {@link CommandMapper} is mocked in {@link #initializeCommandMapper()} to return certain test classes. + */ + private void setInjectorToMockExecutableCommandClasses() { + given(initializer.newInstance(any(Class.class))).willAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + Class clazz = (Class) invocation.getArguments()[0]; + if (ExecutableCommand.class.isAssignableFrom(clazz)) { + @SuppressWarnings("unchecked") + Class commandClass = (Class) clazz; + ExecutableCommand mock = mock(commandClass); + mockedCommands.put(commandClass, mock); + return mock; + } + throw new IllegalStateException("Unexpected class '" + clazz.getName() + + "': Not a child of ExecutableCommand"); + } + }); + } @Test @@ -66,22 +107,18 @@ public class CommandHandlerTest { String[] bukkitArgs = {"Login", "myPass"}; CommandSender sender = mock(CommandSender.class); - ExecutableCommand executableCommand = mock(ExecutableCommand.class); CommandDescription command = mock(CommandDescription.class); - given(command.getExecutableCommand()).willReturn(executableCommand); - given(commandService.mapPartsToCommand(any(CommandSender.class), anyListOf(String.class))) + doReturn(TestLoginCommand.class).when(command).getExecutableCommand(); + given(commandMapper.mapPartsToCommand(any(CommandSender.class), anyListOf(String.class))) .willReturn(new FoundCommandResult(command, asList("Authme", "Login"), asList("myPass"), 0.0, SUCCESS)); // when handler.processCommand(sender, bukkitLabel, bukkitArgs); // then - verify(commandService).mapPartsToCommand(eq(sender), captor.capture()); - assertThat(captor.getValue(), contains("Authme", "Login", "myPass")); - - verify(executableCommand).executeCommand(eq(sender), captor.capture()); - assertThat(captor.getValue(), contains("myPass")); - + ExecutableCommand executableCommand = mockedCommands.get(TestLoginCommand.class); + verify(commandMapper).mapPartsToCommand(sender, asList("Authme", "Login", "myPass")); + verify(executableCommand).executeCommand(sender, asList("myPass")); // Ensure that no error message was issued to the command sender verify(sender, never()).sendMessage(anyString()); } @@ -93,15 +130,14 @@ public class CommandHandlerTest { String[] bukkitArgs = {"testPlayer"}; CommandSender sender = mock(CommandSender.class); CommandDescription command = mock(CommandDescription.class); - given(commandService.mapPartsToCommand(any(CommandSender.class), anyListOf(String.class))) + given(commandMapper.mapPartsToCommand(any(CommandSender.class), anyListOf(String.class))) .willReturn(new FoundCommandResult(command, asList("unreg"), asList("testPlayer"), 0.0, NO_PERMISSION)); // when handler.processCommand(sender, bukkitLabel, bukkitArgs); // then - verify(commandService).mapPartsToCommand(eq(sender), captor.capture()); - assertThat(captor.getValue(), contains("unreg", "testPlayer")); + verify(commandMapper).mapPartsToCommand(sender, asList("unreg", "testPlayer")); verify(command, never()).getExecutableCommand(); verify(sender).sendMessage(argThat(containsString("don't have permission"))); } @@ -113,7 +149,7 @@ public class CommandHandlerTest { String[] bukkitArgs = {"testPlayer"}; CommandSender sender = mock(CommandSender.class); CommandDescription command = mock(CommandDescription.class); - given(commandService.mapPartsToCommand(any(CommandSender.class), anyListOf(String.class))).willReturn( + given(commandMapper.mapPartsToCommand(any(CommandSender.class), anyListOf(String.class))).willReturn( new FoundCommandResult(command, asList("unreg"), asList("testPlayer"), 0.0, INCORRECT_ARGUMENTS)); given(permissionsManager.hasPermission(sender, command.getPermission())).willReturn(true); @@ -121,9 +157,7 @@ public class CommandHandlerTest { handler.processCommand(sender, bukkitLabel, bukkitArgs); // then - verify(commandService).mapPartsToCommand(eq(sender), captor.capture()); - assertThat(captor.getValue(), contains("unreg", "testPlayer")); - + verify(commandMapper).mapPartsToCommand(sender, asList("unreg", "testPlayer")); verify(command, never()).getExecutableCommand(); ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); verify(sender, atLeastOnce()).sendMessage(captor.capture()); @@ -137,7 +171,7 @@ public class CommandHandlerTest { String[] bukkitArgs = {"testPlayer"}; CommandSender sender = mock(CommandSender.class); CommandDescription command = mock(CommandDescription.class); - given(commandService.mapPartsToCommand(any(CommandSender.class), anyListOf(String.class))).willReturn( + given(commandMapper.mapPartsToCommand(any(CommandSender.class), anyListOf(String.class))).willReturn( new FoundCommandResult(command, asList("unreg"), asList("testPlayer"), 0.0, INCORRECT_ARGUMENTS)); given(permissionsManager.hasPermission(sender, command.getPermission())).willReturn(false); @@ -145,9 +179,7 @@ public class CommandHandlerTest { handler.processCommand(sender, bukkitLabel, bukkitArgs); // then - verify(commandService).mapPartsToCommand(eq(sender), captor.capture()); - assertThat(captor.getValue(), contains("unreg", "testPlayer")); - + verify(commandMapper).mapPartsToCommand(sender, asList("unreg", "testPlayer")); verify(command, never()).getExecutableCommand(); ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); verify(sender).sendMessage(captor.capture()); @@ -161,15 +193,14 @@ public class CommandHandlerTest { String[] bukkitArgs = {"testPlayer"}; CommandSender sender = mock(CommandSender.class); CommandDescription command = mock(CommandDescription.class); - given(commandService.mapPartsToCommand(any(CommandSender.class), anyListOf(String.class))).willReturn( + given(commandMapper.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(commandService).mapPartsToCommand(eq(sender), captor.capture()); - assertThat(captor.getValue(), contains("unreg", "testPlayer")); + verify(commandMapper).mapPartsToCommand(sender, asList("unreg", "testPlayer")); verify(command, never()).getExecutableCommand(); verify(sender).sendMessage(argThat(containsString("Failed to parse"))); } @@ -182,16 +213,14 @@ public class CommandHandlerTest { CommandSender sender = mock(CommandSender.class); CommandDescription command = mock(CommandDescription.class); given(command.getLabels()).willReturn(Collections.singletonList("test_cmd")); - given(commandService.mapPartsToCommand(any(CommandSender.class), anyListOf(String.class))).willReturn( + given(commandMapper.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(commandService).mapPartsToCommand(eq(sender), captor.capture()); - assertThat(captor.getValue(), contains("unreg", "testPlayer")); - + verify(commandMapper).mapPartsToCommand(sender, asList("unreg", "testPlayer")); verify(command, never()).getExecutableCommand(); ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); verify(sender, times(3)).sendMessage(captor.capture()); @@ -210,16 +239,14 @@ public class CommandHandlerTest { CommandSender sender = mock(CommandSender.class); CommandDescription command = mock(CommandDescription.class); given(command.getLabels()).willReturn(Collections.singletonList("test_cmd")); - given(commandService.mapPartsToCommand(any(CommandSender.class), anyListOf(String.class))).willReturn( + given(commandMapper.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(commandService).mapPartsToCommand(eq(sender), captor.capture()); - assertThat(captor.getValue(), contains("unreg", "testPlayer")); - + verify(commandMapper).mapPartsToCommand(sender, asList("unreg", "testPlayer")); verify(command, never()).getExecutableCommand(); ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); verify(sender, times(2)).sendMessage(captor.capture()); @@ -232,25 +259,21 @@ public class CommandHandlerTest { public void shouldStripWhitespace() { // given String bukkitLabel = "AuthMe"; - String[] bukkitArgs = {" ", "", "LOGIN", " ", "testArg", " "}; + String[] bukkitArgs = {" ", "", "REGISTER", " ", "testArg", " "}; CommandSender sender = mock(CommandSender.class); - ExecutableCommand executableCommand = mock(ExecutableCommand.class); CommandDescription command = mock(CommandDescription.class); - given(command.getExecutableCommand()).willReturn(executableCommand); - given(commandService.mapPartsToCommand(eq(sender), anyListOf(String.class))) - .willReturn(new FoundCommandResult(command, asList("AuthMe", "LOGIN"), asList("testArg"), 0.0, SUCCESS)); + doReturn(TestRegisterCommand.class).when(command).getExecutableCommand(); + given(commandMapper.mapPartsToCommand(eq(sender), anyListOf(String.class))) + .willReturn(new FoundCommandResult(command, asList("AuthMe", "REGISTER"), asList("testArg"), 0.0, SUCCESS)); // when handler.processCommand(sender, bukkitLabel, bukkitArgs); // then - verify(commandService).mapPartsToCommand(eq(sender), captor.capture()); - assertThat(captor.getValue(), contains("AuthMe", "LOGIN", "testArg")); - - verify(command.getExecutableCommand()).executeCommand(eq(sender), captor.capture()); - assertThat(captor.getValue(), contains("testArg")); - + ExecutableCommand executableCommand = mockedCommands.get(TestRegisterCommand.class); + verify(commandMapper).mapPartsToCommand(sender, asList("AuthMe", "REGISTER", "testArg")); + verify(executableCommand).executeCommand(sender, asList("testArg")); verify(sender, never()).sendMessage(anyString()); } diff --git a/src/test/java/fr/xephi/authme/command/CommandInitializerTest.java b/src/test/java/fr/xephi/authme/command/CommandInitializerTest.java index 81f4741a..97268e49 100644 --- a/src/test/java/fr/xephi/authme/command/CommandInitializerTest.java +++ b/src/test/java/fr/xephi/authme/command/CommandInitializerTest.java @@ -1,13 +1,10 @@ package fr.xephi.authme.command; -import fr.xephi.authme.initialization.AuthMeServiceInitializer; import fr.xephi.authme.permission.AdminPermission; import fr.xephi.authme.permission.PermissionNode; import fr.xephi.authme.util.StringUtils; import org.junit.BeforeClass; import org.junit.Test; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; import java.util.ArrayList; import java.util.Collection; @@ -20,14 +17,8 @@ import java.util.regex.Pattern; import static fr.xephi.authme.permission.DefaultPermission.OP_ONLY; import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.not; -import static org.hamcrest.Matchers.nullValue; -import static org.junit.Assert.assertSame; import static org.junit.Assert.assertThat; import static org.junit.Assert.fail; -import static org.mockito.Matchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; /** * Test for {@link CommandInitializer} to guarantee the integrity of the defined commands. @@ -45,15 +36,7 @@ public class CommandInitializerTest { @SuppressWarnings("unchecked") @BeforeClass public static void initializeCommandCollection() { - AuthMeServiceInitializer initializer = mock(AuthMeServiceInitializer.class); - when(initializer.newInstance(any(Class.class))).thenAnswer(new Answer() { - @Override - public Object answer(InvocationOnMock invocation) { - Class clazz = (Class) invocation.getArguments()[0]; - return mock(clazz); - } - }); - CommandInitializer commandInitializer = new CommandInitializer(initializer); + CommandInitializer commandInitializer = new CommandInitializer(); commands = commandInitializer.getCommands(); } @@ -174,34 +157,6 @@ public class CommandInitializerTest { walkThroughCommands(commands, descriptionTester); } - /** - * Check that the implementation of {@link ExecutableCommand} a command points to is the same for each type: - * it is inefficient to instantiate the same type multiple times. - */ - @Test - public void shouldNotHaveMultipleInstancesOfSameExecutableCommandSubType() { - // given - final Map, ExecutableCommand> implementations = new HashMap<>(); - BiConsumer descriptionTester = new BiConsumer() { - @Override - public void accept(CommandDescription command, int depth) { - assertThat(command.getExecutableCommand(), not(nullValue())); - ExecutableCommand commandExec = command.getExecutableCommand(); - ExecutableCommand storedExec = implementations.get(command.getExecutableCommand().getClass()); - if (storedExec == null) { - implementations.put(commandExec.getClass(), commandExec); - } else { - assertSame("has same implementation of '" + storedExec.getClass().getName() + "' for command with " - + "parent " + (command.getParent() == null ? "null" : command.getParent().getLabels()), - storedExec, commandExec); - } - } - }; - - // when/then - walkThroughCommands(commands, descriptionTester); - } - @Test public void shouldHaveOptionalArgumentsAfterMandatoryOnes() { // given @@ -291,7 +246,7 @@ public class CommandInitializerTest { } private void testCollectionForCommand(CommandDescription command, int argCount, Map, Integer> collection) { - final Class clazz = command.getExecutableCommand().getClass(); + final Class clazz = command.getExecutableCommand(); Integer existingCount = collection.get(clazz); if (existingCount == null) { collection.put(clazz, argCount); diff --git a/src/test/java/fr/xephi/authme/command/CommandMapperTest.java b/src/test/java/fr/xephi/authme/command/CommandMapperTest.java index 96eab841..26e43ab0 100644 --- a/src/test/java/fr/xephi/authme/command/CommandMapperTest.java +++ b/src/test/java/fr/xephi/authme/command/CommandMapperTest.java @@ -1,5 +1,9 @@ package fr.xephi.authme.command; +import fr.xephi.authme.command.TestCommandsUtil.TestLoginCommand; +import fr.xephi.authme.command.TestCommandsUtil.TestRegisterCommand; +import fr.xephi.authme.command.TestCommandsUtil.TestUnregisterCommand; +import fr.xephi.authme.command.executable.HelpCommand; import fr.xephi.authme.permission.PermissionNode; import fr.xephi.authme.permission.PermissionsManager; import org.bukkit.command.CommandSender; @@ -14,6 +18,7 @@ import static fr.xephi.authme.command.TestCommandsUtil.getCommandWithLabel; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.not; @@ -275,4 +280,14 @@ public class CommandMapperTest { assertThat(result.getArguments(), contains(parts.get(2))); } + @Test + public void shouldReturnExecutableCommandClasses() { + // given / when + Set> commandClasses = mapper.getCommandClasses(); + + // then + assertThat(commandClasses, containsInAnyOrder(ExecutableCommand.class, HelpCommand.class, + TestLoginCommand.class, TestRegisterCommand.class, TestUnregisterCommand.class)); + } + } diff --git a/src/test/java/fr/xephi/authme/command/CommandServiceTest.java b/src/test/java/fr/xephi/authme/command/CommandServiceTest.java index 334fc416..3a3b5312 100644 --- a/src/test/java/fr/xephi/authme/command/CommandServiceTest.java +++ b/src/test/java/fr/xephi/authme/command/CommandServiceTest.java @@ -1,6 +1,5 @@ package fr.xephi.authme.command; -import fr.xephi.authme.command.help.HelpProvider; import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.output.Messages; import fr.xephi.authme.settings.NewSetting; @@ -11,19 +10,14 @@ import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; -import java.util.Arrays; -import java.util.List; - 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.times; import static org.mockito.Mockito.verify; /** @@ -35,10 +29,6 @@ public class CommandServiceTest { @InjectMocks private CommandService commandService; @Mock - private CommandMapper commandMapper; - @Mock - private HelpProvider helpProvider; - @Mock private Messages messages; @Mock private NewSetting settings; @@ -69,40 +59,6 @@ public class CommandServiceTest { verify(messages).send(sender, MessageKey.ANTIBOT_AUTO_ENABLED_MESSAGE, "10"); } - @Test - public void shouldMapPartsToCommand() { - // given - CommandSender sender = mock(Player.class); - List commandParts = Arrays.asList("authme", "test", "test2"); - FoundCommandResult givenResult = mock(FoundCommandResult.class); - given(commandMapper.mapPartsToCommand(sender, commandParts)).willReturn(givenResult); - - // when - FoundCommandResult result = commandService.mapPartsToCommand(sender, commandParts); - - // then - assertThat(result, equalTo(givenResult)); - verify(commandMapper).mapPartsToCommand(sender, commandParts); - } - - @Test - public void shouldOutputHelp() { - // given - CommandSender sender = mock(CommandSender.class); - FoundCommandResult result = mock(FoundCommandResult.class); - int options = HelpProvider.SHOW_LONG_DESCRIPTION; - List messages = Arrays.asList("Test message 1", "Other test message", "Third message for test"); - given(helpProvider.printHelp(sender, result, options)).willReturn(messages); - - // when - commandService.outputHelp(sender, result, options); - - // then - ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); - verify(sender, times(3)).sendMessage(captor.capture()); - assertThat(captor.getAllValues(), equalTo(messages)); - } - @Test public void shouldRetrieveMessage() { // given diff --git a/src/test/java/fr/xephi/authme/command/CommandUtilsTest.java b/src/test/java/fr/xephi/authme/command/CommandUtilsTest.java index a290967c..bdff3c25 100644 --- a/src/test/java/fr/xephi/authme/command/CommandUtilsTest.java +++ b/src/test/java/fr/xephi/authme/command/CommandUtilsTest.java @@ -9,7 +9,6 @@ import java.util.List; import static org.hamcrest.Matchers.equalTo; import static org.junit.Assert.assertThat; -import static org.mockito.Mockito.mock; /** * Test for {@link CommandUtils}. @@ -59,14 +58,14 @@ public class CommandUtilsTest { .labels("authme", "auth") .description("Base") .detailedDescription("Test base command.") - .executableCommand(mock(ExecutableCommand.class)) + .executableCommand(ExecutableCommand.class) .build(); CommandDescription command = CommandDescription.builder() .parent(base) .labels("help", "h", "?") .description("Child") .detailedDescription("Test child command.") - .executableCommand(mock(ExecutableCommand.class)) + .executableCommand(ExecutableCommand.class) .build(); // when @@ -131,6 +130,6 @@ public class CommandUtilsTest { .labels("authme", "auth") .description("Base") .detailedDescription("Test base command.") - .executableCommand(mock(ExecutableCommand.class)); + .executableCommand(ExecutableCommand.class); } } diff --git a/src/test/java/fr/xephi/authme/command/TestCommandsUtil.java b/src/test/java/fr/xephi/authme/command/TestCommandsUtil.java index 65fb74e2..28536997 100644 --- a/src/test/java/fr/xephi/authme/command/TestCommandsUtil.java +++ b/src/test/java/fr/xephi/authme/command/TestCommandsUtil.java @@ -4,6 +4,7 @@ import fr.xephi.authme.command.executable.HelpCommand; import fr.xephi.authme.permission.AdminPermission; import fr.xephi.authme.permission.PermissionNode; import fr.xephi.authme.permission.PlayerPermission; +import org.bukkit.command.CommandSender; import java.util.Collection; import java.util.List; @@ -12,7 +13,6 @@ import java.util.Set; import static com.google.common.collect.Sets.newHashSet; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; -import static org.mockito.Mockito.mock; /** * Util class for generating and retrieving test commands. @@ -29,22 +29,24 @@ public final class TestCommandsUtil { */ public static Set generateCommands() { // Register /authme - CommandDescription authMeBase = createCommand(null, null, singletonList("authme")); + CommandDescription authMeBase = createCommand(null, null, singletonList("authme"), ExecutableCommand.class); // Register /authme login - createCommand(PlayerPermission.LOGIN, authMeBase, singletonList("login"), newArgument("password", false)); + createCommand(PlayerPermission.LOGIN, authMeBase, singletonList("login"), + TestLoginCommand.class, newArgument("password", false)); // Register /authme register , aliases: /authme reg, /authme r - createCommand(PlayerPermission.LOGIN, authMeBase, asList("register", "reg", "r"), + createCommand(PlayerPermission.LOGIN, authMeBase, asList("register", "reg", "r"), TestRegisterCommand.class, newArgument("password", false), newArgument("confirmation", false)); // Register /email [player] - CommandDescription emailBase = createCommand(null, null, singletonList("email"), newArgument("player", true)); + CommandDescription emailBase = createCommand(null, null, singletonList("email"), ExecutableCommand.class, + newArgument("player", true)); // Register /email helptest -- use only to test for help command arguments special case - CommandDescription.builder().parent(emailBase).labels("helptest").executableCommand(mock(HelpCommand.class)) + CommandDescription.builder().parent(emailBase).labels("helptest").executableCommand(HelpCommand.class) .description("test").detailedDescription("Test.").withArgument("Query", "", false).build(); // Register /unregister , alias: /unreg CommandDescription unregisterBase = createCommand(AdminPermission.UNREGISTER, null, - asList("unregister", "unreg"), newArgument("player", false)); + asList("unregister", "unreg"), TestUnregisterCommand.class, newArgument("player", false)); return newHashSet(authMeBase, emailBase, unregisterBase); } @@ -83,14 +85,15 @@ public final class TestCommandsUtil { /** Shortcut command to initialize a new test command. */ private static CommandDescription createCommand(PermissionNode permission, CommandDescription parent, - List labels, CommandArgumentDescription... arguments) { + List labels, Class commandClass, + CommandArgumentDescription... arguments) { CommandDescription.CommandBuilder command = CommandDescription.builder() .labels(labels) .parent(parent) .permission(permission) .description(labels.get(0) + " cmd") .detailedDescription("'" + labels.get(0) + "' test command") - .executableCommand(mock(ExecutableCommand.class)); + .executableCommand(commandClass); if (arguments != null && arguments.length > 0) { for (CommandArgumentDescription argument : arguments) { @@ -106,4 +109,25 @@ public final class TestCommandsUtil { return new CommandArgumentDescription(label, "'" + label + "' argument description", isOptional); } + public static class TestLoginCommand implements ExecutableCommand { + @Override + public void executeCommand(CommandSender sender, List arguments) { + // noop + } + } + + public static class TestRegisterCommand implements ExecutableCommand { + @Override + public void executeCommand(CommandSender sender, List arguments) { + // noop + } + } + + public static class TestUnregisterCommand implements ExecutableCommand { + @Override + public void executeCommand(CommandSender sender, List arguments) { + // noop + } + } + } diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/SwitchAntiBotCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/SwitchAntiBotCommandTest.java index cd704557..9b380411 100644 --- a/src/test/java/fr/xephi/authme/command/executable/authme/SwitchAntiBotCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/authme/SwitchAntiBotCommandTest.java @@ -1,7 +1,7 @@ package fr.xephi.authme.command.executable.authme; import fr.xephi.authme.AntiBot; -import fr.xephi.authme.command.CommandService; +import fr.xephi.authme.command.CommandMapper; import fr.xephi.authme.command.FoundCommandResult; import fr.xephi.authme.command.help.HelpProvider; import org.bukkit.command.CommandSender; @@ -35,7 +35,10 @@ public class SwitchAntiBotCommandTest { private AntiBot antiBot; @Mock - private CommandService service; + private CommandMapper commandMapper; + + @Mock + private HelpProvider helpProvider; @Test public void shouldReturnAntiBotState() { @@ -81,7 +84,7 @@ public class SwitchAntiBotCommandTest { // given CommandSender sender = mock(CommandSender.class); FoundCommandResult foundCommandResult = mock(FoundCommandResult.class); - given(service.mapPartsToCommand(sender, asList("authme", "antibot"))).willReturn(foundCommandResult); + given(commandMapper.mapPartsToCommand(sender, asList("authme", "antibot"))).willReturn(foundCommandResult); // when command.executeCommand(sender, Collections.singletonList("wrong")); @@ -89,6 +92,6 @@ public class SwitchAntiBotCommandTest { // then verify(antiBot, never()).overrideAntiBotStatus(anyBoolean()); verify(sender).sendMessage(argThat(containsString("Invalid"))); - verify(service).outputHelp(sender, foundCommandResult, HelpProvider.SHOW_ARGUMENTS); + verify(helpProvider).outputHelp(sender, foundCommandResult, HelpProvider.SHOW_ARGUMENTS); } } diff --git a/src/test/java/fr/xephi/authme/command/help/HelpProviderTest.java b/src/test/java/fr/xephi/authme/command/help/HelpProviderTest.java index f40350e7..b5450fb0 100644 --- a/src/test/java/fr/xephi/authme/command/help/HelpProviderTest.java +++ b/src/test/java/fr/xephi/authme/command/help/HelpProviderTest.java @@ -13,6 +13,7 @@ import org.bukkit.command.CommandSender; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; +import org.mockito.ArgumentCaptor; import java.util.Arrays; import java.util.Collections; @@ -33,7 +34,9 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasSize; import static org.junit.Assert.assertThat; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; /** * Test for {@link HelpProvider}. @@ -67,9 +70,10 @@ public class HelpProviderTest { FoundCommandResult result = newFoundResult(command, Arrays.asList("authme", "login")); // when - List lines = helpProvider.printHelp(sender, result, SHOW_LONG_DESCRIPTION); + helpProvider.outputHelp(sender, result, SHOW_LONG_DESCRIPTION); // then + List lines = getLines(sender); assertThat(lines, hasSize(5)); assertThat(lines.get(0), containsString(HELP_HEADER + " HELP")); assertThat(removeColors(lines.get(1)), containsString("Command: /authme login ")); @@ -85,9 +89,10 @@ public class HelpProviderTest { FoundCommandResult result = newFoundResult(command, Arrays.asList("authme", "reg")); // when - List lines = helpProvider.printHelp(sender, result, HIDE_COMMAND | SHOW_ARGUMENTS); + helpProvider.outputHelp(sender, result, HIDE_COMMAND | SHOW_ARGUMENTS); // then + List lines = getLines(sender); assertThat(lines, hasSize(4)); assertThat(lines.get(0), containsString(HELP_HEADER + " HELP")); assertThat(removeColors(lines.get(1)), equalTo("Arguments:")); @@ -102,9 +107,10 @@ public class HelpProviderTest { FoundCommandResult result = newFoundResult(command, Collections.singletonList("email")); // when - List lines = helpProvider.printHelp(sender, result, HIDE_COMMAND | SHOW_ARGUMENTS); + helpProvider.outputHelp(sender, result, HIDE_COMMAND | SHOW_ARGUMENTS); // then + List lines = getLines(sender); assertThat(lines, hasSize(3)); assertThat(removeColors(lines.get(2)), containsString("player: 'player' argument description (Optional)")); } @@ -117,9 +123,10 @@ public class HelpProviderTest { FoundCommandResult result = newFoundResult(command, Collections.singletonList("authme")); // when - List lines = helpProvider.printHelp(sender, result, HIDE_COMMAND | SHOW_ARGUMENTS); + helpProvider.outputHelp(sender, result, HIDE_COMMAND | SHOW_ARGUMENTS); // then + List lines = getLines(sender); assertThat(lines, hasSize(1)); // only has the help banner } @@ -133,9 +140,10 @@ public class HelpProviderTest { given(permissionsManager.hasPermission(sender, command.getPermission())).willReturn(true); // when - List lines = helpProvider.printHelp(sender, result, HIDE_COMMAND | SHOW_PERMISSIONS); + helpProvider.outputHelp(sender, result, HIDE_COMMAND | SHOW_PERMISSIONS); // then + List lines = getLines(sender); assertThat(lines, hasSize(5)); assertThat(removeColors(lines.get(1)), containsString("Permissions:")); assertThat(removeColors(lines.get(2)), @@ -154,9 +162,10 @@ public class HelpProviderTest { given(permissionsManager.hasPermission(sender, command.getPermission())).willReturn(false); // when - List lines = helpProvider.printHelp(sender, result, HIDE_COMMAND | SHOW_PERMISSIONS); + helpProvider.outputHelp(sender, result, HIDE_COMMAND | SHOW_PERMISSIONS); // then + List lines = getLines(sender); assertThat(lines, hasSize(5)); assertThat(removeColors(lines.get(1)), containsString("Permissions:")); assertThat(removeColors(lines.get(2)), @@ -172,9 +181,10 @@ public class HelpProviderTest { FoundCommandResult result = newFoundResult(command, Collections.singletonList("authme")); // when - List lines = helpProvider.printHelp(sender, result, HIDE_COMMAND | SHOW_PERMISSIONS); + helpProvider.outputHelp(sender, result, HIDE_COMMAND | SHOW_PERMISSIONS); // then + List lines = getLines(sender); assertThat(lines, hasSize(1)); } @@ -187,9 +197,10 @@ public class HelpProviderTest { FoundCommandResult result = newFoundResult(command, Collections.singletonList("test")); // when - List lines = helpProvider.printHelp(sender, result, HIDE_COMMAND | SHOW_PERMISSIONS); + helpProvider.outputHelp(sender, result, HIDE_COMMAND | SHOW_PERMISSIONS); // then + List lines = getLines(sender); assertThat(lines, hasSize(1)); } @@ -200,9 +211,10 @@ public class HelpProviderTest { FoundCommandResult result = newFoundResult(command, Arrays.asList("authme", "reg")); // when - List lines = helpProvider.printHelp(sender, result, HIDE_COMMAND | SHOW_ALTERNATIVES); + helpProvider.outputHelp(sender, result, HIDE_COMMAND | SHOW_ALTERNATIVES); // then + List lines = getLines(sender); assertThat(lines, hasSize(4)); assertThat(removeColors(lines.get(1)), containsString("Alternatives:")); assertThat(removeColors(lines.get(2)), containsString("/authme register ")); @@ -216,9 +228,10 @@ public class HelpProviderTest { FoundCommandResult result = newFoundResult(command, Arrays.asList("authme", "login")); // when - List lines = helpProvider.printHelp(sender, result, HIDE_COMMAND | SHOW_ALTERNATIVES); + helpProvider.outputHelp(sender, result, HIDE_COMMAND | SHOW_ALTERNATIVES); // then + List lines = getLines(sender); assertThat(lines, hasSize(1)); } @@ -229,9 +242,10 @@ public class HelpProviderTest { FoundCommandResult result = newFoundResult(command, Collections.singletonList("authme")); // when - List lines = helpProvider.printHelp(sender, result, HIDE_COMMAND | SHOW_CHILDREN); + helpProvider.outputHelp(sender, result, HIDE_COMMAND | SHOW_CHILDREN); // then + List lines = getLines(sender); assertThat(lines, hasSize(4)); assertThat(removeColors(lines.get(1)), containsString("Commands:")); assertThat(removeColors(lines.get(2)), containsString("/authme login: login cmd")); @@ -245,9 +259,10 @@ public class HelpProviderTest { FoundCommandResult result = newFoundResult(command, Collections.singletonList("authme")); // when - List lines = helpProvider.printHelp(sender, result, HIDE_COMMAND | SHOW_CHILDREN); + helpProvider.outputHelp(sender, result, HIDE_COMMAND | SHOW_CHILDREN); // then + List lines = getLines(sender); assertThat(lines, hasSize(1)); } @@ -258,9 +273,10 @@ public class HelpProviderTest { Collections.emptyList(), 0.0, FoundResultStatus.UNKNOWN_LABEL); // when - List lines = helpProvider.printHelp(sender, result, ALL_OPTIONS); + helpProvider.outputHelp(sender, result, ALL_OPTIONS); // then + List lines = getLines(sender); assertThat(lines, hasSize(1)); assertThat(lines.get(0), containsString("Failed to retrieve any help information")); } @@ -276,9 +292,10 @@ public class HelpProviderTest { FoundCommandResult result = newFoundResult(command, Arrays.asList("authme", "ragister")); // when - List lines = helpProvider.printHelp(sender, result, 0); + helpProvider.outputHelp(sender, result, 0); // then + List lines = getLines(sender); assertThat(lines, hasSize(2)); assertThat(lines.get(0), containsString(HELP_HEADER + " HELP")); assertThat(removeColors(lines.get(1)), containsString("Command: /authme register ")); @@ -328,5 +345,11 @@ public class HelpProviderTest { } return str; } + + private static List getLines(CommandSender sender) { + ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); + verify(sender, atLeastOnce()).sendMessage(captor.capture()); + return captor.getAllValues(); + } } diff --git a/src/test/java/tools/commands/CommandPageCreater.java b/src/test/java/tools/commands/CommandPageCreater.java index b8478205..d5375a2f 100644 --- a/src/test/java/tools/commands/CommandPageCreater.java +++ b/src/test/java/tools/commands/CommandPageCreater.java @@ -4,11 +4,7 @@ import fr.xephi.authme.command.CommandArgumentDescription; import fr.xephi.authme.command.CommandDescription; import fr.xephi.authme.command.CommandInitializer; import fr.xephi.authme.command.CommandUtils; -import fr.xephi.authme.command.ExecutableCommand; -import fr.xephi.authme.initialization.AuthMeServiceInitializer; import fr.xephi.authme.permission.PermissionNode; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; import tools.utils.AutoToolTask; import tools.utils.FileUtils; import tools.utils.TagValue.NestedTagValue; @@ -19,10 +15,6 @@ import java.util.Collection; import java.util.Scanner; import java.util.Set; -import static org.mockito.Matchers.isA; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - public class CommandPageCreater implements AutoToolTask { private static final String OUTPUT_FILE = ToolsConstants.DOCS_FOLDER + "commands.md"; @@ -39,7 +31,7 @@ public class CommandPageCreater implements AutoToolTask { @Override public void executeDefault() { - CommandInitializer commandInitializer = new CommandInitializer(getMockInitializer()); + CommandInitializer commandInitializer = new CommandInitializer(); final Set baseCommands = commandInitializer.getCommands(); NestedTagValue commandTags = new NestedTagValue(); addCommandsInfo(commandTags, baseCommands); @@ -84,25 +76,4 @@ public class CommandPageCreater implements AutoToolTask { } return result.toString(); } - - /** - * Creates an initializer mock that returns mocks of any {@link ExecutableCommand} subclasses passed to it. - * - * @return the initializer mock - */ - @SuppressWarnings("unchecked") - private static AuthMeServiceInitializer getMockInitializer() { - AuthMeServiceInitializer initializer = mock(AuthMeServiceInitializer.class); - when(initializer.newInstance(isA(Class.class))).thenAnswer(new Answer() { - @Override - public Object answer(InvocationOnMock invocation) throws Throwable { - Class clazz = (Class) invocation.getArguments()[0]; - if (ExecutableCommand.class.isAssignableFrom(clazz)) { - return mock(clazz); - } - throw new IllegalStateException("Unexpected request to instantiate class of type " + clazz.getName()); - } - }); - return initializer; - } } From 91111ca47658eef9663b8e29083825d97bd3bccd Mon Sep 17 00:00:00 2001 From: ljacqu Date: Sat, 4 Jun 2016 21:16:58 +0200 Subject: [PATCH 161/200] #727 Remove CommandService being passed as parameter to other methods - No longer need to pass as param to other methods since CommandService is now an injected field --- .../executable/authme/UnregisterAdminCommand.java | 11 +++++------ .../command/executable/captcha/CaptchaCommand.java | 10 +++++----- .../executable/register/RegisterCommand.java | 13 ++++++------- 3 files changed, 16 insertions(+), 18 deletions(-) diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/UnregisterAdminCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/UnregisterAdminCommand.java index d81b9baf..341666f0 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/UnregisterAdminCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/UnregisterAdminCommand.java @@ -72,7 +72,7 @@ public class UnregisterAdminCommand implements ExecutableCommand { Utils.setGroup(target, Utils.GroupType.UNREGISTERED); if (target != null && target.isOnline()) { if (commandService.getProperty(RegistrationSettings.FORCE)) { - applyUnregisteredEffectsAndTasks(target, commandService); + applyUnregisteredEffectsAndTasks(target); } commandService.send(target, MessageKey.UNREGISTERED_SUCCESS); } @@ -88,15 +88,14 @@ public class UnregisterAdminCommand implements ExecutableCommand { * timeout kick, blindness. * * @param target the player that was unregistered - * @param service the command service */ - private void applyUnregisteredEffectsAndTasks(Player target, CommandService service) { + private void applyUnregisteredEffectsAndTasks(Player target) { final String playerNameLowerCase = target.getName().toLowerCase(); Utils.teleportToSpawn(target); limboCache.addLimboPlayer(target); - int timeOut = service.getProperty(RestrictionSettings.TIMEOUT) * TICKS_PER_SECOND; - int interval = service.getProperty(RegistrationSettings.MESSAGE_INTERVAL); + int timeOut = commandService.getProperty(RestrictionSettings.TIMEOUT) * TICKS_PER_SECOND; + int interval = commandService.getProperty(RegistrationSettings.MESSAGE_INTERVAL); if (timeOut != 0) { BukkitTask id = bukkitService.runTaskLater(new TimeoutTask(authMe, playerNameLowerCase, target), timeOut); limboCache.getLimboPlayer(playerNameLowerCase).setTimeoutTask(id); @@ -105,7 +104,7 @@ public class UnregisterAdminCommand implements ExecutableCommand { bukkitService.runTask(new MessageTask(bukkitService, authMe.getMessages(), playerNameLowerCase, MessageKey.REGISTER_MESSAGE, interval))); - if (service.getProperty(RegistrationSettings.APPLY_BLIND_EFFECT)) { + if (commandService.getProperty(RegistrationSettings.APPLY_BLIND_EFFECT)) { target.addPotionEffect(new PotionEffect(PotionEffectType.BLINDNESS, timeOut, 2)); } } 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 0cae9aa0..1577c7ad 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 @@ -30,18 +30,18 @@ public class CaptchaCommand extends PlayerCommand { } else if (!captchaManager.isCaptchaRequired(playerName)) { commandService.send(player, MessageKey.USAGE_LOGIN); } else { - checkCaptcha(player, arguments.get(0), commandService); + checkCaptcha(player, arguments.get(0)); } } - private void checkCaptcha(Player player, String captchaCode, CommandService service) { + private void checkCaptcha(Player player, String captchaCode) { final boolean isCorrectCode = captchaManager.checkCode(player.getName(), captchaCode); if (isCorrectCode) { - service.send(player, MessageKey.CAPTCHA_SUCCESS); - service.send(player, MessageKey.LOGIN_MESSAGE); + commandService.send(player, MessageKey.CAPTCHA_SUCCESS); + commandService.send(player, MessageKey.LOGIN_MESSAGE); } else { String newCode = captchaManager.generateCode(player.getName()); - service.send(player, MessageKey.CAPTCHA_WRONG_ERROR, newCode); + commandService.send(player, MessageKey.CAPTCHA_WRONG_ERROR, newCode); } } } 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 f9a6d165..ed9f9eef 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 @@ -36,16 +36,16 @@ public class RegisterCommand extends PlayerCommand { } // Ensure that there is 1 argument, or 2 if confirmation is required - final boolean useConfirmation = isConfirmationRequired(commandService); + final boolean useConfirmation = isConfirmationRequired(); if (arguments.isEmpty() || useConfirmation && arguments.size() < 2) { commandService.send(player, MessageKey.USAGE_REGISTER); return; } if (commandService.getProperty(USE_EMAIL_REGISTRATION)) { - handleEmailRegistration(player, arguments, commandService); + handleEmailRegistration(player, arguments); } else { - handlePasswordRegistration(player, arguments, commandService); + handlePasswordRegistration(player, arguments); } } @@ -54,7 +54,7 @@ public class RegisterCommand extends PlayerCommand { return "/authme register "; } - private void handlePasswordRegistration(Player player, List arguments, CommandService commandService) { + private void handlePasswordRegistration(Player player, List arguments) { if (commandService.getProperty(ENABLE_PASSWORD_CONFIRMATION) && !arguments.get(0).equals(arguments.get(1))) { commandService.send(player, MessageKey.PASSWORD_MATCH_ERROR); } else { @@ -62,7 +62,7 @@ public class RegisterCommand extends PlayerCommand { } } - private void handleEmailRegistration(Player player, List arguments, CommandService commandService) { + private void handleEmailRegistration(Player player, List arguments) { if (commandService.getProperty(EmailSettings.MAIL_ACCOUNT).isEmpty()) { player.sendMessage("Cannot register: no email address is set for the server. " + "Please contact an administrator"); @@ -85,10 +85,9 @@ public class RegisterCommand extends PlayerCommand { /** * Return whether the password or email has to be confirmed. * - * @param commandService The command service * @return True if the confirmation is needed, false otherwise */ - private boolean isConfirmationRequired(CommandService commandService) { + private boolean isConfirmationRequired() { return commandService.getProperty(USE_EMAIL_REGISTRATION) ? commandService.getProperty(ENABLE_CONFIRM_EMAIL) : commandService.getProperty(ENABLE_PASSWORD_CONFIRMATION); From 2a4bb483a31f71653d187defd674948b7837595f Mon Sep 17 00:00:00 2001 From: ljacqu Date: Sat, 4 Jun 2016 21:57:31 +0200 Subject: [PATCH 162/200] #727 finalization - minor javadoc changes --- .../fr/xephi/authme/command/CommandDescription.java | 10 +++++----- .../java/fr/xephi/authme/command/CommandHandler.java | 6 +++--- .../java/fr/xephi/authme/command/CommandMapper.java | 5 ++--- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/main/java/fr/xephi/authme/command/CommandDescription.java b/src/main/java/fr/xephi/authme/command/CommandDescription.java index 115d0f23..36ad6da6 100644 --- a/src/main/java/fr/xephi/authme/command/CommandDescription.java +++ b/src/main/java/fr/xephi/authme/command/CommandDescription.java @@ -35,7 +35,7 @@ public class CommandDescription { */ private String detailedDescription; /** - * The executable command instance described by this object. + * The class implementing the command described by this object. */ private Class executableCommand; /** @@ -112,7 +112,7 @@ public class CommandDescription { } /** - * Check whether this command description has a specific command. + * Check whether this command description has the given label. * * @param commandLabel The label to check for. * @@ -128,9 +128,9 @@ public class CommandDescription { } /** - * Return the {@link ExecutableCommand} instance defined by the command description. + * Return the {@link ExecutableCommand} class implementing this command. * - * @return The executable command object. + * @return The executable command class */ public Class getExecutableCommand() { return executableCommand; @@ -139,7 +139,7 @@ public class CommandDescription { /** * Return the parent. * - * @return The parent command, or null for base commands. + * @return The parent command, or null for base commands */ public CommandDescription getParent() { return parent; diff --git a/src/main/java/fr/xephi/authme/command/CommandHandler.java b/src/main/java/fr/xephi/authme/command/CommandHandler.java index 0b76a822..da5299e2 100644 --- a/src/main/java/fr/xephi/authme/command/CommandHandler.java +++ b/src/main/java/fr/xephi/authme/command/CommandHandler.java @@ -16,8 +16,8 @@ import java.util.Map; import java.util.Set; /** - * The AuthMe command handler, responsible for mapping incoming commands to the correct {@link CommandDescription} - * or to display help messages for unknown invocations. + * The AuthMe command handler, responsible for invoking the correct {@link ExecutableCommand} based on incoming + * command labels or for displaying a help message for unknown command labels. */ public class CommandHandler { @@ -88,7 +88,7 @@ public class CommandHandler { } /** - * Initializes all required ExecutableCommand objects. + * Initialize all required ExecutableCommand objects. * * @param commandClasses the classes to instantiate */ diff --git a/src/main/java/fr/xephi/authme/command/CommandMapper.java b/src/main/java/fr/xephi/authme/command/CommandMapper.java index d0706c80..44d5b583 100644 --- a/src/main/java/fr/xephi/authme/command/CommandMapper.java +++ b/src/main/java/fr/xephi/authme/command/CommandMapper.java @@ -17,8 +17,7 @@ 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}. + * Maps incoming command parts to the correct {@link CommandDescription}. */ public class CommandMapper { @@ -159,7 +158,7 @@ public class CommandMapper { private static FoundCommandResult transformResultForHelp(FoundCommandResult result) { if (result.getCommandDescription() != null - && HELP_COMMAND_CLASS.isAssignableFrom(result.getCommandDescription().getExecutableCommand())) { + && HELP_COMMAND_CLASS == result.getCommandDescription().getExecutableCommand()) { // For "/authme help register" we have labels = [authme, help] and arguments = [register] // But for the help command we want labels = [authme, help] and arguments = [authme, register], // so we can use the arguments as the labels to the command to show help for From 4fe26f08d457f5ad805e0dab05e5b29470a49497 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Sat, 4 Jun 2016 22:40:30 +0200 Subject: [PATCH 163/200] #612 Check if plugin is permission system within method itself - Iterate over all values within the method - Bug fix: change method to use pluginName field, and not name --- .../authme/permission/PermissionsManager.java | 18 +++----- .../permission/PermissionsSystemType.java | 9 +++- .../permission/PermissionsSystemTypeTest.java | 45 +++++++++++++++++++ 3 files changed, 58 insertions(+), 14 deletions(-) create mode 100644 src/test/java/fr/xephi/authme/permission/PermissionsSystemTypeTest.java diff --git a/src/main/java/fr/xephi/authme/permission/PermissionsManager.java b/src/main/java/fr/xephi/authme/permission/PermissionsManager.java index d8bc5337..4e1c4253 100644 --- a/src/main/java/fr/xephi/authme/permission/PermissionsManager.java +++ b/src/main/java/fr/xephi/authme/permission/PermissionsManager.java @@ -199,12 +199,9 @@ public class PermissionsManager { */ public void onPluginEnable(String pluginName) { // Check if any known permissions system is enabling - for (PermissionsSystemType permissionsSystemType : PermissionsSystemType.values()) { - if (permissionsSystemType.isPermissionSystem(pluginName)) { - ConsoleLogger.info(pluginName + " plugin enabled, dynamically updating permissions hooks!"); - setup(); - break; - } + if (PermissionsSystemType.isPermissionSystem(pluginName)) { + ConsoleLogger.info(pluginName + " plugin enabled, dynamically updating permissions hooks!"); + setup(); } } @@ -215,12 +212,9 @@ public class PermissionsManager { */ public void onPluginDisable(String pluginName) { // Check if any known permission system is being disabled - for (PermissionsSystemType permissionsSystemType : PermissionsSystemType.values()) { - if (permissionsSystemType.isPermissionSystem(pluginName)) { - ConsoleLogger.info(pluginName + " plugin disabled, updating hooks!"); - setup(); - break; - } + if (PermissionsSystemType.isPermissionSystem(pluginName)) { + ConsoleLogger.info(pluginName + " plugin disabled, updating hooks!"); + setup(); } } diff --git a/src/main/java/fr/xephi/authme/permission/PermissionsSystemType.java b/src/main/java/fr/xephi/authme/permission/PermissionsSystemType.java index a65ac50c..88b09d19 100644 --- a/src/main/java/fr/xephi/authme/permission/PermissionsSystemType.java +++ b/src/main/java/fr/xephi/authme/permission/PermissionsSystemType.java @@ -90,7 +90,12 @@ public enum PermissionsSystemType { * @param name The name of the plugin to check. * @return If the plugin is a valid permissions system. */ - public boolean isPermissionSystem(String name) { - return name.equals(pluginName); + public static boolean isPermissionSystem(String name) { + for (PermissionsSystemType permissionsSystemType : values()) { + if (permissionsSystemType.pluginName.equals(name)) { + return true; + } + } + return false; } } diff --git a/src/test/java/fr/xephi/authme/permission/PermissionsSystemTypeTest.java b/src/test/java/fr/xephi/authme/permission/PermissionsSystemTypeTest.java new file mode 100644 index 00000000..809014ad --- /dev/null +++ b/src/test/java/fr/xephi/authme/permission/PermissionsSystemTypeTest.java @@ -0,0 +1,45 @@ +package fr.xephi.authme.permission; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertThat; + +/** + * Test for {@link PermissionsSystemType}. + */ +public class PermissionsSystemTypeTest { + + @Test + public void shouldHaveDefinedAndUniqueNames() { + // given / when / then + List names = new ArrayList<>(PermissionsSystemType.values().length); + List pluginNames = new ArrayList<>(PermissionsSystemType.values().length); + + for (PermissionsSystemType system : PermissionsSystemType.values()) { + assertThat("Name for enum entry '" + system + "' is not null", + system.getName(), not(nullValue())); + assertThat("Plugin name for enum entry '" + system + "' is not null", + system.getPluginName(), not(nullValue())); + assertThat("Only one enum entry has name '" + system.getName() + "'", + names, not(hasItem(system.getName()))); + assertThat("Only one enum entry has plugin name '" + system.getPluginName() + "'", + pluginNames, not(hasItem(system.getPluginName()))); + names.add(system.getName()); + pluginNames.add(system.getPluginName()); + } + } + + @Test + public void shouldRecognizePermissionSystemType() { + assertThat(PermissionsSystemType.isPermissionSystem("bogus"), equalTo(false)); + assertThat(PermissionsSystemType.isPermissionSystem("PermissionsBukkit"), equalTo(true)); + } + +} From 3753a0ef96d7911ed7e9bedf2a7f6250525e57b5 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Sun, 5 Jun 2016 00:29:31 +0200 Subject: [PATCH 164/200] #565 Allow to skip extended encryption methods - Set system property via surefire plugin and create profile that modifies the property - Check for the new property in AbstractEncryptionMethodTest and shorten/skip the tests when necessary --- pom.xml | 15 +++++++++++++++ .../crypts/AbstractEncryptionMethodTest.java | 17 +++++++++++++++-- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index edad3fc7..a221186c 100644 --- a/pom.xml +++ b/pom.xml @@ -56,6 +56,7 @@ AuthMe CUSTOM + false ${project.version}-b${project.buildNumber} ${project.outputName}-${project.version} @@ -81,6 +82,17 @@ ${env.BUILD_NUMBER} + + skipLongHashTests + + + skipLongHashTests + + + + true + + @@ -124,6 +136,9 @@ 2.19.1 -Dfile.encoding=${project.build.sourceEncoding} @{argLine} + + ${project.skipExtendedHashTests} + diff --git a/src/test/java/fr/xephi/authme/security/crypts/AbstractEncryptionMethodTest.java b/src/test/java/fr/xephi/authme/security/crypts/AbstractEncryptionMethodTest.java index 45489710..1ee72840 100644 --- a/src/test/java/fr/xephi/authme/security/crypts/AbstractEncryptionMethodTest.java +++ b/src/test/java/fr/xephi/authme/security/crypts/AbstractEncryptionMethodTest.java @@ -12,6 +12,7 @@ import static org.hamcrest.Matchers.equalTo; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeThat; /** * Test for implementations of {@link EncryptionMethod}. @@ -36,6 +37,14 @@ public abstract class AbstractEncryptionMethodTest { "asdfg:hjkl", "::test", "~#$#~~~#$#~", "d41d8cd98f00b204e9800998ecf427e", "$2y$7a$da641e404b982ed" }; + /** + * Certain hash algorithms are slow by design, which has a considerable effect on these unit tests. + * Setting the property below to "true" will reduce the checks in these unit tests as to offer fast, + * partial tests during development. + */ + private static final boolean SKIP_LONG_TESTS = + "true".equals(System.getProperty("project.skipExtendedHashTests")); + /** The encryption method to test. */ private EncryptionMethod method; /** Map with the hashes against which the entries in GIVEN_PASSWORDS are tested. */ @@ -79,8 +88,10 @@ public abstract class AbstractEncryptionMethodTest { @Test public void testGivenPasswords() { - // Test all entries in GIVEN_PASSWORDS except the last one - for (int i = 0; i < GIVEN_PASSWORDS.length - 1; ++i) { + // Start with the 2nd to last password if we skip long tests + int start = SKIP_LONG_TESTS ? GIVEN_PASSWORDS.length - 2 : 0; + // Test entries in GIVEN_PASSWORDS except the last one + for (int i = start; i < GIVEN_PASSWORDS.length - 1; ++i) { String password = GIVEN_PASSWORDS[i]; assertTrue("Hash for password '" + password + "' should match", doesGivenHashMatch(password, method)); @@ -124,12 +135,14 @@ public abstract class AbstractEncryptionMethodTest { assertFalse("Upper-case of '" + password + "' should not match generated hash '" + hash + "'", method.comparePassword(password.toUpperCase(), hashedPassword, USERNAME)); } + assumeThat(SKIP_LONG_TESTS, equalTo(false)); } } /** Tests various strings to ensure that encryption methods don't rely on the hash's format too much. */ @Test public void testMalformedHashes() { + assumeThat(SKIP_LONG_TESTS, equalTo(false)); String salt = method.hasSeparateSalt() ? "testSalt" : null; for (String bogusHash : BOGUS_HASHES) { HashedPassword hashedPwd = new HashedPassword(bogusHash, salt); From a5a796e90058e174252559fa5f2ce632334949e1 Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Sun, 5 Jun 2016 01:47:17 +0200 Subject: [PATCH 165/200] Update coveralls-maven-plugin --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index a221186c..ac79d331 100644 --- a/pom.xml +++ b/pom.xml @@ -285,7 +285,7 @@ org.eluder.coveralls coveralls-maven-plugin - 4.1.0 + 4.2.0 false From 2e269b6f5e5c610d3d849a7d15284f6f6f2c6a8c Mon Sep 17 00:00:00 2001 From: ljacqu Date: Sun, 5 Jun 2016 13:21:05 +0200 Subject: [PATCH 166/200] Add missing unit tests for commands --- .../executable/authme/ConverterCommand.java | 4 +- .../unregister/UnregisterCommand.java | 5 +- .../command/executable/HelpCommandTest.java | 158 +++++++++++++++ .../authme/ConverterCommandTest.java | 104 ++++++++++ .../authme/SetEmailCommandTest.java | 188 ++++++++++++++++++ .../authme/UnregisterAdminCommandTest.java | 71 +++++++ .../unregister/UnregisterCommandTest.java | 74 +++++++ 7 files changed, 602 insertions(+), 2 deletions(-) create mode 100644 src/test/java/fr/xephi/authme/command/executable/HelpCommandTest.java create mode 100644 src/test/java/fr/xephi/authme/command/executable/authme/ConverterCommandTest.java create mode 100644 src/test/java/fr/xephi/authme/command/executable/authme/SetEmailCommandTest.java create mode 100644 src/test/java/fr/xephi/authme/command/executable/authme/UnregisterAdminCommandTest.java create mode 100644 src/test/java/fr/xephi/authme/command/executable/unregister/UnregisterCommandTest.java diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/ConverterCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/ConverterCommand.java index 4c2865b5..e18c9dc4 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/ConverterCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/ConverterCommand.java @@ -1,5 +1,6 @@ package fr.xephi.authme.command.executable.authme; +import com.google.common.annotations.VisibleForTesting; import fr.xephi.authme.command.CommandService; import fr.xephi.authme.command.ExecutableCommand; import fr.xephi.authme.converter.Converter; @@ -58,7 +59,8 @@ public class ConverterCommand implements ExecutableCommand { sender.sendMessage("[AuthMe] Successfully converted from " + jobType.getName()); } - private enum ConvertType { + @VisibleForTesting + enum ConvertType { XAUTH("xauth", xAuthConverter.class), CRAZYLOGIN("crazylogin", CrazyLoginConverter.class), RAKAMAK("rakamak", RakamakConverter.class), diff --git a/src/main/java/fr/xephi/authme/command/executable/unregister/UnregisterCommand.java b/src/main/java/fr/xephi/authme/command/executable/unregister/UnregisterCommand.java index e289480e..720b136e 100644 --- a/src/main/java/fr/xephi/authme/command/executable/unregister/UnregisterCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/unregister/UnregisterCommand.java @@ -18,13 +18,16 @@ public class UnregisterCommand extends PlayerCommand { @Inject private CommandService commandService; + @Inject + private PlayerCache playerCache; + @Override public void runCommand(Player player, List arguments) { String playerPass = arguments.get(0); final String playerNameLowerCase = player.getName().toLowerCase(); // Make sure the player is authenticated - if (!PlayerCache.getInstance().isAuthenticated(playerNameLowerCase)) { + if (!playerCache.isAuthenticated(playerNameLowerCase)) { commandService.send(player, MessageKey.NOT_LOGGED_IN); return; } diff --git a/src/test/java/fr/xephi/authme/command/executable/HelpCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/HelpCommandTest.java new file mode 100644 index 00000000..143ae26b --- /dev/null +++ b/src/test/java/fr/xephi/authme/command/executable/HelpCommandTest.java @@ -0,0 +1,158 @@ +package fr.xephi.authme.command.executable; + +import fr.xephi.authme.command.CommandDescription; +import fr.xephi.authme.command.CommandMapper; +import fr.xephi.authme.command.FoundCommandResult; +import fr.xephi.authme.command.help.HelpProvider; +import org.bukkit.ChatColor; +import org.bukkit.command.CommandSender; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +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.SUCCESS; +import static fr.xephi.authme.command.FoundResultStatus.UNKNOWN_LABEL; +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static org.hamcrest.CoreMatchers.containsString; +import static org.junit.Assert.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.argThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; + +/** + * Test for {@link HelpCommand}. + */ +@RunWith(MockitoJUnitRunner.class) +public class HelpCommandTest { + + @InjectMocks + private HelpCommand command; + + @Mock + private CommandMapper commandMapper; + + @Mock + private HelpProvider helpProvider; + + @Test + public void shouldHandleMissingBaseCommand() { + // given + List arguments = asList("some", "command"); + CommandSender sender = mock(CommandSender.class); + FoundCommandResult foundCommandResult = new FoundCommandResult(null, null, null, 0.0, MISSING_BASE_COMMAND); + given(commandMapper.mapPartsToCommand(sender, arguments)).willReturn(foundCommandResult); + + // when + command.executeCommand(sender, arguments); + + // then + verify(sender).sendMessage(argThat(containsString("Could not get base command"))); + verifyZeroInteractions(helpProvider); + } + + @Test + public void shouldHandleWrongCommandWithSuggestion() { + // given + List arguments = asList("authme", "ragister", "test"); + CommandSender sender = mock(CommandSender.class); + CommandDescription description = newCommandDescription("authme", "register"); + FoundCommandResult foundCommandResult = new FoundCommandResult(description, asList("authme", "ragister"), + singletonList("test"), 0.1, UNKNOWN_LABEL); + given(commandMapper.mapPartsToCommand(sender, arguments)).willReturn(foundCommandResult); + + // when + command.executeCommand(sender, arguments); + + // then + ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); + verify(sender).sendMessage(captor.capture()); + assertThat(removeColors(captor.getValue()), containsString("Assuming /authme register")); + verify(helpProvider).outputHelp(sender, foundCommandResult, HelpProvider.ALL_OPTIONS); + } + + @Test + public void shouldHandleWrongCommandWithoutSuggestion() { + List arguments = asList("authme", "ragister", "test"); + CommandSender sender = mock(CommandSender.class); + FoundCommandResult foundCommandResult = new FoundCommandResult(null, asList("authme", "ragister"), + singletonList("test"), 0.4, UNKNOWN_LABEL); + given(commandMapper.mapPartsToCommand(sender, arguments)).willReturn(foundCommandResult); + + // when + command.executeCommand(sender, arguments); + + // then + ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); + verify(sender).sendMessage(captor.capture()); + assertThat(removeColors(captor.getValue()), containsString("Unknown command")); + verifyZeroInteractions(helpProvider); + } + + @Test + public void shouldShowChildrenOfBaseCommand() { + List arguments = singletonList("authme"); + CommandSender sender = mock(CommandSender.class); + CommandDescription commandDescription = mock(CommandDescription.class); + given(commandDescription.getLabelCount()).willReturn(1); + FoundCommandResult foundCommandResult = new FoundCommandResult(commandDescription, singletonList("authme"), + Collections.emptyList(), 0.0, SUCCESS); + given(commandMapper.mapPartsToCommand(sender, arguments)).willReturn(foundCommandResult); + + // when + command.executeCommand(sender, arguments); + + // then + verify(sender, never()).sendMessage(anyString()); + verify(helpProvider).outputHelp(sender, foundCommandResult, HelpProvider.SHOW_CHILDREN); + } + + @Test + public void shouldShowDetailedHelpForChildCommand() { + List arguments = asList("authme", "getpos"); + CommandSender sender = mock(CommandSender.class); + CommandDescription commandDescription = mock(CommandDescription.class); + given(commandDescription.getLabelCount()).willReturn(2); + FoundCommandResult foundCommandResult = new FoundCommandResult(commandDescription, asList("authme", "getpos"), + Collections.emptyList(), 0.0, INCORRECT_ARGUMENTS); + given(commandMapper.mapPartsToCommand(sender, arguments)).willReturn(foundCommandResult); + + // when + command.executeCommand(sender, arguments); + + // then + verify(sender, never()).sendMessage(anyString()); + verify(helpProvider).outputHelp(sender, foundCommandResult, HelpProvider.ALL_OPTIONS); + } + + private static CommandDescription newCommandDescription(String... labels) { + CommandDescription parent = null; + // iterate through the labels backwards so we can set the parent + for (String label : labels) { + CommandDescription description = mock(CommandDescription.class); + given(description.getParent()).willReturn(parent); + given(description.getLabels()).willReturn(singletonList(label)); + parent = description; + } + return parent; + } + + private static String removeColors(String str) { + for (ChatColor color : ChatColor.values()) { + str = str.replace(color.toString(), ""); + } + return str; + } +} diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/ConverterCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/ConverterCommandTest.java new file mode 100644 index 00000000..5f781752 --- /dev/null +++ b/src/test/java/fr/xephi/authme/command/executable/authme/ConverterCommandTest.java @@ -0,0 +1,104 @@ +package fr.xephi.authme.command.executable.authme; + +import fr.xephi.authme.TestHelper; +import fr.xephi.authme.command.CommandService; +import fr.xephi.authme.converter.RakamakConverter; +import fr.xephi.authme.initialization.AuthMeServiceInitializer; +import fr.xephi.authme.output.MessageKey; +import fr.xephi.authme.util.BukkitService; +import org.bukkit.command.CommandSender; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.nullValue; +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.verifyNoMoreInteractions; +import static org.mockito.Mockito.verifyZeroInteractions; + +/** + * Test for {@link ConverterCommand}. + */ +@RunWith(MockitoJUnitRunner.class) +public class ConverterCommandTest { + + @InjectMocks + private ConverterCommand command; + + @Mock + private CommandService commandService; + + @Mock + private BukkitService bukkitService; + + @Mock + private AuthMeServiceInitializer initializer; + + @Test + public void shouldHandleUnknownConversionType() { + // given + CommandSender sender = mock(CommandSender.class); + + // when + command.executeCommand(sender, Collections.singletonList("invalid")); + + // then + verify(commandService).send(sender, MessageKey.ERROR); + verifyNoMoreInteractions(commandService); + verifyZeroInteractions(initializer); + verifyZeroInteractions(bukkitService); + } + + @Test + public void shouldHaveUniqueNameAndClassForEachType() { + // given + ConverterCommand.ConvertType[] types = ConverterCommand.ConvertType.values(); + List names = new ArrayList<>(types.length); + List> classes = new ArrayList<>(types.length); + + // when / then + for (ConverterCommand.ConvertType type : types) { + assertThat("Name for '" + type + "' is not null", + type.getName(), not(nullValue())); + assertThat("Class for '" + type + "' is not null", + type.getConverterClass(), not(nullValue())); + assertThat("Name '" + type.getName() + "' is unique", + names, not(hasItem(type.getName()))); + assertThat("Class '" + type.getConverterClass() + "' is unique", + classes, not(hasItem(type.getConverterClass()))); + names.add(type.getName()); + classes.add(type.getConverterClass()); + } + } + + @Test + public void shouldLaunchConverterForAllTypes() { + // given + ConverterCommand.ConvertType type = ConverterCommand.ConvertType.RAKAMAK; + RakamakConverter converter = mock(RakamakConverter.class); + given(initializer.newInstance(RakamakConverter.class)).willReturn(converter); + CommandSender sender = mock(CommandSender.class); + + // when + command.executeCommand(sender, Collections.singletonList(type.getName())); + TestHelper.runInnerRunnable(bukkitService); + + // then + verify(converter).execute(sender); + verifyNoMoreInteractions(converter); + verify(initializer).newInstance(type.getConverterClass()); + verifyNoMoreInteractions(initializer); + } + +} diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/SetEmailCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/SetEmailCommandTest.java new file mode 100644 index 00000000..26d11844 --- /dev/null +++ b/src/test/java/fr/xephi/authme/command/executable/authme/SetEmailCommandTest.java @@ -0,0 +1,188 @@ +package fr.xephi.authme.command.executable.authme; + +import fr.xephi.authme.cache.auth.PlayerAuth; +import fr.xephi.authme.cache.auth.PlayerCache; +import fr.xephi.authme.command.CommandService; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.output.MessageKey; +import fr.xephi.authme.util.BukkitService; +import org.bukkit.command.CommandSender; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import java.util.Arrays; + +import static fr.xephi.authme.TestHelper.runInnerRunnable; +import static org.mockito.BDDMockito.given; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.verifyZeroInteractions; + +/** + * Test for {@link SetEmailCommand}. + */ +@RunWith(MockitoJUnitRunner.class) +public class SetEmailCommandTest { + + @InjectMocks + private SetEmailCommand command; + + @Mock + private DataSource dataSource; + + @Mock + private CommandService commandService; + + @Mock + private PlayerCache playerCache; + + @Mock + private BukkitService bukkitService; + + @Test + public void shouldRejectInvalidMail() { + // given + String user = "somebody"; + String email = "some.test@example.org"; + given(commandService.validateEmail(email)).willReturn(false); + CommandSender sender = mock(CommandSender.class); + + // when + command.executeCommand(sender, Arrays.asList(user, email)); + + // then + verify(commandService).validateEmail(email); + verify(commandService).send(sender, MessageKey.INVALID_EMAIL); + verifyZeroInteractions(dataSource); + } + + @Test + public void shouldHandleUnknownUser() { + // given + String user = "nonexistent"; + String email = "mail@example.com"; + given(commandService.validateEmail(email)).willReturn(true); + given(dataSource.getAuth(user)).willReturn(null); + CommandSender sender = mock(CommandSender.class); + + // when + command.executeCommand(sender, Arrays.asList(user, email)); + runInnerRunnable(bukkitService); + + // then + verify(commandService).validateEmail(email); + verify(dataSource).getAuth(user); + verify(commandService).send(sender, MessageKey.UNKNOWN_USER); + verifyNoMoreInteractions(dataSource); + } + + @Test + public void shouldHandleAlreadyTakenEmail() { + // given + String user = "someone"; + String email = "mail@example.com"; + given(commandService.validateEmail(email)).willReturn(true); + PlayerAuth auth = mock(PlayerAuth.class); + given(dataSource.getAuth(user)).willReturn(auth); + CommandSender sender = mock(CommandSender.class); + given(commandService.isEmailFreeForRegistration(email, sender)).willReturn(false); + + // when + command.executeCommand(sender, Arrays.asList(user, email)); + runInnerRunnable(bukkitService); + + // then + verify(commandService).validateEmail(email); + verify(dataSource).getAuth(user); + verify(commandService).isEmailFreeForRegistration(email, sender); + verify(commandService).send(sender, MessageKey.EMAIL_ALREADY_USED_ERROR); + verifyNoMoreInteractions(dataSource); + verifyZeroInteractions(auth); + } + + @Test + public void shouldHandlePersistenceError() { + // given + String user = "Bobby"; + String email = "new-addr@example.org"; + given(commandService.validateEmail(email)).willReturn(true); + PlayerAuth auth = mock(PlayerAuth.class); + given(dataSource.getAuth(user)).willReturn(auth); + CommandSender sender = mock(CommandSender.class); + given(commandService.isEmailFreeForRegistration(email, sender)).willReturn(true); + given(dataSource.updateEmail(auth)).willReturn(false); + + // when + command.executeCommand(sender, Arrays.asList(user, email)); + runInnerRunnable(bukkitService); + + // then + verify(commandService).validateEmail(email); + verify(dataSource).getAuth(user); + verify(commandService).isEmailFreeForRegistration(email, sender); + verify(commandService).send(sender, MessageKey.ERROR); + verify(dataSource).updateEmail(auth); + verifyNoMoreInteractions(dataSource); + } + + @Test + public void shouldUpdateEmail() { + // given + String user = "Bobby"; + String email = "new-addr@example.org"; + given(commandService.validateEmail(email)).willReturn(true); + PlayerAuth auth = mock(PlayerAuth.class); + given(dataSource.getAuth(user)).willReturn(auth); + CommandSender sender = mock(CommandSender.class); + given(commandService.isEmailFreeForRegistration(email, sender)).willReturn(true); + given(dataSource.updateEmail(auth)).willReturn(true); + given(playerCache.getAuth(user)).willReturn(null); + + // when + command.executeCommand(sender, Arrays.asList(user, email)); + runInnerRunnable(bukkitService); + + // then + verify(commandService).validateEmail(email); + verify(dataSource).getAuth(user); + verify(commandService).isEmailFreeForRegistration(email, sender); + verify(commandService).send(sender, MessageKey.EMAIL_CHANGED_SUCCESS); + verify(dataSource).updateEmail(auth); + verify(playerCache, never()).updatePlayer(any(PlayerAuth.class)); + verifyNoMoreInteractions(dataSource); + } + + @Test + public void shouldUpdateEmailAndPlayerCache() { + // given + String user = "Bobby"; + String email = "new-addr@example.org"; + given(commandService.validateEmail(email)).willReturn(true); + PlayerAuth auth = mock(PlayerAuth.class); + given(dataSource.getAuth(user)).willReturn(auth); + CommandSender sender = mock(CommandSender.class); + given(commandService.isEmailFreeForRegistration(email, sender)).willReturn(true); + given(dataSource.updateEmail(auth)).willReturn(true); + given(playerCache.getAuth(user)).willReturn(mock(PlayerAuth.class)); + + // when + command.executeCommand(sender, Arrays.asList(user, email)); + runInnerRunnable(bukkitService); + + // then + verify(commandService).validateEmail(email); + verify(dataSource).getAuth(user); + verify(commandService).isEmailFreeForRegistration(email, sender); + verify(commandService).send(sender, MessageKey.EMAIL_CHANGED_SUCCESS); + verify(dataSource).updateEmail(auth); + verify(playerCache).updatePlayer(auth); + verifyNoMoreInteractions(dataSource); + } + +} diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/UnregisterAdminCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/UnregisterAdminCommandTest.java new file mode 100644 index 00000000..f41eeb20 --- /dev/null +++ b/src/test/java/fr/xephi/authme/command/executable/authme/UnregisterAdminCommandTest.java @@ -0,0 +1,71 @@ +package fr.xephi.authme.command.executable.authme; + +import fr.xephi.authme.command.CommandService; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.output.MessageKey; +import org.bukkit.command.CommandSender; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import java.util.Collections; + +import static org.hamcrest.Matchers.equalToIgnoringCase; +import static org.mockito.BDDMockito.given; +import static org.mockito.Matchers.argThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +/** + * Test for {@link UnregisterAdminCommand}. + */ +@RunWith(MockitoJUnitRunner.class) +public class UnregisterAdminCommandTest { + + @InjectMocks + private UnregisterAdminCommand command; + + @Mock + private DataSource dataSource; + + @Mock + private CommandService commandService; + + @Test + public void shouldHandleUnknownPlayer() { + // given + String user = "bobby"; + given(dataSource.isAuthAvailable(user)).willReturn(false); + CommandSender sender = mock(CommandSender.class); + + // when + command.executeCommand(sender, Collections.singletonList(user)); + + // then + verify(dataSource).isAuthAvailable(user); + verifyNoMoreInteractions(dataSource); + verify(commandService).send(sender, MessageKey.UNKNOWN_USER); + } + + @Test + public void shouldHandleDatabaseError() { + // given + String user = "personaNonGrata"; + given(dataSource.isAuthAvailable(argThat(equalToIgnoringCase(user)))).willReturn(true); + given(dataSource.removeAuth(argThat(equalToIgnoringCase(user)))).willReturn(false); + CommandSender sender = mock(CommandSender.class); + + // when + command.executeCommand(sender, Collections.singletonList(user)); + + // then + verify(dataSource).isAuthAvailable(argThat(equalToIgnoringCase(user))); + verify(dataSource).removeAuth(argThat(equalToIgnoringCase(user))); + verifyNoMoreInteractions(dataSource); + verify(commandService).send(sender, MessageKey.ERROR); + } + +} diff --git a/src/test/java/fr/xephi/authme/command/executable/unregister/UnregisterCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/unregister/UnregisterCommandTest.java new file mode 100644 index 00000000..976649ae --- /dev/null +++ b/src/test/java/fr/xephi/authme/command/executable/unregister/UnregisterCommandTest.java @@ -0,0 +1,74 @@ +package fr.xephi.authme.command.executable.unregister; + +import fr.xephi.authme.cache.auth.PlayerCache; +import fr.xephi.authme.command.CommandService; +import fr.xephi.authme.output.MessageKey; +import fr.xephi.authme.process.Management; +import org.bukkit.entity.Player; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import java.util.Collections; + +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; + +/** + * Test for {@link UnregisterCommand}. + */ +@RunWith(MockitoJUnitRunner.class) +public class UnregisterCommandTest { + + @InjectMocks + private UnregisterCommand command; + + @Mock + private Management management; + + @Mock + private CommandService commandService; + + @Mock + private PlayerCache playerCache; + + @Test + public void shouldCatchUnauthenticatedUser() { + // given + String password = "mySecret123"; + String name = "player77"; + Player player = mock(Player.class); + given(player.getName()).willReturn(name); + given(playerCache.isAuthenticated(name)).willReturn(false); + + // when + command.executeCommand(player, Collections.singletonList(password)); + + // then + verify(playerCache).isAuthenticated(name); + verify(commandService).send(player, MessageKey.NOT_LOGGED_IN); + verifyZeroInteractions(management); + } + + @Test + public void shouldForwardDataToAsyncTask() { + // given + String password = "p@ssw0rD"; + String name = "jas0n_"; + Player player = mock(Player.class); + given(player.getName()).willReturn(name); + given(playerCache.isAuthenticated(name)).willReturn(true); + + // when + command.executeCommand(player, Collections.singletonList(password)); + + // then + verify(playerCache).isAuthenticated(name); + verify(management).performUnregister(player, password, false); + } + +} From 01f297919d888efee598f27dec3b25fc4e53c0d7 Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Sat, 11 Jun 2016 17:46:24 +0200 Subject: [PATCH 167/200] 1.10 API + cleanup --- pom.xml | 2 +- .../authme/permission/PermissionsManager.java | 9 ++- .../authme/process/join/AsynchronousJoin.java | 67 +++++++++++-------- .../command/CommandConsistencyTest.java | 1 - .../authme/command/CommandHandlerTest.java | 2 +- .../command/CommandInitializerTest.java | 1 - .../authme/command/CommandMapperTest.java | 1 + .../authme/command/PlayerCommandTest.java | 1 + .../executable/authme/ReloadCommandTest.java | 2 + 9 files changed, 52 insertions(+), 34 deletions(-) diff --git a/pom.xml b/pom.xml index ac79d331..a11bc97e 100644 --- a/pom.xml +++ b/pom.xml @@ -66,7 +66,7 @@ Xephi, sgdc3, DNx5, timvisee, games647, ljacqu - 1.9.4-R0.1-SNAPSHOT + 1.10-R0.1-SNAPSHOT diff --git a/src/main/java/fr/xephi/authme/permission/PermissionsManager.java b/src/main/java/fr/xephi/authme/permission/PermissionsManager.java index 4e1c4253..c4efc757 100644 --- a/src/main/java/fr/xephi/authme/permission/PermissionsManager.java +++ b/src/main/java/fr/xephi/authme/permission/PermissionsManager.java @@ -313,9 +313,14 @@ public class PermissionsManager { * False is also returned if this feature isn't supported for the current permissions system. */ public boolean addGroup(Player player, String groupName) { - // If no permissions system is used, return false - if (!isEnabled()) + if(groupName.isEmpty()) { return false; + } + + // If no permissions system is used, return false + if (!isEnabled()) { + return false; + } return handler.addToGroup(player, groupName); } diff --git a/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java b/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java index 59892c58..ee19a02d 100644 --- a/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java +++ b/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java @@ -83,7 +83,7 @@ public class AsynchronousJoin implements AsynchronousProcess { // Prevent player collisions in 1.9 if (DISABLE_COLLISIONS) { - ((LivingEntity) player).setCollidable(false); + player.setCollidable(false); } if (service.getProperty(RestrictionSettings.FORCE_SURVIVAL_MODE) @@ -133,6 +133,12 @@ public class AsynchronousJoin implements AsynchronousProcess { // TODO: continue cleanup from this -sgdc3 if (isAuthAvailable) { + // Registered + + // Groups logic + Utils.setGroup(player, GroupType.NOTLOGGEDIN); + + // Spawn logic if (!service.getProperty(RestrictionSettings.NO_TELEPORT)) { if (Settings.isTeleportToSpawnEnabled || (Settings.isForceSpawnLocOnJoinEnabled && Settings.getForcedWorlds.contains(player.getWorld().getName()))) { bukkitService.scheduleSyncDelayedTask(new Runnable() { @@ -149,9 +155,11 @@ public class AsynchronousJoin implements AsynchronousProcess { } } placePlayerSafely(player, spawnLoc); + + // Limbo cache limboCache.updateLimboPlayer(player); - // protect inventory + // Protect inventory if (service.getProperty(PROTECT_INVENTORY_BEFORE_LOGIN) && plugin.inventoryProtector != null) { ProtectInventoryEvent ev = new ProtectInventoryEvent(player); plugin.getServer().getPluginManager().callEvent(ev); @@ -163,6 +171,7 @@ public class AsynchronousJoin implements AsynchronousProcess { } } + // Session logic if (service.getProperty(PluginSettings.SESSIONS_ENABLED) && (playerCache.isAuthenticated(name) || database.isLogged(name))) { if (plugin.sessions.containsKey(name)) { plugin.sessions.get(name).cancel(); @@ -180,13 +189,17 @@ public class AsynchronousJoin implements AsynchronousProcess { } } } else { - if (!Settings.unRegisteredGroup.isEmpty()) { - Utils.setGroup(player, Utils.GroupType.UNREGISTERED); - } + // Not Registered + + // Groups logic + Utils.setGroup(player, GroupType.UNREGISTERED); + + // Skip if registration is optional if (!service.getProperty(RegistrationSettings.FORCE)) { return; } + // Spawn logic if (!Settings.noTeleport && !needFirstSpawn(player) && Settings.isTeleportToSpawnEnabled || (Settings.isForceSpawnLocOnJoinEnabled && Settings.getForcedWorlds.contains(player.getWorld().getName()))) { bukkitService.scheduleSyncDelayedTask(new Runnable() { @@ -202,37 +215,33 @@ public class AsynchronousJoin implements AsynchronousProcess { }); } } + // The user is not logged in if (!limboCache.hasLimboPlayer(name)) { limboCache.addLimboPlayer(player); } - Utils.setGroup(player, isAuthAvailable ? GroupType.NOTLOGGEDIN : GroupType.UNREGISTERED); final int registrationTimeout = service.getProperty(RestrictionSettings.TIMEOUT) * 20; - bukkitService.scheduleSyncDelayedTask(new Runnable() { - @Override - public void run() { - player.setOp(false); - if (!service.getProperty(RestrictionSettings.ALLOW_UNAUTHED_MOVEMENT) - && service.getProperty(RestrictionSettings.REMOVE_SPEED)) { - player.setFlySpeed(0.0f); - player.setWalkSpeed(0.0f); - } - player.setNoDamageTicks(registrationTimeout); - if (pluginHooks.isEssentialsAvailable() && service.getProperty(HooksSettings.USE_ESSENTIALS_MOTD)) { - player.performCommand("motd"); - } - if (service.getProperty(RegistrationSettings.APPLY_BLIND_EFFECT)) { - // Allow infinite blindness effect - int blindTimeOut = (registrationTimeout <= 0) ? 99999 : registrationTimeout; - player.addPotionEffect(new PotionEffect(PotionEffectType.BLINDNESS, blindTimeOut, 2)); - } - } + // Apply effects + // TODO: clenup! + player.setOp(false); + if (!service.getProperty(RestrictionSettings.ALLOW_UNAUTHED_MOVEMENT) + && service.getProperty(RestrictionSettings.REMOVE_SPEED)) { + player.setFlySpeed(0.0f); + player.setWalkSpeed(0.0f); + } + player.setNoDamageTicks(registrationTimeout); + if (pluginHooks.isEssentialsAvailable() && service.getProperty(HooksSettings.USE_ESSENTIALS_MOTD)) { + player.performCommand("motd"); + } + if (service.getProperty(RegistrationSettings.APPLY_BLIND_EFFECT)) { + // Allow infinite blindness effect + int blindTimeOut = (registrationTimeout <= 0) ? 99999 : registrationTimeout; + player.addPotionEffect(new PotionEffect(PotionEffectType.BLINDNESS, blindTimeOut, 2)); + } - }); - - int msgInterval = service.getProperty(RegistrationSettings.MESSAGE_INTERVAL); + // Timeout task if (registrationTimeout > 0) { BukkitTask id = bukkitService.runTaskLater(new TimeoutTask(plugin, name, player), registrationTimeout); LimboPlayer limboPlayer = limboCache.getLimboPlayer(name); @@ -241,6 +250,8 @@ public class AsynchronousJoin implements AsynchronousProcess { } } + // Message task + int msgInterval = service.getProperty(RegistrationSettings.MESSAGE_INTERVAL); MessageKey msg; if (isAuthAvailable) { msg = MessageKey.LOGIN_MESSAGE; diff --git a/src/test/java/fr/xephi/authme/command/CommandConsistencyTest.java b/src/test/java/fr/xephi/authme/command/CommandConsistencyTest.java index 287b398a..d2033d1b 100644 --- a/src/test/java/fr/xephi/authme/command/CommandConsistencyTest.java +++ b/src/test/java/fr/xephi/authme/command/CommandConsistencyTest.java @@ -50,7 +50,6 @@ public class CommandConsistencyTest { * * @return collection of all base command labels */ - @SuppressWarnings("unchecked") private static Collection> initializeCommands() { CommandInitializer initializer = new CommandInitializer(); Collection> commandLabels = new ArrayList<>(); diff --git a/src/test/java/fr/xephi/authme/command/CommandHandlerTest.java b/src/test/java/fr/xephi/authme/command/CommandHandlerTest.java index ff3fd5a7..d831c85a 100644 --- a/src/test/java/fr/xephi/authme/command/CommandHandlerTest.java +++ b/src/test/java/fr/xephi/authme/command/CommandHandlerTest.java @@ -81,13 +81,13 @@ public class CommandHandlerTest { *

* The {@link CommandMapper} is mocked in {@link #initializeCommandMapper()} to return certain test classes. */ + @SuppressWarnings("unchecked") private void setInjectorToMockExecutableCommandClasses() { given(initializer.newInstance(any(Class.class))).willAnswer(new Answer() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { Class clazz = (Class) invocation.getArguments()[0]; if (ExecutableCommand.class.isAssignableFrom(clazz)) { - @SuppressWarnings("unchecked") Class commandClass = (Class) clazz; ExecutableCommand mock = mock(commandClass); mockedCommands.put(commandClass, mock); diff --git a/src/test/java/fr/xephi/authme/command/CommandInitializerTest.java b/src/test/java/fr/xephi/authme/command/CommandInitializerTest.java index 97268e49..669f71d8 100644 --- a/src/test/java/fr/xephi/authme/command/CommandInitializerTest.java +++ b/src/test/java/fr/xephi/authme/command/CommandInitializerTest.java @@ -33,7 +33,6 @@ public class CommandInitializerTest { private static Set commands; - @SuppressWarnings("unchecked") @BeforeClass public static void initializeCommandCollection() { CommandInitializer commandInitializer = new CommandInitializer(); diff --git a/src/test/java/fr/xephi/authme/command/CommandMapperTest.java b/src/test/java/fr/xephi/authme/command/CommandMapperTest.java index 26e43ab0..28ee861f 100644 --- a/src/test/java/fr/xephi/authme/command/CommandMapperTest.java +++ b/src/test/java/fr/xephi/authme/command/CommandMapperTest.java @@ -280,6 +280,7 @@ public class CommandMapperTest { assertThat(result.getArguments(), contains(parts.get(2))); } + @SuppressWarnings("unchecked") @Test public void shouldReturnExecutableCommandClasses() { // given / when diff --git a/src/test/java/fr/xephi/authme/command/PlayerCommandTest.java b/src/test/java/fr/xephi/authme/command/PlayerCommandTest.java index f7641215..bc9e1929 100644 --- a/src/test/java/fr/xephi/authme/command/PlayerCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/PlayerCommandTest.java @@ -38,6 +38,7 @@ public class PlayerCommandTest { // given Player player = mock(Player.class); List arguments = Arrays.asList("arg1", "testarg2"); + @SuppressWarnings("unused") CommandService service = mock(CommandService.class); PlayerCommandImpl command = new PlayerCommandImpl(); diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/ReloadCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/ReloadCommandTest.java index 2371a500..48ed3f6f 100644 --- a/src/test/java/fr/xephi/authme/command/executable/authme/ReloadCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/authme/ReloadCommandTest.java @@ -85,6 +85,7 @@ public class ReloadCommandTest { public void shouldHandleReloadError() { // given CommandSender sender = mock(CommandSender.class); + @SuppressWarnings("unused") CommandService service = mock(CommandService.class); doThrow(IllegalStateException.class).when(initializer).performReloadOnServices(); given(settings.getProperty(DatabaseSettings.BACKEND)).willReturn(DataSourceType.MYSQL); @@ -104,6 +105,7 @@ public class ReloadCommandTest { public void shouldIssueWarningForChangedDatasourceSetting() { // given CommandSender sender = mock(CommandSender.class); + @SuppressWarnings("unused") CommandService service = mock(CommandService.class); given(settings.getProperty(DatabaseSettings.BACKEND)).willReturn(DataSourceType.MYSQL); given(dataSource.getType()).willReturn(DataSourceType.SQLITE); From 1de086c09036bcf5b63722269a166423963c7556 Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Sat, 11 Jun 2016 18:05:11 +0200 Subject: [PATCH 168/200] Fix 755 --- .../xephi/authme/hooks/BungeeCordMessage.java | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/main/java/fr/xephi/authme/hooks/BungeeCordMessage.java b/src/main/java/fr/xephi/authme/hooks/BungeeCordMessage.java index 22a7ae08..47e7d17d 100644 --- a/src/main/java/fr/xephi/authme/hooks/BungeeCordMessage.java +++ b/src/main/java/fr/xephi/authme/hooks/BungeeCordMessage.java @@ -9,6 +9,7 @@ import fr.xephi.authme.cache.auth.PlayerAuth; import fr.xephi.authme.cache.auth.PlayerCache; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.security.crypts.HashedPassword; +import fr.xephi.authme.settings.properties.SecuritySettings; import fr.xephi.authme.util.BukkitService; import org.bukkit.entity.Player; import org.bukkit.plugin.messaging.PluginMessageListener; @@ -61,16 +62,20 @@ public class BungeeCordMessage implements PluginMessageListener { plugin.sessions.remove(name); } //END - ConsoleLogger.info("Player " + auth.getNickname() - + " has logged in from one of your server!"); + + if (!plugin.getSettings().getProperty(SecuritySettings.REMOVE_SPAM_FROM_CONSOLE)) { + ConsoleLogger.info("Player " + auth.getNickname() + " has logged in from one of your server!"); + } } else if ("logout".equals(act)) { playerCache.removePlayer(name); dataSource.setUnlogged(name); - ConsoleLogger.info("Player " + auth.getNickname() - + " has logged out from one of your server!"); + if (!plugin.getSettings().getProperty(SecuritySettings.REMOVE_SPAM_FROM_CONSOLE)) { + ConsoleLogger.info("Player " + auth.getNickname() + " has logged out from one of your server!"); + } } else if ("register".equals(act)) { - ConsoleLogger.info("Player " + auth.getNickname() - + " has registered out from one of your server!"); + if (!plugin.getSettings().getProperty(SecuritySettings.REMOVE_SPAM_FROM_CONSOLE)) { + ConsoleLogger.info("Player " + auth.getNickname() + " has registered out from one of your server!"); + } } else if ("changepassword".equals(act)) { final String password = args[2]; final String salt = args.length >= 4 ? args[3] : null; From 4d75542594d613b97c4c5370802a53dfc106c37f Mon Sep 17 00:00:00 2001 From: ljacqu Date: Sat, 11 Jun 2016 19:05:10 +0200 Subject: [PATCH 169/200] Remove unused fields instead of suppressing warnings --- src/test/java/fr/xephi/authme/command/PlayerCommandTest.java | 2 -- .../authme/command/executable/authme/ReloadCommandTest.java | 4 ---- 2 files changed, 6 deletions(-) diff --git a/src/test/java/fr/xephi/authme/command/PlayerCommandTest.java b/src/test/java/fr/xephi/authme/command/PlayerCommandTest.java index bc9e1929..815d8a21 100644 --- a/src/test/java/fr/xephi/authme/command/PlayerCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/PlayerCommandTest.java @@ -38,8 +38,6 @@ public class PlayerCommandTest { // given Player player = mock(Player.class); List arguments = Arrays.asList("arg1", "testarg2"); - @SuppressWarnings("unused") - CommandService service = mock(CommandService.class); PlayerCommandImpl command = new PlayerCommandImpl(); // when diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/ReloadCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/ReloadCommandTest.java index 48ed3f6f..676c3b22 100644 --- a/src/test/java/fr/xephi/authme/command/executable/authme/ReloadCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/authme/ReloadCommandTest.java @@ -85,8 +85,6 @@ public class ReloadCommandTest { public void shouldHandleReloadError() { // given CommandSender sender = mock(CommandSender.class); - @SuppressWarnings("unused") - CommandService service = mock(CommandService.class); doThrow(IllegalStateException.class).when(initializer).performReloadOnServices(); given(settings.getProperty(DatabaseSettings.BACKEND)).willReturn(DataSourceType.MYSQL); given(dataSource.getType()).willReturn(DataSourceType.MYSQL); @@ -105,8 +103,6 @@ public class ReloadCommandTest { public void shouldIssueWarningForChangedDatasourceSetting() { // given CommandSender sender = mock(CommandSender.class); - @SuppressWarnings("unused") - CommandService service = mock(CommandService.class); given(settings.getProperty(DatabaseSettings.BACKEND)).willReturn(DataSourceType.MYSQL); given(dataSource.getType()).willReturn(DataSourceType.SQLITE); From 26cb7464e18552188bfea7aff505fd452cda2b08 Mon Sep 17 00:00:00 2001 From: Gnat008 Date: Sat, 11 Jun 2016 20:49:15 -0400 Subject: [PATCH 170/200] Re-add a scheduled task --- .../authme/process/join/AsynchronousJoin.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java b/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java index ee19a02d..19a1ba63 100644 --- a/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java +++ b/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java @@ -223,6 +223,28 @@ public class AsynchronousJoin implements AsynchronousProcess { final int registrationTimeout = service.getProperty(RestrictionSettings.TIMEOUT) * 20; + bukkitService.scheduleSyncDelayedTask(new Runnable() { + @Override + public void run() { + player.setOp(false); + if (!service.getProperty(RestrictionSettings.ALLOW_UNAUTHED_MOVEMENT) + && service.getProperty(RestrictionSettings.REMOVE_SPEED)) { + player.setFlySpeed(0.0f); + player.setWalkSpeed(0.0f); + } + player.setNoDamageTicks(registrationTimeout); + if (pluginHooks.isEssentialsAvailable() && service.getProperty(HooksSettings.USE_ESSENTIALS_MOTD)) { + player.performCommand("motd"); + } + if (service.getProperty(RegistrationSettings.APPLY_BLIND_EFFECT)) { + // Allow infinite blindness effect + int blindTimeOut = (registrationTimeout <= 0) ? 99999 : registrationTimeout; + player.addPotionEffect(new PotionEffect(PotionEffectType.BLINDNESS, blindTimeOut, 2)); + } + } + + }); + // Apply effects // TODO: clenup! player.setOp(false); From 0cc5dd2cd5d2dd4976cd15354a5211a89ccca663 Mon Sep 17 00:00:00 2001 From: Gnat008 Date: Sat, 11 Jun 2016 20:54:50 -0400 Subject: [PATCH 171/200] Remove code causing issues --- .../authme/process/join/AsynchronousJoin.java | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java b/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java index 19a1ba63..fcbfc0bd 100644 --- a/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java +++ b/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java @@ -245,24 +245,6 @@ public class AsynchronousJoin implements AsynchronousProcess { }); - // Apply effects - // TODO: clenup! - player.setOp(false); - if (!service.getProperty(RestrictionSettings.ALLOW_UNAUTHED_MOVEMENT) - && service.getProperty(RestrictionSettings.REMOVE_SPEED)) { - player.setFlySpeed(0.0f); - player.setWalkSpeed(0.0f); - } - player.setNoDamageTicks(registrationTimeout); - if (pluginHooks.isEssentialsAvailable() && service.getProperty(HooksSettings.USE_ESSENTIALS_MOTD)) { - player.performCommand("motd"); - } - if (service.getProperty(RegistrationSettings.APPLY_BLIND_EFFECT)) { - // Allow infinite blindness effect - int blindTimeOut = (registrationTimeout <= 0) ? 99999 : registrationTimeout; - player.addPotionEffect(new PotionEffect(PotionEffectType.BLINDNESS, blindTimeOut, 2)); - } - // Timeout task if (registrationTimeout > 0) { BukkitTask id = bukkitService.runTaskLater(new TimeoutTask(plugin, name, player), registrationTimeout); From f0e42b61c5c1d696d9506fc2dc2ceb9d87f6ab54 Mon Sep 17 00:00:00 2001 From: Gnat008 Date: Sat, 11 Jun 2016 21:23:53 -0400 Subject: [PATCH 172/200] #747 - display captcha immediately after the failed login attempt --- .../process/login/AsynchronousLogin.java | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java b/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java index 4116ffd0..52dd4a6e 100644 --- a/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java +++ b/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java @@ -71,18 +71,17 @@ public class AsynchronousLogin implements AsynchronousProcess { AsynchronousLogin() { } - + /** + * Queries the {@link fr.xephi.authme.cache.CaptchaManager} to + * see if a captcha needs to be entered in order to log in. + * + * @param player The player to check + * @return True if a captcha needs to be entered + */ private boolean needsCaptcha(Player player) { final String playerName = player.getName(); - if (captchaManager.isCaptchaRequired(playerName)) { - service.send(player, MessageKey.USAGE_CAPTCHA, captchaManager.getCaptchaCodeOrGenerateNew(playerName)); - return true; - } else { - // Increase the count here before knowing the result of the login. - // If login is successful, we clear the count for the player - captchaManager.increaseCount(playerName); - } - return false; + + return captchaManager.isCaptchaRequired(playerName); } /** @@ -144,7 +143,13 @@ public class AsynchronousLogin implements AsynchronousProcess { return; } + final String name = player.getName().toLowerCase(); final String ip = Utils.getPlayerIp(player); + + // Increase the count here before knowing the result of the login. + // If the login is successful, we clear the count for the player. + captchaManager.increaseCount(name); + if ("127.0.0.1".equals(pAuth.getIp()) && !pAuth.getIp().equals(ip)) { pAuth.setIp(ip); database.updateIp(pAuth.getNickname(), ip); @@ -153,8 +158,6 @@ public class AsynchronousLogin implements AsynchronousProcess { String email = pAuth.getEmail(); boolean passwordVerified = forceLogin || passwordSecurity.comparePassword( password, pAuth.getPassword(), player.getName()); - - final String name = player.getName().toLowerCase(); if (passwordVerified && player.isOnline()) { PlayerAuth auth = PlayerAuth.builder() .name(name) @@ -213,6 +216,11 @@ public class AsynchronousLogin implements AsynchronousProcess { }); } else { service.send(player, MessageKey.WRONG_PASSWORD); + + // Check again if a captcha is required to log in + if (needsCaptcha(player)) { + service.send(player, MessageKey.USAGE_CAPTCHA, captchaManager.getCaptchaCodeOrGenerateNew(name)); + } } } else { ConsoleLogger.showError("Player " + name + " wasn't online during login process, aborted... "); From 68d5145cd78132b1c761ce723d50ed1b8478bb50 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Sun, 12 Jun 2016 12:46:03 +0200 Subject: [PATCH 173/200] #729 Refactor spawn handling into separate service (work in progress) --- .../authme/events/SpawnTeleportEvent.java | 1 + .../xephi/authme/process/ProcessService.java | 14 - .../authme/process/join/AsynchronousJoin.java | 166 ++----- .../process/login/ProcessSyncPlayerLogin.java | 51 +-- .../register/ProcessSyncPasswordRegister.java | 2 +- .../fr/xephi/authme/settings/Settings.java | 10 +- .../fr/xephi/authme/util/BukkitService.java | 25 +- .../authme/util/TeleportationService.java | 144 ++++++ src/main/java/fr/xephi/authme/util/Utils.java | 30 +- src/test/java/fr/xephi/authme/TestHelper.java | 14 + .../authme/process/ProcessServiceTest.java | 17 - .../authme/util/TeleportationServiceTest.java | 421 ++++++++++++++++++ 12 files changed, 663 insertions(+), 232 deletions(-) create mode 100644 src/main/java/fr/xephi/authme/util/TeleportationService.java create mode 100644 src/test/java/fr/xephi/authme/util/TeleportationServiceTest.java diff --git a/src/main/java/fr/xephi/authme/events/SpawnTeleportEvent.java b/src/main/java/fr/xephi/authme/events/SpawnTeleportEvent.java index 3a429bc3..28f61111 100644 --- a/src/main/java/fr/xephi/authme/events/SpawnTeleportEvent.java +++ b/src/main/java/fr/xephi/authme/events/SpawnTeleportEvent.java @@ -21,6 +21,7 @@ public class SpawnTeleportEvent extends AbstractTeleportEvent { * @param to The teleport destination * @param isAuthenticated Whether or not the player is logged in */ + // TODO ljacqu 20160611: We only ever call this with from = player.getLocation() -> could be done in constructor public SpawnTeleportEvent(Player player, Location from, Location to, boolean isAuthenticated) { super(false, player, from, to); this.isAuthenticated = isAuthenticated; diff --git a/src/main/java/fr/xephi/authme/process/ProcessService.java b/src/main/java/fr/xephi/authme/process/ProcessService.java index f171e368..775344d7 100644 --- a/src/main/java/fr/xephi/authme/process/ProcessService.java +++ b/src/main/java/fr/xephi/authme/process/ProcessService.java @@ -9,8 +9,6 @@ import fr.xephi.authme.settings.domain.Property; import fr.xephi.authme.util.ValidationService; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; -import org.bukkit.event.Event; -import org.bukkit.plugin.PluginManager; import javax.inject.Inject; @@ -25,9 +23,6 @@ public class ProcessService { @Inject private Messages messages; - @Inject - private PluginManager pluginManager; - @Inject private ValidationService validationService; @@ -95,15 +90,6 @@ public class ProcessService { return messages.retrieveSingle(key); } - /** - * Emit an event. - * - * @param event the event to emit - */ - public void callEvent(Event event) { - pluginManager.callEvent(event); - } - public boolean validateEmail(String email) { return validationService.validateEmail(email); } diff --git a/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java b/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java index fcbfc0bd..d56649b4 100644 --- a/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java +++ b/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java @@ -7,16 +7,13 @@ import fr.xephi.authme.cache.auth.PlayerCache; import fr.xephi.authme.cache.limbo.LimboCache; import fr.xephi.authme.cache.limbo.LimboPlayer; import fr.xephi.authme.datasource.DataSource; -import fr.xephi.authme.events.FirstSpawnTeleportEvent; import fr.xephi.authme.events.ProtectInventoryEvent; -import fr.xephi.authme.events.SpawnTeleportEvent; import fr.xephi.authme.hooks.PluginHooks; import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.permission.PlayerStatePermission; import fr.xephi.authme.process.AsynchronousProcess; import fr.xephi.authme.process.ProcessService; -import fr.xephi.authme.settings.Settings; -import fr.xephi.authme.settings.SpawnLoader; +import fr.xephi.authme.util.TeleportationService; import fr.xephi.authme.settings.properties.HooksSettings; import fr.xephi.authme.settings.properties.PluginSettings; import fr.xephi.authme.settings.properties.RegistrationSettings; @@ -29,8 +26,6 @@ import fr.xephi.authme.util.Utils; import fr.xephi.authme.util.Utils.GroupType; import org.apache.commons.lang.reflect.MethodUtils; import org.bukkit.GameMode; -import org.bukkit.Location; -import org.bukkit.Material; import org.bukkit.entity.LivingEntity; import org.bukkit.entity.Player; import org.bukkit.potion.PotionEffect; @@ -44,6 +39,9 @@ import static fr.xephi.authme.settings.properties.RestrictionSettings.PROTECT_IN public class AsynchronousJoin implements AsynchronousProcess { + private static final boolean DISABLE_COLLISIONS = MethodUtils + .getAccessibleMethod(LivingEntity.class, "setCollidable", new Class[]{}) != null; + @Inject private AuthMe plugin; @@ -63,24 +61,22 @@ public class AsynchronousJoin implements AsynchronousProcess { private PluginHooks pluginHooks; @Inject - private SpawnLoader spawnLoader; + private TeleportationService teleportationService; @Inject private BukkitService bukkitService; - private static final boolean DISABLE_COLLISIONS = MethodUtils - .getAccessibleMethod(LivingEntity.class, "setCollidable", new Class[]{}) != null; - AsynchronousJoin() { } - public void processJoin(final Player player) { - if (Utils.isUnrestricted(player)) { - return; - } + public void processJoin(final Player player) { final String name = player.getName().toLowerCase(); final String ip = Utils.getPlayerIp(player); + if (isPlayerUnrestricted(name)) { + return; + } + // Prevent player collisions in 1.9 if (DISABLE_COLLISIONS) { player.setCollidable(false); @@ -113,50 +109,15 @@ public class AsynchronousJoin implements AsynchronousProcess { return; } - if (service.getProperty(RestrictionSettings.MAX_JOIN_PER_IP) > 0 - && !service.hasPermission(player, PlayerStatePermission.ALLOW_MULTIPLE_ACCOUNTS) - && !"127.0.0.1".equalsIgnoreCase(ip) - && !"localhost".equalsIgnoreCase(ip) - && hasJoinedIp(player.getName(), ip)) { - - bukkitService.scheduleSyncDelayedTask(new Runnable() { - @Override - public void run() { - player.kickPlayer(service.retrieveSingleMessage(MessageKey.SAME_IP_ONLINE)); - } - }); + if (!validatePlayerCountForIp(player, ip)) { return; } - final Location spawnLoc = spawnLoader.getSpawnLocation(player); final boolean isAuthAvailable = database.isAuthAvailable(name); - // TODO: continue cleanup from this -sgdc3 if (isAuthAvailable) { - // Registered - - // Groups logic Utils.setGroup(player, GroupType.NOTLOGGEDIN); - - // Spawn logic - if (!service.getProperty(RestrictionSettings.NO_TELEPORT)) { - if (Settings.isTeleportToSpawnEnabled || (Settings.isForceSpawnLocOnJoinEnabled && Settings.getForcedWorlds.contains(player.getWorld().getName()))) { - bukkitService.scheduleSyncDelayedTask(new Runnable() { - @Override - public void run() { - SpawnTeleportEvent tpEvent = new SpawnTeleportEvent(player, player.getLocation(), spawnLoc, playerCache.isAuthenticated(name)); - service.callEvent(tpEvent); - if (!tpEvent.isCancelled() && player.isOnline() && tpEvent.getTo() != null - && tpEvent.getTo().getWorld() != null) { - player.teleport(tpEvent.getTo()); - } - } - }); - } - } - placePlayerSafely(player, spawnLoc); - - // Limbo cache + teleportationService.teleportOnJoin(player); limboCache.updateLimboPlayer(player); // Protect inventory @@ -193,27 +154,13 @@ public class AsynchronousJoin implements AsynchronousProcess { // Groups logic Utils.setGroup(player, GroupType.UNREGISTERED); + teleportationService.teleportOnJoin(player); // Skip if registration is optional if (!service.getProperty(RegistrationSettings.FORCE)) { return; } - // Spawn logic - if (!Settings.noTeleport && !needFirstSpawn(player) && Settings.isTeleportToSpawnEnabled - || (Settings.isForceSpawnLocOnJoinEnabled && Settings.getForcedWorlds.contains(player.getWorld().getName()))) { - bukkitService.scheduleSyncDelayedTask(new Runnable() { - @Override - public void run() { - SpawnTeleportEvent tpEvent = new SpawnTeleportEvent(player, player.getLocation(), spawnLoc, playerCache.isAuthenticated(name)); - service.callEvent(tpEvent); - if (!tpEvent.isCancelled() && player.isOnline() && tpEvent.getTo() != null - && tpEvent.getTo().getWorld() != null) { - player.teleport(tpEvent.getTo()); - } - } - }); - } } // The user is not logged in @@ -274,58 +221,12 @@ public class AsynchronousJoin implements AsynchronousProcess { } } - private boolean needFirstSpawn(final Player player) { - if (player.hasPlayedBefore()) { - return false; - } - Location firstSpawn = spawnLoader.getFirstSpawn(); - if (firstSpawn == null) { - return false; - } - - FirstSpawnTeleportEvent tpEvent = new FirstSpawnTeleportEvent(player, player.getLocation(), firstSpawn); - plugin.getServer().getPluginManager().callEvent(tpEvent); - if (!tpEvent.isCancelled()) { - if (player.isOnline() && tpEvent.getTo() != null && tpEvent.getTo().getWorld() != null) { - final Location fLoc = tpEvent.getTo(); - bukkitService.scheduleSyncDelayedTask(new Runnable() { - @Override - public void run() { - player.teleport(fLoc); - } - }); - } - } - return true; - } - - private void placePlayerSafely(final Player player, final Location spawnLoc) { - if (spawnLoc == null || service.getProperty(RestrictionSettings.NO_TELEPORT)) - return; - if (Settings.isTeleportToSpawnEnabled || (Settings.isForceSpawnLocOnJoinEnabled && Settings.getForcedWorlds.contains(player.getWorld().getName()))) - return; - if (!player.hasPlayedBefore()) - return; - bukkitService.scheduleSyncDelayedTask(new Runnable() { - @Override - public void run() { - if (spawnLoc.getWorld() == null) { - return; - } - Material cur = player.getLocation().getBlock().getType(); - Material top = player.getLocation().add(0, 1, 0).getBlock().getType(); - if (cur == Material.PORTAL || cur == Material.ENDER_PORTAL - || top == Material.PORTAL || top == Material.ENDER_PORTAL) { - service.send(player, MessageKey.UNSAFE_QUIT_LOCATION); - player.teleport(spawnLoc); - } - } - - }); + private boolean isPlayerUnrestricted(String name) { + return service.getProperty(RestrictionSettings.UNRESTRICTED_NAMES).contains(name); } /** - * Return whether the name is restricted based on the restriction setting. + * Returns whether the name is restricted based on the restriction settings. * * @param name The name to check * @param ip The IP address of the player @@ -353,14 +254,39 @@ public class AsynchronousJoin implements AsynchronousProcess { return nameFound; } - private boolean hasJoinedIp(String name, String ip) { + /** + * Checks whether the maximum number of accounts has been exceeded for the given IP address (according to + * settings and permissions). If this is the case, the player is kicked. + * + * @param player the player to verify + * @param ip the ip address of the player + * @return true if the verification is OK (no infraction), false if player has been kicked + */ + private boolean validatePlayerCountForIp(final Player player, String ip) { + if (service.getProperty(RestrictionSettings.MAX_JOIN_PER_IP) > 0 + && !service.hasPermission(player, PlayerStatePermission.ALLOW_MULTIPLE_ACCOUNTS) + && !"127.0.0.1".equalsIgnoreCase(ip) + && !"localhost".equalsIgnoreCase(ip) + && countOnlinePlayersByIp(ip) > service.getProperty(RestrictionSettings.MAX_JOIN_PER_IP)) { + + bukkitService.scheduleSyncDelayedTask(new Runnable() { + @Override + public void run() { + player.kickPlayer(service.retrieveSingleMessage(MessageKey.SAME_IP_ONLINE)); + } + }); + return false; + } + return true; + } + + private int countOnlinePlayersByIp(String ip) { int count = 0; for (Player player : bukkitService.getOnlinePlayers()) { - if (ip.equalsIgnoreCase(Utils.getPlayerIp(player)) - && !player.getName().equalsIgnoreCase(name)) { - count++; + if (ip.equalsIgnoreCase(Utils.getPlayerIp(player))) { + ++count; } } - return count >= service.getProperty(RestrictionSettings.MAX_JOIN_PER_IP); + return count; } } diff --git a/src/main/java/fr/xephi/authme/process/login/ProcessSyncPlayerLogin.java b/src/main/java/fr/xephi/authme/process/login/ProcessSyncPlayerLogin.java index 78df4473..373afd94 100644 --- a/src/main/java/fr/xephi/authme/process/login/ProcessSyncPlayerLogin.java +++ b/src/main/java/fr/xephi/authme/process/login/ProcessSyncPlayerLogin.java @@ -7,14 +7,12 @@ import fr.xephi.authme.cache.auth.PlayerAuth; import fr.xephi.authme.cache.limbo.LimboCache; import fr.xephi.authme.cache.limbo.LimboPlayer; import fr.xephi.authme.datasource.DataSource; -import fr.xephi.authme.events.AuthMeTeleportEvent; import fr.xephi.authme.events.LoginEvent; import fr.xephi.authme.events.RestoreInventoryEvent; -import fr.xephi.authme.events.SpawnTeleportEvent; import fr.xephi.authme.listener.AuthMePlayerListener; import fr.xephi.authme.process.ProcessService; import fr.xephi.authme.process.SynchronousProcess; -import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.util.TeleportationService; import fr.xephi.authme.settings.properties.HooksSettings; import fr.xephi.authme.settings.properties.RegistrationSettings; import fr.xephi.authme.settings.properties.RestrictionSettings; @@ -23,7 +21,6 @@ import fr.xephi.authme.util.Utils; import fr.xephi.authme.util.Utils.GroupType; import org.apache.commons.lang.reflect.MethodUtils; import org.bukkit.Bukkit; -import org.bukkit.Location; import org.bukkit.entity.LivingEntity; import org.bukkit.entity.Player; import org.bukkit.plugin.PluginManager; @@ -36,6 +33,9 @@ import static fr.xephi.authme.settings.properties.RestrictionSettings.PROTECT_IN public class ProcessSyncPlayerLogin implements SynchronousProcess { + private static final boolean RESTORE_COLLISIONS = MethodUtils + .getAccessibleMethod(LivingEntity.class, "setCollidable", new Class[]{}) != null; + @Inject private AuthMe plugin; @@ -54,33 +54,12 @@ public class ProcessSyncPlayerLogin implements SynchronousProcess { @Inject private PluginManager pluginManager; - private final boolean restoreCollisions = MethodUtils - .getAccessibleMethod(LivingEntity.class, "setCollidable", new Class[]{}) != null; + @Inject + private TeleportationService teleportationService; ProcessSyncPlayerLogin() { } - private void packQuitLocation(Player player, PlayerAuth auth) { - Utils.packCoords(auth.getQuitLocX(), auth.getQuitLocY(), auth.getQuitLocZ(), auth.getWorld(), player); - } - - private void teleportBackFromSpawn(Player player, LimboPlayer limboPlayer) { - AuthMeTeleportEvent tpEvent = new AuthMeTeleportEvent(player, limboPlayer.getLoc()); - pluginManager.callEvent(tpEvent); - if (!tpEvent.isCancelled() && tpEvent.getTo() != null) { - player.teleport(tpEvent.getTo()); - } - } - - private void teleportToSpawn(Player player) { - Location spawnL = plugin.getSpawnLocation(player); - SpawnTeleportEvent tpEvent = new SpawnTeleportEvent(player, player.getLocation(), spawnL, true); - pluginManager.callEvent(tpEvent); - if (!tpEvent.isCancelled() && tpEvent.getTo() != null) { - player.teleport(tpEvent.getTo()); - } - } - private void restoreSpeedEffects(Player player) { if (!service.getProperty(RestrictionSettings.ALLOW_UNAUTHED_MOVEMENT) && service.getProperty(RestrictionSettings.REMOVE_SPEED)) { @@ -118,23 +97,9 @@ public class ProcessSyncPlayerLogin implements SynchronousProcess { restoreOpState(player, limbo); Utils.setGroup(player, GroupType.LOGGEDIN); - if (!Settings.noTeleport) { - if (Settings.isTeleportToSpawnEnabled && !Settings.isForceSpawnLocOnJoinEnabled && Settings.getForcedWorlds.contains(player.getWorld().getName())) { - if (Settings.isSaveQuitLocationEnabled && auth.getQuitLocY() != 0) { - packQuitLocation(player, auth); - } else { - teleportBackFromSpawn(player, limbo); - } - } else if (Settings.isForceSpawnLocOnJoinEnabled && Settings.getForcedWorlds.contains(player.getWorld().getName())) { - teleportToSpawn(player); - } else if (Settings.isSaveQuitLocationEnabled && auth.getQuitLocY() != 0) { - packQuitLocation(player, auth); - } else { - teleportBackFromSpawn(player, limbo); - } - } + teleportationService.teleportOnLogin(player, auth, limbo); - if (restoreCollisions && !service.getProperty(KEEP_COLLISIONS_DISABLED)) { + if (RESTORE_COLLISIONS && !service.getProperty(KEEP_COLLISIONS_DISABLED)) { player.setCollidable(true); } diff --git a/src/main/java/fr/xephi/authme/process/register/ProcessSyncPasswordRegister.java b/src/main/java/fr/xephi/authme/process/register/ProcessSyncPasswordRegister.java index 26bf174c..cf6bba2b 100644 --- a/src/main/java/fr/xephi/authme/process/register/ProcessSyncPasswordRegister.java +++ b/src/main/java/fr/xephi/authme/process/register/ProcessSyncPasswordRegister.java @@ -102,7 +102,7 @@ public class ProcessSyncPasswordRegister implements SynchronousProcess { if (service.getProperty(HIDE_TABLIST_BEFORE_LOGIN) && plugin.inventoryProtector != null) { RestoreInventoryEvent event = new RestoreInventoryEvent(player); - service.callEvent(event); + bukkitService.callEvent(event); if (!event.isCancelled()) { plugin.inventoryProtector.sendInventoryPacket(player); } diff --git a/src/main/java/fr/xephi/authme/settings/Settings.java b/src/main/java/fr/xephi/authme/settings/Settings.java index 9684d752..c86cd83b 100644 --- a/src/main/java/fr/xephi/authme/settings/Settings.java +++ b/src/main/java/fr/xephi/authme/settings/Settings.java @@ -9,7 +9,6 @@ import fr.xephi.authme.settings.properties.RestrictionSettings; import fr.xephi.authme.settings.properties.SecuritySettings; import org.bukkit.configuration.file.FileConfiguration; -import java.util.ArrayList; import java.util.List; /** @@ -66,16 +65,11 @@ public final class Settings { isAllowRestrictedIp = load(RestrictionSettings.ENABLE_RESTRICTED_USERS); isRemoveSpeedEnabled = load(RestrictionSettings.REMOVE_SPEED); isForceSpawnLocOnJoinEnabled = load(RestrictionSettings.FORCE_SPAWN_LOCATION_AFTER_LOGIN); - isSaveQuitLocationEnabled = configFile.getBoolean("settings.restrictions.SaveQuitLocation", false); + isSaveQuitLocationEnabled = load(RestrictionSettings.SAVE_QUIT_LOCATION); getUnloggedinGroup = load(SecuritySettings.UNLOGGEDIN_GROUP); getNonActivatedGroup = configFile.getInt("ExternalBoardOptions.nonActivedUserGroup", -1); unRegisteredGroup = configFile.getString("GroupOptions.UnregisteredPlayerGroup", ""); - - getUnrestrictedName = new ArrayList<>(); - for (String name : configFile.getStringList("settings.unrestrictions.UnrestrictedName")) { - getUnrestrictedName.add(name.toLowerCase()); - } - + getUnrestrictedName = load(RestrictionSettings.UNRESTRICTED_NAMES); getRegisteredGroup = configFile.getString("GroupOptions.RegisteredPlayerGroup", ""); protectInventoryBeforeLogInEnabled = load(RestrictionSettings.PROTECT_INVENTORY_BEFORE_LOGIN); isStopEnabled = configFile.getBoolean("Security.SQLProblem.stopServer", true); diff --git a/src/main/java/fr/xephi/authme/util/BukkitService.java b/src/main/java/fr/xephi/authme/util/BukkitService.java index 5837b942..ac8a408b 100644 --- a/src/main/java/fr/xephi/authme/util/BukkitService.java +++ b/src/main/java/fr/xephi/authme/util/BukkitService.java @@ -4,7 +4,9 @@ import fr.xephi.authme.AuthMe; import fr.xephi.authme.ConsoleLogger; import org.bukkit.Bukkit; import org.bukkit.OfflinePlayer; +import org.bukkit.World; import org.bukkit.entity.Player; +import org.bukkit.event.Event; import org.bukkit.scheduler.BukkitTask; import javax.inject.Inject; @@ -30,7 +32,7 @@ public class BukkitService { private Method getOnlinePlayers; @Inject - public BukkitService(AuthMe authMe) { + BukkitService(AuthMe authMe) { this.authMe = authMe; getOnlinePlayersIsCollection = initializeOnlinePlayersIsCollectionField(); } @@ -167,6 +169,27 @@ public class BukkitService { return Collections.emptyList(); } + /** + * Calls an event with the given details. + * + * @param event Event details + * @throws IllegalStateException Thrown when an asynchronous event is + * fired from synchronous code. + */ + public void callEvent(Event event) { + Bukkit.getPluginManager().callEvent(event); + } + + /** + * Gets the world with the given name. + * + * @param name the name of the world to retrieve + * @return a world with the given name, or null if none exists + */ + public World getWorld(String name) { + return Bukkit.getWorld(name); + } + /** * Method run upon initialization to verify whether or not the Bukkit implementation * returns the online players as a Collection. diff --git a/src/main/java/fr/xephi/authme/util/TeleportationService.java b/src/main/java/fr/xephi/authme/util/TeleportationService.java new file mode 100644 index 00000000..b7beaedd --- /dev/null +++ b/src/main/java/fr/xephi/authme/util/TeleportationService.java @@ -0,0 +1,144 @@ +package fr.xephi.authme.util; + +import fr.xephi.authme.cache.auth.PlayerAuth; +import fr.xephi.authme.cache.auth.PlayerCache; +import fr.xephi.authme.cache.limbo.LimboPlayer; +import fr.xephi.authme.events.AbstractTeleportEvent; +import fr.xephi.authme.events.AuthMeTeleportEvent; +import fr.xephi.authme.events.FirstSpawnTeleportEvent; +import fr.xephi.authme.events.SpawnTeleportEvent; +import fr.xephi.authme.initialization.Reloadable; +import fr.xephi.authme.output.Messages; +import fr.xephi.authme.settings.NewSetting; +import fr.xephi.authme.settings.SpawnLoader; +import fr.xephi.authme.settings.properties.RestrictionSettings; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.entity.Player; + +import javax.annotation.PostConstruct; +import javax.inject.Inject; +import java.util.HashSet; +import java.util.Set; + +import static fr.xephi.authme.settings.properties.RestrictionSettings.TELEPORT_UNAUTHED_TO_SPAWN; + +/** + * Handles teleportation (placement of player to spawn). + */ +public class TeleportationService implements Reloadable { + + @Inject + private NewSetting settings; + + @Inject + private Messages messages; + + @Inject + private BukkitService bukkitService; + + @Inject + private SpawnLoader spawnLoader; + + @Inject + private PlayerCache playerCache; + + private Set spawnOnLoginWorlds; + + TeleportationService() { } + + + @PostConstruct + @Override + public void reload() { + // Use a Set for better performance with #contains() + spawnOnLoginWorlds = new HashSet<>(settings.getProperty(RestrictionSettings.FORCE_SPAWN_ON_WORLDS)); + } + + public void teleportOnJoin(final Player player) { + if (settings.getProperty(RestrictionSettings.NO_TELEPORT)) { + return; + } else if (teleportToFirstSpawn(player)) { + return; + } + + if (settings.getProperty(TELEPORT_UNAUTHED_TO_SPAWN) || mustForceSpawnAfterLogin(player.getWorld())) { + teleportToSpawn(player, playerCache.isAuthenticated(player.getName())); + } + } + + public void teleportOnLogin(final Player player, PlayerAuth auth, LimboPlayer limbo) { + if (settings.getProperty(RestrictionSettings.NO_TELEPORT)) { + return; + } + + if (mustForceSpawnAfterLogin(player.getWorld())) { + teleportToSpawn(player, true); + } else if (settings.getProperty(TELEPORT_UNAUTHED_TO_SPAWN)) { + if (settings.getProperty(RestrictionSettings.SAVE_QUIT_LOCATION) && auth.getQuitLocY() != 0) { + Location location = buildLocationFromAuth(player, auth); + teleportBackFromSpawn(player, location); + } else { + teleportBackFromSpawn(player, limbo.getLoc()); + } + } + } + + private boolean teleportToFirstSpawn(final Player player) { + if (player.hasPlayedBefore()) { + return false; + } + Location firstSpawn = spawnLoader.getFirstSpawn(); + if (firstSpawn == null) { + return false; + } + + performTeleportation(player, new FirstSpawnTeleportEvent(player, player.getLocation(), firstSpawn)); + return true; + } + + private static boolean isEventValid(AbstractTeleportEvent event) { + return !event.isCancelled() && event.getTo() != null && event.getTo().getWorld() != null; + } + + private Location buildLocationFromAuth(Player player, PlayerAuth auth) { + World world = bukkitService.getWorld(auth.getWorld()); + if (world == null) { + world = player.getWorld(); + } + return new Location(world, auth.getQuitLocX(), auth.getQuitLocY(), auth.getQuitLocZ()); + } + + private void teleportBackFromSpawn(final Player player, final Location location) { + performTeleportation(player, new AuthMeTeleportEvent(player, location)); + } + + private void teleportToSpawn(final Player player, final boolean isAuthenticated) { + final Location spawnLoc = spawnLoader.getSpawnLocation(player); + performTeleportation(player, new SpawnTeleportEvent(player, player.getLocation(), spawnLoc, isAuthenticated)); + } + + /** + * Emits the teleportation event and performs teleportation according to it (potentially modified + * by external listeners). Note that not teleportation is performed if the event's location is empty. + * + * @param player the player to teleport + * @param event the event to emit and according to which to teleport + */ + private void performTeleportation(final Player player, final AbstractTeleportEvent event) { + bukkitService.scheduleSyncDelayedTask(new Runnable() { + @Override + public void run() { + bukkitService.callEvent(event); + if (player.isOnline() && isEventValid(event)) { + player.teleport(event.getTo()); + } + } + }); + } + + private boolean mustForceSpawnAfterLogin(World world) { + return settings.getProperty(RestrictionSettings.FORCE_SPAWN_LOCATION_AFTER_LOGIN) + && spawnOnLoginWorlds.contains(world.getName()); + } +} diff --git a/src/main/java/fr/xephi/authme/util/Utils.java b/src/main/java/fr/xephi/authme/util/Utils.java index 32cbeb9a..de935bb3 100644 --- a/src/main/java/fr/xephi/authme/util/Utils.java +++ b/src/main/java/fr/xephi/authme/util/Utils.java @@ -7,10 +7,8 @@ import fr.xephi.authme.cache.limbo.LimboPlayer; import fr.xephi.authme.events.AuthMeTeleportEvent; import fr.xephi.authme.permission.PermissionsManager; import fr.xephi.authme.settings.Settings; -import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.OfflinePlayer; -import org.bukkit.World; import org.bukkit.entity.Player; import java.util.Arrays; @@ -117,6 +115,7 @@ public final class Utils { return permsMan.addGroup(player, group); } + @Deprecated public static boolean isUnrestricted(Player player) { // TODO ljacqu 20160602: Checking for Settings.isAllowRestrictedIp is wrong! Nothing in the config suggests // that this setting has anything to do with unrestricted names @@ -124,32 +123,7 @@ public final class Utils { && Settings.getUnrestrictedName.contains(player.getName().toLowerCase()); } - public static void packCoords(double x, double y, double z, String w, final Player pl) { - World theWorld; - if (w.equals("unavailableworld")) { - theWorld = pl.getWorld(); - } else { - theWorld = Bukkit.getWorld(w); - } - if (theWorld == null) { - theWorld = pl.getWorld(); - } - final World world = theWorld; - final Location loc = new Location(world, x, y, z); - - Bukkit.getScheduler().scheduleSyncDelayedTask(plugin, new Runnable() { - - @Override - public void run() { - AuthMeTeleportEvent tpEvent = new AuthMeTeleportEvent(pl, loc); - plugin.getServer().getPluginManager().callEvent(tpEvent); - if (!tpEvent.isCancelled()) { - pl.teleport(tpEvent.getTo()); - } - } - }); - } - + @Deprecated public static void teleportToSpawn(Player player) { if (Settings.isTeleportToSpawnEnabled && !Settings.noTeleport) { Location spawn = plugin.getSpawnLocation(player); diff --git a/src/test/java/fr/xephi/authme/TestHelper.java b/src/test/java/fr/xephi/authme/TestHelper.java index cc9480a4..e9cf35ba 100644 --- a/src/test/java/fr/xephi/authme/TestHelper.java +++ b/src/test/java/fr/xephi/authme/TestHelper.java @@ -72,6 +72,20 @@ public final class TestHelper { runnable.run(); } + /** + * Execute a {@link Runnable} passed to a mock's {@link BukkitService#scheduleSyncDelayedTask(Runnable)} + * method. Note that calling this method expects that there be a runnable sent to the method and will fail + * otherwise. + * + * @param service The mock service + */ + public static void runSyncDelayedTask(BukkitService service) { + ArgumentCaptor captor = ArgumentCaptor.forClass(Runnable.class); + verify(service).scheduleSyncDelayedTask(captor.capture()); + Runnable runnable = captor.getValue(); + runnable.run(); + } + /** * Execute a {@link Runnable} passed to a mock's {@link BukkitService#scheduleSyncDelayedTask(Runnable, long)} * method. Note that calling this method expects that there be a runnable sent to the method and will fail diff --git a/src/test/java/fr/xephi/authme/process/ProcessServiceTest.java b/src/test/java/fr/xephi/authme/process/ProcessServiceTest.java index 164494a9..c0505113 100644 --- a/src/test/java/fr/xephi/authme/process/ProcessServiceTest.java +++ b/src/test/java/fr/xephi/authme/process/ProcessServiceTest.java @@ -10,8 +10,6 @@ import fr.xephi.authme.settings.properties.SecuritySettings; import fr.xephi.authme.util.ValidationService; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; -import org.bukkit.event.Event; -import org.bukkit.plugin.PluginManager; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; @@ -42,9 +40,6 @@ public class ProcessServiceTest { @Mock private Messages messages; - @Mock - private PluginManager pluginManager; - @Mock private PermissionsManager permissionsManager; @@ -157,18 +152,6 @@ public class ProcessServiceTest { verify(validationService).isEmailFreeForRegistration(email, sender); } - @Test - public void shouldEmitEvent() { - // given - Event event = mock(Event.class); - - // when - processService.callEvent(event); - - // then - verify(pluginManager).callEvent(event); - } - @Test public void shouldCheckPermission() { // given diff --git a/src/test/java/fr/xephi/authme/util/TeleportationServiceTest.java b/src/test/java/fr/xephi/authme/util/TeleportationServiceTest.java new file mode 100644 index 00000000..6309bfa9 --- /dev/null +++ b/src/test/java/fr/xephi/authme/util/TeleportationServiceTest.java @@ -0,0 +1,421 @@ +package fr.xephi.authme.util; + +import fr.xephi.authme.cache.auth.PlayerAuth; +import fr.xephi.authme.cache.auth.PlayerCache; +import fr.xephi.authme.cache.limbo.LimboPlayer; +import fr.xephi.authme.events.FirstSpawnTeleportEvent; +import fr.xephi.authme.events.SpawnTeleportEvent; +import fr.xephi.authme.output.Messages; +import fr.xephi.authme.settings.NewSetting; +import fr.xephi.authme.settings.SpawnLoader; +import fr.xephi.authme.settings.properties.RestrictionSettings; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.entity.Player; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; + +import java.util.Arrays; + +import static fr.xephi.authme.TestHelper.runSyncDelayedTask; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; + +/** + * Test for {@link TeleportationService}. + */ +@RunWith(MockitoJUnitRunner.class) +public class TeleportationServiceTest { + + @InjectMocks + private TeleportationService teleportationService; + + @Mock + private NewSetting settings; + + @Mock + private Messages messages; + + @Mock + private BukkitService bukkitService; + + @Mock + private SpawnLoader spawnLoader; + + @Mock + private PlayerCache playerCache; + + @Before + public void setUpForcedWorlds() { + given(settings.getProperty(RestrictionSettings.FORCE_SPAWN_ON_WORLDS)) + .willReturn(Arrays.asList("forced1", "OtherForced")); + teleportationService.reload(); + + given(settings.getProperty(RestrictionSettings.NO_TELEPORT)).willReturn(false); + } + + // ----------- + // JOINING + // ----------- + @Test + public void shouldNotTeleportPlayerOnJoin() { + // given + given(settings.getProperty(RestrictionSettings.NO_TELEPORT)).willReturn(true); + Player player = mock(Player.class); + + // when + teleportationService.teleportOnJoin(player); + + // then + verifyZeroInteractions(player); + verifyZeroInteractions(bukkitService); + } + + @Test + public void shouldTeleportPlayerToFirstSpawn() { + // given + Player player = mock(Player.class); + given(player.hasPlayedBefore()).willReturn(false); + given(player.isOnline()).willReturn(true); + Location firstSpawn = mockLocation(); + given(spawnLoader.getFirstSpawn()).willReturn(firstSpawn); + + // when + teleportationService.teleportOnJoin(player); + runSyncDelayedTask(bukkitService); + + // then + verify(player).teleport(firstSpawn); + verify(bukkitService).callEvent(any(FirstSpawnTeleportEvent.class)); + verify(spawnLoader).getFirstSpawn(); + verify(spawnLoader, never()).getSpawnLocation(any(Player.class)); + } + + @Test + public void shouldTeleportPlayerToSpawn() { + // given + given(settings.getProperty(RestrictionSettings.TELEPORT_UNAUTHED_TO_SPAWN)).willReturn(true); + Player player = mock(Player.class); + given(player.hasPlayedBefore()).willReturn(true); + given(player.isOnline()).willReturn(true); + Location spawn = mockLocation(); + given(spawnLoader.getSpawnLocation(player)).willReturn(spawn); + + // when + teleportationService.teleportOnJoin(player); + runSyncDelayedTask(bukkitService); + + // then + verify(player).teleport(spawn); + verify(bukkitService).callEvent(any(SpawnTeleportEvent.class)); + verify(spawnLoader).getSpawnLocation(player); + } + + @Test + // No first spawn defined, no teleport settings enabled + public void shouldNotTeleportNewPlayer() { + // given + Player player = mock(Player.class); + given(player.hasPlayedBefore()).willReturn(false); + given(player.isOnline()).willReturn(true); + given(settings.getProperty(RestrictionSettings.TELEPORT_UNAUTHED_TO_SPAWN)).willReturn(false); + given(settings.getProperty(RestrictionSettings.FORCE_SPAWN_LOCATION_AFTER_LOGIN)).willReturn(false); + given(spawnLoader.getFirstSpawn()).willReturn(null); + + // when + teleportationService.teleportOnJoin(player); + + // then + verify(player, never()).teleport(any(Location.class)); + verify(spawnLoader).getFirstSpawn(); + verify(spawnLoader, never()).getSpawnLocation(any(Player.class)); + verifyZeroInteractions(bukkitService); + } + + @Test + public void shouldTeleportPlayerDueToForcedWorld() { + // given + Player player = mock(Player.class); + given(player.hasPlayedBefore()).willReturn(true); + given(player.isOnline()).willReturn(true); + + World playerWorld = mock(World.class); + given(playerWorld.getName()).willReturn("OtherForced"); + given(player.getWorld()).willReturn(playerWorld); + given(settings.getProperty(RestrictionSettings.TELEPORT_UNAUTHED_TO_SPAWN)).willReturn(false); + given(settings.getProperty(RestrictionSettings.FORCE_SPAWN_LOCATION_AFTER_LOGIN)).willReturn(true); + + Location spawn = mockLocation(); + given(spawnLoader.getSpawnLocation(player)).willReturn(spawn); + + // when + teleportationService.teleportOnJoin(player); + runSyncDelayedTask(bukkitService); + + // then + verify(player).teleport(spawn); + verify(bukkitService).callEvent(any(SpawnTeleportEvent.class)); + verify(spawnLoader).getSpawnLocation(player); + } + + @Test + public void shouldNotTeleportPlayerForRemovedLocationInEvent() { + // given + final Player player = mock(Player.class); + given(player.hasPlayedBefore()).willReturn(true); + Location spawn = mockLocation(); + given(spawnLoader.getSpawnLocation(player)).willReturn(spawn); + given(settings.getProperty(RestrictionSettings.TELEPORT_UNAUTHED_TO_SPAWN)).willReturn(true); + doAnswer(new Answer() { + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + SpawnTeleportEvent event = (SpawnTeleportEvent) invocation.getArguments()[0]; + assertThat(event.getPlayer(), equalTo(player)); + event.setTo(null); + return null; + } + }).when(bukkitService).callEvent(any(SpawnTeleportEvent.class)); + + // when + teleportationService.teleportOnJoin(player); + runSyncDelayedTask(bukkitService); + + // then + verify(bukkitService).callEvent(any(SpawnTeleportEvent.class)); + verify(player, never()).teleport(any(Location.class)); + } + + @Test + public void shouldNotTeleportPlayerForCanceledEvent() { + // given + final Player player = mock(Player.class); + given(player.hasPlayedBefore()).willReturn(true); + Location spawn = mockLocation(); + given(spawnLoader.getSpawnLocation(player)).willReturn(spawn); + given(settings.getProperty(RestrictionSettings.TELEPORT_UNAUTHED_TO_SPAWN)).willReturn(true); + doAnswer(new Answer() { + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + SpawnTeleportEvent event = (SpawnTeleportEvent) invocation.getArguments()[0]; + assertThat(event.getPlayer(), equalTo(player)); + event.setCancelled(true); + return null; + } + }).when(bukkitService).callEvent(any(SpawnTeleportEvent.class)); + + // when + teleportationService.teleportOnJoin(player); + runSyncDelayedTask(bukkitService); + + // then + verify(bukkitService).callEvent(any(SpawnTeleportEvent.class)); + verify(player, never()).teleport(any(Location.class)); + } + + + // --------- + // LOGIN + // --------- + @Test + public void shouldNotTeleportUponLogin() { + // given + given(settings.getProperty(RestrictionSettings.NO_TELEPORT)).willReturn(true); + Player player = mock(Player.class); + PlayerAuth auth = mock(PlayerAuth.class); + LimboPlayer limbo = mock(LimboPlayer.class); + + // when + teleportationService.teleportOnLogin(player, auth, limbo); + + // then + verifyZeroInteractions(player, auth, limbo, bukkitService, spawnLoader); + } + + @Test + public void shouldTeleportPlayerToSpawnAfterLogin() { + // given + given(settings.getProperty(RestrictionSettings.FORCE_SPAWN_LOCATION_AFTER_LOGIN)).willReturn(true); + World world = mock(World.class); + given(world.getName()).willReturn("forced1"); + Player player = mock(Player.class); + given(player.getWorld()).willReturn(world); + given(player.isOnline()).willReturn(true); + Location spawn = mockLocation(); + given(spawnLoader.getSpawnLocation(player)).willReturn(spawn); + PlayerAuth auth = mock(PlayerAuth.class); + LimboPlayer limbo = mock(LimboPlayer.class); + + // when + teleportationService.teleportOnLogin(player, auth, limbo); + runSyncDelayedTask(bukkitService); + + // then + verify(player).teleport(spawn); + } + + @Test + // Check that the worlds for "force spawn loc after login" are case-sensitive + public void shouldNotTeleportToSpawnForOtherCaseInWorld() { + // given + given(settings.getProperty(RestrictionSettings.FORCE_SPAWN_LOCATION_AFTER_LOGIN)).willReturn(true); + given(settings.getProperty(RestrictionSettings.TELEPORT_UNAUTHED_TO_SPAWN)).willReturn(false); + World world = mock(World.class); + given(world.getName()).willReturn("Forced1"); // different case + Player player = mock(Player.class); + given(player.getWorld()).willReturn(world); + given(player.isOnline()).willReturn(true); + Location spawn = mockLocation(); + given(spawnLoader.getSpawnLocation(player)).willReturn(spawn); + PlayerAuth auth = mock(PlayerAuth.class); + LimboPlayer limbo = mock(LimboPlayer.class); + + // when + teleportationService.teleportOnLogin(player, auth, limbo); + + // then + verify(player, never()).teleport(spawn); + verifyZeroInteractions(bukkitService, spawnLoader); + } + + @Test + public void shouldTeleportBackToPlayerAuthLocation() { + // given + given(settings.getProperty(RestrictionSettings.FORCE_SPAWN_LOCATION_AFTER_LOGIN)).willReturn(false); + given(settings.getProperty(RestrictionSettings.TELEPORT_UNAUTHED_TO_SPAWN)).willReturn(true); + given(settings.getProperty(RestrictionSettings.SAVE_QUIT_LOCATION)).willReturn(true); + + PlayerAuth auth = createAuthWithLocation(); + auth.setWorld("myWorld"); + World world = mock(World.class); + given(bukkitService.getWorld("myWorld")).willReturn(world); + + Player player = mock(Player.class); + given(player.isOnline()).willReturn(true); + LimboPlayer limbo = mock(LimboPlayer.class); + + // when + teleportationService.teleportOnLogin(player, auth, limbo); + runSyncDelayedTask(bukkitService); + + // then + ArgumentCaptor locationCaptor = ArgumentCaptor.forClass(Location.class); + verify(player).teleport(locationCaptor.capture()); + assertCorrectLocation(locationCaptor.getValue(), auth, world); + } + + @Test + public void shouldTeleportAccordingToPlayerAuthAndPlayerWorldAsFallback() { + // given + given(settings.getProperty(RestrictionSettings.FORCE_SPAWN_LOCATION_AFTER_LOGIN)).willReturn(false); + given(settings.getProperty(RestrictionSettings.TELEPORT_UNAUTHED_TO_SPAWN)).willReturn(true); + given(settings.getProperty(RestrictionSettings.SAVE_QUIT_LOCATION)).willReturn(true); + + PlayerAuth auth = createAuthWithLocation(); + auth.setWorld("myWorld"); + given(bukkitService.getWorld("myWorld")).willReturn(null); + + Player player = mock(Player.class); + given(player.isOnline()).willReturn(true); + World world = mock(World.class); + given(player.getWorld()).willReturn(world); + LimboPlayer limbo = mock(LimboPlayer.class); + + // when + teleportationService.teleportOnLogin(player, auth, limbo); + runSyncDelayedTask(bukkitService); + + // then + ArgumentCaptor locationCaptor = ArgumentCaptor.forClass(Location.class); + verify(player).teleport(locationCaptor.capture()); + assertCorrectLocation(locationCaptor.getValue(), auth, world); + } + + @Test + public void shouldTeleportWithLimboPlayerIfAuthYCoordIsNotSet() { + // given + given(settings.getProperty(RestrictionSettings.FORCE_SPAWN_LOCATION_AFTER_LOGIN)).willReturn(false); + given(settings.getProperty(RestrictionSettings.TELEPORT_UNAUTHED_TO_SPAWN)).willReturn(true); + given(settings.getProperty(RestrictionSettings.SAVE_QUIT_LOCATION)).willReturn(true); + + PlayerAuth auth = createAuthWithLocation(); + auth.setQuitLocY(0.0); + auth.setWorld("authWorld"); + Player player = mock(Player.class); + given(player.isOnline()).willReturn(true); + World world = mock(World.class); + given(player.getWorld()).willReturn(world); + LimboPlayer limbo = mock(LimboPlayer.class); + Location location = mockLocation(); + given(limbo.getLoc()).willReturn(location); + + // when + teleportationService.teleportOnLogin(player, auth, limbo); + runSyncDelayedTask(bukkitService); + + // then + verify(player).teleport(location); + verify(bukkitService, never()).getWorld(anyString()); + } + + @Test + public void shouldTeleportWithLimboPlayerIfSaveQuitLocIsDisabled() { + // given + given(settings.getProperty(RestrictionSettings.FORCE_SPAWN_LOCATION_AFTER_LOGIN)).willReturn(false); + given(settings.getProperty(RestrictionSettings.TELEPORT_UNAUTHED_TO_SPAWN)).willReturn(true); + given(settings.getProperty(RestrictionSettings.SAVE_QUIT_LOCATION)).willReturn(false); + + PlayerAuth auth = createAuthWithLocation(); + Player player = mock(Player.class); + given(player.isOnline()).willReturn(true); + World world = mock(World.class); + given(player.getWorld()).willReturn(world); + LimboPlayer limbo = mock(LimboPlayer.class); + Location location = mockLocation(); + given(limbo.getLoc()).willReturn(location); + + // when + teleportationService.teleportOnLogin(player, auth, limbo); + runSyncDelayedTask(bukkitService); + + // then + verify(player).teleport(location); + } + + + // We check that the World in Location is set, this method creates a mock World in Location for us + private static Location mockLocation() { + Location location = mock(Location.class); + given(location.getWorld()).willReturn(mock(World.class)); + return location; + } + + private static PlayerAuth createAuthWithLocation() { + return PlayerAuth.builder() + .name("bobby") + .locX(123.45).locY(23.4).locZ(-4.567) + .build(); + } + + private void assertCorrectLocation(Location location, PlayerAuth auth, World world) { + assertThat(location.getX(), equalTo(auth.getQuitLocX())); + assertThat(location.getY(), equalTo(auth.getQuitLocY())); + assertThat(location.getZ(), equalTo(auth.getQuitLocZ())); + assertThat(location.getWorld(), equalTo(world)); + } + +} From 3cdec91255e10a47b7d63e07059000a8d5e1e627 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Sun, 12 Jun 2016 12:48:32 +0200 Subject: [PATCH 174/200] Do not teleport unregistered player if registration is optional --- .../java/fr/xephi/authme/process/join/AsynchronousJoin.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java b/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java index d56649b4..591fa9b7 100644 --- a/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java +++ b/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java @@ -154,13 +154,13 @@ public class AsynchronousJoin implements AsynchronousProcess { // Groups logic Utils.setGroup(player, GroupType.UNREGISTERED); - teleportationService.teleportOnJoin(player); // Skip if registration is optional if (!service.getProperty(RegistrationSettings.FORCE)) { return; } + teleportationService.teleportOnJoin(player); } // The user is not logged in From 5ef62784b549fe034ac34bf5a5a3480c7a150122 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Sun, 12 Jun 2016 14:10:38 +0200 Subject: [PATCH 175/200] #729 Use world from LimboPlayer for "spawn after login" feat., simplify teleport event constructors --- .../events/FirstSpawnTeleportEvent.java | 5 +-- .../authme/events/SpawnTeleportEvent.java | 6 +-- .../authme/util/TeleportationService.java | 44 ++++++++++--------- .../authme/util/TeleportationServiceTest.java | 17 ++++--- 4 files changed, 38 insertions(+), 34 deletions(-) diff --git a/src/main/java/fr/xephi/authme/events/FirstSpawnTeleportEvent.java b/src/main/java/fr/xephi/authme/events/FirstSpawnTeleportEvent.java index 28532b39..6a90e5dd 100644 --- a/src/main/java/fr/xephi/authme/events/FirstSpawnTeleportEvent.java +++ b/src/main/java/fr/xephi/authme/events/FirstSpawnTeleportEvent.java @@ -17,11 +17,10 @@ public class FirstSpawnTeleportEvent extends AbstractTeleportEvent { * Constructor. * * @param player The player - * @param from The location the player is being teleported away from * @param to The teleport destination */ - public FirstSpawnTeleportEvent(Player player, Location from, Location to) { - super(true, player, from, to); + public FirstSpawnTeleportEvent(Player player, Location to) { + super(true, player, to); } /** diff --git a/src/main/java/fr/xephi/authme/events/SpawnTeleportEvent.java b/src/main/java/fr/xephi/authme/events/SpawnTeleportEvent.java index 28f61111..a1a85d84 100644 --- a/src/main/java/fr/xephi/authme/events/SpawnTeleportEvent.java +++ b/src/main/java/fr/xephi/authme/events/SpawnTeleportEvent.java @@ -17,13 +17,11 @@ public class SpawnTeleportEvent extends AbstractTeleportEvent { * Constructor. * * @param player The player - * @param from The location the player is being teleported away from * @param to The teleport destination * @param isAuthenticated Whether or not the player is logged in */ - // TODO ljacqu 20160611: We only ever call this with from = player.getLocation() -> could be done in constructor - public SpawnTeleportEvent(Player player, Location from, Location to, boolean isAuthenticated) { - super(false, player, from, to); + public SpawnTeleportEvent(Player player, Location to, boolean isAuthenticated) { + super(false, player, to); this.isAuthenticated = isAuthenticated; } diff --git a/src/main/java/fr/xephi/authme/util/TeleportationService.java b/src/main/java/fr/xephi/authme/util/TeleportationService.java index b7beaedd..1042caf4 100644 --- a/src/main/java/fr/xephi/authme/util/TeleportationService.java +++ b/src/main/java/fr/xephi/authme/util/TeleportationService.java @@ -62,7 +62,7 @@ public class TeleportationService implements Reloadable { return; } - if (settings.getProperty(TELEPORT_UNAUTHED_TO_SPAWN) || mustForceSpawnAfterLogin(player.getWorld())) { + if (settings.getProperty(TELEPORT_UNAUTHED_TO_SPAWN) || mustForceSpawnAfterLogin(player.getWorld().getName())) { teleportToSpawn(player, playerCache.isAuthenticated(player.getName())); } } @@ -72,7 +72,9 @@ public class TeleportationService implements Reloadable { return; } - if (mustForceSpawnAfterLogin(player.getWorld())) { + // The world in LimboPlayer is from where the player comes, before any teleportation by AuthMe + String worldName = limbo.getLoc().getWorld().getName(); + if (mustForceSpawnAfterLogin(worldName)) { teleportToSpawn(player, true); } else if (settings.getProperty(TELEPORT_UNAUTHED_TO_SPAWN)) { if (settings.getProperty(RestrictionSettings.SAVE_QUIT_LOCATION) && auth.getQuitLocY() != 0) { @@ -84,21 +86,9 @@ public class TeleportationService implements Reloadable { } } - private boolean teleportToFirstSpawn(final Player player) { - if (player.hasPlayedBefore()) { - return false; - } - Location firstSpawn = spawnLoader.getFirstSpawn(); - if (firstSpawn == null) { - return false; - } - - performTeleportation(player, new FirstSpawnTeleportEvent(player, player.getLocation(), firstSpawn)); - return true; - } - - private static boolean isEventValid(AbstractTeleportEvent event) { - return !event.isCancelled() && event.getTo() != null && event.getTo().getWorld() != null; + private boolean mustForceSpawnAfterLogin(String worldName) { + return settings.getProperty(RestrictionSettings.FORCE_SPAWN_LOCATION_AFTER_LOGIN) + && spawnOnLoginWorlds.contains(worldName); } private Location buildLocationFromAuth(Player player, PlayerAuth auth) { @@ -109,13 +99,26 @@ public class TeleportationService implements Reloadable { return new Location(world, auth.getQuitLocX(), auth.getQuitLocY(), auth.getQuitLocZ()); } + private boolean teleportToFirstSpawn(final Player player) { + if (player.hasPlayedBefore()) { + return false; + } + Location firstSpawn = spawnLoader.getFirstSpawn(); + if (firstSpawn == null) { + return false; + } + + performTeleportation(player, new FirstSpawnTeleportEvent(player, firstSpawn)); + return true; + } + private void teleportBackFromSpawn(final Player player, final Location location) { performTeleportation(player, new AuthMeTeleportEvent(player, location)); } private void teleportToSpawn(final Player player, final boolean isAuthenticated) { final Location spawnLoc = spawnLoader.getSpawnLocation(player); - performTeleportation(player, new SpawnTeleportEvent(player, player.getLocation(), spawnLoc, isAuthenticated)); + performTeleportation(player, new SpawnTeleportEvent(player, spawnLoc, isAuthenticated)); } /** @@ -137,8 +140,7 @@ public class TeleportationService implements Reloadable { }); } - private boolean mustForceSpawnAfterLogin(World world) { - return settings.getProperty(RestrictionSettings.FORCE_SPAWN_LOCATION_AFTER_LOGIN) - && spawnOnLoginWorlds.contains(world.getName()); + private static boolean isEventValid(AbstractTeleportEvent event) { + return !event.isCancelled() && event.getTo() != null && event.getTo().getWorld() != null; } } diff --git a/src/test/java/fr/xephi/authme/util/TeleportationServiceTest.java b/src/test/java/fr/xephi/authme/util/TeleportationServiceTest.java index 6309bfa9..b39fc9cf 100644 --- a/src/test/java/fr/xephi/authme/util/TeleportationServiceTest.java +++ b/src/test/java/fr/xephi/authme/util/TeleportationServiceTest.java @@ -133,6 +133,7 @@ public class TeleportationServiceTest { Player player = mock(Player.class); given(player.hasPlayedBefore()).willReturn(false); given(player.isOnline()).willReturn(true); + given(player.getWorld()).willReturn(mock(World.class)); given(settings.getProperty(RestrictionSettings.TELEPORT_UNAUTHED_TO_SPAWN)).willReturn(false); given(settings.getProperty(RestrictionSettings.FORCE_SPAWN_LOCATION_AFTER_LOGIN)).willReturn(false); given(spawnLoader.getFirstSpawn()).willReturn(null); @@ -250,15 +251,15 @@ public class TeleportationServiceTest { public void shouldTeleportPlayerToSpawnAfterLogin() { // given given(settings.getProperty(RestrictionSettings.FORCE_SPAWN_LOCATION_AFTER_LOGIN)).willReturn(true); - World world = mock(World.class); - given(world.getName()).willReturn("forced1"); Player player = mock(Player.class); - given(player.getWorld()).willReturn(world); given(player.isOnline()).willReturn(true); Location spawn = mockLocation(); given(spawnLoader.getSpawnLocation(player)).willReturn(spawn); PlayerAuth auth = mock(PlayerAuth.class); LimboPlayer limbo = mock(LimboPlayer.class); + Location limboLocation = mockLocation(); + given(limboLocation.getWorld().getName()).willReturn("forced1"); + given(limbo.getLoc()).willReturn(limboLocation); // when teleportationService.teleportOnLogin(player, auth, limbo); @@ -274,15 +275,15 @@ public class TeleportationServiceTest { // given given(settings.getProperty(RestrictionSettings.FORCE_SPAWN_LOCATION_AFTER_LOGIN)).willReturn(true); given(settings.getProperty(RestrictionSettings.TELEPORT_UNAUTHED_TO_SPAWN)).willReturn(false); - World world = mock(World.class); - given(world.getName()).willReturn("Forced1"); // different case Player player = mock(Player.class); - given(player.getWorld()).willReturn(world); given(player.isOnline()).willReturn(true); Location spawn = mockLocation(); given(spawnLoader.getSpawnLocation(player)).willReturn(spawn); PlayerAuth auth = mock(PlayerAuth.class); LimboPlayer limbo = mock(LimboPlayer.class); + Location limboLocation = mockLocation(); + given(limboLocation.getWorld().getName()).willReturn("Forced1"); // different case + given(limbo.getLoc()).willReturn(limboLocation); // when teleportationService.teleportOnLogin(player, auth, limbo); @@ -307,6 +308,8 @@ public class TeleportationServiceTest { Player player = mock(Player.class); given(player.isOnline()).willReturn(true); LimboPlayer limbo = mock(LimboPlayer.class); + Location limboLocation = mockLocation(); + given(limbo.getLoc()).willReturn(limboLocation); // when teleportationService.teleportOnLogin(player, auth, limbo); @@ -334,6 +337,8 @@ public class TeleportationServiceTest { World world = mock(World.class); given(player.getWorld()).willReturn(world); LimboPlayer limbo = mock(LimboPlayer.class); + Location limboLocation = mockLocation(); + given(limbo.getLoc()).willReturn(limboLocation); // when teleportationService.teleportOnLogin(player, auth, limbo); From 347d7bcf46236eb4e72af00f5f2ac778d05d6995 Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Sun, 12 Jun 2016 14:29:16 +0200 Subject: [PATCH 176/200] Update messages_de.yml Thanks to @Platinteufel --- src/main/resources/messages/messages_de.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/resources/messages/messages_de.yml b/src/main/resources/messages/messages_de.yml index 870bf6a9..cef029ca 100644 --- a/src/main/resources/messages/messages_de.yml +++ b/src/main/resources/messages/messages_de.yml @@ -11,8 +11,7 @@ login: '&2Erfolgreich eingeloggt!' vb_nonActiv: '&cDein Account wurde noch nicht aktiviert. Bitte prüfe deine E-Mails!' user_regged: '&cDieser Benutzername ist schon vergeben' usage_reg: '&cBenutze: /register ' -# TODO max_reg: Missing tag %reg_names -max_reg: '&cDu hast die maximale Anzahl an Accounts erreicht (%max_acc/%reg_count).' +max_reg: '&cDu hast die maximale Anzahl an Accounts erreicht (%reg_count/%max_acc %reg_names).' no_perm: '&4Du hast keine Rechte, um diese Aktion auszuführen!' error: '&4Ein Fehler ist aufgetreten. Bitte kontaktiere einen Administrator.' login_msg: '&cBitte logge dich ein mit "/login "' @@ -65,4 +64,4 @@ invalid_name_case: 'Dein registrierter Benutzername ist &2%valid&f - nicht &4%in not_owner_error: 'Du bist nicht der Besitzer dieses Accounts. Bitte wähle einen anderen Namen!' denied_chat: '&cDu musst eingeloggt sein, um chatten zu können!' same_ip_online: 'Ein Spieler mit derselben IP ist bereits online!' -# TODO denied_command: '&cIn order to use this command you must be authenticated!' \ No newline at end of file +denied_command: '&cUm diesen Befehl zu nutzen musst du authentifiziert sein!' From d6e1fd5ceb4c5e3a85e82faf89259bba74c381d5 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Sun, 12 Jun 2016 16:14:06 +0200 Subject: [PATCH 177/200] Use injection in and for LimboCache, migrate some legacy settings, remove setGroup from Utils - New injector method allows to retrieve services if they've already been instantiated -> useful for onDisable() which might be run after aborted initialization - Deprecate various methods that need to be removed --- src/main/java/fr/xephi/authme/AuthMe.java | 79 +++++++++---------- .../xephi/authme/cache/limbo/LimboCache.java | 52 ++++++------ .../authme/UnregisterAdminCommand.java | 7 +- .../AuthMeServiceInitializer.java | 18 ++++- .../authme/permission/AuthGroupType.java | 20 +++++ .../authme/permission/PermissionsManager.java | 59 ++++++++++++++ .../xephi/authme/process/ProcessService.java | 5 ++ .../authme/process/join/AsynchronousJoin.java | 11 +-- .../process/login/AsynchronousLogin.java | 14 +++- .../process/login/ProcessSyncPlayerLogin.java | 7 +- .../process/logout/AsynchronousLogout.java | 4 +- .../ProcessSynchronousPlayerLogout.java | 9 +-- .../register/ProcessSyncEmailRegister.java | 3 +- .../register/ProcessSyncPasswordRegister.java | 19 +++-- .../unregister/AsynchronousUnregister.java | 8 +- .../fr/xephi/authme/settings/Settings.java | 17 +--- .../fr/xephi/authme/settings/SpawnLoader.java | 17 ++-- src/main/java/fr/xephi/authme/util/Utils.java | 71 ----------------- .../HashAlgorithmIntegrationTest.java | 4 +- 19 files changed, 226 insertions(+), 198 deletions(-) create mode 100644 src/main/java/fr/xephi/authme/permission/AuthGroupType.java diff --git a/src/main/java/fr/xephi/authme/AuthMe.java b/src/main/java/fr/xephi/authme/AuthMe.java index 9f29ff86..058c21bd 100644 --- a/src/main/java/fr/xephi/authme/AuthMe.java +++ b/src/main/java/fr/xephi/authme/AuthMe.java @@ -47,6 +47,7 @@ import fr.xephi.authme.settings.properties.HooksSettings; import fr.xephi.authme.settings.properties.PluginSettings; import fr.xephi.authme.settings.properties.PurgeSettings; import fr.xephi.authme.settings.properties.RestrictionSettings; +import fr.xephi.authme.settings.properties.SecuritySettings; import fr.xephi.authme.settings.properties.SettingsFieldRetriever; import fr.xephi.authme.settings.propertymap.PropertyMap; import fr.xephi.authme.task.PurgeTask; @@ -113,7 +114,6 @@ public class AuthMe extends JavaPlugin { public DataManager dataManager; /* * Private instances - * TODO #432: Move instantiation and management of these services */ // TODO #604: Encapsulate ProtocolLib members public AuthMeInventoryPacketAdapter inventoryProtector; @@ -131,12 +131,14 @@ public class AuthMe extends JavaPlugin { private SpawnLoader spawnLoader; private boolean autoPurging; private BukkitService bukkitService; + private AuthMeServiceInitializer initializer; /** * Get the plugin's instance. * * @return AuthMe */ + @Deprecated public static AuthMe getInstance() { return plugin; } @@ -168,24 +170,6 @@ public class AuthMe extends JavaPlugin { return pluginBuildNumber; } - /** - * Get the Messages instance. - * - * @return Plugin's messages. - */ - public Messages getMessages() { - return messages; - } - - /** - * Get the plugin's NewSetting instance. - * - * @return NewSetting. - */ - public NewSetting getSettings() { - return newSettings; - } - // Get version and build number of the plugin private void setPluginInfos() { String versionRaw = this.getDescription().getVersion(); @@ -240,7 +224,7 @@ public class AuthMe extends JavaPlugin { MigrationService.changePlainTextToSha256(newSettings, database, new SHA256()); - AuthMeServiceInitializer initializer = new AuthMeServiceInitializer("fr.xephi.authme"); + initializer = new AuthMeServiceInitializer("fr.xephi.authme"); // Register elements of the Bukkit / JavaPlugin environment initializer.register(AuthMe.class, this); initializer.register(Server.class, getServer()); @@ -255,7 +239,11 @@ public class AuthMe extends JavaPlugin { // Some statically injected things initializer.register(PlayerCache.class, PlayerCache.getInstance()); - initializer.register(LimboCache.class, LimboCache.getInstance()); + + // Note ljacqu 20160612: Instantiate LimboCache first to make sure it is instantiated + // (because sometimes it's used via LimboCache.getInstance()) + // Once LimboCache#getInstance() no longer exists this can be removed! + initializer.get(LimboCache.class); permsMan = initializer.get(PermissionsManager.class); bukkitService = initializer.get(BukkitService.class); @@ -431,7 +419,7 @@ public class AuthMe extends JavaPlugin { * Set up the console filter. */ private void setupConsoleFilter() { - if (Settings.removePassword) { + if (newSettings.getProperty(SecuritySettings.REMOVE_PASSWORD_FROM_CONSOLE)) { ConsoleFilter filter = new ConsoleFilter(); getLogger().setFilter(filter); Bukkit.getLogger().setFilter(filter); @@ -449,10 +437,13 @@ public class AuthMe extends JavaPlugin { @Override public void onDisable() { // Save player data - if (bukkitService != null) { + BukkitService bukkitService = initializer.getIfAvailable(BukkitService.class); + LimboCache limboCache = initializer.getIfAvailable(LimboCache.class); + + if (bukkitService != null && limboCache != null) { Collection players = bukkitService.getOnlinePlayers(); for (Player player : players) { - savePlayer(player); + savePlayer(player, limboCache); } } @@ -495,8 +486,6 @@ public class AuthMe extends JavaPlugin { } }, "AuthMe-DataSource#close").start(); - // Close the database - // Disabled correctly ConsoleLogger.info("AuthMe " + this.getDescription().getVersion() + " disabled!"); ConsoleLogger.close(); @@ -614,7 +603,7 @@ public class AuthMe extends JavaPlugin { } // Save Player Data - private void savePlayer(Player player) { + private void savePlayer(Player player, LimboCache limboCache) { if (safeIsNpc(player) || Utils.isUnrestricted(player)) { return; } @@ -626,8 +615,8 @@ public class AuthMe extends JavaPlugin { .location(player.getLocation()).build(); database.updateQuitLoc(auth); } - if (LimboCache.getInstance().hasLimboPlayer(name)) { - LimboPlayer limbo = LimboCache.getInstance().getLimboPlayer(name); + if (limboCache.hasLimboPlayer(name)) { + LimboPlayer limbo = limboCache.getLimboPlayer(name); if (!Settings.noTeleport) { player.teleport(limbo.getLoc()); } @@ -635,7 +624,7 @@ public class AuthMe extends JavaPlugin { Utils.addNormal(player, limbo.getGroup()); player.setOp(limbo.isOperator()); limbo.getTimeoutTask().cancel(); - LimboCache.getInstance().deleteLimboPlayer(name); + limboCache.deleteLimboPlayer(name); if (this.playerBackup.doesCacheExist(player)) { this.playerBackup.removeCache(player); } @@ -713,17 +702,7 @@ public class AuthMe extends JavaPlugin { .replace("{COUNTRY}", GeoLiteAPI.getCountryName(ipAddress)); } - public boolean isLoggedIp(String name, String ip) { - int count = 0; - for (Player player : bukkitService.getOnlinePlayers()) { - if (ip.equalsIgnoreCase(Utils.getPlayerIp(player)) - && database.isLogged(player.getName().toLowerCase()) - && !player.getName().equalsIgnoreCase(name)) { - ++count; - } - } - return count >= Settings.getMaxLoginPerIp; - } + /** * Handle Bukkit commands. @@ -757,6 +736,24 @@ public class AuthMe extends JavaPlugin { // Service getters (deprecated) // Use @Inject fields instead // ------------- + /** + * @return Plugin's messages. + * @deprecated should be used in API classes only (temporarily) + */ + @Deprecated + public Messages getMessages() { + return messages; + } + + /** + * @return NewSetting + * @deprecated should be used in API classes only (temporarily) + */ + @Deprecated + public NewSetting getSettings() { + return newSettings; + } + /** * @return permission manager * @deprecated should be used in API classes only (temporarily) diff --git a/src/main/java/fr/xephi/authme/cache/limbo/LimboCache.java b/src/main/java/fr/xephi/authme/cache/limbo/LimboCache.java index 6822af3a..7f471ded 100644 --- a/src/main/java/fr/xephi/authme/cache/limbo/LimboCache.java +++ b/src/main/java/fr/xephi/authme/cache/limbo/LimboCache.java @@ -1,45 +1,47 @@ package fr.xephi.authme.cache.limbo; -import fr.xephi.authme.AuthMe; import fr.xephi.authme.cache.backup.JsonCache; import fr.xephi.authme.cache.backup.PlayerData; import fr.xephi.authme.permission.PermissionsManager; +import fr.xephi.authme.settings.SpawnLoader; import org.bukkit.Location; import org.bukkit.entity.Player; +import javax.inject.Inject; import java.util.concurrent.ConcurrentHashMap; import static com.google.common.base.Preconditions.checkNotNull; /** + * Manages all {@link LimboPlayer} instances. */ public class LimboCache { + @Deprecated // TODO ljacqu 20160612: Remove this field private volatile static LimboCache singleton; - private final ConcurrentHashMap cache; - private final AuthMe plugin; - private final JsonCache jsonCache; - /** - * Constructor for LimboCache. - * - * @param plugin AuthMe - */ - private LimboCache(AuthMe plugin) { - this.plugin = plugin; - this.cache = new ConcurrentHashMap<>(); - this.jsonCache = new JsonCache(); + private final ConcurrentHashMap cache = new ConcurrentHashMap<>(); + private final JsonCache jsonCache = new JsonCache(); + + @Inject + private PermissionsManager permissionsManager; + @Inject + private SpawnLoader spawnLoader; + + @Inject + LimboCache(PermissionsManager permissionsManager, SpawnLoader spawnLoader) { + this.permissionsManager = permissionsManager; + this.spawnLoader = spawnLoader; + + singleton = this; } /** - * Method getInstance. - * - * @return LimboCache + * @return LimboCache instance + * @deprecated Inject the instance properly instead */ + @Deprecated public static LimboCache getInstance() { - if (singleton == null) { - singleton = new LimboCache(AuthMe.getInstance()); - } return singleton; } @@ -50,13 +52,12 @@ public class LimboCache { */ public void addLimboPlayer(Player player) { String name = player.getName().toLowerCase(); - Location loc = player.getLocation(); + Location location = player.isDead() ? spawnLoader.getSpawnLocation(player) : player.getLocation(); boolean operator = player.isOp(); boolean flyEnabled = player.getAllowFlight(); String playerGroup = ""; - PermissionsManager permsMan = plugin.getPermissionsManager(); - if (permsMan.hasGroupSupport()) { - playerGroup = permsMan.getPrimaryGroup(player); + if (permissionsManager.hasGroupSupport()) { + playerGroup = permissionsManager.getPrimaryGroup(player); } if (jsonCache.doesCacheExist(player)) { @@ -68,11 +69,8 @@ public class LimboCache { } } - if (player.isDead()) { - loc = plugin.getSpawnLocation(player); - } - cache.put(name, new LimboPlayer(name, loc, operator, playerGroup, flyEnabled)); + cache.put(name, new LimboPlayer(name, location, operator, playerGroup, flyEnabled)); } /** diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/UnregisterAdminCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/UnregisterAdminCommand.java index 341666f0..382f14b6 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/UnregisterAdminCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/UnregisterAdminCommand.java @@ -8,6 +8,8 @@ import fr.xephi.authme.command.CommandService; import fr.xephi.authme.command.ExecutableCommand; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.output.MessageKey; +import fr.xephi.authme.permission.AuthGroupType; +import fr.xephi.authme.permission.PermissionsManager; import fr.xephi.authme.settings.properties.RegistrationSettings; import fr.xephi.authme.settings.properties.RestrictionSettings; import fr.xephi.authme.task.MessageTask; @@ -48,6 +50,9 @@ public class UnregisterAdminCommand implements ExecutableCommand { @Inject private LimboCache limboCache; + @Inject + private PermissionsManager permissionsManager; + @Override public void executeCommand(final CommandSender sender, List arguments) { // Get the player name @@ -69,7 +74,7 @@ public class UnregisterAdminCommand implements ExecutableCommand { // Unregister the player Player target = bukkitService.getPlayerExact(playerNameLowerCase); playerCache.removePlayer(playerNameLowerCase); - Utils.setGroup(target, Utils.GroupType.UNREGISTERED); + permissionsManager.setGroup(target, AuthGroupType.UNREGISTERED); if (target != null && target.isOnline()) { if (commandService.getProperty(RegistrationSettings.FORCE)) { applyUnregisteredEffectsAndTasks(target); diff --git a/src/main/java/fr/xephi/authme/initialization/AuthMeServiceInitializer.java b/src/main/java/fr/xephi/authme/initialization/AuthMeServiceInitializer.java index 9213772f..29e5adcf 100644 --- a/src/main/java/fr/xephi/authme/initialization/AuthMeServiceInitializer.java +++ b/src/main/java/fr/xephi/authme/initialization/AuthMeServiceInitializer.java @@ -96,8 +96,22 @@ public class AuthMeServiceInitializer { } /** - * Returns an instance of the given class or the value associated with an annotation, - * by retrieving it or by instantiating it if not yet present. + * Returns an instance of the given class if available. This simply returns the instance if present and + * otherwise {@code null}. Calling this method will not instantiate anything. + * + * @param clazz the class to retrieve the instance for + * @param the class' type + * @return instance or null if none available + */ + public T getIfAvailable(Class clazz) { + if (Annotation.class.isAssignableFrom(clazz)) { + throw new UnsupportedOperationException("Annotations may not be retrieved in this way!"); + } + return clazz.cast(objects.get(clazz)); + } + + /** + * Returns an instance of the given class by retrieving it or by instantiating it if not yet present. * * @param clazz the class to retrieve a value for * @param traversedClasses the list of traversed classes diff --git a/src/main/java/fr/xephi/authme/permission/AuthGroupType.java b/src/main/java/fr/xephi/authme/permission/AuthGroupType.java new file mode 100644 index 00000000..9ab2a370 --- /dev/null +++ b/src/main/java/fr/xephi/authme/permission/AuthGroupType.java @@ -0,0 +1,20 @@ +package fr.xephi.authme.permission; + +/** + * Represents the group type based on the user's auth status. + */ +public enum AuthGroupType { + + /** Player does not have an account. */ + UNREGISTERED, + + /** Registered? */ + REGISTERED, // TODO #761: Remove this or the NOT_LOGGED_IN one + + /** Player is registered and not logged in. */ + NOT_LOGGED_IN, + + /** Player is logged in. */ + LOGGED_IN + +} diff --git a/src/main/java/fr/xephi/authme/permission/PermissionsManager.java b/src/main/java/fr/xephi/authme/permission/PermissionsManager.java index c4efc757..fb3e2fca 100644 --- a/src/main/java/fr/xephi/authme/permission/PermissionsManager.java +++ b/src/main/java/fr/xephi/authme/permission/PermissionsManager.java @@ -1,6 +1,8 @@ package fr.xephi.authme.permission; import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.cache.limbo.LimboCache; +import fr.xephi.authme.cache.limbo.LimboPlayer; import fr.xephi.authme.permission.handlers.BPermissionsHandler; import fr.xephi.authme.permission.handlers.GroupManagerHandler; import fr.xephi.authme.permission.handlers.PermissionHandler; @@ -8,6 +10,7 @@ import fr.xephi.authme.permission.handlers.PermissionsBukkitHandler; import fr.xephi.authme.permission.handlers.PermissionsExHandler; import fr.xephi.authme.permission.handlers.VaultHandler; import fr.xephi.authme.permission.handlers.ZPermissionsHandler; +import fr.xephi.authme.settings.Settings; import net.milkbowl.vault.permission.Permission; import org.anjocaido.groupmanager.GroupManager; import org.bukkit.Bukkit; @@ -23,6 +26,7 @@ import ru.tehkode.permissions.bukkit.PermissionsEx; import javax.annotation.PostConstruct; import javax.inject.Inject; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; /** @@ -349,6 +353,61 @@ public class PermissionsManager { return result; } + /** + * Set the group of a player, by its AuthMe group type. + * + * @param player The player. + * @param group The group type. + * + * @return True if succeeded, false otherwise. False is also returned if groups aren't supported + * with the current permissions system. + */ + public boolean setGroup(Player player, AuthGroupType group) { + // Check whether the permissions check is enabled + if (!isEnabled() || !Settings.isPermissionCheckEnabled) { + return false; + } + + // Make sure group support is available + if (!handler.hasGroupSupport()) { + ConsoleLogger.showError("The current permissions system doesn't have group support, unable to set group!"); + return false; + } + + switch (group) { + case UNREGISTERED: + // Remove the other group type groups, set the current group + removeGroups(player, Arrays.asList(Settings.getRegisteredGroup, Settings.getUnloggedinGroup)); + return addGroup(player, Settings.unRegisteredGroup); + + case REGISTERED: + // Remove the other group type groups, set the current group + removeGroups(player, Arrays.asList(Settings.unRegisteredGroup, Settings.getUnloggedinGroup)); + return addGroup(player, Settings.getRegisteredGroup); + + case NOT_LOGGED_IN: + // Remove the other group type groups, set the current group + removeGroups(player, Arrays.asList(Settings.unRegisteredGroup, Settings.getRegisteredGroup)); + return addGroup(player, Settings.getUnloggedinGroup); + + case LOGGED_IN: + // Get the limbo player data + LimboPlayer limbo = LimboCache.getInstance().getLimboPlayer(player.getName().toLowerCase()); + if (limbo == null) + return false; + + // Get the players group + String realGroup = limbo.getGroup(); + + // Remove the other group types groups, set the real group + removeGroups(player, Arrays.asList(Settings.unRegisteredGroup, Settings.getRegisteredGroup, Settings.getUnloggedinGroup)); + return addGroup(player, realGroup); + + default: + return false; + } + } + /** * Remove the permission group of a player, if supported. * diff --git a/src/main/java/fr/xephi/authme/process/ProcessService.java b/src/main/java/fr/xephi/authme/process/ProcessService.java index 775344d7..be762200 100644 --- a/src/main/java/fr/xephi/authme/process/ProcessService.java +++ b/src/main/java/fr/xephi/authme/process/ProcessService.java @@ -2,6 +2,7 @@ package fr.xephi.authme.process; import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.output.Messages; +import fr.xephi.authme.permission.AuthGroupType; import fr.xephi.authme.permission.PermissionNode; import fr.xephi.authme.permission.PermissionsManager; import fr.xephi.authme.settings.NewSetting; @@ -102,4 +103,8 @@ public class ProcessService { return permissionsManager.hasPermission(player, node); } + public boolean setGroup(Player player, AuthGroupType group) { + return permissionsManager.setGroup(player, group); + } + } diff --git a/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java b/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java index 591fa9b7..63622d5f 100644 --- a/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java +++ b/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java @@ -10,10 +10,10 @@ import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.events.ProtectInventoryEvent; import fr.xephi.authme.hooks.PluginHooks; import fr.xephi.authme.output.MessageKey; +import fr.xephi.authme.permission.AuthGroupType; import fr.xephi.authme.permission.PlayerStatePermission; import fr.xephi.authme.process.AsynchronousProcess; import fr.xephi.authme.process.ProcessService; -import fr.xephi.authme.util.TeleportationService; import fr.xephi.authme.settings.properties.HooksSettings; import fr.xephi.authme.settings.properties.PluginSettings; import fr.xephi.authme.settings.properties.RegistrationSettings; @@ -22,8 +22,8 @@ import fr.xephi.authme.settings.properties.SecuritySettings; import fr.xephi.authme.task.MessageTask; import fr.xephi.authme.task.TimeoutTask; import fr.xephi.authme.util.BukkitService; +import fr.xephi.authme.util.TeleportationService; import fr.xephi.authme.util.Utils; -import fr.xephi.authme.util.Utils.GroupType; import org.apache.commons.lang.reflect.MethodUtils; import org.bukkit.GameMode; import org.bukkit.entity.LivingEntity; @@ -35,6 +35,7 @@ import org.bukkit.scheduler.BukkitTask; import javax.inject.Inject; import static fr.xephi.authme.settings.properties.RestrictionSettings.PROTECT_INVENTORY_BEFORE_LOGIN; +import static fr.xephi.authme.util.BukkitService.TICKS_PER_SECOND; public class AsynchronousJoin implements AsynchronousProcess { @@ -116,7 +117,7 @@ public class AsynchronousJoin implements AsynchronousProcess { final boolean isAuthAvailable = database.isAuthAvailable(name); if (isAuthAvailable) { - Utils.setGroup(player, GroupType.NOTLOGGEDIN); + service.setGroup(player, AuthGroupType.NOT_LOGGED_IN); teleportationService.teleportOnJoin(player); limboCache.updateLimboPlayer(player); @@ -153,7 +154,7 @@ public class AsynchronousJoin implements AsynchronousProcess { // Not Registered // Groups logic - Utils.setGroup(player, GroupType.UNREGISTERED); + service.setGroup(player, AuthGroupType.UNREGISTERED); // Skip if registration is optional if (!service.getProperty(RegistrationSettings.FORCE)) { @@ -168,7 +169,7 @@ public class AsynchronousJoin implements AsynchronousProcess { limboCache.addLimboPlayer(player); } - final int registrationTimeout = service.getProperty(RestrictionSettings.TIMEOUT) * 20; + final int registrationTimeout = service.getProperty(RestrictionSettings.TIMEOUT) * TICKS_PER_SECOND; bukkitService.scheduleSyncDelayedTask(new Runnable() { @Override diff --git a/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java b/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java index 52dd4a6e..2e22150b 100644 --- a/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java +++ b/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java @@ -123,7 +123,7 @@ public class AsynchronousLogin implements AsynchronousProcess { if (service.getProperty(RestrictionSettings.MAX_LOGIN_PER_IP) > 0 && !permissionsManager.hasPermission(player, PlayerStatePermission.ALLOW_MULTIPLE_ACCOUNTS) && !"127.0.0.1".equalsIgnoreCase(ip) && !"localhost".equalsIgnoreCase(ip)) { - if (plugin.isLoggedIp(name, ip)) { + if (isLoggedIp(name, ip)) { service.send(player, MessageKey.ALREADY_LOGGED_IN_ERROR); return null; } @@ -254,4 +254,16 @@ public class AsynchronousLogin implements AsynchronousProcess { } } } + + private boolean isLoggedIp(String name, String ip) { + int count = 0; + for (Player player : bukkitService.getOnlinePlayers()) { + if (ip.equalsIgnoreCase(Utils.getPlayerIp(player)) + && database.isLogged(player.getName().toLowerCase()) + && !player.getName().equalsIgnoreCase(name)) { + ++count; + } + } + return count >= service.getProperty(RestrictionSettings.MAX_LOGIN_PER_IP); + } } diff --git a/src/main/java/fr/xephi/authme/process/login/ProcessSyncPlayerLogin.java b/src/main/java/fr/xephi/authme/process/login/ProcessSyncPlayerLogin.java index 373afd94..d040d49f 100644 --- a/src/main/java/fr/xephi/authme/process/login/ProcessSyncPlayerLogin.java +++ b/src/main/java/fr/xephi/authme/process/login/ProcessSyncPlayerLogin.java @@ -10,15 +10,14 @@ import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.events.LoginEvent; import fr.xephi.authme.events.RestoreInventoryEvent; import fr.xephi.authme.listener.AuthMePlayerListener; +import fr.xephi.authme.permission.AuthGroupType; import fr.xephi.authme.process.ProcessService; import fr.xephi.authme.process.SynchronousProcess; -import fr.xephi.authme.util.TeleportationService; import fr.xephi.authme.settings.properties.HooksSettings; import fr.xephi.authme.settings.properties.RegistrationSettings; import fr.xephi.authme.settings.properties.RestrictionSettings; import fr.xephi.authme.util.BukkitService; -import fr.xephi.authme.util.Utils; -import fr.xephi.authme.util.Utils.GroupType; +import fr.xephi.authme.util.TeleportationService; import org.apache.commons.lang.reflect.MethodUtils; import org.bukkit.Bukkit; import org.bukkit.entity.LivingEntity; @@ -95,7 +94,7 @@ public class ProcessSyncPlayerLogin implements SynchronousProcess { if (limbo != null) { // Restore Op state and Permission Group restoreOpState(player, limbo); - Utils.setGroup(player, GroupType.LOGGEDIN); + service.setGroup(player, AuthGroupType.LOGGED_IN); teleportationService.teleportOnLogin(player, auth, limbo); diff --git a/src/main/java/fr/xephi/authme/process/logout/AsynchronousLogout.java b/src/main/java/fr/xephi/authme/process/logout/AsynchronousLogout.java index e3ff2f76..fc81017d 100644 --- a/src/main/java/fr/xephi/authme/process/logout/AsynchronousLogout.java +++ b/src/main/java/fr/xephi/authme/process/logout/AsynchronousLogout.java @@ -5,12 +5,12 @@ import fr.xephi.authme.cache.auth.PlayerCache; import fr.xephi.authme.cache.limbo.LimboCache; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.output.MessageKey; +import fr.xephi.authme.permission.AuthGroupType; import fr.xephi.authme.process.AsynchronousProcess; import fr.xephi.authme.process.ProcessService; import fr.xephi.authme.process.SyncProcessManager; import fr.xephi.authme.util.BukkitService; import fr.xephi.authme.util.Utils; -import fr.xephi.authme.util.Utils.GroupType; import org.bukkit.entity.Player; import javax.inject.Inject; @@ -63,7 +63,7 @@ public class AsynchronousLogout implements AsynchronousProcess { limboCache.deleteLimboPlayer(name); } limboCache.addLimboPlayer(player); - Utils.setGroup(player, GroupType.NOTLOGGEDIN); + service.setGroup(player, AuthGroupType.NOT_LOGGED_IN); syncProcessManager.processSyncPlayerLogout(player); } } diff --git a/src/main/java/fr/xephi/authme/process/logout/ProcessSynchronousPlayerLogout.java b/src/main/java/fr/xephi/authme/process/logout/ProcessSynchronousPlayerLogout.java index 60f0391f..6fbd8ce6 100644 --- a/src/main/java/fr/xephi/authme/process/logout/ProcessSynchronousPlayerLogout.java +++ b/src/main/java/fr/xephi/authme/process/logout/ProcessSynchronousPlayerLogout.java @@ -10,13 +10,12 @@ import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.process.ProcessService; import fr.xephi.authme.process.SynchronousProcess; import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.properties.HooksSettings; import fr.xephi.authme.settings.properties.RegistrationSettings; import fr.xephi.authme.settings.properties.RestrictionSettings; import fr.xephi.authme.task.MessageTask; import fr.xephi.authme.task.TimeoutTask; import fr.xephi.authme.util.BukkitService; - -import org.bukkit.Bukkit; import org.bukkit.entity.Player; import org.bukkit.potion.PotionEffect; import org.bukkit.potion.PotionEffectType; @@ -53,7 +52,7 @@ public class ProcessSynchronousPlayerLogout implements SynchronousProcess { } private void restoreSpeedEffect(Player player) { - if (Settings.isRemoveSpeedEnabled) { + if (service.getProperty(RestrictionSettings.REMOVE_SPEED)) { player.setWalkSpeed(0.0F); player.setFlySpeed(0.0F); } @@ -86,8 +85,8 @@ public class ProcessSynchronousPlayerLogout implements SynchronousProcess { player.setOp(false); restoreSpeedEffect(player); // Player is now logout... Time to fire event ! - Bukkit.getServer().getPluginManager().callEvent(new LogoutEvent(player)); - if (Settings.bungee) { + bukkitService.callEvent(new LogoutEvent(player)); + if (service.getProperty(HooksSettings.BUNGEECORD)) { sendBungeeMessage(player); } service.send(player, MessageKey.LOGOUT_SUCCESS); diff --git a/src/main/java/fr/xephi/authme/process/register/ProcessSyncEmailRegister.java b/src/main/java/fr/xephi/authme/process/register/ProcessSyncEmailRegister.java index 84d767ae..146ffb46 100644 --- a/src/main/java/fr/xephi/authme/process/register/ProcessSyncEmailRegister.java +++ b/src/main/java/fr/xephi/authme/process/register/ProcessSyncEmailRegister.java @@ -5,6 +5,7 @@ import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.cache.limbo.LimboCache; import fr.xephi.authme.cache.limbo.LimboPlayer; import fr.xephi.authme.output.MessageKey; +import fr.xephi.authme.permission.AuthGroupType; import fr.xephi.authme.process.ProcessService; import fr.xephi.authme.process.SynchronousProcess; import fr.xephi.authme.settings.Settings; @@ -43,7 +44,7 @@ public class ProcessSyncEmailRegister implements SynchronousProcess { final String name = player.getName().toLowerCase(); LimboPlayer limbo = limboCache.getLimboPlayer(name); if (!Settings.getRegisteredGroup.isEmpty()) { - Utils.setGroup(player, Utils.GroupType.REGISTERED); + service.setGroup(player, AuthGroupType.REGISTERED); } service.send(player, MessageKey.ACCOUNT_NOT_ACTIVATED); int time = service.getProperty(RestrictionSettings.TIMEOUT) * TICKS_PER_SECOND; diff --git a/src/main/java/fr/xephi/authme/process/register/ProcessSyncPasswordRegister.java b/src/main/java/fr/xephi/authme/process/register/ProcessSyncPasswordRegister.java index cf6bba2b..34fc4c26 100644 --- a/src/main/java/fr/xephi/authme/process/register/ProcessSyncPasswordRegister.java +++ b/src/main/java/fr/xephi/authme/process/register/ProcessSyncPasswordRegister.java @@ -9,8 +9,9 @@ import fr.xephi.authme.cache.limbo.LimboPlayer; import fr.xephi.authme.events.LoginEvent; import fr.xephi.authme.events.RestoreInventoryEvent; import fr.xephi.authme.output.MessageKey; -import fr.xephi.authme.process.SynchronousProcess; +import fr.xephi.authme.permission.AuthGroupType; import fr.xephi.authme.process.ProcessService; +import fr.xephi.authme.process.SynchronousProcess; import fr.xephi.authme.settings.Settings; import fr.xephi.authme.settings.properties.EmailSettings; import fr.xephi.authme.settings.properties.HooksSettings; @@ -44,6 +45,9 @@ public class ProcessSyncPasswordRegister implements SynchronousProcess { @Inject private BukkitService bukkitService; + @Inject + private LimboCache limboCache; + ProcessSyncPasswordRegister() { } private void sendBungeeMessage(Player player) { @@ -73,18 +77,17 @@ public class ProcessSyncPasswordRegister implements SynchronousProcess { private void requestLogin(Player player) { final String name = player.getName().toLowerCase(); Utils.teleportToSpawn(player); - LimboCache cache = LimboCache.getInstance(); - cache.updateLimboPlayer(player); + limboCache.updateLimboPlayer(player); int delay = service.getProperty(RestrictionSettings.TIMEOUT) * TICKS_PER_SECOND; int interval = service.getProperty(RegistrationSettings.MESSAGE_INTERVAL); BukkitTask task; if (delay != 0) { task = bukkitService.runTaskLater(new TimeoutTask(plugin, name, player), delay); - cache.getLimboPlayer(name).setTimeoutTask(task); + limboCache.getLimboPlayer(name).setTimeoutTask(task); } task = bukkitService.runTask(new MessageTask(bukkitService, plugin.getMessages(), name, MessageKey.LOGIN_MESSAGE, interval)); - cache.getLimboPlayer(name).setMessageTask(task); + limboCache.getLimboPlayer(name).setMessageTask(task); if (player.isInsideVehicle() && player.getVehicle() != null) { player.getVehicle().eject(); } @@ -92,7 +95,7 @@ public class ProcessSyncPasswordRegister implements SynchronousProcess { public void processPasswordRegister(Player player) { final String name = player.getName().toLowerCase(); - LimboPlayer limbo = LimboCache.getInstance().getLimboPlayer(name); + LimboPlayer limbo = limboCache.getLimboPlayer(name); if (limbo != null) { if (service.getProperty(RestrictionSettings.HIDE_TABLIST_BEFORE_LOGIN) && plugin.tablistHider != null) { plugin.tablistHider.sendTablist(player); @@ -108,11 +111,11 @@ public class ProcessSyncPasswordRegister implements SynchronousProcess { } } - LimboCache.getInstance().deleteLimboPlayer(name); + limboCache.deleteLimboPlayer(name); } if (!Settings.getRegisteredGroup.isEmpty()) { - Utils.setGroup(player, Utils.GroupType.REGISTERED); + service.setGroup(player, AuthGroupType.REGISTERED); } service.send(player, MessageKey.REGISTER_SUCCESS); diff --git a/src/main/java/fr/xephi/authme/process/unregister/AsynchronousUnregister.java b/src/main/java/fr/xephi/authme/process/unregister/AsynchronousUnregister.java index 60c45f23..59e88d89 100644 --- a/src/main/java/fr/xephi/authme/process/unregister/AsynchronousUnregister.java +++ b/src/main/java/fr/xephi/authme/process/unregister/AsynchronousUnregister.java @@ -8,6 +8,7 @@ import fr.xephi.authme.cache.limbo.LimboCache; import fr.xephi.authme.cache.limbo.LimboPlayer; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.output.MessageKey; +import fr.xephi.authme.permission.AuthGroupType; import fr.xephi.authme.process.AsynchronousProcess; import fr.xephi.authme.process.ProcessService; import fr.xephi.authme.security.PasswordSecurity; @@ -18,7 +19,6 @@ import fr.xephi.authme.task.MessageTask; import fr.xephi.authme.task.TimeoutTask; import fr.xephi.authme.util.BukkitService; import fr.xephi.authme.util.Utils; -import fr.xephi.authme.util.Utils.GroupType; import org.bukkit.entity.Player; import org.bukkit.potion.PotionEffect; import org.bukkit.potion.PotionEffectType; @@ -62,12 +62,12 @@ public class AsynchronousUnregister implements AsynchronousProcess { return; } int timeOut = service.getProperty(RestrictionSettings.TIMEOUT) * TICKS_PER_SECOND; - if (Settings.isForcedRegistrationEnabled) { + if (service.getProperty(RegistrationSettings.FORCE)) { Utils.teleportToSpawn(player); player.saveData(); playerCache.removePlayer(player.getName().toLowerCase()); if (!Settings.getRegisteredGroup.isEmpty()) { - Utils.setGroup(player, GroupType.UNREGISTERED); + service.setGroup(player, AuthGroupType.UNREGISTERED); } limboCache.addLimboPlayer(player); LimboPlayer limboPlayer = limboCache.getLimboPlayer(name); @@ -83,7 +83,7 @@ public class AsynchronousUnregister implements AsynchronousProcess { return; } if (!Settings.unRegisteredGroup.isEmpty()) { - Utils.setGroup(player, Utils.GroupType.UNREGISTERED); + service.setGroup(player, AuthGroupType.UNREGISTERED); } playerCache.removePlayer(name); diff --git a/src/main/java/fr/xephi/authme/settings/Settings.java b/src/main/java/fr/xephi/authme/settings/Settings.java index c86cd83b..b3980665 100644 --- a/src/main/java/fr/xephi/authme/settings/Settings.java +++ b/src/main/java/fr/xephi/authme/settings/Settings.java @@ -14,22 +14,18 @@ import java.util.List; /** * Old settings manager. See {@link NewSetting} for the new manager. */ +@Deprecated public final class Settings { public static List getUnrestrictedName; - public static List getForcedWorlds; public static boolean isPermissionCheckEnabled; - public static boolean isForcedRegistrationEnabled; public static boolean isTeleportToSpawnEnabled; public static boolean isSessionsEnabled; public static boolean isAllowRestrictedIp; - public static boolean isForceSpawnLocOnJoinEnabled; public static boolean isSaveQuitLocationEnabled; public static boolean protectInventoryBeforeLogInEnabled; public static boolean isStopEnabled; public static boolean reloadSupport; - public static boolean removePassword; - public static boolean multiverse; public static boolean bungee; public static boolean forceRegLogin; public static boolean noTeleport; @@ -41,9 +37,6 @@ public final class Settings { public static String crazyloginFileName; public static int getSessionTimeout; public static int getNonActivatedGroup; - public static int maxLoginTry; - public static int captchaLength; - public static int getMaxLoginPerIp; private static FileConfiguration configFile; /** @@ -58,13 +51,11 @@ public final class Settings { private static void loadVariables() { isPermissionCheckEnabled = load(PluginSettings.ENABLE_PERMISSION_CHECK); - isForcedRegistrationEnabled = load(RegistrationSettings.FORCE); isTeleportToSpawnEnabled = load(RestrictionSettings.TELEPORT_UNAUTHED_TO_SPAWN); isSessionsEnabled = load(PluginSettings.SESSIONS_ENABLED); getSessionTimeout = configFile.getInt("settings.sessions.timeout", 10); isAllowRestrictedIp = load(RestrictionSettings.ENABLE_RESTRICTED_USERS); isRemoveSpeedEnabled = load(RestrictionSettings.REMOVE_SPEED); - isForceSpawnLocOnJoinEnabled = load(RestrictionSettings.FORCE_SPAWN_LOCATION_AFTER_LOGIN); isSaveQuitLocationEnabled = load(RestrictionSettings.SAVE_QUIT_LOCATION); getUnloggedinGroup = load(SecuritySettings.UNLOGGEDIN_GROUP); getNonActivatedGroup = configFile.getInt("ExternalBoardOptions.nonActivedUserGroup", -1); @@ -74,15 +65,9 @@ public final class Settings { protectInventoryBeforeLogInEnabled = load(RestrictionSettings.PROTECT_INVENTORY_BEFORE_LOGIN); isStopEnabled = configFile.getBoolean("Security.SQLProblem.stopServer", true); reloadSupport = configFile.getBoolean("Security.ReloadCommand.useReloadCommandSupport", true); - removePassword = configFile.getBoolean("Security.console.removePassword", true); - maxLoginTry = configFile.getInt("Security.captcha.maxLoginTry", 5); - captchaLength = configFile.getInt("Security.captcha.captchaLength", 5); - multiverse = load(HooksSettings.MULTIVERSE); bungee = load(HooksSettings.BUNGEECORD); - getForcedWorlds = load(RestrictionSettings.FORCE_SPAWN_ON_WORLDS); defaultWorld = configFile.getString("Purge.defaultWorld", "world"); forceRegLogin = load(RegistrationSettings.FORCE_LOGIN_AFTER_REGISTER); - getMaxLoginPerIp = load(RestrictionSettings.MAX_LOGIN_PER_IP); noTeleport = load(RestrictionSettings.NO_TELEPORT); crazyloginFileName = configFile.getString("Converter.CrazyLogin.fileName", "accounts.db"); } diff --git a/src/main/java/fr/xephi/authme/settings/SpawnLoader.java b/src/main/java/fr/xephi/authme/settings/SpawnLoader.java index 212e0235..ecb1d379 100644 --- a/src/main/java/fr/xephi/authme/settings/SpawnLoader.java +++ b/src/main/java/fr/xephi/authme/settings/SpawnLoader.java @@ -6,7 +6,8 @@ import fr.xephi.authme.cache.auth.PlayerCache; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.hooks.PluginHooks; import fr.xephi.authme.initialization.DataFolder; -import fr.xephi.authme.initialization.SettingsDependent; +import fr.xephi.authme.initialization.Reloadable; +import fr.xephi.authme.settings.properties.HooksSettings; import fr.xephi.authme.settings.properties.RestrictionSettings; import fr.xephi.authme.util.FileUtils; import fr.xephi.authme.util.StringUtils; @@ -29,9 +30,10 @@ import java.io.IOException; * should be taken from. In AuthMe, we can distinguish between the regular spawn and a "first spawn", * to which players will be teleported who have joined for the first time. */ -public class SpawnLoader implements SettingsDependent { +public class SpawnLoader implements Reloadable { private final File authMeConfigurationFile; + private final NewSetting settings; private final PluginHooks pluginHooks; private final DataSource dataSource; private FileConfiguration authMeConfiguration; @@ -53,18 +55,17 @@ public class SpawnLoader implements SettingsDependent { // TODO ljacqu 20160312: Check if resource could be copied and handle the case if not FileUtils.copyFileFromResource(spawnFile, "spawn.yml"); this.authMeConfigurationFile = new File(pluginFolder, "spawn.yml"); + this.settings = settings; this.pluginHooks = pluginHooks; this.dataSource = dataSource; - loadSettings(settings); + reload(); } /** - * Retrieve the relevant settings and load the AuthMe spawn.yml file. - * - * @param settings The settings instance + * (Re)loads the spawn file and relevant settings. */ @Override - public void loadSettings(NewSetting settings) { + public void reload() { spawnPriority = settings.getProperty(RestrictionSettings.SPAWN_PRIORITY).split(","); authMeConfiguration = YamlConfiguration.loadConfiguration(authMeConfigurationFile); loadEssentialsSpawn(); @@ -159,7 +160,7 @@ public class SpawnLoader implements SettingsDependent { } break; case "multiverse": - if (Settings.multiverse) { + if (settings.getProperty(HooksSettings.MULTIVERSE)) { spawnLoc = pluginHooks.getMultiverseSpawn(world); } break; diff --git a/src/main/java/fr/xephi/authme/util/Utils.java b/src/main/java/fr/xephi/authme/util/Utils.java index de935bb3..ae940656 100644 --- a/src/main/java/fr/xephi/authme/util/Utils.java +++ b/src/main/java/fr/xephi/authme/util/Utils.java @@ -2,8 +2,6 @@ package fr.xephi.authme.util; import fr.xephi.authme.AuthMe; import fr.xephi.authme.ConsoleLogger; -import fr.xephi.authme.cache.limbo.LimboCache; -import fr.xephi.authme.cache.limbo.LimboPlayer; import fr.xephi.authme.events.AuthMeTeleportEvent; import fr.xephi.authme.permission.PermissionsManager; import fr.xephi.authme.settings.Settings; @@ -23,68 +21,6 @@ public final class Utils { private Utils() { } - /** - * Set the group of a player, by its AuthMe group type. - * - * @param player The player. - * @param group The group type. - * - * @return True if succeeded, false otherwise. False is also returned if groups aren't supported - * with the current permissions system. - */ - public static boolean setGroup(Player player, GroupType group) { - // Check whether the permissions check is enabled - if (!Settings.isPermissionCheckEnabled) { - return false; - } - - // Get the permissions manager, and make sure it's valid - PermissionsManager permsMan = plugin.getPermissionsManager(); - if (permsMan == null) { - ConsoleLogger.showError("Failed to access permissions manager instance, shutting down."); - return false; - } - - // Make sure group support is available - if (!permsMan.hasGroupSupport()) { - ConsoleLogger.showError("The current permissions system doesn't have group support, unable to set group!"); - return false; - } - - switch (group) { - case UNREGISTERED: - // Remove the other group type groups, set the current group - permsMan.removeGroups(player, Arrays.asList(Settings.getRegisteredGroup, Settings.getUnloggedinGroup)); - return permsMan.addGroup(player, Settings.unRegisteredGroup); - - case REGISTERED: - // Remove the other group type groups, set the current group - permsMan.removeGroups(player, Arrays.asList(Settings.unRegisteredGroup, Settings.getUnloggedinGroup)); - return permsMan.addGroup(player, Settings.getRegisteredGroup); - - case NOTLOGGEDIN: - // Remove the other group type groups, set the current group - permsMan.removeGroups(player, Arrays.asList(Settings.unRegisteredGroup, Settings.getRegisteredGroup)); - return permsMan.addGroup(player, Settings.getUnloggedinGroup); - - case LOGGEDIN: - // Get the limbo player data - LimboPlayer limbo = LimboCache.getInstance().getLimboPlayer(player.getName().toLowerCase()); - if (limbo == null) - return false; - - // Get the players group - String realGroup = limbo.getGroup(); - - // Remove the other group types groups, set the real group - permsMan.removeGroups(player, Arrays.asList(Settings.unRegisteredGroup, Settings.getRegisteredGroup, Settings.getUnloggedinGroup)); - return permsMan.addGroup(player, realGroup); - - default: - return false; - } - } - /** * TODO: This method requires better explanation. *

@@ -143,13 +79,6 @@ public final class Utils { } } - public enum GroupType { - UNREGISTERED, - REGISTERED, - NOTLOGGEDIN, - LOGGEDIN - } - /** * Returns the IP of the given player. * diff --git a/src/test/java/fr/xephi/authme/security/HashAlgorithmIntegrationTest.java b/src/test/java/fr/xephi/authme/security/HashAlgorithmIntegrationTest.java index 30940c34..79745a33 100644 --- a/src/test/java/fr/xephi/authme/security/HashAlgorithmIntegrationTest.java +++ b/src/test/java/fr/xephi/authme/security/HashAlgorithmIntegrationTest.java @@ -27,7 +27,7 @@ public class HashAlgorithmIntegrationTest { private static AuthMeServiceInitializer initializer; @BeforeClass - public static void setUpWrapper() { + public static void setUpConfigAndInjector() { NewSetting settings = mock(NewSetting.class); given(settings.getProperty(HooksSettings.BCRYPT_LOG2_ROUND)).willReturn(8); given(settings.getProperty(SecuritySettings.DOUBLE_MD5_SALT_LENGTH)).willReturn(16); @@ -52,7 +52,7 @@ public class HashAlgorithmIntegrationTest { } @Test - public void shouldBeAbleToInstantiateEncryptionAlgorithms() throws InstantiationException, IllegalAccessException { + public void shouldBeAbleToInstantiateEncryptionAlgorithms() { // given / when / then for (HashAlgorithm algorithm : HashAlgorithm.values()) { if (!HashAlgorithm.CUSTOM.equals(algorithm) && !HashAlgorithm.PLAINTEXT.equals(algorithm)) { From ac4add9f54a34b2ec9e66ca10d5a5cacf18880ea Mon Sep 17 00:00:00 2001 From: Gnat008 Date: Sun, 12 Jun 2016 13:40:34 -0400 Subject: [PATCH 178/200] add ability to tempban users after x wrong logins - ref #520 #192 --- .../fr/xephi/authme/cache/TempbanManager.java | 115 ++++++++++++++++++ .../fr/xephi/authme/output/MessageKey.java | 4 +- .../process/login/AsynchronousLogin.java | 25 +++- .../settings/properties/SecuritySettings.java | 13 ++ src/main/resources/config.yml | 8 ++ src/main/resources/messages/messages_en.yml | 1 + .../authme/cache/TempbanManagerTest.java | 114 +++++++++++++++++ 7 files changed, 277 insertions(+), 3 deletions(-) create mode 100644 src/main/java/fr/xephi/authme/cache/TempbanManager.java create mode 100644 src/test/java/fr/xephi/authme/cache/TempbanManagerTest.java diff --git a/src/main/java/fr/xephi/authme/cache/TempbanManager.java b/src/main/java/fr/xephi/authme/cache/TempbanManager.java new file mode 100644 index 00000000..30f94502 --- /dev/null +++ b/src/main/java/fr/xephi/authme/cache/TempbanManager.java @@ -0,0 +1,115 @@ +package fr.xephi.authme.cache; + +import fr.xephi.authme.initialization.SettingsDependent; +import fr.xephi.authme.output.MessageKey; +import fr.xephi.authme.output.Messages; +import fr.xephi.authme.settings.NewSetting; +import fr.xephi.authme.settings.properties.SecuritySettings; +import fr.xephi.authme.util.BukkitService; +import fr.xephi.authme.util.Utils; +import org.bukkit.BanList; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import javax.inject.Inject; +import java.util.Date; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Manager for handling tempbans + */ +public class TempbanManager implements SettingsDependent { + + private final ConcurrentHashMap playerCounts; + + private final long MINUTE_IN_MILLISECONDS = 60000; + + @Inject + private BukkitService bukkitService; + + @Inject + private Messages messages; + + private boolean isEnabled; + private int threshold; + private int length; + + @Inject + TempbanManager(NewSetting settings) { + this.playerCounts = new ConcurrentHashMap<>(); + loadSettings(settings); + } + + /** + * Increases the failure count for the given player. + * + * @param name the player's name + */ + public void increaseCount(String name) { + if (isEnabled) { + String nameLower = name.toLowerCase(); + Integer count = playerCounts.get(nameLower); + + if (count == null) { + playerCounts.put(nameLower, 1); + } else { + playerCounts.put(nameLower, count + 1); + } + } + } + + public void resetCount(String name) { + if (isEnabled) { + playerCounts.remove(name.toLowerCase()); + } + } + + /** + * Return whether the player should be tempbanned. + * + * @param name The player's name + * @return True if the player should be tempbanned + */ + public boolean shouldTempban(String name) { + if (isEnabled) { + Integer count = playerCounts.get(name.toLowerCase()); + return count != null && count >= threshold; + } + + return false; + } + + /** + * Tempban a player for failing to log in too many times. + * This bans the player's IP address, and calculates the expire + * time based on the time the method was called. + * + * @param player The player to tempban + */ + public void tempbanPlayer(Player player) { + if (isEnabled) { + resetCount(player.getName()); + + final String ip = Utils.getPlayerIp(player); + final String reason = messages.retrieveSingle(MessageKey.TEMPBAN_MAX_LOGINS); + + final Date expires = new Date(); + long newTime = expires.getTime() + (length * MINUTE_IN_MILLISECONDS); + expires.setTime(newTime); + + bukkitService.scheduleSyncDelayedTask(new Runnable() { + @Override + public void run() { + Bukkit.getServer().getBanList(BanList.Type.IP).addBan(ip, reason, expires, "AuthMe"); + } + }); + } + } + + @Override + public void loadSettings(NewSetting settings) { + this.isEnabled = settings.getProperty(SecuritySettings.TEMPBAN_ON_MAX_LOGINS); + this.threshold = settings.getProperty(SecuritySettings.MAX_LOGIN_TEMPBAN); + this.length = settings.getProperty(SecuritySettings.TEMPBAN_LENGTH); + } +} diff --git a/src/main/java/fr/xephi/authme/output/MessageKey.java b/src/main/java/fr/xephi/authme/output/MessageKey.java index 9cfa960d..7bd27621 100644 --- a/src/main/java/fr/xephi/authme/output/MessageKey.java +++ b/src/main/java/fr/xephi/authme/output/MessageKey.java @@ -137,7 +137,9 @@ public enum MessageKey { NOT_OWNER_ERROR("not_owner_error"), - INVALID_NAME_CASE("invalid_name_case", "%valid", "%invalid"); + INVALID_NAME_CASE("invalid_name_case", "%valid", "%invalid"), + + TEMPBAN_MAX_LOGINS("tempban_max_logins"); private String key; private String[] tags; diff --git a/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java b/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java index 2e22150b..5905010a 100644 --- a/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java +++ b/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java @@ -3,6 +3,7 @@ package fr.xephi.authme.process.login; import fr.xephi.authme.AuthMe; import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.cache.CaptchaManager; +import fr.xephi.authme.cache.TempbanManager; import fr.xephi.authme.cache.auth.PlayerAuth; import fr.xephi.authme.cache.auth.PlayerCache; import fr.xephi.authme.cache.limbo.LimboCache; @@ -69,6 +70,9 @@ public class AsynchronousLogin implements AsynchronousProcess { @Inject private CaptchaManager captchaManager; + @Inject + private TempbanManager tempbanManager; + AsynchronousLogin() { } /** @@ -84,6 +88,19 @@ public class AsynchronousLogin implements AsynchronousProcess { return captchaManager.isCaptchaRequired(playerName); } + /** + * Queries the {@link fr.xephi.authme.cache.TempbanManager} to + * see if the player has reached the tempban threshold. + * + * @param player The player to check + * @return True if the player needs to be tempbanned + */ + private boolean shouldTempban(Player player) { + final String playerName = player.getName(); + + return tempbanManager.shouldTempban(playerName); + } + /** * Checks the precondition for authentication (like user known) and returns * the playerAuth-State @@ -146,9 +163,10 @@ public class AsynchronousLogin implements AsynchronousProcess { final String name = player.getName().toLowerCase(); final String ip = Utils.getPlayerIp(player); - // Increase the count here before knowing the result of the login. + // Increase the counts here before knowing the result of the login. // If the login is successful, we clear the count for the player. captchaManager.increaseCount(name); + tempbanManager.increaseCount(name); if ("127.0.0.1".equals(pAuth.getIp()) && !pAuth.getIp().equals(ip)) { pAuth.setIp(ip); @@ -169,6 +187,7 @@ public class AsynchronousLogin implements AsynchronousProcess { database.updateSession(auth); captchaManager.resetCounts(name); + tempbanManager.resetCount(name); player.setNoDamageTicks(0); if (!forceLogin) @@ -214,7 +233,9 @@ public class AsynchronousLogin implements AsynchronousProcess { player.kickPlayer(service.retrieveSingleMessage(MessageKey.WRONG_PASSWORD)); } }); - } else { + } else if (shouldTempban(player)) { + tempbanManager.tempbanPlayer(player); + } else { service.send(player, MessageKey.WRONG_PASSWORD); // Check again if a captcha is required to log in diff --git a/src/main/java/fr/xephi/authme/settings/properties/SecuritySettings.java b/src/main/java/fr/xephi/authme/settings/properties/SecuritySettings.java index 49398d69..365f4a9a 100644 --- a/src/main/java/fr/xephi/authme/settings/properties/SecuritySettings.java +++ b/src/main/java/fr/xephi/authme/settings/properties/SecuritySettings.java @@ -100,6 +100,19 @@ public class SecuritySettings implements SettingsClass { public static final Property> UNSAFE_PASSWORDS = newLowercaseListProperty("settings.security.unsafePasswords", "123456", "password", "qwerty", "12345", "54321"); + @Comment("Tempban users if they enter the wrong password too many times") + public static final Property TEMPBAN_ON_MAX_LOGINS = + newProperty("Security.tempban.enableTempban", false); + + @Comment("How many times a user can attempt to login before being tempbanned") + public static final Property MAX_LOGIN_TEMPBAN = + newProperty("Security.tempban.maxLoginTries", 10); + + @Comment({"The length of time a player will be tempbanned in minutes", + "Default: 480 minutes, or 8 hours"}) + public static final Property TEMPBAN_LENGTH = + newProperty("Security.tempban.tempbanLength", 480); + private SecuritySettings() { } diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 1a9582c2..e7c67763 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -331,6 +331,14 @@ Security: # Kick players before stopping the server, that allow us to save position of players, and all needed # information correctly without any corruption. kickPlayersBeforeStopping: true + tempban: + # Tempban users if they enter the wrong password too many times + enableTempban: false + # How many times a user can attempt to login before being tempbanned + maxLoginTries: 10 + # The length of time a player will be tempbanned in minutes + # Default: 480 minutes, or 8 hours + tempbanLength: 480 Converter: Rakamak: # Rakamak file name diff --git a/src/main/resources/messages/messages_en.yml b/src/main/resources/messages/messages_en.yml index 1bb5d3ff..aabbae89 100644 --- a/src/main/resources/messages/messages_en.yml +++ b/src/main/resources/messages/messages_en.yml @@ -65,3 +65,4 @@ email_already_used: '&4The email address is already being used' two_factor_create: '&2Your secret code is %code. You can scan it from here %url' not_owner_error: 'You are not the owner of this account. Please choose another name!' invalid_name_case: 'You should join using username %valid, not %invalid.' +tempban_max_logins: '&cYou have been temporarily banned for failing to log in too many times.' diff --git a/src/test/java/fr/xephi/authme/cache/TempbanManagerTest.java b/src/test/java/fr/xephi/authme/cache/TempbanManagerTest.java new file mode 100644 index 00000000..ab6fd50d --- /dev/null +++ b/src/test/java/fr/xephi/authme/cache/TempbanManagerTest.java @@ -0,0 +1,114 @@ +package fr.xephi.authme.cache; + +import fr.xephi.authme.ReflectionTestUtils; +import fr.xephi.authme.settings.NewSetting; +import fr.xephi.authme.settings.properties.SecuritySettings; +import org.junit.Test; + +import java.util.Map; + +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 TempbanManager}. + */ +public class TempbanManagerTest { + + @Test + public void shouldAddCounts() { + // given + NewSetting settings = mockSettings(3, 60); + TempbanManager manager = new TempbanManager(settings); + String player = "Tester"; + + // when + for (int i = 0; i < 2; ++i) { + manager.increaseCount(player); + } + + // then + assertThat(manager.shouldTempban(player), equalTo(false)); + manager.increaseCount(player); + assertThat(manager.shouldTempban(player), equalTo(true)); + assertThat(manager.shouldTempban("otherPlayer"), equalTo(false)); + } + + @Test + public void shouldIncreaseAndResetCount() { + // given + String player = "plaYah"; + NewSetting settings = mockSettings(3, 60); + TempbanManager manager = new TempbanManager(settings); + + // when + manager.increaseCount(player); + manager.increaseCount(player); + manager.increaseCount(player); + + // then + assertThat(manager.shouldTempban(player), equalTo(true)); + assertHasCount(manager, player, 3); + + // when 2 + manager.resetCount(player); + + // then 2 + assertThat(manager.shouldTempban(player), equalTo(false)); + assertHasCount(manager, player, null); + } + + @Test + public void shouldNotIncreaseCountForDisabledTempban() { + // given + String player = "playah"; + NewSetting settings = mockSettings(1, 5); + given(settings.getProperty(SecuritySettings.TEMPBAN_ON_MAX_LOGINS)).willReturn(false); + TempbanManager manager = new TempbanManager(settings); + + // when + manager.increaseCount(player); + + // then + assertThat(manager.shouldTempban(player), equalTo(false)); + assertHasCount(manager, player, null); + } + + @Test + public void shouldNotCheckCountIfTempbanIsDisabled() { + // given + String player = "playah"; + NewSetting settings = mockSettings(1, 5); + TempbanManager manager = new TempbanManager(settings); + given(settings.getProperty(SecuritySettings.TEMPBAN_ON_MAX_LOGINS)).willReturn(false); + + // when + manager.increaseCount(player); + // assumptions + assertThat(manager.shouldTempban(player), equalTo(true)); + assertHasCount(manager, player, 1); + // end assumptions + manager.loadSettings(settings); + boolean result = manager.shouldTempban(player); + + // then + assertThat(result, equalTo(false)); + } + + private static NewSetting mockSettings(int maxTries, int tempbanLength) { + NewSetting settings = mock(NewSetting.class); + given(settings.getProperty(SecuritySettings.TEMPBAN_ON_MAX_LOGINS)).willReturn(true); + given(settings.getProperty(SecuritySettings.MAX_LOGIN_TEMPBAN)).willReturn(maxTries); + given(settings.getProperty(SecuritySettings.TEMPBAN_LENGTH)).willReturn(tempbanLength); + return settings; + } + + private static void assertHasCount(TempbanManager manager, String player, Integer count) { + @SuppressWarnings("unchecked") + Map playerCounts = (Map) ReflectionTestUtils + .getFieldValue(TempbanManager.class, manager, "playerCounts"); + assertThat(playerCounts.get(player.toLowerCase()), equalTo(count)); + } +} From c3d391aeafb268ef83f2cea492559c77b27a083b Mon Sep 17 00:00:00 2001 From: Gnat008 Date: Sun, 12 Jun 2016 14:53:18 -0400 Subject: [PATCH 179/200] lets only inject in one place (moved injects to constructor) --- src/main/java/fr/xephi/authme/cache/TempbanManager.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/fr/xephi/authme/cache/TempbanManager.java b/src/main/java/fr/xephi/authme/cache/TempbanManager.java index 30f94502..aa41c473 100644 --- a/src/main/java/fr/xephi/authme/cache/TempbanManager.java +++ b/src/main/java/fr/xephi/authme/cache/TempbanManager.java @@ -11,6 +11,7 @@ import org.bukkit.BanList; import org.bukkit.Bukkit; import org.bukkit.entity.Player; +import javax.annotation.PostConstruct; import javax.inject.Inject; import java.util.Date; import java.util.concurrent.ConcurrentHashMap; @@ -24,10 +25,8 @@ public class TempbanManager implements SettingsDependent { private final long MINUTE_IN_MILLISECONDS = 60000; - @Inject private BukkitService bukkitService; - @Inject private Messages messages; private boolean isEnabled; @@ -36,7 +35,7 @@ public class TempbanManager implements SettingsDependent { @Inject TempbanManager(NewSetting settings) { - this.playerCounts = new ConcurrentHashMap<>(); + playerCounts = new ConcurrentHashMap<>(); loadSettings(settings); } From 69100daba523f18b1b63625bc92e9e81b5233d45 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Sun, 12 Jun 2016 20:54:39 +0200 Subject: [PATCH 180/200] Get jar files via URI objects in tests to prevent issue with spaces --- src/test/java/fr/xephi/authme/TestHelper.java | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/test/java/fr/xephi/authme/TestHelper.java b/src/test/java/fr/xephi/authme/TestHelper.java index e9cf35ba..907cb8e2 100644 --- a/src/test/java/fr/xephi/authme/TestHelper.java +++ b/src/test/java/fr/xephi/authme/TestHelper.java @@ -8,6 +8,8 @@ import java.io.File; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Modifier; +import java.net.URI; +import java.net.URISyntaxException; import java.net.URL; import java.nio.file.Path; import java.nio.file.Paths; @@ -31,8 +33,8 @@ public final class TestHelper { * @return The project file */ public static File getJarFile(String path) { - URL url = getUrlOrThrow(path); - return new File(url.getFile()); + URI uri = getUriOrThrow(path); + return new File(uri.getPath()); } /** @@ -42,7 +44,7 @@ public final class TestHelper { * @return The Path object to the file */ public static Path getJarPath(String path) { - String sqlFilePath = getUrlOrThrow(path).getPath(); + String sqlFilePath = getUriOrThrow(path).getPath(); // Windows preprends the path with a '/' or '\', which Paths cannot handle String appropriatePath = System.getProperty("os.name").contains("indow") ? sqlFilePath.substring(1) @@ -50,12 +52,16 @@ public final class TestHelper { return Paths.get(appropriatePath); } - private static URL getUrlOrThrow(String path) { + private static URI getUriOrThrow(String path) { URL url = TestHelper.class.getResource(path); if (url == null) { throw new IllegalStateException("File '" + path + "' could not be loaded"); } - return url; + try { + return new URI(url.toString()); + } catch (URISyntaxException e) { + throw new IllegalStateException("File '" + path + "' cannot be converted to a URI"); + } } /** From 67c72dc46dda4401b55c642989725fe9284454f4 Mon Sep 17 00:00:00 2001 From: Gnat008 Date: Sun, 12 Jun 2016 15:17:37 -0400 Subject: [PATCH 181/200] fix TempbanManager injecting --- .../fr/xephi/authme/cache/TempbanManager.java | 6 ++++-- .../authme/cache/TempbanManagerTest.java | 20 +++++++++++++++---- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/main/java/fr/xephi/authme/cache/TempbanManager.java b/src/main/java/fr/xephi/authme/cache/TempbanManager.java index aa41c473..6372d257 100644 --- a/src/main/java/fr/xephi/authme/cache/TempbanManager.java +++ b/src/main/java/fr/xephi/authme/cache/TempbanManager.java @@ -34,8 +34,10 @@ public class TempbanManager implements SettingsDependent { private int length; @Inject - TempbanManager(NewSetting settings) { - playerCounts = new ConcurrentHashMap<>(); + TempbanManager(BukkitService bukkitService, Messages messages, NewSetting settings) { + this.playerCounts = new ConcurrentHashMap<>(); + this.bukkitService = bukkitService; + this.messages = messages; loadSettings(settings); } diff --git a/src/test/java/fr/xephi/authme/cache/TempbanManagerTest.java b/src/test/java/fr/xephi/authme/cache/TempbanManagerTest.java index ab6fd50d..bf02253b 100644 --- a/src/test/java/fr/xephi/authme/cache/TempbanManagerTest.java +++ b/src/test/java/fr/xephi/authme/cache/TempbanManagerTest.java @@ -1,9 +1,14 @@ package fr.xephi.authme.cache; import fr.xephi.authme.ReflectionTestUtils; +import fr.xephi.authme.output.Messages; import fr.xephi.authme.settings.NewSetting; import fr.xephi.authme.settings.properties.SecuritySettings; +import fr.xephi.authme.util.BukkitService; import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; import java.util.Map; @@ -15,13 +20,20 @@ import static org.mockito.Mockito.mock; /** * Test for {@link TempbanManager}. */ +@RunWith(MockitoJUnitRunner.class) public class TempbanManagerTest { + @Mock + BukkitService bukkitService; + + @Mock + Messages messages; + @Test public void shouldAddCounts() { // given NewSetting settings = mockSettings(3, 60); - TempbanManager manager = new TempbanManager(settings); + TempbanManager manager = new TempbanManager(bukkitService, messages, settings); String player = "Tester"; // when @@ -41,7 +53,7 @@ public class TempbanManagerTest { // given String player = "plaYah"; NewSetting settings = mockSettings(3, 60); - TempbanManager manager = new TempbanManager(settings); + TempbanManager manager = new TempbanManager(bukkitService, messages, settings); // when manager.increaseCount(player); @@ -66,7 +78,7 @@ public class TempbanManagerTest { String player = "playah"; NewSetting settings = mockSettings(1, 5); given(settings.getProperty(SecuritySettings.TEMPBAN_ON_MAX_LOGINS)).willReturn(false); - TempbanManager manager = new TempbanManager(settings); + TempbanManager manager = new TempbanManager(bukkitService, messages, settings); // when manager.increaseCount(player); @@ -81,7 +93,7 @@ public class TempbanManagerTest { // given String player = "playah"; NewSetting settings = mockSettings(1, 5); - TempbanManager manager = new TempbanManager(settings); + TempbanManager manager = new TempbanManager(bukkitService, messages, settings); given(settings.getProperty(SecuritySettings.TEMPBAN_ON_MAX_LOGINS)).willReturn(false); // when From 89bbfc48ee3b2363197ddafea31797e785eb8dfa Mon Sep 17 00:00:00 2001 From: ljacqu Date: Sun, 12 Jun 2016 21:59:26 +0200 Subject: [PATCH 182/200] #723 Create provider for TimeoutTask and MessageTask, remove LimboCache#getInstance - Create class to handle the creation of "LimboPlayer tasks" (adds encapsulation, reduces duplication) - Move group setting into its own class because (mutual dependency between LimboCache and PermissionsManager otherwise) --- .../xephi/authme/cache/limbo/LimboCache.java | 14 -- .../authme/UnregisterAdminCommand.java | 35 ++- .../authme/permission/AuthGroupHandler.java | 84 +++++++ .../authme/permission/PermissionsManager.java | 65 +----- .../xephi/authme/process/ProcessService.java | 6 +- .../authme/process/join/AsynchronousJoin.java | 29 +-- .../process/login/AsynchronousLogin.java | 26 +-- .../ProcessSynchronousPlayerLogout.java | 31 +-- .../register/ProcessSyncEmailRegister.java | 31 +-- .../register/ProcessSyncPasswordRegister.java | 23 +- .../unregister/AsynchronousUnregister.java | 27 +-- .../authme/task/LimboPlayerTaskManager.java | 94 ++++++++ .../fr/xephi/authme/task/MessageTask.java | 38 ++- .../fr/xephi/authme/task/TimeoutTask.java | 26 +-- .../task/LimboPlayerTaskManagerTest.java | 219 ++++++++++++++++++ .../tools/dependencygraph/DrawDependency.java | 3 +- 16 files changed, 508 insertions(+), 243 deletions(-) create mode 100644 src/main/java/fr/xephi/authme/permission/AuthGroupHandler.java create mode 100644 src/main/java/fr/xephi/authme/task/LimboPlayerTaskManager.java create mode 100644 src/test/java/fr/xephi/authme/task/LimboPlayerTaskManagerTest.java diff --git a/src/main/java/fr/xephi/authme/cache/limbo/LimboCache.java b/src/main/java/fr/xephi/authme/cache/limbo/LimboCache.java index 7f471ded..97670a88 100644 --- a/src/main/java/fr/xephi/authme/cache/limbo/LimboCache.java +++ b/src/main/java/fr/xephi/authme/cache/limbo/LimboCache.java @@ -17,9 +17,6 @@ import static com.google.common.base.Preconditions.checkNotNull; */ public class LimboCache { - @Deprecated // TODO ljacqu 20160612: Remove this field - private volatile static LimboCache singleton; - private final ConcurrentHashMap cache = new ConcurrentHashMap<>(); private final JsonCache jsonCache = new JsonCache(); @@ -32,17 +29,6 @@ public class LimboCache { LimboCache(PermissionsManager permissionsManager, SpawnLoader spawnLoader) { this.permissionsManager = permissionsManager; this.spawnLoader = spawnLoader; - - singleton = this; - } - - /** - * @return LimboCache instance - * @deprecated Inject the instance properly instead - */ - @Deprecated - public static LimboCache getInstance() { - return singleton; } /** diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/UnregisterAdminCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/UnregisterAdminCommand.java index 382f14b6..a743d9fe 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/UnregisterAdminCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/UnregisterAdminCommand.java @@ -8,19 +8,17 @@ import fr.xephi.authme.command.CommandService; import fr.xephi.authme.command.ExecutableCommand; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.output.MessageKey; +import fr.xephi.authme.permission.AuthGroupHandler; import fr.xephi.authme.permission.AuthGroupType; -import fr.xephi.authme.permission.PermissionsManager; import fr.xephi.authme.settings.properties.RegistrationSettings; import fr.xephi.authme.settings.properties.RestrictionSettings; -import fr.xephi.authme.task.MessageTask; -import fr.xephi.authme.task.TimeoutTask; +import fr.xephi.authme.task.LimboPlayerTaskManager; import fr.xephi.authme.util.BukkitService; import fr.xephi.authme.util.Utils; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.bukkit.potion.PotionEffect; import org.bukkit.potion.PotionEffectType; -import org.bukkit.scheduler.BukkitTask; import javax.inject.Inject; import java.util.List; @@ -51,7 +49,11 @@ public class UnregisterAdminCommand implements ExecutableCommand { private LimboCache limboCache; @Inject - private PermissionsManager permissionsManager; + private LimboPlayerTaskManager limboPlayerTaskManager; + + @Inject + private AuthGroupHandler authGroupHandler; + @Override public void executeCommand(final CommandSender sender, List arguments) { @@ -74,7 +76,7 @@ public class UnregisterAdminCommand implements ExecutableCommand { // Unregister the player Player target = bukkitService.getPlayerExact(playerNameLowerCase); playerCache.removePlayer(playerNameLowerCase); - permissionsManager.setGroup(target, AuthGroupType.UNREGISTERED); + authGroupHandler.setGroup(target, AuthGroupType.UNREGISTERED); if (target != null && target.isOnline()) { if (commandService.getProperty(RegistrationSettings.FORCE)) { applyUnregisteredEffectsAndTasks(target); @@ -95,22 +97,17 @@ public class UnregisterAdminCommand implements ExecutableCommand { * @param target the player that was unregistered */ private void applyUnregisteredEffectsAndTasks(Player target) { - final String playerNameLowerCase = target.getName().toLowerCase(); - + // TODO ljacqu 20160612: Remove use of Utils method and behave according to settings Utils.teleportToSpawn(target); - limboCache.addLimboPlayer(target); - int timeOut = commandService.getProperty(RestrictionSettings.TIMEOUT) * TICKS_PER_SECOND; - int interval = commandService.getProperty(RegistrationSettings.MESSAGE_INTERVAL); - if (timeOut != 0) { - BukkitTask id = bukkitService.runTaskLater(new TimeoutTask(authMe, playerNameLowerCase, target), timeOut); - limboCache.getLimboPlayer(playerNameLowerCase).setTimeoutTask(id); - } - limboCache.getLimboPlayer(playerNameLowerCase).setMessageTask( - bukkitService.runTask(new MessageTask(bukkitService, authMe.getMessages(), - playerNameLowerCase, MessageKey.REGISTER_MESSAGE, interval))); + limboCache.addLimboPlayer(target); + limboPlayerTaskManager.registerTimeoutTask(target); + limboPlayerTaskManager.registerMessageTask(target.getName(), + MessageKey.REGISTER_MESSAGE); + + final int timeout = commandService.getProperty(RestrictionSettings.TIMEOUT) * TICKS_PER_SECOND; if (commandService.getProperty(RegistrationSettings.APPLY_BLIND_EFFECT)) { - target.addPotionEffect(new PotionEffect(PotionEffectType.BLINDNESS, timeOut, 2)); + target.addPotionEffect(new PotionEffect(PotionEffectType.BLINDNESS, timeout, 2)); } } } diff --git a/src/main/java/fr/xephi/authme/permission/AuthGroupHandler.java b/src/main/java/fr/xephi/authme/permission/AuthGroupHandler.java new file mode 100644 index 00000000..c7557fc2 --- /dev/null +++ b/src/main/java/fr/xephi/authme/permission/AuthGroupHandler.java @@ -0,0 +1,84 @@ +package fr.xephi.authme.permission; + +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.cache.limbo.LimboCache; +import fr.xephi.authme.cache.limbo.LimboPlayer; +import fr.xephi.authme.settings.NewSetting; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.properties.PluginSettings; +import org.bukkit.entity.Player; + +import javax.inject.Inject; +import java.util.Arrays; + +/** + * Changes the permission group according to the auth status of the player and the configuration. + */ +public class AuthGroupHandler { + + @Inject + private PermissionsManager permissionsManager; + + @Inject + private NewSetting settings; + + @Inject + private LimboCache limboCache; + + AuthGroupHandler() { } + + /** + * Set the group of a player, by its AuthMe group type. + * + * @param player The player. + * @param group The group type. + * + * @return True if succeeded, false otherwise. False is also returned if groups aren't supported + * with the current permissions system. + */ + public boolean setGroup(Player player, AuthGroupType group) { + // Check whether the permissions check is enabled + if (!settings.getProperty(PluginSettings.ENABLE_PERMISSION_CHECK)) { + return false; + } + + // Make sure group support is available + if (!permissionsManager.hasGroupSupport()) { + ConsoleLogger.showError("The current permissions system doesn't have group support, unable to set group!"); + return false; + } + + switch (group) { + case UNREGISTERED: + // Remove the other group type groups, set the current group + permissionsManager.removeGroups(player, Arrays.asList(Settings.getRegisteredGroup, Settings.getUnloggedinGroup)); + return permissionsManager.addGroup(player, Settings.unRegisteredGroup); + + case REGISTERED: + // Remove the other group type groups, set the current group + permissionsManager.removeGroups(player, Arrays.asList(Settings.unRegisteredGroup, Settings.getUnloggedinGroup)); + return permissionsManager.addGroup(player, Settings.getRegisteredGroup); + + case NOT_LOGGED_IN: + // Remove the other group type groups, set the current group + permissionsManager.removeGroups(player, Arrays.asList(Settings.unRegisteredGroup, Settings.getRegisteredGroup)); + return permissionsManager.addGroup(player, Settings.getUnloggedinGroup); + + case LOGGED_IN: + // Get the limbo player data + LimboPlayer limbo = limboCache.getLimboPlayer(player.getName().toLowerCase()); + if (limbo == null) + return false; + + // Get the players group + String realGroup = limbo.getGroup(); + + // Remove the other group types groups, set the real group + permissionsManager.removeGroups(player, Arrays.asList(Settings.unRegisteredGroup, Settings.getRegisteredGroup, Settings.getUnloggedinGroup)); + return permissionsManager.addGroup(player, realGroup); + default: + return false; + } + } + +} diff --git a/src/main/java/fr/xephi/authme/permission/PermissionsManager.java b/src/main/java/fr/xephi/authme/permission/PermissionsManager.java index fb3e2fca..e0065fa4 100644 --- a/src/main/java/fr/xephi/authme/permission/PermissionsManager.java +++ b/src/main/java/fr/xephi/authme/permission/PermissionsManager.java @@ -1,8 +1,6 @@ package fr.xephi.authme.permission; import fr.xephi.authme.ConsoleLogger; -import fr.xephi.authme.cache.limbo.LimboCache; -import fr.xephi.authme.cache.limbo.LimboPlayer; import fr.xephi.authme.permission.handlers.BPermissionsHandler; import fr.xephi.authme.permission.handlers.GroupManagerHandler; import fr.xephi.authme.permission.handlers.PermissionHandler; @@ -10,7 +8,7 @@ import fr.xephi.authme.permission.handlers.PermissionsBukkitHandler; import fr.xephi.authme.permission.handlers.PermissionsExHandler; import fr.xephi.authme.permission.handlers.VaultHandler; import fr.xephi.authme.permission.handlers.ZPermissionsHandler; -import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.util.StringUtils; import net.milkbowl.vault.permission.Permission; import org.anjocaido.groupmanager.GroupManager; import org.bukkit.Bukkit; @@ -26,7 +24,6 @@ import ru.tehkode.permissions.bukkit.PermissionsEx; import javax.annotation.PostConstruct; import javax.inject.Inject; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; /** @@ -43,9 +40,6 @@ import java.util.List; */ public class PermissionsManager { - /** - * Server instance. - */ private final Server server; private final PluginManager pluginManager; @@ -317,7 +311,7 @@ public class PermissionsManager { * False is also returned if this feature isn't supported for the current permissions system. */ public boolean addGroup(Player player, String groupName) { - if(groupName.isEmpty()) { + if (StringUtils.isEmpty(groupName)) { return false; } @@ -353,61 +347,6 @@ public class PermissionsManager { return result; } - /** - * Set the group of a player, by its AuthMe group type. - * - * @param player The player. - * @param group The group type. - * - * @return True if succeeded, false otherwise. False is also returned if groups aren't supported - * with the current permissions system. - */ - public boolean setGroup(Player player, AuthGroupType group) { - // Check whether the permissions check is enabled - if (!isEnabled() || !Settings.isPermissionCheckEnabled) { - return false; - } - - // Make sure group support is available - if (!handler.hasGroupSupport()) { - ConsoleLogger.showError("The current permissions system doesn't have group support, unable to set group!"); - return false; - } - - switch (group) { - case UNREGISTERED: - // Remove the other group type groups, set the current group - removeGroups(player, Arrays.asList(Settings.getRegisteredGroup, Settings.getUnloggedinGroup)); - return addGroup(player, Settings.unRegisteredGroup); - - case REGISTERED: - // Remove the other group type groups, set the current group - removeGroups(player, Arrays.asList(Settings.unRegisteredGroup, Settings.getUnloggedinGroup)); - return addGroup(player, Settings.getRegisteredGroup); - - case NOT_LOGGED_IN: - // Remove the other group type groups, set the current group - removeGroups(player, Arrays.asList(Settings.unRegisteredGroup, Settings.getRegisteredGroup)); - return addGroup(player, Settings.getUnloggedinGroup); - - case LOGGED_IN: - // Get the limbo player data - LimboPlayer limbo = LimboCache.getInstance().getLimboPlayer(player.getName().toLowerCase()); - if (limbo == null) - return false; - - // Get the players group - String realGroup = limbo.getGroup(); - - // Remove the other group types groups, set the real group - removeGroups(player, Arrays.asList(Settings.unRegisteredGroup, Settings.getRegisteredGroup, Settings.getUnloggedinGroup)); - return addGroup(player, realGroup); - - default: - return false; - } - } - /** * Remove the permission group of a player, if supported. * diff --git a/src/main/java/fr/xephi/authme/process/ProcessService.java b/src/main/java/fr/xephi/authme/process/ProcessService.java index be762200..fd2e5657 100644 --- a/src/main/java/fr/xephi/authme/process/ProcessService.java +++ b/src/main/java/fr/xephi/authme/process/ProcessService.java @@ -2,6 +2,7 @@ package fr.xephi.authme.process; import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.output.Messages; +import fr.xephi.authme.permission.AuthGroupHandler; import fr.xephi.authme.permission.AuthGroupType; import fr.xephi.authme.permission.PermissionNode; import fr.xephi.authme.permission.PermissionsManager; @@ -30,6 +31,9 @@ public class ProcessService { @Inject private PermissionsManager permissionsManager; + @Inject + private AuthGroupHandler authGroupHandler; + /** * Retrieve a property's value. * @@ -104,7 +108,7 @@ public class ProcessService { } public boolean setGroup(Player player, AuthGroupType group) { - return permissionsManager.setGroup(player, group); + return authGroupHandler.setGroup(player, group); } } diff --git a/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java b/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java index 63622d5f..27fe17de 100644 --- a/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java +++ b/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java @@ -5,7 +5,6 @@ import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.cache.auth.PlayerAuth; import fr.xephi.authme.cache.auth.PlayerCache; import fr.xephi.authme.cache.limbo.LimboCache; -import fr.xephi.authme.cache.limbo.LimboPlayer; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.events.ProtectInventoryEvent; import fr.xephi.authme.hooks.PluginHooks; @@ -19,8 +18,7 @@ import fr.xephi.authme.settings.properties.PluginSettings; import fr.xephi.authme.settings.properties.RegistrationSettings; import fr.xephi.authme.settings.properties.RestrictionSettings; import fr.xephi.authme.settings.properties.SecuritySettings; -import fr.xephi.authme.task.MessageTask; -import fr.xephi.authme.task.TimeoutTask; +import fr.xephi.authme.task.LimboPlayerTaskManager; import fr.xephi.authme.util.BukkitService; import fr.xephi.authme.util.TeleportationService; import fr.xephi.authme.util.Utils; @@ -30,7 +28,6 @@ import org.bukkit.entity.LivingEntity; import org.bukkit.entity.Player; import org.bukkit.potion.PotionEffect; import org.bukkit.potion.PotionEffectType; -import org.bukkit.scheduler.BukkitTask; import javax.inject.Inject; @@ -67,6 +64,9 @@ public class AsynchronousJoin implements AsynchronousProcess { @Inject private BukkitService bukkitService; + @Inject + private LimboPlayerTaskManager limboPlayerTaskManager; + AsynchronousJoin() { } @@ -193,17 +193,9 @@ public class AsynchronousJoin implements AsynchronousProcess { }); - // Timeout task - if (registrationTimeout > 0) { - BukkitTask id = bukkitService.runTaskLater(new TimeoutTask(plugin, name, player), registrationTimeout); - LimboPlayer limboPlayer = limboCache.getLimboPlayer(name); - if (limboPlayer != null) { - limboPlayer.setTimeoutTask(id); - } - } + // Timeout and message task + limboPlayerTaskManager.registerTimeoutTask(player); - // Message task - int msgInterval = service.getProperty(RegistrationSettings.MESSAGE_INTERVAL); MessageKey msg; if (isAuthAvailable) { msg = MessageKey.LOGIN_MESSAGE; @@ -212,14 +204,7 @@ public class AsynchronousJoin implements AsynchronousProcess { ? MessageKey.REGISTER_EMAIL_MESSAGE : MessageKey.REGISTER_MESSAGE; } - if (msgInterval > 0 && limboCache.getLimboPlayer(name) != null) { - BukkitTask msgTask = bukkitService.runTaskLater(new MessageTask(bukkitService, plugin.getMessages(), - name, msg, msgInterval), 20L); - LimboPlayer limboPlayer = limboCache.getLimboPlayer(name); - if (limboPlayer != null) { - limboPlayer.setMessageTask(msgTask); - } - } + limboPlayerTaskManager.registerMessageTask(name, msg); } private boolean isPlayerUnrestricted(String name) { diff --git a/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java b/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java index 5905010a..7f2fd8f5 100644 --- a/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java +++ b/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java @@ -1,6 +1,5 @@ package fr.xephi.authme.process.login; -import fr.xephi.authme.AuthMe; import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.cache.CaptchaManager; import fr.xephi.authme.cache.TempbanManager; @@ -25,13 +24,12 @@ import fr.xephi.authme.settings.properties.EmailSettings; import fr.xephi.authme.settings.properties.RegistrationSettings; import fr.xephi.authme.settings.properties.RestrictionSettings; import fr.xephi.authme.settings.properties.SecuritySettings; -import fr.xephi.authme.task.MessageTask; +import fr.xephi.authme.task.LimboPlayerTaskManager; import fr.xephi.authme.util.BukkitService; import fr.xephi.authme.util.StringUtils; import fr.xephi.authme.util.Utils; import org.bukkit.Bukkit; import org.bukkit.entity.Player; -import org.bukkit.scheduler.BukkitTask; import javax.inject.Inject; import java.util.List; @@ -40,9 +38,6 @@ import java.util.List; */ public class AsynchronousLogin implements AsynchronousProcess { - @Inject - private AuthMe plugin; - @Inject private DataSource database; @@ -73,8 +68,12 @@ public class AsynchronousLogin implements AsynchronousProcess { @Inject private TempbanManager tempbanManager; + @Inject + private LimboPlayerTaskManager limboPlayerTaskManager; + AsynchronousLogin() { } + /** * Queries the {@link fr.xephi.authme.cache.CaptchaManager} to * see if a captcha needs to be entered in order to log in. @@ -118,16 +117,11 @@ public class AsynchronousLogin implements AsynchronousProcess { if (pAuth == null) { service.send(player, MessageKey.USER_NOT_REGISTERED); - LimboPlayer limboPlayer = limboCache.getLimboPlayer(name); - if (limboPlayer != null) { - limboPlayer.getMessageTask().cancel(); - String[] msg = service.getProperty(RegistrationSettings.USE_EMAIL_REGISTRATION) - ? service.retrieveMessage(MessageKey.REGISTER_EMAIL_MESSAGE) - : service.retrieveMessage(MessageKey.REGISTER_MESSAGE); - BukkitTask messageTask = bukkitService.runTask(new MessageTask(bukkitService, - name, msg, service.getProperty(RegistrationSettings.MESSAGE_INTERVAL))); - limboPlayer.setMessageTask(messageTask); - } + // TODO ljacqu 20160612: Why is the message task being canceled and added again here? + MessageKey key = service.getProperty(RegistrationSettings.USE_EMAIL_REGISTRATION) + ? MessageKey.REGISTER_EMAIL_MESSAGE + : MessageKey.REGISTER_MESSAGE; + limboPlayerTaskManager.registerMessageTask(name, key); return null; } diff --git a/src/main/java/fr/xephi/authme/process/logout/ProcessSynchronousPlayerLogout.java b/src/main/java/fr/xephi/authme/process/logout/ProcessSynchronousPlayerLogout.java index 6fbd8ce6..cd8eb3b3 100644 --- a/src/main/java/fr/xephi/authme/process/logout/ProcessSynchronousPlayerLogout.java +++ b/src/main/java/fr/xephi/authme/process/logout/ProcessSynchronousPlayerLogout.java @@ -4,22 +4,18 @@ import com.google.common.io.ByteArrayDataOutput; import com.google.common.io.ByteStreams; import fr.xephi.authme.AuthMe; import fr.xephi.authme.ConsoleLogger; -import fr.xephi.authme.cache.limbo.LimboCache; import fr.xephi.authme.events.LogoutEvent; import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.process.ProcessService; import fr.xephi.authme.process.SynchronousProcess; -import fr.xephi.authme.settings.Settings; import fr.xephi.authme.settings.properties.HooksSettings; import fr.xephi.authme.settings.properties.RegistrationSettings; import fr.xephi.authme.settings.properties.RestrictionSettings; -import fr.xephi.authme.task.MessageTask; -import fr.xephi.authme.task.TimeoutTask; +import fr.xephi.authme.task.LimboPlayerTaskManager; import fr.xephi.authme.util.BukkitService; import org.bukkit.entity.Player; import org.bukkit.potion.PotionEffect; import org.bukkit.potion.PotionEffectType; -import org.bukkit.scheduler.BukkitTask; import javax.inject.Inject; @@ -34,14 +30,15 @@ public class ProcessSynchronousPlayerLogout implements SynchronousProcess { @Inject private ProcessService service; - @Inject - private LimboCache limboCache; - @Inject private BukkitService bukkitService; + @Inject + private LimboPlayerTaskManager limboPlayerTaskManager; + ProcessSynchronousPlayerLogout() { } + private void sendBungeeMessage(Player player) { ByteArrayDataOutput out = ByteStreams.newDataOutput(); out.writeUTF("Forward"); @@ -64,23 +61,19 @@ public class ProcessSynchronousPlayerLogout implements SynchronousProcess { plugin.sessions.get(name).cancel(); plugin.sessions.remove(name); } - if (Settings.protectInventoryBeforeLogInEnabled) { + if (service.getProperty(RestrictionSettings.PROTECT_INVENTORY_BEFORE_LOGIN)) { plugin.inventoryProtector.sendBlankInventoryPacket(player); } - int timeOut = service.getProperty(RestrictionSettings.TIMEOUT) * TICKS_PER_SECOND; - int interval = service.getProperty(RegistrationSettings.MESSAGE_INTERVAL); - if (timeOut != 0) { - BukkitTask id = bukkitService.runTaskLater(new TimeoutTask(plugin, name, player), timeOut); - limboCache.getLimboPlayer(name).setTimeoutTask(id); - } - BukkitTask msgT = bukkitService.runTask(new MessageTask(bukkitService, plugin.getMessages(), - name, MessageKey.LOGIN_MESSAGE, interval)); - limboCache.getLimboPlayer(name).setMessageTask(msgT); + + limboPlayerTaskManager.registerTimeoutTask(player); + limboPlayerTaskManager.registerMessageTask(name, MessageKey.LOGIN_MESSAGE); + if (player.isInsideVehicle() && player.getVehicle() != null) { player.getVehicle().eject(); } + final int timeout = service.getProperty(RestrictionSettings.TIMEOUT) * TICKS_PER_SECOND; if (service.getProperty(RegistrationSettings.APPLY_BLIND_EFFECT)) { - player.addPotionEffect(new PotionEffect(PotionEffectType.BLINDNESS, timeOut, 2)); + player.addPotionEffect(new PotionEffect(PotionEffectType.BLINDNESS, timeout, 2)); } player.setOp(false); restoreSpeedEffect(player); diff --git a/src/main/java/fr/xephi/authme/process/register/ProcessSyncEmailRegister.java b/src/main/java/fr/xephi/authme/process/register/ProcessSyncEmailRegister.java index 146ffb46..c498d41d 100644 --- a/src/main/java/fr/xephi/authme/process/register/ProcessSyncEmailRegister.java +++ b/src/main/java/fr/xephi/authme/process/register/ProcessSyncEmailRegister.java @@ -1,64 +1,43 @@ package fr.xephi.authme.process.register; -import fr.xephi.authme.AuthMe; import fr.xephi.authme.ConsoleLogger; -import fr.xephi.authme.cache.limbo.LimboCache; -import fr.xephi.authme.cache.limbo.LimboPlayer; import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.permission.AuthGroupType; import fr.xephi.authme.process.ProcessService; import fr.xephi.authme.process.SynchronousProcess; import fr.xephi.authme.settings.Settings; -import fr.xephi.authme.settings.properties.RegistrationSettings; -import fr.xephi.authme.settings.properties.RestrictionSettings; import fr.xephi.authme.settings.properties.SecuritySettings; -import fr.xephi.authme.task.MessageTask; -import fr.xephi.authme.task.TimeoutTask; +import fr.xephi.authme.task.LimboPlayerTaskManager; import fr.xephi.authme.util.BukkitService; import fr.xephi.authme.util.Utils; import org.bukkit.entity.Player; -import org.bukkit.scheduler.BukkitTask; import javax.inject.Inject; -import static fr.xephi.authme.util.BukkitService.TICKS_PER_SECOND; - public class ProcessSyncEmailRegister implements SynchronousProcess { @Inject private ProcessService service; - @Inject - private LimboCache limboCache; - @Inject private BukkitService bukkitService; @Inject - private AuthMe authMe; + private LimboPlayerTaskManager limboPlayerTaskManager; ProcessSyncEmailRegister() { } + public void processEmailRegister(Player player) { final String name = player.getName().toLowerCase(); - LimboPlayer limbo = limboCache.getLimboPlayer(name); if (!Settings.getRegisteredGroup.isEmpty()) { service.setGroup(player, AuthGroupType.REGISTERED); } service.send(player, MessageKey.ACCOUNT_NOT_ACTIVATED); - int time = service.getProperty(RestrictionSettings.TIMEOUT) * TICKS_PER_SECOND; - int msgInterval = service.getProperty(RegistrationSettings.MESSAGE_INTERVAL); - if (limbo != null) { - if (time != 0) { - BukkitTask id = bukkitService.runTaskLater(new TimeoutTask(authMe, name, player), time); - limbo.setTimeoutTask(id); - } - BukkitTask messageTask = bukkitService.runTask(new MessageTask( - bukkitService, name, service.retrieveMessage(MessageKey.LOGIN_MESSAGE), msgInterval)); - limbo.setMessageTask(messageTask); - } + limboPlayerTaskManager.registerTimeoutTask(player); + limboPlayerTaskManager.registerMessageTask(name, MessageKey.LOGIN_MESSAGE); player.saveData(); if (!service.getProperty(SecuritySettings.REMOVE_SPAM_FROM_CONSOLE)) { diff --git a/src/main/java/fr/xephi/authme/process/register/ProcessSyncPasswordRegister.java b/src/main/java/fr/xephi/authme/process/register/ProcessSyncPasswordRegister.java index 34fc4c26..cf936257 100644 --- a/src/main/java/fr/xephi/authme/process/register/ProcessSyncPasswordRegister.java +++ b/src/main/java/fr/xephi/authme/process/register/ProcessSyncPasswordRegister.java @@ -18,19 +18,16 @@ import fr.xephi.authme.settings.properties.HooksSettings; import fr.xephi.authme.settings.properties.RegistrationSettings; import fr.xephi.authme.settings.properties.RestrictionSettings; import fr.xephi.authme.settings.properties.SecuritySettings; -import fr.xephi.authme.task.MessageTask; -import fr.xephi.authme.task.TimeoutTask; +import fr.xephi.authme.task.LimboPlayerTaskManager; import fr.xephi.authme.util.BukkitService; import fr.xephi.authme.util.Utils; import org.bukkit.Bukkit; import org.bukkit.entity.Player; import org.bukkit.potion.PotionEffectType; -import org.bukkit.scheduler.BukkitTask; import javax.inject.Inject; import static fr.xephi.authme.settings.properties.RestrictionSettings.HIDE_TABLIST_BEFORE_LOGIN; -import static fr.xephi.authme.util.BukkitService.TICKS_PER_SECOND; /** */ @@ -48,8 +45,12 @@ public class ProcessSyncPasswordRegister implements SynchronousProcess { @Inject private LimboCache limboCache; + @Inject + private LimboPlayerTaskManager limboPlayerTaskManager; + ProcessSyncPasswordRegister() { } + private void sendBungeeMessage(Player player) { ByteArrayDataOutput out = ByteStreams.newDataOutput(); out.writeUTF("Forward"); @@ -77,17 +78,11 @@ public class ProcessSyncPasswordRegister implements SynchronousProcess { private void requestLogin(Player player) { final String name = player.getName().toLowerCase(); Utils.teleportToSpawn(player); + limboCache.updateLimboPlayer(player); - int delay = service.getProperty(RestrictionSettings.TIMEOUT) * TICKS_PER_SECOND; - int interval = service.getProperty(RegistrationSettings.MESSAGE_INTERVAL); - BukkitTask task; - if (delay != 0) { - task = bukkitService.runTaskLater(new TimeoutTask(plugin, name, player), delay); - limboCache.getLimboPlayer(name).setTimeoutTask(task); - } - task = bukkitService.runTask(new MessageTask(bukkitService, plugin.getMessages(), - name, MessageKey.LOGIN_MESSAGE, interval)); - limboCache.getLimboPlayer(name).setMessageTask(task); + limboPlayerTaskManager.registerTimeoutTask(player); + limboPlayerTaskManager.registerMessageTask(name, MessageKey.LOGIN_MESSAGE); + if (player.isInsideVehicle() && player.getVehicle() != null) { player.getVehicle().eject(); } diff --git a/src/main/java/fr/xephi/authme/process/unregister/AsynchronousUnregister.java b/src/main/java/fr/xephi/authme/process/unregister/AsynchronousUnregister.java index 59e88d89..41f9a2d9 100644 --- a/src/main/java/fr/xephi/authme/process/unregister/AsynchronousUnregister.java +++ b/src/main/java/fr/xephi/authme/process/unregister/AsynchronousUnregister.java @@ -5,7 +5,6 @@ import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.cache.auth.PlayerAuth; import fr.xephi.authme.cache.auth.PlayerCache; import fr.xephi.authme.cache.limbo.LimboCache; -import fr.xephi.authme.cache.limbo.LimboPlayer; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.permission.AuthGroupType; @@ -15,14 +14,12 @@ import fr.xephi.authme.security.PasswordSecurity; import fr.xephi.authme.settings.Settings; import fr.xephi.authme.settings.properties.RegistrationSettings; import fr.xephi.authme.settings.properties.RestrictionSettings; -import fr.xephi.authme.task.MessageTask; -import fr.xephi.authme.task.TimeoutTask; +import fr.xephi.authme.task.LimboPlayerTaskManager; import fr.xephi.authme.util.BukkitService; import fr.xephi.authme.util.Utils; import org.bukkit.entity.Player; import org.bukkit.potion.PotionEffect; import org.bukkit.potion.PotionEffectType; -import org.bukkit.scheduler.BukkitTask; import javax.inject.Inject; @@ -51,8 +48,12 @@ public class AsynchronousUnregister implements AsynchronousProcess { @Inject private BukkitService bukkitService; + @Inject + private LimboPlayerTaskManager limboPlayerTaskManager; + AsynchronousUnregister() { } + public void unregister(Player player, String password, boolean force) { final String name = player.getName().toLowerCase(); PlayerAuth cachedAuth = playerCache.getAuth(name); @@ -61,7 +62,7 @@ public class AsynchronousUnregister implements AsynchronousProcess { service.send(player, MessageKey.ERROR); return; } - int timeOut = service.getProperty(RestrictionSettings.TIMEOUT) * TICKS_PER_SECOND; + if (service.getProperty(RegistrationSettings.FORCE)) { Utils.teleportToSpawn(player); player.saveData(); @@ -70,17 +71,12 @@ public class AsynchronousUnregister implements AsynchronousProcess { service.setGroup(player, AuthGroupType.UNREGISTERED); } limboCache.addLimboPlayer(player); - LimboPlayer limboPlayer = limboCache.getLimboPlayer(name); - int interval = service.getProperty(RegistrationSettings.MESSAGE_INTERVAL); - if (timeOut != 0) { - BukkitTask id = bukkitService.runTaskLater(new TimeoutTask(plugin, name, player), timeOut); - limboPlayer.setTimeoutTask(id); - } - limboPlayer.setMessageTask(bukkitService.runTask(new MessageTask(bukkitService, - plugin.getMessages(), name, MessageKey.REGISTER_MESSAGE, interval))); + limboPlayerTaskManager.registerTimeoutTask(player); + limboPlayerTaskManager.registerMessageTask(name, MessageKey.REGISTER_MESSAGE); + service.send(player, MessageKey.UNREGISTERED_SUCCESS); ConsoleLogger.info(player.getDisplayName() + " unregistered himself"); - return; + return; // TODO ljacqu 20160612: Why return here? No blind effect? Player not removed from PlayerCache? } if (!Settings.unRegisteredGroup.isEmpty()) { service.setGroup(player, AuthGroupType.UNREGISTERED); @@ -88,8 +84,9 @@ public class AsynchronousUnregister implements AsynchronousProcess { playerCache.removePlayer(name); // Apply blind effect + int timeout = service.getProperty(RestrictionSettings.TIMEOUT) * TICKS_PER_SECOND; if (service.getProperty(RegistrationSettings.APPLY_BLIND_EFFECT)) { - player.addPotionEffect(new PotionEffect(PotionEffectType.BLINDNESS, timeOut, 2)); + player.addPotionEffect(new PotionEffect(PotionEffectType.BLINDNESS, timeout, 2)); } service.send(player, MessageKey.UNREGISTERED_SUCCESS); ConsoleLogger.info(player.getDisplayName() + " unregistered himself"); diff --git a/src/main/java/fr/xephi/authme/task/LimboPlayerTaskManager.java b/src/main/java/fr/xephi/authme/task/LimboPlayerTaskManager.java new file mode 100644 index 00000000..e1a95603 --- /dev/null +++ b/src/main/java/fr/xephi/authme/task/LimboPlayerTaskManager.java @@ -0,0 +1,94 @@ +package fr.xephi.authme.task; + +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.cache.auth.PlayerCache; +import fr.xephi.authme.cache.limbo.LimboCache; +import fr.xephi.authme.cache.limbo.LimboPlayer; +import fr.xephi.authme.output.MessageKey; +import fr.xephi.authme.output.Messages; +import fr.xephi.authme.settings.NewSetting; +import fr.xephi.authme.settings.properties.RegistrationSettings; +import fr.xephi.authme.settings.properties.RestrictionSettings; +import fr.xephi.authme.util.BukkitService; +import org.bukkit.entity.Player; +import org.bukkit.scheduler.BukkitTask; + +import javax.inject.Inject; + +import static fr.xephi.authme.util.BukkitService.TICKS_PER_SECOND; + +/** + * Registers tasks associated with a LimboPlayer. + */ +public class LimboPlayerTaskManager { + + @Inject + private Messages messages; + + @Inject + private NewSetting settings; + + @Inject + private BukkitService bukkitService; + + @Inject + private LimboCache limboCache; + + @Inject + private PlayerCache playerCache; + + LimboPlayerTaskManager() { } + + + /** + * Registers a {@link MessageTask} for the given player name. + * + * @param name the name of the player to schedule a repeating message task for + * @param key the key of the message to display + */ + public void registerMessageTask(String name, MessageKey key) { + final int interval = settings.getProperty(RegistrationSettings.MESSAGE_INTERVAL); + if (interval > 0) { + final LimboPlayer limboPlayer = limboCache.getLimboPlayer(name); + if (limboPlayer == null) { + ConsoleLogger.info("LimboPlayer for '" + name + "' is not available"); + } else { + cancelTask(limboPlayer.getMessageTask()); + BukkitTask messageTask = bukkitService.runTask(new MessageTask(name, messages.retrieve(key), + interval, bukkitService, limboCache, playerCache)); + limboPlayer.setMessageTask(messageTask); + } + } + } + + /** + * Registers a {@link TimeoutTask} for the given player according to the configuration. + * + * @param player the player to register a timeout task for + */ + public void registerTimeoutTask(Player player) { + final int timeout = settings.getProperty(RestrictionSettings.TIMEOUT) * TICKS_PER_SECOND; + if (timeout > 0) { + final LimboPlayer limboPlayer = limboCache.getLimboPlayer(player.getName()); + if (limboPlayer == null) { + ConsoleLogger.info("LimboPlayer for '" + player.getName() + "' is not available"); + } else { + cancelTask(limboPlayer.getTimeoutTask()); + String message = messages.retrieveSingle(MessageKey.LOGIN_TIMEOUT_ERROR); + BukkitTask task = bukkitService.runTaskLater(new TimeoutTask(player, message, playerCache), timeout); + limboPlayer.setTimeoutTask(task); + } + } + } + + /** + * Null-safe method to cancel a potentially existing task. + * + * @param task the task to cancel (or null) + */ + private static void cancelTask(BukkitTask task) { + if (task != null) { + task.cancel(); + } + } +} diff --git a/src/main/java/fr/xephi/authme/task/MessageTask.java b/src/main/java/fr/xephi/authme/task/MessageTask.java index e09d5039..590913db 100644 --- a/src/main/java/fr/xephi/authme/task/MessageTask.java +++ b/src/main/java/fr/xephi/authme/task/MessageTask.java @@ -2,53 +2,51 @@ package fr.xephi.authme.task; import fr.xephi.authme.cache.auth.PlayerCache; import fr.xephi.authme.cache.limbo.LimboCache; -import fr.xephi.authme.output.MessageKey; -import fr.xephi.authme.output.Messages; import fr.xephi.authme.util.BukkitService; import org.bukkit.entity.Player; import org.bukkit.scheduler.BukkitTask; +import static fr.xephi.authme.util.BukkitService.TICKS_PER_SECOND; + /** + * Message shown to a player in a regular interval as long as he is not logged in. */ public class MessageTask implements Runnable { - private final BukkitService bukkitService; private final String name; - private final String[] msg; + private final String[] message; private final int interval; + private final BukkitService bukkitService; + private final LimboCache limboCache; + private final PlayerCache playerCache; /* * Constructor. */ - public MessageTask(BukkitService bukkitService, String name, String[] lines, int interval) { - this.bukkitService = bukkitService; + public MessageTask(String name, String[] lines, int interval, BukkitService bukkitService, + LimboCache limboCache, PlayerCache playerCache) { this.name = name; - this.msg = lines; + this.message = lines; this.interval = interval; - } - - /* - * Constructor. - */ - public MessageTask(BukkitService bukkitService, Messages messages, String name, MessageKey messageKey, - int interval) { - this(bukkitService, name, messages.retrieve(messageKey), interval); + this.bukkitService = bukkitService; + this.limboCache = limboCache; + this.playerCache = playerCache; } @Override public void run() { - if (PlayerCache.getInstance().isAuthenticated(name)) { + if (playerCache.isAuthenticated(name)) { return; } for (Player player : bukkitService.getOnlinePlayers()) { if (player.getName().equalsIgnoreCase(name)) { - for (String ms : msg) { + for (String ms : message) { player.sendMessage(ms); } - BukkitTask nextTask = bukkitService.runTaskLater(this, interval * 20); - if (LimboCache.getInstance().hasLimboPlayer(name)) { - LimboCache.getInstance().getLimboPlayer(name).setMessageTask(nextTask); + BukkitTask nextTask = bukkitService.runTaskLater(this, interval * TICKS_PER_SECOND); + if (limboCache.hasLimboPlayer(name)) { + limboCache.getLimboPlayer(name).setMessageTask(nextTask); } return; } diff --git a/src/main/java/fr/xephi/authme/task/TimeoutTask.java b/src/main/java/fr/xephi/authme/task/TimeoutTask.java index b304632d..8aa719cd 100644 --- a/src/main/java/fr/xephi/authme/task/TimeoutTask.java +++ b/src/main/java/fr/xephi/authme/task/TimeoutTask.java @@ -1,34 +1,34 @@ package fr.xephi.authme.task; -import fr.xephi.authme.AuthMe; import fr.xephi.authme.cache.auth.PlayerCache; -import fr.xephi.authme.output.MessageKey; -import fr.xephi.authme.output.Messages; import org.bukkit.entity.Player; +/** + * Kicks a player if he hasn't logged in (scheduled to run after a configured delay). + */ public class TimeoutTask implements Runnable { - private final String name; - private final Messages m; private final Player player; + private final String message; + private final PlayerCache playerCache; /** * Constructor for TimeoutTask. * - * @param plugin AuthMe - * @param name String - * @param player Player + * @param player the player to check + * @param message the kick message + * @param playerCache player cache instance */ - public TimeoutTask(AuthMe plugin, String name, Player player) { - this.m = plugin.getMessages(); - this.name = name; + public TimeoutTask(Player player, String message, PlayerCache playerCache) { + this.message = message; this.player = player; + this.playerCache = playerCache; } @Override public void run() { - if (!PlayerCache.getInstance().isAuthenticated(name)) { - player.kickPlayer(m.retrieveSingle(MessageKey.LOGIN_TIMEOUT_ERROR)); + if (!playerCache.isAuthenticated(player.getName())) { + player.kickPlayer(message); } } } diff --git a/src/test/java/fr/xephi/authme/task/LimboPlayerTaskManagerTest.java b/src/test/java/fr/xephi/authme/task/LimboPlayerTaskManagerTest.java new file mode 100644 index 00000000..b0329927 --- /dev/null +++ b/src/test/java/fr/xephi/authme/task/LimboPlayerTaskManagerTest.java @@ -0,0 +1,219 @@ +package fr.xephi.authme.task; + +import fr.xephi.authme.TestHelper; +import fr.xephi.authme.cache.auth.PlayerCache; +import fr.xephi.authme.cache.limbo.LimboCache; +import fr.xephi.authme.cache.limbo.LimboPlayer; +import fr.xephi.authme.output.MessageKey; +import fr.xephi.authme.output.Messages; +import fr.xephi.authme.settings.NewSetting; +import fr.xephi.authme.settings.properties.RegistrationSettings; +import fr.xephi.authme.settings.properties.RestrictionSettings; +import fr.xephi.authme.util.BukkitService; +import org.bukkit.entity.Player; +import org.bukkit.scheduler.BukkitTask; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import static org.mockito.BDDMockito.given; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyLong; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; + +/** + * Test for {@link LimboPlayerTaskManager}. + */ +@RunWith(MockitoJUnitRunner.class) +public class LimboPlayerTaskManagerTest { + + @InjectMocks + private LimboPlayerTaskManager limboPlayerTaskManager; + + @Mock + private Messages messages; + + @Mock + private NewSetting settings; + + @Mock + private BukkitService bukkitService; + + @Mock + private LimboCache limboCache; + + @Mock + private PlayerCache playerCache; + + @BeforeClass + public static void setupLogger() { + TestHelper.setupLogger(); + } + + @Test + public void shouldRegisterMessageTask() { + // given + String name = "bobby"; + LimboPlayer limboPlayer = mock(LimboPlayer.class); + given(limboCache.getLimboPlayer(name)).willReturn(limboPlayer); + MessageKey key = MessageKey.REGISTER_EMAIL_MESSAGE; + given(messages.retrieve(key)).willReturn(new String[]{"Please register!"}); + BukkitTask bukkiTask = mock(BukkitTask.class); + given(bukkitService.runTask(any(MessageTask.class))).willReturn(bukkiTask); + given(settings.getProperty(RegistrationSettings.MESSAGE_INTERVAL)).willReturn(12); + + // when + limboPlayerTaskManager.registerMessageTask(name, key); + + // then + verify(limboPlayer).setMessageTask(bukkiTask); + verify(messages).retrieve(key); + } + + @Test + public void shouldNotScheduleTaskForMissingLimboPlayer() { + // given + String name = "ghost"; + given(limboCache.getLimboPlayer(name)).willReturn(null); + MessageKey key = MessageKey.REGISTER_MESSAGE; + given(messages.retrieve(key)).willReturn(new String[]{"Please register!"}); + given(settings.getProperty(RegistrationSettings.MESSAGE_INTERVAL)).willReturn(5); + + // when + limboPlayerTaskManager.registerMessageTask(name, key); + + // then + verify(limboCache).getLimboPlayer(name); + verifyZeroInteractions(bukkitService); + verifyZeroInteractions(messages); + } + + @Test + public void shouldNotScheduleTaskForZeroAsInterval() { + // given + String name = "Tester1"; + LimboPlayer limboPlayer = mock(LimboPlayer.class); + given(limboCache.getLimboPlayer(name)).willReturn(limboPlayer); + MessageKey key = MessageKey.REGISTER_EMAIL_MESSAGE; + given(messages.retrieve(key)).willReturn(new String[]{"Please register!"}); + BukkitTask bukkiTask = mock(BukkitTask.class); + given(bukkitService.runTask(any(MessageTask.class))).willReturn(bukkiTask); + given(settings.getProperty(RegistrationSettings.MESSAGE_INTERVAL)).willReturn(0); + + // when + limboPlayerTaskManager.registerMessageTask(name, key); + + // then + verifyZeroInteractions(limboPlayer, bukkitService); + } + + @Test + public void shouldCancelExistingMessageTask() { + // given + LimboPlayer limboPlayer = mock(LimboPlayer.class); + BukkitTask existingMessageTask = mock(BukkitTask.class); + given(limboPlayer.getMessageTask()).willReturn(existingMessageTask); + + String name = "bobby"; + given(limboCache.getLimboPlayer(name)).willReturn(limboPlayer); + MessageKey key = MessageKey.REGISTER_EMAIL_MESSAGE; + given(messages.retrieve(key)).willReturn(new String[]{"Please register", "Use /register"}); + + BukkitTask bukkiTask = mock(BukkitTask.class); + given(bukkitService.runTask(any(MessageTask.class))).willReturn(bukkiTask); + given(settings.getProperty(RegistrationSettings.MESSAGE_INTERVAL)).willReturn(8); + + // when + limboPlayerTaskManager.registerMessageTask(name, key); + + // then + verify(limboPlayer).setMessageTask(bukkiTask); + verify(messages).retrieve(key); + verify(existingMessageTask).cancel(); + } + + @Test + public void shouldRegisterTimeoutTask() { + // given + String name = "l33tPlayer"; + Player player = mock(Player.class); + given(player.getName()).willReturn(name); + LimboPlayer limboPlayer = mock(LimboPlayer.class); + given(limboCache.getLimboPlayer(name)).willReturn(limboPlayer); + given(settings.getProperty(RestrictionSettings.TIMEOUT)).willReturn(30); + BukkitTask bukkitTask = mock(BukkitTask.class); + given(bukkitService.runTaskLater(any(TimeoutTask.class), anyLong())).willReturn(bukkitTask); + + // when + limboPlayerTaskManager.registerTimeoutTask(player); + + // then + verify(limboPlayer).setTimeoutTask(bukkitTask); + verify(bukkitService).runTaskLater(any(TimeoutTask.class), eq(600L)); // 30 * TICKS_PER_SECOND + verify(messages).retrieveSingle(MessageKey.LOGIN_TIMEOUT_ERROR); + } + + @Test + public void shouldNotRegisterTimeoutTaskForMissingLimboPlayer() { + // given + String name = "Phantom_"; + Player player = mock(Player.class); + given(player.getName()).willReturn(name); + given(limboCache.getLimboPlayer(name)).willReturn(null); + given(settings.getProperty(RestrictionSettings.TIMEOUT)).willReturn(27); + + // when + limboPlayerTaskManager.registerTimeoutTask(player); + + // then + verifyZeroInteractions(bukkitService, messages); + } + + @Test + public void shouldNotRegisterTimeoutTaskForZeroTimeout() { + // given + String name = "snail"; + Player player = mock(Player.class); + given(player.getName()).willReturn(name); + LimboPlayer limboPlayer = mock(LimboPlayer.class); + given(limboCache.getLimboPlayer(name)).willReturn(limboPlayer); + given(settings.getProperty(RestrictionSettings.TIMEOUT)).willReturn(0); + + // when + limboPlayerTaskManager.registerTimeoutTask(player); + + // then + verifyZeroInteractions(limboPlayer, bukkitService); + } + + @Test + public void shouldCancelExistingTimeoutTask() { + // given + String name = "l33tPlayer"; + Player player = mock(Player.class); + given(player.getName()).willReturn(name); + LimboPlayer limboPlayer = mock(LimboPlayer.class); + BukkitTask existingTask = mock(BukkitTask.class); + given(limboPlayer.getTimeoutTask()).willReturn(existingTask); + given(limboCache.getLimboPlayer(name)).willReturn(limboPlayer); + given(settings.getProperty(RestrictionSettings.TIMEOUT)).willReturn(18); + BukkitTask bukkitTask = mock(BukkitTask.class); + given(bukkitService.runTaskLater(any(TimeoutTask.class), anyLong())).willReturn(bukkitTask); + + // when + limboPlayerTaskManager.registerTimeoutTask(player); + + // then + verify(existingTask).cancel(); + verify(limboPlayer).setTimeoutTask(bukkitTask); + verify(bukkitService).runTaskLater(any(TimeoutTask.class), eq(360L)); // 18 * TICKS_PER_SECOND + verify(messages).retrieveSingle(MessageKey.LOGIN_TIMEOUT_ERROR); + } + +} diff --git a/src/test/java/tools/dependencygraph/DrawDependency.java b/src/test/java/tools/dependencygraph/DrawDependency.java index 706a734e..4f9dd507 100644 --- a/src/test/java/tools/dependencygraph/DrawDependency.java +++ b/src/test/java/tools/dependencygraph/DrawDependency.java @@ -11,6 +11,7 @@ import fr.xephi.authme.initialization.Injection; import fr.xephi.authme.process.AsynchronousProcess; import fr.xephi.authme.process.SynchronousProcess; import fr.xephi.authme.security.crypts.EncryptionMethod; +import org.bukkit.event.Listener; import tools.utils.ToolTask; import tools.utils.ToolsConstants; @@ -35,7 +36,7 @@ public class DrawDependency implements ToolTask { private static final String ROOT_PACKAGE = "fr.xephi.authme"; private static final List> SUPER_TYPES = ImmutableList.of(ExecutableCommand.class, - SynchronousProcess.class, AsynchronousProcess.class, EncryptionMethod.class, Converter.class); + SynchronousProcess.class, AsynchronousProcess.class, EncryptionMethod.class, Converter.class, Listener.class); private boolean mapToSupertype; // Map with the graph's nodes: value is one of the key's dependencies From 450c80f63c19bdcac5d174c0a4e2967959afe4ad Mon Sep 17 00:00:00 2001 From: ljacqu Date: Sun, 12 Jun 2016 22:10:30 +0200 Subject: [PATCH 183/200] #720 Update Hungarian messages kindly submitted by @rErEaT --- src/main/resources/messages/messages_hu.yml | 29 +++++++++++---------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/src/main/resources/messages/messages_hu.yml b/src/main/resources/messages/messages_hu.yml index 326bd2bc..7dd9cca1 100644 --- a/src/main/resources/messages/messages_hu.yml +++ b/src/main/resources/messages/messages_hu.yml @@ -16,34 +16,34 @@ password_error_unsafe: '&cA választott jelszó nem biztonságos, kérlek válas unregistered: '&cRegisztráció sikeresen törölve!' same_nick: 'Ezzel a játékosnévvel már játszanak a szerveren.' valid_session: '&2A megadott időkereten belül csatlakoztál vissza így a rendszer automatikusan beléptetett.' -pwd_changed: '&cJelszó cserélve!' -reload: 'Beálítások és adatbázis újratöltve!' +pwd_changed: '&cJelszó sikeresen megváltoztatva!' +reload: 'Beállítások és adatbázis újratöltve!' timeout: 'Bejelentkezési időtúllépés!' -error: 'Hiba lépett fel! Lépj kapcsolatba a tulajjal!' +error: 'Hiba lépett fel! Lépj kapcsolatba a szerver tulajával sürgősen!' logged_in: '&cMár be vagy jelentkezve!' login: '&aSikeresen beléptél!' wrong_pwd: '&4Hibás jelszó!' user_unknown: '&cEz a felhasználó nincs regisztrálva!' reg_msg: '&cKérlek Regisztrálj: "/register "' -reg_email_msg: '&cKérlek regisztrálj: "/register "' -unsafe_spawn: 'A kilépési helyzeted nem biztonságos, teleportálás a kezdő pozícióra.' +reg_email_msg: '&cKérlek regisztrálj: "/register "' +unsafe_spawn: 'A kilépési helyzeted nem biztonságos, ezért elteleportálunk a kezdő pozícióra.' max_reg: '&cElérted a maximálisan beregisztrálható karakterek számát. (%reg_count/%max_acc %reg_names)!' password_error: 'A két jelszó nem egyezik!' invalid_session: '&cAz IP címed megváltozott, ezért a visszacsatlakozási időkereted lejárt.' pass_len: 'A jelszavad nem éri el a minimális hosszúságot!' vb_nonActiv: '&cA felhasználód aktiválása még nem történt meg, ellenőrizd a megadott emailed!' -usage_changepassword: 'Használat: "/changepassword <új Jelszó>"' -name_len: '&4A felhasználó neved túl hosszú, vagy túl rövid! Válassz másikat!' -regex: '&4A felhasználóneved nem használható karaktereket tartalmaz. Elfogadott karakterek: REG_EX' -add_email: '&3Kérlek add hozzá a felhasználódhoz az email címedet "/email add "' -recovery_email: '&3Ha elfelejtetted a jelszavad, használd az: "/email recovery "' +usage_changepassword: 'Használat: "/changepassword <új jelszó>"' +name_len: '&4A felhasználóneved túl hosszú, vagy túl rövid! Válassz másikat!' +regex: '&4A felhasználóneved nem engedélyezett karaktereket tartalmaz. Engedélyezett karakterek: REG_EX' +add_email: '&3Kérlek rendeld hozzá a felhasználódhoz az email címedet "/email add "' +recovery_email: '&3Ha elfelejtetted a jelszavad, használd az: "/email recovery "' usage_captcha: '&3A bejelentkezéshez CAPTCHA szükséges, kérlek használd a következő parancsot "/captcha "' -wrong_captcha: '&cHibás CAPTCHA, kérlek írd be a következő parancsot "/captcha THE_CAPTCHA"!' +wrong_captcha: '&cHibás CAPTCHA, kérlek írd be a következő parancsot: "/captcha THE_CAPTCHA"!' valid_captcha: '&2CAPTCHA sikeresen feloldva!' kick_forvip: '&3VIP játékos csatlakozott a szerverhez!' kick_fullserver: '&4A szerver megtelt, próbálj csatlakozni később!' -usage_email_add: '&cHasználat: "/email add "' -usage_email_change: '&cHasználat: "/email change <új Email>"' +usage_email_add: '&cHasználat: "/email add "' +usage_email_change: '&cHasználat: "/email change <új email>"' usage_email_recovery: '&cHasználat: "/email recovery "' new_email_invalid: '&cHibás az új email cím, próbáld újra!' old_email_invalid: '&cHibás a régi email cím, próbáld újra!' @@ -64,4 +64,5 @@ two_factor_create: '&2A te titkos kódod a következő: %code. Vagy skenneld be denied_chat: '&cAmíg nem vagy bejelentkezve, nem használhatod a csevegőt!' denied_command: '&cAmíg nem vagy bejelentkezve, nem használhatod ezt a parancsot!' same_ip_online: 'Már valaki csatlakozott a szerverhez ezzel az IP címmel!' -# TODO password_error_chars: '&4Your password contains illegal characters. Allowed chars: REG_EX' \ No newline at end of file +password_error_chars: '&4A választott jelszó nem engedélyezett karaktereket tartalmaz. Engedélyezett karakterek: REG_EX' +tempban_max_logins: '&cIdeiglenesen ki lettél tiltva mert túl sok alkalommal rontottad el a jelszavad!' From 98bbf51594a63cccf3eebb708ec5698729c968e3 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Sun, 12 Jun 2016 22:31:11 +0200 Subject: [PATCH 184/200] Update dependency graph --- .../graph_stripped1time_20160612.dot | 60 ++++++++++++++++++ .../graph_stripped1time_20160612.png | Bin 0 -> 272327 bytes .../graph_stripped4times_20160525.dot | 32 ---------- .../graph_stripped4times_20160525.png | Bin 127561 -> 0 bytes 4 files changed, 60 insertions(+), 32 deletions(-) create mode 100644 src/test/java/tools/dependencygraph/graph_stripped1time_20160612.dot create mode 100644 src/test/java/tools/dependencygraph/graph_stripped1time_20160612.png delete mode 100644 src/test/java/tools/dependencygraph/graph_stripped4times_20160525.dot delete mode 100644 src/test/java/tools/dependencygraph/graph_stripped4times_20160525.png diff --git a/src/test/java/tools/dependencygraph/graph_stripped1time_20160612.dot b/src/test/java/tools/dependencygraph/graph_stripped1time_20160612.dot new file mode 100644 index 00000000..afaaf9f2 --- /dev/null +++ b/src/test/java/tools/dependencygraph/graph_stripped1time_20160612.dot @@ -0,0 +1,60 @@ +digraph G { + + "PermissionsManager" -> "ValidationService"; + "NewSetting" -> "ValidationService"; + "DataSource" -> "ValidationService"; + "BukkitService" -> "AntiBot"; + "PermissionsManager" -> "AntiBot"; + "NewSetting" -> "AntiBot"; + "Messages" -> "AntiBot"; + "BukkitService" -> "TeleportationService"; + "PlayerCache" -> "TeleportationService"; + "NewSetting" -> "TeleportationService"; + "Messages" -> "TeleportationService"; + "SpawnLoader" -> "TeleportationService"; + "BukkitService" -> "SynchronousProcess"; + "PluginManager" -> "SynchronousProcess"; + "AuthMe" -> "SynchronousProcess"; + "TeleportationService" -> "SynchronousProcess"; + "LimboPlayerTaskManager" -> "SynchronousProcess"; + "ProcessService" -> "SynchronousProcess"; + "LimboCache" -> "SynchronousProcess"; + "DataSource" -> "SynchronousProcess"; + "BukkitService" -> "TempbanManager"; + "Messages" -> "TempbanManager"; + "NewSetting" -> "TempbanManager"; + "BukkitService" -> "LimboPlayerTaskManager"; + "PlayerCache" -> "LimboPlayerTaskManager"; + "Messages" -> "LimboPlayerTaskManager"; + "NewSetting" -> "LimboPlayerTaskManager"; + "LimboCache" -> "LimboPlayerTaskManager"; + "PluginManager" -> "PasswordSecurity"; + "AuthMeServiceInitializer" -> "PasswordSecurity"; + "NewSetting" -> "PasswordSecurity"; + "DataSource" -> "PasswordSecurity"; + "PluginManager" -> "PluginHooks"; + "Server" -> "PermissionsManager"; + "PluginManager" -> "PermissionsManager"; + "PermissionsManager" -> "LimboCache"; + "SpawnLoader" -> "LimboCache"; + "@DataFolder" -> "SpawnLoader"; + "NewSetting" -> "SpawnLoader"; + "PluginHooks" -> "SpawnLoader"; + "DataSource" -> "SpawnLoader"; + "BukkitService" -> "AsynchronousProcess"; + "CaptchaManager" -> "AsynchronousProcess"; + "SyncProcessManager" -> "AsynchronousProcess"; + "TempbanManager" -> "AsynchronousProcess"; + "PlayerCache" -> "AsynchronousProcess"; + "PasswordSecurity" -> "AsynchronousProcess"; + "LimboCache" -> "AsynchronousProcess"; + "DataSource" -> "AsynchronousProcess"; + "AuthMe" -> "AsynchronousProcess"; + "TeleportationService" -> "AsynchronousProcess"; + "LimboPlayerTaskManager" -> "AsynchronousProcess"; + "PermissionsManager" -> "AsynchronousProcess"; + "ValidationService" -> "AsynchronousProcess"; + "ProcessService" -> "AsynchronousProcess"; + "PluginHooks" -> "AsynchronousProcess"; + "AuthMe" -> "BukkitService"; +} \ No newline at end of file diff --git a/src/test/java/tools/dependencygraph/graph_stripped1time_20160612.png b/src/test/java/tools/dependencygraph/graph_stripped1time_20160612.png new file mode 100644 index 0000000000000000000000000000000000000000..79d3b504060dc6a3f53ef2bd40e81e4ff20d0e00 GIT binary patch literal 272327 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�^{wK6TcmPn4Ej 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≥`<0*aKcb02+t{cdrWGw*WywpFX3! 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 z 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/@q!ReH3oS6x*g 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 +De&>p113(OR~7qJiS!=3W#w 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 "ProcessSyncEmailRegister"; - "LimboCache" -> "ProcessSyncEmailRegister"; - "BukkitService" -> "ProcessSyncEmailRegister"; - "AuthMe" -> "ProcessSyncEmailRegister"; - "AuthMe" -> "ProcessSyncPlayerLogin"; - "ProcessService" -> "ProcessSyncPlayerLogin"; - "LimboCache" -> "ProcessSyncPlayerLogin"; - "DataSource" -> "ProcessSyncPlayerLogin"; - "BukkitService" -> "ProcessSyncPlayerLogin"; - "PluginManager" -> "ProcessSyncPlayerLogin"; - "AuthMe" -> "ProcessSyncPasswordRegister"; - "ProcessService" -> "ProcessSyncPasswordRegister"; - "BukkitService" -> "ProcessSyncPasswordRegister"; - "AuthMe" -> "ProcessSynchronousPlayerLogout"; - "ProcessService" -> "ProcessSynchronousPlayerLogout"; - "LimboCache" -> "ProcessSynchronousPlayerLogout"; - "BukkitService" -> "ProcessSynchronousPlayerLogout"; - "AuthMe" -> "BukkitService"; - "NewSetting" -> "ValidationService"; - "DataSource" -> "ValidationService"; - "PermissionsManager" -> "ValidationService"; - "Server" -> "PermissionsManager"; - "PluginManager" -> "PermissionsManager"; - "PluginManager" -> "PluginHooks"; - "NewSetting" -> "ProcessService"; - "Messages" -> "ProcessService"; - "PluginManager" -> "ProcessService"; - "ValidationService" -> "ProcessService"; - "PermissionsManager" -> "ProcessService"; -} \ No newline at end of file diff --git a/src/test/java/tools/dependencygraph/graph_stripped4times_20160525.png b/src/test/java/tools/dependencygraph/graph_stripped4times_20160525.png deleted file mode 100644 index 537d608e72212eaa78ab461de3a439f978b03897..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 127561 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%0DtPF70yxOiQsq6su$*Ul%0h%i0fg*@9I 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 YqQl(`MbW~_RQS(cZ9}a>4XdF42Uki)-~a#s From 05b259892a73670ad0249426fddbf1a5476948dd Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Mon, 13 Jun 2016 00:58:01 +0200 Subject: [PATCH 185/200] Update messages_vn.yml #666 Thaks to kythuat --- src/main/resources/messages/messages_vn.yml | 132 ++++++++++---------- 1 file changed, 63 insertions(+), 69 deletions(-) diff --git a/src/main/resources/messages/messages_vn.yml b/src/main/resources/messages/messages_vn.yml index b65b32fe..a3939aee 100644 --- a/src/main/resources/messages/messages_vn.yml +++ b/src/main/resources/messages/messages_vn.yml @@ -1,69 +1,63 @@ -unknown_user: '&fNgười chơi không tồn tại trong cơ sở dữ liệu' -unsafe_spawn: '&fNơi thoát server của bạn không an toàn, đang dịch chuyển bạn tới điểm spawn của server' -not_logged_in: '&cChưa đăng nhập!' -reg_voluntarily: '&fBạn có thể đăng kí tài khoản với lệnh "/register mật-khẩu nhập-lại-mật-khẩu"' -usage_log: '&eSử dụng: /login password' -wrong_pwd: '&cSai mật khẩu' -unregistered: '&cHuỷ đăng kí thành công!' -reg_disabled: '&cHệ thống đăng kí đã bị vô hiệu' -valid_session: '&cPhiên đăng nhập còn tồn tại, bạn không cần nhập mật khẩu' -login: '&cĐăng nhập thành công!' -vb_nonActiv: '&fTài khoản của bạn chưa được kích hoạt, kiểm tra email!' -user_regged: '&cTên đăng nhập này đã được đăng kí' -usage_reg: '&eSử dụng: /register mật-khẩu nhập-lại-mật-khẩu' -# TODO max_reg: Missing tags %reg_count, %max_acc, %reg_names -max_reg: '&fSố lượng tài khoản ở IP của bạn trong server này đã quá giới hạn cho phép' -no_perm: '&cKhông có quyền' -error: '&fCó lỗi xảy ra; Báo lại cho người điều hành server' -login_msg: '&cĐăng nhập với lệnh "/login mật-khẩu"' -reg_msg: '&cĐăng kí tài khoản với lệnh "/register mật-khẩu nhập-lại-mật-khẩu"' -reg_email_msg: '&cĐăng kí email cho tài khoản với lệnh "/register "' -usage_unreg: '&eSử dụng: /unregister mật-khẩu' -pwd_changed: '&cĐã đổi mật khẩu!' -user_unknown: '&cTài khoản này chưa được đăng kí' -password_error: '&fMật khẩu không khớp' -invalid_session: '&fPhiên đăng nhập không hồi đáp, vui lòng chờ phiên đăng nhập kết thúc' -reg_only: '&fChỉ cho phép người đã đăng kí! Hãy vào trang http://web-của.bạn/ để đăng kí' -logged_in: '&cĐã đăng nhập!' -logout: '&cThoát đăng nhập thành công' -same_nick: '&fTài khoản đang được người khác sử dụng trong server' -registered: '&cĐăng kí thành công!' -pass_len: '&fMật khẩu của bạn quá ngắn hoặc quá dài' -reload: '&fThiết lập và dữ liệu đã được nạp lại' -timeout: '&fQuá thời gian đăng nhập' -usage_changepassword: '&eSử dụng: /changepassword mật-khẩu-cũ mật-khẩu-mới' -name_len: '&cTên đăng nhập của bạn quá ngắn hoặc quá dài' -regex: '&cTên đăng nhập của bạn có chứa kí tự đặc biệt không được cho phép. Các kí tự hợp lệ: REG_EX' -add_email: '&cVui lòng thêm địa chỉ email cho tài khoản với lệnh: /email add email-của-bạn nhập-lại-email-của-bạn' -recovery_email: '&cQuên mật khẩu? Hãy dùng lệnh /email recovery ' -usage_captcha: '&cBạn cần nhập mã xác nhận: /captcha ' -# TODO wrong_captcha: Missing tag THE_CAPTCHA -wrong_captcha: '&cSai mã xác nhận, nhập lại: /captcha ' -valid_captcha: '&aMã xác nhận hợp lệ!' -kick_forvip: '&cNgười chơi VIP đã vào server hiện đang full!' -kick_fullserver: '&cXin lỗi, hiện tại server không còn trống slot để bạn có thể vào!' -usage_email_add: '&eSử dụng: /email add ' -usage_email_change: '&eSử dụng: /email change ' -usage_email_recovery: '&eSử dụng: /email recovery ' -new_email_invalid: '[AuthMe] Địa chỉ email mới không hợp lệ!' -old_email_invalid: '[AuthMe] Địa chỉ email cũ không hợp lệ!' -email_invalid: '[AuthMe] Sai địa chỉ email' -email_added: '[AuthMe] Đã thêm địa chỉ email !' -email_confirm: '[AuthMe] Xác nhận email !' -email_changed: '[AuthMe] Đã thay đổi email !' -email_send: '[AuthMe] Đã gửi email khôi phục mật khẩu tới bạn !' -country_banned: 'Rất tiếc, quốc gia của bạn không được phép gia nhập server' -antibot_auto_enabled: '[AuthMe] AntiBot đã được kích hoạt vì lượng người chơi kết nối vượt quá giới hạn!' -antibot_auto_disabled: '[AuthMe] AntiBot tự huỷ kích hoạt sau %m phút, hi vọng lượng kết nối sẽ giảm bớt' -# TODO password_error_nick: '&cYou can''t use your name as password, please choose another one...' -# TODO not_owner_error: 'You are not the owner of this account. Please choose another name!' -# TODO two_factor_create: '&2Your secret code is %code. You can scan it from here %url' -# TODO email_already_used: '&4The email address is already being used' -# TODO denied_command: '&cIn order to use this command you must be authenticated!' -# TODO same_ip_online: 'A player with the same IP is already in game!' -# TODO password_error_chars: '&4Your password contains illegal characters. Allowed chars: REG_EX' -# TODO denied_chat: '&cIn order to chat you must be authenticated!' -# TODO password_error_unsafe: '&cThe chosen password isn''t safe, please choose another one...' -# TODO kick_antibot: 'AntiBot protection mode is enabled! You have to wait some minutes before joining the server.' -# TODO email_exists: '&cA recovery email was already sent! You can discard it and send a new one using the command below:' -# TODO invalid_name_case: 'You should join using username %valid, not %invalid.' \ No newline at end of file +kick_antibot: 'Chế độ AntiBot đã được kích hoạt! Bạn phải đợi vài phút trước khi tham gia vào máy chủ.' +unknown_user: '&cTài khoảng không có cơ sở dử liệu' +unsafe_spawn: '&cBạn đang ở vị trí không an toàn, bạn đã được dịch chuyển về Spawn.' +not_logged_in: '&cBạn chưa đăng nhập!' +reg_voluntarily: 'Bạn cần đăng ký tài khoảng để tham gia máy chủ với lệnh "/register "' +usage_log: '&cSử dụng: /login ' +wrong_pwd: '&cSai mật khẩu!' +unregistered: '&cHủy đăng ký thành công!' +reg_disabled: '&cKhông cho phép đăng ký tài khoảng trong máy chủ!' +valid_session: '&2Phiên đăng nhập đã được kết nối trở lại.' +login: '&2Đăng nhập thành công!' +vb_nonActiv: '&cTài khoảng của bạn chưa được kích hoạt, vui lòng kiễm tra email!' +user_regged: '&cBạn có thể đăng ký với tên tài khoảng này!' +usage_reg: '&cSử dụng: /register ' +max_reg: '&cBạn đã vượt quá giới hạn tối đa đăng ký tài khoảng (%reg_count/%max_acc %reg_names) cho những lần kết nối tài khoảng!' +no_perm: '&4Bạn không có quyền truy cập lệnh này!' +error: '&4Lỗi! Vio lòng liên hệ quản trị viên hoặc admin' +login_msg: '&cXin vui lòng đăng nhập bằng lệnh "/login "' +reg_msg: '&2Xin vui lòng đăng ký tài khoảng với lệnh "/register "' +reg_email_msg: '&2Sử dụng email để đăng ký tham gia máy chủ với lệnh "/register "' +usage_unreg: '&cSử dụng: /unregister ' +pwd_changed: '&2Thay đỗi mật khẩu thành công!' +user_unknown: '&cNgười dùng này đã được đăng ký!' +password_error: '&cMật khẩu không hợp, xin vui lòng kiễm tra lại!' +password_error_nick: '&cBạn không thể đặt mật khẩu bằng tên của mình, vui lòng đặt lại...' +password_error_unsafe: '&cMật khẩu của bạn vừa đặt không an toàn, vui lòng đặt lại...' +invalid_session: '&cIP của bạn đã bị thay đổi và phiên đăng nhập của bạn đã hết hạn!' +reg_only: '&4Chỉ có thành viên mới có thể tham gia máy chủ, vui lòng truy cập trang web http://kythuat.mobi/forum/ để đăng ký thành viên!' +logged_in: '&cBạn đã đăng nhập!' +logout: '&2Bạn đã đăng xuất!' +same_nick: '&4Tài khoảng đang được sử dụng trên máy chủ!' +registered: '&2Đăng ký thành công!' +pass_len: '&cMật khẩu của bạn đặt quá dài hoặc quá ngắn, vui lòng đặt lại!' +reload: '&2Cấu hình và cơ sở dử liệu đã được nạp lại!' +timeout: '&4Thời gian đăng nhập đã hết, bạn đã bị văng khỏi máy chủ. Xin vui lòng thử lại!' +usage_changepassword: '&cSử dụng: /changepassword ' +name_len: '&4Tên đăng nhập của bạn quá ngắn hoặc quá dài!' +regex: '&4Tên nhân vật có chứa ký tự không hợp lệ hoặc chữ Hoa, vui lòng đặt lại chử thường, số hoặc _' +add_email: '&eVui lòng thêm email của bạn với lệnh "/email add "' +recovery_email: '&aBạn quên mật khẩu? Vui lòng gõ lệnh "/email recovery "' +usage_captcha: '&eĐể đăng nhập vui lòng hãy gõ mã Captcha, nhấn lệnh "/captcha "' +wrong_captcha: '&cSai mã captcha, Vui lòng nhấn "/captcha THE_CAPTCHA" trong kênh chát!' +valid_captcha: '&2Mã captcha đã được xác nhận!' +kick_forvip: '&eChỉ có thành viên VIP mới được tham gia khi máy chủ đầy!' +kick_fullserver: '&4Máy chủ quá tải, vui lòng thử lại sau!' +usage_email_add: '&cSử dụng: /email add ' +usage_email_change: '&cSử dụng: /email change ' +usage_email_recovery: '&cSử dụng: /email recovery ' +new_email_invalid: '&cEmail mới không hợp lệ, vui lòng thử lại!' +old_email_invalid: '&cEmail củ không hợp lệ, vui lòng thử lại!' +email_invalid: '&cĐại chỉ email không hợp lệ, vui lòng thử lại!' +email_added: '&2Đại chỉ email đã thêm vào tài khoảng của bạn thành công!' +email_confirm: '&cVui lòng xác nhận địa chỉ email của bạn!' +email_changed: '&2Địa chỉ email đã thay đổi!' +email_send: '&2Email phục hồi đã được gửi thành công! Vui lòng kiễm tra hộp thư đến trong email của bạn.' +email_exists: '&cEmail phục hồi đã được gửi, bạn có thể hủy bỏ và gửi thư mới bằng cách sử dụng lệnh sau:' +country_banned: '&4Quốc gia của bạn bị cấm tham gia máy chủ này!' +antibot_auto_enabled: '&4[AntiBotService] AntiBot đã được kích hoạt do số lượng lớn kết nối đến máy chủ!' +antibot_auto_disabled: '&2[AntiBotService] AntiBot đã được tắt sau %m phút!' +email_already_used: '&4Địa chỉ email đã được sử dụng' +two_factor_create: '&2Mã bí mật của bạn là %code. Bạn có thể quét nó tại đây %url' +not_owner_error: 'Bạn không phải là chủ sở hữu tài khoảng này, hãy chọn tên khác!' +invalid_name_case: 'Bạn nên vào máy chủ với tên đăng nhập là %valid, không phải là %invalid.' From 26531e93efdcd8ba13d5e8d31ffc0ce1bf4617c5 Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Mon, 13 Jun 2016 15:29:40 +0200 Subject: [PATCH 186/200] Cleanup --- src/main/java/fr/xephi/authme/cache/TempbanManager.java | 1 - .../command/executable/authme/UnregisterAdminCommand.java | 3 --- .../authme/process/register/ProcessSyncEmailRegister.java | 3 --- .../authme/process/unregister/AsynchronousUnregister.java | 6 ------ .../java/fr/xephi/authme/util/TeleportationService.java | 3 --- 5 files changed, 16 deletions(-) diff --git a/src/main/java/fr/xephi/authme/cache/TempbanManager.java b/src/main/java/fr/xephi/authme/cache/TempbanManager.java index 6372d257..afeb0905 100644 --- a/src/main/java/fr/xephi/authme/cache/TempbanManager.java +++ b/src/main/java/fr/xephi/authme/cache/TempbanManager.java @@ -11,7 +11,6 @@ import org.bukkit.BanList; import org.bukkit.Bukkit; import org.bukkit.entity.Player; -import javax.annotation.PostConstruct; import javax.inject.Inject; import java.util.Date; import java.util.concurrent.ConcurrentHashMap; diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/UnregisterAdminCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/UnregisterAdminCommand.java index a743d9fe..fe67da7a 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/UnregisterAdminCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/UnregisterAdminCommand.java @@ -39,9 +39,6 @@ public class UnregisterAdminCommand implements ExecutableCommand { @Inject private PlayerCache playerCache; - @Inject - private AuthMe authMe; - @Inject private BukkitService bukkitService; diff --git a/src/main/java/fr/xephi/authme/process/register/ProcessSyncEmailRegister.java b/src/main/java/fr/xephi/authme/process/register/ProcessSyncEmailRegister.java index c498d41d..ede9e9ff 100644 --- a/src/main/java/fr/xephi/authme/process/register/ProcessSyncEmailRegister.java +++ b/src/main/java/fr/xephi/authme/process/register/ProcessSyncEmailRegister.java @@ -20,9 +20,6 @@ public class ProcessSyncEmailRegister implements SynchronousProcess { @Inject private ProcessService service; - @Inject - private BukkitService bukkitService; - @Inject private LimboPlayerTaskManager limboPlayerTaskManager; diff --git a/src/main/java/fr/xephi/authme/process/unregister/AsynchronousUnregister.java b/src/main/java/fr/xephi/authme/process/unregister/AsynchronousUnregister.java index 41f9a2d9..9d96d24c 100644 --- a/src/main/java/fr/xephi/authme/process/unregister/AsynchronousUnregister.java +++ b/src/main/java/fr/xephi/authme/process/unregister/AsynchronousUnregister.java @@ -27,9 +27,6 @@ import static fr.xephi.authme.util.BukkitService.TICKS_PER_SECOND; public class AsynchronousUnregister implements AsynchronousProcess { - @Inject - private AuthMe plugin; - @Inject private DataSource dataSource; @@ -45,9 +42,6 @@ public class AsynchronousUnregister implements AsynchronousProcess { @Inject private LimboCache limboCache; - @Inject - private BukkitService bukkitService; - @Inject private LimboPlayerTaskManager limboPlayerTaskManager; diff --git a/src/main/java/fr/xephi/authme/util/TeleportationService.java b/src/main/java/fr/xephi/authme/util/TeleportationService.java index 1042caf4..e757d4e8 100644 --- a/src/main/java/fr/xephi/authme/util/TeleportationService.java +++ b/src/main/java/fr/xephi/authme/util/TeleportationService.java @@ -31,9 +31,6 @@ public class TeleportationService implements Reloadable { @Inject private NewSetting settings; - @Inject - private Messages messages; - @Inject private BukkitService bukkitService; From e12ae2cf9602cba6ff29411ffbd5e07c58cb6d3c Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Mon, 13 Jun 2016 16:13:03 +0200 Subject: [PATCH 187/200] Use spaces, finish working on #423, import cleanup --- .../authme/UnregisterAdminCommand.java | 1 - .../fr/xephi/authme/mail/SendMailSSL.java | 2 +- .../fr/xephi/authme/output/Log4JFilter.java | 8 +-- .../process/login/AsynchronousLogin.java | 37 +++++++++-- .../register/ProcessSyncEmailRegister.java | 1 - .../unregister/AsynchronousUnregister.java | 2 - .../authme/security/crypts/BCryptService.java | 64 +++++++++---------- .../authme/util/TeleportationService.java | 1 - 8 files changed, 67 insertions(+), 49 deletions(-) diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/UnregisterAdminCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/UnregisterAdminCommand.java index fe67da7a..064ea536 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/UnregisterAdminCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/UnregisterAdminCommand.java @@ -1,6 +1,5 @@ package fr.xephi.authme.command.executable.authme; -import fr.xephi.authme.AuthMe; import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.cache.auth.PlayerCache; import fr.xephi.authme.cache.limbo.LimboCache; diff --git a/src/main/java/fr/xephi/authme/mail/SendMailSSL.java b/src/main/java/fr/xephi/authme/mail/SendMailSSL.java index 88d57d9a..58b64d6b 100644 --- a/src/main/java/fr/xephi/authme/mail/SendMailSSL.java +++ b/src/main/java/fr/xephi/authme/mail/SendMailSSL.java @@ -131,7 +131,7 @@ public class SendMailSSL { .replace("", newPass); } - private static void setPropertiesForPort(HtmlEmail email, int port, NewSetting settings) + private static void setPropertiesForPort(HtmlEmail email, int port, NewSetting settings) throws EmailException { switch (port) { case 587: diff --git a/src/main/java/fr/xephi/authme/output/Log4JFilter.java b/src/main/java/fr/xephi/authme/output/Log4JFilter.java index 336af779..8bc52452 100644 --- a/src/main/java/fr/xephi/authme/output/Log4JFilter.java +++ b/src/main/java/fr/xephi/authme/output/Log4JFilter.java @@ -59,16 +59,16 @@ public class Log4JFilter implements Filter { @Override public Result filter(Logger arg0, Level arg1, Marker arg2, String message, Object... arg4) { - if (message == null) { - return Result.NEUTRAL; + if (message == null) { + return Result.NEUTRAL; } return validateMessage(message); } @Override public Result filter(Logger arg0, Level arg1, Marker arg2, Object message, Throwable arg4) { - if (message == null) { - return Result.NEUTRAL; + if (message == null) { + return Result.NEUTRAL; } return validateMessage(message.toString()); } diff --git a/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java b/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java index 7f2fd8f5..2ea6fd07 100644 --- a/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java +++ b/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java @@ -29,9 +29,12 @@ import fr.xephi.authme.util.BukkitService; import fr.xephi.authme.util.StringUtils; import fr.xephi.authme.util.Utils; import org.bukkit.Bukkit; +import org.bukkit.ChatColor; import org.bukkit.entity.Player; import javax.inject.Inject; + +import java.util.ArrayList; import java.util.List; /** @@ -150,11 +153,18 @@ public class AsynchronousLogin implements AsynchronousProcess { public void login(final Player player, String password, boolean forceLogin) { PlayerAuth pAuth = preAuth(player); - if (pAuth == null || needsCaptcha(player)) { + if (pAuth == null) { return; } final String name = player.getName().toLowerCase(); + + // If Captcha is required send a message to the player and deny to login + if (needsCaptcha(player)) { + service.send(player, MessageKey.USAGE_CAPTCHA, captchaManager.getCaptchaCodeOrGenerateNew(name)); + return; + } + final String ip = Utils.getPlayerIp(player); // Increase the counts here before knowing the result of the login. @@ -232,7 +242,7 @@ public class AsynchronousLogin implements AsynchronousProcess { } else { service.send(player, MessageKey.WRONG_PASSWORD); - // Check again if a captcha is required to log in + // If the authentication fails check if Captcha is required and send a message to the player if (needsCaptcha(player)) { service.send(player, MessageKey.USAGE_CAPTCHA, captchaManager.getCaptchaCodeOrGenerateNew(name)); } @@ -249,14 +259,27 @@ public class AsynchronousLogin implements AsynchronousProcess { } List auths = database.getAllAuthsByIp(auth.getIp()); - if (auths.size() < 2) { + if (auths.size() <= 1) { return; } - // TODO #423: color player names with green if the account is online - String message = StringUtils.join(", ", auths) + "."; - ConsoleLogger.info("The user " + player.getName() + " has " + auths.size() + " accounts:"); - ConsoleLogger.info(message); + List tmp = new ArrayList(); + for(String currentName : auths) { + Player currentPlayer = bukkitService.getPlayerExact(currentName); + if(currentPlayer != null && currentPlayer.isOnline()) { + tmp.add(ChatColor.GREEN + currentName); + } else { + tmp.add(currentName); + } + } + auths = tmp; + + String message = StringUtils.join(ChatColor.GRAY + ", ", auths) + "."; + + if(!service.getProperty(SecuritySettings.REMOVE_SPAM_FROM_CONSOLE)) { + ConsoleLogger.info("The user " + player.getName() + " has " + auths.size() + " accounts:"); + ConsoleLogger.info(message); + } for (Player onlinePlayer : bukkitService.getOnlinePlayers()) { if (onlinePlayer.getName().equalsIgnoreCase(player.getName()) diff --git a/src/main/java/fr/xephi/authme/process/register/ProcessSyncEmailRegister.java b/src/main/java/fr/xephi/authme/process/register/ProcessSyncEmailRegister.java index ede9e9ff..15901606 100644 --- a/src/main/java/fr/xephi/authme/process/register/ProcessSyncEmailRegister.java +++ b/src/main/java/fr/xephi/authme/process/register/ProcessSyncEmailRegister.java @@ -8,7 +8,6 @@ import fr.xephi.authme.process.SynchronousProcess; import fr.xephi.authme.settings.Settings; import fr.xephi.authme.settings.properties.SecuritySettings; import fr.xephi.authme.task.LimboPlayerTaskManager; -import fr.xephi.authme.util.BukkitService; import fr.xephi.authme.util.Utils; import org.bukkit.entity.Player; diff --git a/src/main/java/fr/xephi/authme/process/unregister/AsynchronousUnregister.java b/src/main/java/fr/xephi/authme/process/unregister/AsynchronousUnregister.java index 9d96d24c..87cb4808 100644 --- a/src/main/java/fr/xephi/authme/process/unregister/AsynchronousUnregister.java +++ b/src/main/java/fr/xephi/authme/process/unregister/AsynchronousUnregister.java @@ -1,6 +1,5 @@ package fr.xephi.authme.process.unregister; -import fr.xephi.authme.AuthMe; import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.cache.auth.PlayerAuth; import fr.xephi.authme.cache.auth.PlayerCache; @@ -15,7 +14,6 @@ import fr.xephi.authme.settings.Settings; import fr.xephi.authme.settings.properties.RegistrationSettings; import fr.xephi.authme.settings.properties.RestrictionSettings; import fr.xephi.authme.task.LimboPlayerTaskManager; -import fr.xephi.authme.util.BukkitService; import fr.xephi.authme.util.Utils; import org.bukkit.entity.Player; import org.bukkit.potion.PotionEffect; diff --git a/src/main/java/fr/xephi/authme/security/crypts/BCryptService.java b/src/main/java/fr/xephi/authme/security/crypts/BCryptService.java index 194ea7b6..6b82305c 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/BCryptService.java +++ b/src/main/java/fr/xephi/authme/security/crypts/BCryptService.java @@ -380,9 +380,9 @@ public class BCryptService { * encoding scheme. Note that this is *not* compatible with * the standard MIME-base64 encoding. * - * @param d the byte array to encode - * @param len the number of bytes to encode - * @return base64-encoded string + * @param d the byte array to encode + * @param len the number of bytes to encode + * @return base64-encoded string * @exception IllegalArgumentException if the length is invalid */ private static String encode_base64(byte d[], int len) @@ -421,8 +421,8 @@ public class BCryptService { /** * Look up the 3 bits base64-encoded by the specified character, * range-checking againt conversion table - * @param x the base64-encoded value - * @return the decoded value of x + * @param x the base64-encoded value + * @return the decoded value of x */ private static byte char64(char x) { if ((int)x < 0 || (int)x > index_64.length) @@ -434,9 +434,9 @@ public class BCryptService { * Decode a string encoded using bcrypt's base64 scheme to a * byte array. Note that this is *not* compatible with * the standard MIME-base64 encoding. - * @param s the string to decode - * @param maxolen the maximum number of bytes to decode - * @return an array containing the decoded bytes + * @param s the string to decode + * @param maxolen the maximum number of bytes to decode + * @return an array containing the decoded bytes * @throws IllegalArgumentException if maxolen is invalid */ private static byte[] decode_base64(String s, int maxolen) @@ -483,8 +483,8 @@ public class BCryptService { /** * Blowfish encipher a single 64-bit block encoded as * two 32-bit halves - * @param lr an array containing the two 32-bit half blocks - * @param off the position in the array of the blocks + * @param lr an array containing the two 32-bit half blocks + * @param off the position in the array of the blocks */ private void encipher(int lr[], int off) { int i, n, l = lr[off], r = lr[off + 1]; @@ -511,10 +511,10 @@ public class BCryptService { /** * Cycically extract a word of key material - * @param data the string to extract the data from - * @param offp a "pointer" (as a one-entry array) to the + * @param data the string to extract the data from + * @param offp a "pointer" (as a one-entry array) to the * current offset into data - * @return the next word of material from data + * @return the next word of material from data */ private static int streamtoword(byte data[], int offp[]) { int i; @@ -540,7 +540,7 @@ public class BCryptService { /** * Key the Blowfish cipher - * @param key an array containing the key + * @param key an array containing the key */ private void key(byte key[]) { int i; @@ -568,8 +568,8 @@ public class BCryptService { * Perform the "enhanced key schedule" step described by * Provos and Mazieres in "A Future-Adaptable Password Scheme" * http://www.openbsd.org/papers/bcrypt-paper.ps - * @param data salt information - * @param key password information + * @param data salt information + * @param key password information */ private void ekskey(byte data[], byte key[]) { int i; @@ -600,12 +600,12 @@ public class BCryptService { /** * Perform the central password hashing step in the * bcrypt scheme - * @param password the password to hash - * @param salt the binary salt to hash with the password - * @param log_rounds the binary logarithm of the number + * @param password the password to hash + * @param salt the binary salt to hash with the password + * @param log_rounds the binary logarithm of the number * of rounds of hashing to apply * @param cdata the plaintext to encrypt - * @return an array containing the binary hashed password + * @return an array containing the binary hashed password */ public byte[] crypt_raw(byte password[], byte salt[], int log_rounds, int cdata[]) { @@ -643,10 +643,10 @@ public class BCryptService { /** * Hash a password using the OpenBSD bcrypt scheme - * @param password the password to hash - * @param salt the salt to hash with (perhaps generated + * @param password the password to hash + * @param salt the salt to hash with (perhaps generated * using BCrypt.gensalt) - * @return the hashed password + * @return the hashed password */ public static String hashpw(String password, String salt) { BCryptService B; @@ -706,11 +706,11 @@ public class BCryptService { /** * Generate a salt for use with the BCrypt.hashpw() method - * @param log_rounds the log2 of the number of rounds of + * @param log_rounds the log2 of the number of rounds of * hashing to apply - the work factor therefore increases as * 2**log_rounds. - * @param random an instance of SecureRandom to use - * @return an encoded salt value + * @param random an instance of SecureRandom to use + * @return an encoded salt value */ public static String gensalt(int log_rounds, SecureRandom random) { StringBuilder rs = new StringBuilder(); @@ -733,10 +733,10 @@ public class BCryptService { /** * Generate a salt for use with the BCrypt.hashpw() method - * @param log_rounds the log2 of the number of rounds of + * @param log_rounds the log2 of the number of rounds of * hashing to apply - the work factor therefore increases as * 2**log_rounds. - * @return an encoded salt value + * @return an encoded salt value */ public static String gensalt(int log_rounds) { return gensalt(log_rounds, new SecureRandom()); @@ -746,7 +746,7 @@ public class BCryptService { * Generate a salt for use with the BCrypt.hashpw() method, * selecting a reasonable default for the number of hashing * rounds to apply - * @return an encoded salt value + * @return an encoded salt value */ public static String gensalt() { return gensalt(GENSALT_DEFAULT_LOG2_ROUNDS); @@ -755,9 +755,9 @@ public class BCryptService { /** * Check that a plaintext password matches a previously hashed * one - * @param plaintext the plaintext password to verify - * @param hashed the previously-hashed password - * @return true if the passwords match, false otherwise + * @param plaintext the plaintext password to verify + * @param hashed the previously-hashed password + * @return true if the passwords match, false otherwise */ public static boolean checkpw(String plaintext, String hashed) { byte hashed_bytes[]; diff --git a/src/main/java/fr/xephi/authme/util/TeleportationService.java b/src/main/java/fr/xephi/authme/util/TeleportationService.java index e757d4e8..06c7558d 100644 --- a/src/main/java/fr/xephi/authme/util/TeleportationService.java +++ b/src/main/java/fr/xephi/authme/util/TeleportationService.java @@ -8,7 +8,6 @@ import fr.xephi.authme.events.AuthMeTeleportEvent; import fr.xephi.authme.events.FirstSpawnTeleportEvent; import fr.xephi.authme.events.SpawnTeleportEvent; import fr.xephi.authme.initialization.Reloadable; -import fr.xephi.authme.output.Messages; import fr.xephi.authme.settings.NewSetting; import fr.xephi.authme.settings.SpawnLoader; import fr.xephi.authme.settings.properties.RestrictionSettings; From 58278a2bbed259ac6fbbb9bf15d14742af1e2d8d Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Mon, 13 Jun 2016 17:26:06 +0200 Subject: [PATCH 188/200] Update messages_de.yml #763 --- src/main/resources/messages/messages_de.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/resources/messages/messages_de.yml b/src/main/resources/messages/messages_de.yml index cef029ca..95f578e7 100644 --- a/src/main/resources/messages/messages_de.yml +++ b/src/main/resources/messages/messages_de.yml @@ -65,3 +65,4 @@ not_owner_error: 'Du bist nicht der Besitzer dieses Accounts. Bitte wähle einen denied_chat: '&cDu musst eingeloggt sein, um chatten zu können!' same_ip_online: 'Ein Spieler mit derselben IP ist bereits online!' denied_command: '&cUm diesen Befehl zu nutzen musst du authentifiziert sein!' +tempban_max_logins: '&cDu bist wegen zu vielen fehlgeschlagenen Login-Versuchen temporär gebannt!' From d5ce172e1435c0cd8e0ad9de9dae46a5942b3adf Mon Sep 17 00:00:00 2001 From: ljacqu Date: Mon, 13 Jun 2016 19:19:00 +0200 Subject: [PATCH 189/200] #729 Make FirstSpawn event synchronous --- .../authme/events/AbstractTeleportEvent.java | 20 ++++--------------- .../events/FirstSpawnTeleportEvent.java | 2 +- 2 files changed, 5 insertions(+), 17 deletions(-) diff --git a/src/main/java/fr/xephi/authme/events/AbstractTeleportEvent.java b/src/main/java/fr/xephi/authme/events/AbstractTeleportEvent.java index 57a456b6..51d366d9 100644 --- a/src/main/java/fr/xephi/authme/events/AbstractTeleportEvent.java +++ b/src/main/java/fr/xephi/authme/events/AbstractTeleportEvent.java @@ -19,25 +19,13 @@ public abstract class AbstractTeleportEvent extends CustomEvent implements Cance * * @param isAsync Whether to fire the event asynchronously or not * @param player The player - * @param from The location the player is being teleported away from - * @param to The teleport destination - */ - public AbstractTeleportEvent(boolean isAsync, Player player, Location from, Location to) { - super(isAsync); - this.player = player; - this.from = from; - this.to = to; - } - - /** - * Constructor, using the player's current location as "from" location. - * - * @param isAsync Whether to fire the event asynchronously or not - * @param player The player * @param to The teleport destination */ public AbstractTeleportEvent(boolean isAsync, Player player, Location to) { - this(isAsync, player, player.getLocation(), to); + super(isAsync); + this.player = player; + this.from = player.getLocation(); + this.to = to; } /** diff --git a/src/main/java/fr/xephi/authme/events/FirstSpawnTeleportEvent.java b/src/main/java/fr/xephi/authme/events/FirstSpawnTeleportEvent.java index 6a90e5dd..e5e6868d 100644 --- a/src/main/java/fr/xephi/authme/events/FirstSpawnTeleportEvent.java +++ b/src/main/java/fr/xephi/authme/events/FirstSpawnTeleportEvent.java @@ -20,7 +20,7 @@ public class FirstSpawnTeleportEvent extends AbstractTeleportEvent { * @param to The teleport destination */ public FirstSpawnTeleportEvent(Player player, Location to) { - super(true, player, to); + super(false, player, to); } /** From 209625ab00bdd6432b3973ee710a208be48095c8 Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Mon, 13 Jun 2016 20:04:02 +0200 Subject: [PATCH 190/200] Update maven plugins --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index a11bc97e..ec21ff63 100644 --- a/pom.xml +++ b/pom.xml @@ -271,7 +271,7 @@ org.jacoco jacoco-maven-plugin - 0.7.6.201602180812 + 0.7.7.201606060606 prepare-agent @@ -295,7 +295,7 @@ org.apache.maven.plugins maven-javadoc-plugin - 2.10.3 + 2.10.4 public false From 221ce13176f5c80a2cb40556927e49aa46940b29 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Mon, 13 Jun 2016 20:45:56 +0200 Subject: [PATCH 191/200] #423 Display first name gray also --- .../fr/xephi/authme/process/login/AsynchronousLogin.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java b/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java index 2ea6fd07..83b453dd 100644 --- a/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java +++ b/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java @@ -264,9 +264,9 @@ public class AsynchronousLogin implements AsynchronousProcess { } List tmp = new ArrayList(); - for(String currentName : auths) { + for (String currentName : auths) { Player currentPlayer = bukkitService.getPlayerExact(currentName); - if(currentPlayer != null && currentPlayer.isOnline()) { + if (currentPlayer != null && currentPlayer.isOnline()) { tmp.add(ChatColor.GREEN + currentName); } else { tmp.add(currentName); @@ -274,9 +274,9 @@ public class AsynchronousLogin implements AsynchronousProcess { } auths = tmp; - String message = StringUtils.join(ChatColor.GRAY + ", ", auths) + "."; + String message = ChatColor.GRAY + StringUtils.join(ChatColor.GRAY + ", ", auths) + "."; - if(!service.getProperty(SecuritySettings.REMOVE_SPAM_FROM_CONSOLE)) { + if (!service.getProperty(SecuritySettings.REMOVE_SPAM_FROM_CONSOLE)) { ConsoleLogger.info("The user " + player.getName() + " has " + auths.size() + " accounts:"); ConsoleLogger.info(message); } From b7015f56c2f30731840ea9e82eb040575a2fdef5 Mon Sep 17 00:00:00 2001 From: EbonJaguar Date: Mon, 13 Jun 2016 14:55:25 -0400 Subject: [PATCH 192/200] kick player on tempban, change setting comments to better reflect the process --- src/main/java/fr/xephi/authme/cache/TempbanManager.java | 3 ++- .../xephi/authme/settings/properties/SecuritySettings.java | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/java/fr/xephi/authme/cache/TempbanManager.java b/src/main/java/fr/xephi/authme/cache/TempbanManager.java index afeb0905..eacbd183 100644 --- a/src/main/java/fr/xephi/authme/cache/TempbanManager.java +++ b/src/main/java/fr/xephi/authme/cache/TempbanManager.java @@ -86,7 +86,7 @@ public class TempbanManager implements SettingsDependent { * * @param player The player to tempban */ - public void tempbanPlayer(Player player) { + public void tempbanPlayer(final Player player) { if (isEnabled) { resetCount(player.getName()); @@ -101,6 +101,7 @@ public class TempbanManager implements SettingsDependent { @Override public void run() { Bukkit.getServer().getBanList(BanList.Type.IP).addBan(ip, reason, expires, "AuthMe"); + player.kickPlayer(reason); } }); } diff --git a/src/main/java/fr/xephi/authme/settings/properties/SecuritySettings.java b/src/main/java/fr/xephi/authme/settings/properties/SecuritySettings.java index 365f4a9a..ddc98dbd 100644 --- a/src/main/java/fr/xephi/authme/settings/properties/SecuritySettings.java +++ b/src/main/java/fr/xephi/authme/settings/properties/SecuritySettings.java @@ -100,15 +100,15 @@ public class SecuritySettings implements SettingsClass { public static final Property> UNSAFE_PASSWORDS = newLowercaseListProperty("settings.security.unsafePasswords", "123456", "password", "qwerty", "12345", "54321"); - @Comment("Tempban users if they enter the wrong password too many times") + @Comment("Tempban a user's IP address if they enter the wrong password too many times") public static final Property TEMPBAN_ON_MAX_LOGINS = newProperty("Security.tempban.enableTempban", false); - @Comment("How many times a user can attempt to login before being tempbanned") + @Comment("How many times a user can attempt to login before their IP being tempbanned") public static final Property MAX_LOGIN_TEMPBAN = newProperty("Security.tempban.maxLoginTries", 10); - @Comment({"The length of time a player will be tempbanned in minutes", + @Comment({"The length of time a IP address will be tempbanned in minutes", "Default: 480 minutes, or 8 hours"}) public static final Property TEMPBAN_LENGTH = newProperty("Security.tempban.tempbanLength", 480); From 043ee902541cbe5a0902a654f0a511ee60cb2e35 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Mon, 13 Jun 2016 21:45:21 +0200 Subject: [PATCH 193/200] #423 Make seeownaccounts / seeotheraccounts messages translatable --- .../java/fr/xephi/authme/output/MessageKey.java | 6 +++++- .../authme/process/login/AsynchronousLogin.java | 15 +++++++-------- src/main/resources/messages/messages_de.yml | 2 ++ src/main/resources/messages/messages_en.yml | 2 ++ 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/main/java/fr/xephi/authme/output/MessageKey.java b/src/main/java/fr/xephi/authme/output/MessageKey.java index 7bd27621..670366f5 100644 --- a/src/main/java/fr/xephi/authme/output/MessageKey.java +++ b/src/main/java/fr/xephi/authme/output/MessageKey.java @@ -139,7 +139,11 @@ public enum MessageKey { INVALID_NAME_CASE("invalid_name_case", "%valid", "%invalid"), - TEMPBAN_MAX_LOGINS("tempban_max_logins"); + TEMPBAN_MAX_LOGINS("tempban_max_logins"), + + ACCOUNTS_OWNED_SELF("accounts_owned_self", "%count"), + + ACCOUNTS_OWNED_OTHER("accounts_owned_other", "%name", "%count"); private String key; private String[] tags; diff --git a/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java b/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java index 83b453dd..e60851e3 100644 --- a/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java +++ b/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java @@ -252,7 +252,6 @@ public class AsynchronousLogin implements AsynchronousProcess { } } - // TODO #423: allow translation! private void displayOtherAccounts(PlayerAuth auth, Player player) { if (!service.getProperty(RestrictionSettings.DISPLAY_OTHER_ACCOUNTS) || auth == null) { return; @@ -263,18 +262,17 @@ public class AsynchronousLogin implements AsynchronousProcess { return; } - List tmp = new ArrayList(); + List formattedNames = new ArrayList<>(auths.size()); for (String currentName : auths) { Player currentPlayer = bukkitService.getPlayerExact(currentName); if (currentPlayer != null && currentPlayer.isOnline()) { - tmp.add(ChatColor.GREEN + currentName); + formattedNames.add(ChatColor.GREEN + currentName); } else { - tmp.add(currentName); + formattedNames.add(currentName); } } - auths = tmp; - String message = ChatColor.GRAY + StringUtils.join(ChatColor.GRAY + ", ", auths) + "."; + String message = ChatColor.GRAY + StringUtils.join(ChatColor.GRAY + ", ", formattedNames) + "."; if (!service.getProperty(SecuritySettings.REMOVE_SPAM_FROM_CONSOLE)) { ConsoleLogger.info("The user " + player.getName() + " has " + auths.size() + " accounts:"); @@ -284,10 +282,11 @@ public class AsynchronousLogin implements AsynchronousProcess { for (Player onlinePlayer : bukkitService.getOnlinePlayers()) { if (onlinePlayer.getName().equalsIgnoreCase(player.getName()) && permissionsManager.hasPermission(onlinePlayer, PlayerPermission.SEE_OWN_ACCOUNTS)) { - onlinePlayer.sendMessage("You own " + auths.size() + " accounts:"); + service.send(onlinePlayer, MessageKey.ACCOUNTS_OWNED_SELF, Integer.toString(auths.size())); onlinePlayer.sendMessage(message); } else if (permissionsManager.hasPermission(onlinePlayer, AdminPermission.SEE_OTHER_ACCOUNTS)) { - onlinePlayer.sendMessage("The user " + player.getName() + " has " + auths.size() + " accounts:"); + service.send(onlinePlayer, MessageKey.ACCOUNTS_OWNED_OTHER, + player.getName(), Integer.toString(auths.size())); onlinePlayer.sendMessage(message); } } diff --git a/src/main/resources/messages/messages_de.yml b/src/main/resources/messages/messages_de.yml index 95f578e7..c196f3b0 100644 --- a/src/main/resources/messages/messages_de.yml +++ b/src/main/resources/messages/messages_de.yml @@ -66,3 +66,5 @@ denied_chat: '&cDu musst eingeloggt sein, um chatten zu können!' same_ip_online: 'Ein Spieler mit derselben IP ist bereits online!' denied_command: '&cUm diesen Befehl zu nutzen musst du authentifiziert sein!' tempban_max_logins: '&cDu bist wegen zu vielen fehlgeschlagenen Login-Versuchen temporär gebannt!' +accounts_owned_self: 'Du besitzt %count Accounts:' +accounts_owned_other: 'Der Spieler %name hat %count Accounts:' diff --git a/src/main/resources/messages/messages_en.yml b/src/main/resources/messages/messages_en.yml index aabbae89..f2926eb9 100644 --- a/src/main/resources/messages/messages_en.yml +++ b/src/main/resources/messages/messages_en.yml @@ -66,3 +66,5 @@ two_factor_create: '&2Your secret code is %code. You can scan it from here %url' not_owner_error: 'You are not the owner of this account. Please choose another name!' invalid_name_case: 'You should join using username %valid, not %invalid.' tempban_max_logins: '&cYou have been temporarily banned for failing to log in too many times.' +accounts_owned_self: 'You own %count accounts:' +accounts_owned_other: 'The player %name has %count accounts:' From 367f78561055ae43e18877cf3022ef1cfdf9c03e Mon Sep 17 00:00:00 2001 From: EbonJaguar Date: Mon, 13 Jun 2016 15:58:03 -0400 Subject: [PATCH 194/200] count login failures by ip address and not by name --- .../fr/xephi/authme/cache/TempbanManager.java | 46 ++++++++-------- .../process/login/AsynchronousLogin.java | 20 ++----- .../authme/cache/TempbanManagerTest.java | 54 +++++++++---------- 3 files changed, 55 insertions(+), 65 deletions(-) diff --git a/src/main/java/fr/xephi/authme/cache/TempbanManager.java b/src/main/java/fr/xephi/authme/cache/TempbanManager.java index eacbd183..8042b603 100644 --- a/src/main/java/fr/xephi/authme/cache/TempbanManager.java +++ b/src/main/java/fr/xephi/authme/cache/TempbanManager.java @@ -18,9 +18,10 @@ import java.util.concurrent.ConcurrentHashMap; /** * Manager for handling tempbans */ +// TODO Gnat008 20160613: Figure out the best way to remove entries based on time public class TempbanManager implements SettingsDependent { - private final ConcurrentHashMap playerCounts; + private final ConcurrentHashMap ipLoginFailureCounts; private final long MINUTE_IN_MILLISECONDS = 60000; @@ -34,45 +35,49 @@ public class TempbanManager implements SettingsDependent { @Inject TempbanManager(BukkitService bukkitService, Messages messages, NewSetting settings) { - this.playerCounts = new ConcurrentHashMap<>(); + this.ipLoginFailureCounts = new ConcurrentHashMap<>(); this.bukkitService = bukkitService; this.messages = messages; loadSettings(settings); } /** - * Increases the failure count for the given player. + * Increases the failure count for the given IP address. * - * @param name the player's name + * @param address The player's IP address */ - public void increaseCount(String name) { + public void increaseCount(String address) { if (isEnabled) { - String nameLower = name.toLowerCase(); - Integer count = playerCounts.get(nameLower); + Integer count = ipLoginFailureCounts.get(address); if (count == null) { - playerCounts.put(nameLower, 1); + ipLoginFailureCounts.put(address, 1); } else { - playerCounts.put(nameLower, count + 1); + ipLoginFailureCounts.put(address, count + 1); } } } - public void resetCount(String name) { + /** + * Set the failure count for a given IP address to 0. + * + * @param address The IP address + */ + public void resetCount(String address) { if (isEnabled) { - playerCounts.remove(name.toLowerCase()); + ipLoginFailureCounts.remove(address); } } /** - * Return whether the player should be tempbanned. + * Return whether the IP address should be tempbanned. * - * @param name The player's name - * @return True if the player should be tempbanned + * @param address The player's IP address + * @return True if the IP should be tempbanned */ - public boolean shouldTempban(String name) { + public boolean shouldTempban(String address) { if (isEnabled) { - Integer count = playerCounts.get(name.toLowerCase()); + Integer count = ipLoginFailureCounts.get(address); return count != null && count >= threshold; } @@ -80,16 +85,13 @@ public class TempbanManager implements SettingsDependent { } /** - * Tempban a player for failing to log in too many times. - * This bans the player's IP address, and calculates the expire - * time based on the time the method was called. + * Tempban a player's IP address for failing to log in too many times. + * This calculates the expire time based on the time the method was called. * * @param player The player to tempban */ public void tempbanPlayer(final Player player) { if (isEnabled) { - resetCount(player.getName()); - final String ip = Utils.getPlayerIp(player); final String reason = messages.retrieveSingle(MessageKey.TEMPBAN_MAX_LOGINS); @@ -104,6 +106,8 @@ public class TempbanManager implements SettingsDependent { player.kickPlayer(reason); } }); + + resetCount(ip); } } diff --git a/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java b/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java index e60851e3..33b6defa 100644 --- a/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java +++ b/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java @@ -90,19 +90,6 @@ public class AsynchronousLogin implements AsynchronousProcess { return captchaManager.isCaptchaRequired(playerName); } - /** - * Queries the {@link fr.xephi.authme.cache.TempbanManager} to - * see if the player has reached the tempban threshold. - * - * @param player The player to check - * @return True if the player needs to be tempbanned - */ - private boolean shouldTempban(Player player) { - final String playerName = player.getName(); - - return tempbanManager.shouldTempban(playerName); - } - /** * Checks the precondition for authentication (like user known) and returns * the playerAuth-State @@ -168,9 +155,9 @@ public class AsynchronousLogin implements AsynchronousProcess { final String ip = Utils.getPlayerIp(player); // Increase the counts here before knowing the result of the login. - // If the login is successful, we clear the count for the player. + // If the login is successful, we clear the captcha count for the player. captchaManager.increaseCount(name); - tempbanManager.increaseCount(name); + tempbanManager.increaseCount(ip); if ("127.0.0.1".equals(pAuth.getIp()) && !pAuth.getIp().equals(ip)) { pAuth.setIp(ip); @@ -191,7 +178,6 @@ public class AsynchronousLogin implements AsynchronousProcess { database.updateSession(auth); captchaManager.resetCounts(name); - tempbanManager.resetCount(name); player.setNoDamageTicks(0); if (!forceLogin) @@ -237,7 +223,7 @@ public class AsynchronousLogin implements AsynchronousProcess { player.kickPlayer(service.retrieveSingleMessage(MessageKey.WRONG_PASSWORD)); } }); - } else if (shouldTempban(player)) { + } else if (tempbanManager.shouldTempban(ip)) { tempbanManager.tempbanPlayer(player); } else { service.send(player, MessageKey.WRONG_PASSWORD); diff --git a/src/test/java/fr/xephi/authme/cache/TempbanManagerTest.java b/src/test/java/fr/xephi/authme/cache/TempbanManagerTest.java index bf02253b..e960a084 100644 --- a/src/test/java/fr/xephi/authme/cache/TempbanManagerTest.java +++ b/src/test/java/fr/xephi/authme/cache/TempbanManagerTest.java @@ -34,76 +34,76 @@ public class TempbanManagerTest { // given NewSetting settings = mockSettings(3, 60); TempbanManager manager = new TempbanManager(bukkitService, messages, settings); - String player = "Tester"; + String address = "192.168.1.1"; // when for (int i = 0; i < 2; ++i) { - manager.increaseCount(player); + manager.increaseCount(address); } // then - assertThat(manager.shouldTempban(player), equalTo(false)); - manager.increaseCount(player); - assertThat(manager.shouldTempban(player), equalTo(true)); - assertThat(manager.shouldTempban("otherPlayer"), equalTo(false)); + assertThat(manager.shouldTempban(address), equalTo(false)); + manager.increaseCount(address); + assertThat(manager.shouldTempban(address), equalTo(true)); + assertThat(manager.shouldTempban("10.0.0.1"), equalTo(false)); } @Test public void shouldIncreaseAndResetCount() { // given - String player = "plaYah"; + String address = "192.168.1.2"; NewSetting settings = mockSettings(3, 60); TempbanManager manager = new TempbanManager(bukkitService, messages, settings); // when - manager.increaseCount(player); - manager.increaseCount(player); - manager.increaseCount(player); + manager.increaseCount(address); + manager.increaseCount(address); + manager.increaseCount(address); // then - assertThat(manager.shouldTempban(player), equalTo(true)); - assertHasCount(manager, player, 3); + assertThat(manager.shouldTempban(address), equalTo(true)); + assertHasCount(manager, address, 3); // when 2 - manager.resetCount(player); + manager.resetCount(address); // then 2 - assertThat(manager.shouldTempban(player), equalTo(false)); - assertHasCount(manager, player, null); + assertThat(manager.shouldTempban(address), equalTo(false)); + assertHasCount(manager, address, null); } @Test public void shouldNotIncreaseCountForDisabledTempban() { // given - String player = "playah"; + String address = "192.168.1.3"; NewSetting settings = mockSettings(1, 5); given(settings.getProperty(SecuritySettings.TEMPBAN_ON_MAX_LOGINS)).willReturn(false); TempbanManager manager = new TempbanManager(bukkitService, messages, settings); // when - manager.increaseCount(player); + manager.increaseCount(address); // then - assertThat(manager.shouldTempban(player), equalTo(false)); - assertHasCount(manager, player, null); + assertThat(manager.shouldTempban(address), equalTo(false)); + assertHasCount(manager, address, null); } @Test public void shouldNotCheckCountIfTempbanIsDisabled() { // given - String player = "playah"; + String address = "192.168.1.4"; NewSetting settings = mockSettings(1, 5); TempbanManager manager = new TempbanManager(bukkitService, messages, settings); given(settings.getProperty(SecuritySettings.TEMPBAN_ON_MAX_LOGINS)).willReturn(false); // when - manager.increaseCount(player); + manager.increaseCount(address); // assumptions - assertThat(manager.shouldTempban(player), equalTo(true)); - assertHasCount(manager, player, 1); + assertThat(manager.shouldTempban(address), equalTo(true)); + assertHasCount(manager, address, 1); // end assumptions manager.loadSettings(settings); - boolean result = manager.shouldTempban(player); + boolean result = manager.shouldTempban(address); // then assertThat(result, equalTo(false)); @@ -117,10 +117,10 @@ public class TempbanManagerTest { return settings; } - private static void assertHasCount(TempbanManager manager, String player, Integer count) { + private static void assertHasCount(TempbanManager manager, String address, Integer count) { @SuppressWarnings("unchecked") Map playerCounts = (Map) ReflectionTestUtils - .getFieldValue(TempbanManager.class, manager, "playerCounts"); - assertThat(playerCounts.get(player.toLowerCase()), equalTo(count)); + .getFieldValue(TempbanManager.class, manager, "ipLoginFailureCounts"); + assertThat(playerCounts.get(address), equalTo(count)); } } From 3411450ff1a6b4c051aa344842c310166363b360 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Tue, 14 Jun 2016 21:03:32 +0200 Subject: [PATCH 195/200] #760 Fix single session feature - Move the check from PlayerLoginEvent to AsyncPlayerPreLoginEvent. Single session can only be implemented with PreLoginEvent; it is already to late to check this in the PlayerLoginEvent. Ergo, we cannot offer this for CraftBukkit. - Remove interactions with LimboCache - no interactions with LimboCache expected until after OnJoinVerification checks. (Thanks sgdc3!) --- .../authme/listener/AuthMePlayerListener.java | 4 +++- .../xephi/authme/listener/OnJoinVerifier.java | 18 +++--------------- .../authme/listener/OnJoinVerifierTest.java | 19 +++++++------------ 3 files changed, 13 insertions(+), 28 deletions(-) diff --git a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java index 67e57ee8..84fff695 100644 --- a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java +++ b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java @@ -217,6 +217,9 @@ public class AuthMePlayerListener implements Listener { onJoinVerifier.checkAntibot(name, isAuthAvailable); onJoinVerifier.checkKickNonRegistered(isAuthAvailable); onJoinVerifier.checkIsValidName(name); + // Note #760: Single session must be checked here - checking with PlayerLoginEvent is too late and + // the first connection will have been kicked. This means this feature doesn't work on CraftBukkit. + onJoinVerifier.checkSingleSession(name); } catch (FailedVerificationException e) { event.setKickMessage(m.retrieveSingle(e.getReason(), e.getArgs())); event.setLoginResult(AsyncPlayerPreLoginEvent.Result.KICK_OTHER); @@ -243,7 +246,6 @@ public class AuthMePlayerListener implements Listener { onJoinVerifier.checkKickNonRegistered(isAuthAvailable); onJoinVerifier.checkIsValidName(name); onJoinVerifier.checkNameCasing(player, auth); - onJoinVerifier.checkSingleSession(player); onJoinVerifier.checkPlayerCountry(isAuthAvailable, event); } catch (FailedVerificationException e) { event.setKickMessage(m.retrieveSingle(e.getReason(), e.getArgs())); diff --git a/src/main/java/fr/xephi/authme/listener/OnJoinVerifier.java b/src/main/java/fr/xephi/authme/listener/OnJoinVerifier.java index 64f8c3f7..297e74c6 100644 --- a/src/main/java/fr/xephi/authme/listener/OnJoinVerifier.java +++ b/src/main/java/fr/xephi/authme/listener/OnJoinVerifier.java @@ -3,9 +3,6 @@ package fr.xephi.authme.listener; import fr.xephi.authme.AntiBot; import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.cache.auth.PlayerAuth; -import fr.xephi.authme.cache.auth.PlayerCache; -import fr.xephi.authme.cache.limbo.LimboCache; -import fr.xephi.authme.cache.limbo.LimboPlayer; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.initialization.Reloadable; import fr.xephi.authme.output.MessageKey; @@ -18,7 +15,6 @@ import fr.xephi.authme.settings.properties.RegistrationSettings; import fr.xephi.authme.settings.properties.RestrictionSettings; import fr.xephi.authme.util.BukkitService; import fr.xephi.authme.util.StringUtils; -import fr.xephi.authme.util.Utils; import fr.xephi.authme.util.ValidationService; import org.bukkit.Server; import org.bukkit.entity.Player; @@ -49,8 +45,6 @@ class OnJoinVerifier implements Reloadable { @Inject private BukkitService bukkitService; @Inject - private LimboCache limboCache; - @Inject private Server server; private Pattern nicknamePattern; @@ -187,21 +181,15 @@ class OnJoinVerifier implements Reloadable { * Checks if a player with the same name (case-insensitive) is already playing and refuses the * connection if so configured. * - * @param player the player to verify + * @param name the player name to check */ - public void checkSingleSession(Player player) throws FailedVerificationException { + public void checkSingleSession(String name) throws FailedVerificationException { if (!settings.getProperty(RestrictionSettings.FORCE_SINGLE_SESSION)) { return; } - Player onlinePlayer = bukkitService.getPlayerExact(player.getName()); + Player onlinePlayer = bukkitService.getPlayerExact(name); if (onlinePlayer != null) { - String name = player.getName().toLowerCase(); - LimboPlayer limbo = limboCache.getLimboPlayer(name); - if (limbo != null && PlayerCache.getInstance().isAuthenticated(name)) { - Utils.addNormal(player, limbo.getGroup()); - limboCache.deleteLimboPlayer(name); - } throw new FailedVerificationException(MessageKey.USERNAME_ALREADY_ONLINE_ERROR); } } diff --git a/src/test/java/fr/xephi/authme/listener/OnJoinVerifierTest.java b/src/test/java/fr/xephi/authme/listener/OnJoinVerifierTest.java index 1cf9d0c1..6d5d27e4 100644 --- a/src/test/java/fr/xephi/authme/listener/OnJoinVerifierTest.java +++ b/src/test/java/fr/xephi/authme/listener/OnJoinVerifierTest.java @@ -3,7 +3,6 @@ package fr.xephi.authme.listener; import fr.xephi.authme.AntiBot; import fr.xephi.authme.TestHelper; import fr.xephi.authme.cache.auth.PlayerAuth; -import fr.xephi.authme.cache.limbo.LimboCache; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.output.Messages; @@ -67,8 +66,6 @@ public class OnJoinVerifierTest { @Mock private BukkitService bukkitService; @Mock - private LimboCache limboCache; - @Mock private Server server; @Rule @@ -341,21 +338,21 @@ public class OnJoinVerifierTest { @Test public void shouldAcceptNameThatIsNotOnline() throws FailedVerificationException { // given - Player player = newPlayerWithName("bobby"); + String name = "bobby"; given(settings.getProperty(RestrictionSettings.FORCE_SINGLE_SESSION)).willReturn(true); given(bukkitService.getPlayerExact("bobby")).willReturn(null); // when - onJoinVerifier.checkSingleSession(player); + onJoinVerifier.checkSingleSession(name); // then - verifyZeroInteractions(limboCache); + verify(bukkitService).getPlayerExact(name); } @Test public void shouldRejectNameAlreadyOnline() throws FailedVerificationException { // given - Player player = newPlayerWithName("Charlie"); + String name = "Charlie"; Player onlinePlayer = newPlayerWithName("charlie"); given(bukkitService.getPlayerExact("Charlie")).willReturn(onlinePlayer); given(settings.getProperty(RestrictionSettings.FORCE_SINGLE_SESSION)).willReturn(true); @@ -364,22 +361,20 @@ public class OnJoinVerifierTest { expectValidationExceptionWith(MessageKey.USERNAME_ALREADY_ONLINE_ERROR); // when / then - onJoinVerifier.checkSingleSession(player); - verify(limboCache).getLimboPlayer("charlie"); + onJoinVerifier.checkSingleSession(name); } @Test public void shouldAcceptAlreadyOnlineNameForDisabledSetting() throws FailedVerificationException { // given - Player player = newPlayerWithName("Felipe"); + String name = "Felipe"; given(settings.getProperty(RestrictionSettings.FORCE_SINGLE_SESSION)).willReturn(false); // when - onJoinVerifier.checkSingleSession(player); + onJoinVerifier.checkSingleSession(name); // then verifyZeroInteractions(bukkitService); - verifyZeroInteractions(limboCache); } private static Player newPlayerWithName(String name) { From 5cbb83e15337a2c88ce4cdb52878f8e64f605ec6 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Tue, 14 Jun 2016 21:52:43 +0200 Subject: [PATCH 196/200] Code householding, add tests to TempbanManager - Delegate event firing to BukkitService - Write tests for IP banning function - Update comments on tempban properties in config.yml --- .../fr/xephi/authme/cache/TempbanManager.java | 14 ++-- .../authme/process/join/AsynchronousJoin.java | 2 +- .../process/login/ProcessSyncPlayerLogin.java | 2 +- .../register/ProcessSyncPasswordRegister.java | 2 +- .../fr/xephi/authme/util/BukkitService.java | 19 +++++ src/main/resources/config.yml | 6 +- src/test/java/fr/xephi/authme/TestHelper.java | 18 ++++ .../authme/cache/TempbanManagerTest.java | 83 ++++++++++++++++++- .../executable/authme/GetIpCommandTest.java | 8 +- 9 files changed, 130 insertions(+), 24 deletions(-) diff --git a/src/main/java/fr/xephi/authme/cache/TempbanManager.java b/src/main/java/fr/xephi/authme/cache/TempbanManager.java index 8042b603..0a55353c 100644 --- a/src/main/java/fr/xephi/authme/cache/TempbanManager.java +++ b/src/main/java/fr/xephi/authme/cache/TempbanManager.java @@ -7,8 +7,6 @@ import fr.xephi.authme.settings.NewSetting; import fr.xephi.authme.settings.properties.SecuritySettings; import fr.xephi.authme.util.BukkitService; import fr.xephi.authme.util.Utils; -import org.bukkit.BanList; -import org.bukkit.Bukkit; import org.bukkit.entity.Player; import javax.inject.Inject; @@ -21,13 +19,11 @@ import java.util.concurrent.ConcurrentHashMap; // TODO Gnat008 20160613: Figure out the best way to remove entries based on time public class TempbanManager implements SettingsDependent { + private static final long MINUTE_IN_MILLISECONDS = 60000; + private final ConcurrentHashMap ipLoginFailureCounts; - - private final long MINUTE_IN_MILLISECONDS = 60000; - - private BukkitService bukkitService; - - private Messages messages; + private final BukkitService bukkitService; + private final Messages messages; private boolean isEnabled; private int threshold; @@ -102,7 +98,7 @@ public class TempbanManager implements SettingsDependent { bukkitService.scheduleSyncDelayedTask(new Runnable() { @Override public void run() { - Bukkit.getServer().getBanList(BanList.Type.IP).addBan(ip, reason, expires, "AuthMe"); + bukkitService.banIp(ip, reason, expires, "AuthMe"); player.kickPlayer(reason); } }); diff --git a/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java b/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java index 27fe17de..5d1c2fca 100644 --- a/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java +++ b/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java @@ -124,7 +124,7 @@ public class AsynchronousJoin implements AsynchronousProcess { // Protect inventory if (service.getProperty(PROTECT_INVENTORY_BEFORE_LOGIN) && plugin.inventoryProtector != null) { ProtectInventoryEvent ev = new ProtectInventoryEvent(player); - plugin.getServer().getPluginManager().callEvent(ev); + bukkitService.callEvent(ev); if (ev.isCancelled()) { plugin.inventoryProtector.sendInventoryPacket(player); if (!service.getProperty(SecuritySettings.REMOVE_SPAM_FROM_CONSOLE)) { diff --git a/src/main/java/fr/xephi/authme/process/login/ProcessSyncPlayerLogin.java b/src/main/java/fr/xephi/authme/process/login/ProcessSyncPlayerLogin.java index d040d49f..28304e00 100644 --- a/src/main/java/fr/xephi/authme/process/login/ProcessSyncPlayerLogin.java +++ b/src/main/java/fr/xephi/authme/process/login/ProcessSyncPlayerLogin.java @@ -133,7 +133,7 @@ public class ProcessSyncPlayerLogin implements SynchronousProcess { } // The Login event now fires (as intended) after everything is processed - Bukkit.getServer().getPluginManager().callEvent(new LoginEvent(player)); + bukkitService.callEvent(new LoginEvent(player)); player.saveData(); if (service.getProperty(HooksSettings.BUNGEECORD)) { sendBungeeMessage(player); diff --git a/src/main/java/fr/xephi/authme/process/register/ProcessSyncPasswordRegister.java b/src/main/java/fr/xephi/authme/process/register/ProcessSyncPasswordRegister.java index cf936257..d8f790e5 100644 --- a/src/main/java/fr/xephi/authme/process/register/ProcessSyncPasswordRegister.java +++ b/src/main/java/fr/xephi/authme/process/register/ProcessSyncPasswordRegister.java @@ -124,7 +124,7 @@ public class ProcessSyncPasswordRegister implements SynchronousProcess { } // The LoginEvent now fires (as intended) after everything is processed - plugin.getServer().getPluginManager().callEvent(new LoginEvent(player)); + bukkitService.callEvent(new LoginEvent(player)); player.saveData(); if (!service.getProperty(SecuritySettings.REMOVE_SPAM_FROM_CONSOLE)) { diff --git a/src/main/java/fr/xephi/authme/util/BukkitService.java b/src/main/java/fr/xephi/authme/util/BukkitService.java index ac8a408b..8b94fe49 100644 --- a/src/main/java/fr/xephi/authme/util/BukkitService.java +++ b/src/main/java/fr/xephi/authme/util/BukkitService.java @@ -2,6 +2,8 @@ package fr.xephi.authme.util; import fr.xephi.authme.AuthMe; import fr.xephi.authme.ConsoleLogger; +import org.bukkit.BanEntry; +import org.bukkit.BanList; import org.bukkit.Bukkit; import org.bukkit.OfflinePlayer; import org.bukkit.World; @@ -15,6 +17,7 @@ import java.lang.reflect.Method; import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.Date; import java.util.Set; /** @@ -206,4 +209,20 @@ public class BukkitService { return false; } + /** + * Adds a ban to the this list. If a previous ban exists, this will + * update the previous entry. + * + * @param ip the ip of the ban + * @param reason reason for the ban, null indicates implementation default + * @param expires date for the ban's expiration (unban), or null to imply + * forever + * @param source source of the ban, null indicates implementation default + * @return the entry for the newly created ban, or the entry for the + * (updated) previous ban + */ + public BanEntry banIp(String ip, String reason, Date expires, String source) { + return Bukkit.getServer().getBanList(BanList.Type.IP).addBan(ip, reason, expires, source); + } + } diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index e7c67763..99cc3a2b 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -332,11 +332,11 @@ Security: # information correctly without any corruption. kickPlayersBeforeStopping: true tempban: - # Tempban users if they enter the wrong password too many times + # Tempban a user's IP address if they enter the wrong password too many times enableTempban: false - # How many times a user can attempt to login before being tempbanned + # How many times a user can attempt to login before their IP being tempbanned maxLoginTries: 10 - # The length of time a player will be tempbanned in minutes + # The length of time a IP address will be tempbanned in minutes # Default: 480 minutes, or 8 hours tempbanLength: 480 Converter: diff --git a/src/test/java/fr/xephi/authme/TestHelper.java b/src/test/java/fr/xephi/authme/TestHelper.java index 907cb8e2..bef65621 100644 --- a/src/test/java/fr/xephi/authme/TestHelper.java +++ b/src/test/java/fr/xephi/authme/TestHelper.java @@ -1,6 +1,7 @@ package fr.xephi.authme; import fr.xephi.authme.util.BukkitService; +import org.bukkit.entity.Player; import org.mockito.ArgumentCaptor; import org.mockito.Mockito; @@ -8,6 +9,8 @@ import java.io.File; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Modifier; +import java.net.InetAddress; +import java.net.InetSocketAddress; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; @@ -15,7 +18,9 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.logging.Logger; +import static org.mockito.BDDMockito.given; import static org.mockito.Matchers.anyLong; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; /** @@ -144,4 +149,17 @@ public final class TestHelper { } } + /** + * Configures the player mock to return the given IP address. + * + * @param player the player mock + * @param ip the ip address it should return + */ + public static void mockPlayerIp(Player player, String ip) { + InetAddress inetAddress = mock(InetAddress.class); + given(inetAddress.getHostAddress()).willReturn(ip); + InetSocketAddress inetSocketAddress = new InetSocketAddress(inetAddress, 8093); + given(player.getAddress()).willReturn(inetSocketAddress); + } + } diff --git a/src/test/java/fr/xephi/authme/cache/TempbanManagerTest.java b/src/test/java/fr/xephi/authme/cache/TempbanManagerTest.java index e960a084..b3c6fc03 100644 --- a/src/test/java/fr/xephi/authme/cache/TempbanManagerTest.java +++ b/src/test/java/fr/xephi/authme/cache/TempbanManagerTest.java @@ -1,21 +1,31 @@ package fr.xephi.authme.cache; import fr.xephi.authme.ReflectionTestUtils; +import fr.xephi.authme.TestHelper; +import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.output.Messages; import fr.xephi.authme.settings.NewSetting; import fr.xephi.authme.settings.properties.SecuritySettings; import fr.xephi.authme.util.BukkitService; +import org.bukkit.entity.Player; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; +import java.util.Calendar; +import java.util.Date; import java.util.Map; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.lessThan; import static org.junit.Assert.assertThat; import static org.mockito.BDDMockito.given; +import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; /** * Test for {@link TempbanManager}. @@ -23,11 +33,13 @@ import static org.mockito.Mockito.mock; @RunWith(MockitoJUnitRunner.class) public class TempbanManagerTest { - @Mock - BukkitService bukkitService; + private static final long DATE_TOLERANCE_MILLISECONDS = 100L; @Mock - Messages messages; + private BukkitService bukkitService; + + @Mock + private Messages messages; @Test public void shouldAddCounts() { @@ -109,6 +121,71 @@ public class TempbanManagerTest { assertThat(result, equalTo(false)); } + @Test + public void shouldNotIssueBanIfDisabled() { + // given + NewSetting settings = mockSettings(0, 0); + given(settings.getProperty(SecuritySettings.TEMPBAN_ON_MAX_LOGINS)).willReturn(false); + Player player = mock(Player.class); + TempbanManager manager = new TempbanManager(bukkitService, messages, settings); + + // when + manager.tempbanPlayer(player); + + // then + verifyZeroInteractions(player, bukkitService); + } + + @Test + public void shouldBanPlayerIp() { + // given + Player player = mock(Player.class); + String ip = "123.45.67.89"; + TestHelper.mockPlayerIp(player, ip); + String banReason = "IP ban too many logins"; + given(messages.retrieveSingle(MessageKey.TEMPBAN_MAX_LOGINS)).willReturn(banReason); + NewSetting settings = mockSettings(2, 100); + TempbanManager manager = new TempbanManager(bukkitService, messages, settings); + + // when + manager.tempbanPlayer(player); + TestHelper.runSyncDelayedTask(bukkitService); + + // then + verify(player).kickPlayer(banReason); + ArgumentCaptor captor = ArgumentCaptor.forClass(Date.class); + verify(bukkitService).banIp(eq(ip), eq(banReason), captor.capture(), eq("AuthMe")); + + // Compute the expected expiration date and check that the actual date is within the difference tolerance + Calendar cal = Calendar.getInstance(); + cal.add(Calendar.MINUTE, 100); + long expectedExpiration = cal.getTime().getTime(); + assertThat(Math.abs(captor.getValue().getTime() - expectedExpiration), lessThan(DATE_TOLERANCE_MILLISECONDS)); + } + + @Test + public void shouldResetCountAfterBan() { + // given + Player player = mock(Player.class); + String ip = "22.44.66.88"; + TestHelper.mockPlayerIp(player, ip); + String banReason = "kick msg"; + given(messages.retrieveSingle(MessageKey.TEMPBAN_MAX_LOGINS)).willReturn(banReason); + NewSetting settings = mockSettings(10, 60); + TempbanManager manager = new TempbanManager(bukkitService, messages, settings); + manager.increaseCount(ip); + manager.increaseCount(ip); + manager.increaseCount(ip); + + // when + manager.tempbanPlayer(player); + TestHelper.runSyncDelayedTask(bukkitService); + + // then + verify(player).kickPlayer(banReason); + assertHasCount(manager, ip, null); + } + private static NewSetting mockSettings(int maxTries, int tempbanLength) { NewSetting settings = mock(NewSetting.class); given(settings.getProperty(SecuritySettings.TEMPBAN_ON_MAX_LOGINS)).willReturn(true); diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/GetIpCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/GetIpCommandTest.java index d3d5c376..65ed998a 100644 --- a/src/test/java/fr/xephi/authme/command/executable/authme/GetIpCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/authme/GetIpCommandTest.java @@ -1,5 +1,6 @@ package fr.xephi.authme.command.executable.authme; +import fr.xephi.authme.TestHelper; import fr.xephi.authme.util.BukkitService; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; @@ -9,8 +10,6 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; -import java.net.InetAddress; -import java.net.InetSocketAddress; import java.util.Collections; import static org.hamcrest.Matchers.allOf; @@ -68,10 +67,7 @@ public class GetIpCommandTest { private static Player mockPlayer(String name, String ip) { Player player = mock(Player.class); given(player.getName()).willReturn(name); - InetAddress inetAddress = mock(InetAddress.class); - given(inetAddress.getHostAddress()).willReturn(ip); - InetSocketAddress inetSocketAddress = new InetSocketAddress(inetAddress, 8093); - given(player.getAddress()).willReturn(inetSocketAddress); + TestHelper.mockPlayerIp(player, ip); return player; } } From 15886fb517fc0aa8c9e2a7ca04f48527aa4ee339 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Wed, 15 Jun 2016 20:37:00 +0200 Subject: [PATCH 197/200] #723 Let LimboPlayer task manager decide which message key to use - Pass boolean (is registered) value and determine internally which message key (email registration vs. regular) to use in the message task --- .../authme/UnregisterAdminCommand.java | 5 ++--- .../authme/process/join/AsynchronousJoin.java | 11 +--------- .../process/login/AsynchronousLogin.java | 7 +----- .../ProcessSynchronousPlayerLogout.java | 2 +- .../register/ProcessSyncEmailRegister.java | 2 +- .../register/ProcessSyncPasswordRegister.java | 2 +- .../unregister/AsynchronousUnregister.java | 2 +- .../authme/task/LimboPlayerTaskManager.java | 22 +++++++++++++++++-- .../task/LimboPlayerTaskManagerTest.java | 20 ++++++++--------- 9 files changed, 37 insertions(+), 36 deletions(-) diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/UnregisterAdminCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/UnregisterAdminCommand.java index 064ea536..2e746f0d 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/UnregisterAdminCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/UnregisterAdminCommand.java @@ -93,13 +93,12 @@ public class UnregisterAdminCommand implements ExecutableCommand { * @param target the player that was unregistered */ private void applyUnregisteredEffectsAndTasks(Player target) { - // TODO ljacqu 20160612: Remove use of Utils method and behave according to settings + // TODO #765: Remove use of Utils method and behave according to settings Utils.teleportToSpawn(target); limboCache.addLimboPlayer(target); limboPlayerTaskManager.registerTimeoutTask(target); - limboPlayerTaskManager.registerMessageTask(target.getName(), - MessageKey.REGISTER_MESSAGE); + limboPlayerTaskManager.registerMessageTask(target.getName(), false); final int timeout = commandService.getProperty(RestrictionSettings.TIMEOUT) * TICKS_PER_SECOND; if (commandService.getProperty(RegistrationSettings.APPLY_BLIND_EFFECT)) { diff --git a/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java b/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java index 5d1c2fca..cf1741c1 100644 --- a/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java +++ b/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java @@ -195,16 +195,7 @@ public class AsynchronousJoin implements AsynchronousProcess { // Timeout and message task limboPlayerTaskManager.registerTimeoutTask(player); - - MessageKey msg; - if (isAuthAvailable) { - msg = MessageKey.LOGIN_MESSAGE; - } else { - msg = service.getProperty(RegistrationSettings.USE_EMAIL_REGISTRATION) - ? MessageKey.REGISTER_EMAIL_MESSAGE - : MessageKey.REGISTER_MESSAGE; - } - limboPlayerTaskManager.registerMessageTask(name, msg); + limboPlayerTaskManager.registerMessageTask(name, isAuthAvailable); } private boolean isPlayerUnrestricted(String name) { diff --git a/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java b/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java index 33b6defa..18e7e8e4 100644 --- a/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java +++ b/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java @@ -21,7 +21,6 @@ import fr.xephi.authme.security.PasswordSecurity; import fr.xephi.authme.settings.Settings; import fr.xephi.authme.settings.properties.DatabaseSettings; import fr.xephi.authme.settings.properties.EmailSettings; -import fr.xephi.authme.settings.properties.RegistrationSettings; import fr.xephi.authme.settings.properties.RestrictionSettings; import fr.xephi.authme.settings.properties.SecuritySettings; import fr.xephi.authme.task.LimboPlayerTaskManager; @@ -33,7 +32,6 @@ import org.bukkit.ChatColor; import org.bukkit.entity.Player; import javax.inject.Inject; - import java.util.ArrayList; import java.util.List; @@ -108,10 +106,7 @@ public class AsynchronousLogin implements AsynchronousProcess { service.send(player, MessageKey.USER_NOT_REGISTERED); // TODO ljacqu 20160612: Why is the message task being canceled and added again here? - MessageKey key = service.getProperty(RegistrationSettings.USE_EMAIL_REGISTRATION) - ? MessageKey.REGISTER_EMAIL_MESSAGE - : MessageKey.REGISTER_MESSAGE; - limboPlayerTaskManager.registerMessageTask(name, key); + limboPlayerTaskManager.registerMessageTask(name, false); return null; } diff --git a/src/main/java/fr/xephi/authme/process/logout/ProcessSynchronousPlayerLogout.java b/src/main/java/fr/xephi/authme/process/logout/ProcessSynchronousPlayerLogout.java index cd8eb3b3..c70f4cd0 100644 --- a/src/main/java/fr/xephi/authme/process/logout/ProcessSynchronousPlayerLogout.java +++ b/src/main/java/fr/xephi/authme/process/logout/ProcessSynchronousPlayerLogout.java @@ -66,7 +66,7 @@ public class ProcessSynchronousPlayerLogout implements SynchronousProcess { } limboPlayerTaskManager.registerTimeoutTask(player); - limboPlayerTaskManager.registerMessageTask(name, MessageKey.LOGIN_MESSAGE); + limboPlayerTaskManager.registerMessageTask(name, true); if (player.isInsideVehicle() && player.getVehicle() != null) { player.getVehicle().eject(); diff --git a/src/main/java/fr/xephi/authme/process/register/ProcessSyncEmailRegister.java b/src/main/java/fr/xephi/authme/process/register/ProcessSyncEmailRegister.java index 15901606..41d8fe97 100644 --- a/src/main/java/fr/xephi/authme/process/register/ProcessSyncEmailRegister.java +++ b/src/main/java/fr/xephi/authme/process/register/ProcessSyncEmailRegister.java @@ -33,7 +33,7 @@ public class ProcessSyncEmailRegister implements SynchronousProcess { service.send(player, MessageKey.ACCOUNT_NOT_ACTIVATED); limboPlayerTaskManager.registerTimeoutTask(player); - limboPlayerTaskManager.registerMessageTask(name, MessageKey.LOGIN_MESSAGE); + limboPlayerTaskManager.registerMessageTask(name, true); player.saveData(); if (!service.getProperty(SecuritySettings.REMOVE_SPAM_FROM_CONSOLE)) { diff --git a/src/main/java/fr/xephi/authme/process/register/ProcessSyncPasswordRegister.java b/src/main/java/fr/xephi/authme/process/register/ProcessSyncPasswordRegister.java index d8f790e5..16f1ad36 100644 --- a/src/main/java/fr/xephi/authme/process/register/ProcessSyncPasswordRegister.java +++ b/src/main/java/fr/xephi/authme/process/register/ProcessSyncPasswordRegister.java @@ -81,7 +81,7 @@ public class ProcessSyncPasswordRegister implements SynchronousProcess { limboCache.updateLimboPlayer(player); limboPlayerTaskManager.registerTimeoutTask(player); - limboPlayerTaskManager.registerMessageTask(name, MessageKey.LOGIN_MESSAGE); + limboPlayerTaskManager.registerMessageTask(name, true); if (player.isInsideVehicle() && player.getVehicle() != null) { player.getVehicle().eject(); diff --git a/src/main/java/fr/xephi/authme/process/unregister/AsynchronousUnregister.java b/src/main/java/fr/xephi/authme/process/unregister/AsynchronousUnregister.java index 87cb4808..b9b87252 100644 --- a/src/main/java/fr/xephi/authme/process/unregister/AsynchronousUnregister.java +++ b/src/main/java/fr/xephi/authme/process/unregister/AsynchronousUnregister.java @@ -64,7 +64,7 @@ public class AsynchronousUnregister implements AsynchronousProcess { } limboCache.addLimboPlayer(player); limboPlayerTaskManager.registerTimeoutTask(player); - limboPlayerTaskManager.registerMessageTask(name, MessageKey.REGISTER_MESSAGE); + limboPlayerTaskManager.registerMessageTask(name, false); service.send(player, MessageKey.UNREGISTERED_SUCCESS); ConsoleLogger.info(player.getDisplayName() + " unregistered himself"); diff --git a/src/main/java/fr/xephi/authme/task/LimboPlayerTaskManager.java b/src/main/java/fr/xephi/authme/task/LimboPlayerTaskManager.java index e1a95603..36530c99 100644 --- a/src/main/java/fr/xephi/authme/task/LimboPlayerTaskManager.java +++ b/src/main/java/fr/xephi/authme/task/LimboPlayerTaskManager.java @@ -44,10 +44,12 @@ public class LimboPlayerTaskManager { * Registers a {@link MessageTask} for the given player name. * * @param name the name of the player to schedule a repeating message task for - * @param key the key of the message to display + * @param isRegistered whether the name is registered or not + * (false shows "please register", true shows "please log in") */ - public void registerMessageTask(String name, MessageKey key) { + public void registerMessageTask(String name, boolean isRegistered) { final int interval = settings.getProperty(RegistrationSettings.MESSAGE_INTERVAL); + final MessageKey key = getMessageKey(isRegistered); if (interval > 0) { final LimboPlayer limboPlayer = limboCache.getLimboPlayer(name); if (limboPlayer == null) { @@ -81,6 +83,22 @@ public class LimboPlayerTaskManager { } } + /** + * Returns the appropriate message key according to the registration status and settings. + * + * @param isRegistered whether or not the username is registered + * @return the message key to display to the user + */ + private MessageKey getMessageKey(boolean isRegistered) { + if (isRegistered) { + return MessageKey.LOGIN_MESSAGE; + } else { + return settings.getProperty(RegistrationSettings.USE_EMAIL_REGISTRATION) + ? MessageKey.REGISTER_EMAIL_MESSAGE + : MessageKey.REGISTER_MESSAGE; + } + } + /** * Null-safe method to cancel a potentially existing task. * diff --git a/src/test/java/fr/xephi/authme/task/LimboPlayerTaskManagerTest.java b/src/test/java/fr/xephi/authme/task/LimboPlayerTaskManagerTest.java index b0329927..51e390c4 100644 --- a/src/test/java/fr/xephi/authme/task/LimboPlayerTaskManagerTest.java +++ b/src/test/java/fr/xephi/authme/task/LimboPlayerTaskManagerTest.java @@ -67,9 +67,10 @@ public class LimboPlayerTaskManagerTest { BukkitTask bukkiTask = mock(BukkitTask.class); given(bukkitService.runTask(any(MessageTask.class))).willReturn(bukkiTask); given(settings.getProperty(RegistrationSettings.MESSAGE_INTERVAL)).willReturn(12); + given(settings.getProperty(RegistrationSettings.USE_EMAIL_REGISTRATION)).willReturn(true); // when - limboPlayerTaskManager.registerMessageTask(name, key); + limboPlayerTaskManager.registerMessageTask(name, false); // then verify(limboPlayer).setMessageTask(bukkiTask); @@ -81,12 +82,10 @@ public class LimboPlayerTaskManagerTest { // given String name = "ghost"; given(limboCache.getLimboPlayer(name)).willReturn(null); - MessageKey key = MessageKey.REGISTER_MESSAGE; - given(messages.retrieve(key)).willReturn(new String[]{"Please register!"}); given(settings.getProperty(RegistrationSettings.MESSAGE_INTERVAL)).willReturn(5); // when - limboPlayerTaskManager.registerMessageTask(name, key); + limboPlayerTaskManager.registerMessageTask(name, true); // then verify(limboCache).getLimboPlayer(name); @@ -100,14 +99,12 @@ public class LimboPlayerTaskManagerTest { String name = "Tester1"; LimboPlayer limboPlayer = mock(LimboPlayer.class); given(limboCache.getLimboPlayer(name)).willReturn(limboPlayer); - MessageKey key = MessageKey.REGISTER_EMAIL_MESSAGE; - given(messages.retrieve(key)).willReturn(new String[]{"Please register!"}); BukkitTask bukkiTask = mock(BukkitTask.class); given(bukkitService.runTask(any(MessageTask.class))).willReturn(bukkiTask); given(settings.getProperty(RegistrationSettings.MESSAGE_INTERVAL)).willReturn(0); // when - limboPlayerTaskManager.registerMessageTask(name, key); + limboPlayerTaskManager.registerMessageTask(name, true); // then verifyZeroInteractions(limboPlayer, bukkitService); @@ -122,19 +119,20 @@ public class LimboPlayerTaskManagerTest { String name = "bobby"; given(limboCache.getLimboPlayer(name)).willReturn(limboPlayer); - MessageKey key = MessageKey.REGISTER_EMAIL_MESSAGE; - given(messages.retrieve(key)).willReturn(new String[]{"Please register", "Use /register"}); + given(messages.retrieve(MessageKey.REGISTER_EMAIL_MESSAGE)) + .willReturn(new String[]{"Please register", "Use /register"}); BukkitTask bukkiTask = mock(BukkitTask.class); given(bukkitService.runTask(any(MessageTask.class))).willReturn(bukkiTask); given(settings.getProperty(RegistrationSettings.MESSAGE_INTERVAL)).willReturn(8); + given(settings.getProperty(RegistrationSettings.USE_EMAIL_REGISTRATION)).willReturn(true); // when - limboPlayerTaskManager.registerMessageTask(name, key); + limboPlayerTaskManager.registerMessageTask(name, false); // then verify(limboPlayer).setMessageTask(bukkiTask); - verify(messages).retrieve(key); + verify(messages).retrieve(MessageKey.REGISTER_EMAIL_MESSAGE); verify(existingMessageTask).cancel(); } From ac484345a279bcb4ccb57202d3648da07f558740 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Wed, 15 Jun 2016 20:56:34 +0200 Subject: [PATCH 198/200] Change password task to async process - Perform async change password task just like other async processes: via Management - Remove legacy setting - Remove now unused service getter (#736) --- src/main/java/fr/xephi/authme/AuthMe.java | 9 --- .../changepassword/ChangePasswordCommand.java | 15 +---- .../fr/xephi/authme/process/Management.java | 13 ++++ .../changepassword/AsyncChangePassword.java} | 66 +++++++++++-------- .../fr/xephi/authme/settings/Settings.java | 3 - .../ChangePasswordCommandTest.java | 31 ++++----- 6 files changed, 65 insertions(+), 72 deletions(-) rename src/main/java/fr/xephi/authme/{task/ChangePasswordTask.java => process/changepassword/AsyncChangePassword.java} (53%) diff --git a/src/main/java/fr/xephi/authme/AuthMe.java b/src/main/java/fr/xephi/authme/AuthMe.java index 058c21bd..0fb32ddb 100644 --- a/src/main/java/fr/xephi/authme/AuthMe.java +++ b/src/main/java/fr/xephi/authme/AuthMe.java @@ -736,15 +736,6 @@ public class AuthMe extends JavaPlugin { // Service getters (deprecated) // Use @Inject fields instead // ------------- - /** - * @return Plugin's messages. - * @deprecated should be used in API classes only (temporarily) - */ - @Deprecated - public Messages getMessages() { - return messages; - } - /** * @return NewSetting * @deprecated should be used in API classes only (temporarily) 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 6f05d364..68377390 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 @@ -1,13 +1,10 @@ package fr.xephi.authme.command.executable.changepassword; -import fr.xephi.authme.AuthMe; 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.security.PasswordSecurity; -import fr.xephi.authme.task.ChangePasswordTask; -import fr.xephi.authme.util.BukkitService; +import fr.xephi.authme.process.Management; import fr.xephi.authme.util.ValidationService; import fr.xephi.authme.util.ValidationService.ValidationResult; import org.bukkit.entity.Player; @@ -26,15 +23,11 @@ public class ChangePasswordCommand extends PlayerCommand { @Inject private PlayerCache playerCache; - @Inject - private BukkitService bukkitService; - @Inject private ValidationService validationService; @Inject - // TODO ljacqu 20160531: Remove this once change password task runs as a process (via Management) - private PasswordSecurity passwordSecurity; + private Management management; @Override public void runCommand(Player player, List arguments) { @@ -54,9 +47,7 @@ public class ChangePasswordCommand extends PlayerCommand { return; } - AuthMe plugin = AuthMe.getInstance(); // TODO ljacqu 20160117: Call async task via Management - bukkitService.runTaskAsynchronously( - new ChangePasswordTask(plugin, player, oldPassword, newPassword, passwordSecurity)); + management.performPasswordChange(player, oldPassword, newPassword); } } diff --git a/src/main/java/fr/xephi/authme/process/Management.java b/src/main/java/fr/xephi/authme/process/Management.java index 2cd0b388..5e72fad7 100644 --- a/src/main/java/fr/xephi/authme/process/Management.java +++ b/src/main/java/fr/xephi/authme/process/Management.java @@ -1,5 +1,6 @@ package fr.xephi.authme.process; +import fr.xephi.authme.process.changepassword.AsyncChangePassword; import fr.xephi.authme.process.email.AsyncAddEmail; import fr.xephi.authme.process.email.AsyncChangeEmail; import fr.xephi.authme.process.join.AsynchronousJoin; @@ -36,9 +37,12 @@ public class Management { private AsynchronousLogin asynchronousLogin; @Inject private AsynchronousUnregister asynchronousUnregister; + @Inject + private AsyncChangePassword asyncChangePassword; Management() { } + public void performLogin(final Player player, final String password, final boolean forceLogin) { runTask(new Runnable() { @Override @@ -111,6 +115,15 @@ public class Management { }); } + public void performPasswordChange(final Player player, final String oldPassword, final String newPassword) { + runTask(new Runnable() { + @Override + public void run() { + asyncChangePassword.changePassword(player, oldPassword, newPassword); + } + }); + } + private void runTask(Runnable runnable) { bukkitService.runTaskAsynchronously(runnable); } diff --git a/src/main/java/fr/xephi/authme/task/ChangePasswordTask.java b/src/main/java/fr/xephi/authme/process/changepassword/AsyncChangePassword.java similarity index 53% rename from src/main/java/fr/xephi/authme/task/ChangePasswordTask.java rename to src/main/java/fr/xephi/authme/process/changepassword/AsyncChangePassword.java index 1fbd8c14..4389ac35 100644 --- a/src/main/java/fr/xephi/authme/task/ChangePasswordTask.java +++ b/src/main/java/fr/xephi/authme/process/changepassword/AsyncChangePassword.java @@ -1,4 +1,4 @@ -package fr.xephi.authme.task; +package fr.xephi.authme.process.changepassword; import com.google.common.io.ByteArrayDataOutput; import com.google.common.io.ByteStreams; @@ -6,52 +6,60 @@ import fr.xephi.authme.AuthMe; import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.cache.auth.PlayerAuth; import fr.xephi.authme.cache.auth.PlayerCache; +import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.output.MessageKey; -import fr.xephi.authme.output.Messages; +import fr.xephi.authme.process.AsynchronousProcess; +import fr.xephi.authme.process.ProcessService; import fr.xephi.authme.security.PasswordSecurity; import fr.xephi.authme.security.crypts.HashedPassword; -import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.properties.HooksSettings; +import fr.xephi.authme.util.BukkitService; import org.bukkit.entity.Player; -public class ChangePasswordTask implements Runnable { +import javax.inject.Inject; - private final AuthMe plugin; - private final Player player; - private final String oldPassword; - private final String newPassword; - private final PasswordSecurity passwordSecurity; +public class AsyncChangePassword implements AsynchronousProcess { - public ChangePasswordTask(AuthMe plugin, Player player, String oldPassword, String newPassword, - PasswordSecurity passwordSecurity) { - this.plugin = plugin; - this.player = player; - this.oldPassword = oldPassword; - this.newPassword = newPassword; - this.passwordSecurity = passwordSecurity; - } + @Inject + private AuthMe plugin; - @Override - public void run() { - Messages m = plugin.getMessages(); + @Inject + private DataSource dataSource; + + @Inject + private ProcessService processService; + + @Inject + private PasswordSecurity passwordSecurity; + + @Inject + private PlayerCache playerCache; + + @Inject + private BukkitService bukkitService; + + AsyncChangePassword() { } + + + public void changePassword(final Player player, String oldPassword, String newPassword) { final String name = player.getName().toLowerCase(); - PlayerAuth auth = PlayerCache.getInstance().getAuth(name); + PlayerAuth auth = playerCache.getAuth(name); if (passwordSecurity.comparePassword(oldPassword, auth.getPassword(), player.getName())) { HashedPassword hashedPassword = passwordSecurity.computeHash(newPassword, name); auth.setPassword(hashedPassword); - if (!plugin.getDataSource().updatePassword(auth)) { - m.send(player, MessageKey.ERROR); + if (!dataSource.updatePassword(auth)) { + processService.send(player, MessageKey.ERROR); return; } - PlayerCache.getInstance().updatePlayer(auth); - m.send(player, MessageKey.PASSWORD_CHANGED_SUCCESS); + playerCache.updatePlayer(auth); + processService.send(player, MessageKey.PASSWORD_CHANGED_SUCCESS); ConsoleLogger.info(player.getName() + " changed his password"); - if (Settings.bungee) { + if (processService.getProperty(HooksSettings.BUNGEECORD)) { final String hash = hashedPassword.getHash(); final String salt = hashedPassword.getSalt(); - plugin.getServer().getScheduler().scheduleSyncDelayedTask(plugin, new Runnable(){ - + bukkitService.scheduleSyncDelayedTask(new Runnable() { @Override public void run() { ByteArrayDataOutput out = ByteStreams.newDataOutput(); @@ -64,7 +72,7 @@ public class ChangePasswordTask implements Runnable { }); } } else { - m.send(player, MessageKey.WRONG_PASSWORD); + processService.send(player, MessageKey.WRONG_PASSWORD); } } } diff --git a/src/main/java/fr/xephi/authme/settings/Settings.java b/src/main/java/fr/xephi/authme/settings/Settings.java index b3980665..23580155 100644 --- a/src/main/java/fr/xephi/authme/settings/Settings.java +++ b/src/main/java/fr/xephi/authme/settings/Settings.java @@ -2,7 +2,6 @@ package fr.xephi.authme.settings; import fr.xephi.authme.AuthMe; import fr.xephi.authme.settings.domain.Property; -import fr.xephi.authme.settings.properties.HooksSettings; import fr.xephi.authme.settings.properties.PluginSettings; import fr.xephi.authme.settings.properties.RegistrationSettings; import fr.xephi.authme.settings.properties.RestrictionSettings; @@ -26,7 +25,6 @@ public final class Settings { public static boolean protectInventoryBeforeLogInEnabled; public static boolean isStopEnabled; public static boolean reloadSupport; - public static boolean bungee; public static boolean forceRegLogin; public static boolean noTeleport; public static boolean isRemoveSpeedEnabled; @@ -65,7 +63,6 @@ public final class Settings { protectInventoryBeforeLogInEnabled = load(RestrictionSettings.PROTECT_INVENTORY_BEFORE_LOGIN); isStopEnabled = configFile.getBoolean("Security.SQLProblem.stopServer", true); reloadSupport = configFile.getBoolean("Security.ReloadCommand.useReloadCommandSupport", true); - bungee = load(HooksSettings.BUNGEECORD); defaultWorld = configFile.getString("Purge.defaultWorld", "world"); forceRegLogin = load(RegistrationSettings.FORCE_LOGIN_AFTER_REGISTER); noTeleport = load(RestrictionSettings.NO_TELEPORT); 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 1c19b945..3a67bd77 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 @@ -1,13 +1,11 @@ package fr.xephi.authme.command.executable.changepassword; -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.process.Management; import fr.xephi.authme.settings.properties.RestrictionSettings; import fr.xephi.authme.settings.properties.SecuritySettings; -import fr.xephi.authme.task.ChangePasswordTask; -import fr.xephi.authme.util.BukkitService; import fr.xephi.authme.util.ValidationService; import fr.xephi.authme.util.ValidationService.ValidationResult; import org.bukkit.command.BlockCommandSender; @@ -16,7 +14,6 @@ import org.bukkit.entity.Player; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; @@ -26,8 +23,6 @@ import java.util.Arrays; import java.util.Collections; import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.equalTo; -import static org.junit.Assert.assertThat; import static org.mockito.BDDMockito.given; import static org.mockito.Matchers.argThat; import static org.mockito.Mockito.any; @@ -46,18 +41,18 @@ public class ChangePasswordCommandTest { @InjectMocks private ChangePasswordCommand command; - @Mock - private PlayerCache playerCache; - @Mock private CommandService commandService; @Mock - private BukkitService bukkitService; + private PlayerCache playerCache; @Mock private ValidationService validationService; + @Mock + private Management management; + @Before public void setSettings() { when(commandService.getProperty(SecuritySettings.MIN_PASSWORD_LENGTH)).thenReturn(2); @@ -110,20 +105,18 @@ public class ChangePasswordCommandTest { @Test public void shouldForwardTheDataForValidPassword() { // given - CommandSender sender = initPlayerWithName("parker", true); + String oldPass = "oldpass"; + String newPass = "abc123"; + Player player = initPlayerWithName("parker", true); given(validationService.validatePassword("abc123", "parker")).willReturn(new ValidationResult()); // when - command.executeCommand(sender, Arrays.asList("abc123", "abc123")); + command.executeCommand(player, Arrays.asList(oldPass, newPass)); // then - verify(validationService).validatePassword("abc123", "parker"); - verify(commandService, never()).send(eq(sender), any(MessageKey.class)); - ArgumentCaptor taskCaptor = ArgumentCaptor.forClass(ChangePasswordTask.class); - verify(bukkitService).runTaskAsynchronously(taskCaptor.capture()); - ChangePasswordTask task = taskCaptor.getValue(); - assertThat((String) ReflectionTestUtils.getFieldValue(ChangePasswordTask.class, task, "newPassword"), - equalTo("abc123")); + verify(validationService).validatePassword(newPass, "parker"); + verify(commandService, never()).send(eq(player), any(MessageKey.class)); + verify(management).performPasswordChange(player, oldPass, newPass); } private Player initPlayerWithName(String name, boolean loggedIn) { From acd4a772e806f28b9a39624585570f5e63e3bb3a Mon Sep 17 00:00:00 2001 From: ljacqu Date: Wed, 15 Jun 2016 21:24:57 +0200 Subject: [PATCH 199/200] Minor householding in tests - Add missing test for injector - Remove redundant Mock fields --- .../authme/command/CommandHandlerTest.java | 2 -- .../authme/AccountsCommandTest.java | 9 +++++++-- .../AuthMeServiceInitializerTest.java | 16 +++++++++++++++ .../authme/process/ProcessServiceTest.java | 20 +++++++++++++++++++ .../fr/xephi/authme/util/StringUtilsTest.java | 6 ++++++ .../authme/util/TeleportationServiceTest.java | 4 ---- 6 files changed, 49 insertions(+), 8 deletions(-) diff --git a/src/test/java/fr/xephi/authme/command/CommandHandlerTest.java b/src/test/java/fr/xephi/authme/command/CommandHandlerTest.java index d831c85a..137d9f1f 100644 --- a/src/test/java/fr/xephi/authme/command/CommandHandlerTest.java +++ b/src/test/java/fr/xephi/authme/command/CommandHandlerTest.java @@ -56,8 +56,6 @@ public class CommandHandlerTest { @Mock private AuthMeServiceInitializer initializer; @Mock - private CommandInitializer commandInitializer; - @Mock private CommandMapper commandMapper; @Mock private PermissionsManager permissionsManager; diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/AccountsCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/AccountsCommandTest.java index df464811..09615751 100644 --- a/src/test/java/fr/xephi/authme/command/executable/authme/AccountsCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/authme/AccountsCommandTest.java @@ -36,8 +36,6 @@ public class AccountsCommandTest { @InjectMocks private AccountsCommand command; @Mock - private CommandSender sender; - @Mock private CommandService service; @Mock private DataSource dataSource; @@ -47,6 +45,7 @@ public class AccountsCommandTest { @Test public void shouldGetAccountsOfCurrentUser() { // given + CommandSender sender = mock(CommandSender.class); given(sender.getName()).willReturn("Tester"); List arguments = Collections.emptyList(); given(dataSource.getAuth("tester")).willReturn(authWithIp("123.45.67.89")); @@ -65,6 +64,7 @@ public class AccountsCommandTest { @Test public void shouldReturnUnknownUserForNullAuth() { // given + CommandSender sender = mock(CommandSender.class); List arguments = Collections.singletonList("SomeUser"); given(dataSource.getAuth("someuser")).willReturn(null); @@ -80,6 +80,7 @@ public class AccountsCommandTest { @Test public void shouldReturnUnregisteredMessageForEmptyAuthList() { // given + CommandSender sender = mock(CommandSender.class); List arguments = Collections.singletonList("SomeUser"); given(dataSource.getAuth("someuser")).willReturn(mock(PlayerAuth.class)); given(dataSource.getAllAuthsByIp(anyString())).willReturn(Collections.emptyList()); @@ -96,6 +97,7 @@ public class AccountsCommandTest { @Test public void shouldReturnSingleAccountMessage() { // given + CommandSender sender = mock(CommandSender.class); List arguments = Collections.singletonList("SomeUser"); given(dataSource.getAuth("someuser")).willReturn(authWithIp("56.78.90.123")); given(dataSource.getAllAuthsByIp("56.78.90.123")).willReturn(Collections.singletonList("SomeUser")); @@ -115,6 +117,7 @@ public class AccountsCommandTest { @Test public void shouldReturnIpUnknown() { // given + CommandSender sender = mock(CommandSender.class); List arguments = Collections.singletonList("123.45.67.89"); given(dataSource.getAllAuthsByIp("123.45.67.89")).willReturn(Collections.emptyList()); @@ -130,6 +133,7 @@ public class AccountsCommandTest { @Test public void shouldReturnSingleAccountForIpQuery() { // given + CommandSender sender = mock(CommandSender.class); List arguments = Collections.singletonList("24.24.48.48"); given(dataSource.getAllAuthsByIp("24.24.48.48")).willReturn(Collections.singletonList("SomeUser")); @@ -145,6 +149,7 @@ public class AccountsCommandTest { @Test public void shouldReturnAccountListForIpQuery() { // given + CommandSender sender = mock(CommandSender.class); List arguments = Collections.singletonList("98.76.41.122"); given(dataSource.getAllAuthsByIp("98.76.41.122")).willReturn(Arrays.asList("Tester", "Lester", "Taster")); diff --git a/src/test/java/fr/xephi/authme/initialization/AuthMeServiceInitializerTest.java b/src/test/java/fr/xephi/authme/initialization/AuthMeServiceInitializerTest.java index d85377f5..e03a1435 100644 --- a/src/test/java/fr/xephi/authme/initialization/AuthMeServiceInitializerTest.java +++ b/src/test/java/fr/xephi/authme/initialization/AuthMeServiceInitializerTest.java @@ -316,6 +316,22 @@ public class AuthMeServiceInitializerTest { initializer.performReloadOnServices(); } + @Test + public void shouldRetrieveExistingInstancesOnly() { + // given + initializer.get(GammaService.class); + + // when + AlphaService alphaService = initializer.getIfAvailable(AlphaService.class); + BetaManager betaManager = initializer.getIfAvailable(BetaManager.class); + + // then + // was initialized because is dependency of GammaService + assertThat(alphaService, not(nullValue())); + // nothing caused this to be initialized + assertThat(betaManager, nullValue()); + } + private void expectRuntimeExceptionWith(String message) { expectedException.expect(RuntimeException.class); expectedException.expectMessage(containsString(message)); diff --git a/src/test/java/fr/xephi/authme/process/ProcessServiceTest.java b/src/test/java/fr/xephi/authme/process/ProcessServiceTest.java index c0505113..617e0d80 100644 --- a/src/test/java/fr/xephi/authme/process/ProcessServiceTest.java +++ b/src/test/java/fr/xephi/authme/process/ProcessServiceTest.java @@ -2,6 +2,8 @@ package fr.xephi.authme.process; import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.output.Messages; +import fr.xephi.authme.permission.AuthGroupHandler; +import fr.xephi.authme.permission.AuthGroupType; import fr.xephi.authme.permission.PermissionNode; import fr.xephi.authme.permission.PermissionsManager; import fr.xephi.authme.permission.PlayerPermission; @@ -43,6 +45,9 @@ public class ProcessServiceTest { @Mock private PermissionsManager permissionsManager; + @Mock + private AuthGroupHandler authGroupHandler; + @Test public void shouldGetProperty() { // given @@ -166,4 +171,19 @@ public class ProcessServiceTest { verify(permissionsManager).hasPermission(player, permission); assertThat(result, equalTo(true)); } + + @Test + public void shouldSetPermissionGroup() { + // given + Player player = mock(Player.class); + AuthGroupType type = AuthGroupType.LOGGED_IN; + given(authGroupHandler.setGroup(player, type)).willReturn(true); + + // when + boolean result = processService.setGroup(player, type); + + // then + verify(authGroupHandler).setGroup(player, type); + assertThat(result, equalTo(true)); + } } diff --git a/src/test/java/fr/xephi/authme/util/StringUtilsTest.java b/src/test/java/fr/xephi/authme/util/StringUtilsTest.java index 6cc7799a..a09ac87c 100644 --- a/src/test/java/fr/xephi/authme/util/StringUtilsTest.java +++ b/src/test/java/fr/xephi/authme/util/StringUtilsTest.java @@ -1,5 +1,6 @@ package fr.xephi.authme.util; +import fr.xephi.authme.TestHelper; import org.junit.Test; import java.io.File; @@ -145,4 +146,9 @@ public class StringUtilsTest { // then assertThat(result, equalTo("path" + File.separator + "to" + File.separator + "test-file.txt")); } + + @Test + public void shouldHaveHiddenConstructor() { + TestHelper.validateHasOnlyPrivateEmptyConstructor(StringUtils.class); + } } diff --git a/src/test/java/fr/xephi/authme/util/TeleportationServiceTest.java b/src/test/java/fr/xephi/authme/util/TeleportationServiceTest.java index b39fc9cf..ad9d9620 100644 --- a/src/test/java/fr/xephi/authme/util/TeleportationServiceTest.java +++ b/src/test/java/fr/xephi/authme/util/TeleportationServiceTest.java @@ -5,7 +5,6 @@ import fr.xephi.authme.cache.auth.PlayerCache; import fr.xephi.authme.cache.limbo.LimboPlayer; import fr.xephi.authme.events.FirstSpawnTeleportEvent; import fr.xephi.authme.events.SpawnTeleportEvent; -import fr.xephi.authme.output.Messages; import fr.xephi.authme.settings.NewSetting; import fr.xephi.authme.settings.SpawnLoader; import fr.xephi.authme.settings.properties.RestrictionSettings; @@ -48,9 +47,6 @@ public class TeleportationServiceTest { @Mock private NewSetting settings; - @Mock - private Messages messages; - @Mock private BukkitService bukkitService; From 941d4f09be85b417bc35937a8e0bad6ff358271a Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Thu, 16 Jun 2016 04:14:18 +0200 Subject: [PATCH 200/200] Beta3! --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index ec21ff63..33181e69 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ fr.xephi authme - 5.2-SNAPSHOT + 5.2-BETA3 jar AuthMeReloaded