diff --git a/gradle.properties b/gradle.properties index 95a9328a..35049466 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,2 @@ -group=fr.xephi +group=fr.xephi.authme version=5.6.0-FORK-b50 \ No newline at end of file diff --git a/plugin/build.gradle.kts b/plugin/build.gradle.kts index dc6edf87..f811c88e 100644 --- a/plugin/build.gradle.kts +++ b/plugin/build.gradle.kts @@ -20,6 +20,8 @@ subprojects { } dependencies { + implementation(project(":project:module-util")) + implementation(project(":project:module-configuration")) // Spigot API, https://www.spigotmc.org/ compileOnly("org.spigotmc:spigot-api:1.20.6-R0.1-SNAPSHOT") // Java Libraries diff --git a/plugin/platform-bukkit/build.gradle.kts b/plugin/platform-bukkit/build.gradle.kts new file mode 100644 index 00000000..54b3cb5c --- /dev/null +++ b/plugin/platform-bukkit/build.gradle.kts @@ -0,0 +1,5 @@ +description = "Fork of the first authentication plugin for the Bukkit API!" + +dependencies { + +} \ No newline at end of file diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/AuthMe.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/AuthMe.java new file mode 100644 index 00000000..f2bff2dd --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/AuthMe.java @@ -0,0 +1,474 @@ +package fr.xephi.authme; + +import ch.jalu.injector.Injector; +import ch.jalu.injector.InjectorBuilder; +import com.alessiodp.libby.BukkitLibraryManager; +import com.github.Anon8281.universalScheduler.UniversalScheduler; +import com.github.Anon8281.universalScheduler.scheduling.schedulers.TaskScheduler; +import fr.xephi.authme.api.v3.AuthMeApi; +import fr.xephi.authme.command.CommandHandler; +import fr.xephi.authme.command.TabCompleteHandler; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.initialization.DataFolder; +import fr.xephi.authme.initialization.DataSourceProvider; +import fr.xephi.authme.initialization.OnShutdownPlayerSaver; +import fr.xephi.authme.initialization.OnStartupTasks; +import fr.xephi.authme.initialization.SettingsProvider; +import fr.xephi.authme.initialization.TaskCloser; +import fr.xephi.authme.listener.AdvancedShulkerFixListener; +import fr.xephi.authme.listener.BedrockAutoLoginListener; +import fr.xephi.authme.listener.BlockListener; +import fr.xephi.authme.listener.DoubleLoginFixListener; +import fr.xephi.authme.listener.EntityListener; +import fr.xephi.authme.listener.LoginLocationFixListener; +import fr.xephi.authme.listener.PlayerListener; +import fr.xephi.authme.listener.PlayerListener111; +import fr.xephi.authme.listener.PlayerListener19; +import fr.xephi.authme.listener.PlayerListener19Spigot; +import fr.xephi.authme.listener.PlayerListenerHigherThan18; +import fr.xephi.authme.listener.PurgeListener; +import fr.xephi.authme.listener.ServerListener; +import fr.xephi.authme.mail.EmailService; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.security.crypts.Sha256; +import fr.xephi.authme.service.BackupService; +import fr.xephi.authme.service.BukkitService; +import fr.xephi.authme.service.MigrationService; +import fr.xephi.authme.service.bungeecord.BungeeReceiver; +import fr.xephi.authme.service.velocity.VelocityReceiver; +import fr.xephi.authme.service.yaml.YamlParseException; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.SettingsWarner; +import fr.xephi.authme.settings.properties.EmailSettings; +import fr.xephi.authme.settings.properties.HooksSettings; +import fr.xephi.authme.settings.properties.SecuritySettings; +import fr.xephi.authme.task.CleanupTask; +import fr.xephi.authme.task.Updater; +import fr.xephi.authme.task.purge.PurgeService; +import fr.xephi.authme.util.ExceptionUtils; +import org.bukkit.Server; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.plugin.PluginManager; +import org.bukkit.plugin.java.JavaPlugin; +import org.jetbrains.annotations.NotNull; + +import javax.inject.Inject; +import java.io.File; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Objects; +import java.util.function.Consumer; + +import static fr.xephi.authme.service.BukkitService.TICKS_PER_MINUTE; +import static fr.xephi.authme.util.Utils.isClassLoaded; + +/** + * The AuthMe main class. + */ +public class AuthMe extends JavaPlugin { + + // Constants + private static final String PLUGIN_NAME = "AuthMeReloaded"; + private static final String LOG_FILENAME = "authme.log"; + private static final int CLEANUP_INTERVAL = 5 * TICKS_PER_MINUTE; + + // Version and build number values + private static String pluginVersion = "5.7.0-Fork"; + private static final String pluginBuild = "b"; + private static String pluginBuildNumber = "51"; + // Private instances + private EmailService emailService; + private CommandHandler commandHandler; + private static TaskScheduler scheduler; + @Inject + public static Settings settings; + private DataSource database; + private BukkitService bukkitService; + private Injector injector; + private BackupService backupService; + public static ConsoleLogger logger; + + /** + * Constructor. + */ + public AuthMe() { + } + + /** + * Get the plugin's build + * + * @return The plugin's build + */ + public static String getPluginBuild() { + return pluginBuild; + } + + + /** + * Get the plugin's name. + * + * @return The plugin's name. + */ + public static String getPluginName() { + return PLUGIN_NAME; + } + + /** + * Get the plugin's version. + * + * @return The plugin's version. + */ + public static String getPluginVersion() { + return pluginVersion; + } + + /** + * Get the plugin's build number. + * + * @return The plugin's build number. + */ + public static String getPluginBuildNumber() { + return pluginBuildNumber; + } + + /** + * Get the scheduler + */ + public static TaskScheduler getScheduler() { + return scheduler; + } + + /** + * The library manager + */ + public static BukkitLibraryManager libraryManager; + + /** + * Method called when the server enables the plugin. + */ + @Override + public void onEnable() { + // Load the plugin version data from the plugin description file + loadPluginInfo(getDescription().getVersion()); + scheduler = UniversalScheduler.getScheduler(this); + libraryManager = new BukkitLibraryManager(this); + + // Set the Logger instance and log file path + ConsoleLogger.initialize(getLogger(), new File(getDataFolder(), LOG_FILENAME)); + logger = ConsoleLoggerFactory.get(AuthMe.class); + logger.info("You are running an unofficial fork version of AuthMe!"); + + + // Check server version + if (!isClassLoaded("org.spigotmc.event.player.PlayerSpawnLocationEvent") + || !isClassLoaded("org.bukkit.event.player.PlayerInteractAtEntityEvent")) { + logger.warning("You are running an unsupported server version (" + getServerNameVersionSafe() + "). " + + "AuthMe requires Spigot 1.8.X or later!"); + stopOrUnload(); + return; + } + + // Prevent running AuthMeBridge due to major exploit issues + if (getServer().getPluginManager().isPluginEnabled("AuthMeBridge")) { + logger.warning("Detected AuthMeBridge, support for it has been dropped as it was " + + "causing exploit issues, please use AuthMeBungee instead! Aborting!"); + stopOrUnload(); + return; + } + + // Initialize the plugin + try { + initialize(); + } catch (Throwable th) { + YamlParseException yamlParseException = ExceptionUtils.findThrowableInCause(YamlParseException.class, th); + if (yamlParseException == null) { + logger.logException("Aborting initialization of AuthMe:", th); + th.printStackTrace(); + } else { + logger.logException("File '" + yamlParseException.getFile() + "' contains invalid YAML. " + + "Please run its contents through http://yamllint.com", yamlParseException); + } + stopOrUnload(); + return; + } + + // Show settings warnings + injector.getSingleton(SettingsWarner.class).logWarningsForMisconfigurations(); + + // Schedule clean up task + CleanupTask cleanupTask = injector.getSingleton(CleanupTask.class); + cleanupTask.runTaskTimerAsynchronously(this, CLEANUP_INTERVAL, CLEANUP_INTERVAL); + // Do a backup on start + backupService.doBackup(BackupService.BackupCause.START); + // Set up Metrics + OnStartupTasks.sendMetrics(this, settings); + if (settings.getProperty(SecuritySettings.SHOW_STARTUP_BANNER)) { + logger.info("\n" + " ___ __ __ __ ___ \n" + + " / | __ __/ /_/ /_ / |/ /__ \n" + + " / /| |/ / / / __/ __ \\/ /|_/ / _ \\\n" + + " / ___ / /_/ / /_/ / / / / / / __/\n" + + "/_/ |_\\__,_/\\__/_/ /_/_/ /_/\\___/ \n" + + " "); + } + //detect server brand with classloader + checkServerType(); + try { + Objects.requireNonNull(getCommand("register")).setTabCompleter(new TabCompleteHandler()); + Objects.requireNonNull(getCommand("login")).setTabCompleter(new TabCompleteHandler()); + } catch (NullPointerException ignored) { + } + logger.info("AuthMeReReloaded is enabled successfully!"); + // Purge on start if enabled + PurgeService purgeService = injector.getSingleton(PurgeService.class); + purgeService.runAutoPurge(); + logger.info("GitHub: https://github.com/HaHaWTH/AuthMeReReloaded/"); + if (settings.getProperty(SecuritySettings.CHECK_FOR_UPDATES)) { + checkForUpdates(); + } + } + + + /** + * Load the version and build number of the plugin from the description file. + * + * @param versionRaw the version as given by the plugin description file + */ + + private static void loadPluginInfo(String versionRaw) { + int index = versionRaw.lastIndexOf("-"); + if (index != -1) { + pluginVersion = versionRaw.substring(0, index); + pluginBuildNumber = versionRaw.substring(index + 1); + if (pluginBuildNumber.startsWith("b")) { + pluginBuildNumber = pluginBuildNumber.substring(1); + } + } + } + + /** + * Initialize the plugin and all the services. + */ + private void initialize() { + // Create plugin folder + getDataFolder().mkdir(); + + // Create injector, provide elements from the Bukkit environment and register providers + injector = new InjectorBuilder() + .addDefaultHandlers("fr.xephi.authme") + .create(); + injector.register(AuthMe.class, this); + injector.register(Server.class, getServer()); + injector.register(PluginManager.class, getServer().getPluginManager()); + injector.provide(DataFolder.class, getDataFolder()); + injector.registerProvider(Settings.class, SettingsProvider.class); + injector.registerProvider(DataSource.class, DataSourceProvider.class); + + // Get settings and set up logger + settings = injector.getSingleton(Settings.class); + ConsoleLoggerFactory.reloadSettings(settings); + OnStartupTasks.setupConsoleFilter(getLogger()); + + // Set all service fields on the AuthMe class + instantiateServices(injector); + + // Convert deprecated PLAINTEXT hash entries + MigrationService.changePlainTextToSha256(settings, database, new Sha256()); + + // If the server is empty (fresh start) just set all the players as unlogged + if (bukkitService.getOnlinePlayers().isEmpty()) { + database.purgeLogged(); + } + + // Register event listeners + registerEventListeners(injector); + + // Start Email recall task if needed + OnStartupTasks onStartupTasks = injector.newInstance(OnStartupTasks.class); + onStartupTasks.scheduleRecallEmailTask(); + } + + /** + * Instantiates all services. + * + * @param injector the injector + */ + void instantiateServices(Injector injector) { + database = injector.getSingleton(DataSource.class); + bukkitService = injector.getSingleton(BukkitService.class); + commandHandler = injector.getSingleton(CommandHandler.class); + emailService = injector.getSingleton(EmailService.class); + backupService = injector.getSingleton(BackupService.class); + + // Trigger instantiation (class not used elsewhere) + injector.getSingleton(BungeeReceiver.class); + injector.getSingleton(VelocityReceiver.class); + + // Trigger construction of API classes; they will keep track of the singleton + injector.getSingleton(AuthMeApi.class); + } + + /** + * Registers all event listeners. + * + * @param injector the injector + */ + void registerEventListeners(Injector injector) { + // Get the plugin manager instance + PluginManager pluginManager = getServer().getPluginManager(); + + // Register event listeners + pluginManager.registerEvents(injector.getSingleton(PlayerListener.class), this); + pluginManager.registerEvents(injector.getSingleton(BlockListener.class), this); + pluginManager.registerEvents(injector.getSingleton(EntityListener.class), this); + pluginManager.registerEvents(injector.getSingleton(ServerListener.class), this); + + + // Try to register 1.8+ player listeners + if (isClassLoaded("org.bukkit.event.entity.EntityPickupItemEvent") && isClassLoaded("org.bukkit.event.player.PlayerSwapHandItemsEvent")) { + pluginManager.registerEvents(injector.getSingleton(PlayerListenerHigherThan18.class), this); + } else if (isClassLoaded("org.bukkit.event.player.PlayerSwapHandItemsEvent")) { + pluginManager.registerEvents(injector.getSingleton(PlayerListener19.class), this); + } +// Try to register 1.9 player listeners(Moved to else-if) +// if (isClassLoaded("org.bukkit.event.player.PlayerSwapHandItemsEvent")) { +// pluginManager.registerEvents(injector.getSingleton(PlayerListener19.class), this); +// } + + // Try to register 1.9 spigot player listeners + if (isClassLoaded("org.spigotmc.event.player.PlayerSpawnLocationEvent")) { + pluginManager.registerEvents(injector.getSingleton(PlayerListener19Spigot.class), this); + } + + // Register listener for 1.11 events if available + if (isClassLoaded("org.bukkit.event.entity.EntityAirChangeEvent")) { + pluginManager.registerEvents(injector.getSingleton(PlayerListener111.class), this); + } + + //Register 3rd party listeners + if (settings.getProperty(SecuritySettings.FORCE_LOGIN_BEDROCK) && settings.getProperty(HooksSettings.HOOK_FLOODGATE_PLAYER) && getServer().getPluginManager().getPlugin("floodgate") != null) { + pluginManager.registerEvents(injector.getSingleton(BedrockAutoLoginListener.class), this); + } else if (settings.getProperty(SecuritySettings.FORCE_LOGIN_BEDROCK) && (!settings.getProperty(HooksSettings.HOOK_FLOODGATE_PLAYER) || getServer().getPluginManager().getPlugin("floodgate") == null)) { + logger.warning("Failed to enable BedrockAutoLogin, ensure hookFloodgate: true and floodgate is loaded."); + } + if (settings.getProperty(SecuritySettings.LOGIN_LOC_FIX_SUB_UNDERGROUND) || settings.getProperty(SecuritySettings.LOGIN_LOC_FIX_SUB_PORTAL)) { + pluginManager.registerEvents(injector.getSingleton(LoginLocationFixListener.class), this); + } + if (settings.getProperty(SecuritySettings.ANTI_GHOST_PLAYERS)) { + pluginManager.registerEvents(injector.getSingleton(DoubleLoginFixListener.class), this); + } + if (settings.getProperty(SecuritySettings.ADVANCED_SHULKER_FIX) && !isClassLoaded("org.bukkit.event.player.PlayerCommandSendEvent")) { + pluginManager.registerEvents(injector.getSingleton(AdvancedShulkerFixListener.class), this); + } else if (settings.getProperty(SecuritySettings.ADVANCED_SHULKER_FIX) && isClassLoaded("org.bukkit.event.player.PlayerCommandSendEvent")) { + logger.warning("You are running an 1.13+ minecraft server, AdvancedShulkerFix won't enable."); + } + if (settings.getProperty(SecuritySettings.PURGE_DATA_ON_QUIT)) { + pluginManager.registerEvents(injector.getSingleton(PurgeListener.class), this); + } + } + + /** + * Stops the server or disables the plugin, as defined in the configuration. + */ + public void stopOrUnload() { + if (settings == null || settings.getProperty(SecuritySettings.STOP_SERVER_ON_PROBLEM)) { + getLogger().warning("THE SERVER IS GOING TO SHUT DOWN AS DEFINED IN THE CONFIGURATION!"); + setEnabled(false); + getServer().shutdown(); + } else { + setEnabled(false); + } + } + + @Override + public void onDisable() { + // onDisable is also called when we prematurely abort, so any field may be null + OnShutdownPlayerSaver onShutdownPlayerSaver = injector == null + ? null + : injector.createIfHasDependencies(OnShutdownPlayerSaver.class); + if (onShutdownPlayerSaver != null) { + onShutdownPlayerSaver.saveAllPlayers(); + } + if (settings != null && settings.getProperty(EmailSettings.SHUTDOWN_MAIL)) { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy'.'MM'.'dd'.' HH:mm:ss"); + Date date = new Date(System.currentTimeMillis()); + emailService.sendShutDown(settings.getProperty(EmailSettings.SHUTDOWN_MAIL_ADDRESS),dateFormat.format(date)); + } + + // Do backup on stop if enabled + if (backupService != null) { + backupService.doBackup(BackupService.BackupCause.STOP); + } + + // Wait for tasks and close data source + new TaskCloser(database).run(); + + // Disabled correctly + Consumer infoLogMethod = logger == null ? getLogger()::info : logger::info; + infoLogMethod.accept("AuthMe " + this.getDescription().getVersion() + " is unloaded successfully!"); + ConsoleLogger.closeFileWriter(); + } + + private void checkForUpdates() { + logger.info("Checking for updates..."); + Updater updater = new Updater(pluginBuild + pluginBuildNumber); + bukkitService.runTaskAsynchronously(() -> { + if (updater.isUpdateAvailable()) { + String message = "New version available! Latest:" + updater.getLatestVersion() + " Current:" + pluginBuild + pluginBuildNumber; + logger.warning(message); + logger.warning("Download from here: https://modrinth.com/plugin/authmerereloaded"); + } else { + logger.info("You are running the latest version."); + } + }); + } + + + private void checkServerType() { + if (isClassLoaded("io.papermc.paper.threadedregions.RegionizedServer")) { + logger.info("AuthMeReReloaded is running on Folia"); + } else if (isClassLoaded("com.destroystokyo.paper.PaperConfig")) { + logger.info("AuthMeReReloaded is running on Paper"); + } else if (isClassLoaded("catserver.server.CatServerConfig")) { + logger.info("AuthMeReReloaded is running on CatServer"); + } else if (isClassLoaded("org.spigotmc.SpigotConfig")) { + logger.info("AuthMeReReloaded is running on Spigot"); + } else if (isClassLoaded("org.bukkit.craftbukkit.CraftServer")) { + logger.info("AuthMeReReloaded is running on Bukkit"); + } else { + logger.info("AuthMeReReloaded is running on Unknown*"); + } + } + + + /** + * Handle Bukkit commands. + * + * @param sender The command sender (Bukkit). + * @param cmd The command (Bukkit). + * @param commandLabel The command label (Bukkit). + * @param args The command arguments (Bukkit). + * @return True if the command was executed, false otherwise. + */ + @Override + public boolean onCommand(@NotNull CommandSender sender, @NotNull Command cmd, + @NotNull String commandLabel, String[] args) { + // Make sure the command handler has been initialized + if (commandHandler == null) { + getLogger().severe("AuthMe command handler is not available"); + return false; + } + + // Handle the command + return commandHandler.processCommand(sender, commandLabel, args); + } + + private String getServerNameVersionSafe() { + try { + Server server = getServer(); + return server.getName() + " v. " + server.getVersion(); + } catch (Throwable ignore) { + return "-"; + } + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/ConsoleLogger.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/ConsoleLogger.java new file mode 100644 index 00000000..1b1d6788 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/ConsoleLogger.java @@ -0,0 +1,287 @@ +package fr.xephi.authme; + +import com.google.common.base.Throwables; +import fr.xephi.authme.output.LogLevel; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.properties.PluginSettings; +import fr.xephi.authme.settings.properties.SecuritySettings; +import fr.xephi.authme.util.ExceptionUtils; + +import java.io.Closeable; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.nio.charset.StandardCharsets; +import java.text.MessageFormat; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.util.function.Supplier; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * AuthMe logger. + */ +public final class ConsoleLogger { + + private static final String NEW_LINE = System.getProperty("line.separator"); + /** Formatter which formats dates to something like "[08-16 21:18:46]" for any given LocalDateTime. */ + private static final DateTimeFormatter DATE_FORMAT = new DateTimeFormatterBuilder() + .appendLiteral('[') + .appendPattern("MM-dd HH:mm:ss") + .appendLiteral(']') + .toFormatter(); + + // Outside references + private static File logFile; + private static Logger logger; + + // Shared state + private static OutputStreamWriter fileWriter; + + // Individual state + private final String name; + private LogLevel logLevel = LogLevel.INFO; + + /** + * Constructor. + * + * @param name the name of this logger (the fully qualified class name using it) + */ + public ConsoleLogger(String name) { + this.name = name; + } + + // -------- + // Configurations + // -------- + + public static void initialize(Logger logger, File logFile) { + ConsoleLogger.logger = logger; + ConsoleLogger.logFile = logFile; + } + + /** + * Sets logging settings which are shared by all logger instances. + * + * @param settings the settings to read from + */ + public static void initializeSharedSettings(Settings settings) { + boolean useLogging = settings.getProperty(SecuritySettings.USE_LOGGING); + if (useLogging) { + initializeFileWriter(); + } else { + closeFileWriter(); + } + } + + /** + * Sets logging settings which are individual to all loggers. + * + * @param settings the settings to read from + */ + public void initializeSettings(Settings settings) { + this.logLevel = settings.getProperty(PluginSettings.LOG_LEVEL); + } + + public LogLevel getLogLevel() { + return logLevel; + } + + public String getName() { + return name; + } + + + // -------- + // Logging methods + // -------- + + /** + * Log a WARN message. + * + * @param message The message to log + */ + public void warning(String message) { + logger.warning(message); + writeLog("[WARN] " + message); + } + + /** + * Log a Throwable with the provided message on WARNING level + * and save the stack trace to the log file. + * + * @param message The message to accompany the exception + * @param th The Throwable to log + */ + public void logException(String message, Throwable th) { + warning(message + " " + ExceptionUtils.formatException(th)); + writeLog(Throwables.getStackTraceAsString(th)); + } + + /** + * Log an INFO message. + * + * @param message The message to log + */ + public void info(String message) { + logger.info(message); + writeLog("[INFO] " + message); + } + + /** + * Log a FINE message if enabled. + *

+ * Implementation note: this logs a message on INFO level because + * levels below INFO are disabled by Bukkit/Spigot. + * + * @param message The message to log + */ + public void fine(String message) { + if (logLevel.includes(LogLevel.FINE)) { + logger.info(message); + writeLog("[INFO:FINE] " + message); + } + } + + // -------- + // Debug log methods + // -------- + + /** + * Log a DEBUG message if enabled. + *

+ * Implementation note: this logs a message on INFO level and prefixes it with "DEBUG" because + * levels below INFO are disabled by Bukkit/Spigot. + * + * @param message The message to log + */ + public void debug(String message) { + if (logLevel.includes(LogLevel.DEBUG)) { + logAndWriteWithDebugPrefix(message); + } + } + + /** + * Log the DEBUG message. + * + * @param message the message + * @param param1 parameter to replace in the message + */ + // Avoids array creation if DEBUG level is disabled + public void debug(String message, Object param1) { + if (logLevel.includes(LogLevel.DEBUG)) { + debug(message, new Object[]{param1}); + } + } + + /** + * Log the DEBUG message. + * + * @param message the message + * @param param1 first param to replace in message + * @param param2 second param to replace in message + */ + // Avoids array creation if DEBUG level is disabled + public void debug(String message, Object param1, Object param2) { + if (logLevel.includes(LogLevel.DEBUG)) { + debug(message, new Object[]{param1, param2}); + } + } + + /** + * Log the DEBUG message. + * + * @param message the message + * @param params the params to replace in the message + */ + public void debug(String message, Object... params) { + if (logLevel.includes(LogLevel.DEBUG)) { + logAndWriteWithDebugPrefix(MessageFormat.format(message, params)); + } + } + + /** + * Log the DEBUG message from the supplier if enabled. + * + * @param msgSupplier the message supplier + */ + public void debug(Supplier msgSupplier) { + if (logLevel.includes(LogLevel.DEBUG)) { + logAndWriteWithDebugPrefix(msgSupplier.get()); + } + } + + private void logAndWriteWithDebugPrefix(String message) { + String debugMessage = "[INFO:DEBUG] " + message; + logger.info(debugMessage); + writeLog(debugMessage); + } + + // -------- + // Helpers + // -------- + + /** + * Closes the file writer. + */ + public static void closeFileWriter() { + if (fileWriter != null) { + try { + fileWriter.flush(); + } catch (IOException ignored) { + } finally { + closeSafely(fileWriter); + fileWriter = null; + } + } + } + + /** + * Write a message into the log file with a TimeStamp if enabled. + * + * @param message The message to write to the log + */ + private static void writeLog(String message) { + if (fileWriter != null) { + String dateTime = DATE_FORMAT.format(LocalDateTime.now()); + try { + fileWriter.write(dateTime); + fileWriter.write(": "); + fileWriter.write(message); + fileWriter.write(NEW_LINE); + fileWriter.flush(); + } catch (IOException ignored) { + } + } + } + + private static void closeSafely(Closeable closeable) { + if (closeable != null) { + try { + closeable.close(); + } catch (IOException e) { + logger.log(Level.SEVERE, "Failed to close resource", e); + } + } + } + + /** + * Populates the {@link #fileWriter} field if it is null, handling any exceptions that might + * arise during its creation. + */ + private static void initializeFileWriter() { + if (fileWriter == null) { + FileOutputStream fos = null; + try { + fos = new FileOutputStream(logFile, true); + fileWriter = new OutputStreamWriter(fos, StandardCharsets.UTF_8); + } catch (Exception e) { + closeSafely(fos); + logger.log(Level.SEVERE, "Failed to create writer to AuthMe log file", e); + } + } + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/api/v3/AuthMeApi.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/api/v3/AuthMeApi.java new file mode 100644 index 00000000..27743d5d --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/api/v3/AuthMeApi.java @@ -0,0 +1,397 @@ +package fr.xephi.authme.api.v3; + +import fr.xephi.authme.AuthMe; +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.data.auth.PlayerCache; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.process.Management; +import fr.xephi.authme.process.register.executors.ApiPasswordRegisterParams; +import fr.xephi.authme.process.register.executors.RegistrationMethod; +import fr.xephi.authme.security.PasswordSecurity; +import fr.xephi.authme.security.crypts.HashedPassword; +import fr.xephi.authme.service.GeoIpService; +import fr.xephi.authme.service.ValidationService; +import fr.xephi.authme.util.PlayerUtils; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.entity.Player; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.InventoryView; + +import javax.inject.Inject; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.Optional; + +import static fr.xephi.authme.listener.PlayerListener.PENDING_INVENTORIES; + +/** + * The current API of AuthMe. + * + * Recommended method of retrieving the AuthMeApi object: + * + * AuthMeApi authmeApi = AuthMeApi.getInstance(); + * + */ +@SuppressWarnings("unused") +public class AuthMeApi { + + private static AuthMeApi singleton; + private final AuthMe plugin; + private final DataSource dataSource; + private final PasswordSecurity passwordSecurity; + private final Management management; + private final ValidationService validationService; + private final PlayerCache playerCache; + private final GeoIpService geoIpService; + + /* + * Constructor for AuthMeApi. + */ + @Inject + AuthMeApi(AuthMe plugin, DataSource dataSource, PlayerCache playerCache, PasswordSecurity passwordSecurity, + Management management, ValidationService validationService, GeoIpService geoIpService) { + this.plugin = plugin; + this.dataSource = dataSource; + this.passwordSecurity = passwordSecurity; + this.management = management; + this.validationService = validationService; + this.playerCache = playerCache; + this.geoIpService = geoIpService; + AuthMeApi.singleton = this; + } + + /** + * Get the AuthMeApi object for AuthMe. + * + * @return The AuthMeApi object, or null if the AuthMe plugin is not enabled or not fully initialized yet + */ + public static AuthMeApi getInstance() { + return singleton; + } + + /** + * Return the plugin instance. + * + * @return The AuthMe instance + */ + public AuthMe getPlugin() { + return plugin; + } + + /** + * Gather the version number of the plugin. + * This can be used to determine whether certain AuthMeApi features are available or not. + * + * @return Plugin version identifier as a string. + */ + public String getPluginVersion() { + return AuthMe.getPluginVersion(); + } + + /** + * Return whether the given player is authenticated. + * + * @param player The player to verify + * @return true if the player is authenticated + */ + public boolean isAuthenticated(Player player) { + return playerCache.isAuthenticated(player.getName()); + } + + /** + * Check whether the given player is an NPC. + * + * @param player The player to verify + * @return true if the player is an npc + */ + public boolean isNpc(Player player) { + return PlayerUtils.isNpc(player); + } + + /** + * Check whether the given player is unrestricted. For such players, AuthMe will not require + * them to authenticate. + * + * @param player The player to verify + * @return true if the player is unrestricted + * @see fr.xephi.authme.settings.properties.RestrictionSettings#UNRESTRICTED_NAMES + */ + public boolean isUnrestricted(Player player) { + return validationService.isUnrestricted(player.getName()); + } + + /** + * Get the last location of an online player. + * + * @param player The player to process + * @return The location of the player + */ + public Location getLastLocation(Player player) { + PlayerAuth auth = playerCache.getAuth(player.getName()); + if (auth != null) { + return new Location(Bukkit.getWorld(auth.getWorld()), + auth.getQuitLocX(), auth.getQuitLocY(), auth.getQuitLocZ(), auth.getYaw(), auth.getPitch()); + } + return null; + } + + /** + * Returns the AuthMe info of the given player's name, or empty optional if the player doesn't exist. + * + * @param playerName The player name to look up + * @return AuthMe player info, or empty optional if the player doesn't exist + */ + public Optional getPlayerInfo(String playerName) { + PlayerAuth auth = playerCache.getAuth(playerName); + if (auth == null) { + auth = dataSource.getAuth(playerName); + } + return AuthMePlayerImpl.fromPlayerAuth(auth); + } + + /** + * Get the last ip address of a player. + * + * @param playerName The name of the player to process + * @return The last ip address of the player + */ + public String getLastIp(String playerName) { + PlayerAuth auth = playerCache.getAuth(playerName); + if (auth == null) { + auth = dataSource.getAuth(playerName); + } + if (auth != null) { + return auth.getLastIp(); + } + return null; + } + + /** + * Get user names by ip. + * + * @param address The ip address to process + * @return The list of user names related to the ip address + */ + public List getNamesByIp(String address) { + return dataSource.getAllAuthsByIp(address); + } + + /** + * Get the last (AuthMe) login date of a player. + * + * @param playerName The name of the player to process + * @return The date of the last login, or null if the player doesn't exist or has never logged in + * @deprecated Use Java 8's Instant method {@link #getLastLoginTime(String)} + */ + @Deprecated + public Date getLastLogin(String playerName) { + Long lastLogin = getLastLoginMillis(playerName); + return lastLogin == null ? null : new Date(lastLogin); + } + + /** + * Get the last (AuthMe) login timestamp of a player. + * + * @param playerName The name of the player to process + * + * @return The timestamp of the last login, or null if the player doesn't exist or has never logged in + */ + public Instant getLastLoginTime(String playerName) { + Long lastLogin = getLastLoginMillis(playerName); + return lastLogin == null ? null : Instant.ofEpochMilli(lastLogin); + } + + private Long getLastLoginMillis(String playerName) { + PlayerAuth auth = playerCache.getAuth(playerName); + if (auth == null) { + auth = dataSource.getAuth(playerName); + } + if (auth != null) { + return auth.getLastLogin(); + } + return null; + } + + /** + * Return whether the player is registered. + * + * @param playerName The player name to check + * @return true if player is registered, false otherwise + */ + public boolean isRegistered(String playerName) { + String player = playerName.toLowerCase(Locale.ROOT); + return dataSource.isAuthAvailable(player); + } + + /** + * Check the password for the given player. + * + * @param playerName The player to check the password for + * @param passwordToCheck The password to check + * @return true if the password is correct, false otherwise + */ + public boolean checkPassword(String playerName, String passwordToCheck) { + return passwordSecurity.comparePassword(passwordToCheck, playerName); + } + + /** + * 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) { + String name = playerName.toLowerCase(Locale.ROOT); + if (isRegistered(name)) { + return false; + } + HashedPassword result = passwordSecurity.computeHash(password, name); + PlayerAuth auth = PlayerAuth.builder() + .name(name) + .password(result) + .realName(playerName) + .registrationDate(System.currentTimeMillis()) + .build(); + return dataSource.saveAuth(auth); + } + + /** + * Open an inventory for the given player at any time. + * + * @param player The player to open the inventory for + * @param inventory The inventory to open + * @return The inventory view + */ + public InventoryView openInventory(Player player, Inventory inventory) { + PENDING_INVENTORIES.add(inventory); + return player.openInventory(inventory); + } + + /** + * Force a player to login, i.e. the player is logged in without needing his password. + * + * @param player The player to log in + */ + public void forceLogin(Player player) { + management.forceLogin(player); + } + + /** + * Force a player to login, i.e. the player is logged in without needing his password. + * + * @param player The player to log in + * @param quiet Whether to suppress the login message + */ + public void forceLogin(Player player, boolean quiet) { + management.forceLogin(player, quiet); + } + + /** + * Force a player to logout. + * + * @param player The player to log out + */ + public void forceLogout(Player player) { + management.performLogout(player); + } + + /** + * 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) { + management.performRegister(RegistrationMethod.API_REGISTRATION, + ApiPasswordRegisterParams.of(player, password, 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) { + forceRegister(player, password, true); + } + + /** + * Unregister a player from AuthMe. + * + * @param player The player to unregister + */ + public void forceUnregister(Player player) { + management.performUnregisterByAdmin(null, player.getName(), player); + } + + /** + * Unregister a player from AuthMe by name. + * + * @param name the name of the player (case-insensitive) + */ + public void forceUnregister(String name) { + management.performUnregisterByAdmin(null, name, Bukkit.getPlayer(name)); + } + + /** + * Change a user's password + * + * @param name the user name + * @param newPassword the new password + */ + public void changePassword(String name, String newPassword) { + management.performPasswordChangeAsAdmin(null, name, newPassword); + } + + /** + * Get all the registered names (lowercase) + * + * @return registered names + */ + public List getRegisteredNames() { + List registeredNames = new ArrayList<>(); + dataSource.getAllAuths().forEach(auth -> registeredNames.add(auth.getNickname())); + return registeredNames; + } + + /** + * Get all the registered real-names (original case) + * + * @return registered real-names + */ + public List getRegisteredRealNames() { + List registeredNames = new ArrayList<>(); + dataSource.getAllAuths().forEach(auth -> registeredNames.add(auth.getRealName())); + return registeredNames; + } + + /** + * Get the country code of the given IP address. + * + * @param ip textual IP address to lookup. + * + * @return two-character ISO 3166-1 alpha code for the country. + */ + public String getCountryCode(String ip) { + return geoIpService.getCountryCode(ip); + } + + /** + * Get the country name of the given IP address. + * + * @param ip textual IP address to lookup. + * + * @return The name of the country. + */ + public String getCountryName(String ip) { + return geoIpService.getCountryName(ip); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/api/v3/AuthMePlayer.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/api/v3/AuthMePlayer.java new file mode 100644 index 00000000..4c64073b --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/api/v3/AuthMePlayer.java @@ -0,0 +1,65 @@ +package fr.xephi.authme.api.v3; + +import java.time.Instant; +import java.util.Optional; +import java.util.UUID; + +/** + * Read-only player info exposed in the AuthMe API. The data in this object is copied from the + * database and not updated afterwards. As such, it may become outdated if the player data changes + * in AuthMe. + * + * @see AuthMeApi#getPlayerInfo + */ +public interface AuthMePlayer { + + /** + * @return the case-sensitive name of the player, e.g. "thePlayer3030" - never null + */ + String getName(); + + /** + * Returns the UUID of the player as given by the server (may be offline UUID or not). + * The UUID is not present if AuthMe is configured not to store the UUID or if the data is not + * present (e.g. older record). + * + * @return player uuid, or empty optional if not available + */ + Optional getUuid(); + + /** + * Returns the email address associated with this player, or an empty optional if not available. + * + * @return player's email or empty optional + */ + Optional getEmail(); + + /** + * @return the registration date of the player's account - never null + */ + Instant getRegistrationDate(); + + /** + * Returns the IP address with which the player's account was registered. Returns an empty optional + * for older accounts, or if the account was registered by someone else (e.g. by an admin). + * + * @return the ip address used during the registration of the account, or empty optional + */ + Optional getRegistrationIpAddress(); + + /** + * Returns the last login date of the player. An empty optional is returned if the player never logged in. + * + * @return date the player last logged in successfully, or empty optional if not applicable + */ + Optional getLastLoginDate(); + + /** + * Returns the IP address the player last logged in with successfully. Returns an empty optional if the + * player never logged in. + * + * @return ip address the player last logged in with successfully, or empty optional if not applicable + */ + Optional getLastLoginIpAddress(); + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/api/v3/AuthMePlayerImpl.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/api/v3/AuthMePlayerImpl.java new file mode 100644 index 00000000..9ea0b643 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/api/v3/AuthMePlayerImpl.java @@ -0,0 +1,93 @@ +package fr.xephi.authme.api.v3; + +import fr.xephi.authme.data.auth.PlayerAuth; + +import java.time.Instant; +import java.util.Optional; +import java.util.UUID; + +/** + * Implementation of {@link AuthMePlayer}. This implementation is not part of the API and + * may have breaking changes in subsequent releases. + */ +class AuthMePlayerImpl implements AuthMePlayer { + + private String name; + private UUID uuid; + private String email; + + private Instant registrationDate; + private String registrationIpAddress; + + private Instant lastLoginDate; + private String lastLoginIpAddress; + + AuthMePlayerImpl() { + } + + /** + * Maps the given player auth to an AuthMePlayer instance. Returns an empty optional if + * the player auth is null. + * + * @param playerAuth the player auth or null + * @return the mapped player auth, or empty optional if the argument was null + */ + static Optional fromPlayerAuth(PlayerAuth playerAuth) { + if (playerAuth == null) { + return Optional.empty(); + } + + AuthMePlayerImpl authMeUser = new AuthMePlayerImpl(); + authMeUser.name = playerAuth.getRealName(); + authMeUser.uuid = playerAuth.getUuid(); + authMeUser.email = nullIfDefault(playerAuth.getEmail(), PlayerAuth.DB_EMAIL_DEFAULT); + Long lastLoginMillis = nullIfDefault(playerAuth.getLastLogin(), PlayerAuth.DB_LAST_LOGIN_DEFAULT); + authMeUser.registrationDate = toInstant(playerAuth.getRegistrationDate()); + authMeUser.registrationIpAddress = playerAuth.getRegistrationIp(); + authMeUser.lastLoginDate = toInstant(lastLoginMillis); + authMeUser.lastLoginIpAddress = nullIfDefault(playerAuth.getLastIp(), PlayerAuth.DB_LAST_IP_DEFAULT); + return Optional.of(authMeUser); + } + + @Override + public String getName() { + return name; + } + + public Optional getUuid() { + return Optional.ofNullable(uuid); + } + + @Override + public Optional getEmail() { + return Optional.ofNullable(email); + } + + @Override + public Instant getRegistrationDate() { + return registrationDate; + } + + @Override + public Optional getRegistrationIpAddress() { + return Optional.ofNullable(registrationIpAddress); + } + + @Override + public Optional getLastLoginDate() { + return Optional.ofNullable( lastLoginDate); + } + + @Override + public Optional getLastLoginIpAddress() { + return Optional.ofNullable(lastLoginIpAddress); + } + + private static Instant toInstant(Long epochMillis) { + return epochMillis == null ? null : Instant.ofEpochMilli(epochMillis); + } + + private static T nullIfDefault(T value, T defaultValue) { + return defaultValue.equals(value) ? null : value; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/CommandArgumentDescription.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/CommandArgumentDescription.java new file mode 100644 index 00000000..9ae3da4b --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/CommandArgumentDescription.java @@ -0,0 +1,61 @@ +package fr.xephi.authme.command; + +/** + * Wrapper for the description of a command argument. + */ +public class CommandArgumentDescription { + + /** + * Argument name (one-word description of the argument). + */ + private final String name; + /** + * Argument description. + */ + private final String description; + /** + * Defines whether the argument is optional. + */ + private final boolean isOptional; + + /** + * Constructor. + * + * @param name The argument name. + * @param description The argument description. + * @param isOptional True if the argument is optional, false otherwise. + */ + public CommandArgumentDescription(String name, String description, boolean isOptional) { + this.name = name; + this.description = description; + this.isOptional = isOptional; + } + + /** + * Get the argument name. + * + * @return Argument name. + */ + public String getName() { + return this.name; + } + + /** + * Get the argument description. + * + * @return Argument description. + */ + public String getDescription() { + return description; + } + + /** + * Return whether the argument is optional. + * + * @return True if the argument is optional, false otherwise. + */ + public boolean isOptional() { + return isOptional; + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/CommandDescription.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/CommandDescription.java new file mode 100644 index 00000000..040894f9 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/CommandDescription.java @@ -0,0 +1,294 @@ +package fr.xephi.authme.command; + +import fr.xephi.authme.permission.PermissionNode; +import fr.xephi.authme.util.StringUtils; +import fr.xephi.authme.util.Utils; + +import java.util.ArrayList; +import java.util.List; + +import static com.google.common.base.Preconditions.checkArgument; +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 + * the child defines. + */ +@SuppressWarnings("checkstyle:FinalClass") // Justification: class is mocked in multiple tests +public class CommandDescription { + + /** + * Defines the labels to execute the command. For example, if labels are "register" and "r" and the parent is + * the command for "/authme", then both "/authme register" and "/authme r" will be handled by this command. + */ + private List labels; + /** + * Short description of the command. + */ + private String description; + /** + * Detailed description of what the command does. + */ + private String detailedDescription; + /** + * The class implementing the command described by this object. + */ + private Class executableCommand; + /** + * The parent command. + */ + private CommandDescription parent; + /** + * The child commands that extend this command. + */ + private List children = new ArrayList<>(); + /** + * The arguments the command takes. + */ + private List arguments; + /** + * Permission node required to execute this command. + */ + private PermissionNode permission; + + /** + * Private constructor. + *

+ * Note for developers: Instances should be created with {@link CommandBuilder#register()} to be properly + * registered in the command tree. + * + * @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 + */ + private CommandDescription(List labels, String description, String detailedDescription, + Class executableCommand, CommandDescription parent, + List arguments, PermissionNode permission) { + this.labels = labels; + this.description = description; + this.detailedDescription = detailedDescription; + this.executableCommand = executableCommand; + this.parent = parent; + this.arguments = arguments; + this.permission = permission; + } + + /** + * Return all relative labels of this command. For example, if this object describes {@code /authme register} and + * {@code /authme r}, then it will return a list with {@code register} and {@code r}. The parent label + * {@code authme} is not returned. + * + * @return All labels of the command description. + */ + public List getLabels() { + return labels; + } + + /** + * Check whether this command description has the given label. + * + * @param commandLabel The label to check for. + * + * @return {@code true} if this command contains the given label, {@code false} otherwise. + */ + public boolean hasLabel(String commandLabel) { + for (String label : labels) { + if (label.equalsIgnoreCase(commandLabel)) { + return true; + } + } + return false; + } + + /** + * Return the {@link ExecutableCommand} class implementing this command. + * + * @return The executable command class + */ + public Class getExecutableCommand() { + return executableCommand; + } + + /** + * Return the parent. + * + * @return The parent command, or null for base commands + */ + public CommandDescription getParent() { + return parent; + } + + /** + * Return the number of labels necessary to get to this command. This corresponds to the number of parents + 1. + * + * @return The number of labels, e.g. for "/authme abc def" the label count is 3 + */ + public int getLabelCount() { + if (parent == null) { + return 1; + } + return parent.getLabelCount() + 1; + } + + /** + * Return all command children. + * + * @return Command children. + */ + public List getChildren() { + return children; + } + + /** + * Return all arguments the command takes. + * + * @return Command arguments. + */ + public List getArguments() { + return arguments; + } + + /** + * Return a short description of the command. + * + * @return Command description. + */ + public String getDescription() { + return description; + } + + /** + * Return a detailed description of the command. + * + * @return Detailed description. + */ + public String getDetailedDescription() { + return detailedDescription; + } + + /** + * Return the permission node required to execute the command. + * + * @return The permission node, or null if none are required to execute the command. + */ + public PermissionNode getPermission() { + return permission; + } + + /** + * Return a builder instance to create a new command description. + * + * @return The builder + */ + public static CommandBuilder builder() { + return new CommandBuilder(); + } + + /** + * Builder for initializing CommandDescription objects. + */ + public static final class CommandBuilder { + private List labels; + private String description; + private String detailedDescription; + private Class executableCommand; + private CommandDescription parent; + private List arguments = new ArrayList<>(); + private PermissionNode permission; + + /** + * Build a CommandDescription and register it onto the parent if available. + * + * @return The generated CommandDescription object + */ + public CommandDescription register() { + CommandDescription command = build(); + + if (command.parent != null) { + command.parent.children.add(command); + } + return command; + } + + /** + * Build a CommandDescription (without registering it on the parent). + * + * @return The generated CommandDescription object + */ + public CommandDescription build() { + checkArgument(!Utils.isCollectionEmpty(labels), "Labels may not be empty"); + checkArgument(!StringUtils.isBlank(description), "Description may not be empty"); + checkArgument(!StringUtils.isBlank(detailedDescription), "Detailed description may not be empty"); + checkArgument(executableCommand != null, "Executable command must be set"); + // parents and permissions may be null; arguments may be empty + + return new CommandDescription(labels, description, detailedDescription, executableCommand, + parent, arguments, permission); + } + + public CommandBuilder labels(List labels) { + this.labels = labels; + return this; + } + + public CommandBuilder labels(String... labels) { + return labels(asList(labels)); + } + + public CommandBuilder description(String description) { + this.description = description; + return this; + } + + public CommandBuilder detailedDescription(String detailedDescription) { + this.detailedDescription = detailedDescription; + return this; + } + + public CommandBuilder executableCommand(Class executableCommand) { + this.executableCommand = executableCommand; + return this; + } + + public CommandBuilder parent(CommandDescription parent) { + this.parent = parent; + return this; + } + + /** + * Add an argument that the command description requires. This method can be called multiples times to add + * multiple arguments. + * + * @param label The label of the argument (single word name of the argument) + * @param description The description of the argument + * @param isOptional True if the argument is optional, false if it is mandatory + * + * @return The builder + */ + public CommandBuilder withArgument(String label, String description, boolean isOptional) { + arguments.add(new CommandArgumentDescription(label, description, isOptional)); + return this; + } + + /** + * Add a permission node that 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/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/CommandHandler.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/CommandHandler.java new file mode 100644 index 00000000..08629c6f --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/CommandHandler.java @@ -0,0 +1,186 @@ +package fr.xephi.authme.command; + +import ch.jalu.injector.factory.Factory; +import fr.xephi.authme.AuthMe; +import fr.xephi.authme.command.help.HelpProvider; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.message.Messages; +import fr.xephi.authme.permission.PermissionsManager; +import fr.xephi.authme.util.StringUtils; +import org.bukkit.ChatColor; +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 invoking the correct {@link ExecutableCommand} based on incoming + * command labels or for displaying a help message for unknown command labels. + */ +public class CommandHandler { + + /** + * The threshold for suggesting a similar command. If the difference is below this value, we will + * ask the player whether he meant the similar command. + */ + private static final double SUGGEST_COMMAND_THRESHOLD = 0.75; + + private final CommandMapper commandMapper; + private final PermissionsManager permissionsManager; + private final Messages messages; + private final HelpProvider helpProvider; + + /** + * Map with ExecutableCommand children. The key is the type of the value. + */ + private Map, ExecutableCommand> commands = new HashMap<>(); + + @Inject + CommandHandler(Factory commandFactory, CommandMapper commandMapper, + PermissionsManager permissionsManager, Messages messages, HelpProvider helpProvider) { + this.commandMapper = commandMapper; + this.permissionsManager = permissionsManager; + this.messages = messages; + this.helpProvider = helpProvider; + initializeCommands(commandFactory, commandMapper.getCommandClasses()); + } + + /** + * Map a command that was invoked to the proper {@link CommandDescription} or return a useful error + * message upon failure. + * + * @param sender The command sender. + * @param bukkitCommandLabel The command label (Bukkit). + * @param bukkitArgs The command arguments (Bukkit). + * + * @return True if the command was executed, false otherwise. + */ + public boolean processCommand(CommandSender sender, String bukkitCommandLabel, String[] bukkitArgs) { + // Add the Bukkit command label to the front so we get a list like [authme, register, bobby, mysecret] + List parts = skipEmptyArguments(bukkitArgs); + parts.add(0, bukkitCommandLabel); + + FoundCommandResult result = commandMapper.mapPartsToCommand(sender, parts); + handleCommandResult(sender, result); + return !FoundResultStatus.MISSING_BASE_COMMAND.equals(result.getResultStatus()); + } + + /** + * Processes the given {@link FoundCommandResult} for the provided command sender. + * + * @param sender the command sender who executed the command + * @param result the command mapping result + */ + private void handleCommandResult(CommandSender sender, FoundCommandResult result) { + switch (result.getResultStatus()) { + case SUCCESS: + executeCommand(sender, result); + break; + case MISSING_BASE_COMMAND: + sender.sendMessage(ChatColor.DARK_RED + "Failed to parse " + AuthMe.getPluginName() + " command!"); + break; + case INCORRECT_ARGUMENTS: + sendImproperArgumentsMessage(sender, result); + break; + case UNKNOWN_LABEL: + sendUnknownCommandMessage(sender, result); + break; + case NO_PERMISSION: + messages.send(sender, MessageKey.NO_PERMISSION); + break; + default: + throw new IllegalStateException("Unknown result status '" + result.getResultStatus() + "'"); + } + } + + /** + * Initialize all required ExecutableCommand objects. + * + * @param commandFactory factory to create command objects + * @param commandClasses the classes to instantiate + */ + private void initializeCommands(Factory commandFactory, + Set> commandClasses) { + for (Class clazz : commandClasses) { + commands.put(clazz, commandFactory.newInstance(clazz)); + } + } + + /** + * Execute the command for the given command sender. + * + * @param sender The sender which initiated the command + * @param result The mapped result + */ + private void executeCommand(CommandSender sender, FoundCommandResult result) { + ExecutableCommand executableCommand = commands.get(result.getCommandDescription().getExecutableCommand()); + List arguments = result.getArguments(); + executableCommand.executeCommand(sender, arguments); + } + + /** + * Skip all entries of the given array that are simply whitespace. + * + * @param args The array to process + * @return List of the items that are not empty + */ + private static List skipEmptyArguments(String[] args) { + List cleanArguments = new ArrayList<>(); + for (String argument : args) { + if (!StringUtils.isBlank(argument)) { + cleanArguments.add(argument); + } + } + return cleanArguments; + } + + /** + * Show an "unknown command" message to the user and suggest an existing command if its similarity is within + * the defined threshold. + * + * @param sender The command sender + * @param result The command that was found during the mapping process + */ + private static void sendUnknownCommandMessage(CommandSender sender, FoundCommandResult result) { + sender.sendMessage(ChatColor.DARK_RED + "Unknown command!"); + + // Show a command suggestion if available and the difference isn't too big + if (result.getDifference() <= SUGGEST_COMMAND_THRESHOLD && result.getCommandDescription() != null) { + sender.sendMessage(ChatColor.YELLOW + "Did you mean " + ChatColor.GOLD + + CommandUtils.constructCommandPath(result.getCommandDescription()) + ChatColor.YELLOW + "?"); + } + + sender.sendMessage(ChatColor.YELLOW + "Use the command " + ChatColor.GOLD + "/" + result.getLabels().get(0) + + " help" + ChatColor.YELLOW + " to view help."); + } + + private void sendImproperArgumentsMessage(CommandSender sender, FoundCommandResult result) { + CommandDescription command = result.getCommandDescription(); + if (!permissionsManager.hasPermission(sender, command.getPermission())) { + messages.send(sender, MessageKey.NO_PERMISSION); + return; + } + + ExecutableCommand executableCommand = commands.get(command.getExecutableCommand()); + MessageKey usageMessage = executableCommand.getArgumentsMismatchMessage(); + if (usageMessage == null) { + showHelpForCommand(sender, result); + } else { + messages.send(sender, usageMessage); + } + } + + private void showHelpForCommand(CommandSender sender, FoundCommandResult result) { + sender.sendMessage(ChatColor.DARK_RED + "Incorrect command arguments!"); + helpProvider.outputHelp(sender, result, HelpProvider.SHOW_ARGUMENTS); + + List labels = result.getLabels(); + String childLabel = labels.size() >= 2 ? labels.get(1) : ""; + sender.sendMessage(ChatColor.GOLD + "Detailed help: " + ChatColor.WHITE + + "/" + labels.get(0) + " help " + childLabel); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/CommandInitializer.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/CommandInitializer.java new file mode 100644 index 00000000..fcc3ce61 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/CommandInitializer.java @@ -0,0 +1,652 @@ +package fr.xephi.authme.command; + +import com.google.common.collect.ImmutableList; +import fr.xephi.authme.command.executable.HelpCommand; +import fr.xephi.authme.command.executable.authme.AccountsCommand; +import fr.xephi.authme.command.executable.authme.AuthMeCommand; +import fr.xephi.authme.command.executable.authme.BackupCommand; +import fr.xephi.authme.command.executable.authme.ChangePasswordAdminCommand; +import fr.xephi.authme.command.executable.authme.ConverterCommand; +import fr.xephi.authme.command.executable.authme.FirstSpawnCommand; +import fr.xephi.authme.command.executable.authme.ForceLoginCommand; +import fr.xephi.authme.command.executable.authme.GetEmailCommand; +import fr.xephi.authme.command.executable.authme.GetIpCommand; +import fr.xephi.authme.command.executable.authme.LastLoginCommand; +import fr.xephi.authme.command.executable.authme.PurgeBannedPlayersCommand; +import fr.xephi.authme.command.executable.authme.PurgeCommand; +import fr.xephi.authme.command.executable.authme.PurgeLastPositionCommand; +import fr.xephi.authme.command.executable.authme.PurgePlayerCommand; +import fr.xephi.authme.command.executable.authme.RecentPlayersCommand; +import fr.xephi.authme.command.executable.authme.RegisterAdminCommand; +import fr.xephi.authme.command.executable.authme.ReloadCommand; +import fr.xephi.authme.command.executable.authme.SetEmailCommand; +import fr.xephi.authme.command.executable.authme.SetFirstSpawnCommand; +import fr.xephi.authme.command.executable.authme.SetSpawnCommand; +import fr.xephi.authme.command.executable.authme.SpawnCommand; +import fr.xephi.authme.command.executable.authme.SwitchAntiBotCommand; +import fr.xephi.authme.command.executable.authme.TotpDisableAdminCommand; +import fr.xephi.authme.command.executable.authme.TotpViewStatusCommand; +import fr.xephi.authme.command.executable.authme.UnregisterAdminCommand; +import fr.xephi.authme.command.executable.authme.UpdateHelpMessagesCommand; +import fr.xephi.authme.command.executable.authme.VersionCommand; +import fr.xephi.authme.command.executable.authme.debug.DebugCommand; +import fr.xephi.authme.command.executable.captcha.CaptchaCommand; +import fr.xephi.authme.command.executable.changepassword.ChangePasswordCommand; +import fr.xephi.authme.command.executable.email.AddEmailCommand; +import fr.xephi.authme.command.executable.email.ChangeEmailCommand; +import fr.xephi.authme.command.executable.email.EmailBaseCommand; +import fr.xephi.authme.command.executable.email.EmailSetPasswordCommand; +import fr.xephi.authme.command.executable.email.ProcessCodeCommand; +import fr.xephi.authme.command.executable.email.RecoverEmailCommand; +import fr.xephi.authme.command.executable.email.ShowEmailCommand; +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.totp.AddTotpCommand; +import fr.xephi.authme.command.executable.totp.ConfirmTotpCommand; +import fr.xephi.authme.command.executable.totp.RemoveTotpCommand; +import fr.xephi.authme.command.executable.totp.TotpBaseCommand; +import fr.xephi.authme.command.executable.totp.TotpCodeCommand; +import fr.xephi.authme.command.executable.unregister.UnregisterCommand; +import fr.xephi.authme.command.executable.verification.VerificationCommand; +import fr.xephi.authme.permission.AdminPermission; +import fr.xephi.authme.permission.DebugSectionPermissions; +import fr.xephi.authme.permission.PlayerPermission; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +/** + * Initializes all available AuthMe commands. + */ +public class CommandInitializer { + + private static final boolean OPTIONAL = true; + private static final boolean MANDATORY = false; + + private List commands; + + public CommandInitializer() { + buildCommands(); + } + + /** + * Returns the description of all AuthMe commands. + * + * @return the command descriptions + */ + public List getCommands() { + return commands; + } + + /** + * Builds the command description objects for all available AuthMe commands. + */ + private void buildCommands() { + // Register /authme and /email commands + CommandDescription authMeBase = buildAuthMeBaseCommand(); + CommandDescription emailBase = buildEmailBaseCommand(); + + // Register the base login command + CommandDescription loginBase = CommandDescription.builder() + .parent(null) + .labels("login", "l", "log") + .description("Login command") + .detailedDescription("Command to log in using AuthMeReloaded.") + .withArgument("password", "Login password", MANDATORY) + .permission(PlayerPermission.LOGIN) + .executableCommand(LoginCommand.class) + .register(); + + // Register the base logout command + CommandDescription logoutBase = CommandDescription.builder() + .parent(null) + .labels("logout") + .description("Logout command") + .detailedDescription("Command to logout using AuthMeReloaded.") + .permission(PlayerPermission.LOGOUT) + .executableCommand(LogoutCommand.class) + .register(); + + // Register the base register command + CommandDescription registerBase = CommandDescription.builder() + .parent(null) + .labels("register", "reg") + .description("Register an account") + .detailedDescription("Command to register using AuthMeReloaded.") + .withArgument("password", "Password", OPTIONAL) + .withArgument("verifyPassword", "Verify password", OPTIONAL) + .permission(PlayerPermission.REGISTER) + .executableCommand(RegisterCommand.class) + .register(); + + // Register the base unregister command + CommandDescription unregisterBase = CommandDescription.builder() + .parent(null) + .labels("unregister", "unreg") + .description("Unregister an account") + .detailedDescription("Command to unregister using AuthMeReloaded.") + .withArgument("password", "Password", MANDATORY) + .permission(PlayerPermission.UNREGISTER) + .executableCommand(UnregisterCommand.class) + .register(); + + // Register the base changepassword command + CommandDescription changePasswordBase = CommandDescription.builder() + .parent(null) + .labels("changepassword", "changepass", "cp") + .description("Change password of an account") + .detailedDescription("Command to change your password using AuthMeReloaded.") + .withArgument("oldPassword", "Old password", MANDATORY) + .withArgument("newPassword", "New password", MANDATORY) + .permission(PlayerPermission.CHANGE_PASSWORD) + .executableCommand(ChangePasswordCommand.class) + .register(); + + // Create totp base command + CommandDescription totpBase = buildTotpBaseCommand(); + + // Register the base captcha command + CommandDescription captchaBase = CommandDescription.builder() + .parent(null) + .labels("captcha") + .description("Captcha command") + .detailedDescription("Captcha command for AuthMeReloaded.") + .withArgument("captcha", "The Captcha", MANDATORY) + .permission(PlayerPermission.CAPTCHA) + .executableCommand(CaptchaCommand.class) + .register(); + + // Register the base verification code command + CommandDescription verificationBase = CommandDescription.builder() + .parent(null) + .labels("verification") + .description("Verification command") + .detailedDescription("Command to complete the verification process for AuthMeReloaded.") + .withArgument("code", "The code", MANDATORY) + .permission(PlayerPermission.VERIFICATION_CODE) + .executableCommand(VerificationCommand.class) + .register(); + + List baseCommands = ImmutableList.of(authMeBase, emailBase, loginBase, logoutBase, + registerBase, unregisterBase, changePasswordBase, totpBase, captchaBase, verificationBase); + + setHelpOnAllBases(baseCommands); + commands = baseCommands; + } + + /** + * Creates a command description object for {@code /authme} including its children. + * + * @return the authme base command description + */ + private CommandDescription buildAuthMeBaseCommand() { + // Register the base AuthMe Reloaded command + CommandDescription authmeBase = CommandDescription.builder() + .labels("authme") + .description("AuthMe op commands") + .detailedDescription("The main AuthMeReloaded command. The root for all admin commands.") + .executableCommand(AuthMeCommand.class) + .register(); + + // Register the register command + CommandDescription.builder() + .parent(authmeBase) + .labels("register", "reg", "r") + .description("Register a player") + .detailedDescription("Register the specified player with the specified password.") + .withArgument("player", "Player name", MANDATORY) + .withArgument("password", "Password", MANDATORY) + .permission(AdminPermission.REGISTER) + .executableCommand(RegisterAdminCommand.class) + .register(); + + // Register the unregister command + CommandDescription.builder() + .parent(authmeBase) + .labels("unregister", "unreg", "unr") + .description("Unregister a player") + .detailedDescription("Unregister the specified player.") + .withArgument("player", "Player name", MANDATORY) + .permission(AdminPermission.UNREGISTER) + .executableCommand(UnregisterAdminCommand.class) + .register(); + + // Register the forcelogin command + CommandDescription.builder() + .parent(authmeBase) + .labels("forcelogin", "login") + .description("Enforce login player") + .detailedDescription("Enforce the specified player to login.") + .withArgument("player", "Online player name", OPTIONAL) + .permission(AdminPermission.FORCE_LOGIN) + .executableCommand(ForceLoginCommand.class) + .register(); + + // Register the changepassword command + CommandDescription.builder() + .parent(authmeBase) + .labels("password", "changepassword", "changepass", "cp") + .description("Change a player's password") + .detailedDescription("Change the password of a player.") + .withArgument("player", "Player name", MANDATORY) + .withArgument("pwd", "New password", MANDATORY) + .permission(AdminPermission.CHANGE_PASSWORD) + .executableCommand(ChangePasswordAdminCommand.class) + .register(); + + // Register the last login command + CommandDescription.builder() + .parent(authmeBase) + .labels("lastlogin", "ll") + .description("Player's last login") + .detailedDescription("View the date of the specified players last login.") + .withArgument("player", "Player name", OPTIONAL) + .permission(AdminPermission.LAST_LOGIN) + .executableCommand(LastLoginCommand.class) + .register(); + + // Register the accounts command + CommandDescription.builder() + .parent(authmeBase) + .labels("accounts", "account") + .description("Display player accounts") + .detailedDescription("Display all accounts of a player by his player name or IP.") + .withArgument("player", "Player name or IP", OPTIONAL) + .permission(AdminPermission.ACCOUNTS) + .executableCommand(AccountsCommand.class) + .register(); + + // Register the getemail command + CommandDescription.builder() + .parent(authmeBase) + .labels("email", "mail", "getemail", "getmail") + .description("Display player's email") + .detailedDescription("Display the email address of the specified player if set.") + .withArgument("player", "Player name", OPTIONAL) + .permission(AdminPermission.GET_EMAIL) + .executableCommand(GetEmailCommand.class) + .register(); + + // Register the setemail command + CommandDescription.builder() + .parent(authmeBase) + .labels("setemail", "setmail", "chgemail", "chgmail") + .description("Change player's email") + .detailedDescription("Change the email address of the specified player.") + .withArgument("player", "Player name", MANDATORY) + .withArgument("email", "Player email", MANDATORY) + .permission(AdminPermission.CHANGE_EMAIL) + .executableCommand(SetEmailCommand.class) + .register(); + + // Register the getip command + CommandDescription.builder() + .parent(authmeBase) + .labels("getip", "ip") + .description("Get player's IP") + .detailedDescription("Get the IP address of the specified online player.") + .withArgument("player", "Player name", MANDATORY) + .permission(AdminPermission.GET_IP) + .executableCommand(GetIpCommand.class) + .register(); + + // Register totp command + CommandDescription.builder() + .parent(authmeBase) + .labels("totp", "2fa") + .description("See if a player has enabled TOTP") + .detailedDescription("Returns whether the specified player has enabled two-factor authentication.") + .withArgument("player", "Player name", MANDATORY) + .permission(AdminPermission.VIEW_TOTP_STATUS) + .executableCommand(TotpViewStatusCommand.class) + .register(); + + // Register disable totp command + CommandDescription.builder() + .parent(authmeBase) + .labels("disabletotp", "disable2fa", "deletetotp", "delete2fa") + .description("Delete TOTP token of a player") + .detailedDescription("Disable two-factor authentication for a player.") + .withArgument("player", "Player name", MANDATORY) + .permission(AdminPermission.DISABLE_TOTP) + .executableCommand(TotpDisableAdminCommand.class) + .register(); + + // Register the spawn command + CommandDescription.builder() + .parent(authmeBase) + .labels("spawn", "home") + .description("Teleport to spawn") + .detailedDescription("Teleport to the spawn.") + .permission(AdminPermission.SPAWN) + .executableCommand(SpawnCommand.class) + .register(); + + // Register the setspawn command + CommandDescription.builder() + .parent(authmeBase) + .labels("setspawn", "chgspawn") + .description("Change the spawn") + .detailedDescription("Change the player's spawn to your current position.") + .permission(AdminPermission.SET_SPAWN) + .executableCommand(SetSpawnCommand.class) + .register(); + + // Register the firstspawn command + CommandDescription.builder() + .parent(authmeBase) + .labels("firstspawn", "firsthome") + .description("Teleport to first spawn") + .detailedDescription("Teleport to the first spawn.") + .permission(AdminPermission.FIRST_SPAWN) + .executableCommand(FirstSpawnCommand.class) + .register(); + + // Register the setfirstspawn command + CommandDescription.builder() + .parent(authmeBase) + .labels("setfirstspawn", "chgfirstspawn") + .description("Change the first spawn") + .detailedDescription("Change the first player's spawn to your current position.") + .permission(AdminPermission.SET_FIRST_SPAWN) + .executableCommand(SetFirstSpawnCommand.class) + .register(); + + // Register the purge command + CommandDescription.builder() + .parent(authmeBase) + .labels("purge", "delete") + .description("Purge old data") + .detailedDescription("Purge old AuthMeReloaded data longer than the specified number of days ago.") + .withArgument("days", "Number of days", MANDATORY) + .permission(AdminPermission.PURGE) + .executableCommand(PurgeCommand.class) + .register(); + + // Purge player command + CommandDescription.builder() + .parent(authmeBase) + .labels("purgeplayer") + .description("Purges the data of one player") + .detailedDescription("Purges data of the given player.") + .withArgument("player", "The player to purge", MANDATORY) + .withArgument("options", "'force' to run without checking if player is registered", OPTIONAL) + .permission(AdminPermission.PURGE_PLAYER) + .executableCommand(PurgePlayerCommand.class) + .register(); + + // Backup command + CommandDescription.builder() + .parent(authmeBase) + .labels("backup") + .description("Perform a backup") + .detailedDescription("Creates a backup of the registered users.") + .permission(AdminPermission.BACKUP) + .executableCommand(BackupCommand.class) + .register(); + + // Register the purgelastposition command + CommandDescription.builder() + .parent(authmeBase) + .labels("resetpos", "purgelastposition", "purgelastpos", "resetposition", + "resetlastposition", "resetlastpos") + .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", MANDATORY) + .permission(AdminPermission.PURGE_LAST_POSITION) + .executableCommand(PurgeLastPositionCommand.class) + .register(); + + // Register the purgebannedplayers command + CommandDescription.builder() + .parent(authmeBase) + .labels("purgebannedplayers", "purgebannedplayer", "deletebannedplayers", "deletebannedplayer") + .description("Purge banned players data") + .detailedDescription("Purge all AuthMeReloaded data for banned players.") + .permission(AdminPermission.PURGE_BANNED_PLAYERS) + .executableCommand(PurgeBannedPlayersCommand.class) + .register(); + + // Register the switchantibot command + CommandDescription.builder() + .parent(authmeBase) + .labels("switchantibot", "toggleantibot", "antibot") + .description("Switch AntiBot mode") + .detailedDescription("Switch or toggle the AntiBot mode to the specified state.") + .withArgument("mode", "ON / OFF", OPTIONAL) + .permission(AdminPermission.SWITCH_ANTIBOT) + .executableCommand(SwitchAntiBotCommand.class) + .register(); + + // Register the reload command + CommandDescription.builder() + .parent(authmeBase) + .labels("reload", "rld") + .description("Reload plugin") + .detailedDescription("Reload the AuthMeReloaded plugin.") + .permission(AdminPermission.RELOAD) + .executableCommand(ReloadCommand.class) + .register(); + + // Register the version command + CommandDescription.builder() + .parent(authmeBase) + .labels("version", "ver", "v", "about", "info") + .description("Version info") + .detailedDescription("Show detailed information about the installed AuthMeReloaded version, the " + + "developers, contributors, and license.") + .executableCommand(VersionCommand.class) + .register(); + + CommandDescription.builder() + .parent(authmeBase) + .labels("converter", "convert", "conv") + .description("Converter command") + .detailedDescription("Converter command for AuthMeReloaded.") + .withArgument("job", "Conversion job: xauth / crazylogin / rakamak / " + + "royalauth / vauth / sqliteToSql / mysqlToSqlite / loginsecurity", OPTIONAL) + .permission(AdminPermission.CONVERTER) + .executableCommand(ConverterCommand.class) + .register(); + + CommandDescription.builder() + .parent(authmeBase) + .labels("messages", "msg") + .description("Add missing help messages") + .detailedDescription("Adds missing texts to the current help messages file.") + .permission(AdminPermission.UPDATE_MESSAGES) + .executableCommand(UpdateHelpMessagesCommand.class) + .register(); + + CommandDescription.builder() + .parent(authmeBase) + .labels("recent") + .description("See players who have recently logged in") + .detailedDescription("Shows the last players that have logged in.") + .permission(AdminPermission.SEE_RECENT_PLAYERS) + .executableCommand(RecentPlayersCommand.class) + .register(); + + CommandDescription.builder() + .parent(authmeBase) + .labels("debug", "dbg") + .description("Debug features") + .detailedDescription("Allows various operations for debugging.") + .withArgument("child", "The child to execute", OPTIONAL) + .withArgument("arg", "argument (depends on debug section)", OPTIONAL) + .withArgument("arg", "argument (depends on debug section)", OPTIONAL) + .permission(DebugSectionPermissions.DEBUG_COMMAND) + .executableCommand(DebugCommand.class) + .register(); + + return authmeBase; + } + + /** + * Creates a command description for {@code /email} including its children. + * + * @return the email base command description + */ + private CommandDescription buildEmailBaseCommand() { + // Register the base Email command + CommandDescription emailBase = CommandDescription.builder() + .parent(null) + .labels("email") + .description("Add email or recover password") + .detailedDescription("The AuthMeReloaded email command base.") + .executableCommand(EmailBaseCommand.class) + .register(); + + // Register the show command + CommandDescription.builder() + .parent(emailBase) + .labels("show", "myemail") + .description("Show Email") + .detailedDescription("Show your current email address.") + .permission(PlayerPermission.SEE_EMAIL) + .executableCommand(ShowEmailCommand.class) + .register(); + + // Register the add command + CommandDescription.builder() + .parent(emailBase) + .labels("add", "addemail", "addmail") + .description("Add Email") + .detailedDescription("Add a new email address to your account.") + .withArgument("email", "Email address", MANDATORY) + .withArgument("verifyEmail", "Email address verification", MANDATORY) + .permission(PlayerPermission.ADD_EMAIL) + .executableCommand(AddEmailCommand.class) + .register(); + + // Register the change command + CommandDescription.builder() + .parent(emailBase) + .labels("change", "changeemail", "changemail") + .description("Change Email") + .detailedDescription("Change an email address of your account.") + .withArgument("oldEmail", "Old email address", MANDATORY) + .withArgument("newEmail", "New email address", MANDATORY) + .permission(PlayerPermission.CHANGE_EMAIL) + .executableCommand(ChangeEmailCommand.class) + .register(); + + // Register the recover command + CommandDescription.builder() + .parent(emailBase) + .labels("recover", "recovery", "recoveremail", "recovermail") + .description("Recover password using email") + .detailedDescription("Recover your account using an Email address by sending a mail containing " + + "a new password.") + .withArgument("email", "Email address", MANDATORY) + .permission(PlayerPermission.RECOVER_EMAIL) + .executableCommand(RecoverEmailCommand.class) + .register(); + + // Register the process recovery code command + CommandDescription.builder() + .parent(emailBase) + .labels("code") + .description("Submit code to recover password") + .detailedDescription("Recover your account by submitting a code delivered to your email.") + .withArgument("code", "Recovery code", MANDATORY) + .permission(PlayerPermission.RECOVER_EMAIL) + .executableCommand(ProcessCodeCommand.class) + .register(); + + // Register the change password after recovery command + CommandDescription.builder() + .parent(emailBase) + .labels("setpassword") + .description("Set new password after recovery") + .detailedDescription("Set a new password after successfully recovering your account.") + .withArgument("password", "New password", MANDATORY) + .permission(PlayerPermission.RECOVER_EMAIL) + .executableCommand(EmailSetPasswordCommand.class) + .register(); + + return emailBase; + } + + /** + * Creates a command description object for {@code /totp} including its children. + * + * @return the totp base command description + */ + private CommandDescription buildTotpBaseCommand() { + // Register the base totp command + CommandDescription totpBase = CommandDescription.builder() + .parent(null) + .labels("totp", "2fa") + .description("TOTP commands") + .detailedDescription("Performs actions related to two-factor authentication.") + .executableCommand(TotpBaseCommand.class) + .register(); + + // Register the base totp code + CommandDescription.builder() + .parent(totpBase) + .labels("code", "c") + .description("Command for logging in") + .detailedDescription("Processes the two-factor authentication code during login.") + .withArgument("code", "The TOTP code to use to log in", MANDATORY) + .executableCommand(TotpCodeCommand.class) + .register(); + + // Register totp add + CommandDescription.builder() + .parent(totpBase) + .labels("add") + .description("Enables TOTP") + .detailedDescription("Enables two-factor authentication for your account.") + .permission(PlayerPermission.ENABLE_TWO_FACTOR_AUTH) + .executableCommand(AddTotpCommand.class) + .register(); + + // Register totp confirm + CommandDescription.builder() + .parent(totpBase) + .labels("confirm") + .description("Enables TOTP after successful code") + .detailedDescription("Saves the generated TOTP secret after confirmation.") + .withArgument("code", "Code from the given secret from /totp add", MANDATORY) + .permission(PlayerPermission.ENABLE_TWO_FACTOR_AUTH) + .executableCommand(ConfirmTotpCommand.class) + .register(); + + // Register totp remove + CommandDescription.builder() + .parent(totpBase) + .labels("remove") + .description("Removes TOTP") + .detailedDescription("Disables two-factor authentication for your account.") + .withArgument("code", "Current 2FA code", MANDATORY) + .permission(PlayerPermission.DISABLE_TWO_FACTOR_AUTH) + .executableCommand(RemoveTotpCommand.class) + .register(); + + return totpBase; + } + + /** + * 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 + */ + private void setHelpOnAllBases(Collection commands) { + final List helpCommandLabels = Arrays.asList("help", "hlp", "h", "sos", "?"); + + for (CommandDescription base : commands) { + CommandDescription.builder() + .parent(base) + .labels(helpCommandLabels) + .description("View help") + .detailedDescription("View detailed help for /" + base.getLabels().get(0) + " commands.") + .withArgument("query", "The command or query to view help for.", OPTIONAL) + .executableCommand(HelpCommand.class) + .register(); + } + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/CommandMapper.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/CommandMapper.java new file mode 100644 index 00000000..5beb5e33 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/CommandMapper.java @@ -0,0 +1,207 @@ +package fr.xephi.authme.command; + +import fr.xephi.authme.command.executable.HelpCommand; +import fr.xephi.authme.permission.PermissionsManager; +import fr.xephi.authme.util.StringUtils; +import fr.xephi.authme.util.Utils; +import org.bukkit.command.CommandSender; + +import javax.inject.Inject; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +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.UNKNOWN_LABEL; + +/** + * Maps incoming command parts to the correct {@link CommandDescription}. + */ +public class CommandMapper { + + /** + * The class of the help command, to which the base label should also be passed in the arguments. + */ + private static final Class HELP_COMMAND_CLASS = HelpCommand.class; + + private final Collection baseCommands; + private final PermissionsManager permissionsManager; + + @Inject + public CommandMapper(CommandInitializer commandInitializer, PermissionsManager permissionsManager) { + this.baseCommands = commandInitializer.getCommands(); + this.permissionsManager = permissionsManager; + } + + + /** + * Map incoming command parts to a command. This processes all parts and distinguishes the labels from arguments. + * + * @param sender The command sender (null if none applicable) + * @param parts The parts to map to commands and arguments + * @return The generated {@link FoundCommandResult} + */ + public FoundCommandResult mapPartsToCommand(CommandSender sender, List parts) { + if (Utils.isCollectionEmpty(parts)) { + return new FoundCommandResult(null, parts, null, 0.0, MISSING_BASE_COMMAND); + } + + CommandDescription base = getBaseCommand(parts.get(0)); + if (base == null) { + return new FoundCommandResult(null, parts, null, 0.0, MISSING_BASE_COMMAND); + } + + // Prefer labels: /register help goes to "Help command", not "Register command" with argument 'help' + List remainingParts = parts.subList(1, parts.size()); + CommandDescription childCommand = getSuitableChild(base, remainingParts); + if (childCommand != null) { + FoundResultStatus status = getPermissionAwareStatus(sender, childCommand); + FoundCommandResult result = new FoundCommandResult( + childCommand, parts.subList(0, 2), parts.subList(2, parts.size()), 0.0, status); + return transformResultForHelp(result); + } else if (hasSuitableArgumentCount(base, remainingParts.size())) { + FoundResultStatus status = getPermissionAwareStatus(sender, base); + return new FoundCommandResult(base, parts.subList(0, 1), parts.subList(1, parts.size()), 0.0, status); + } + + 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; + } + + /** + * Return the command whose label matches the given parts the best. This method is called when + * a successful mapping could not be performed. + * + * @param base the base command + * @param parts the command parts + * @return the closest result + */ + private static 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) { + return new FoundCommandResult(base, parts, new ArrayList<>(), 0.0, INCORRECT_ARGUMENTS); + } + + final String childLabel = parts.get(1); + double minDifference = Double.POSITIVE_INFINITY; + CommandDescription closestCommand = null; + + for (CommandDescription child : base.getChildren()) { + double difference = getLabelDifference(child, childLabel); + if (difference < minDifference) { + minDifference = difference; + closestCommand = child; + } + } + + // base command may have no children, in which case we return the base command with incorrect arguments error + if (closestCommand == null) { + return new FoundCommandResult( + base, parts.subList(0, 1), parts.subList(1, parts.size()), 0.0, INCORRECT_ARGUMENTS); + } + + FoundResultStatus status = (minDifference == 0.0) ? INCORRECT_ARGUMENTS : UNKNOWN_LABEL; + final int partsSize = parts.size(); + List labels = parts.subList(0, Math.min(closestCommand.getLabelCount(), partsSize)); + List arguments = (labels.size() == partsSize) + ? new ArrayList<>() + : parts.subList(labels.size(), partsSize); + + return new FoundCommandResult(closestCommand, labels, arguments, minDifference, status); + } + + private CommandDescription getBaseCommand(String label) { + String baseLabel = label.toLowerCase(Locale.ROOT); + if (baseLabel.startsWith("authme:")) { + baseLabel = baseLabel.substring("authme:".length()); + } + for (CommandDescription command : baseCommands) { + if (command.hasLabel(baseLabel)) { + return command; + } + } + return null; + } + + /** + * Return a child from a base command if the label and the argument count match. + * + * @param baseCommand The base command whose children should be checked + * @param parts The command parts received from the invocation; the first item is the potential label and any + * other items are command arguments. The first initial part that led to the base command should not + * be present. + * + * @return A command if there was a complete match (including proper argument count), null otherwise + */ + private static CommandDescription getSuitableChild(CommandDescription baseCommand, List parts) { + if (Utils.isCollectionEmpty(parts)) { + return null; + } + + final String label = parts.get(0).toLowerCase(Locale.ROOT); + final int argumentCount = parts.size() - 1; + + for (CommandDescription child : baseCommand.getChildren()) { + if (child.hasLabel(label) && hasSuitableArgumentCount(child, argumentCount)) { + return child; + } + } + return null; + } + + private static FoundCommandResult transformResultForHelp(FoundCommandResult result) { + if (result.getCommandDescription() != null + && 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 + List arguments = new ArrayList<>(result.getArguments()); + arguments.add(0, result.getLabels().get(0)); + return new FoundCommandResult(result.getCommandDescription(), result.getLabels(), + arguments, result.getDifference(), result.getResultStatus()); + } + return result; + } + + private FoundResultStatus getPermissionAwareStatus(CommandSender sender, CommandDescription command) { + if (sender != null && !permissionsManager.hasPermission(sender, command.getPermission())) { + return FoundResultStatus.NO_PERMISSION; + } + return FoundResultStatus.SUCCESS; + } + + private static boolean hasSuitableArgumentCount(CommandDescription command, int argumentCount) { + int minArgs = CommandUtils.getMinNumberOfArguments(command); + int maxArgs = CommandUtils.getMaxNumberOfArguments(command); + + return argumentCount >= minArgs && argumentCount <= maxArgs; + } + + private static double getLabelDifference(CommandDescription command, String givenLabel) { + return command.getLabels().stream() + .map(label -> StringUtils.getDifference(label, givenLabel)) + .min(Double::compareTo) + .orElseThrow(() -> new IllegalStateException("Command does not have any labels set")); + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/CommandUtils.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/CommandUtils.java new file mode 100644 index 00000000..595f23ab --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/CommandUtils.java @@ -0,0 +1,109 @@ +package fr.xephi.authme.command; + +import com.google.common.collect.Lists; +import org.bukkit.ChatColor; + +import java.util.ArrayList; +import java.util.List; + +/** + * Utility functions for {@link CommandDescription} objects. + */ +public final class CommandUtils { + + private CommandUtils() { + } + + /** + * Returns the minimum number of arguments required for running the command (= number of mandatory arguments). + * + * @param command the command to process + * @return min number of arguments required by the command + */ + public static int getMinNumberOfArguments(CommandDescription command) { + int mandatoryArguments = 0; + for (CommandArgumentDescription argument : command.getArguments()) { + if (!argument.isOptional()) { + ++mandatoryArguments; + } + } + return mandatoryArguments; + } + + /** + * Returns the maximum number of arguments the command accepts. + * + * @param command the command to process + * @return max number of arguments that may be passed to the command + */ + public static int getMaxNumberOfArguments(CommandDescription command) { + return command.getArguments().size(); + } + + /** + * Constructs a hierarchical list of commands for the given command. The commands are in order: + * the parents of the given command precede the provided command. For example, given the command + * for {@code /authme register}, a list with {@code [{authme}, {authme register}]} is returned. + * + * @param command the command to build a parent list for + * @return the parent list + */ + public static List constructParentList(CommandDescription command) { + List commands = new ArrayList<>(); + CommandDescription currentCommand = command; + while (currentCommand != null) { + commands.add(currentCommand); + currentCommand = currentCommand.getParent(); + } + return Lists.reverse(commands); + } + + /** + * Returns a textual representation of the command, e.g. {@code /authme register}. + * + * @param command the command to create the path for + * @return the command string + */ + public static String constructCommandPath(CommandDescription command) { + StringBuilder sb = new StringBuilder(); + String prefix = "/"; + for (CommandDescription ancestor : constructParentList(command)) { + sb.append(prefix).append(ancestor.getLabels().get(0)); + prefix = " "; + } + return sb.toString(); + } + + /** + * Constructs a command path with color formatting, based on the supplied labels. This includes + * the command's arguments, as defined in the provided command description. The list of labels + * must contain all labels to be used. + * + * @param command the command to read arguments from + * @param correctLabels the labels to use (must be complete) + * @return formatted command syntax incl. arguments + */ + public static String buildSyntax(CommandDescription command, List correctLabels) { + StringBuilder commandSyntax = new StringBuilder(ChatColor.WHITE + "/" + correctLabels.get(0) + ChatColor.YELLOW); + for (int i = 1; i < correctLabels.size(); ++i) { + commandSyntax.append(" ").append(correctLabels.get(i)); + } + for (CommandArgumentDescription argument : command.getArguments()) { + commandSyntax.append(" ").append(formatArgument(argument)); + } + return commandSyntax.toString(); + } + + /** + * Formats a command argument with the proper type of brackets. + * + * @param argument the argument to format + * @return the formatted argument + */ + public static String formatArgument(CommandArgumentDescription argument) { + if (argument.isOptional()) { + return "[" + argument.getName() + "]"; + } + return "<" + argument.getName() + ">"; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/ExecutableCommand.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/ExecutableCommand.java new file mode 100644 index 00000000..54ee9e84 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/ExecutableCommand.java @@ -0,0 +1,31 @@ +package fr.xephi.authme.command; + +import fr.xephi.authme.message.MessageKey; +import org.bukkit.command.CommandSender; + +import java.util.List; + +/** + * Base class for AuthMe commands that can be executed. + */ +public interface ExecutableCommand { + + /** + * Executes the command with the given arguments. + * + * @param sender the command sender (initiator of the command) + * @param arguments the arguments + */ + void executeCommand(CommandSender sender, List arguments); + + /** + * Returns the message to show to the user if the command is used with the wrong arguments. + * If null is returned, the standard help (/command help) output is shown. + * + * @return the message explaining the command's usage, or {@code null} for default behavior + */ + default MessageKey getArgumentsMismatchMessage() { + return null; + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/FoundCommandResult.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/FoundCommandResult.java new file mode 100644 index 00000000..520eec24 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/FoundCommandResult.java @@ -0,0 +1,79 @@ +package fr.xephi.authme.command; + +import java.util.List; + +/** + * Result of a command mapping by {@link CommandHandler}. An object of this class represents a successful mapping + * as well as erroneous ones, as communicated with {@link FoundResultStatus}. + *

+ * Fields other than {@link FoundResultStatus} are available depending, among other factors, on the status: + *

+ */ +public class FoundCommandResult { + + /** + * The command description instance. + */ + private final CommandDescription commandDescription; + /** + * The labels used to invoke the command. This may be different for the same {@link ExecutableCommand} instance + * if multiple labels have been defined, e.g. "/authme register" and "/authme reg". + */ + private final List labels; + /** The command arguments. */ + private final List arguments; + /** The difference between the matched command and the supplied labels. */ + private final double difference; + /** The status of the result (see class description). */ + private final FoundResultStatus resultStatus; + + /** + * Constructor. + * + * @param commandDescription The command description. + * @param labels The labels used to access the command. + * @param arguments The command arguments. + * @param difference The difference between the supplied labels and the matched command. + * @param resultStatus The status of the result. + */ + public FoundCommandResult(CommandDescription commandDescription, List labels, List arguments, + double difference, FoundResultStatus resultStatus) { + this.commandDescription = commandDescription; + this.labels = labels; + this.arguments = arguments; + this.difference = difference; + this.resultStatus = resultStatus; + } + + public CommandDescription getCommandDescription() { + return this.commandDescription; + } + + public List getArguments() { + return this.arguments; + } + + public List getLabels() { + return this.labels; + } + + public double getDifference() { + return difference; + } + + public FoundResultStatus getResultStatus() { + return resultStatus; + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/FoundResultStatus.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/FoundResultStatus.java new file mode 100644 index 00000000..32ca6b7b --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/FoundResultStatus.java @@ -0,0 +1,18 @@ +package fr.xephi.authme.command; + +/** + * Result status for mapping command parts. See {@link FoundCommandResult} for a detailed description of the states. + */ +public enum FoundResultStatus { + + SUCCESS, + + INCORRECT_ARGUMENTS, + + UNKNOWN_LABEL, + + NO_PERMISSION, + + MISSING_BASE_COMMAND + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/PlayerCommand.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/PlayerCommand.java new file mode 100644 index 00000000..8ce6e05f --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/PlayerCommand.java @@ -0,0 +1,45 @@ +package fr.xephi.authme.command; + +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) { + if (sender instanceof Player) { + runCommand((Player) sender, arguments); + } else { + String alternative = getAlternativeCommand(); + if (alternative != null) { + sender.sendMessage("Player only! Please use " + alternative + " instead."); + } else { + sender.sendMessage("This command is only for players."); + } + } + } + + /** + * 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 + */ + protected abstract void runCommand(Player player, List arguments); + + /** + * Returns an alternative command (textual representation) that is not restricted to players only. + * Example: {@code "/authme register "} + * + * @return Alternative command not restricted to players, or null if not applicable + */ + protected String getAlternativeCommand() { + return null; + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/TabCompleteHandler.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/TabCompleteHandler.java new file mode 100644 index 00000000..36390084 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/TabCompleteHandler.java @@ -0,0 +1,17 @@ +package fr.xephi.authme.command; + +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; + +public class TabCompleteHandler implements TabCompleter { + + @Override + public List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, String[] args) { + return new ArrayList<>(); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/HelpCommand.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/HelpCommand.java new file mode 100644 index 00000000..118994f0 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/HelpCommand.java @@ -0,0 +1,64 @@ +package fr.xephi.authme.command.executable; + +import fr.xephi.authme.command.CommandMapper; +import fr.xephi.authme.command.CommandUtils; +import fr.xephi.authme.command.ExecutableCommand; +import fr.xephi.authme.command.FoundCommandResult; +import fr.xephi.authme.command.FoundResultStatus; +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; +import static fr.xephi.authme.command.FoundResultStatus.UNKNOWN_LABEL; +import static fr.xephi.authme.command.help.HelpProvider.ALL_OPTIONS; +import static fr.xephi.authme.command.help.HelpProvider.SHOW_ALTERNATIVES; +import static fr.xephi.authme.command.help.HelpProvider.SHOW_CHILDREN; +import static fr.xephi.authme.command.help.HelpProvider.SHOW_COMMAND; +import static fr.xephi.authme.command.help.HelpProvider.SHOW_DESCRIPTION; + +/** + * Displays help information to a user. + */ +public class HelpCommand implements ExecutableCommand { + + @Inject + 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 = commandMapper.mapPartsToCommand(sender, arguments); + + FoundResultStatus resultStatus = result.getResultStatus(); + if (MISSING_BASE_COMMAND.equals(resultStatus)) { + sender.sendMessage(ChatColor.DARK_RED + "Could not get base command"); + return; + } else if (UNKNOWN_LABEL.equals(resultStatus)) { + if (result.getCommandDescription() == null) { + sender.sendMessage(ChatColor.DARK_RED + "Unknown command"); + return; + } else { + sender.sendMessage(ChatColor.GOLD + "Assuming " + ChatColor.WHITE + + CommandUtils.constructCommandPath(result.getCommandDescription())); + } + } + + int mappedCommandLevel = result.getCommandDescription().getLabelCount(); + if (mappedCommandLevel == 1) { + helpProvider.outputHelp(sender, result, + SHOW_COMMAND | SHOW_DESCRIPTION | SHOW_CHILDREN | SHOW_ALTERNATIVES); + } else { + helpProvider.outputHelp(sender, result, ALL_OPTIONS); + } + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/AccountsCommand.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/AccountsCommand.java new file mode 100644 index 00000000..20f6bff8 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/AccountsCommand.java @@ -0,0 +1,74 @@ +package fr.xephi.authme.command.executable.authme; + +import fr.xephi.authme.command.ExecutableCommand; +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.service.BukkitService; +import fr.xephi.authme.service.CommonService; +import org.bukkit.command.CommandSender; + +import javax.inject.Inject; +import java.util.List; +import java.util.Locale; + +/** + * Shows all accounts registered by the same IP address for the given player name or IP address. + */ +public class AccountsCommand implements ExecutableCommand { + + @Inject + private DataSource dataSource; + + @Inject + private BukkitService bukkitService; + + @Inject + private CommonService commonService; + + @Override + public void executeCommand(final CommandSender sender, List arguments) { + // TODO #1366: last IP vs. registration IP? + final String playerName = arguments.isEmpty() ? sender.getName() : arguments.get(0); + + // Assumption: a player name cannot contain '.' + if (playerName.contains(".")) { + bukkitService.runTaskAsynchronously(() -> { + 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(() -> { + PlayerAuth auth = dataSource.getAuth(playerName.toLowerCase(Locale.ROOT)); + if (auth == null) { + commonService.send(sender, MessageKey.UNKNOWN_USER); + return; + } else if (auth.getLastIp() == null) { + sender.sendMessage("No known last IP address for player"); + return; + } + + List accountList = dataSource.getAllAuthsByIp(auth.getLastIp()); + if (accountList.isEmpty()) { + commonService.send(sender, MessageKey.UNKNOWN_USER); + } else if (accountList.size() == 1) { + sender.sendMessage("[AuthMe] " + playerName + " is a single account player"); + } else { + outputAccountsList(sender, playerName, accountList); + } + }); + } + } + + private static void outputAccountsList(CommandSender sender, String playerName, List accountList) { + sender.sendMessage("[AuthMe] " + playerName + " has " + accountList.size() + " accounts."); + String message = "[AuthMe] " + String.join(", ", accountList) + "."; + sender.sendMessage(message); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/AuthMeCommand.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/AuthMeCommand.java new file mode 100644 index 00000000..2648177e --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/AuthMeCommand.java @@ -0,0 +1,24 @@ +package fr.xephi.authme.command.executable.authme; + +import fr.xephi.authme.AuthMe; +import fr.xephi.authme.command.ExecutableCommand; +import org.bukkit.ChatColor; +import org.bukkit.command.CommandSender; + +import java.util.List; + +/** + * AuthMe base command; shows the version and some command pointers. + */ +public class AuthMeCommand implements ExecutableCommand { + + @Override + 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 + + " to view help."); + sender.sendMessage(ChatColor.YELLOW + "Use the command " + ChatColor.GOLD + "/authme about" + ChatColor.YELLOW + + " to view about."); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/BackupCommand.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/BackupCommand.java new file mode 100644 index 00000000..0e72fb60 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/BackupCommand.java @@ -0,0 +1,23 @@ +package fr.xephi.authme.command.executable.authme; + +import fr.xephi.authme.command.ExecutableCommand; +import fr.xephi.authme.service.BackupService; +import fr.xephi.authme.service.BackupService.BackupCause; +import org.bukkit.command.CommandSender; + +import javax.inject.Inject; +import java.util.List; + +/** + * Command to perform a backup. + */ +public class BackupCommand implements ExecutableCommand { + + @Inject + private BackupService backupService; + + @Override + public void executeCommand(CommandSender sender, List arguments) { + backupService.doBackup(BackupCause.COMMAND, sender); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/ChangePasswordAdminCommand.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/ChangePasswordAdminCommand.java new file mode 100644 index 00000000..3c6264cc --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/ChangePasswordAdminCommand.java @@ -0,0 +1,41 @@ +package fr.xephi.authme.command.executable.authme; + +import fr.xephi.authme.command.ExecutableCommand; +import fr.xephi.authme.process.Management; +import fr.xephi.authme.service.CommonService; +import fr.xephi.authme.service.ValidationService; +import fr.xephi.authme.service.ValidationService.ValidationResult; +import org.bukkit.command.CommandSender; + +import javax.inject.Inject; +import java.util.List; + +/** + * Admin command for changing a player's password. + */ +public class ChangePasswordAdminCommand implements ExecutableCommand { + + @Inject + private ValidationService validationService; + + @Inject + private CommonService commonService; + + @Inject + private Management management; + + @Override + public void executeCommand(CommandSender sender, List arguments) { + // Get the player and password + final String playerName = arguments.get(0); + final String playerPass = arguments.get(1); + + // Validate the password + ValidationResult validationResult = validationService.validatePassword(playerPass, playerName); + if (validationResult.hasError()) { + commonService.send(sender, validationResult.getMessageKey(), validationResult.getArgs()); + } else { + management.performPasswordChangeAsAdmin(sender, playerName, playerPass); + } + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/ConverterCommand.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/ConverterCommand.java new file mode 100644 index 00000000..15875ac9 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/ConverterCommand.java @@ -0,0 +1,95 @@ +package fr.xephi.authme.command.executable.authme; + +import ch.jalu.injector.factory.Factory; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableSortedMap; +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.command.ExecutableCommand; +import fr.xephi.authme.datasource.converter.Converter; +import fr.xephi.authme.datasource.converter.CrazyLoginConverter; +import fr.xephi.authme.datasource.converter.H2ToSqlite; +import fr.xephi.authme.datasource.converter.LoginSecurityConverter; +import fr.xephi.authme.datasource.converter.MySqlToSqlite; +import fr.xephi.authme.datasource.converter.RoyalAuthConverter; +import fr.xephi.authme.datasource.converter.SqliteToH2; +import fr.xephi.authme.datasource.converter.SqliteToSql; +import fr.xephi.authme.datasource.converter.XAuthConverter; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.service.BukkitService; +import fr.xephi.authme.service.CommonService; +import org.bukkit.command.CommandSender; + +import javax.inject.Inject; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +/** + * Converter command: launches conversion based on its parameters. + */ +public class ConverterCommand implements ExecutableCommand { + + @VisibleForTesting + static final Map> CONVERTERS = getConverters(); + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(ConverterCommand.class); + + @Inject + private CommonService commonService; + + @Inject + private BukkitService bukkitService; + + @Inject + private Factory converterFactory; + + @Override + public void executeCommand(CommandSender sender, List arguments) { + Class converterClass = getConverterClassFromArgs(arguments); + if (converterClass == null) { + sender.sendMessage("Converters: " + String.join(", ", CONVERTERS.keySet())); + return; + } + + // Get the proper converter instance + final Converter converter = converterFactory.newInstance(converterClass); + + // Run the convert job + bukkitService.runTaskAsynchronously(() -> { + try { + converter.execute(sender); + } catch (Exception e) { + commonService.send(sender, MessageKey.ERROR); + logger.logException("Error during conversion:", e); + } + }); + + // Show a status message + sender.sendMessage("[AuthMe] Successfully started " + arguments.get(0)); + } + + private static Class getConverterClassFromArgs(List arguments) { + return arguments.isEmpty() + ? null + : CONVERTERS.get(arguments.get(0).toLowerCase(Locale.ROOT)); + } + + /** + * Initializes a map with all available converters. + * + * @return map with all available converters + */ + private static Map> getConverters() { + return ImmutableSortedMap.>naturalOrder() + .put("xauth", XAuthConverter.class) + .put("crazylogin", CrazyLoginConverter.class) + .put("royalauth", RoyalAuthConverter.class) + .put("sqlitetosql", SqliteToSql.class) + .put("mysqltosqlite", MySqlToSqlite.class) + .put("sqlitetoh2", SqliteToH2.class) + .put("h2tosqlite", H2ToSqlite.class) + .put("loginsecurity", LoginSecurityConverter.class) + .build(); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/FirstSpawnCommand.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/FirstSpawnCommand.java new file mode 100644 index 00000000..9eba3d15 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/FirstSpawnCommand.java @@ -0,0 +1,35 @@ +package fr.xephi.authme.command.executable.authme; + +import fr.xephi.authme.command.PlayerCommand; +import fr.xephi.authme.service.BukkitService; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.SpawnLoader; +import fr.xephi.authme.util.TeleportUtils; +import org.bukkit.entity.Player; + +import javax.inject.Inject; +import java.util.List; + +/** + * Teleports the player to the first spawn. + */ +public class FirstSpawnCommand extends PlayerCommand { + @Inject + private Settings settings; + @Inject + private SpawnLoader spawnLoader; + @Inject + private BukkitService bukkitService; + @Override + 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 { + //String name= player.getName(); + bukkitService.runTaskIfFolia(player, () -> { + TeleportUtils.teleport(player, spawnLoader.getFirstSpawn()); + }); + //player.teleport(spawnLoader.getFirstSpawn()); + } + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/ForceLoginCommand.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/ForceLoginCommand.java new file mode 100644 index 00000000..3adcad45 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/ForceLoginCommand.java @@ -0,0 +1,44 @@ +package fr.xephi.authme.command.executable.authme; + +import fr.xephi.authme.command.ExecutableCommand; +import fr.xephi.authme.permission.PermissionsManager; +import fr.xephi.authme.process.Management; +import fr.xephi.authme.service.BukkitService; +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; + +/** + * Forces the login of a player, i.e. logs the player in without the need of a (correct) password. + */ +public class ForceLoginCommand implements ExecutableCommand { + + @Inject + private PermissionsManager permissionsManager; + + @Inject + private Management management; + + @Inject + private BukkitService bukkitService; + + @Override + public void executeCommand(CommandSender sender, List arguments) { + // Get the player query + String playerName = arguments.isEmpty() ? sender.getName() : arguments.get(0); + + 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)) { + sender.sendMessage("You cannot force login the player " + playerName + "!"); + } else { + management.forceLogin(player); + sender.sendMessage("Force login for " + playerName + " performed!"); + } + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/GetEmailCommand.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/GetEmailCommand.java new file mode 100644 index 00000000..b6691438 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/GetEmailCommand.java @@ -0,0 +1,35 @@ +package fr.xephi.authme.command.executable.authme; + +import ch.jalu.datasourcecolumns.data.DataSourceValue; +import fr.xephi.authme.command.ExecutableCommand; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.service.CommonService; +import org.bukkit.command.CommandSender; + +import javax.inject.Inject; +import java.util.List; + +/** + * Returns a player's email. + */ +public class GetEmailCommand implements ExecutableCommand { + + @Inject + private DataSource dataSource; + + @Inject + private CommonService commonService; + + @Override + public void executeCommand(CommandSender sender, List arguments) { + String playerName = arguments.isEmpty() ? sender.getName() : arguments.get(0); + + DataSourceValue email = dataSource.getEmail(playerName); + if (email.rowExists()) { + sender.sendMessage("[AuthMe] " + playerName + "'s email: " + email.getValue()); + } else { + commonService.send(sender, MessageKey.UNKNOWN_USER); + } + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/GetIpCommand.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/GetIpCommand.java new file mode 100644 index 00000000..2e00c65e --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/GetIpCommand.java @@ -0,0 +1,41 @@ +package fr.xephi.authme.command.executable.authme; + +import fr.xephi.authme.command.ExecutableCommand; +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.service.BukkitService; +import fr.xephi.authme.util.PlayerUtils; +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; + + @Inject + private DataSource dataSource; + + @Override + public void executeCommand(CommandSender sender, List arguments) { + String playerName = arguments.get(0); + Player player = bukkitService.getPlayerExact(playerName); + PlayerAuth auth = dataSource.getAuth(playerName); + + if (player != null) { + sender.sendMessage("Current IP of " + player.getName() + " is " + PlayerUtils.getPlayerIp(player) + + ":" + player.getAddress().getPort()); + } + + if (auth == null) { + String displayName = player == null ? playerName : player.getName(); + sender.sendMessage(displayName + " is not registered in the database"); + } else { + sender.sendMessage("Database: last IP: " + auth.getLastIp() + ", registration IP: " + + auth.getRegistrationIp()); + } + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/LastLoginCommand.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/LastLoginCommand.java new file mode 100644 index 00000000..f522e80f --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/LastLoginCommand.java @@ -0,0 +1,56 @@ +package fr.xephi.authme.command.executable.authme; + +import fr.xephi.authme.command.ExecutableCommand; +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.service.CommonService; +import org.bukkit.command.CommandSender; + +import javax.inject.Inject; +import java.util.Date; +import java.util.List; + +/** + * Returns the last login date of the given user. + */ +public class LastLoginCommand implements ExecutableCommand { + + @Inject + private DataSource dataSource; + + @Inject + private CommonService commonService; + + @Override + public void executeCommand(CommandSender sender, List arguments) { + // Get the player + String playerName = arguments.isEmpty() ? sender.getName() : arguments.get(0); + + PlayerAuth auth = dataSource.getAuth(playerName); + if (auth == null) { + commonService.send(sender, MessageKey.UNKNOWN_USER); + return; + } + + // Get the last login date + final Long lastLogin = auth.getLastLogin(); + final String lastLoginDate = lastLogin == null ? "never" : new Date(lastLogin).toString(); + + // Show the player status + sender.sendMessage("[AuthMe] " + playerName + " last login: " + lastLoginDate); + if (lastLogin != null) { + sender.sendMessage("[AuthMe] The player " + playerName + " last logged in " + + createLastLoginIntervalMessage(lastLogin) + " ago"); + } + sender.sendMessage("[AuthMe] Last player's IP: " + auth.getLastIp()); + } + + private static String createLastLoginIntervalMessage(long lastLogin) { + final long diff = System.currentTimeMillis() - lastLogin; + return (int) (diff / 86400000) + " days " + + (int) (diff / 3600000 % 24) + " hours " + + (int) (diff / 60000 % 60) + " mins " + + (int) (diff / 1000 % 60) + " secs"; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/PurgeBannedPlayersCommand.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/PurgeBannedPlayersCommand.java new file mode 100644 index 00000000..860ae73d --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/PurgeBannedPlayersCommand.java @@ -0,0 +1,38 @@ +package fr.xephi.authme.command.executable.authme; + +import fr.xephi.authme.command.ExecutableCommand; +import fr.xephi.authme.service.BukkitService; +import fr.xephi.authme.task.purge.PurgeService; +import org.bukkit.OfflinePlayer; +import org.bukkit.command.CommandSender; + +import javax.inject.Inject; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +/** + * Command for purging data of banned players. Depending on the settings + * it purges (deletes) data from third-party plugins as well. + */ +public class PurgeBannedPlayersCommand implements ExecutableCommand { + + @Inject + private PurgeService purgeService; + + @Inject + private BukkitService bukkitService; + + @Override + public void executeCommand(CommandSender sender, List arguments) { + // Get the list of banned players + Set bannedPlayers = bukkitService.getBannedPlayers(); + Set namedBanned = new HashSet<>(bannedPlayers.size()); + for (OfflinePlayer offlinePlayer : bannedPlayers) { + namedBanned.add(offlinePlayer.getName().toLowerCase(Locale.ROOT)); + } + + purgeService.purgePlayers(sender, namedBanned, bannedPlayers.toArray(new OfflinePlayer[bannedPlayers.size()])); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/PurgeCommand.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/PurgeCommand.java new file mode 100644 index 00000000..1538061e --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/PurgeCommand.java @@ -0,0 +1,51 @@ +package fr.xephi.authme.command.executable.authme; + +import com.google.common.primitives.Ints; +import fr.xephi.authme.command.ExecutableCommand; +import fr.xephi.authme.task.purge.PurgeService; +import org.bukkit.ChatColor; +import org.bukkit.command.CommandSender; + +import javax.inject.Inject; +import java.util.Calendar; +import java.util.List; + +/** + * Command for purging the data of players which have not been online for a given number + * of days. Depending on the settings, this removes player data in third-party plugins as well. + */ +public class PurgeCommand implements ExecutableCommand { + + private static final int MINIMUM_LAST_SEEN_DAYS = 30; + + @Inject + private PurgeService purgeService; + + @Override + public void executeCommand(CommandSender sender, List arguments) { + // Get the days parameter + String daysStr = arguments.get(0); + + // Convert the days string to an integer value, and make sure it's valid + Integer days = Ints.tryParse(daysStr); + if (days == null) { + sender.sendMessage(ChatColor.RED + "The value you've entered is invalid!"); + return; + } + + // Validate the value + if (days < MINIMUM_LAST_SEEN_DAYS) { + sender.sendMessage(ChatColor.RED + "You can only purge data older than " + + MINIMUM_LAST_SEEN_DAYS + " days"); + return; + } + + // Create a calender instance to determine the date + Calendar calendar = Calendar.getInstance(); + calendar.add(Calendar.DATE, -days); + long until = calendar.getTimeInMillis(); + + // Run the purge + purgeService.runPurge(sender, until); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/PurgeLastPositionCommand.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/PurgeLastPositionCommand.java new file mode 100644 index 00000000..4e20b031 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/PurgeLastPositionCommand.java @@ -0,0 +1,56 @@ +package fr.xephi.authme.command.executable.authme; + +import fr.xephi.authme.command.ExecutableCommand; +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.service.CommonService; +import org.bukkit.command.CommandSender; + +import javax.inject.Inject; +import java.util.List; + +/** + * Removes the stored last position of a user or of all. + */ +public class PurgeLastPositionCommand implements ExecutableCommand { + + @Inject + private DataSource dataSource; + + @Inject + private CommonService commonService; + + @Override + public void executeCommand(CommandSender sender, List arguments) { + String playerName = arguments.isEmpty() ? sender.getName() : arguments.get(0); + + if ("*".equals(playerName)) { + for (PlayerAuth auth : dataSource.getAllAuths()) { + resetLastPosition(auth); + dataSource.updateQuitLoc(auth); + // TODO: send an update when a messaging service will be implemented (QUITLOC) + } + sender.sendMessage("All players last position locations are now reset"); + } else { + // Get the user auth and make sure the user exists + PlayerAuth auth = dataSource.getAuth(playerName); + if (auth == null) { + commonService.send(sender, MessageKey.UNKNOWN_USER); + return; + } + + resetLastPosition(auth); + dataSource.updateQuitLoc(auth); + // TODO: send an update when a messaging service will be implemented (QUITLOC) + sender.sendMessage(playerName + "'s last position location is now reset"); + } + } + + private static void resetLastPosition(PlayerAuth auth) { + auth.setQuitLocX(0d); + auth.setQuitLocY(0d); + auth.setQuitLocZ(0d); + auth.setWorld("world"); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/PurgePlayerCommand.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/PurgePlayerCommand.java new file mode 100644 index 00000000..e0bf9040 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/PurgePlayerCommand.java @@ -0,0 +1,47 @@ +package fr.xephi.authme.command.executable.authme; + +import fr.xephi.authme.command.ExecutableCommand; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.service.BukkitService; +import fr.xephi.authme.task.purge.PurgeExecutor; +import org.bukkit.OfflinePlayer; +import org.bukkit.command.CommandSender; + +import javax.inject.Inject; +import java.util.List; +import java.util.Locale; + +import static java.util.Collections.singletonList; + +/** + * Command to purge a player. + */ +public class PurgePlayerCommand implements ExecutableCommand { + + @Inject + private PurgeExecutor purgeExecutor; + + @Inject + private BukkitService bukkitService; + + @Inject + private DataSource dataSource; + + @Override + public void executeCommand(CommandSender sender, List arguments) { + String option = arguments.size() > 1 ? arguments.get(1) : null; + bukkitService.runTaskAsynchronously( + () -> executeCommand(sender, arguments.get(0), option)); + } + + private void executeCommand(CommandSender sender, String name, String option) { + if ("force".equals(option) || !dataSource.isAuthAvailable(name)) { + OfflinePlayer offlinePlayer = bukkitService.getOfflinePlayer(name); + purgeExecutor.executePurge(singletonList(offlinePlayer), singletonList(name.toLowerCase(Locale.ROOT))); + sender.sendMessage("Purged data for player " + name); + } else { + sender.sendMessage("This player is still registered! Are you sure you want to proceed? " + + "Use '/authme purgeplayer " + name + " force' to run the command anyway"); + } + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/RecentPlayersCommand.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/RecentPlayersCommand.java new file mode 100644 index 00000000..3ae185e3 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/RecentPlayersCommand.java @@ -0,0 +1,55 @@ +package fr.xephi.authme.command.executable.authme; + +import com.google.common.annotations.VisibleForTesting; +import fr.xephi.authme.command.ExecutableCommand; +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.datasource.DataSource; +import org.bukkit.ChatColor; +import org.bukkit.command.CommandSender; + +import javax.inject.Inject; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.List; + +import static java.time.Instant.ofEpochMilli; + +/** + * Command showing the most recent logged in players. + */ +public class RecentPlayersCommand implements ExecutableCommand { + + /** DateTime formatter, producing Strings such as "10:42 AM, 11 Jul". */ + private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("hh:mm a, dd MMM"); + + @Inject + private DataSource dataSource; + + @Override + public void executeCommand(CommandSender sender, List arguments) { + List recentPlayers = dataSource.getRecentlyLoggedInPlayers(); + + sender.sendMessage(ChatColor.BLUE + "[AuthMe] Recently logged in players"); + for (PlayerAuth auth : recentPlayers) { + sender.sendMessage(formatPlayerMessage(auth)); + } + } + + @VisibleForTesting + ZoneId getZoneId() { + return ZoneId.systemDefault(); + } + + private String formatPlayerMessage(PlayerAuth auth) { + String lastLoginText; + if (auth.getLastLogin() == null) { + lastLoginText = "never"; + } else { + LocalDateTime lastLogin = LocalDateTime.ofInstant(ofEpochMilli(auth.getLastLogin()), getZoneId()); + lastLoginText = DATE_FORMAT.format(lastLogin); + } + + return "- " + auth.getRealName() + " (" + lastLoginText + " with IP " + auth.getLastIp() + ")"; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/RegisterAdminCommand.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/RegisterAdminCommand.java new file mode 100644 index 00000000..7b2847f7 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/RegisterAdminCommand.java @@ -0,0 +1,86 @@ +package fr.xephi.authme.command.executable.authme; + +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.command.ExecutableCommand; +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.security.PasswordSecurity; +import fr.xephi.authme.security.crypts.HashedPassword; +import fr.xephi.authme.service.BukkitService; +import fr.xephi.authme.service.CommonService; +import fr.xephi.authme.service.ValidationService; +import fr.xephi.authme.service.ValidationService.ValidationResult; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import javax.inject.Inject; +import java.util.List; +import java.util.Locale; + +/** + * Admin command to register a user. + */ +public class RegisterAdminCommand implements ExecutableCommand { + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(RegisterAdminCommand.class); + + @Inject + private PasswordSecurity passwordSecurity; + + @Inject + private CommonService commonService; + + @Inject + private DataSource dataSource; + + @Inject + private BukkitService bukkitService; + + @Inject + private ValidationService validationService; + + @Override + 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); + final String playerNameLowerCase = playerName.toLowerCase(Locale.ROOT); + + // Command logic + ValidationResult passwordValidation = validationService.validatePassword(playerPass, playerName); + if (passwordValidation.hasError()) { + commonService.send(sender, passwordValidation.getMessageKey(), passwordValidation.getArgs()); + return; + } + + bukkitService.runTaskOptionallyAsync(() -> { + if (dataSource.isAuthAvailable(playerNameLowerCase)) { + commonService.send(sender, MessageKey.NAME_ALREADY_REGISTERED); + return; + } + HashedPassword hashedPassword = passwordSecurity.computeHash(playerPass, playerNameLowerCase); + PlayerAuth auth = PlayerAuth.builder() + .name(playerNameLowerCase) + .realName(playerName) + .password(hashedPassword) + .registrationDate(System.currentTimeMillis()) + .build(); + + if (!dataSource.saveAuth(auth)) { + commonService.send(sender, MessageKey.ERROR); + return; + } + + commonService.send(sender, MessageKey.REGISTER_SUCCESS); + logger.info(sender.getName() + " registered " + playerName); + final Player player = bukkitService.getPlayerExact(playerName); + if (player != null) { + bukkitService.scheduleSyncTaskFromOptionallyAsyncTask(() -> + // AuthMeReReloaded - Folia compatibility + bukkitService.runTaskIfFolia(player, () -> player.kickPlayer(commonService.retrieveSingleMessage(player, MessageKey.KICK_FOR_ADMIN_REGISTER)))); + } + }); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/ReloadCommand.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/ReloadCommand.java new file mode 100644 index 00000000..2956a39f --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/ReloadCommand.java @@ -0,0 +1,77 @@ +package fr.xephi.authme.command.executable.authme; + +import ch.jalu.injector.factory.SingletonStore; +import fr.xephi.authme.AuthMe; +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.command.ExecutableCommand; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.initialization.Reloadable; +import fr.xephi.authme.initialization.SettingsDependent; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.service.CommonService; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.SettingsWarner; +import fr.xephi.authme.settings.properties.DatabaseSettings; +import fr.xephi.authme.util.Utils; +import org.bukkit.command.CommandSender; + +import javax.inject.Inject; +import java.util.List; + +/** + * The reload command. + */ +public class ReloadCommand implements ExecutableCommand { + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(ReloadCommand.class); + + @Inject + private AuthMe plugin; + + @Inject + private Settings settings; + + @Inject + private DataSource dataSource; + + @Inject + private CommonService commonService; + + @Inject + private SettingsWarner settingsWarner; + + @Inject + private SingletonStore reloadableStore; + + @Inject + private SingletonStore settingsDependentStore; + + @Override + public void executeCommand(CommandSender sender, List arguments) { + try { + settings.reload(); + ConsoleLoggerFactory.reloadSettings(settings); + settingsWarner.logWarningsForMisconfigurations(); + + // 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())) { + Utils.logAndSendMessage(sender, "Note: cannot change database type during /authme reload"); + } + performReloadOnServices(); + commonService.send(sender, MessageKey.CONFIG_RELOAD_SUCCESS); + } catch (Exception e) { + sender.sendMessage("Error occurred during reload of AuthMe: aborting"); + logger.logException("Aborting! Encountered exception during reload of AuthMe:", e); + plugin.stopOrUnload(); + } + } + + private void performReloadOnServices() { + reloadableStore.retrieveAllOfType() + .forEach(r -> r.reload()); + + settingsDependentStore.retrieveAllOfType() + .forEach(s -> s.reload(settings)); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/SetEmailCommand.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/SetEmailCommand.java new file mode 100644 index 00000000..51890edc --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/SetEmailCommand.java @@ -0,0 +1,75 @@ +package fr.xephi.authme.command.executable.authme; + +import fr.xephi.authme.command.ExecutableCommand; +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.data.auth.PlayerCache; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.service.BukkitService; +import fr.xephi.authme.service.CommonService; +import fr.xephi.authme.service.ValidationService; +import org.bukkit.command.CommandSender; + +import javax.inject.Inject; +import java.util.List; + +/** + * Admin command for setting an email to an account. + */ +public class SetEmailCommand implements ExecutableCommand { + + @Inject + private DataSource dataSource; + + @Inject + private CommonService commonService; + + @Inject + private PlayerCache playerCache; + + @Inject + private BukkitService bukkitService; + + @Inject + private ValidationService validationService; + + @Override + 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); + + // Validate the email address + if (!validationService.validateEmail(playerEmail)) { + commonService.send(sender, MessageKey.INVALID_EMAIL); + return; + } + + bukkitService.runTaskOptionallyAsync(() -> { // AuthMeReReloaded - Folia compatibility + // Validate the user + PlayerAuth auth = dataSource.getAuth(playerName); + if (auth == null) { + commonService.send(sender, MessageKey.UNKNOWN_USER); + return; + } else if (!validationService.isEmailFreeForRegistration(playerEmail, sender)) { + commonService.send(sender, MessageKey.EMAIL_ALREADY_USED_ERROR); + return; + } + + // Set the email address + auth.setEmail(playerEmail); + if (!dataSource.updateEmail(auth)) { + commonService.send(sender, MessageKey.ERROR); + return; + } + + // Update the player cache + if (playerCache.getAuth(playerName) != null) { + playerCache.updatePlayer(auth); + } + + // Show a status message + commonService.send(sender, MessageKey.EMAIL_CHANGED_SUCCESS); + }); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/SetFirstSpawnCommand.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/SetFirstSpawnCommand.java new file mode 100644 index 00000000..899a1103 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/SetFirstSpawnCommand.java @@ -0,0 +1,23 @@ +package fr.xephi.authme.command.executable.authme; + +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) { + 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/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/SetSpawnCommand.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/SetSpawnCommand.java new file mode 100644 index 00000000..fc9a67b9 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/SetSpawnCommand.java @@ -0,0 +1,23 @@ +package fr.xephi.authme.command.executable.authme; + +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) { + 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/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/SpawnCommand.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/SpawnCommand.java new file mode 100644 index 00000000..92ad0a30 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/SpawnCommand.java @@ -0,0 +1,27 @@ +package fr.xephi.authme.command.executable.authme; + +import fr.xephi.authme.command.PlayerCommand; +import fr.xephi.authme.service.BukkitService; +import fr.xephi.authme.settings.SpawnLoader; +import fr.xephi.authme.util.TeleportUtils; +import org.bukkit.entity.Player; + +import javax.inject.Inject; +import java.util.List; + +public class SpawnCommand extends PlayerCommand { + + @Inject + private SpawnLoader spawnLoader; + @Inject + private BukkitService bukkitService; + + @Override + public void runCommand(Player player, List arguments) { + if (spawnLoader.getSpawn() == null) { + player.sendMessage("[AuthMe] Spawn has failed, please try to define the spawn"); + } else { + bukkitService.runTaskIfFolia(player, () -> TeleportUtils.teleport(player, spawnLoader.getSpawn())); + } + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/SwitchAntiBotCommand.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/SwitchAntiBotCommand.java new file mode 100644 index 00000000..1e72c058 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/SwitchAntiBotCommand.java @@ -0,0 +1,52 @@ +package fr.xephi.authme.command.executable.authme; + +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; +import fr.xephi.authme.service.AntiBotService; +import org.bukkit.ChatColor; +import org.bukkit.command.CommandSender; + +import javax.inject.Inject; +import java.util.Arrays; +import java.util.List; + +/** + * Display or change the status of the antibot mod. + */ +public class SwitchAntiBotCommand implements ExecutableCommand { + + @Inject + private AntiBotService antiBotService; + + @Inject + private CommandMapper commandMapper; + + @Inject + private HelpProvider helpProvider; + + @Override + public void executeCommand(final CommandSender sender, List arguments) { + if (arguments.isEmpty()) { + sender.sendMessage("[AuthMe] AntiBot status: " + antiBotService.getAntiBotStatus().name()); + return; + } + + String newState = arguments.get(0); + + // Enable or disable the mod + if ("ON".equalsIgnoreCase(newState)) { + antiBotService.overrideAntiBotStatus(true); + sender.sendMessage("[AuthMe] AntiBot Manual Override: enabled!"); + } else if ("OFF".equalsIgnoreCase(newState)) { + antiBotService.overrideAntiBotStatus(false); + sender.sendMessage("[AuthMe] AntiBot Manual Override: disabled!"); + } else { + sender.sendMessage(ChatColor.DARK_RED + "Invalid AntiBot mode!"); + 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/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/TotpDisableAdminCommand.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/TotpDisableAdminCommand.java new file mode 100644 index 00000000..4789e043 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/TotpDisableAdminCommand.java @@ -0,0 +1,61 @@ +package fr.xephi.authme.command.executable.authme; + +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.command.ExecutableCommand; +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.message.Messages; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.service.BukkitService; +import org.bukkit.ChatColor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import javax.inject.Inject; +import java.util.List; + +/** + * Command to disable two-factor authentication for a user. + */ +public class TotpDisableAdminCommand implements ExecutableCommand { + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(TotpDisableAdminCommand.class); + + @Inject + private DataSource dataSource; + + @Inject + private Messages messages; + + @Inject + private BukkitService bukkitService; + + @Override + public void executeCommand(CommandSender sender, List arguments) { + String player = arguments.get(0); + + PlayerAuth auth = dataSource.getAuth(player); + if (auth == null) { + messages.send(sender, MessageKey.UNKNOWN_USER); + } else if (auth.getTotpKey() == null) { + sender.sendMessage(ChatColor.RED + "Player '" + player + "' does not have two-factor auth enabled"); + } else { + removeTotpKey(sender, player); + } + } + + private void removeTotpKey(CommandSender sender, String player) { + if (dataSource.removeTotpKey(player)) { + sender.sendMessage("Disabled two-factor authentication successfully for '" + player + "'"); + logger.info(sender.getName() + " disable two-factor authentication for '" + player + "'"); + + Player onlinePlayer = bukkitService.getPlayerExact(player); + if (onlinePlayer != null) { + messages.send(onlinePlayer, MessageKey.TWO_FACTOR_REMOVED_SUCCESS); + } + } else { + messages.send(sender, MessageKey.ERROR); + } + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/TotpViewStatusCommand.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/TotpViewStatusCommand.java new file mode 100644 index 00000000..d9b2c92c --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/TotpViewStatusCommand.java @@ -0,0 +1,38 @@ +package fr.xephi.authme.command.executable.authme; + +import fr.xephi.authme.command.ExecutableCommand; +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.message.Messages; +import org.bukkit.ChatColor; +import org.bukkit.command.CommandSender; + +import javax.inject.Inject; +import java.util.List; + +/** + * Command to see whether a user has enabled two-factor authentication. + */ +public class TotpViewStatusCommand implements ExecutableCommand { + + @Inject + private DataSource dataSource; + + @Inject + private Messages messages; + + @Override + public void executeCommand(CommandSender sender, List arguments) { + String player = arguments.get(0); + + PlayerAuth auth = dataSource.getAuth(player); + if (auth == null) { + messages.send(sender, MessageKey.UNKNOWN_USER); + } else if (auth.getTotpKey() == null) { + sender.sendMessage(ChatColor.RED + "Player '" + player + "' does NOT have two-factor auth enabled"); + } else { + sender.sendMessage(ChatColor.DARK_GREEN + "Player '" + player + "' has enabled two-factor authentication"); + } + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/UnregisterAdminCommand.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/UnregisterAdminCommand.java new file mode 100644 index 00000000..d8901994 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/UnregisterAdminCommand.java @@ -0,0 +1,49 @@ +package fr.xephi.authme.command.executable.authme; + +import fr.xephi.authme.command.ExecutableCommand; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.process.Management; +import fr.xephi.authme.service.BukkitService; +import fr.xephi.authme.service.CommonService; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import javax.inject.Inject; +import java.util.List; + +/** + * Admin command to unregister a player. + */ +public class UnregisterAdminCommand implements ExecutableCommand { + + @Inject + private DataSource dataSource; + + @Inject + private CommonService commonService; + + @Inject + private BukkitService bukkitService; + + @Inject + private Management management; + + UnregisterAdminCommand() { + } + + @Override + public void executeCommand(final CommandSender sender, List arguments) { + String playerName = arguments.get(0); + + // Make sure the user exists + if (!dataSource.isAuthAvailable(playerName)) { + commonService.send(sender, MessageKey.UNKNOWN_USER); + return; + } + + // Get the player from the server and perform unregister + Player target = bukkitService.getPlayerExact(playerName); + management.performUnregisterByAdmin(sender, playerName, target); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/UpdateHelpMessagesCommand.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/UpdateHelpMessagesCommand.java new file mode 100644 index 00000000..7f61afd0 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/UpdateHelpMessagesCommand.java @@ -0,0 +1,39 @@ +package fr.xephi.authme.command.executable.authme; + +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.command.ExecutableCommand; +import fr.xephi.authme.command.help.HelpMessagesService; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.service.HelpTranslationGenerator; +import org.bukkit.command.CommandSender; + +import javax.inject.Inject; +import java.io.File; +import java.io.IOException; +import java.util.List; + +/** + * Messages command, updates the user's help messages file with any missing files + * from the provided file in the JAR. + */ +public class UpdateHelpMessagesCommand implements ExecutableCommand { + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(UpdateHelpMessagesCommand.class); + + @Inject + private HelpTranslationGenerator helpTranslationGenerator; + @Inject + private HelpMessagesService helpMessagesService; + + @Override + public void executeCommand(CommandSender sender, List arguments) { + try { + File updatedFile = helpTranslationGenerator.updateHelpFile(); + sender.sendMessage("Successfully updated the help file '" + updatedFile.getName() + "'"); + helpMessagesService.reloadMessagesFile(); + } catch (IOException e) { + sender.sendMessage("Could not update help file: " + e.getMessage()); + logger.logException("Could not update help file:", e); + } + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/VersionCommand.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/VersionCommand.java new file mode 100644 index 00000000..2f7e8a7c --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/VersionCommand.java @@ -0,0 +1,99 @@ +package fr.xephi.authme.command.executable.authme; + +import fr.xephi.authme.AuthMe; +import fr.xephi.authme.command.ExecutableCommand; +import fr.xephi.authme.service.BukkitService; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.properties.DatabaseSettings; +import org.bukkit.ChatColor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import javax.inject.Inject; +import java.text.SimpleDateFormat; +import java.util.Collection; +import java.util.Date; +import java.util.List; + +public class VersionCommand implements ExecutableCommand { + + @Inject + private BukkitService bukkitService; + @Inject + private Settings settings; + + @Override + public void executeCommand(CommandSender sender, List arguments) { + // Show some version info + sender.sendMessage(ChatColor.GOLD + "==========[ " + AuthMe.getPluginName() + " ABOUT ]=========="); + sender.sendMessage(ChatColor.GOLD + "Version: " + ChatColor.WHITE + AuthMe.getPluginName() + + " v" + AuthMe.getPluginVersion() + ChatColor.GRAY + " (build: " + AuthMe.getPluginBuildNumber() + ")"); + sender.sendMessage(ChatColor.GOLD + "Database Implementation: " + ChatColor.WHITE + settings.getProperty(DatabaseSettings.BACKEND).toString()); + sender.sendMessage(ChatColor.GOLD + "Authors:"); + Collection onlinePlayers = bukkitService.getOnlinePlayers(); + printDeveloper(sender, "Gabriele C.", "sgdc3", "Project manager, Contributor", onlinePlayers); + printDeveloper(sender, "Lucas J.", "ljacqu", "Main Developer", onlinePlayers); + printDeveloper(sender, "games647", "games647", "Developer", onlinePlayers); + printDeveloper(sender, "Hex3l", "Hex3l", "Developer", onlinePlayers); + printDeveloper(sender, "krusic22", "krusic22", "Support", onlinePlayers); + sender.sendMessage(ChatColor.GOLD + "Retired authors:"); + printDeveloper(sender, "Alexandre Vanhecke", "xephi59", "Original Author", onlinePlayers); + printDeveloper(sender, "Gnat008", "gnat008", "Developer, Retired", onlinePlayers); + printDeveloper(sender, "DNx5", "DNx5", "Developer, Retired", onlinePlayers); + printDeveloper(sender, "Tim Visee", "timvisee", "Developer, Retired", onlinePlayers); + sender.sendMessage(ChatColor.GOLD + "Website: " + ChatColor.WHITE + + "https://github.com/AuthMe/AuthMeReloaded"); + sender.sendMessage(ChatColor.GOLD + "License: " + ChatColor.WHITE + "GNU GPL v3.0" + + ChatColor.GRAY + ChatColor.ITALIC + " (See LICENSE file)"); + sender.sendMessage(ChatColor.GOLD + "Copyright: " + ChatColor.WHITE + + "Copyright (c) AuthMe-Team " + new SimpleDateFormat("yyyy").format(new Date()) + + ". Released under GPL v3 License."); + } + + /** + * Print a developer with proper styling. + * + * @param sender The command sender + * @param name The display name of the developer + * @param minecraftName The Minecraft username of the developer, if available + * @param function The function of the developer + * @param onlinePlayers The list of online players + */ + private static void printDeveloper(CommandSender sender, String name, String minecraftName, String function, + Collection onlinePlayers) { + // Print the name + StringBuilder msg = new StringBuilder(); + msg.append(" ") + .append(ChatColor.WHITE) + .append(name); + + // Append the Minecraft name + msg.append(ChatColor.GRAY).append(" // ").append(ChatColor.WHITE).append(minecraftName); + msg.append(ChatColor.GRAY).append(ChatColor.ITALIC).append(" (").append(function).append(")"); + + // Show the online status + if (isPlayerOnline(minecraftName, onlinePlayers)) { + msg.append(ChatColor.GREEN).append(ChatColor.ITALIC).append(" (In-Game)"); + } + + // Print the message + sender.sendMessage(msg.toString()); + } + + /** + * Check whether a player is online. + * + * @param minecraftName The Minecraft player name + * @param onlinePlayers List of online players + * + * @return True if the player is online, false otherwise + */ + private static boolean isPlayerOnline(String minecraftName, Collection onlinePlayers) { + for (Player player : onlinePlayers) { + if (player.getName().equalsIgnoreCase(minecraftName)) { + return true; + } + } + return false; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/debug/CountryLookup.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/debug/CountryLookup.java new file mode 100644 index 00000000..78cee462 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/debug/CountryLookup.java @@ -0,0 +1,88 @@ +package fr.xephi.authme.command.executable.authme.debug; + +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.permission.DebugSectionPermissions; +import fr.xephi.authme.permission.PermissionNode; +import fr.xephi.authme.service.GeoIpService; +import fr.xephi.authme.service.ValidationService; +import fr.xephi.authme.settings.properties.ProtectionSettings; +import org.bukkit.ChatColor; +import org.bukkit.command.CommandSender; + +import javax.inject.Inject; +import java.util.List; +import java.util.regex.Pattern; + +/** + * Shows the GeoIP information as returned by the geoIpService. + */ +class CountryLookup implements DebugSection { + + private static final Pattern IS_IP_ADDR = Pattern.compile("(\\d{1,3}\\.){3}\\d{1,3}"); + + @Inject + private GeoIpService geoIpService; + + @Inject + private DataSource dataSource; + + @Inject + private ValidationService validationService; + + @Override + public String getName() { + return "cty"; + } + + @Override + public String getDescription() { + return "Check country protection / country data"; + } + + @Override + public void execute(CommandSender sender, List arguments) { + sender.sendMessage(ChatColor.BLUE + "AuthMe country lookup"); + if (arguments.isEmpty()) { + sender.sendMessage("Check player: /authme debug cty Bobby"); + sender.sendMessage("Check IP address: /authme debug cty 127.123.45.67"); + return; + } + + String argument = arguments.get(0); + if (IS_IP_ADDR.matcher(argument).matches()) { + outputInfoForIpAddr(sender, argument); + } else { + outputInfoForPlayer(sender, argument); + } + } + + @Override + public PermissionNode getRequiredPermission() { + return DebugSectionPermissions.COUNTRY_LOOKUP; + } + + private void outputInfoForIpAddr(CommandSender sender, String ipAddr) { + sender.sendMessage("IP '" + ipAddr + "' maps to country '" + geoIpService.getCountryCode(ipAddr) + + "' (" + geoIpService.getCountryName(ipAddr) + ")"); + if (validationService.isCountryAdmitted(ipAddr)) { + sender.sendMessage(ChatColor.DARK_GREEN + "This IP address' country is not blocked"); + } else { + sender.sendMessage(ChatColor.DARK_RED + "This IP address' country is blocked from the server"); + } + sender.sendMessage("Note: if " + ProtectionSettings.ENABLE_PROTECTION + " is false no country is blocked"); + } + + // TODO #1366: Extend with registration IP? + private void outputInfoForPlayer(CommandSender sender, String name) { + PlayerAuth auth = dataSource.getAuth(name); + if (auth == null) { + sender.sendMessage("No player with name '" + name + "'"); + } else if (auth.getLastIp() == null) { + sender.sendMessage("No last IP address known for '" + name + "'"); + } else { + sender.sendMessage("Player '" + name + "' has IP address " + auth.getLastIp()); + outputInfoForIpAddr(sender, auth.getLastIp()); + } + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/debug/DataStatistics.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/debug/DataStatistics.java new file mode 100644 index 00000000..406ee17d --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/debug/DataStatistics.java @@ -0,0 +1,81 @@ +package fr.xephi.authme.command.executable.authme.debug; + +import ch.jalu.injector.factory.SingletonStore; +import fr.xephi.authme.data.auth.PlayerCache; +import fr.xephi.authme.data.limbo.LimboService; +import fr.xephi.authme.datasource.CacheDataSource; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.initialization.HasCleanup; +import fr.xephi.authme.initialization.Reloadable; +import fr.xephi.authme.initialization.SettingsDependent; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.permission.DebugSectionPermissions; +import fr.xephi.authme.permission.PermissionNode; +import org.bukkit.ChatColor; +import org.bukkit.command.CommandSender; + +import javax.inject.Inject; +import java.util.List; +import java.util.Map; + +import static fr.xephi.authme.command.executable.authme.debug.DebugSectionUtils.applyToLimboPlayersMap; + +/** + * Fetches various statistics, particularly regarding in-memory data that is stored. + */ +class DataStatistics implements DebugSection { + + @Inject + private PlayerCache playerCache; + + @Inject + private LimboService limboService; + + @Inject + private DataSource dataSource; + + @Inject + private SingletonStore singletonStore; + + @Override + public String getName() { + return "stats"; + } + + @Override + public String getDescription() { + return "Outputs general data statistics"; + } + + @Override + public void execute(CommandSender sender, List arguments) { + sender.sendMessage(ChatColor.BLUE + "AuthMe statistics"); + sender.sendMessage("LimboPlayers in memory: " + applyToLimboPlayersMap(limboService, Map::size)); + sender.sendMessage("PlayerCache size: " + playerCache.getLogged() + " (= logged in players)"); + + outputDatabaseStats(sender); + outputInjectorStats(sender); + sender.sendMessage("Total logger instances: " + ConsoleLoggerFactory.getTotalLoggers()); + } + + @Override + public PermissionNode getRequiredPermission() { + return DebugSectionPermissions.DATA_STATISTICS; + } + + private void outputDatabaseStats(CommandSender sender) { + sender.sendMessage("Total players in DB: " + dataSource.getAccountsRegistered()); + if (dataSource instanceof CacheDataSource) { + CacheDataSource cacheDataSource = (CacheDataSource) this.dataSource; + sender.sendMessage("Cached PlayerAuth objects: " + cacheDataSource.getCachedAuths().size()); + } + } + + private void outputInjectorStats(CommandSender sender) { + sender.sendMessage("Singleton Java classes: " + singletonStore.retrieveAllOfType().size()); + sender.sendMessage(String.format("(Reloadable: %d / SettingsDependent: %d / HasCleanup: %d)", + singletonStore.retrieveAllOfType(Reloadable.class).size(), + singletonStore.retrieveAllOfType(SettingsDependent.class).size(), + singletonStore.retrieveAllOfType(HasCleanup.class).size())); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/debug/DebugCommand.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/debug/DebugCommand.java new file mode 100644 index 00000000..4198a19e --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/debug/DebugCommand.java @@ -0,0 +1,85 @@ +package fr.xephi.authme.command.executable.authme.debug; + +import ch.jalu.injector.factory.Factory; +import com.google.common.collect.ImmutableSet; +import fr.xephi.authme.command.ExecutableCommand; +import fr.xephi.authme.permission.PermissionsManager; +import org.bukkit.ChatColor; +import org.bukkit.command.CommandSender; + +import javax.inject.Inject; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +/** + * Debug command main. + */ +public class DebugCommand implements ExecutableCommand { + + private static final Set> SECTION_CLASSES = ImmutableSet.of( + PermissionGroups.class, DataStatistics.class, CountryLookup.class, PlayerAuthViewer.class, InputValidator.class, + LimboPlayerViewer.class, CountryLookup.class, HasPermissionChecker.class, TestEmailSender.class, + SpawnLocationViewer.class, MySqlDefaultChanger.class); + + @Inject + private Factory debugSectionFactory; + + @Inject + private PermissionsManager permissionsManager; + + private Map sections; + + @Override + public void executeCommand(CommandSender sender, List arguments) { + DebugSection debugSection = findDebugSection(arguments); + if (debugSection == null) { + sendAvailableSections(sender); + } else { + executeSection(debugSection, sender, arguments); + } + } + + private DebugSection findDebugSection(List arguments) { + if (arguments.isEmpty()) { + return null; + } + return getSections().get(arguments.get(0).toLowerCase(Locale.ROOT)); + } + + private void sendAvailableSections(CommandSender sender) { + sender.sendMessage(ChatColor.BLUE + "AuthMe debug utils"); + sender.sendMessage("Sections available to you:"); + long availableSections = getSections().values().stream() + .filter(section -> permissionsManager.hasPermission(sender, section.getRequiredPermission())) + .peek(e -> sender.sendMessage("- " + e.getName() + ": " + e.getDescription())) + .count(); + + if (availableSections == 0) { + sender.sendMessage(ChatColor.RED + "You don't have permission to view any debug section"); + } + } + + private void executeSection(DebugSection section, CommandSender sender, List arguments) { + if (permissionsManager.hasPermission(sender, section.getRequiredPermission())) { + section.execute(sender, arguments.subList(1, arguments.size())); + } else { + sender.sendMessage(ChatColor.RED + "You don't have permission for this section. See /authme debug"); + } + } + + // Lazy getter + private Map getSections() { + if (sections == null) { + Map sections = new TreeMap<>(); + for (Class sectionClass : SECTION_CLASSES) { + DebugSection section = debugSectionFactory.newInstance(sectionClass); + sections.put(section.getName(), section); + } + this.sections = sections; + } + return sections; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/debug/DebugSection.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/debug/DebugSection.java new file mode 100644 index 00000000..155520c4 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/debug/DebugSection.java @@ -0,0 +1,36 @@ +package fr.xephi.authme.command.executable.authme.debug; + +import fr.xephi.authme.permission.PermissionNode; +import org.bukkit.command.CommandSender; + +import java.util.List; + +/** + * A debug section: "child" command of the debug command. + */ +interface DebugSection { + + /** + * @return the name to get to this child command + */ + String getName(); + + /** + * @return short description of the child command + */ + String getDescription(); + + /** + * Executes the debug child command. + * + * @param sender the sender executing the command + * @param arguments the arguments, without the label of the child command + */ + void execute(CommandSender sender, List arguments); + + /** + * @return permission required to run this section + */ + PermissionNode getRequiredPermission(); + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/debug/DebugSectionUtils.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/debug/DebugSectionUtils.java new file mode 100644 index 00000000..fa4200a0 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/debug/DebugSectionUtils.java @@ -0,0 +1,130 @@ +package fr.xephi.authme.command.executable.authme.debug; + +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.data.limbo.LimboService; +import fr.xephi.authme.datasource.CacheDataSource; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import org.bukkit.Location; + +import java.lang.reflect.Field; +import java.math.RoundingMode; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.util.Locale; +import java.util.Map; +import java.util.function.Function; + +/** + * Utilities used within the DebugSection implementations. + */ +final class DebugSectionUtils { + + private static ConsoleLogger logger = ConsoleLoggerFactory.get(DebugSectionUtils.class); + private static Field limboEntriesField; + + private DebugSectionUtils() { + } + + /** + * Formats the given location in a human readable way. Null-safe. + * + * @param location the location to format + * @return the formatted location + */ + static String formatLocation(Location location) { + if (location == null) { + return "null"; + } + + String worldName = location.getWorld() == null ? "null" : location.getWorld().getName(); + return formatLocation(location.getX(), location.getY(), location.getZ(), worldName); + } + + /** + * Formats the given location in a human readable way. + * + * @param x the x coordinate + * @param y the y coordinate + * @param z the z coordinate + * @param world the world name + * @return the formatted location + */ + static String formatLocation(double x, double y, double z, String world) { + return "(" + round(x) + ", " + round(y) + ", " + round(z) + ") in '" + world + "'"; + } + + /** + * Rounds the given number to two decimals. + * + * @param number the number to round + * @return the rounded number + */ + private static String round(double number) { + DecimalFormat df = new DecimalFormat("#.##"); + df.setDecimalFormatSymbols(DecimalFormatSymbols.getInstance(Locale.US)); + df.setRoundingMode(RoundingMode.HALF_UP); + return df.format(number); + } + + private static Field getLimboPlayerEntriesField() { + if (limboEntriesField == null) { + try { + Field field = LimboService.class.getDeclaredField("entries"); + field.setAccessible(true); + limboEntriesField = field; + } catch (Exception e) { + logger.logException("Could not retrieve LimboService entries field:", e); + } + } + return limboEntriesField; + } + + /** + * Applies the given function to the map in LimboService containing the LimboPlayers. + * As we don't want to expose this information in non-debug settings, this is done with reflection. + * Exceptions are generously caught and {@code null} is returned on failure. + * + * @param limboService the limbo service instance to get the map from + * @param function the function to apply to the map + * @param the result type of the function + * + * @return the value of the function applied to the map, or null upon error + */ + static U applyToLimboPlayersMap(LimboService limboService, Function function) { + Field limboPlayerEntriesField = getLimboPlayerEntriesField(); + if (limboPlayerEntriesField != null) { + try { + return function.apply((Map) limboEntriesField.get(limboService)); + } catch (Exception e) { + logger.logException("Could not retrieve LimboService values:", e); + } + } + return null; + } + + static T castToTypeOrNull(Object object, Class clazz) { + return clazz.isInstance(object) ? clazz.cast(object) : null; + } + + /** + * Unwraps the "cache data source" and returns the underlying source. Returns the + * same as the input argument otherwise. + * + * @param dataSource the data source to unwrap if applicable + * @return the non-cache data source + */ + static DataSource unwrapSourceFromCacheDataSource(DataSource dataSource) { + if (dataSource instanceof CacheDataSource) { + try { + Field source = CacheDataSource.class.getDeclaredField("source"); + source.setAccessible(true); + return (DataSource) source.get(dataSource); + } catch (NoSuchFieldException | IllegalAccessException e) { + logger.logException("Could not get source of CacheDataSource:", e); + return null; + } + } + return dataSource; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/debug/HasPermissionChecker.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/debug/HasPermissionChecker.java new file mode 100644 index 00000000..e8a15b79 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/debug/HasPermissionChecker.java @@ -0,0 +1,138 @@ +package fr.xephi.authme.command.executable.authme.debug; + +import com.google.common.collect.ImmutableList; +import fr.xephi.authme.permission.AdminPermission; +import fr.xephi.authme.permission.DebugSectionPermissions; +import fr.xephi.authme.permission.DefaultPermission; +import fr.xephi.authme.permission.PermissionNode; +import fr.xephi.authme.permission.PermissionsManager; +import fr.xephi.authme.permission.PlayerPermission; +import fr.xephi.authme.permission.PlayerStatePermission; +import fr.xephi.authme.service.BukkitService; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.OfflinePlayer; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import javax.inject.Inject; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.function.BiFunction; + +/** + * Checks if a player has a given permission, as checked by AuthMe. + */ +class HasPermissionChecker implements DebugSection { + + static final List> PERMISSION_NODE_CLASSES = ImmutableList.of( + AdminPermission.class, PlayerPermission.class, PlayerStatePermission.class, DebugSectionPermissions.class); + + @Inject + private PermissionsManager permissionsManager; + + @Inject + private BukkitService bukkitService; + + @Override + public String getName() { + return "perm"; + } + + @Override + public String getDescription() { + return "Checks if a player has a given permission"; + } + + @Override + public void execute(CommandSender sender, List arguments) { + sender.sendMessage(ChatColor.BLUE + "AuthMe permission check"); + if (arguments.size() < 2) { + sender.sendMessage("Check if a player has permission:"); + sender.sendMessage("Example: /authme debug perm bobby my.perm.node"); + sender.sendMessage("Permission system type used: " + permissionsManager.getPermissionSystem()); + return; + } + + final String playerName = arguments.get(0); + final String permissionNode = arguments.get(1); + + Player player = bukkitService.getPlayerExact(playerName); + if (player == null) { + OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(playerName); + if (offlinePlayer == null) { + sender.sendMessage(ChatColor.DARK_RED + "Player '" + playerName + "' does not exist"); + } else { + sender.sendMessage("Player '" + playerName + "' not online; checking with offline player"); + performPermissionCheck(offlinePlayer, permissionNode, permissionsManager::hasPermissionOffline, sender); + } + } else { + performPermissionCheck(player, permissionNode, permissionsManager::hasPermission, sender); + } + } + + @Override + public PermissionNode getRequiredPermission() { + return DebugSectionPermissions.HAS_PERMISSION_CHECK; + } + + /** + * Performs a permission check and informs the given sender of the result. {@code permissionChecker} is the + * permission check to perform with the given {@code node} and the {@code player}. + * + * @param player the player to check a permission for + * @param node the node of the permission to check + * @param permissionChecker permission checking function + * @param sender the sender to inform of the result + * @param

the player type + */ + private static

void performPermissionCheck( + P player, String node, BiFunction permissionChecker, CommandSender sender) { + + PermissionNode permNode = getPermissionNode(sender, node); + if (permissionChecker.apply(player, permNode)) { + sender.sendMessage(ChatColor.DARK_GREEN + "Success: player '" + player.getName() + + "' has permission '" + node + "'"); + } else { + sender.sendMessage(ChatColor.DARK_RED + "Check failed: player '" + player.getName() + + "' does NOT have permission '" + node + "'"); + } + } + + /** + * Based on the given permission node (String), tries to find the according AuthMe {@link PermissionNode} + * instance, or creates a new one if not available. + * + * @param sender the sender (used to inform him if no AuthMe PermissionNode can be matched) + * @param node the node to search for + * @return the node as {@link PermissionNode} object + */ + private static PermissionNode getPermissionNode(CommandSender sender, String node) { + Optional permNode = PERMISSION_NODE_CLASSES.stream() + .map(Class::getEnumConstants) + .flatMap(Arrays::stream) + .filter(perm -> perm.getNode().equals(node)) + .findFirst(); + if (permNode.isPresent()) { + return permNode.get(); + } else { + sender.sendMessage("Did not detect AuthMe permission; using default permission = DENIED"); + return createPermNode(node); + } + } + + private static PermissionNode createPermNode(String node) { + return new PermissionNode() { + @Override + public String getNode() { + return node; + } + + @Override + public DefaultPermission getDefaultPermission() { + return DefaultPermission.NOT_ALLOWED; + } + }; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/debug/InputValidator.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/debug/InputValidator.java new file mode 100644 index 00000000..2e82c3c8 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/debug/InputValidator.java @@ -0,0 +1,124 @@ +package fr.xephi.authme.command.executable.authme.debug; + +import fr.xephi.authme.listener.FailedVerificationException; +import fr.xephi.authme.listener.OnJoinVerifier; +import fr.xephi.authme.message.Messages; +import fr.xephi.authme.permission.DebugSectionPermissions; +import fr.xephi.authme.permission.PermissionNode; +import fr.xephi.authme.service.ValidationService; +import fr.xephi.authme.service.ValidationService.ValidationResult; +import org.bukkit.ChatColor; +import org.bukkit.command.CommandSender; + +import javax.inject.Inject; +import java.util.Arrays; +import java.util.List; + +import static fr.xephi.authme.command.executable.authme.debug.InputValidator.ValidationObject.MAIL; +import static fr.xephi.authme.command.executable.authme.debug.InputValidator.ValidationObject.NAME; +import static fr.xephi.authme.command.executable.authme.debug.InputValidator.ValidationObject.PASS; + +/** + * Checks if a sample username, email or password is valid according to the AuthMe settings. + */ +class InputValidator implements DebugSection { + + @Inject + private ValidationService validationService; + + @Inject + private Messages messages; + + @Inject + private OnJoinVerifier onJoinVerifier; + + + @Override + public String getName() { + return "valid"; + } + + @Override + public String getDescription() { + return "Checks if your config.yml allows a password / email"; + } + + @Override + public void execute(CommandSender sender, List arguments) { + if (arguments.size() < 2 || !ValidationObject.matchesAny(arguments.get(0))) { + displayUsageHint(sender); + + } else if (PASS.matches(arguments.get(0))) { + validatePassword(sender, arguments.get(1)); + + } else if (MAIL.matches(arguments.get(0))) { + validateEmail(sender, arguments.get(1)); + + } else if (NAME.matches(arguments.get(0))) { + validateUsername(sender, arguments.get(1)); + + } else { + throw new IllegalStateException("Unexpected validation object with arg[0] = '" + arguments.get(0) + "'"); + } + } + + @Override + public PermissionNode getRequiredPermission() { + return DebugSectionPermissions.INPUT_VALIDATOR; + } + + private void displayUsageHint(CommandSender sender) { + sender.sendMessage(ChatColor.BLUE + "Validation tests"); + sender.sendMessage("You can define forbidden emails and passwords in your config.yml." + + " You can test your settings with this command."); + final String command = ChatColor.GOLD + "/authme debug valid"; + sender.sendMessage(" Use " + command + " pass " + ChatColor.RESET + " to check a password"); + sender.sendMessage(" Use " + command + " mail " + ChatColor.RESET + " to check an email"); + sender.sendMessage(" Use " + command + " name " + ChatColor.RESET + " to check a username"); + } + + private void validatePassword(CommandSender sender, String password) { + ValidationResult validationResult = validationService.validatePassword(password, ""); + sender.sendMessage(ChatColor.BLUE + "Validation of password '" + password + "'"); + if (validationResult.hasError()) { + messages.send(sender, validationResult.getMessageKey(), validationResult.getArgs()); + } else { + sender.sendMessage(ChatColor.DARK_GREEN + "Valid password!"); + } + } + + private void validateEmail(CommandSender sender, String email) { + boolean isValidEmail = validationService.validateEmail(email); + sender.sendMessage(ChatColor.BLUE + "Validation of email '" + email + "'"); + if (isValidEmail) { + sender.sendMessage(ChatColor.DARK_GREEN + "Valid email!"); + } else { + sender.sendMessage(ChatColor.DARK_RED + "Email is not valid!"); + } + } + + private void validateUsername(CommandSender sender, String username) { + sender.sendMessage(ChatColor.BLUE + "Validation of username '" + username + "'"); + try { + onJoinVerifier.checkIsValidName(username); + sender.sendMessage("Valid username!"); + } catch (FailedVerificationException failedVerificationEx) { + messages.send(sender, failedVerificationEx.getReason(), failedVerificationEx.getArgs()); + } + } + + + enum ValidationObject { + + PASS, MAIL, NAME; + + static boolean matchesAny(String arg) { + return Arrays.stream(values()).anyMatch(vo -> vo.matches(arg)); + } + + boolean matches(String arg) { + return name().equalsIgnoreCase(arg); + } + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/debug/LimboPlayerViewer.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/debug/LimboPlayerViewer.java new file mode 100644 index 00000000..7338c868 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/debug/LimboPlayerViewer.java @@ -0,0 +1,143 @@ +package fr.xephi.authme.command.executable.authme.debug; + +import fr.xephi.authme.data.limbo.LimboPlayer; +import fr.xephi.authme.data.limbo.LimboService; +import fr.xephi.authme.data.limbo.persistence.LimboPersistence; +import fr.xephi.authme.permission.DebugSectionPermissions; +import fr.xephi.authme.permission.PermissionNode; +import fr.xephi.authme.permission.PermissionsManager; +import fr.xephi.authme.service.BukkitService; +import org.bukkit.ChatColor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import javax.inject.Inject; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; + +import static fr.xephi.authme.command.executable.authme.debug.DebugSectionUtils.applyToLimboPlayersMap; +import static fr.xephi.authme.command.executable.authme.debug.DebugSectionUtils.formatLocation; + +/** + * Shows the data stored in LimboPlayers and the equivalent properties on online players. + */ +class LimboPlayerViewer implements DebugSection { + + @Inject + private LimboService limboService; + + @Inject + private LimboPersistence limboPersistence; + + @Inject + private BukkitService bukkitService; + + @Inject + private PermissionsManager permissionsManager; + + @Override + public String getName() { + return "limbo"; + } + + @Override + public String getDescription() { + return "View LimboPlayers and player's \"limbo stats\""; + } + + @Override + public void execute(CommandSender sender, List arguments) { + if (arguments.isEmpty()) { + sender.sendMessage(ChatColor.BLUE + "AuthMe limbo viewer"); + sender.sendMessage("/authme debug limbo : show a player's limbo info"); + sender.sendMessage("Available limbo records: " + applyToLimboPlayersMap(limboService, Map::keySet)); + return; + } + + LimboPlayer memoryLimbo = limboService.getLimboPlayer(arguments.get(0)); + Player player = bukkitService.getPlayerExact(arguments.get(0)); + LimboPlayer diskLimbo = player != null ? limboPersistence.getLimboPlayer(player) : null; + if (memoryLimbo == null && player == null) { + sender.sendMessage(ChatColor.BLUE + "No AuthMe limbo data"); + sender.sendMessage("No limbo data and no player online with name '" + arguments.get(0) + "'"); + return; + } + + sender.sendMessage(ChatColor.BLUE + "Player / limbo / disk limbo info for '" + arguments.get(0) + "'"); + new InfoDisplayer(sender, player, memoryLimbo, diskLimbo) + .sendEntry("Is op", Player::isOp, LimboPlayer::isOperator) + .sendEntry("Walk speed", Player::getWalkSpeed, LimboPlayer::getWalkSpeed) + .sendEntry("Can fly", Player::getAllowFlight, LimboPlayer::isCanFly) + .sendEntry("Fly speed", Player::getFlySpeed, LimboPlayer::getFlySpeed) + .sendEntry("Location", p -> formatLocation(p.getLocation()), l -> formatLocation(l.getLocation())) + .sendEntry("Prim. group", + p -> permissionsManager.hasGroupSupport() ? permissionsManager.getPrimaryGroup(p) : "N/A", + LimboPlayer::getGroups); + } + + @Override + public PermissionNode getRequiredPermission() { + return DebugSectionPermissions.LIMBO_PLAYER_VIEWER; + } + + /** + * Displays the info for the given LimboPlayer and Player to the provided CommandSender. + */ + private static final class InfoDisplayer { + private final CommandSender sender; + private final Optional player; + private final Optional memoryLimbo; + private final Optional diskLimbo; + + /** + * Constructor. + * + * @param sender command sender to send the information to + * @param player the player to get data from + * @param memoryLimbo the limbo player to get data from + */ + InfoDisplayer(CommandSender sender, Player player, LimboPlayer memoryLimbo, LimboPlayer diskLimbo) { + this.sender = sender; + this.player = Optional.ofNullable(player); + this.memoryLimbo = Optional.ofNullable(memoryLimbo); + this.diskLimbo = Optional.ofNullable(diskLimbo); + + if (memoryLimbo == null) { + sender.sendMessage("Note: no Limbo information available"); + } + if (player == null) { + sender.sendMessage("Note: player is not online"); + } else if (diskLimbo == null) { + sender.sendMessage("Note: no Limbo on disk available"); + } + } + + /** + * Displays a piece of information to the command sender. + * + * @param title the designation of the piece of information + * @param playerGetter getter for data retrieval on Player + * @param limboGetter getter for data retrieval on the LimboPlayer + * @param the data type + * @return this instance (for chaining) + */ + InfoDisplayer sendEntry(String title, + Function playerGetter, + Function limboGetter) { + sender.sendMessage( + title + ": " + + getData(player, playerGetter) + + " / " + + getData(memoryLimbo, limboGetter) + + " / " + + getData(diskLimbo, limboGetter)); + return this; + } + + static String getData(Optional entity, Function getter) { + return entity.map(getter).map(String::valueOf).orElse(" -- "); + } + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/debug/MySqlDefaultChanger.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/debug/MySqlDefaultChanger.java new file mode 100644 index 00000000..d4eb1d3c --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/debug/MySqlDefaultChanger.java @@ -0,0 +1,327 @@ +package fr.xephi.authme.command.executable.authme.debug; + +import ch.jalu.configme.properties.Property; +import com.google.common.annotations.VisibleForTesting; +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.datasource.MySQL; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.permission.DebugSectionPermissions; +import fr.xephi.authme.permission.PermissionNode; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.properties.DatabaseSettings; +import org.bukkit.ChatColor; +import org.bukkit.command.CommandSender; + +import javax.annotation.PostConstruct; +import javax.inject.Inject; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static fr.xephi.authme.command.executable.authme.debug.DebugSectionUtils.castToTypeOrNull; +import static fr.xephi.authme.command.executable.authme.debug.DebugSectionUtils.unwrapSourceFromCacheDataSource; +import static fr.xephi.authme.data.auth.PlayerAuth.DB_EMAIL_DEFAULT; +import static fr.xephi.authme.data.auth.PlayerAuth.DB_LAST_IP_DEFAULT; +import static fr.xephi.authme.data.auth.PlayerAuth.DB_LAST_LOGIN_DEFAULT; +import static fr.xephi.authme.datasource.SqlDataSourceUtils.getColumnDefaultValue; +import static fr.xephi.authme.datasource.SqlDataSourceUtils.isNotNullColumn; +import static java.lang.String.format; + +/** + * Convenience command to add or remove the default value of a column and its nullable status + * in the MySQL data source. + */ +class MySqlDefaultChanger implements DebugSection { + + private static final String NOT_NULL_SUFFIX = ChatColor.DARK_AQUA + "@" + ChatColor.RESET; + private static final String DEFAULT_VALUE_SUFFIX = ChatColor.GOLD + "#" + ChatColor.RESET; + + private ConsoleLogger logger = ConsoleLoggerFactory.get(MySqlDefaultChanger.class); + + @Inject + private Settings settings; + + @Inject + private DataSource dataSource; + + private MySQL mySql; + + @PostConstruct + void setMySqlField() { + this.mySql = castToTypeOrNull(unwrapSourceFromCacheDataSource(this.dataSource), MySQL.class); + } + + @Override + public String getName() { + return "mysqldef"; + } + + @Override + public String getDescription() { + return "Add or remove the default value of MySQL columns"; + } + + @Override + public PermissionNode getRequiredPermission() { + return DebugSectionPermissions.MYSQL_DEFAULT_CHANGER; + } + + @Override + public void execute(CommandSender sender, List arguments) { + if (mySql == null) { + sender.sendMessage("Defaults can be changed for the MySQL data source only."); + return; + } + + Operation operation = matchToEnum(arguments, 0, Operation.class); + Columns column = matchToEnum(arguments, 1, Columns.class); + if (operation == Operation.DETAILS) { + showColumnDetails(sender); + } else if (operation == null || column == null) { + displayUsageHints(sender); + } else { + sender.sendMessage(ChatColor.BLUE + "[AuthMe] MySQL change '" + column + "'"); + try (Connection con = getConnection(mySql)) { + switch (operation) { + case ADD: + changeColumnToNotNullWithDefault(sender, column, con); + break; + case REMOVE: + removeNotNullAndDefault(sender, column, con); + break; + default: + throw new IllegalStateException("Unknown operation '" + operation + "'"); + } + } catch (SQLException | IllegalStateException e) { + logger.logException("Failed to perform MySQL default altering operation:", e); + } + } + } + + /** + * Adds a default value to the column definition and adds a {@code NOT NULL} constraint for + * the specified column. + * + * @param sender the command sender initiation the action + * @param column the column to modify + * @param con connection to the database + * @throws SQLException . + */ + private void changeColumnToNotNullWithDefault(CommandSender sender, Columns column, + Connection con) throws SQLException { + final String tableName = settings.getProperty(DatabaseSettings.MYSQL_TABLE); + final String columnName = settings.getProperty(column.getColumnNameProperty()); + + // Replace NULLs with future default value + String sql = format("UPDATE %s SET %s = ? WHERE %s IS NULL;", tableName, columnName, columnName); + int updatedRows; + try (PreparedStatement pst = con.prepareStatement(sql)) { + pst.setObject(1, column.getDefaultValue()); + updatedRows = pst.executeUpdate(); + } + sender.sendMessage("Replaced NULLs with default value ('" + column.getDefaultValue() + + "'), modifying " + updatedRows + " entries"); + + // Change column definition to NOT NULL version + try (Statement st = con.createStatement()) { + st.execute(format("ALTER TABLE %s MODIFY %s %s", tableName, columnName, column.getNotNullDefinition())); + sender.sendMessage("Changed column '" + columnName + "' to have NOT NULL constraint"); + } + + // Log success message + logger.info("Changed MySQL column '" + columnName + "' to be NOT NULL, as initiated by '" + + sender.getName() + "'"); + } + + /** + * Removes the {@code NOT NULL} constraint of a column definition and replaces rows with the + * default value to {@code NULL}. + * + * @param sender the command sender initiation the action + * @param column the column to modify + * @param con connection to the database + * @throws SQLException . + */ + private void removeNotNullAndDefault(CommandSender sender, Columns column, Connection con) throws SQLException { + final String tableName = settings.getProperty(DatabaseSettings.MYSQL_TABLE); + final String columnName = settings.getProperty(column.getColumnNameProperty()); + + // Change column definition to nullable version + try (Statement st = con.createStatement()) { + st.execute(format("ALTER TABLE %s MODIFY %s %s", tableName, columnName, column.getNullableDefinition())); + sender.sendMessage("Changed column '" + columnName + "' to allow nulls"); + } + + // Replace old default value with NULL + String sql = format("UPDATE %s SET %s = NULL WHERE %s = ?;", tableName, columnName, columnName); + int updatedRows; + try (PreparedStatement pst = con.prepareStatement(sql)) { + pst.setObject(1, column.getDefaultValue()); + updatedRows = pst.executeUpdate(); + } + sender.sendMessage("Replaced default value ('" + column.getDefaultValue() + + "') to be NULL, modifying " + updatedRows + " entries"); + + // Log success message + logger.info("Changed MySQL column '" + columnName + "' to allow NULL, as initiated by '" + + sender.getName() + "'"); + } + + /** + * Outputs the current definitions of all {@link Columns} which can be migrated. + * + * @param sender command sender to output the data to + */ + private void showColumnDetails(CommandSender sender) { + sender.sendMessage(ChatColor.BLUE + "MySQL column details"); + final String tableName = settings.getProperty(DatabaseSettings.MYSQL_TABLE); + try (Connection con = getConnection(mySql)) { + final DatabaseMetaData metaData = con.getMetaData(); + for (Columns col : Columns.values()) { + String columnName = settings.getProperty(col.getColumnNameProperty()); + String isNullText = isNotNullColumn(metaData, tableName, columnName) ? "NOT NULL" : "nullable"; + Object defaultValue = getColumnDefaultValue(metaData, tableName, columnName); + String defaultText = defaultValue == null ? "no default" : "default: '" + defaultValue + "'"; + sender.sendMessage(formatColumnWithMetadata(col, metaData, tableName) + + " (" + columnName + "): " + isNullText + ", " + defaultText); + } + } catch (SQLException e) { + logger.logException("Failed while showing column details:", e); + sender.sendMessage("Failed while showing column details. See log for info"); + } + + } + + /** + * Displays sample commands and the list of columns that can be changed. + * + * @param sender the sender issuing the command + */ + private void displayUsageHints(CommandSender sender) { + sender.sendMessage(ChatColor.BLUE + "MySQL column changer"); + sender.sendMessage("Adds or removes a NOT NULL constraint for a column."); + sender.sendMessage("Examples: add a NOT NULL constraint with"); + sender.sendMessage(" /authme debug mysqldef add "); + sender.sendMessage("Remove one with /authme debug mysqldef remove "); + + sender.sendMessage("Available columns: " + constructColumnListWithMetadata()); + sender.sendMessage(" " + NOT_NULL_SUFFIX + ": not-null, " + DEFAULT_VALUE_SUFFIX + + ": has default. See /authme debug mysqldef details"); + } + + /** + * @return list of {@link Columns} we can toggle with suffixes indicating their NOT NULL and default value status + */ + private String constructColumnListWithMetadata() { + try (Connection con = getConnection(mySql)) { + final DatabaseMetaData metaData = con.getMetaData(); + final String tableName = settings.getProperty(DatabaseSettings.MYSQL_TABLE); + + List formattedColumns = new ArrayList<>(Columns.values().length); + for (Columns col : Columns.values()) { + formattedColumns.add(formatColumnWithMetadata(col, metaData, tableName)); + } + return String.join(ChatColor.RESET + ", ", formattedColumns); + } catch (SQLException e) { + logger.logException("Failed to construct column list:", e); + return ChatColor.RED + "An error occurred! Please see the console for details."; + } + } + + private String formatColumnWithMetadata(Columns column, DatabaseMetaData metaData, + String tableName) throws SQLException { + String columnName = settings.getProperty(column.getColumnNameProperty()); + boolean isNotNull = isNotNullColumn(metaData, tableName, columnName); + boolean hasDefaultValue = getColumnDefaultValue(metaData, tableName, columnName) != null; + return column.name() + + (isNotNull ? NOT_NULL_SUFFIX : "") + + (hasDefaultValue ? DEFAULT_VALUE_SUFFIX : ""); + } + + /** + * Gets the Connection object from the MySQL data source. + * + * @param mySql the MySQL data source to get the connection from + * @return the connection + */ + @VisibleForTesting + Connection getConnection(MySQL mySql) { + try { + Method method = MySQL.class.getDeclaredMethod("getConnection"); + method.setAccessible(true); + return (Connection) method.invoke(mySql); + } catch (IllegalAccessException | NoSuchMethodException | InvocationTargetException e) { + throw new IllegalStateException("Could not get MySQL connection", e); + } + } + + private static > E matchToEnum(List arguments, int index, Class clazz) { + if (arguments.size() <= index) { + return null; + } + String str = arguments.get(index); + return Arrays.stream(clazz.getEnumConstants()) + .filter(e -> e.name().equalsIgnoreCase(str)) + .findFirst().orElse(null); + } + + private enum Operation { + ADD, REMOVE, DETAILS + } + + /** MySQL columns which can be toggled between being NOT NULL and allowing NULL values. */ + enum Columns { + + LASTLOGIN(DatabaseSettings.MYSQL_COL_LASTLOGIN, + "BIGINT", "BIGINT NOT NULL DEFAULT 0", DB_LAST_LOGIN_DEFAULT), + + LASTIP(DatabaseSettings.MYSQL_COL_LAST_IP, + "VARCHAR(40) CHARACTER SET ascii COLLATE ascii_bin", + "VARCHAR(40) CHARACTER SET ascii COLLATE ascii_bin NOT NULL DEFAULT '127.0.0.1'", + DB_LAST_IP_DEFAULT), + + EMAIL(DatabaseSettings.MYSQL_COL_EMAIL, + "VARCHAR(255)", "VARCHAR(255) NOT NULL DEFAULT 'your@email.com'", DB_EMAIL_DEFAULT); + + private final Property columnNameProperty; + private final String nullableDefinition; + private final String notNullDefinition; + private final Object defaultValue; + + Columns(Property columnNameProperty, String nullableDefinition, + String notNullDefinition, Object defaultValue) { + this.columnNameProperty = columnNameProperty; + this.nullableDefinition = nullableDefinition; + this.notNullDefinition = notNullDefinition; + this.defaultValue = defaultValue; + } + + /** @return property defining the column name in the database */ + Property getColumnNameProperty() { + return columnNameProperty; + } + + /** @return SQL definition of the column allowing NULL values */ + String getNullableDefinition() { + return nullableDefinition; + } + + /** @return SQL definition of the column with a NOT NULL constraint */ + String getNotNullDefinition() { + return notNullDefinition; + } + + /** @return the default value used in {@link #notNullDefinition} */ + Object getDefaultValue() { + return defaultValue; + } + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/debug/PermissionGroups.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/debug/PermissionGroups.java new file mode 100644 index 00000000..4515ca0b --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/debug/PermissionGroups.java @@ -0,0 +1,56 @@ +package fr.xephi.authme.command.executable.authme.debug; + +import fr.xephi.authme.data.limbo.UserGroup; +import fr.xephi.authme.permission.DebugSectionPermissions; +import fr.xephi.authme.permission.PermissionNode; +import fr.xephi.authme.permission.PermissionsManager; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import javax.inject.Inject; +import java.util.List; + +import static java.util.stream.Collectors.toList; + +/** + * Outputs the permission groups of a player. + */ +class PermissionGroups implements DebugSection { + + @Inject + private PermissionsManager permissionsManager; + + @Override + public String getName() { + return "groups"; + } + + @Override + public String getDescription() { + return "Show permission groups a player belongs to"; + } + + @Override + public void execute(CommandSender sender, List arguments) { + sender.sendMessage(ChatColor.BLUE + "AuthMe permission groups"); + String name = arguments.isEmpty() ? sender.getName() : arguments.get(0); + Player player = Bukkit.getPlayer(name); + if (player == null) { + sender.sendMessage("Player " + name + " could not be found"); + } else { + List groupNames = permissionsManager.getGroups(player).stream() + .map(UserGroup::getGroupName) + .collect(toList()); + + sender.sendMessage("Player " + name + " has permission groups: " + String.join(", ", groupNames)); + sender.sendMessage("Primary group is: " + permissionsManager.getGroups(player)); + } + } + + @Override + public PermissionNode getRequiredPermission() { + return DebugSectionPermissions.PERM_GROUPS; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/debug/PlayerAuthViewer.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/debug/PlayerAuthViewer.java new file mode 100644 index 00000000..99115cfe --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/debug/PlayerAuthViewer.java @@ -0,0 +1,117 @@ +package fr.xephi.authme.command.executable.authme.debug; + +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.permission.DebugSectionPermissions; +import fr.xephi.authme.permission.PermissionNode; +import fr.xephi.authme.security.crypts.HashedPassword; +import fr.xephi.authme.util.StringUtils; +import org.bukkit.ChatColor; +import org.bukkit.command.CommandSender; + +import javax.inject.Inject; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.List; + +import static fr.xephi.authme.command.executable.authme.debug.DebugSectionUtils.formatLocation; + +/** + * Allows to view the data of a PlayerAuth in the database. + */ +class PlayerAuthViewer implements DebugSection { + + @Inject + private DataSource dataSource; + + @Override + public String getName() { + return "db"; + } + + @Override + public String getDescription() { + return "View player's data in the database"; + } + + @Override + public void execute(CommandSender sender, List arguments) { + if (arguments.isEmpty()) { + sender.sendMessage(ChatColor.BLUE + "AuthMe database viewer"); + sender.sendMessage("Enter player name to view his data in the database."); + sender.sendMessage("Example: /authme debug db Bobby"); + return; + } + + PlayerAuth auth = dataSource.getAuth(arguments.get(0)); + if (auth == null) { + sender.sendMessage(ChatColor.BLUE + "AuthMe database viewer"); + sender.sendMessage("No record exists for '" + arguments.get(0) + "'"); + } else { + displayAuthToSender(auth, sender); + } + } + + @Override + public PermissionNode getRequiredPermission() { + return DebugSectionPermissions.PLAYER_AUTH_VIEWER; + } + + /** + * Outputs the PlayerAuth information to the given sender. + * + * @param auth the PlayerAuth to display + * @param sender the sender to send the messages to + */ + private void displayAuthToSender(PlayerAuth auth, CommandSender sender) { + sender.sendMessage(ChatColor.BLUE + "[AuthMe] Player " + auth.getNickname() + " / " + auth.getRealName()); + sender.sendMessage("Email: " + auth.getEmail() + ". IP: " + auth.getLastIp() + ". Group: " + auth.getGroupId()); + sender.sendMessage("Quit location: " + + formatLocation(auth.getQuitLocX(), auth.getQuitLocY(), auth.getQuitLocZ(), auth.getWorld())); + sender.sendMessage("Last login: " + formatDate(auth.getLastLogin())); + sender.sendMessage("Registration: " + formatDate(auth.getRegistrationDate()) + + " with IP " + auth.getRegistrationIp()); + + HashedPassword hashedPass = auth.getPassword(); + sender.sendMessage("Hash / salt (partial): '" + safeSubstring(hashedPass.getHash(), 6) + + "' / '" + safeSubstring(hashedPass.getSalt(), 4) + "'"); + sender.sendMessage("TOTP code (partial): '" + safeSubstring(auth.getTotpKey(), 3) + "'"); + } + + /** + * Fail-safe substring method. Guarantees not to show the entire String. + * + * @param str the string to transform + * @param length number of characters to show from the start of the String + * @return the first length characters of the string, or half of the string if it is shorter, + * or empty string if the string is null or empty + */ + private static String safeSubstring(String str, int length) { + if (StringUtils.isBlank(str)) { + return ""; + } else if (str.length() < length) { + return str.substring(0, str.length() / 2) + "..."; + } else { + return str.substring(0, length) + "..."; + } + } + + /** + * Formats the given timestamp to a human readable date. + * + * @param timestamp the timestamp to format (nullable) + * @return the formatted timestamp + */ + private static String formatDate(Long timestamp) { + if (timestamp == null) { + return "Not available (null)"; + } else if (timestamp == 0) { + return "Not available (0)"; + } else { + LocalDateTime date = LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneId.systemDefault()); + return DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(date); + } + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/debug/SpawnLocationViewer.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/debug/SpawnLocationViewer.java new file mode 100644 index 00000000..30f756c3 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/debug/SpawnLocationViewer.java @@ -0,0 +1,87 @@ +package fr.xephi.authme.command.executable.authme.debug; + +import fr.xephi.authme.permission.DebugSectionPermissions; +import fr.xephi.authme.permission.PermissionNode; +import fr.xephi.authme.service.BukkitService; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.SpawnLoader; +import fr.xephi.authme.settings.properties.RestrictionSettings; +import org.bukkit.ChatColor; +import org.bukkit.Location; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import javax.inject.Inject; +import java.util.List; + +import static fr.xephi.authme.command.executable.authme.debug.DebugSectionUtils.formatLocation; + +/** + * Shows the spawn location that AuthMe is configured to use. + */ +class SpawnLocationViewer implements DebugSection { + + @Inject + private SpawnLoader spawnLoader; + + @Inject + private Settings settings; + + @Inject + private BukkitService bukkitService; + + + @Override + public String getName() { + return "spawn"; + } + + @Override + public String getDescription() { + return "Shows the spawn location that AuthMe will use"; + } + + @Override + public void execute(CommandSender sender, List arguments) { + sender.sendMessage(ChatColor.BLUE + "AuthMe spawn location viewer"); + if (arguments.isEmpty()) { + showGeneralInfo(sender); + } else if ("?".equals(arguments.get(0))) { + showHelp(sender); + } else { + showPlayerSpawn(sender, arguments.get(0)); + } + } + + @Override + public PermissionNode getRequiredPermission() { + return DebugSectionPermissions.SPAWN_LOCATION; + } + + private void showGeneralInfo(CommandSender sender) { + sender.sendMessage("Spawn priority: " + + String.join(", ", settings.getProperty(RestrictionSettings.SPAWN_PRIORITY))); + sender.sendMessage("AuthMe spawn location: " + formatLocation(spawnLoader.getSpawn())); + sender.sendMessage("AuthMe first spawn location: " + formatLocation(spawnLoader.getFirstSpawn())); + sender.sendMessage("AuthMe (first)spawn are only used depending on the configured priority!"); + sender.sendMessage("Use '/authme debug spawn ?' for further help"); + } + + private void showHelp(CommandSender sender) { + sender.sendMessage("Use /authme spawn and /authme firstspawn to teleport to the spawns."); + sender.sendMessage("/authme set(first)spawn sets the (first) spawn to your current location."); + sender.sendMessage("Use /authme debug spawn to view where a player would be teleported to."); + sender.sendMessage("Read more at https://github.com/AuthMe/AuthMeReloaded/wiki/Spawn-Handling"); + } + + private void showPlayerSpawn(CommandSender sender, String playerName) { + Player player = bukkitService.getPlayerExact(playerName); + if (player == null) { + sender.sendMessage("Player '" + playerName + "' is not online!"); + } else { + Location spawn = spawnLoader.getSpawnLocation(player); + sender.sendMessage("Player '" + playerName + "' has spawn location: " + formatLocation(spawn)); + sender.sendMessage("Note: this check excludes the AuthMe firstspawn."); + } + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/debug/TestEmailSender.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/debug/TestEmailSender.java new file mode 100644 index 00000000..388a18af --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/authme/debug/TestEmailSender.java @@ -0,0 +1,125 @@ +package fr.xephi.authme.command.executable.authme.debug; + +import ch.jalu.datasourcecolumns.data.DataSourceValue; +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.mail.SendMailSsl; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.permission.DebugSectionPermissions; +import fr.xephi.authme.permission.PermissionNode; +import fr.xephi.authme.util.StringUtils; +import fr.xephi.authme.util.Utils; +import org.apache.commons.mail.EmailException; +import org.apache.commons.mail.HtmlEmail; +import org.bukkit.ChatColor; +import org.bukkit.Server; +import org.bukkit.command.CommandSender; + +import javax.inject.Inject; +import java.util.List; + +/** + * Sends out a test email. + */ +class TestEmailSender implements DebugSection { + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(TestEmailSender.class); + + @Inject + private DataSource dataSource; + + @Inject + private SendMailSsl sendMailSsl; + + @Inject + private Server server; + + + @Override + public String getName() { + return "mail"; + } + + @Override + public String getDescription() { + return "Sends out a test email"; + } + + @Override + public void execute(CommandSender sender, List arguments) { + sender.sendMessage(ChatColor.BLUE + "AuthMe test email sender"); + if (!sendMailSsl.hasAllInformation()) { + sender.sendMessage(ChatColor.RED + "You haven't set all required configurations in config.yml " + + "for sending emails. Please check your config.yml"); + return; + } + + String email = getEmail(sender, arguments); + + // getEmail() takes care of informing the sender of the error if email == null + if (email != null) { + boolean sendMail = sendTestEmail(email); + if (sendMail) { + sender.sendMessage("Test email sent to " + email + " with success"); + } else { + sender.sendMessage(ChatColor.RED + "Failed to send test mail to " + email + "; please check your logs"); + } + } + } + + @Override + public PermissionNode getRequiredPermission() { + return DebugSectionPermissions.TEST_EMAIL; + } + + /** + * Gets the email address to use based on the sender and the arguments. If the arguments are empty, + * we attempt to retrieve the email from the sender. If there is an argument, we verify that it is + * an email address. + * {@code null} is returned if no email address could be found. This method informs the sender of + * the specific error in such cases. + * + * @param sender the command sender + * @param arguments the provided arguments + * @return the email to use, or null if none found + */ + private String getEmail(CommandSender sender, List arguments) { + if (arguments.isEmpty()) { + DataSourceValue emailResult = dataSource.getEmail(sender.getName()); + if (!emailResult.rowExists()) { + sender.sendMessage(ChatColor.RED + "Please provide an email address, " + + "e.g. /authme debug mail test@example.com"); + return null; + } + final String email = emailResult.getValue(); + if (Utils.isEmailEmpty(email)) { + sender.sendMessage(ChatColor.RED + "No email set for your account!" + + " Please use /authme debug mail "); + return null; + } + return email; + } else { + String email = arguments.get(0); + if (StringUtils.isInsideString('@', email)) { + return email; + } + sender.sendMessage(ChatColor.RED + "Invalid email! Usage: /authme debug mail test@example.com"); + return null; + } + } + + private boolean sendTestEmail(String email) { + HtmlEmail htmlEmail; + try { + htmlEmail = sendMailSsl.initializeMail(email); + } catch (EmailException e) { + logger.logException("Failed to create email for sample email:", e); + return false; + } + + htmlEmail.setSubject("AuthMe test email"); + String message = "Hello there!
This is a sample email sent to you from a Minecraft server (" + + server.getName() + ") via /authme debug mail. If you're seeing this, sending emails should be fine."; + return sendMailSsl.sendEmail(message, htmlEmail); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/captcha/CaptchaCommand.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/captcha/CaptchaCommand.java new file mode 100644 index 00000000..c486dd04 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/captcha/CaptchaCommand.java @@ -0,0 +1,86 @@ +package fr.xephi.authme.command.executable.captcha; + +import fr.xephi.authme.command.PlayerCommand; +import fr.xephi.authme.data.auth.PlayerCache; +import fr.xephi.authme.data.captcha.LoginCaptchaManager; +import fr.xephi.authme.data.captcha.RegistrationCaptchaManager; +import fr.xephi.authme.data.limbo.LimboMessageType; +import fr.xephi.authme.data.limbo.LimboService; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.service.CommonService; +import org.bukkit.entity.Player; + +import javax.inject.Inject; +import java.util.List; + +/** + * Captcha command, allowing a player to solve a captcha. + */ +public class CaptchaCommand extends PlayerCommand { + + @Inject + private PlayerCache playerCache; + + @Inject + private LoginCaptchaManager loginCaptchaManager; + + @Inject + private RegistrationCaptchaManager registrationCaptchaManager; + + @Inject + private CommonService commonService; + + @Inject + private LimboService limboService; + + @Inject + private DataSource dataSource; + + @Override + public void runCommand(Player player, List arguments) { + final String name = player.getName(); + + if (playerCache.isAuthenticated(name)) { + // No captcha is relevant if the player is logged in + commonService.send(player, MessageKey.ALREADY_LOGGED_IN_ERROR); + return; + } + + if (loginCaptchaManager.isCaptchaRequired(name)) { + checkLoginCaptcha(player, arguments.get(0)); + } else { + final boolean isPlayerRegistered = dataSource.isAuthAvailable(name); + if (!isPlayerRegistered && registrationCaptchaManager.isCaptchaRequired(name)) { + checkRegisterCaptcha(player, arguments.get(0)); + } else { + MessageKey errorMessage = isPlayerRegistered ? MessageKey.USAGE_LOGIN : MessageKey.USAGE_REGISTER; + commonService.send(player, errorMessage); + } + } + } + + private void checkLoginCaptcha(Player player, String captchaCode) { + final boolean isCorrectCode = loginCaptchaManager.checkCode(player, captchaCode); + if (isCorrectCode) { + commonService.send(player, MessageKey.CAPTCHA_SUCCESS); + commonService.send(player, MessageKey.LOGIN_MESSAGE); + limboService.unmuteMessageTask(player); + } else { + String newCode = loginCaptchaManager.getCaptchaCodeOrGenerateNew(player.getName()); + commonService.send(player, MessageKey.CAPTCHA_WRONG_ERROR, newCode); + } + } + + private void checkRegisterCaptcha(Player player, String captchaCode) { + final boolean isCorrectCode = registrationCaptchaManager.checkCode(player, captchaCode); + if (isCorrectCode) { + commonService.send(player, MessageKey.REGISTER_CAPTCHA_SUCCESS); + commonService.send(player, MessageKey.REGISTER_MESSAGE); + } else { + String newCode = registrationCaptchaManager.getCaptchaCodeOrGenerateNew(player.getName()); + commonService.send(player, MessageKey.CAPTCHA_WRONG_ERROR, newCode); + } + limboService.resetMessageTask(player, LimboMessageType.REGISTER); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/changepassword/ChangePasswordCommand.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/changepassword/ChangePasswordCommand.java new file mode 100644 index 00000000..83a0e5b2 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/changepassword/ChangePasswordCommand.java @@ -0,0 +1,74 @@ +package fr.xephi.authme.command.executable.changepassword; + +import fr.xephi.authme.command.PlayerCommand; +import fr.xephi.authme.data.VerificationCodeManager; +import fr.xephi.authme.data.auth.PlayerCache; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.process.Management; +import fr.xephi.authme.service.CommonService; +import fr.xephi.authme.service.ValidationService; +import fr.xephi.authme.service.ValidationService.ValidationResult; +import org.bukkit.entity.Player; + +import javax.inject.Inject; +import java.util.List; +import java.util.Locale; + +/** + * The command for a player to change his password with. + */ +public class ChangePasswordCommand extends PlayerCommand { + + @Inject + private CommonService commonService; + + @Inject + private PlayerCache playerCache; + + @Inject + private ValidationService validationService; + + @Inject + private Management management; + + @Inject + private VerificationCodeManager codeManager; + + @Override + public void runCommand(Player player, List arguments) { + String name = player.getName().toLowerCase(Locale.ROOT); + + if (!playerCache.isAuthenticated(name)) { + commonService.send(player, MessageKey.NOT_LOGGED_IN); + return; + } + // Check if the user has been verified or not + if (codeManager.isVerificationRequired(player)) { + codeManager.codeExistOrGenerateNew(name); + commonService.send(player, MessageKey.VERIFICATION_CODE_REQUIRED); + return; + } + + String oldPassword = arguments.get(0); + String newPassword = arguments.get(1); + + // Make sure the password is allowed + ValidationResult passwordValidation = validationService.validatePassword(newPassword, name); + if (passwordValidation.hasError()) { + commonService.send(player, passwordValidation.getMessageKey(), passwordValidation.getArgs()); + return; + } + + management.performPasswordChange(player, oldPassword, newPassword); + } + + @Override + protected String getAlternativeCommand() { + return "/authme password "; + } + + @Override + public MessageKey getArgumentsMismatchMessage() { + return MessageKey.USAGE_CHANGE_PASSWORD; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/email/AddEmailCommand.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/email/AddEmailCommand.java new file mode 100644 index 00000000..87e38104 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/email/AddEmailCommand.java @@ -0,0 +1,40 @@ +package fr.xephi.authme.command.executable.email; + +import fr.xephi.authme.command.PlayerCommand; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.process.Management; +import fr.xephi.authme.service.CommonService; +import org.bukkit.entity.Player; + +import javax.inject.Inject; +import java.util.List; + +/** + * Command for setting an email to an account. + */ +public class AddEmailCommand extends PlayerCommand { + + @Inject + private Management management; + + @Inject + private CommonService commonService; + + @Override + public void runCommand(Player player, List arguments) { + String email = arguments.get(0); + String emailConfirmation = arguments.get(1); + + if (email.equals(emailConfirmation)) { + // Closer inspection of the mail address handled by the async task + management.performAddEmail(player, email); + } else { + commonService.send(player, MessageKey.CONFIRM_EMAIL_MESSAGE); + } + } + + @Override + public MessageKey getArgumentsMismatchMessage() { + return MessageKey.USAGE_ADD_EMAIL; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/email/ChangeEmailCommand.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/email/ChangeEmailCommand.java new file mode 100644 index 00000000..a6d52e0a --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/email/ChangeEmailCommand.java @@ -0,0 +1,46 @@ +package fr.xephi.authme.command.executable.email; + +import fr.xephi.authme.command.PlayerCommand; +import fr.xephi.authme.data.VerificationCodeManager; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.process.Management; +import fr.xephi.authme.service.CommonService; +import org.bukkit.entity.Player; + +import javax.inject.Inject; +import java.util.List; + +/** + * Change email command. + */ +public class ChangeEmailCommand extends PlayerCommand { + + @Inject + private Management management; + + @Inject + private CommonService commonService; + + @Inject + private VerificationCodeManager codeManager; + + @Override + public void runCommand(Player player, List arguments) { + final String playerName = player.getName(); + // Check if the user has been verified or not + if (codeManager.isVerificationRequired(player)) { + codeManager.codeExistOrGenerateNew(playerName); + commonService.send(player, MessageKey.VERIFICATION_CODE_REQUIRED); + return; + } + + String playerMailOld = arguments.get(0); + String playerMailNew = arguments.get(1); + management.performChangeEmail(player, playerMailOld, playerMailNew); + } + + @Override + public MessageKey getArgumentsMismatchMessage() { + return MessageKey.USAGE_CHANGE_EMAIL; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/email/EmailBaseCommand.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/email/EmailBaseCommand.java new file mode 100644 index 00000000..0484e218 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/email/EmailBaseCommand.java @@ -0,0 +1,29 @@ +package fr.xephi.authme.command.executable.email; + +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; +import org.bukkit.command.CommandSender; + +import javax.inject.Inject; +import java.util.Collections; +import java.util.List; + +/** + * Base command for /email, showing information about the child commands. + */ +public class EmailBaseCommand implements ExecutableCommand { + + @Inject + private CommandMapper commandMapper; + + @Inject + private HelpProvider helpProvider; + + @Override + public void executeCommand(CommandSender sender, List arguments) { + FoundCommandResult result = commandMapper.mapPartsToCommand(sender, Collections.singletonList("email")); + helpProvider.outputHelp(sender, result, HelpProvider.SHOW_CHILDREN); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/email/EmailSetPasswordCommand.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/email/EmailSetPasswordCommand.java new file mode 100644 index 00000000..f3979adc --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/email/EmailSetPasswordCommand.java @@ -0,0 +1,61 @@ +package fr.xephi.authme.command.executable.email; + +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.command.PlayerCommand; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.security.PasswordSecurity; +import fr.xephi.authme.security.crypts.HashedPassword; +import fr.xephi.authme.service.CommonService; +import fr.xephi.authme.service.PasswordRecoveryService; +import fr.xephi.authme.service.ValidationService; +import fr.xephi.authme.service.ValidationService.ValidationResult; +import org.bukkit.entity.Player; + +import javax.inject.Inject; +import java.util.List; + +/** + * Command for changing password following successful recovery. + */ +public class EmailSetPasswordCommand extends PlayerCommand { + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(EmailSetPasswordCommand.class); + + @Inject + private DataSource dataSource; + + @Inject + private CommonService commonService; + + @Inject + private PasswordRecoveryService recoveryService; + + @Inject + private PasswordSecurity passwordSecurity; + + @Inject + private ValidationService validationService; + + @Override + protected void runCommand(Player player, List arguments) { + if (recoveryService.canChangePassword(player)) { + String name = player.getName(); + String password = arguments.get(0); + + ValidationResult result = validationService.validatePassword(password, name); + if (!result.hasError()) { + HashedPassword hashedPassword = passwordSecurity.computeHash(password, name); + dataSource.updatePassword(name, hashedPassword); + recoveryService.removeFromSuccessfulRecovery(player); + logger.info("Player '" + name + "' has changed their password from recovery"); + commonService.send(player, MessageKey.PASSWORD_CHANGED_SUCCESS); + } else { + commonService.send(player, result.getMessageKey(), result.getArgs()); + } + } else { + commonService.send(player, MessageKey.CHANGE_PASSWORD_EXPIRED); + } + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/email/ProcessCodeCommand.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/email/ProcessCodeCommand.java new file mode 100644 index 00000000..0883c18f --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/email/ProcessCodeCommand.java @@ -0,0 +1,46 @@ +package fr.xephi.authme.command.executable.email; + +import fr.xephi.authme.command.PlayerCommand; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.service.CommonService; +import fr.xephi.authme.service.PasswordRecoveryService; +import fr.xephi.authme.service.RecoveryCodeService; +import org.bukkit.entity.Player; + +import javax.inject.Inject; +import java.util.List; + +/** + * Command for submitting email recovery code. + */ +public class ProcessCodeCommand extends PlayerCommand { + + @Inject + private CommonService commonService; + + @Inject + private RecoveryCodeService codeService; + + @Inject + private PasswordRecoveryService recoveryService; + + @Override + protected void runCommand(Player player, List arguments) { + String name = player.getName(); + String code = arguments.get(0); + + if (codeService.hasTriesLeft(name)) { + if (codeService.isCodeValid(name, code)) { + commonService.send(player, MessageKey.RECOVERY_CODE_CORRECT); + recoveryService.addSuccessfulRecovery(player); + codeService.removeCode(name); + } else { + commonService.send(player, MessageKey.INCORRECT_RECOVERY_CODE, + Integer.toString(codeService.getTriesLeft(name))); + } + } else { + codeService.removeCode(name); + commonService.send(player, MessageKey.RECOVERY_TRIES_EXCEEDED); + } + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/email/RecoverEmailCommand.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/email/RecoverEmailCommand.java new file mode 100644 index 00000000..ebb71133 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/email/RecoverEmailCommand.java @@ -0,0 +1,91 @@ +package fr.xephi.authme.command.executable.email; + +import ch.jalu.datasourcecolumns.data.DataSourceValue; +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.command.PlayerCommand; +import fr.xephi.authme.data.auth.PlayerCache; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.mail.EmailService; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.service.BukkitService; +import fr.xephi.authme.service.CommonService; +import fr.xephi.authme.service.PasswordRecoveryService; +import fr.xephi.authme.service.RecoveryCodeService; +import fr.xephi.authme.util.Utils; +import org.bukkit.entity.Player; + +import javax.inject.Inject; +import java.util.List; + +/** + * Command for password recovery by email. + */ +public class RecoverEmailCommand extends PlayerCommand { + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(RecoverEmailCommand.class); + + @Inject + private CommonService commonService; + + @Inject + private DataSource dataSource; + + @Inject + private PlayerCache playerCache; + + @Inject + private EmailService emailService; + + @Inject + private PasswordRecoveryService recoveryService; + + @Inject + private RecoveryCodeService recoveryCodeService; + + @Inject + private BukkitService bukkitService; + + @Override + protected void runCommand(Player player, List arguments) { + final String playerMail = arguments.get(0); + final String playerName = player.getName(); + + if (!emailService.hasAllInformation()) { + logger.warning("Mail API is not set"); + commonService.send(player, MessageKey.INCOMPLETE_EMAIL_SETTINGS); + return; + } + if (playerCache.isAuthenticated(playerName)) { + commonService.send(player, MessageKey.ALREADY_LOGGED_IN_ERROR); + return; + } + + DataSourceValue emailResult = dataSource.getEmail(playerName); + if (!emailResult.rowExists()) { + commonService.send(player, MessageKey.USAGE_REGISTER); + return; + } + + final String email = emailResult.getValue(); + if (Utils.isEmailEmpty(email) || !email.equalsIgnoreCase(playerMail)) { + commonService.send(player, MessageKey.INVALID_EMAIL); + return; + } + + bukkitService.runTaskAsynchronously(() -> { + if (recoveryCodeService.isRecoveryCodeNeeded()) { + // Recovery code is needed; generate and send one + recoveryService.createAndSendRecoveryCode(player, email); + } else { + // Code not needed, just send them a new password + recoveryService.generateAndSendNewPassword(player, email); + } + }); + } + + @Override + public MessageKey getArgumentsMismatchMessage() { + return MessageKey.USAGE_RECOVER_EMAIL; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/email/ShowEmailCommand.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/email/ShowEmailCommand.java new file mode 100644 index 00000000..9e204987 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/email/ShowEmailCommand.java @@ -0,0 +1,48 @@ +package fr.xephi.authme.command.executable.email; + +import fr.xephi.authme.command.PlayerCommand; +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.data.auth.PlayerCache; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.service.CommonService; +import fr.xephi.authme.settings.properties.SecuritySettings; +import fr.xephi.authme.util.Utils; +import org.bukkit.entity.Player; + +import javax.inject.Inject; +import java.util.List; + +/** + * Show email command. + */ +public class ShowEmailCommand extends PlayerCommand { + + @Inject + private CommonService commonService; + + @Inject + private PlayerCache playerCache; + + @Override + public void runCommand(Player player, List arguments) { + PlayerAuth auth = playerCache.getAuth(player.getName()); + if (auth != null && !Utils.isEmailEmpty(auth.getEmail())) { + if (commonService.getProperty(SecuritySettings.USE_EMAIL_MASKING)){ + commonService.send(player, MessageKey.EMAIL_SHOW, emailMask(auth.getEmail())); + } else { + commonService.send(player, MessageKey.EMAIL_SHOW, auth.getEmail()); + } + } else { + commonService.send(player, MessageKey.SHOW_NO_EMAIL); + } + } + + private String emailMask(String email){ + String[] frag = email.split("@"); //Split id and domain + int sid = frag[0].length() / 3 + 1; //Define the id view (required length >= 1) + int sdomain = frag[1].length() / 3; //Define the domain view (required length >= 0) + String id = frag[0].substring(0, sid) + "***"; //Build the id + String domain = "***" + frag[1].substring(sdomain); //Build the domain + return id + "@" + domain; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/login/LoginCommand.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/login/LoginCommand.java new file mode 100644 index 00000000..57ae8639 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/login/LoginCommand.java @@ -0,0 +1,34 @@ +package fr.xephi.authme.command.executable.login; + +import fr.xephi.authme.command.PlayerCommand; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.process.Management; +import org.bukkit.entity.Player; + +import javax.inject.Inject; +import java.util.List; + +/** + * Login command. + */ +public class LoginCommand extends PlayerCommand { + + @Inject + private Management management; + + @Override + public void runCommand(Player player, List arguments) { + String password = arguments.get(0); + management.performLogin(player, password); + } + + @Override + public MessageKey getArgumentsMismatchMessage() { + return MessageKey.USAGE_LOGIN; + } + + @Override + protected String getAlternativeCommand() { + return "/authme forcelogin "; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/logout/LogoutCommand.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/logout/LogoutCommand.java new file mode 100644 index 00000000..83b17882 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/logout/LogoutCommand.java @@ -0,0 +1,22 @@ +package fr.xephi.authme.command.executable.logout; + +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; + +/** + * Logout command. + */ +public class LogoutCommand extends PlayerCommand { + + @Inject + private Management management; + + @Override + public void runCommand(Player player, List arguments) { + management.performLogout(player); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/register/RegisterCommand.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/register/RegisterCommand.java new file mode 100644 index 00000000..29e98333 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/register/RegisterCommand.java @@ -0,0 +1,222 @@ +package fr.xephi.authme.command.executable.register; + +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.command.PlayerCommand; +import fr.xephi.authme.data.captcha.RegistrationCaptchaManager; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.mail.EmailService; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.process.Management; +import fr.xephi.authme.process.register.RegisterSecondaryArgument; +import fr.xephi.authme.process.register.RegistrationType; +import fr.xephi.authme.process.register.executors.EmailRegisterParams; +import fr.xephi.authme.process.register.executors.PasswordRegisterParams; +import fr.xephi.authme.process.register.executors.RegistrationMethod; +import fr.xephi.authme.process.register.executors.TwoFactorRegisterParams; +import fr.xephi.authme.security.HashAlgorithm; +import fr.xephi.authme.service.BukkitService; +import fr.xephi.authme.service.CommonService; +import fr.xephi.authme.service.ValidationService; +import fr.xephi.authme.settings.properties.EmailSettings; +import fr.xephi.authme.settings.properties.RegistrationSettings; +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.process.register.RegisterSecondaryArgument.CONFIRMATION; +import static fr.xephi.authme.process.register.RegisterSecondaryArgument.EMAIL_MANDATORY; +import static fr.xephi.authme.process.register.RegisterSecondaryArgument.EMAIL_OPTIONAL; +import static fr.xephi.authme.process.register.RegisterSecondaryArgument.NONE; +import static fr.xephi.authme.settings.properties.RegistrationSettings.REGISTER_SECOND_ARGUMENT; + +/** + * Command for /register. + */ +public class RegisterCommand extends PlayerCommand { + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(RegisterCommand.class); + + @Inject + private Management management; + + @Inject + private CommonService commonService; + + @Inject + private BukkitService bukkitService; + + @Inject + private DataSource dataSource; + + @Inject + private EmailService emailService; + + @Inject + private ValidationService validationService; + + @Inject + private RegistrationCaptchaManager registrationCaptchaManager; + + @Override + public void runCommand(Player player, List arguments) { + if (!isCaptchaFulfilled(player)) { + return; // isCaptchaFulfilled handles informing the player on failure + } + + if (commonService.getProperty(SecuritySettings.PASSWORD_HASH) == HashAlgorithm.TWO_FACTOR) { + //for two factor auth we don't need to check the usage + management.performRegister(RegistrationMethod.TWO_FACTOR_REGISTRATION, + TwoFactorRegisterParams.of(player)); + return; + } else if (arguments.size() < 1) { + commonService.send(player, MessageKey.USAGE_REGISTER); + return; + } + + RegistrationType registrationType = commonService.getProperty(RegistrationSettings.REGISTRATION_TYPE); + if (registrationType == RegistrationType.PASSWORD) { + handlePasswordRegistration(player, arguments); + } else if (registrationType == RegistrationType.EMAIL) { + handleEmailRegistration(player, arguments); + } else { + throw new IllegalStateException("Unknown registration type '" + registrationType + "'"); + } + } + + @Override + protected String getAlternativeCommand() { + return "/authme register "; + } + + @Override + public MessageKey getArgumentsMismatchMessage() { + return MessageKey.USAGE_REGISTER; + } + + private boolean isCaptchaFulfilled(Player player) { + if (registrationCaptchaManager.isCaptchaRequired(player.getName())) { + String code = registrationCaptchaManager.getCaptchaCodeOrGenerateNew(player.getName()); + commonService.send(player, MessageKey.CAPTCHA_FOR_REGISTRATION_REQUIRED, code); + return false; + } + return true; + } + + private void handlePasswordRegistration(Player player, List arguments) { + if (isSecondArgValidForPasswordRegistration(player, arguments)) { + final String password = arguments.get(0); + final String email = getEmailIfAvailable(arguments); + + management.performRegister(RegistrationMethod.PASSWORD_REGISTRATION, + PasswordRegisterParams.of(player, password, email)); + } + } + + private String getEmailIfAvailable(List arguments) { + if (arguments.size() >= 2) { + RegisterSecondaryArgument secondArgType = commonService.getProperty(REGISTER_SECOND_ARGUMENT); + if (secondArgType == EMAIL_MANDATORY || secondArgType == EMAIL_OPTIONAL) { + return arguments.get(1); + } + } + return null; + } + + /** + * Verifies that the second argument is valid (based on the configuration) + * to perform a password registration. The player is informed if the check + * is unsuccessful. + * + * @param player the player to register + * @param arguments the provided arguments + * @return true if valid, false otherwise + */ + private boolean isSecondArgValidForPasswordRegistration(Player player, List arguments) { + RegisterSecondaryArgument secondArgType = commonService.getProperty(REGISTER_SECOND_ARGUMENT); + // cases where args.size < 2 + if (secondArgType == NONE || secondArgType == EMAIL_OPTIONAL && arguments.size() < 2) { + return true; + } else if (arguments.size() < 2) { + commonService.send(player, MessageKey.USAGE_REGISTER); + return false; + } + + if (secondArgType == CONFIRMATION) { + if (arguments.get(0).equals(arguments.get(1))) { + return true; + } else { + commonService.send(player, MessageKey.PASSWORD_MATCH_ERROR); + return false; + } + } else if (secondArgType == EMAIL_MANDATORY || secondArgType == EMAIL_OPTIONAL) { + if (validationService.validateEmail(arguments.get(1))) { + return true; + } else { + commonService.send(player, MessageKey.INVALID_EMAIL); + return false; + } + } else { + throw new IllegalStateException("Unknown secondary argument type '" + secondArgType + "'"); + } + } + + private void handleEmailRegistration(Player player, List arguments) { + if (!emailService.hasAllInformation()) { + commonService.send(player, MessageKey.INCOMPLETE_EMAIL_SETTINGS); + logger.warning("Cannot register player '" + player.getName() + "': no email or password is set " + + "to send emails from. Please adjust your config at " + EmailSettings.MAIL_ACCOUNT.getPath()); + return; + } + + final String email = arguments.get(0); + if (!validationService.validateEmail(email)) { + commonService.send(player, MessageKey.INVALID_EMAIL); + } else if (isSecondArgValidForEmailRegistration(player, arguments)) { + management.performRegister(RegistrationMethod.EMAIL_REGISTRATION, + EmailRegisterParams.of(player, email)); + if (commonService.getProperty(RegistrationSettings.UNREGISTER_ON_EMAIL_VERIFICATION_FAILURE) && commonService.getProperty(RegistrationSettings.UNREGISTER_AFTER_MINUTES) > 0) { + bukkitService.runTaskLater(player, () -> { + if (dataSource.getAuth(player.getName()) != null) { + if (dataSource.getAuth(player.getName()).getLastLogin() == null) { + management.performUnregisterByAdmin(null, player.getName(), player); + } + } + }, 60 * 20 * commonService.getProperty(RegistrationSettings.UNREGISTER_AFTER_MINUTES)); + } + } + } + + /** + * Verifies that the second argument is valid (based on the configuration) + * to perform an email registration. The player is informed if the check + * is unsuccessful. + * + * @param player the player to register + * @param arguments the provided arguments + * @return true if valid, false otherwise + */ + private boolean isSecondArgValidForEmailRegistration(Player player, List arguments) { + RegisterSecondaryArgument secondArgType = commonService.getProperty(REGISTER_SECOND_ARGUMENT); + // cases where args.size < 2 + if (secondArgType == NONE || secondArgType == EMAIL_OPTIONAL && arguments.size() < 2) { + return true; + } else if (arguments.size() < 2) { + commonService.send(player, MessageKey.USAGE_REGISTER); + return false; + } + + if (secondArgType == EMAIL_OPTIONAL || secondArgType == EMAIL_MANDATORY || secondArgType == CONFIRMATION) { + if (arguments.get(0).equals(arguments.get(1))) { + return true; + } else { + commonService.send(player, MessageKey.USAGE_REGISTER); + return false; + } + } else { + throw new IllegalStateException("Unknown secondary argument type '" + secondArgType + "'"); + } + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/totp/AddTotpCommand.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/totp/AddTotpCommand.java new file mode 100644 index 00000000..e8a78bd1 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/totp/AddTotpCommand.java @@ -0,0 +1,43 @@ +package fr.xephi.authme.command.executable.totp; + +import fr.xephi.authme.command.PlayerCommand; +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.data.auth.PlayerCache; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.message.Messages; +import fr.xephi.authme.security.totp.GenerateTotpService; +import fr.xephi.authme.security.totp.TotpAuthenticator.TotpGenerationResult; +import org.bukkit.entity.Player; + +import javax.inject.Inject; +import java.util.List; + +/** + * Command for a player to enable TOTP. + */ +public class AddTotpCommand extends PlayerCommand { + + @Inject + private GenerateTotpService generateTotpService; + + @Inject + private PlayerCache playerCache; + + @Inject + private Messages messages; + + @Override + protected void runCommand(Player player, List arguments) { + PlayerAuth auth = playerCache.getAuth(player.getName()); + if (auth == null) { + messages.send(player, MessageKey.NOT_LOGGED_IN); + } else if (auth.getTotpKey() == null) { + TotpGenerationResult createdTotpInfo = generateTotpService.generateTotpKey(player); + messages.send(player, MessageKey.TWO_FACTOR_CREATE, + createdTotpInfo.getTotpKey(), createdTotpInfo.getAuthenticatorQrCodeUrl()); + messages.send(player, MessageKey.TWO_FACTOR_CREATE_CONFIRMATION_REQUIRED); + } else { + messages.send(player, MessageKey.TWO_FACTOR_ALREADY_ENABLED); + } + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/totp/ConfirmTotpCommand.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/totp/ConfirmTotpCommand.java new file mode 100644 index 00000000..85d7bc8a --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/totp/ConfirmTotpCommand.java @@ -0,0 +1,74 @@ +package fr.xephi.authme.command.executable.totp; + +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.command.PlayerCommand; +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.data.auth.PlayerCache; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.message.Messages; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.security.totp.GenerateTotpService; +import fr.xephi.authme.security.totp.TotpAuthenticator.TotpGenerationResult; +import org.bukkit.entity.Player; + +import javax.inject.Inject; +import java.util.List; + +/** + * Command to enable TOTP by supplying the proper code as confirmation. + */ +public class ConfirmTotpCommand extends PlayerCommand { + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(ConfirmTotpCommand.class); + + @Inject + private GenerateTotpService generateTotpService; + + @Inject + private PlayerCache playerCache; + + @Inject + private DataSource dataSource; + + @Inject + private Messages messages; + + @Override + protected void runCommand(Player player, List arguments) { + PlayerAuth auth = playerCache.getAuth(player.getName()); + if (auth == null) { + messages.send(player, MessageKey.NOT_LOGGED_IN); + } else if (auth.getTotpKey() != null) { + messages.send(player, MessageKey.TWO_FACTOR_ALREADY_ENABLED); + } else { + verifyTotpCodeConfirmation(player, auth, arguments.get(0)); + } + } + + private void verifyTotpCodeConfirmation(Player player, PlayerAuth auth, String inputTotpCode) { + final TotpGenerationResult totpDetails = generateTotpService.getGeneratedTotpKey(player); + if (totpDetails == null) { + messages.send(player, MessageKey.TWO_FACTOR_ENABLE_ERROR_NO_CODE); + } else { + boolean isCodeValid = generateTotpService.isTotpCodeCorrectForGeneratedTotpKey(player, inputTotpCode); + if (isCodeValid) { + generateTotpService.removeGenerateTotpKey(player); + insertTotpKeyIntoDatabase(player, auth, totpDetails); + } else { + messages.send(player, MessageKey.TWO_FACTOR_ENABLE_ERROR_WRONG_CODE); + } + } + } + + private void insertTotpKeyIntoDatabase(Player player, PlayerAuth auth, TotpGenerationResult totpDetails) { + if (dataSource.setTotpKey(player.getName(), totpDetails.getTotpKey())) { + messages.send(player, MessageKey.TWO_FACTOR_ENABLE_SUCCESS); + auth.setTotpKey(totpDetails.getTotpKey()); + playerCache.updatePlayer(auth); + logger.info("Player '" + player.getName() + "' has successfully added a TOTP key to their account"); + } else { + messages.send(player, MessageKey.ERROR); + } + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/totp/RemoveTotpCommand.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/totp/RemoveTotpCommand.java new file mode 100644 index 00000000..649f13e4 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/totp/RemoveTotpCommand.java @@ -0,0 +1,62 @@ +package fr.xephi.authme.command.executable.totp; + +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.command.PlayerCommand; +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.data.auth.PlayerCache; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.message.Messages; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.security.totp.TotpAuthenticator; +import org.bukkit.entity.Player; + +import javax.inject.Inject; +import java.util.List; + +/** + * Command for a player to remove 2FA authentication. + */ +public class RemoveTotpCommand extends PlayerCommand { + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(RemoveTotpCommand.class); + + @Inject + private DataSource dataSource; + + @Inject + private PlayerCache playerCache; + + @Inject + private TotpAuthenticator totpAuthenticator; + + @Inject + private Messages messages; + + @Override + protected void runCommand(Player player, List arguments) { + PlayerAuth auth = playerCache.getAuth(player.getName()); + if (auth == null) { + messages.send(player, MessageKey.NOT_LOGGED_IN); + } else if (auth.getTotpKey() == null) { + messages.send(player, MessageKey.TWO_FACTOR_NOT_ENABLED_ERROR); + } else { + if (totpAuthenticator.checkCode(auth, arguments.get(0))) { + removeTotpKeyFromDatabase(player, auth); + } else { + messages.send(player, MessageKey.TWO_FACTOR_INVALID_CODE); + } + } + } + + private void removeTotpKeyFromDatabase(Player player, PlayerAuth auth) { + if (dataSource.removeTotpKey(auth.getNickname())) { + auth.setTotpKey(null); + playerCache.updatePlayer(auth); + messages.send(player, MessageKey.TWO_FACTOR_REMOVED_SUCCESS); + logger.info("Player '" + player.getName() + "' removed their TOTP key"); + } else { + messages.send(player, MessageKey.ERROR); + } + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/totp/TotpBaseCommand.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/totp/TotpBaseCommand.java new file mode 100644 index 00000000..2b170a03 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/totp/TotpBaseCommand.java @@ -0,0 +1,29 @@ +package fr.xephi.authme.command.executable.totp; + +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; +import org.bukkit.command.CommandSender; + +import javax.inject.Inject; +import java.util.Collections; +import java.util.List; + +/** + * Base command for /totp. + */ +public class TotpBaseCommand implements ExecutableCommand { + + @Inject + private CommandMapper commandMapper; + + @Inject + private HelpProvider helpProvider; + + @Override + public void executeCommand(CommandSender sender, List arguments) { + FoundCommandResult result = commandMapper.mapPartsToCommand(sender, Collections.singletonList("totp")); + helpProvider.outputHelp(sender, result, HelpProvider.SHOW_CHILDREN); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/totp/TotpCodeCommand.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/totp/TotpCodeCommand.java new file mode 100644 index 00000000..760b83ec --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/totp/TotpCodeCommand.java @@ -0,0 +1,79 @@ +package fr.xephi.authme.command.executable.totp; + +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.command.PlayerCommand; +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.data.auth.PlayerCache; +import fr.xephi.authme.data.limbo.LimboPlayer; +import fr.xephi.authme.data.limbo.LimboPlayerState; +import fr.xephi.authme.data.limbo.LimboService; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.message.Messages; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.process.login.AsynchronousLogin; +import fr.xephi.authme.security.totp.TotpAuthenticator; +import org.bukkit.entity.Player; + +import javax.inject.Inject; +import java.util.List; + +/** + * TOTP code command for processing the 2FA code during the login process. + */ +public class TotpCodeCommand extends PlayerCommand { + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(TotpCodeCommand.class); + + @Inject + private LimboService limboService; + + @Inject + private PlayerCache playerCache; + + @Inject + private Messages messages; + + @Inject + private TotpAuthenticator totpAuthenticator; + + @Inject + private DataSource dataSource; + + @Inject + private AsynchronousLogin asynchronousLogin; + + @Override + protected void runCommand(Player player, List arguments) { + if (playerCache.isAuthenticated(player.getName())) { + messages.send(player, MessageKey.ALREADY_LOGGED_IN_ERROR); + return; + } + + PlayerAuth auth = dataSource.getAuth(player.getName()); + if (auth == null) { + messages.send(player, MessageKey.REGISTER_MESSAGE); + return; + } + + LimboPlayer limbo = limboService.getLimboPlayer(player.getName()); + if (limbo != null && limbo.getState() == LimboPlayerState.TOTP_REQUIRED) { + processCode(player, auth, arguments.get(0)); + } else { + logger.debug(() -> "Aborting TOTP check for player '" + player.getName() + + "'. Invalid limbo state: " + (limbo == null ? "no limbo" : limbo.getState())); + messages.send(player, MessageKey.LOGIN_MESSAGE); + } + } + + private void processCode(Player player, PlayerAuth auth, String inputCode) { + boolean isCodeValid = totpAuthenticator.checkCode(auth, inputCode); + if (isCodeValid) { + logger.debug("Successfully checked TOTP code for `{0}`", player.getName()); + asynchronousLogin.performLogin(player, auth); + } else { + logger.debug("Input TOTP code was invalid for player `{0}`", player.getName()); + messages.send(player, MessageKey.TWO_FACTOR_INVALID_CODE); + } + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/unregister/UnregisterCommand.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/unregister/UnregisterCommand.java new file mode 100644 index 00000000..21993f38 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/unregister/UnregisterCommand.java @@ -0,0 +1,62 @@ +package fr.xephi.authme.command.executable.unregister; + +import fr.xephi.authme.command.PlayerCommand; +import fr.xephi.authme.data.VerificationCodeManager; +import fr.xephi.authme.data.auth.PlayerCache; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.process.Management; +import fr.xephi.authme.service.CommonService; +import org.bukkit.entity.Player; + +import javax.inject.Inject; +import java.util.List; + +/** + * Command for a player to unregister himself. + */ +public class UnregisterCommand extends PlayerCommand { + + @Inject + private Management management; + + @Inject + private CommonService commonService; + + @Inject + private PlayerCache playerCache; + + @Inject + private VerificationCodeManager codeManager; + + @Override + public void runCommand(Player player, List arguments) { + String playerPass = arguments.get(0); + String playerName = player.getName(); + + // Make sure the player is authenticated + if (!playerCache.isAuthenticated(playerName)) { + commonService.send(player, MessageKey.NOT_LOGGED_IN); + return; + } + + // Check if the user has been verified or not + if (codeManager.isVerificationRequired(player)) { + codeManager.codeExistOrGenerateNew(playerName); + commonService.send(player, MessageKey.VERIFICATION_CODE_REQUIRED); + return; + } + + // Unregister the player + management.performUnregister(player, playerPass); + } + + @Override + public MessageKey getArgumentsMismatchMessage() { + return MessageKey.USAGE_UNREGISTER; + } + + @Override + protected String getAlternativeCommand() { + return "/authme unregister "; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/verification/VerificationCommand.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/verification/VerificationCommand.java new file mode 100644 index 00000000..dc28c95f --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/executable/verification/VerificationCommand.java @@ -0,0 +1,61 @@ +package fr.xephi.authme.command.executable.verification; + +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.command.PlayerCommand; +import fr.xephi.authme.data.VerificationCodeManager; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.service.CommonService; +import org.bukkit.entity.Player; + +import javax.inject.Inject; +import java.util.List; + +/** + * Used to complete the email verification process. + */ +public class VerificationCommand extends PlayerCommand { + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(VerificationCommand.class); + + @Inject + private CommonService commonService; + + @Inject + private VerificationCodeManager codeManager; + + @Override + public void runCommand(Player player, List arguments) { + final String playerName = player.getName(); + + if (!codeManager.canSendMail()) { + logger.warning("Mail API is not set"); + commonService.send(player, MessageKey.INCOMPLETE_EMAIL_SETTINGS); + return; + } + + if (codeManager.isVerificationRequired(player)) { + if (codeManager.isCodeRequired(playerName)) { + if (codeManager.checkCode(playerName, arguments.get(0))) { + commonService.send(player, MessageKey.VERIFICATION_CODE_VERIFIED); + } else { + commonService.send(player, MessageKey.INCORRECT_VERIFICATION_CODE); + } + } else { + commonService.send(player, MessageKey.VERIFICATION_CODE_EXPIRED); + } + } else { + if (codeManager.hasEmail(playerName)) { + commonService.send(player, MessageKey.VERIFICATION_CODE_ALREADY_VERIFIED); + } else { + commonService.send(player, MessageKey.VERIFICATION_CODE_EMAIL_NEEDED); + commonService.send(player, MessageKey.ADD_EMAIL_MESSAGE); + } + } + } + + @Override + public MessageKey getArgumentsMismatchMessage() { + return MessageKey.USAGE_VERIFICATION_CODE; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/help/HelpMessage.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/help/HelpMessage.java new file mode 100644 index 00000000..ae306a40 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/help/HelpMessage.java @@ -0,0 +1,43 @@ +package fr.xephi.authme.command.help; + +/** + * Common, non-generic keys for messages used when showing command help. + * All keys are prefixed with {@code common}. + */ +public enum HelpMessage { + + HEADER("header"), + + OPTIONAL("optional"), + + HAS_PERMISSION("hasPermission"), + + NO_PERMISSION("noPermission"), + + DEFAULT("default"), + + RESULT("result"); + + private static final String PREFIX = "common."; + private final String key; + + /** + * Constructor. + * + * @param key the message key + */ + HelpMessage(String key) { + this.key = PREFIX + key; + } + + /** @return the message key */ + public String getKey() { + return key; + } + + /** @return the key without the common prefix */ + public String getEntryKey() { + // Note ljacqu 20171008: #getKey is called more often than this method, so we optimize for the former method + return key.substring(PREFIX.length()); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/help/HelpMessagesService.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/help/HelpMessagesService.java new file mode 100644 index 00000000..994d967b --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/help/HelpMessagesService.java @@ -0,0 +1,117 @@ +package fr.xephi.authme.command.help; + +import com.google.common.base.CaseFormat; +import fr.xephi.authme.command.CommandArgumentDescription; +import fr.xephi.authme.command.CommandDescription; +import fr.xephi.authme.command.CommandUtils; +import fr.xephi.authme.message.HelpMessagesFileHandler; +import fr.xephi.authme.permission.DefaultPermission; + +import javax.inject.Inject; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +/** + * Manages translatable help messages. + */ +public class HelpMessagesService { + + private static final String COMMAND_PREFIX = "commands."; + private static final String DESCRIPTION_SUFFIX = ".description"; + private static final String DETAILED_DESCRIPTION_SUFFIX = ".detailedDescription"; + private static final String DEFAULT_PERMISSIONS_PATH = "common.defaultPermissions."; + + private final HelpMessagesFileHandler helpMessagesFileHandler; + + @Inject + HelpMessagesService(HelpMessagesFileHandler helpMessagesFileHandler) { + this.helpMessagesFileHandler = helpMessagesFileHandler; + } + + /** + * Creates a copy of the supplied command description with localized messages where present. + * + * @param command the command to build a localized version of + * @return the localized description + */ + public CommandDescription buildLocalizedDescription(CommandDescription command) { + final String path = COMMAND_PREFIX + getCommandSubPath(command); + if (!helpMessagesFileHandler.hasSection(path)) { + // Messages file does not have a section for this command - return the provided command + return command; + } + + CommandDescription.CommandBuilder builder = CommandDescription.builder() + .description(getText(path + DESCRIPTION_SUFFIX, command::getDescription)) + .detailedDescription(getText(path + DETAILED_DESCRIPTION_SUFFIX, command::getDetailedDescription)) + .executableCommand(command.getExecutableCommand()) + .parent(command.getParent()) + .labels(command.getLabels()) + .permission(command.getPermission()); + + int i = 1; + for (CommandArgumentDescription argument : command.getArguments()) { + String argPath = path + ".arg" + i; + String label = getText(argPath + ".label", argument::getName); + String description = getText(argPath + ".description", argument::getDescription); + builder.withArgument(label, description, argument.isOptional()); + ++i; + } + + CommandDescription localCommand = builder.build(); + localCommand.getChildren().addAll(command.getChildren()); + return localCommand; + } + + public String getDescription(CommandDescription command) { + return getText(COMMAND_PREFIX + getCommandSubPath(command) + DESCRIPTION_SUFFIX, command::getDescription); + } + + public String getMessage(HelpMessage message) { + return helpMessagesFileHandler.getMessage(message.getKey()); + } + + public String getMessage(HelpSection section) { + return helpMessagesFileHandler.getMessage(section.getKey()); + } + + public String getMessage(DefaultPermission defaultPermission) { + // e.g. {default_permissions_path}.opOnly for DefaultPermission.OP_ONLY + String path = DEFAULT_PERMISSIONS_PATH + getDefaultPermissionsSubPath(defaultPermission); + return helpMessagesFileHandler.getMessage(path); + } + + public static String getDefaultPermissionsSubPath(DefaultPermission defaultPermission) { + return CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, defaultPermission.name()); + } + + private String getText(String path, Supplier defaultTextGetter) { + String message = helpMessagesFileHandler.getMessageIfExists(path); + return message == null + ? defaultTextGetter.get() + : message; + } + + + /** + * Triggers a reload of the help messages file. Note that this method is not needed + * to be called for /authme reload. + */ + public void reloadMessagesFile() { + helpMessagesFileHandler.reload(); + } + + /** + * Returns the command subpath for the given command (i.e. the path to the translations for the given + * command under "commands"). + * + * @param command the command to process + * @return the subpath for the command's texts + */ + public static String getCommandSubPath(CommandDescription command) { + return CommandUtils.constructParentList(command) + .stream() + .map(cmd -> cmd.getLabels().get(0)) + .collect(Collectors.joining(".")); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/help/HelpProvider.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/help/HelpProvider.java new file mode 100644 index 00000000..4327b827 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/help/HelpProvider.java @@ -0,0 +1,328 @@ +package fr.xephi.authme.command.help; + +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.CommandUtils; +import fr.xephi.authme.command.FoundCommandResult; +import fr.xephi.authme.initialization.Reloadable; +import fr.xephi.authme.permission.DefaultPermission; +import fr.xephi.authme.permission.PermissionNode; +import fr.xephi.authme.permission.PermissionsManager; +import org.bukkit.ChatColor; +import org.bukkit.command.CommandSender; + +import javax.inject.Inject; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.Function; + +import static fr.xephi.authme.command.help.HelpSection.DETAILED_DESCRIPTION; +import static fr.xephi.authme.command.help.HelpSection.SHORT_DESCRIPTION; +import static java.util.Collections.singletonList; + +/** + * Help syntax generator for AuthMe commands. + */ +public class HelpProvider implements Reloadable { + + // --- Bit flags --- + /** Set to show a command overview. */ + public static final int SHOW_COMMAND = 0x001; + /** Set to show the description of the command. */ + public static final int SHOW_DESCRIPTION = 0x002; + /** Set to show the detailed description of the command. */ + public static final int SHOW_LONG_DESCRIPTION = 0x004; + /** Set to include the arguments the command takes. */ + public static final int SHOW_ARGUMENTS = 0x008; + /** Set to show the permissions required to execute the command. */ + public static final int SHOW_PERMISSIONS = 0x010; + /** Set to show alternative labels for the command. */ + public static final int SHOW_ALTERNATIVES = 0x020; + /** Set to show the child commands of the command. */ + public static final int SHOW_CHILDREN = 0x040; + + /** Shortcut for setting all options. */ + public static final int ALL_OPTIONS = ~0; + + private final PermissionsManager permissionsManager; + private final HelpMessagesService helpMessagesService; + /** int with bit flags set corresponding to the above constants for enabled sections. */ + private Integer enabledSections; + + @Inject + HelpProvider(PermissionsManager permissionsManager, HelpMessagesService helpMessagesService) { + this.permissionsManager = permissionsManager; + this.helpMessagesService = helpMessagesService; + } + + /** + * Builds the help messages based on the provided arguments. + * + * @param sender the sender to evaluate permissions with + * @param result the command result to create help for + * @param options output options + * @return the generated help messages + */ + private List buildHelpOutput(CommandSender sender, FoundCommandResult result, int options) { + if (result.getCommandDescription() == null) { + return singletonList(ChatColor.DARK_RED + "Failed to retrieve any help information!"); + } + + List lines = new ArrayList<>(); + options = filterDisabledSections(options); + if (options == 0) { + // Return directly if no options are enabled so we don't include the help header + return lines; + } + String header = helpMessagesService.getMessage(HelpMessage.HEADER); + if (!header.isEmpty()) { + lines.add(ChatColor.GOLD + header); + } + + CommandDescription command = helpMessagesService.buildLocalizedDescription(result.getCommandDescription()); + List correctLabels = ImmutableList.copyOf(filterCorrectLabels(command, result.getLabels())); + + if (hasFlag(SHOW_COMMAND, options)) { + lines.add(ChatColor.GOLD + helpMessagesService.getMessage(HelpSection.COMMAND) + ": " + + CommandUtils.buildSyntax(command, correctLabels)); + } + if (hasFlag(SHOW_DESCRIPTION, options)) { + lines.add(ChatColor.GOLD + helpMessagesService.getMessage(SHORT_DESCRIPTION) + ": " + + ChatColor.WHITE + command.getDescription()); + } + if (hasFlag(SHOW_LONG_DESCRIPTION, options)) { + lines.add(ChatColor.GOLD + helpMessagesService.getMessage(DETAILED_DESCRIPTION) + ":"); + lines.add(ChatColor.WHITE + " " + command.getDetailedDescription()); + } + if (hasFlag(SHOW_ARGUMENTS, options)) { + addArgumentsInfo(command, lines); + } + if (hasFlag(SHOW_PERMISSIONS, options) && sender != null) { + addPermissionsInfo(command, sender, lines); + } + if (hasFlag(SHOW_ALTERNATIVES, options)) { + addAlternativesInfo(command, correctLabels, lines); + } + if (hasFlag(SHOW_CHILDREN, options)) { + addChildrenInfo(command, correctLabels, lines); + } + + return lines; + } + + /** + * Outputs 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 + */ + public void outputHelp(CommandSender sender, FoundCommandResult result, int options) { + List lines = buildHelpOutput(sender, result, options); + for (String line : lines) { + sender.sendMessage(line); + } + } + + @Override + public void reload() { + // We don't know about the reloading order of the classes, i.e. we cannot assume that HelpMessagesService + // has already been reloaded. So set the enabledSections flag to null and redefine it first time needed. + enabledSections = null; + } + + /** + * Removes any disabled sections from the options. Sections are considered disabled + * if the translated text for the section is empty. + * + * @param options the options to process + * @return the options without any disabled sections + */ + @SuppressWarnings("checkstyle:BooleanExpressionComplexity") + private int filterDisabledSections(int options) { + if (enabledSections == null) { + enabledSections = flagFor(HelpSection.COMMAND, SHOW_COMMAND) + | flagFor(HelpSection.SHORT_DESCRIPTION, SHOW_DESCRIPTION) + | flagFor(HelpSection.DETAILED_DESCRIPTION, SHOW_LONG_DESCRIPTION) + | flagFor(HelpSection.ARGUMENTS, SHOW_ARGUMENTS) + | flagFor(HelpSection.PERMISSIONS, SHOW_PERMISSIONS) + | flagFor(HelpSection.ALTERNATIVES, SHOW_ALTERNATIVES) + | flagFor(HelpSection.CHILDREN, SHOW_CHILDREN); + } + return options & enabledSections; + } + + private int flagFor(HelpSection section, int flag) { + return helpMessagesService.getMessage(section).isEmpty() ? 0 : flag; + } + + /** + * Adds help info about the given command's arguments into the provided list. + * + * @param command the command to generate arguments info for + * @param lines the output collection to add the info to + */ + private void addArgumentsInfo(CommandDescription command, List lines) { + if (command.getArguments().isEmpty()) { + return; + } + + lines.add(ChatColor.GOLD + helpMessagesService.getMessage(HelpSection.ARGUMENTS) + ":"); + StringBuilder argString = new StringBuilder(); + String optionalText = " (" + helpMessagesService.getMessage(HelpMessage.OPTIONAL) + ")"; + for (CommandArgumentDescription argument : command.getArguments()) { + argString.setLength(0); + argString.append(" ").append(ChatColor.YELLOW).append(ChatColor.ITALIC).append(argument.getName()) + .append(": ").append(ChatColor.WHITE).append(argument.getDescription()); + + if (argument.isOptional()) { + argString.append(ChatColor.GRAY).append(ChatColor.ITALIC).append(optionalText); + } + lines.add(argString.toString()); + } + } + + /** + * Adds help info about the given command's alternative labels into the provided list. + * + * @param command the command for which to generate info about its labels + * @param correctLabels labels used to access the command (sanitized) + * @param lines the output collection to add the info to + */ + private void addAlternativesInfo(CommandDescription command, List correctLabels, List lines) { + if (command.getLabels().size() <= 1) { + return; + } + + lines.add(ChatColor.GOLD + helpMessagesService.getMessage(HelpSection.ALTERNATIVES) + ":"); + + // Label with which the command was called -> don't show it as an alternative + final String usedLabel; + // Takes alternative label and constructs list of labels, e.g. "reg" -> [authme, reg] + final Function> commandLabelsFn; + + if (correctLabels.size() == 1) { + usedLabel = correctLabels.get(0); + commandLabelsFn = label -> singletonList(label); + } else { + usedLabel = correctLabels.get(1); + commandLabelsFn = label -> Arrays.asList(correctLabels.get(0), label); + } + + // Create a list of alternatives + for (String label : command.getLabels()) { + if (!label.equalsIgnoreCase(usedLabel)) { + lines.add(" " + CommandUtils.buildSyntax(command, commandLabelsFn.apply(label))); + } + } + } + + /** + * Adds help info about the given command's permissions into the provided list. + * + * @param command the command to generate permissions info for + * @param sender the command sender, used to evaluate permissions + * @param lines the output collection to add the info to + */ + private void addPermissionsInfo(CommandDescription command, CommandSender sender, List lines) { + PermissionNode permission = command.getPermission(); + if (permission == null) { + return; + } + lines.add(ChatColor.GOLD + helpMessagesService.getMessage(HelpSection.PERMISSIONS) + ":"); + + boolean hasPermission = permissionsManager.hasPermission(sender, permission); + lines.add(String.format(" " + ChatColor.YELLOW + ChatColor.ITALIC + "%s" + ChatColor.GRAY + " (%s)", + permission.getNode(), getLocalPermissionText(hasPermission))); + + // Addendum to the line to specify whether the sender has permission or not when default is OP_ONLY + final DefaultPermission defaultPermission = permission.getDefaultPermission(); + String addendum = ""; + if (DefaultPermission.OP_ONLY.equals(defaultPermission)) { + addendum = " (" + getLocalPermissionText(defaultPermission.evaluate(sender)) + ")"; + } + lines.add(ChatColor.GOLD + helpMessagesService.getMessage(HelpMessage.DEFAULT) + ": " + + ChatColor.GRAY + ChatColor.ITALIC + helpMessagesService.getMessage(defaultPermission) + addendum); + + // Evaluate if the sender has permission to the command + ChatColor permissionColor; + String permissionText; + if (permissionsManager.hasPermission(sender, command.getPermission())) { + permissionColor = ChatColor.GREEN; + permissionText = getLocalPermissionText(true); + } else { + permissionColor = ChatColor.DARK_RED; + permissionText = getLocalPermissionText(false); + } + lines.add(String.format(ChatColor.GOLD + " %s: %s" + ChatColor.ITALIC + "%s", + helpMessagesService.getMessage(HelpMessage.RESULT), permissionColor, permissionText)); + } + + private String getLocalPermissionText(boolean hasPermission) { + if (hasPermission) { + return helpMessagesService.getMessage(HelpMessage.HAS_PERMISSION); + } + return helpMessagesService.getMessage(HelpMessage.NO_PERMISSION); + } + + /** + * Adds help info about the given command's child command into the provided list. + * + * @param command the command for which to generate info about its child commands + * @param correctLabels the labels used to access the given command (sanitized) + * @param lines the output collection to add the info to + */ + private void addChildrenInfo(CommandDescription command, List correctLabels, List lines) { + if (command.getChildren().isEmpty()) { + return; + } + + lines.add(ChatColor.GOLD + helpMessagesService.getMessage(HelpSection.CHILDREN) + ":"); + String parentCommandPath = String.join(" ", correctLabels); + for (CommandDescription child : command.getChildren()) { + lines.add(" /" + parentCommandPath + " " + child.getLabels().get(0) + + ChatColor.GRAY + ChatColor.ITALIC + ": " + helpMessagesService.getDescription(child)); + } + } + + private static boolean hasFlag(int flag, int options) { + return (flag & options) != 0; + } + + /** + * Returns a list of labels for the given command, using the labels from the provided labels list + * as long as they are correct. + *

+ * Background: commands may have multiple labels (e.g. /authme register vs. /authme reg). It is interesting + * for us to keep with which label the user requested the command. At the same time, when a user inputs a + * non-existent label, we try to find the most similar one. This method keeps all labels that exists and will + * default to the command's first label when an invalid label is encountered. + *

+ * Examples: + * command = "authme register", labels = {authme, egister}. Output: {authme, register} + * command = "authme register", labels = {authme, reg}. Output: {authme, reg} + * + * @param command the command to compare the labels against + * @param labels the labels as input by the user + * @return list of correct labels, keeping the user's input where possible + */ + @VisibleForTesting + static List filterCorrectLabels(CommandDescription command, List labels) { + List commands = CommandUtils.constructParentList(command); + List correctLabels = new ArrayList<>(); + boolean foundIncorrectLabel = false; + for (int i = 0; i < commands.size(); ++i) { + if (!foundIncorrectLabel && i < labels.size() && commands.get(i).hasLabel(labels.get(i))) { + correctLabels.add(labels.get(i)); + } else { + foundIncorrectLabel = true; + correctLabels.add(commands.get(i).getLabels().get(0)); + } + } + return correctLabels; + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/help/HelpSection.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/help/HelpSection.java new file mode 100644 index 00000000..506262bf --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/command/help/HelpSection.java @@ -0,0 +1,44 @@ +package fr.xephi.authme.command.help; + +/** + * Translatable sections. Message keys are prefixed by {@code section}. + */ +public enum HelpSection { + + COMMAND("command"), + + SHORT_DESCRIPTION("description"), + + DETAILED_DESCRIPTION("detailedDescription"), + + ARGUMENTS("arguments"), + + ALTERNATIVES("alternatives"), + + PERMISSIONS("permissions"), + + CHILDREN("children"); + + private static final String PREFIX = "section."; + private final String key; + + /** + * Constructor. + * + * @param key the message key + */ + HelpSection(String key) { + this.key = PREFIX + key; + } + + /** @return the message key */ + public String getKey() { + return key; + } + + /** @return the key without the common prefix */ + public String getEntryKey() { + // Note ljacqu 20171008: #getKey is called more often than this method, so we optimize for the former method + return key.substring(PREFIX.length()); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/ProxySessionManager.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/ProxySessionManager.java new file mode 100644 index 00000000..d0c60fb2 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/ProxySessionManager.java @@ -0,0 +1,49 @@ +package fr.xephi.authme.data; + +import fr.xephi.authme.initialization.HasCleanup; +import fr.xephi.authme.util.expiring.ExpiringSet; + +import javax.inject.Inject; +import java.util.Locale; +import java.util.concurrent.TimeUnit; + +public class ProxySessionManager implements HasCleanup { + + private final ExpiringSet activeProxySessions; + + @Inject + public ProxySessionManager() { + long countTimeout = 5; + activeProxySessions = new ExpiringSet<>(countTimeout, TimeUnit.SECONDS); + } + + /** + * Saves the player in the set + * @param name the player's name + */ + private void setActiveSession(String name) { + activeProxySessions.add(name.toLowerCase(Locale.ROOT)); + } + + /** + * Process a proxy session message from AuthMeBungee + * @param name the player to process + */ + public void processProxySessionMessage(String name) { + setActiveSession(name); + } + + /** + * Returns if the player should be logged in or not + * @param name the name of the player to check + * @return true if player has to be logged in, false otherwise + */ + public boolean shouldResumeSession(String name) { + return activeProxySessions.contains(name); + } + + @Override + public void performCleanup() { + activeProxySessions.removeExpiredEntries(); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/QuickCommandsProtectionManager.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/QuickCommandsProtectionManager.java new file mode 100644 index 00000000..7414e21c --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/QuickCommandsProtectionManager.java @@ -0,0 +1,75 @@ +package fr.xephi.authme.data; + +import fr.xephi.authme.initialization.HasCleanup; +import fr.xephi.authme.initialization.SettingsDependent; +import fr.xephi.authme.permission.PermissionsManager; +import fr.xephi.authme.permission.PlayerPermission; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.properties.ProtectionSettings; +import fr.xephi.authme.util.expiring.ExpiringSet; +import org.bukkit.entity.Player; + +import javax.inject.Inject; +import java.util.concurrent.TimeUnit; + +public class QuickCommandsProtectionManager implements SettingsDependent, HasCleanup { + + private final PermissionsManager permissionsManager; + + private final ExpiringSet latestJoin; + + @Inject + public QuickCommandsProtectionManager(Settings settings, PermissionsManager permissionsManager) { + this.permissionsManager = permissionsManager; + long countTimeout = settings.getProperty(ProtectionSettings.QUICK_COMMANDS_DENIED_BEFORE_MILLISECONDS); + latestJoin = new ExpiringSet<>(countTimeout, TimeUnit.MILLISECONDS); + reload(settings); + } + + /** + * Save the player in the set + * @param name the player's name + */ + private void setJoin(String name) { + latestJoin.add(name); + } + + /** + * Returns whether the given player has the permission and should be saved in the set + * @param player the player to check + * @return true if the player has the permission, false otherwise + */ + private boolean shouldSavePlayer(Player player) { + return permissionsManager.hasPermission(player, PlayerPermission.QUICK_COMMANDS_PROTECTION); + } + + /** + * Process the player join + * @param player the player to process + */ + public void processJoin(Player player) { + if (shouldSavePlayer(player)) { + setJoin(player.getName()); + } + } + + /** + * Returns whether the given player is able to perform the command + * @param name the name of the player to check + * @return true if the player is not in the set (so it's allowed to perform the command), false otherwise + */ + public boolean isAllowed(String name) { + return !latestJoin.contains(name); + } + + @Override + public void reload(Settings settings) { + long countTimeout = settings.getProperty(ProtectionSettings.QUICK_COMMANDS_DENIED_BEFORE_MILLISECONDS); + latestJoin.setExpiration(countTimeout, TimeUnit.MILLISECONDS); + } + + @Override + public void performCleanup() { + latestJoin.removeExpiredEntries(); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/TempbanManager.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/TempbanManager.java new file mode 100644 index 00000000..2a767874 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/TempbanManager.java @@ -0,0 +1,138 @@ +package fr.xephi.authme.data; + +import fr.xephi.authme.initialization.HasCleanup; +import fr.xephi.authme.initialization.SettingsDependent; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.message.Messages; +import fr.xephi.authme.service.BukkitService; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.properties.SecuritySettings; +import fr.xephi.authme.util.PlayerUtils; +import fr.xephi.authme.util.expiring.TimedCounter; +import org.bukkit.entity.Player; + +import javax.inject.Inject; +import java.util.Date; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; + +import static fr.xephi.authme.util.Utils.MILLIS_PER_MINUTE; + +/** + * Manager for handling temporary bans. + */ +public class TempbanManager implements SettingsDependent, HasCleanup { + + private final Map> ipLoginFailureCounts; + private final BukkitService bukkitService; + private final Messages messages; + + private boolean isEnabled; + private int threshold; + private int length; + private long resetThreshold; + private String customCommand; + + @Inject + TempbanManager(BukkitService bukkitService, Messages messages, Settings settings) { + this.ipLoginFailureCounts = new ConcurrentHashMap<>(); + this.bukkitService = bukkitService; + this.messages = messages; + reload(settings); + } + + /** + * Increases the failure count for the given IP address/username combination. + * + * @param address The player's IP address + * @param name The username + */ + public void increaseCount(String address, String name) { + if (isEnabled) { + TimedCounter countsByName = ipLoginFailureCounts.computeIfAbsent( + address, k -> new TimedCounter<>(resetThreshold, TimeUnit.MINUTES)); + countsByName.increment(name); + } + } + + /** + * Set the failure count for a given IP address / username combination to 0. + * + * @param address The IP address + * @param name The username + */ + public void resetCount(String address, String name) { + if (isEnabled) { + TimedCounter counter = ipLoginFailureCounts.get(address); + if (counter != null) { + counter.remove(name); + } + } + } + + /** + * Return whether the IP address should be tempbanned. + * + * @param address The player's IP address + * @return True if the IP should be tempbanned + */ + public boolean shouldTempban(String address) { + if (isEnabled) { + TimedCounter countsByName = ipLoginFailureCounts.get(address); + if (countsByName != null) { + return countsByName.total() >= threshold; + } + } + return false; + } + + /** + * 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) { + final String name = player.getName(); + final String ip = PlayerUtils.getPlayerIp(player); + final String reason = messages.retrieveSingle(player, MessageKey.TEMPBAN_MAX_LOGINS); + + final Date expires = new Date(); + long newTime = expires.getTime() + (length * MILLIS_PER_MINUTE); + expires.setTime(newTime); + + bukkitService.runTask(player,() -> { // AuthMeReReloaded - Folia compatibility + if (customCommand.isEmpty()) { + bukkitService.banIp(ip, reason, expires, "AuthMe"); + player.kickPlayer(reason); + } else { + String command = customCommand + .replace("%player%", name) + .replace("%ip%", ip); + bukkitService.dispatchConsoleCommand(command); + } + }); + + ipLoginFailureCounts.remove(ip); + } + } + + @Override + public void reload(Settings 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); + this.resetThreshold = settings.getProperty(SecuritySettings.TEMPBAN_MINUTES_BEFORE_RESET); + this.customCommand = settings.getProperty(SecuritySettings.TEMPBAN_CUSTOM_COMMAND); + } + + @Override + public void performCleanup() { + for (TimedCounter countsByIp : ipLoginFailureCounts.values()) { + countsByIp.removeExpiredEntries(); + } + ipLoginFailureCounts.entrySet().removeIf(e -> e.getValue().isEmpty()); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/VerificationCodeManager.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/VerificationCodeManager.java new file mode 100644 index 00000000..ad1b778d --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/VerificationCodeManager.java @@ -0,0 +1,194 @@ +package fr.xephi.authme.data; + +import ch.jalu.datasourcecolumns.data.DataSourceValue; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.initialization.HasCleanup; +import fr.xephi.authme.initialization.SettingsDependent; +import fr.xephi.authme.mail.EmailService; +import fr.xephi.authme.permission.PermissionsManager; +import fr.xephi.authme.permission.PlayerPermission; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.properties.SecuritySettings; +import fr.xephi.authme.util.RandomStringUtils; +import fr.xephi.authme.util.Utils; +import fr.xephi.authme.util.expiring.ExpiringMap; +import org.bukkit.entity.Player; + +import javax.inject.Inject; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.HashSet; +import java.util.Locale; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +public class VerificationCodeManager implements SettingsDependent, HasCleanup { + + private final EmailService emailService; + private final DataSource dataSource; + private final PermissionsManager permissionsManager; + + private final ExpiringMap verificationCodes; + private final Set verifiedPlayers; + + private boolean canSendMail; + + @Inject + VerificationCodeManager(Settings settings, DataSource dataSource, EmailService emailService, + PermissionsManager permissionsManager) { + this.emailService = emailService; + this.dataSource = dataSource; + this.permissionsManager = permissionsManager; + verifiedPlayers = new HashSet<>(); + long countTimeout = settings.getProperty(SecuritySettings.VERIFICATION_CODE_EXPIRATION_MINUTES); + verificationCodes = new ExpiringMap<>(countTimeout, TimeUnit.MINUTES); + reload(settings); + } + + /** + * Returns if it is possible to send emails + * + * @return true if the service is enabled, false otherwise + */ + public boolean canSendMail() { + return canSendMail; + } + + /** + * Returns whether the given player is able to verify his identity + * + * @param player the player to verify + * @return true if the player has not been verified yet, false otherwise + */ + public boolean isVerificationRequired(Player player) { + final String name = player.getName(); + return canSendMail + && !isPlayerVerified(name) + && permissionsManager.hasPermission(player, PlayerPermission.VERIFICATION_CODE) + && hasEmail(name); + } + + /** + * Returns whether the given player is required to verify his identity through a command + * + * @param name the name of the player to verify + * @return true if the player has an existing code and has not been verified yet, false otherwise + */ + public boolean isCodeRequired(String name) { + return canSendMail && hasCode(name) && !isPlayerVerified(name); + } + + /** + * Returns whether the given player has been verified or not + * + * @param name the name of the player to verify + * @return true if the player has been verified, false otherwise + */ + private boolean isPlayerVerified(String name) { + return verifiedPlayers.contains(name.toLowerCase(Locale.ROOT)); + } + + /** + * Returns if a code exists for the player + * + * @param name the name of the player to verify + * @return true if the code exists, false otherwise + */ + public boolean hasCode(String name) { + return (verificationCodes.get(name.toLowerCase(Locale.ROOT)) != null); + } + + /** + * Returns whether the given player is able to receive emails + * + * @param name the name of the player to verify + * @return true if the player is able to receive emails, false otherwise + */ + public boolean hasEmail(String name) { + boolean result = false; + DataSourceValue emailResult = dataSource.getEmail(name); + if (emailResult.rowExists()) { + final String email = emailResult.getValue(); + if (!Utils.isEmailEmpty(email)) { + result = true; + } + } + return result; + } + + /** + * Check if a code exists for the player or generates and saves a new one. + * + * @param name the player's name + */ + public void codeExistOrGenerateNew(String name) { + if (!hasCode(name)) { + generateCode(name); + } + } + + /** + * Generates a code for the player and returns it. + * + * @param name the name of the player to generate a code for + */ + private void generateCode(String name) { + DataSourceValue emailResult = dataSource.getEmail(name); + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy'年'MM'月'dd'日' HH:mm:ss"); + Date date = new Date(System.currentTimeMillis()); + if (emailResult.rowExists()) { + final String email = emailResult.getValue(); + if (!Utils.isEmailEmpty(email)) { + String code = RandomStringUtils.generateNum(6); // 6 digits code + verificationCodes.put(name.toLowerCase(Locale.ROOT), code); + emailService.sendVerificationMail(name, email, code, dateFormat.format(date)); + } + } + } + + /** + * Checks the given code against the existing one. + * + * @param name the name of the player to check + * @param code the supplied code + * @return true if the code matches, false otherwise + */ + public boolean checkCode(String name, String code) { + boolean correct = false; + if (code.equals(verificationCodes.get(name.toLowerCase(Locale.ROOT)))) { + verify(name); + correct = true; + } + return correct; + } + + /** + * Add the user to the set of verified users + * + * @param name the name of the player to generate a code for + */ + public void verify(String name) { + verifiedPlayers.add(name.toLowerCase(Locale.ROOT)); + } + + /** + * Remove the user from the set of verified users + * + * @param name the name of the player to generate a code for + */ + public void unverify(String name){ + verifiedPlayers.remove(name.toLowerCase(Locale.ROOT)); + } + + @Override + public void reload(Settings settings) { + canSendMail = emailService.hasAllInformation(); + long countTimeout = settings.getProperty(SecuritySettings.VERIFICATION_CODE_EXPIRATION_MINUTES); + verificationCodes.setExpiration(countTimeout, TimeUnit.MINUTES); + } + + @Override + public void performCleanup() { + verificationCodes.removeExpiredEntries(); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/auth/PlayerAuth.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/auth/PlayerAuth.java new file mode 100644 index 00000000..82082629 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/auth/PlayerAuth.java @@ -0,0 +1,373 @@ +package fr.xephi.authme.data.auth; + +import fr.xephi.authme.security.crypts.HashedPassword; +import org.bukkit.Location; + +import java.util.Locale; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; + +import static com.google.common.base.Preconditions.checkNotNull; + + +/** + * AuthMe player data. + */ +@SuppressWarnings("checkstyle:FinalClass") // Justification: class is mocked in multiple tests +public class PlayerAuth { + + /** Default email used in the database if the email column is defined to be NOT NULL. */ + public static final String DB_EMAIL_DEFAULT = "your@email.com"; + /** Default last login value used in the database if the last login column is NOT NULL. */ + public static final long DB_LAST_LOGIN_DEFAULT = 0; + /** Default last ip value used in the database if the last IP column is NOT NULL. */ + public static final String DB_LAST_IP_DEFAULT = "127.0.0.1"; + + /** The player's name in lowercase, e.g. "xephi". */ + private String nickname; + /** The player's name in the correct casing, e.g. "Xephi". */ + private String realName; + private HashedPassword password; + private String totpKey; + private String email; + private String lastIp; + private int groupId; + private Long lastLogin; + private String registrationIp; + private long registrationDate; + // Fields storing the player's quit location + private double x; + private double y; + private double z; + private String world; + private float yaw; + private float pitch; + private UUID uuid; + + /** + * Hidden constructor. + * + * @see #builder() + */ + private PlayerAuth() { + } + + + public void setNickname(String nickname) { + this.nickname = nickname.toLowerCase(Locale.ROOT); + } + + public String getNickname() { + return nickname; + } + + public String getRealName() { + return realName; + } + + public void setRealName(String realName) { + this.realName = realName; + } + + public int getGroupId() { + return groupId; + } + + public void setQuitLocation(Location location) { + x = location.getBlockX(); + y = location.getBlockY(); + z = location.getBlockZ(); + world = location.getWorld().getName(); + } + + public double getQuitLocX() { + return x; + } + + public void setQuitLocX(double d) { + this.x = d; + } + + public double getQuitLocY() { + return y; + } + + public void setQuitLocY(double d) { + this.y = d; + } + + public double getQuitLocZ() { + return z; + } + + public void setQuitLocZ(double d) { + this.z = d; + } + + public String getWorld() { + return world; + } + + public void setWorld(String world) { + this.world = world; + } + + public float getYaw() { + return yaw; + } + + public float getPitch() { + return pitch; + } + + public String getLastIp() { + return lastIp; + } + + public void setLastIp(String lastIp) { + this.lastIp = lastIp; + } + + public Long getLastLogin() { + return lastLogin; + } + + public void setLastLogin(long lastLogin) { + this.lastLogin = lastLogin; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public HashedPassword getPassword() { + return password; + } + + public void setPassword(HashedPassword password) { + this.password = password; + } + + public String getRegistrationIp() { + return registrationIp; + } + + public long getRegistrationDate() { + return registrationDate; + } + + public void setRegistrationDate(long registrationDate) { + this.registrationDate = registrationDate; + } + + public String getTotpKey() { + return totpKey; + } + + public void setTotpKey(String totpKey) { + this.totpKey = totpKey; + } + + public UUID getUuid() { + return uuid; + } + + public void setUuid(UUID uuid) { + this.uuid = uuid; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof PlayerAuth)) { + return false; + } + PlayerAuth other = (PlayerAuth) obj; + return Objects.equals(other.lastIp, this.lastIp) && Objects.equals(other.nickname, this.nickname); + } + + @Override + public int hashCode() { + int hashCode = 7; + hashCode = 71 * hashCode + (this.nickname != null ? this.nickname.hashCode() : 0); + hashCode = 71 * hashCode + (this.lastIp != null ? this.lastIp.hashCode() : 0); + return hashCode; + } + + @Override + public String toString() { + return "Player : " + nickname + " | " + realName + + " ! IP : " + lastIp + + " ! LastLogin : " + lastLogin + + " ! LastPosition : " + x + "," + y + "," + z + "," + world + + " ! Email : " + email + + " ! Password : {" + password.getHash() + ", " + password.getSalt() + "}" + + " ! UUID : " + uuid; + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private String name; + private String realName; + private HashedPassword password; + private String totpKey; + private String lastIp; + private String email; + private int groupId = -1; + private Long lastLogin; + private String registrationIp; + private Long registrationDate; + + private double x; + private double y; + private double z; + private String world; + private float yaw; + private float pitch; + private UUID uuid; + + /** + * Creates a PlayerAuth object. + * + * @return the generated PlayerAuth + */ + public PlayerAuth build() { + PlayerAuth auth = new PlayerAuth(); + auth.nickname = checkNotNull(name).toLowerCase(Locale.ROOT); + auth.realName = Optional.ofNullable(realName).orElse("Player"); + auth.password = Optional.ofNullable(password).orElse(new HashedPassword("")); + auth.totpKey = totpKey; + auth.email = DB_EMAIL_DEFAULT.equals(email) ? null : email; + auth.lastIp = lastIp; // Don't check against default value 127.0.0.1 as it may be a legit value + auth.groupId = groupId; + auth.lastLogin = isEqualTo(lastLogin, DB_LAST_LOGIN_DEFAULT) ? null : lastLogin; + auth.registrationIp = registrationIp; + auth.registrationDate = registrationDate == null ? System.currentTimeMillis() : registrationDate; + + auth.x = x; + auth.y = y; + auth.z = z; + auth.world = Optional.ofNullable(world).orElse("world"); + auth.yaw = yaw; + auth.pitch = pitch; + auth.uuid = uuid; + return auth; + } + + private static boolean isEqualTo(Long value, long defaultValue) { + return value != null && defaultValue == value; + } + + public Builder name(String name) { + this.name = name; + return this; + } + + public Builder realName(String realName) { + this.realName = realName; + return this; + } + + public Builder password(HashedPassword password) { + this.password = password; + return this; + } + + public Builder password(String hash, String salt) { + return password(new HashedPassword(hash, salt)); + } + + public Builder totpKey(String totpKey) { + this.totpKey = totpKey; + return this; + } + + public Builder lastIp(String lastIp) { + this.lastIp = lastIp; + return this; + } + + /** + * Sets the location info based on the argument. + * + * @param location the location info to set + * @return this builder instance + */ + public Builder location(Location location) { + this.x = location.getX(); + this.y = location.getY(); + this.z = location.getZ(); + this.world = location.getWorld().getName(); + this.yaw = location.getYaw(); + this.pitch = location.getPitch(); + return this; + } + + public Builder locX(double x) { + this.x = x; + return this; + } + + public Builder locY(double y) { + this.y = y; + return this; + } + + public Builder locZ(double z) { + this.z = z; + return this; + } + + public Builder locWorld(String world) { + this.world = world; + return this; + } + + public Builder locYaw(float yaw) { + this.yaw = yaw; + return this; + } + + public Builder locPitch(float pitch) { + this.pitch = pitch; + return this; + } + + public Builder lastLogin(Long lastLogin) { + this.lastLogin = lastLogin; + return this; + } + + public Builder groupId(int groupId) { + this.groupId = groupId; + return this; + } + + public Builder email(String email) { + this.email = email; + return this; + } + + public Builder registrationIp(String ip) { + this.registrationIp = ip; + return this; + } + + public Builder registrationDate(long date) { + this.registrationDate = date; + return this; + } + + public Builder uuid(UUID uuid) { + this.uuid = uuid; + return this; + } + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/auth/PlayerCache.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/auth/PlayerCache.java new file mode 100644 index 00000000..e617f20e --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/auth/PlayerCache.java @@ -0,0 +1,74 @@ +package fr.xephi.authme.data.auth; + + +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Used to manage player's Authenticated status + */ +public class PlayerCache { + + private final Map cache = new ConcurrentHashMap<>(); + + PlayerCache() { + } + + /** + * Adds the given auth object to the player cache (for the name defined in the PlayerAuth). + * + * @param auth the player auth object to save + */ + public void updatePlayer(PlayerAuth auth) { + cache.put(auth.getNickname().toLowerCase(Locale.ROOT), auth); + } + + /** + * Removes a player from the player cache. + * + * @param user name of the player to remove + */ + public void removePlayer(String user) { + cache.remove(user.toLowerCase(Locale.ROOT)); + } + + /** + * Get whether a player is authenticated (i.e. whether he is present in the player cache). + * + * @param user player's name + * + * @return true if player is logged in, false otherwise. + */ + public boolean isAuthenticated(String user) { + return cache.containsKey(user.toLowerCase(Locale.ROOT)); + } + + /** + * Returns the PlayerAuth associated with the given user, if available. + * + * @param user name of the player + * + * @return the associated auth object, or null if not available + */ + public PlayerAuth getAuth(String user) { + return cache.get(user.toLowerCase(Locale.ROOT)); + } + + /** + * @return number of logged in players + */ + public int getLogged() { + return cache.size(); + } + + /** + * Returns the player cache data. + * + * @return all player auths inside the player cache + */ + public Map getCache() { + return this.cache; + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/captcha/CaptchaCodeStorage.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/captcha/CaptchaCodeStorage.java new file mode 100644 index 00000000..5667a6f1 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/captcha/CaptchaCodeStorage.java @@ -0,0 +1,94 @@ +package fr.xephi.authme.data.captcha; + +import fr.xephi.authme.util.RandomStringUtils; +import fr.xephi.authme.util.expiring.ExpiringMap; + +import java.util.Locale; +import java.util.concurrent.TimeUnit; + +/** + * Primitive service for storing captcha codes. + */ +public class CaptchaCodeStorage { + + /** Map of captcha codes (with player name as key, case-insensitive). */ + private ExpiringMap captchaCodes; + /** Number of characters newly generated captcha codes should have. */ + private int captchaLength; + + /** + * Constructor. + * + * @param expirationInMinutes minutes after which a saved captcha code expires + * @param captchaLength the number of characters a captcha code should have + */ + public CaptchaCodeStorage(long expirationInMinutes, int captchaLength) { + this.captchaCodes = new ExpiringMap<>(expirationInMinutes, TimeUnit.MINUTES); + this.captchaLength = captchaLength; + } + + /** + * Sets the expiration of captcha codes. + * + * @param expirationInMinutes minutes after which a saved captcha code expires + */ + public void setExpirationInMinutes(long expirationInMinutes) { + captchaCodes.setExpiration(expirationInMinutes, TimeUnit.MINUTES); + } + + /** + * Sets the captcha length. + * + * @param captchaLength number of characters a captcha code should have + */ + public void setCaptchaLength(int captchaLength) { + this.captchaLength = captchaLength; + } + + /** + * 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 getCodeOrGenerateNew(String name) { + String code = captchaCodes.get(name.toLowerCase(Locale.ROOT)); + 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 + */ + private String generateCode(String name) { + String code = RandomStringUtils.generate(captchaLength); + captchaCodes.put(name.toLowerCase(Locale.ROOT), code); + return code; + } + + /** + * Checks the given code against the existing one. Upon success, the saved captcha code is removed from storage. + * Upon failure, a new code is generated. + * + * @param name the name of the player to check + * @param code the supplied code + * @return true if the code matches, false otherwise + */ + public boolean checkCode(String name, String code) { + String nameLowerCase = name.toLowerCase(Locale.ROOT); + String savedCode = captchaCodes.get(nameLowerCase); + if (savedCode != null && savedCode.equalsIgnoreCase(code)) { + captchaCodes.remove(nameLowerCase); + return true; + } else { + generateCode(name); + } + return false; + } + + public void removeExpiredEntries() { + captchaCodes.removeExpiredEntries(); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/captcha/CaptchaManager.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/captcha/CaptchaManager.java new file mode 100644 index 00000000..c3daeb6c --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/captcha/CaptchaManager.java @@ -0,0 +1,38 @@ +package fr.xephi.authme.data.captcha; + +import org.bukkit.entity.Player; + +/** + * Manages captcha codes. + */ +public interface CaptchaManager { + + /** + * Returns whether the given player is required to solve a captcha. + * + * @param name the name of the player to verify + * @return true if the player has to solve a captcha, false otherwise + */ + boolean isCaptchaRequired(String name); + + /** + * 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 + */ + String getCaptchaCodeOrGenerateNew(String name); + + /** + * Checks the given code against the existing one. This method is not reentrant, i.e. it performs additional + * state changes on success or failure, such as modifying some counter or setting a player as verified. + *

+ * On success, the code associated with the player is cleared; on failure, a new code is generated. + * + * @param player the player to check + * @param code the supplied code + * @return true if the code matches, false otherwise + */ + boolean checkCode(Player player, String code); + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/captcha/LoginCaptchaManager.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/captcha/LoginCaptchaManager.java new file mode 100644 index 00000000..1d97af69 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/captcha/LoginCaptchaManager.java @@ -0,0 +1,95 @@ +package fr.xephi.authme.data.captcha; + +import fr.xephi.authme.initialization.HasCleanup; +import fr.xephi.authme.initialization.SettingsDependent; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.properties.SecuritySettings; +import fr.xephi.authme.util.expiring.TimedCounter; +import org.bukkit.entity.Player; + +import javax.inject.Inject; +import java.util.Locale; +import java.util.concurrent.TimeUnit; + +/** + * Manager for the handling of captchas after too many failed login attempts. + */ +public class LoginCaptchaManager implements CaptchaManager, SettingsDependent, HasCleanup { + + private final TimedCounter playerCounts; + private final CaptchaCodeStorage captchaCodeStorage; + + private boolean isEnabled; + private int threshold; + + @Inject + LoginCaptchaManager(Settings settings) { + // Note: Proper values are set in reload() + this.captchaCodeStorage = new CaptchaCodeStorage(30, 4); + this.playerCounts = new TimedCounter<>(9, TimeUnit.MINUTES); + reload(settings); + } + + /** + * Increases the failure count for the given player. + * + * @param name the player's name + */ + public void increaseLoginFailureCount(String name) { + if (isEnabled) { + String playerLower = name.toLowerCase(Locale.ROOT); + playerCounts.increment(playerLower); + } + } + + @Override + public boolean isCaptchaRequired(String playerName) { + return isEnabled && playerCounts.get(playerName.toLowerCase(Locale.ROOT)) >= threshold; + } + + @Override + public String getCaptchaCodeOrGenerateNew(String name) { + return captchaCodeStorage.getCodeOrGenerateNew(name); + } + + @Override + public boolean checkCode(Player player, String code) { + String nameLower = player.getName().toLowerCase(Locale.ROOT); + boolean isCodeCorrect = captchaCodeStorage.checkCode(nameLower, code); + if (isCodeCorrect) { + playerCounts.remove(nameLower); + } + return isCodeCorrect; + } + + /** + * Resets the login count of the given player to 0. + * + * @param name the player's name + */ + public void resetLoginFailureCount(String name) { + if (isEnabled) { + playerCounts.remove(name.toLowerCase(Locale.ROOT)); + } + } + + @Override + public void reload(Settings settings) { + int expirationInMinutes = settings.getProperty(SecuritySettings.CAPTCHA_COUNT_MINUTES_BEFORE_RESET); + captchaCodeStorage.setExpirationInMinutes(expirationInMinutes); + int captchaLength = settings.getProperty(SecuritySettings.CAPTCHA_LENGTH); + captchaCodeStorage.setCaptchaLength(captchaLength); + + int countTimeout = settings.getProperty(SecuritySettings.CAPTCHA_COUNT_MINUTES_BEFORE_RESET); + playerCounts.setExpiration(countTimeout, TimeUnit.MINUTES); + + isEnabled = settings.getProperty(SecuritySettings.ENABLE_LOGIN_FAILURE_CAPTCHA); + threshold = settings.getProperty(SecuritySettings.MAX_LOGIN_TRIES_BEFORE_CAPTCHA); + } + + @Override + public void performCleanup() { + playerCounts.removeExpiredEntries(); + captchaCodeStorage.removeExpiredEntries(); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/captcha/RegistrationCaptchaManager.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/captcha/RegistrationCaptchaManager.java new file mode 100644 index 00000000..655d9940 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/captcha/RegistrationCaptchaManager.java @@ -0,0 +1,66 @@ +package fr.xephi.authme.data.captcha; + +import fr.xephi.authme.initialization.HasCleanup; +import fr.xephi.authme.initialization.SettingsDependent; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.properties.SecuritySettings; +import fr.xephi.authme.util.expiring.ExpiringSet; +import org.bukkit.entity.Player; + +import javax.inject.Inject; +import java.util.Locale; +import java.util.concurrent.TimeUnit; + +/** + * Captcha manager for registration. + */ +public class RegistrationCaptchaManager implements CaptchaManager, SettingsDependent, HasCleanup { + + private static final int MINUTES_VALID_FOR_REGISTRATION = 30; + + private final ExpiringSet verifiedNamesForRegistration; + private final CaptchaCodeStorage captchaCodeStorage; + private boolean isEnabled; + + @Inject + RegistrationCaptchaManager(Settings settings) { + // NOTE: proper captcha length is set in reload() + this.captchaCodeStorage = new CaptchaCodeStorage(MINUTES_VALID_FOR_REGISTRATION, 4); + this.verifiedNamesForRegistration = new ExpiringSet<>(MINUTES_VALID_FOR_REGISTRATION, TimeUnit.MINUTES); + reload(settings); + } + + @Override + public boolean isCaptchaRequired(String name) { + return isEnabled && !verifiedNamesForRegistration.contains(name.toLowerCase(Locale.ROOT)); + } + + @Override + public String getCaptchaCodeOrGenerateNew(String name) { + return captchaCodeStorage.getCodeOrGenerateNew(name); + } + + @Override + public boolean checkCode(Player player, String code) { + String nameLower = player.getName().toLowerCase(Locale.ROOT); + boolean isCodeCorrect = captchaCodeStorage.checkCode(nameLower, code); + if (isCodeCorrect) { + verifiedNamesForRegistration.add(nameLower); + } + return isCodeCorrect; + } + + @Override + public void reload(Settings settings) { + int captchaLength = settings.getProperty(SecuritySettings.CAPTCHA_LENGTH); + captchaCodeStorage.setCaptchaLength(captchaLength); + + isEnabled = settings.getProperty(SecuritySettings.ENABLE_CAPTCHA_FOR_REGISTRATION); + } + + @Override + public void performCleanup() { + verifiedNamesForRegistration.removeExpiredEntries(); + captchaCodeStorage.removeExpiredEntries(); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/limbo/AllowFlightRestoreType.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/limbo/AllowFlightRestoreType.java new file mode 100644 index 00000000..753650b6 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/limbo/AllowFlightRestoreType.java @@ -0,0 +1,66 @@ +package fr.xephi.authme.data.limbo; + +import org.bukkit.entity.Player; + +/** + * Possible types to restore the "allow flight" property + * from LimboPlayer to Bukkit Player. + */ +public enum AllowFlightRestoreType { + + /** Set value from LimboPlayer to Player. */ + RESTORE { + @Override + public void restoreAllowFlight(Player player, LimboPlayer limbo) { + player.setAllowFlight(limbo.isCanFly()); + } + }, + + /** Always set flight enabled to true. */ + ENABLE { + @Override + public void restoreAllowFlight(Player player, LimboPlayer limbo) { + player.setAllowFlight(true); + } + }, + + /** Always set flight enabled to false. */ + DISABLE { + @Override + public void restoreAllowFlight(Player player, LimboPlayer limbo) { + player.setAllowFlight(false); + } + }, + + /** The user's flight handling is not modified. */ + NOTHING { + @Override + public void restoreAllowFlight(Player player, LimboPlayer limbo) { + // noop + } + + @Override + public void processPlayer(Player player) { + // noop + } + }; + + /** + * Restores the "allow flight" property from the LimboPlayer to the Player. + * This method behaves differently for each restoration type. + * + * @param player the player to modify + * @param limbo the limbo player to read from + */ + public abstract void restoreAllowFlight(Player player, LimboPlayer limbo); + + /** + * Processes the player when a LimboPlayer instance is created based on him. Typically this + * method revokes the {@code allowFlight} property to be restored again later. + * + * @param player the player to process + */ + public void processPlayer(Player player) { + player.setAllowFlight(false); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/limbo/AuthGroupHandler.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/limbo/AuthGroupHandler.java new file mode 100644 index 00000000..6e8c241a --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/limbo/AuthGroupHandler.java @@ -0,0 +1,114 @@ +package fr.xephi.authme.data.limbo; + +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.initialization.Reloadable; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.permission.PermissionsManager; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.properties.PluginSettings; +import org.bukkit.entity.Player; + +import javax.annotation.PostConstruct; +import javax.inject.Inject; +import java.util.Collection; +import java.util.Collections; + +/** + * Changes the permission group according to the auth status of the player and the configuration. + *

+ * If this feature is enabled, the primary permissions group of a player is replaced until he has + * logged in. Some permission plugins have a notion of a primary group; for other permission plugins the + * first group is simply taken. + *

+ * The groups that are used as replacement until the player logs in is configurable and depends on if + * the player is registered or not. Note that some (all?) permission systems require the group to actually + * exist for the replacement to take place. Furthermore, since some permission groups require that players + * be in at least one group, this will mean that the player is not removed from his primary group. + */ +class AuthGroupHandler implements Reloadable { + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(AuthGroupHandler.class); + + @Inject + private PermissionsManager permissionsManager; + + @Inject + private Settings settings; + + private UserGroup unregisteredGroup; + private UserGroup registeredGroup; + + AuthGroupHandler() { + } + + /** + * Sets the group of a player by its authentication status. + * + * @param player the player + * @param limbo the associated limbo player (nullable) + * @param groupType the group type + */ + void setGroup(Player player, LimboPlayer limbo, AuthGroupType groupType) { + if (!useAuthGroups()) { + return; + } + + Collection previousGroups = limbo == null ? Collections.emptyList() : limbo.getGroups(); + + switch (groupType) { + // Implementation note: some permission systems don't support players not being in any group, + // so add the new group before removing the old ones + case UNREGISTERED: + permissionsManager.addGroup(player, unregisteredGroup); + permissionsManager.removeGroup(player, registeredGroup); + permissionsManager.removeGroups(player, previousGroups); + break; + + case REGISTERED_UNAUTHENTICATED: + permissionsManager.addGroup(player, registeredGroup); + permissionsManager.removeGroup(player, unregisteredGroup); + permissionsManager.removeGroups(player, previousGroups); + + break; + + case LOGGED_IN: + permissionsManager.addGroups(player, previousGroups); + permissionsManager.removeGroup(player, unregisteredGroup); + permissionsManager.removeGroup(player, registeredGroup); + break; + + default: + throw new IllegalStateException("Encountered unhandled auth group type '" + groupType + "'"); + } + + logger.debug(() -> player.getName() + " changed to " + + groupType + ": has groups " + permissionsManager.getGroups(player)); + } + + /** + * Returns whether the auth permissions group function should be used. + * + * @return true if should be used, false otherwise + */ + private boolean useAuthGroups() { + // 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()) { + logger.warning("The current permissions system doesn't have group support, unable to set group!"); + return false; + } + return true; + } + + @Override + @PostConstruct + public void reload() { + unregisteredGroup = new UserGroup(settings.getProperty(PluginSettings.UNREGISTERED_GROUP)); + registeredGroup = new UserGroup(settings.getProperty(PluginSettings.REGISTERED_GROUP)); + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/limbo/AuthGroupType.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/limbo/AuthGroupType.java new file mode 100644 index 00000000..501a20d8 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/limbo/AuthGroupType.java @@ -0,0 +1,17 @@ +package fr.xephi.authme.data.limbo; + +/** + * Represents the group type based on the user's auth status. + */ +enum AuthGroupType { + + /** Player does not have an account. */ + UNREGISTERED, + + /** Player is registered but not logged in. */ + REGISTERED_UNAUTHENTICATED, + + /** Player is logged in. */ + LOGGED_IN + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/limbo/LimboMessageType.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/limbo/LimboMessageType.java new file mode 100644 index 00000000..4d0af3e5 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/limbo/LimboMessageType.java @@ -0,0 +1,11 @@ +package fr.xephi.authme.data.limbo; + +public enum LimboMessageType { + + REGISTER, + + LOG_IN, + + TOTP_CODE + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/limbo/LimboPlayer.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/limbo/LimboPlayer.java new file mode 100644 index 00000000..9446222f --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/limbo/LimboPlayer.java @@ -0,0 +1,138 @@ +package fr.xephi.authme.data.limbo; + +import com.github.Anon8281.universalScheduler.scheduling.tasks.MyScheduledTask; +import fr.xephi.authme.task.MessageTask; +import org.bukkit.Location; + +import java.util.ArrayList; +import java.util.Collection; + +/** + * Represents a player which is not logged in and keeps track of certain states (like OP status, flying) + * which may be revoked from the player until he has logged in or registered. + */ +public class LimboPlayer { + + public static final float DEFAULT_WALK_SPEED = 0.2f; + public static final float DEFAULT_FLY_SPEED = 0.1f; + + private final boolean canFly; + private final boolean operator; + private final Collection groups; + private final Location loc; + private final float walkSpeed; + private final float flySpeed; + private MyScheduledTask timeoutTask = null; + private MessageTask messageTask = null; + + private LimboPlayerState state = LimboPlayerState.PASSWORD_REQUIRED; + + public LimboPlayer(Location loc, boolean operator, Collection groups, boolean fly, float walkSpeed, + float flySpeed) { + this.loc = loc; + this.operator = operator; + this.groups = new ArrayList<>(groups); // prevent bug #2413 + this.canFly = fly; + this.walkSpeed = walkSpeed; + this.flySpeed = flySpeed; + } + + /** + * Return the player's original location. + * + * @return The player's location + */ + public Location getLocation() { + return loc; + } + + /** + * Return whether the player is an operator or not (i.e. whether he is an OP). + * + * @return True if the player has OP status, false otherwise + */ + public boolean isOperator() { + return operator; + } + + /** + * Return the player's permissions groups. + * + * @return The permissions groups the player belongs to + */ + public Collection getGroups() { + return groups; + } + + public boolean isCanFly() { + return canFly; + } + + public float getWalkSpeed() { + return walkSpeed; + } + + public float getFlySpeed() { + return flySpeed; + } + + /** + * Return the timeout task, which kicks the player if he hasn't registered or logged in + * after a configurable amount of time. + * + * @return The timeout task associated to the player + */ + public MyScheduledTask getTimeoutTask() { + return timeoutTask; + } + + /** + * Set the timeout task of the player. The timeout task kicks the player after a configurable + * amount of time if he hasn't logged in or registered. + * + * @param timeoutTask The task to set + */ + public void setTimeoutTask(MyScheduledTask timeoutTask) { + if (this.timeoutTask != null) { + this.timeoutTask.cancel(); + } + this.timeoutTask = timeoutTask; + } + + /** + * Return the message task reminding the player to log in or register. + * + * @return The task responsible for sending the message regularly + */ + public MessageTask getMessageTask() { + return messageTask; + } + + /** + * Set the messages task responsible for telling the player to log in or register. + * + * @param messageTask The message task to set + */ + public void setMessageTask(MessageTask messageTask) { + if (this.messageTask != null) { + this.messageTask.cancel(); + } + this.messageTask = messageTask; + } + + /** + * Clears all tasks associated to the player. + */ + public void clearTasks() { + setMessageTask(null); + setTimeoutTask(null); + } + + public LimboPlayerState getState() { + return state; + } + + public void setState(LimboPlayerState state) { + this.state = state; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/limbo/LimboPlayerState.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/limbo/LimboPlayerState.java new file mode 100644 index 00000000..5940ab20 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/limbo/LimboPlayerState.java @@ -0,0 +1,9 @@ +package fr.xephi.authme.data.limbo; + +public enum LimboPlayerState { + + PASSWORD_REQUIRED, + + TOTP_REQUIRED + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/limbo/LimboPlayerTaskManager.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/limbo/LimboPlayerTaskManager.java new file mode 100644 index 00000000..ca5eaa1f --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/limbo/LimboPlayerTaskManager.java @@ -0,0 +1,117 @@ +package fr.xephi.authme.data.limbo; + +import com.github.Anon8281.universalScheduler.scheduling.tasks.MyScheduledTask; +import fr.xephi.authme.data.auth.PlayerCache; +import fr.xephi.authme.data.captcha.RegistrationCaptchaManager; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.message.Messages; +import fr.xephi.authme.service.BukkitService; +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 org.bukkit.entity.Player; + +import javax.inject.Inject; + +import static fr.xephi.authme.service.BukkitService.TICKS_PER_SECOND; + +/** + * Registers tasks associated with a LimboPlayer. + */ +class LimboPlayerTaskManager { + + @Inject + private Messages messages; + + @Inject + private Settings settings; + + @Inject + private BukkitService bukkitService; + + @Inject + private PlayerCache playerCache; + + @Inject + private RegistrationCaptchaManager registrationCaptchaManager; + + LimboPlayerTaskManager() { + } + + /** + * Registers a {@link MessageTask} for the given player name. + * + * @param player the player + * @param limbo the associated limbo player of the player + * @param messageType message type + */ + void registerMessageTask(Player player, LimboPlayer limbo, LimboMessageType messageType) { + int interval = settings.getProperty(RegistrationSettings.MESSAGE_INTERVAL); + MessageResult result = getMessageKey(player.getName(), messageType); + if (interval > 0) { + String[] joinMessage = messages.retrieveSingle(player, result.messageKey, result.args).split("\n"); + MessageTask messageTask = new MessageTask(player, joinMessage); + bukkitService.runTaskTimer(messageTask, 2 * TICKS_PER_SECOND, (long) interval * TICKS_PER_SECOND); + limbo.setMessageTask(messageTask); + } + } + + /** + * Registers a {@link TimeoutTask} for the given player according to the configuration. + * + * @param player the player to register a timeout task for + * @param limbo the associated limbo player + */ + void registerTimeoutTask(Player player, LimboPlayer limbo) { + final int timeout = settings.getProperty(RestrictionSettings.TIMEOUT) * TICKS_PER_SECOND; + if (timeout > 0) { + String message = messages.retrieveSingle(player, MessageKey.LOGIN_TIMEOUT_ERROR); + MyScheduledTask task = bukkitService.runTaskLater(new TimeoutTask(player, message, playerCache), timeout); + limbo.setTimeoutTask(task); + } + } + + /** + * Null-safe method to set the muted flag on a message task. + * + * @param task the task to modify (or null) + * @param isMuted the value to set if task is not null + */ + static void setMuted(MessageTask task, boolean isMuted) { + if (task != null) { + task.setMuted(isMuted); + } + } + + /** + * Returns the appropriate message key according to the registration status and settings. + * + * @param name the player's name + * @param messageType the message to show + * @return the message key to display to the user + */ + private MessageResult getMessageKey(String name, LimboMessageType messageType) { + if (messageType == LimboMessageType.LOG_IN) { + return new MessageResult(MessageKey.LOGIN_MESSAGE); + } else if (messageType == LimboMessageType.TOTP_CODE) { + return new MessageResult(MessageKey.TWO_FACTOR_CODE_REQUIRED); + } else if (registrationCaptchaManager.isCaptchaRequired(name)) { + final String captchaCode = registrationCaptchaManager.getCaptchaCodeOrGenerateNew(name); + return new MessageResult(MessageKey.CAPTCHA_FOR_REGISTRATION_REQUIRED, captchaCode); + } else { + return new MessageResult(MessageKey.REGISTER_MESSAGE); + } + } + + private static final class MessageResult { + private final MessageKey messageKey; + private final String[] args; + + MessageResult(MessageKey messageKey, String... args) { + this.messageKey = messageKey; + this.args = args; + } + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/limbo/LimboService.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/limbo/LimboService.java new file mode 100644 index 00000000..c8b46922 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/limbo/LimboService.java @@ -0,0 +1,188 @@ +package fr.xephi.authme.data.limbo; + +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.data.limbo.persistence.LimboPersistence; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.SpawnLoader; +import org.bukkit.Location; +import org.bukkit.entity.Player; + +import javax.inject.Inject; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +import static fr.xephi.authme.settings.properties.LimboSettings.RESTORE_ALLOW_FLIGHT; +import static fr.xephi.authme.settings.properties.LimboSettings.RESTORE_FLY_SPEED; +import static fr.xephi.authme.settings.properties.LimboSettings.RESTORE_WALK_SPEED; + +/** + * Service for managing players that are in "limbo," a temporary state players are + * put in which have joined but not yet logged in. + */ +public class LimboService { + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(LimboService.class); + + private final Map entries = new ConcurrentHashMap<>(); + + @Inject + private Settings settings; + + @Inject + private LimboPlayerTaskManager taskManager; + + @Inject + private LimboServiceHelper helper; + + @Inject + private LimboPersistence persistence; + + @Inject + private AuthGroupHandler authGroupHandler; + + @Inject + private SpawnLoader spawnLoader; + + LimboService() { + } + + /** + * Creates a LimboPlayer for the given player and revokes all "limbo data" from the player. + * + * @param player the player to process + * @param isRegistered whether or not the player is registered + */ + public void createLimboPlayer(Player player, boolean isRegistered) { + final String name = player.getName().toLowerCase(Locale.ROOT); + + LimboPlayer limboFromDisk = persistence.getLimboPlayer(player); + if (limboFromDisk != null) { + logger.debug("LimboPlayer for `{0}` already exists on disk", name); + } + + LimboPlayer existingLimbo = entries.remove(name); + if (existingLimbo != null) { + existingLimbo.clearTasks(); + logger.debug("LimboPlayer for `{0}` already present in memory", name); + } + + Location location = spawnLoader.getPlayerLocationOrSpawn(player); + LimboPlayer limboPlayer = helper.merge(existingLimbo, limboFromDisk); + limboPlayer = helper.merge(helper.createLimboPlayer(player, isRegistered, location), limboPlayer); + + taskManager.registerMessageTask(player, limboPlayer, + isRegistered ? LimboMessageType.LOG_IN : LimboMessageType.REGISTER); + taskManager.registerTimeoutTask(player, limboPlayer); + helper.revokeLimboStates(player); + authGroupHandler.setGroup(player, limboPlayer, + isRegistered ? AuthGroupType.REGISTERED_UNAUTHENTICATED : AuthGroupType.UNREGISTERED); + entries.put(name, limboPlayer); + persistence.saveLimboPlayer(player, limboPlayer); + } + + /** + * Returns the limbo player for the given name, or null otherwise. + * + * @param name the name to retrieve the data for + * @return the associated limbo player, or null if none available + */ + public LimboPlayer getLimboPlayer(String name) { + return entries.get(name.toLowerCase(Locale.ROOT)); + } + + /** + * Returns whether there is a limbo player for the given name. + * + * @param name the name to check + * @return true if present, false otherwise + */ + public boolean hasLimboPlayer(String name) { + return entries.containsKey(name.toLowerCase(Locale.ROOT)); + } + + /** + * Restores the limbo data and subsequently deletes the entry. + *

+ * Note that teleportation on the player is performed by {@link fr.xephi.authme.service.TeleportationService} and + * changing the permission group is handled by {@link fr.xephi.authme.data.limbo.AuthGroupHandler}. + * + * @param player the player whose data should be restored + */ + public void restoreData(Player player) { + String lowerName = player.getName().toLowerCase(Locale.ROOT); + LimboPlayer limbo = entries.remove(lowerName); + + if (limbo == null) { + logger.debug("No LimboPlayer found for `{0}` - cannot restore", lowerName); + } else { + player.setOp(limbo.isOperator()); + settings.getProperty(RESTORE_ALLOW_FLIGHT).restoreAllowFlight(player, limbo); + settings.getProperty(RESTORE_FLY_SPEED).restoreFlySpeed(player, limbo); + settings.getProperty(RESTORE_WALK_SPEED).restoreWalkSpeed(player, limbo); + limbo.clearTasks(); + logger.debug("Restored LimboPlayer stats for `{0}`", lowerName); + persistence.removeLimboPlayer(player); + } + authGroupHandler.setGroup(player, limbo, AuthGroupType.LOGGED_IN); + } + + /** + * Creates new tasks for the given player and cancels the old ones for a newly registered player. + * This resets his time to log in (TimeoutTask) and updates the message he is shown (MessageTask). + * + * @param player the player to reset the tasks for + */ + public void replaceTasksAfterRegistration(Player player) { + Optional limboPlayer = getLimboOrLogError(player, "reset tasks"); + limboPlayer.ifPresent(limbo -> { + taskManager.registerTimeoutTask(player, limbo); + taskManager.registerMessageTask(player, limbo, LimboMessageType.LOG_IN); + }); + authGroupHandler.setGroup(player, limboPlayer.orElse(null), AuthGroupType.REGISTERED_UNAUTHENTICATED); + } + + /** + * Resets the message task associated with the player's LimboPlayer. + * + * @param player the player to set a new message task for + * @param messageType the message to show for the limbo player + */ + public void resetMessageTask(Player player, LimboMessageType messageType) { + getLimboOrLogError(player, "reset message task") + .ifPresent(limbo -> taskManager.registerMessageTask(player, limbo, messageType)); + } + + /** + * @param player the player whose message task should be muted + */ + public void muteMessageTask(Player player) { + getLimboOrLogError(player, "mute message task") + .ifPresent(limbo -> LimboPlayerTaskManager.setMuted(limbo.getMessageTask(), true)); + } + + /** + * @param player the player whose message task should be unmuted + */ + public void unmuteMessageTask(Player player) { + getLimboOrLogError(player, "unmute message task") + .ifPresent(limbo -> LimboPlayerTaskManager.setMuted(limbo.getMessageTask(), false)); + } + + /** + * Returns the limbo player for the given player or logs an error. + * + * @param player the player to retrieve the limbo player for + * @param context the action for which the limbo player is being retrieved (for logging) + * @return Optional with the limbo player + */ + private Optional getLimboOrLogError(Player player, String context) { + LimboPlayer limbo = entries.get(player.getName().toLowerCase(Locale.ROOT)); + if (limbo == null) { + logger.debug("No LimboPlayer found for `{0}`. Action: {1}", player.getName(), context); + } + return Optional.ofNullable(limbo); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/limbo/LimboServiceHelper.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/limbo/LimboServiceHelper.java new file mode 100644 index 00000000..a3edb164 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/limbo/LimboServiceHelper.java @@ -0,0 +1,115 @@ +package fr.xephi.authme.data.limbo; + +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.permission.PermissionsManager; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.properties.LimboSettings; +import fr.xephi.authme.settings.properties.RestrictionSettings; +import org.bukkit.Location; +import org.bukkit.entity.Player; + +import javax.inject.Inject; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import static fr.xephi.authme.util.Utils.isCollectionEmpty; +import static java.util.stream.Collectors.toList; + +/** + * Helper class for the LimboService. + */ +class LimboServiceHelper { + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(LimboServiceHelper.class); + + @Inject + private PermissionsManager permissionsManager; + + @Inject + private Settings settings; + + /** + * Creates a LimboPlayer with the given player's details. + * + * @param player the player to process + * @param isRegistered whether the player is registered + * @param location the player location + * @return limbo player with the player's data + */ + LimboPlayer createLimboPlayer(Player player, boolean isRegistered, Location location) { + // For safety reasons an unregistered player should not have OP status after registration + boolean isOperator = isRegistered && player.isOp(); + boolean flyEnabled = player.getAllowFlight(); + float walkSpeed = player.getWalkSpeed(); + float flySpeed = player.getFlySpeed(); + Collection playerGroups = permissionsManager.hasGroupSupport() + ? permissionsManager.getGroups(player) : Collections.emptyList(); + + List groupNames = playerGroups.stream() + .map(UserGroup::getGroupName) + .collect(toList()); + + logger.debug("Player `{0}` has groups `{1}`", player.getName(), String.join(", ", groupNames)); + return new LimboPlayer(location, isOperator, playerGroups, flyEnabled, walkSpeed, flySpeed); + } + + /** + * Removes the data that is saved in a LimboPlayer from the player. + *

+ * Note that teleportation on the player is performed by {@link fr.xephi.authme.service.TeleportationService} and + * changing the permission group is handled by {@link fr.xephi.authme.data.limbo.AuthGroupHandler}. + * + * @param player the player to set defaults to + */ + void revokeLimboStates(Player player) { + player.setOp(false); + settings.getProperty(LimboSettings.RESTORE_ALLOW_FLIGHT) + .processPlayer(player); + + if (!settings.getProperty(RestrictionSettings.ALLOW_UNAUTHED_MOVEMENT)) { + player.setFlySpeed(0.0f); + player.setWalkSpeed(0.0f); + } + } + + /** + * Merges two existing LimboPlayer instances of a player. Merging is done the following way: + *

    + *
  • isOperator, allowFlight: true if either limbo has true
  • + *
  • flySpeed, walkSpeed: maximum value of either limbo player
  • + *
  • groups, location: from old limbo if not empty/null, otherwise from new limbo
  • + *
+ * + * @param newLimbo the new limbo player + * @param oldLimbo the old limbo player + * @return merged limbo player if both arguments are not null, otherwise the first non-null argument + */ + LimboPlayer merge(LimboPlayer newLimbo, LimboPlayer oldLimbo) { + if (newLimbo == null) { + return oldLimbo; + } else if (oldLimbo == null) { + return newLimbo; + } + + boolean isOperator = newLimbo.isOperator() || oldLimbo.isOperator(); + boolean canFly = newLimbo.isCanFly() || oldLimbo.isCanFly(); + float flySpeed = Math.max(newLimbo.getFlySpeed(), oldLimbo.getFlySpeed()); + float walkSpeed = Math.max(newLimbo.getWalkSpeed(), oldLimbo.getWalkSpeed()); + Collection groups = getLimboGroups(oldLimbo.getGroups(), newLimbo.getGroups()); + Location location = firstNotNull(oldLimbo.getLocation(), newLimbo.getLocation()); + + return new LimboPlayer(location, isOperator, groups, canFly, walkSpeed, flySpeed); + } + + private static Location firstNotNull(Location first, Location second) { + return first == null ? second : first; + } + + private Collection getLimboGroups(Collection oldLimboGroups, + Collection newLimboGroups) { + logger.debug("Limbo merge: new and old groups are `{0}` and `{1}`", newLimboGroups, oldLimboGroups); + return isCollectionEmpty(oldLimboGroups) ? newLimboGroups : oldLimboGroups; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/limbo/UserGroup.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/limbo/UserGroup.java new file mode 100644 index 00000000..9ed028a5 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/limbo/UserGroup.java @@ -0,0 +1,45 @@ +package fr.xephi.authme.data.limbo; + +import java.util.Map; +import java.util.Objects; + +public class UserGroup { + + private String groupName; + private Map contextMap; + + public UserGroup(String groupName) { + this.groupName = groupName; + } + + public UserGroup(String groupName, Map contextMap) { + this.groupName = groupName; + this.contextMap = contextMap; + } + + public String getGroupName() { + return groupName; + } + + public Map getContextMap() { + return contextMap; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + UserGroup userGroup = (UserGroup) o; + return Objects.equals(groupName, userGroup.groupName) + && Objects.equals(contextMap, userGroup.contextMap); + } + + @Override + public int hashCode() { + return Objects.hash(groupName, contextMap); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/limbo/WalkFlySpeedRestoreType.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/limbo/WalkFlySpeedRestoreType.java new file mode 100644 index 00000000..f4a24901 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/limbo/WalkFlySpeedRestoreType.java @@ -0,0 +1,123 @@ +package fr.xephi.authme.data.limbo; + +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import org.bukkit.entity.Player; + +/** + * Possible types to restore the walk and fly speed from LimboPlayer + * back to Bukkit Player. + */ +public enum WalkFlySpeedRestoreType { + + /** + * Restores from LimboPlayer to Player. + */ + RESTORE { + @Override + public void restoreFlySpeed(Player player, LimboPlayer limbo) { + logger.debug(() -> "Restoring fly speed for LimboPlayer " + player.getName() + " to " + + limbo.getFlySpeed() + " (RESTORE mode)"); + player.setFlySpeed(limbo.getFlySpeed()); + } + + @Override + public void restoreWalkSpeed(Player player, LimboPlayer limbo) { + logger.debug(() -> "Restoring walk speed for LimboPlayer " + player.getName() + " to " + + limbo.getWalkSpeed() + " (RESTORE mode)"); + player.setWalkSpeed(limbo.getWalkSpeed()); + } + }, + + /** + * Restores from LimboPlayer, using the default speed if the speed on LimboPlayer is 0. + */ + RESTORE_NO_ZERO { + @Override + public void restoreFlySpeed(Player player, LimboPlayer limbo) { + float limboFlySpeed = limbo.getFlySpeed(); + if (limboFlySpeed > 0.01f) { + logger.debug(() -> "Restoring fly speed for LimboPlayer " + player.getName() + " to " + + limboFlySpeed + " (RESTORE_NO_ZERO mode)"); + player.setFlySpeed(limboFlySpeed); + } else { + logger.debug(() -> "Restoring fly speed for LimboPlayer " + player.getName() + + " to DEFAULT, it was 0! (RESTORE_NO_ZERO mode)"); + player.setFlySpeed(LimboPlayer.DEFAULT_FLY_SPEED); + } + } + + @Override + public void restoreWalkSpeed(Player player, LimboPlayer limbo) { + float limboWalkSpeed = limbo.getWalkSpeed(); + if (limboWalkSpeed > 0.01f) { + logger.debug(() -> "Restoring walk speed for LimboPlayer " + player.getName() + " to " + + limboWalkSpeed + " (RESTORE_NO_ZERO mode)"); + player.setWalkSpeed(limboWalkSpeed); + } else { + logger.debug(() -> "Restoring walk speed for LimboPlayer " + player.getName() + "" + + " to DEFAULT, it was 0! (RESTORE_NO_ZERO mode)"); + player.setWalkSpeed(LimboPlayer.DEFAULT_WALK_SPEED); + } + } + }, + + /** + * Uses the max speed of Player (current speed) and the LimboPlayer. + */ + MAX_RESTORE { + @Override + public void restoreFlySpeed(Player player, LimboPlayer limbo) { + float newSpeed = Math.max(player.getFlySpeed(), limbo.getFlySpeed()); + logger.debug(() -> "Restoring fly speed for LimboPlayer " + player.getName() + " to " + newSpeed + + " (Current: " + player.getFlySpeed() + ", Limbo: " + limbo.getFlySpeed() + ") (MAX_RESTORE mode)"); + player.setFlySpeed(newSpeed); + } + + @Override + public void restoreWalkSpeed(Player player, LimboPlayer limbo) { + float newSpeed = Math.max(player.getWalkSpeed(), limbo.getWalkSpeed()); + logger.debug(() -> "Restoring walk speed for LimboPlayer " + player.getName() + " to " + newSpeed + + " (Current: " + player.getWalkSpeed() + ", Limbo: " + limbo.getWalkSpeed() + ") (MAX_RESTORE mode)"); + player.setWalkSpeed(newSpeed); + } + }, + + /** + * Always sets the default speed to the player. + */ + DEFAULT { + @Override + public void restoreFlySpeed(Player player, LimboPlayer limbo) { + logger.debug(() -> "Restoring fly speed for LimboPlayer " + player.getName() + + " to DEFAULT (DEFAULT mode)"); + player.setFlySpeed(LimboPlayer.DEFAULT_FLY_SPEED); + } + + @Override + public void restoreWalkSpeed(Player player, LimboPlayer limbo) { + logger.debug(() -> "Restoring walk speed for LimboPlayer " + player.getName() + + " to DEFAULT (DEFAULT mode)"); + player.setWalkSpeed(LimboPlayer.DEFAULT_WALK_SPEED); + } + }; + + private static final ConsoleLogger logger = ConsoleLoggerFactory.get(WalkFlySpeedRestoreType.class); + + /** + * Restores the fly speed from Limbo to Player according to the restoration type. + * + * @param player the player to modify + * @param limbo the limbo player to read from + */ + public abstract void restoreFlySpeed(Player player, LimboPlayer limbo); + + /** + * Restores the walk speed from Limbo to Player according to the restoration type. + * + * @param player the player to modify + * @param limbo the limbo player to read from + */ + public abstract void restoreWalkSpeed(Player player, LimboPlayer limbo); + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/limbo/persistence/DistributedFilesPersistenceHandler.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/limbo/persistence/DistributedFilesPersistenceHandler.java new file mode 100644 index 00000000..9708ce88 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/limbo/persistence/DistributedFilesPersistenceHandler.java @@ -0,0 +1,228 @@ +package fr.xephi.authme.data.limbo.persistence; + +import com.google.common.io.Files; +import com.google.common.reflect.TypeToken; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.data.limbo.LimboPlayer; +import fr.xephi.authme.initialization.DataFolder; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.service.BukkitService; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.properties.LimboSettings; +import fr.xephi.authme.util.FileUtils; +import org.bukkit.entity.Player; + +import javax.inject.Inject; +import java.io.File; +import java.io.FileWriter; +import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * Persistence handler for LimboPlayer objects by distributing the objects to store + * in various segments (buckets) based on the start of the player's UUID. + */ +class DistributedFilesPersistenceHandler implements LimboPersistenceHandler { + + private static final Type LIMBO_MAP_TYPE = new TypeToken>(){}.getType(); + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(DistributedFilesPersistenceHandler.class); + private final File cacheFolder; + private final Gson gson; + private final SegmentNameBuilder segmentNameBuilder; + + @Inject + DistributedFilesPersistenceHandler(@DataFolder File dataFolder, BukkitService bukkitService, Settings settings) { + cacheFolder = new File(dataFolder, "playerdata"); + FileUtils.createDirectory(cacheFolder); + + gson = new GsonBuilder() + .registerTypeAdapter(LimboPlayer.class, new LimboPlayerSerializer()) + .registerTypeAdapter(LimboPlayer.class, new LimboPlayerDeserializer(bukkitService)) + .setPrettyPrinting() + .create(); + + segmentNameBuilder = new SegmentNameBuilder(settings.getProperty(LimboSettings.DISTRIBUTION_SIZE)); + + convertOldDataToCurrentSegmentScheme(); + deleteEmptyFiles(); + } + + @Override + public LimboPlayer getLimboPlayer(Player player) { + String uuid = player.getUniqueId().toString(); + File file = getPlayerSegmentFile(uuid); + Map entries = readLimboPlayers(file); + return entries == null ? null : entries.get(uuid); + } + + @Override + public void saveLimboPlayer(Player player, LimboPlayer limbo) { + String uuid = player.getUniqueId().toString(); + File file = getPlayerSegmentFile(uuid); + + Map entries = null; + if (file.exists()) { + entries = readLimboPlayers(file); + } else { + FileUtils.create(file); + } + /* intentionally separate if */ + if (entries == null) { + entries = new HashMap<>(); + } + + entries.put(uuid, limbo); + saveEntries(entries, file); + } + + @Override + public void removeLimboPlayer(Player player) { + String uuid = player.getUniqueId().toString(); + File file = getPlayerSegmentFile(uuid); + if (file.exists()) { + Map entries = readLimboPlayers(file); + if (entries != null && entries.remove(uuid) != null) { + saveEntries(entries, file); + } + } + } + + @Override + public LimboPersistenceType getType() { + return LimboPersistenceType.DISTRIBUTED_FILES; + } + + private void saveEntries(Map entries, File file) { + try (FileWriter fw = new FileWriter(file)) { + gson.toJson(entries, fw); + } catch (Exception e) { + logger.logException("Could not write to '" + file + "':", e); + } + } + + private Map readLimboPlayers(File file) { + if (!file.exists()) { + return null; + } + + try { + return gson.fromJson(Files.asCharSource(file, StandardCharsets.UTF_8).read(), LIMBO_MAP_TYPE); + } catch (Exception e) { + logger.logException("Failed reading '" + file + "':", e); + } + return null; + } + + private File getPlayerSegmentFile(String uuid) { + String segment = segmentNameBuilder.createSegmentName(uuid); + return getSegmentFile(segment); + } + + private File getSegmentFile(String segmentId) { + return new File(cacheFolder, segmentId + "-limbo.json"); + } + + /** + * Loads segment files in the cache folder that don't correspond to the current segmenting scheme + * and migrates the data into files of the current segments. This allows a player to change the + * segment size without any loss of data. + */ + private void convertOldDataToCurrentSegmentScheme() { + String currentPrefix = segmentNameBuilder.getPrefix(); + File[] files = listFiles(cacheFolder); + Map allLimboPlayers = new HashMap<>(); + List migratedFiles = new ArrayList<>(); + + for (File file : files) { + if (isLimboJsonFile(file) && !file.getName().startsWith(currentPrefix)) { + Map data = readLimboPlayers(file); + if (data != null) { + allLimboPlayers.putAll(data); + migratedFiles.add(file); + } + } + } + + if (!allLimboPlayers.isEmpty()) { + saveToNewSegments(allLimboPlayers); + migratedFiles.forEach(FileUtils::delete); + } + } + + /** + * Saves the LimboPlayer data read from old segmenting schemes into the current segmenting scheme. + * + * @param limbosFromOldSegments the limbo players to store into the current segment files + */ + private void saveToNewSegments(Map limbosFromOldSegments) { + Map> limboBySegment = groupBySegment(limbosFromOldSegments); + + logger.info("Saving " + limbosFromOldSegments.size() + " LimboPlayers from old segments into " + + limboBySegment.size() + " current segments"); + for (Map.Entry> entry : limboBySegment.entrySet()) { + File file = getSegmentFile(entry.getKey()); + Map limbosToSave = Optional.ofNullable(readLimboPlayers(file)) + .orElseGet(HashMap::new); + limbosToSave.putAll(entry.getValue()); + saveEntries(limbosToSave, file); + } + } + + /** + * Converts a Map of UUID to LimboPlayers to a 2-dimensional Map of LimboPlayers by segment ID and UUID. + * {@code Map(uuid -> LimboPlayer) to Map(segment -> Map(uuid -> LimboPlayer))} + * + * @param readLimboPlayers the limbo players to order by segment + * @return limbo players ordered by segment ID and associated player UUID + */ + private Map> groupBySegment(Map readLimboPlayers) { + Map> limboBySegment = new HashMap<>(); + for (Map.Entry entry : readLimboPlayers.entrySet()) { + String segmentId = segmentNameBuilder.createSegmentName(entry.getKey()); + limboBySegment.computeIfAbsent(segmentId, s -> new HashMap<>()) + .put(entry.getKey(), entry.getValue()); + } + return limboBySegment; + } + + /** + * Deletes segment files that are empty. + */ + private void deleteEmptyFiles() { + File[] files = listFiles(cacheFolder); + + long deletedFiles = Arrays.stream(files) + // typically the size is 2 because there's an empty JSON map: {} + .filter(f -> isLimboJsonFile(f) && f.length() < 3) + .peek(FileUtils::delete) + .count(); + logger.debug("Limbo: Deleted {0} empty segment files", deletedFiles); + } + + /** + * @param file the file to check + * @return true if it is a segment file storing Limbo data, false otherwise + */ + private static boolean isLimboJsonFile(File file) { + String name = file.getName(); + return name.startsWith("seg") && name.endsWith("-limbo.json"); + } + + private File[] listFiles(File folder) { + File[] files = folder.listFiles(); + if (files == null) { + logger.warning("Could not get files of '" + folder + "'"); + return new File[0]; + } + return files; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/limbo/persistence/IndividualFilesPersistenceHandler.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/limbo/persistence/IndividualFilesPersistenceHandler.java new file mode 100644 index 00000000..9772ec06 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/limbo/persistence/IndividualFilesPersistenceHandler.java @@ -0,0 +1,92 @@ +package fr.xephi.authme.data.limbo.persistence; + +import com.google.common.io.Files; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.data.limbo.LimboPlayer; +import fr.xephi.authme.initialization.DataFolder; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.service.BukkitService; +import fr.xephi.authme.util.FileUtils; +import org.bukkit.entity.Player; + +import javax.inject.Inject; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +/** + * Saves LimboPlayer objects as JSON into individual files. + */ +class IndividualFilesPersistenceHandler implements LimboPersistenceHandler { + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(IndividualFilesPersistenceHandler.class); + + private final Gson gson; + private final File cacheDir; + + @Inject + IndividualFilesPersistenceHandler(@DataFolder File dataFolder, BukkitService bukkitService) { + cacheDir = new File(dataFolder, "playerdata"); + if (!cacheDir.exists() && !cacheDir.isDirectory() && !cacheDir.mkdir()) { + logger.warning("Failed to create playerdata directory '" + cacheDir + "'"); + } + gson = new GsonBuilder() + .registerTypeAdapter(LimboPlayer.class, new LimboPlayerSerializer()) + .registerTypeAdapter(LimboPlayer.class, new LimboPlayerDeserializer(bukkitService)) + .setPrettyPrinting() + .create(); + } + + @Override + public LimboPlayer getLimboPlayer(Player player) { + String id = player.getUniqueId().toString(); + File file = new File(cacheDir, id + File.separator + "data.json"); + if (!file.exists()) { + return null; + } + + try { + String str = Files.asCharSource(file, StandardCharsets.UTF_8).read(); + return gson.fromJson(str, LimboPlayer.class); + } catch (IOException e) { + logger.logException("Could not read player data on disk for '" + player.getName() + "'", e); + return null; + } + } + + @Override + public void saveLimboPlayer(Player player, LimboPlayer limboPlayer) { + String id = player.getUniqueId().toString(); + try { + File file = new File(cacheDir, id + File.separator + "data.json"); + Files.createParentDirs(file); + Files.touch(file); + Files.write(gson.toJson(limboPlayer), file, StandardCharsets.UTF_8); + } catch (IOException e) { + logger.logException("Failed to write " + player.getName() + " data:", e); + } + } + + /** + * Removes the LimboPlayer. This will delete the + * "playerdata/<uuid or name>/" folder from disk. + * + * @param player player to remove + */ + @Override + public void removeLimboPlayer(Player player) { + String id = player.getUniqueId().toString(); + File file = new File(cacheDir, id); + if (file.exists()) { + FileUtils.purgeDirectory(file); + FileUtils.delete(file); + } + } + + @Override + public LimboPersistenceType getType() { + return LimboPersistenceType.INDIVIDUAL_FILES; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/limbo/persistence/LimboPersistence.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/limbo/persistence/LimboPersistence.java new file mode 100644 index 00000000..844c9cdf --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/limbo/persistence/LimboPersistence.java @@ -0,0 +1,82 @@ +package fr.xephi.authme.data.limbo.persistence; + +import ch.jalu.injector.factory.Factory; +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.data.limbo.LimboPlayer; +import fr.xephi.authme.initialization.SettingsDependent; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.properties.LimboSettings; +import org.bukkit.entity.Player; + +import javax.inject.Inject; + +/** + * Handles the persistence of LimboPlayers. + */ +public class LimboPersistence implements SettingsDependent { + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(LimboPersistence.class); + + private final Factory handlerFactory; + + private LimboPersistenceHandler handler; + + @Inject + LimboPersistence(Settings settings, Factory handlerFactory) { + this.handlerFactory = handlerFactory; + reload(settings); + } + + /** + * Retrieves the LimboPlayer for the given player if available. + * + * @param player the player to retrieve the LimboPlayer for + * @return the player's limbo player, or null if not available + */ + public LimboPlayer getLimboPlayer(Player player) { + try { + return handler.getLimboPlayer(player); + } catch (Exception e) { + logger.logException("Could not get LimboPlayer for '" + player.getName() + "'", e); + } + return null; + } + + /** + * Saves the given LimboPlayer for the provided player. + * + * @param player the player to save the LimboPlayer for + * @param limbo the limbo player to save + */ + public void saveLimboPlayer(Player player, LimboPlayer limbo) { + try { + handler.saveLimboPlayer(player, limbo); + } catch (Exception e) { + logger.logException("Could not save LimboPlayer for '" + player.getName() + "'", e); + } + } + + /** + * Removes the LimboPlayer for the given player. + * + * @param player the player whose LimboPlayer should be removed + */ + public void removeLimboPlayer(Player player) { + try { + handler.removeLimboPlayer(player); + } catch (Exception e) { + logger.logException("Could not remove LimboPlayer for '" + player.getName() + "'", e); + } + } + + @Override + public void reload(Settings settings) { + LimboPersistenceType persistenceType = settings.getProperty(LimboSettings.LIMBO_PERSISTENCE_TYPE); + // If we're changing from an existing handler, output a quick hint that nothing is converted. + if (handler != null && handler.getType() != persistenceType) { + logger.info("Limbo persistence type has changed! Note that the data is not converted."); + } + handler = handlerFactory.newInstance(persistenceType.getImplementationClass()); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/limbo/persistence/LimboPersistenceHandler.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/limbo/persistence/LimboPersistenceHandler.java new file mode 100644 index 00000000..95e88aad --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/limbo/persistence/LimboPersistenceHandler.java @@ -0,0 +1,39 @@ +package fr.xephi.authme.data.limbo.persistence; + +import fr.xephi.authme.data.limbo.LimboPlayer; +import org.bukkit.entity.Player; + +/** + * Handles I/O for storing LimboPlayer objects. + */ +interface LimboPersistenceHandler { + + /** + * Returns the limbo player for the given player if it exists. + * + * @param player the player + * @return the stored limbo player, or null if not available + */ + LimboPlayer getLimboPlayer(Player player); + + /** + * Saves the given limbo player for the given player to the disk. + * + * @param player the player to save the limbo player for + * @param limbo the limbo player to save + */ + void saveLimboPlayer(Player player, LimboPlayer limbo); + + /** + * Removes the limbo player from the disk. + * + * @param player the player whose limbo player should be removed + */ + void removeLimboPlayer(Player player); + + /** + * @return the type of the limbo persistence implementation + */ + LimboPersistenceType getType(); + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/limbo/persistence/LimboPersistenceType.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/limbo/persistence/LimboPersistenceType.java new file mode 100644 index 00000000..8119e669 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/limbo/persistence/LimboPersistenceType.java @@ -0,0 +1,34 @@ +package fr.xephi.authme.data.limbo.persistence; + +/** + * Types of persistence for LimboPlayer objects. + */ +public enum LimboPersistenceType { + + /** Store each LimboPlayer in a separate file. */ + INDIVIDUAL_FILES(IndividualFilesPersistenceHandler.class), + + /** Store LimboPlayers distributed in a configured number of files. */ + DISTRIBUTED_FILES(DistributedFilesPersistenceHandler.class), + + /** No persistence to disk. */ + DISABLED(NoOpPersistenceHandler.class); + + private final Class implementationClass; + + /** + * Constructor. + * + * @param implementationClass the implementation class + */ + LimboPersistenceType(Class implementationClass) { + this.implementationClass= implementationClass; + } + + /** + * @return class implementing the persistence type + */ + public Class getImplementationClass() { + return implementationClass; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/limbo/persistence/LimboPlayerDeserializer.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/limbo/persistence/LimboPlayerDeserializer.java new file mode 100644 index 00000000..6dea20ef --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/limbo/persistence/LimboPlayerDeserializer.java @@ -0,0 +1,163 @@ +package fr.xephi.authme.data.limbo.persistence; + +import com.google.common.reflect.TypeToken; +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import fr.xephi.authme.data.limbo.LimboPlayer; +import fr.xephi.authme.data.limbo.UserGroup; +import fr.xephi.authme.service.BukkitService; +import org.bukkit.Location; +import org.bukkit.World; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +import static fr.xephi.authme.data.limbo.persistence.LimboPlayerSerializer.CAN_FLY; +import static fr.xephi.authme.data.limbo.persistence.LimboPlayerSerializer.FLY_SPEED; +import static fr.xephi.authme.data.limbo.persistence.LimboPlayerSerializer.GROUPS; +import static fr.xephi.authme.data.limbo.persistence.LimboPlayerSerializer.IS_OP; +import static fr.xephi.authme.data.limbo.persistence.LimboPlayerSerializer.LOCATION; +import static fr.xephi.authme.data.limbo.persistence.LimboPlayerSerializer.LOC_PITCH; +import static fr.xephi.authme.data.limbo.persistence.LimboPlayerSerializer.LOC_WORLD; +import static fr.xephi.authme.data.limbo.persistence.LimboPlayerSerializer.LOC_X; +import static fr.xephi.authme.data.limbo.persistence.LimboPlayerSerializer.LOC_Y; +import static fr.xephi.authme.data.limbo.persistence.LimboPlayerSerializer.LOC_YAW; +import static fr.xephi.authme.data.limbo.persistence.LimboPlayerSerializer.LOC_Z; +import static fr.xephi.authme.data.limbo.persistence.LimboPlayerSerializer.WALK_SPEED; +import static java.util.Optional.ofNullable; + +/** + * Converts a JsonElement to a LimboPlayer. + */ +class LimboPlayerDeserializer implements JsonDeserializer { + + private static final String GROUP_LEGACY = "group"; + private static final String CONTEXT_MAP = "contextMap"; + private static final String GROUP_NAME = "groupName"; + + private BukkitService bukkitService; + + LimboPlayerDeserializer(BukkitService bukkitService) { + this.bukkitService = bukkitService; + } + + @Override + public LimboPlayer deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext context) { + JsonObject jsonObject = jsonElement.getAsJsonObject(); + if (jsonObject == null) { + return null; + } + + Location loc = deserializeLocation(jsonObject); + boolean operator = getBoolean(jsonObject, IS_OP); + + Collection groups = getLimboGroups(jsonObject); + boolean canFly = getBoolean(jsonObject, CAN_FLY); + float walkSpeed = getFloat(jsonObject, WALK_SPEED, LimboPlayer.DEFAULT_WALK_SPEED); + float flySpeed = getFloat(jsonObject, FLY_SPEED, LimboPlayer.DEFAULT_FLY_SPEED); + + return new LimboPlayer(loc, operator, groups, canFly, walkSpeed, flySpeed); + } + + private Location deserializeLocation(JsonObject jsonObject) { + JsonElement e; + if ((e = jsonObject.getAsJsonObject(LOCATION)) != null) { + JsonObject locationObject = e.getAsJsonObject(); + World world = bukkitService.getWorld(getString(locationObject, LOC_WORLD)); + if (world != null) { + double x = getDouble(locationObject, LOC_X); + double y = getDouble(locationObject, LOC_Y); + double z = getDouble(locationObject, LOC_Z); + float yaw = getFloat(locationObject, LOC_YAW); + float pitch = getFloat(locationObject, LOC_PITCH); + return new Location(world, x, y, z, yaw, pitch); + } + } + return null; + } + + private static String getString(JsonObject jsonObject, String memberName) { + JsonElement element = jsonObject.get(memberName); + return element != null ? element.getAsString() : ""; + } + + /** + * @param jsonObject LimboPlayer represented as JSON + * @return The list of UserGroups create from JSON + */ + private static List getLimboGroups(JsonObject jsonObject) { + JsonElement element = jsonObject.get(GROUPS); + if (element == null) { + String legacyGroup = ofNullable(jsonObject.get(GROUP_LEGACY)).map(JsonElement::getAsString).orElse(null); + return legacyGroup == null ? Collections.emptyList() : + Collections.singletonList(new UserGroup(legacyGroup, null)); + } + List result = new ArrayList<>(); + JsonArray jsonArray = element.getAsJsonArray(); + for (JsonElement arrayElement : jsonArray) { + if (!arrayElement.isJsonObject()) { + result.add(new UserGroup(arrayElement.getAsString(), null)); + } else { + JsonObject jsonGroup = arrayElement.getAsJsonObject(); + Map contextMap = null; + if (jsonGroup.has(CONTEXT_MAP)) { + JsonElement contextMapJson = jsonGroup.get("contextMap"); + Type type = new TypeToken>() { + }.getType(); + contextMap = new Gson().fromJson(contextMapJson.getAsString(), type); + } + + String groupName = jsonGroup.get(GROUP_NAME).getAsString(); + result.add(new UserGroup(groupName, contextMap)); + } + } + return result; + } + + private static boolean getBoolean(JsonObject jsonObject, String memberName) { + JsonElement element = jsonObject.get(memberName); + return element != null && element.getAsBoolean(); + } + + private static float getFloat(JsonObject jsonObject, String memberName) { + return getNumberFromElement(jsonObject.get(memberName), JsonElement::getAsFloat, 0.0f); + } + + private static float getFloat(JsonObject jsonObject, String memberName, float defaultValue) { + return getNumberFromElement(jsonObject.get(memberName), JsonElement::getAsFloat, defaultValue); + } + + private static double getDouble(JsonObject jsonObject, String memberName) { + return getNumberFromElement(jsonObject.get(memberName), JsonElement::getAsDouble, 0.0); + } + + /** + * Gets a number from the given JsonElement safely. + * + * @param jsonElement the element to retrieve the number from + * @param numberFunction the function to get the number from the element + * @param defaultValue the value to return if the element is null or the number cannot be retrieved + * @param the number type + * @return the number from the given JSON element, or the default value + */ + private static N getNumberFromElement(JsonElement jsonElement, + Function numberFunction, + N defaultValue) { + if (jsonElement != null) { + try { + return numberFunction.apply(jsonElement); + } catch (NumberFormatException ignore) { + } + } + return defaultValue; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/limbo/persistence/LimboPlayerSerializer.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/limbo/persistence/LimboPlayerSerializer.java new file mode 100644 index 00000000..eaad86b7 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/limbo/persistence/LimboPlayerSerializer.java @@ -0,0 +1,71 @@ +package fr.xephi.authme.data.limbo.persistence; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import fr.xephi.authme.data.limbo.LimboPlayer; +import org.bukkit.Location; + +import java.lang.reflect.Type; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Converts a LimboPlayer to a JsonElement. + */ +class LimboPlayerSerializer implements JsonSerializer { + + static final String LOCATION = "location"; + static final String LOC_WORLD = "world"; + static final String LOC_X = "x"; + static final String LOC_Y = "y"; + static final String LOC_Z = "z"; + static final String LOC_YAW = "yaw"; + static final String LOC_PITCH = "pitch"; + + static final String GROUPS = "groups"; + static final String IS_OP = "operator"; + static final String CAN_FLY = "can-fly"; + static final String WALK_SPEED = "walk-speed"; + static final String FLY_SPEED = "fly-speed"; + + private static final Gson GSON = new Gson(); + + + @Override + public JsonElement serialize(LimboPlayer limboPlayer, Type type, JsonSerializationContext context) { + Location loc = limboPlayer.getLocation(); + JsonObject locationObject = new JsonObject(); + locationObject.addProperty(LOC_WORLD, loc.getWorld().getName()); + locationObject.addProperty(LOC_X, loc.getX()); + locationObject.addProperty(LOC_Y, loc.getY()); + locationObject.addProperty(LOC_Z, loc.getZ()); + locationObject.addProperty(LOC_YAW, loc.getYaw()); + locationObject.addProperty(LOC_PITCH, loc.getPitch()); + + JsonObject obj = new JsonObject(); + obj.add(LOCATION, locationObject); + + List groups = limboPlayer.getGroups().stream().map(g -> { + JsonObject jsonGroup = new JsonObject(); + jsonGroup.addProperty("groupName", g.getGroupName()); + if (g.getContextMap() != null) { + jsonGroup.addProperty("contextMap", GSON.toJson(g.getContextMap())); + } + return jsonGroup; + }).collect(Collectors.toList()); + + JsonArray jsonGroups = new JsonArray(); + groups.forEach(jsonGroups::add); + obj.add(GROUPS, jsonGroups); + + obj.addProperty(IS_OP, limboPlayer.isOperator()); + obj.addProperty(CAN_FLY, limboPlayer.isCanFly()); + obj.addProperty(WALK_SPEED, limboPlayer.getWalkSpeed()); + obj.addProperty(FLY_SPEED, limboPlayer.getFlySpeed()); + return obj; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/limbo/persistence/NoOpPersistenceHandler.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/limbo/persistence/NoOpPersistenceHandler.java new file mode 100644 index 00000000..ac6ff9b3 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/limbo/persistence/NoOpPersistenceHandler.java @@ -0,0 +1,30 @@ +package fr.xephi.authme.data.limbo.persistence; + +import fr.xephi.authme.data.limbo.LimboPlayer; +import org.bukkit.entity.Player; + +/** + * Limbo player persistence implementation that does nothing. + */ +class NoOpPersistenceHandler implements LimboPersistenceHandler { + + @Override + public LimboPlayer getLimboPlayer(Player player) { + return null; + } + + @Override + public void saveLimboPlayer(Player player, LimboPlayer limbo) { + // noop + } + + @Override + public void removeLimboPlayer(Player player) { + // noop + } + + @Override + public LimboPersistenceType getType() { + return LimboPersistenceType.DISABLED; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/limbo/persistence/SegmentNameBuilder.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/limbo/persistence/SegmentNameBuilder.java new file mode 100644 index 00000000..24e0737d --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/limbo/persistence/SegmentNameBuilder.java @@ -0,0 +1,73 @@ +package fr.xephi.authme.data.limbo.persistence; + +import java.util.HashMap; +import java.util.Map; + +/** + * Creates segment names for {@link DistributedFilesPersistenceHandler}. + */ +class SegmentNameBuilder { + + private final int length; + private final int distribution; + private final String prefix; + private final Map charToSegmentChar; + + /** + * Constructor. + * + * @param partition the segment configuration + */ + SegmentNameBuilder(SegmentSize partition) { + this.length = partition.getLength(); + this.distribution = partition.getDistribution(); + this.prefix = "seg" + partition.getTotalSegments() + "-"; + this.charToSegmentChar = buildCharMap(distribution); + } + + /** + * Returns the segment ID for the given UUID. + * + * @param uuid the player's uuid to get the segment for + * @return id the uuid belongs to + */ + String createSegmentName(String uuid) { + if (distribution == 16) { + return prefix + uuid.substring(0, length); + } else { + return prefix + buildSegmentName(uuid.substring(0, length).toCharArray()); + } + } + + /** + * @return the prefix used for the current segment configuration + */ + String getPrefix() { + return prefix; + } + + private String buildSegmentName(char[] chars) { + if (chars.length == 1) { + return String.valueOf(charToSegmentChar.get(chars[0])); + } + + StringBuilder sb = new StringBuilder(chars.length); + for (char chr : chars) { + sb.append(charToSegmentChar.get(chr)); + } + return sb.toString(); + } + + private static Map buildCharMap(int distribution) { + final char[] hexChars = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; + final int divisor = 16 / distribution; + + Map charToSegmentChar = new HashMap<>(); + for (int i = 0; i < hexChars.length; ++i) { + int mappedChar = i / divisor; + charToSegmentChar.put(hexChars[i], hexChars[mappedChar]); + } + return charToSegmentChar; + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/limbo/persistence/SegmentSize.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/limbo/persistence/SegmentSize.java new file mode 100644 index 00000000..0e290cea --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/data/limbo/persistence/SegmentSize.java @@ -0,0 +1,94 @@ +package fr.xephi.authme.data.limbo.persistence; + +/** + * Configuration for the total number of segments to use. + *

+ * The {@link DistributedFilesPersistenceHandler} reduces the number of files by assigning each UUID + * to a segment. This enum allows to define how many segments the UUIDs should be distributed in. + *

+ * Segments are defined by a distribution and a length. The distribution defines + * to how many outputs a single hexadecimal characters should be mapped. So e.g. a distribution + * of 3 means that all hexadecimal characters 0-f should be distributed over three different + * outputs evenly. The {@link SegmentNameBuilder} simply uses hexadecimal characters as outputs, + * so e.g. with a distribution of 3 all hex characters 0-f are mapped to 0, 1, or 2. + *

+ * To ensure an even distribution the segments must be powers of 2. Trivially, to implement a + * distribution of 16, the same character may be returned as was input (since 0-f make up 16 + * characters). A distribution of 1, on the other hand, means that the same output is returned + * regardless of the input character. + *

+ * The length parameter defines how many characters of a player's UUID should be used to + * create the segment ID. In other words, with a distribution of 2 and a length of 3, the first + * three characters of the UUID are taken into consideration, each mapped to one of two possible + * characters. For instance, a UUID starting with "0f5c9321" may yield the segment ID "010." + * Such a segment ID defines in which file the given UUID can be found and stored. + *

+ * The number of segments such a configuration yields is computed as {@code distribution ^ length}, + * since distribution defines how many outputs there are per digit, and length defines the number + * of digits. For instance, a distribution of 2 and a length of 3 will yield segment IDs 000, 001, + * 010, 011, 100, 101, 110 and 111 (i.e. all binary numbers from 0 to 7). + *

+ * There are multiple possibilities to achieve certain segment totals, e.g. 8 different segments + * may be created by setting distribution to 8 and length to 1, or distr. to 2 and length to 3. + * Where possible, prefer a length of 1 (no string concatenation required) or a distribution of + * 16 (no remapping of the characters required). + */ +public enum SegmentSize { + + /** 1. */ + ONE(1, 1), + + // /** 2. */ + // TWO(2, 1), + + /** 4. */ + FOUR(4, 1), + + /** 8. */ + EIGHT(8, 1), + + /** 16. */ + SIXTEEN(16, 1), + + /** 32. */ + THIRTY_TWO(2, 5), + + /** 64. */ + SIXTY_FOUR(4, 3), + + /** 128. */ + ONE_TWENTY(2, 7), + + /** 256. */ + TWO_FIFTY(16, 2); + + private final int distribution; + private final int length; + + SegmentSize(int distribution, int length) { + this.distribution = distribution; + this.length = length; + } + + /** + * @return the distribution size per character, i.e. how many possible outputs there are + * for any hexadecimal character + */ + public int getDistribution() { + return distribution; + } + + /** + * @return number of characters from a UUID that should be used to create a segment ID + */ + public int getLength() { + return length; + } + + /** + * @return number of segments to which this configuration will distribute all UUIDs + */ + public int getTotalSegments() { + return (int) Math.pow(distribution, length); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/AbstractSqlDataSource.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/AbstractSqlDataSource.java new file mode 100644 index 00000000..6c12a171 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/AbstractSqlDataSource.java @@ -0,0 +1,172 @@ +package fr.xephi.authme.datasource; + +import ch.jalu.datasourcecolumns.data.DataSourceValue; +import ch.jalu.datasourcecolumns.data.DataSourceValueImpl; +import ch.jalu.datasourcecolumns.data.DataSourceValues; +import ch.jalu.datasourcecolumns.predicate.AlwaysTruePredicate; +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.datasource.columnshandler.AuthMeColumns; +import fr.xephi.authme.datasource.columnshandler.AuthMeColumnsHandler; +import fr.xephi.authme.security.crypts.HashedPassword; + +import java.sql.SQLException; +import java.util.Collections; +import java.util.List; + +import static ch.jalu.datasourcecolumns.data.UpdateValues.with; +import static ch.jalu.datasourcecolumns.predicate.StandardPredicates.eq; +import static ch.jalu.datasourcecolumns.predicate.StandardPredicates.eqIgnoreCase; +import static fr.xephi.authme.datasource.SqlDataSourceUtils.logSqlException; + +/** + * Common type for SQL-based data sources. Classes implementing this + * must ensure that {@link #columnsHandler} is initialized on creation. + */ +public abstract class AbstractSqlDataSource implements DataSource { + + protected AuthMeColumnsHandler columnsHandler; + + @Override + public boolean isAuthAvailable(String user) { + try { + return columnsHandler.retrieve(user, AuthMeColumns.NAME).rowExists(); + } catch (SQLException e) { + logSqlException(e); + return false; + } + } + + @Override + public HashedPassword getPassword(String user) { + try { + DataSourceValues values = columnsHandler.retrieve(user, AuthMeColumns.PASSWORD, AuthMeColumns.SALT); + if (values.rowExists()) { + return new HashedPassword(values.get(AuthMeColumns.PASSWORD), values.get(AuthMeColumns.SALT)); + } + } catch (SQLException e) { + logSqlException(e); + } + return null; + } + + @Override + public boolean saveAuth(PlayerAuth auth) { + return columnsHandler.insert(auth, + AuthMeColumns.NAME, AuthMeColumns.NICK_NAME, AuthMeColumns.PASSWORD, AuthMeColumns.SALT, + AuthMeColumns.EMAIL, AuthMeColumns.REGISTRATION_DATE, AuthMeColumns.REGISTRATION_IP, + AuthMeColumns.UUID); + } + + @Override + public boolean hasSession(String user) { + try { + DataSourceValue result = columnsHandler.retrieve(user, AuthMeColumns.HAS_SESSION); + return result.rowExists() && Integer.valueOf(1).equals(result.getValue()); + } catch (SQLException e) { + logSqlException(e); + return false; + } + } + + @Override + public boolean updateSession(PlayerAuth auth) { + return columnsHandler.update(auth, AuthMeColumns.LAST_IP, AuthMeColumns.LAST_LOGIN, AuthMeColumns.NICK_NAME); + } + + @Override + public boolean updatePassword(PlayerAuth auth) { + return updatePassword(auth.getNickname(), auth.getPassword()); + } + + @Override + public boolean updatePassword(String user, HashedPassword password) { + return columnsHandler.update(user, + with(AuthMeColumns.PASSWORD, password.getHash()) + .and(AuthMeColumns.SALT, password.getSalt()).build()); + } + + @Override + public boolean updateQuitLoc(PlayerAuth auth) { + return columnsHandler.update(auth, + AuthMeColumns.LOCATION_X, AuthMeColumns.LOCATION_Y, AuthMeColumns.LOCATION_Z, + AuthMeColumns.LOCATION_WORLD, AuthMeColumns.LOCATION_YAW, AuthMeColumns.LOCATION_PITCH); + } + + @Override + public List getAllAuthsByIp(String ip) { + try { + return columnsHandler.retrieve(eq(AuthMeColumns.LAST_IP, ip), AuthMeColumns.NAME); + } catch (SQLException e) { + logSqlException(e); + return Collections.emptyList(); + } + } + + @Override + public int countAuthsByEmail(String email) { + return columnsHandler.count(eqIgnoreCase(AuthMeColumns.EMAIL, email)); + } + + @Override + public boolean updateEmail(PlayerAuth auth) { + return columnsHandler.update(auth, AuthMeColumns.EMAIL); + } + + @Override + public boolean isLogged(String user) { + try { + DataSourceValue result = columnsHandler.retrieve(user, AuthMeColumns.IS_LOGGED); + return result.rowExists() && Integer.valueOf(1).equals(result.getValue()); + } catch (SQLException e) { + logSqlException(e); + return false; + } + } + + @Override + public void setLogged(String user) { + columnsHandler.update(user, AuthMeColumns.IS_LOGGED, 1); + } + + @Override + public void setUnlogged(String user) { + columnsHandler.update(user, AuthMeColumns.IS_LOGGED, 0); + } + + @Override + public void grantSession(String user) { + columnsHandler.update(user, AuthMeColumns.HAS_SESSION, 1); + } + + @Override + public void revokeSession(String user) { + columnsHandler.update(user, AuthMeColumns.HAS_SESSION, 0); + } + + @Override + public void purgeLogged() { + columnsHandler.update(eq(AuthMeColumns.IS_LOGGED, 1), AuthMeColumns.IS_LOGGED, 0); + } + + @Override + public int getAccountsRegistered() { + return columnsHandler.count(new AlwaysTruePredicate<>()); + } + + @Override + public boolean updateRealName(String user, String realName) { + return columnsHandler.update(user, AuthMeColumns.NICK_NAME, realName); + } + + @Override + public DataSourceValue getEmail(String user) { + try { + return columnsHandler.retrieve(user, AuthMeColumns.EMAIL); + } catch (SQLException e) { + logSqlException(e); + return DataSourceValueImpl.unknownRow(); + } + } + + abstract String getJdbcUrl(String host, String port, String database); +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/CacheDataSource.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/CacheDataSource.java new file mode 100644 index 00000000..450fd051 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/CacheDataSource.java @@ -0,0 +1,323 @@ +package fr.xephi.authme.datasource; + +import ch.jalu.datasourcecolumns.data.DataSourceValue; +import ch.jalu.datasourcecolumns.data.DataSourceValueImpl; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +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.AuthMe; +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.data.auth.PlayerCache; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.security.crypts.HashedPassword; +import fr.xephi.authme.settings.properties.DatabaseSettings; +import fr.xephi.authme.util.Utils; + +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +public class CacheDataSource implements DataSource { + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(CacheDataSource.class); + private final DataSource source; + private final PlayerCache playerCache; + private final LoadingCache> cachedAuths; + private ListeningExecutorService executorService; + + /** + * Constructor for CacheDataSource. + * + * @param source the source + * @param playerCache the player cache + */ + public CacheDataSource(DataSource source, PlayerCache playerCache) { + this.source = source; + this.playerCache = playerCache; + if (AuthMe.settings.getProperty(DatabaseSettings.USE_VIRTUAL_THREADS)) { + try { + Method method = Executors.class.getMethod("newVirtualThreadPerTaskExecutor"); + method.setAccessible(true); + ExecutorService ex = (ExecutorService) method.invoke(null); + executorService = MoreExecutors.listeningDecorator(ex); + logger.info("Using virtual threads for cache loader"); + } catch (Exception e) { + executorService = MoreExecutors.listeningDecorator( + Executors.newCachedThreadPool(new ThreadFactoryBuilder() + .setDaemon(true) + .setNameFormat("AuthMe-CacheLoader") + .build()) + ); + logger.info("Cannot enable virtual threads, fallback to CachedThreadPool"); + } + } else { + executorService = MoreExecutors.listeningDecorator( + Executors.newCachedThreadPool(new ThreadFactoryBuilder() + .setDaemon(true) + .setNameFormat("AuthMe-CacheLoader") + .build()) + ); + } + cachedAuths = CacheBuilder.newBuilder() + .refreshAfterWrite(5, TimeUnit.MINUTES) + .expireAfterAccess(15, TimeUnit.MINUTES) + .build(new CacheLoader>() { + @Override + public Optional load(String key) { + return Optional.ofNullable(source.getAuth(key)); + } + + @Override + public ListenableFuture> reload(final String key, Optional oldValue) { + return executorService.submit(() -> load(key)); + } + }); + } + + public LoadingCache> getCachedAuths() { + return cachedAuths; + } + + @Override + public void reload() { + source.reload(); + } + + @Override + public boolean isCached() { + return true; + } + + @Override + public boolean isAuthAvailable(String user) { + return getAuth(user) != null; + } + + @Override + public HashedPassword getPassword(String user) { + user = user.toLowerCase(Locale.ROOT); + Optional pAuthOpt = cachedAuths.getIfPresent(user); + if (pAuthOpt != null && pAuthOpt.isPresent()) { + return pAuthOpt.get().getPassword(); + } + return source.getPassword(user); + } + + @Override + public PlayerAuth getAuth(String user) { + user = user.toLowerCase(Locale.ROOT); + return cachedAuths.getUnchecked(user).orElse(null); + } + + @Override + public boolean saveAuth(PlayerAuth auth) { + boolean result = source.saveAuth(auth); + if (result) { + cachedAuths.refresh(auth.getNickname()); + } + return result; + } + + @Override + public boolean updatePassword(PlayerAuth auth) { + boolean result = source.updatePassword(auth); + if (result) { + cachedAuths.refresh(auth.getNickname()); + } + return result; + } + + @Override + public boolean updatePassword(String user, HashedPassword password) { + user = user.toLowerCase(Locale.ROOT); + boolean result = source.updatePassword(user, password); + if (result) { + cachedAuths.refresh(user); + } + return result; + } + + @Override + public boolean updateSession(PlayerAuth auth) { + boolean result = source.updateSession(auth); + if (result) { + cachedAuths.refresh(auth.getNickname()); + } + return result; + } + + @Override + public boolean updateQuitLoc(final PlayerAuth auth) { + boolean result = source.updateQuitLoc(auth); + if (result) { + cachedAuths.refresh(auth.getNickname()); + } + return result; + } + + @Override + public Set getRecordsToPurge(long until) { + return source.getRecordsToPurge(until); + } + + @Override + public boolean removeAuth(String name) { + name = name.toLowerCase(Locale.ROOT); + boolean result = source.removeAuth(name); + if (result) { + cachedAuths.invalidate(name); + } + return result; + } + + @Override + public void closeConnection() { + executorService.shutdown(); + try { + executorService.awaitTermination(5, TimeUnit.SECONDS); + } catch (InterruptedException e) { + logger.logException("Could not close executor service:", e); + } + cachedAuths.invalidateAll(); + source.closeConnection(); + } + + @Override + public boolean updateEmail(final PlayerAuth auth) { + boolean result = source.updateEmail(auth); + if (result) { + cachedAuths.refresh(auth.getNickname()); + } + return result; + } + + @Override + public List getAllAuthsByIp(String ip) { + return source.getAllAuthsByIp(ip); + } + + @Override + public int countAuthsByEmail(String email) { + return source.countAuthsByEmail(email); + } + + @Override + public void purgeRecords(Collection banned) { + source.purgeRecords(banned); + cachedAuths.invalidateAll(banned); + } + + @Override + public DataSourceType getType() { + return source.getType(); + } + + @Override + public boolean isLogged(String user) { + return source.isLogged(user); + } + + @Override + public void setLogged(final String user) { + source.setLogged(user.toLowerCase(Locale.ROOT)); + } + + @Override + public void setUnlogged(final String user) { + source.setUnlogged(user.toLowerCase(Locale.ROOT)); + } + + @Override + public boolean hasSession(final String user) { + return source.hasSession(user); + } + + @Override + public void grantSession(final String user) { + source.grantSession(user); + } + + @Override + public void revokeSession(final String user) { + source.revokeSession(user); + } + + @Override + public void purgeLogged() { + source.purgeLogged(); + cachedAuths.invalidateAll(); + } + + @Override + public int getAccountsRegistered() { + return source.getAccountsRegistered(); + } + + @Override + public boolean updateRealName(String user, String realName) { + boolean result = source.updateRealName(user, realName); + if (result) { + cachedAuths.refresh(user); + } + return result; + } + + @Override + public DataSourceValue getEmail(String user) { + return cachedAuths.getUnchecked(user) + .map(auth -> DataSourceValueImpl.of(auth.getEmail())) + .orElse(DataSourceValueImpl.unknownRow()); + } + + @Override + public List getAllAuths() { + return source.getAllAuths(); + } + + @Override + public List getLoggedPlayersWithEmptyMail() { + return playerCache.getCache().values().stream() + .filter(auth -> Utils.isEmailEmpty(auth.getEmail())) + .map(PlayerAuth::getRealName) + .collect(Collectors.toList()); + } + + @Override + public List getRecentlyLoggedInPlayers() { + return source.getRecentlyLoggedInPlayers(); + } + + @Override + public boolean setTotpKey(String user, String totpKey) { + boolean result = source.setTotpKey(user, totpKey); + if (result) { + cachedAuths.refresh(user); + } + return result; + } + + @Override + public void invalidateCache(String playerName) { + cachedAuths.invalidate(playerName); + } + + @Override + public void refreshCache(String playerName) { + if (cachedAuths.getIfPresent(playerName) != null) { + cachedAuths.refresh(playerName); + } + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/Columns.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/Columns.java new file mode 100644 index 00000000..a604d0a4 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/Columns.java @@ -0,0 +1,59 @@ +package fr.xephi.authme.datasource; + +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.properties.DatabaseSettings; + +/** + * Database column names. + */ +// Justification: String is immutable and this class is used to easily access the configurable column names +@SuppressWarnings({"checkstyle:VisibilityModifier", "checkstyle:MemberName", "checkstyle:AbbreviationAsWordInName"}) +public final class Columns { + + public final String NAME; + public final String REAL_NAME; + public final String PASSWORD; + public final String SALT; + public final String TOTP_KEY; + public final String LAST_IP; + public final String LAST_LOGIN; + public final String GROUP; + public final String LASTLOC_X; + public final String LASTLOC_Y; + public final String LASTLOC_Z; + public final String LASTLOC_WORLD; + public final String LASTLOC_YAW; + public final String LASTLOC_PITCH; + public final String EMAIL; + public final String ID; + public final String IS_LOGGED; + public final String HAS_SESSION; + public final String REGISTRATION_DATE; + public final String REGISTRATION_IP; + public final String PLAYER_UUID; + + public Columns(Settings settings) { + NAME = settings.getProperty(DatabaseSettings.MYSQL_COL_NAME); + REAL_NAME = settings.getProperty(DatabaseSettings.MYSQL_COL_REALNAME); + PASSWORD = settings.getProperty(DatabaseSettings.MYSQL_COL_PASSWORD); + SALT = settings.getProperty(DatabaseSettings.MYSQL_COL_SALT); + TOTP_KEY = settings.getProperty(DatabaseSettings.MYSQL_COL_TOTP_KEY); + LAST_IP = settings.getProperty(DatabaseSettings.MYSQL_COL_LAST_IP); + LAST_LOGIN = settings.getProperty(DatabaseSettings.MYSQL_COL_LASTLOGIN); + GROUP = settings.getProperty(DatabaseSettings.MYSQL_COL_GROUP); + LASTLOC_X = settings.getProperty(DatabaseSettings.MYSQL_COL_LASTLOC_X); + LASTLOC_Y = settings.getProperty(DatabaseSettings.MYSQL_COL_LASTLOC_Y); + LASTLOC_Z = settings.getProperty(DatabaseSettings.MYSQL_COL_LASTLOC_Z); + LASTLOC_WORLD = settings.getProperty(DatabaseSettings.MYSQL_COL_LASTLOC_WORLD); + LASTLOC_YAW = settings.getProperty(DatabaseSettings.MYSQL_COL_LASTLOC_YAW); + LASTLOC_PITCH = settings.getProperty(DatabaseSettings.MYSQL_COL_LASTLOC_PITCH); + EMAIL = settings.getProperty(DatabaseSettings.MYSQL_COL_EMAIL); + ID = settings.getProperty(DatabaseSettings.MYSQL_COL_ID); + IS_LOGGED = settings.getProperty(DatabaseSettings.MYSQL_COL_ISLOGGED); + HAS_SESSION = settings.getProperty(DatabaseSettings.MYSQL_COL_HASSESSION); + REGISTRATION_DATE = settings.getProperty(DatabaseSettings.MYSQL_COL_REGISTER_DATE); + REGISTRATION_IP = settings.getProperty(DatabaseSettings.MYSQL_COL_REGISTER_IP); + PLAYER_UUID = settings.getProperty(DatabaseSettings.MYSQL_COL_PLAYER_UUID); + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/DataSource.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/DataSource.java new file mode 100644 index 00000000..3152eb17 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/DataSource.java @@ -0,0 +1,286 @@ +package fr.xephi.authme.datasource; + +import ch.jalu.datasourcecolumns.data.DataSourceValue; +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.initialization.Reloadable; +import fr.xephi.authme.security.crypts.HashedPassword; + +import java.util.Collection; +import java.util.List; +import java.util.Set; + +/** + * Interface for manipulating {@link PlayerAuth} objects from a data source. + */ +public interface DataSource extends Reloadable { + + /** + * Return whether the data source is cached and needs to send plugin messaging updates. + * + * @return true if the data source is cached. + */ + default boolean isCached() { + return false; + } + + /** + * Return whether there is a record for the given username. + * + * @param user The username to look up + * @return True if there is a record, false otherwise + */ + boolean isAuthAvailable(String user); + + /** + * Return the hashed password of the player. + * + * @param user The user whose password should be retrieve + * @return The password hash of the player + */ + HashedPassword getPassword(String user); + + /** + * Retrieve the entire PlayerAuth object associated with the username. + * + * @param user The user to retrieve + * @return The PlayerAuth object for the given username + */ + PlayerAuth getAuth(String user); + + /** + * Save a new PlayerAuth object. + * + * @param auth The new PlayerAuth to persist + * @return True upon success, false upon failure + */ + boolean saveAuth(PlayerAuth auth); + + /** + * Update the session of a record (IP, last login, real name). + * + * @param auth The PlayerAuth object to update in the database + * @return True upon success, false upon failure + */ + boolean updateSession(PlayerAuth auth); + + /** + * Update the password of the given PlayerAuth object. + * + * @param auth The PlayerAuth whose password should be updated + * @return True upon success, false upon failure + */ + boolean updatePassword(PlayerAuth auth); + + /** + * Update the password of the given player. + * + * @param user The user whose password should be updated + * @param password The new password + * @return True upon success, false upon failure + */ + boolean updatePassword(String user, HashedPassword password); + + /** + * Get all records in the database whose last login was before the given time. + * + * @param until The minimum last login + * @return The account names selected to purge + */ + Set getRecordsToPurge(long until); + + /** + * Purge the given players from the database. + * + * @param toPurge The players to purge + */ + void purgeRecords(Collection toPurge); + + /** + * Remove a user record from the database. + * + * @param user The user to remove + * @return True upon success, false upon failure + */ + boolean removeAuth(String user); + + /** + * Update the quit location of a PlayerAuth. + * + * @param auth The entry whose quit location should be updated + * @return True upon success, false upon failure + */ + boolean updateQuitLoc(PlayerAuth auth); + + /** + * Return all usernames associated with the given IP address. + * + * @param ip The IP address to look up + * @return Usernames associated with the given IP address + */ + List getAllAuthsByIp(String ip); + + /** + * Return the number of accounts associated with the given email address. + * + * @param email The email address to look up + * @return Number of accounts using the given email address + */ + int countAuthsByEmail(String email); + + /** + * Update the email of the PlayerAuth in the data source. + * + * @param auth The PlayerAuth whose email should be updated + * @return True upon success, false upon failure + */ + boolean updateEmail(PlayerAuth auth); + + /** + * Close the underlying connections to the data source. + */ + void closeConnection(); + + /** + * Return the data source type. + * + * @return the data source type + */ + DataSourceType getType(); + + /** + * Query the datasource whether the player is logged in or not. + * + * @param user The name of the player to verify + * @return True if logged in, false otherwise + */ + boolean isLogged(String user); + + /** + * Set a player as logged in. + * + * @param user The name of the player to change + */ + void setLogged(String user); + + /** + * Set a player as unlogged (not logged in). + * + * @param user The name of the player to change + */ + void setUnlogged(String user); + + /** + * Query the datasource whether the player has an active session or not. + * Warning: this value won't expire, you have also to check the user's last login timestamp. + * + * @param user The name of the player to verify + * @return True if the user has a valid session, false otherwise + */ + boolean hasSession(String user); + + /** + * Mark the user's hasSession value to true. + * + * @param user The name of the player to change + */ + void grantSession(String user); + + /** + * Mark the user's hasSession value to false. + * + * @param user The name of the player to change + */ + void revokeSession(String user); + + /** + * Set all players who are marked as logged in as NOT logged in. + */ + void purgeLogged(); + + /** + * Return all players which are logged in and whose email is not set. + * + * @return logged in players with no email + */ + List getLoggedPlayersWithEmptyMail(); + + /** + * Return the number of registered accounts. + * + * @return Total number of accounts + */ + int getAccountsRegistered(); + + /** + * Update a player's real name (capitalization). + * + * @param user The name of the user (lowercase) + * @param realName The real name of the user (proper casing) + * @return True upon success, false upon failure + */ + boolean updateRealName(String user, String realName); + + /** + * Returns the email of the user. + * + * @param user the user to retrieve an email for + * @return the email saved for the user, or null if user or email is not present + */ + DataSourceValue getEmail(String user); + + /** + * Return all players of the database. + * + * @return List of all players + */ + List getAllAuths(); + + /** + * Returns the last ten players who have recently logged in (first ten players with highest last login date). + * + * @return the 10 last players who last logged in + */ + List getRecentlyLoggedInPlayers(); + + /** + * Sets the given TOTP key to the player's account. + * + * @param user the name of the player to modify + * @param totpKey the totp key to set + * @return True upon success, false upon failure + */ + boolean setTotpKey(String user, String totpKey); + + /** + * Removes the TOTP key if present of the given player's account. + * + * @param user the name of the player to modify + * @return True upon success, false upon failure + */ + default boolean removeTotpKey(String user) { + return setTotpKey(user, null); + } + + /** + * Reload the data source. + */ + @Override + void reload(); + + /** + * Invalidate any cached data related to the specified player name. + * + * @param playerName the player name + */ + default void invalidateCache(String playerName) { + } + + /** + * Refresh any cached data (if present) related to the specified player name. + * + * @param playerName the player name + */ + default void refreshCache(String playerName) { + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/DataSourceType.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/DataSourceType.java new file mode 100644 index 00000000..f36faba7 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/DataSourceType.java @@ -0,0 +1,17 @@ +package fr.xephi.authme.datasource; + +/** + * DataSource type. + */ +public enum DataSourceType { + H2, + + MYSQL, + + MARIADB, + + POSTGRESQL, + + SQLITE + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/H2.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/H2.java new file mode 100644 index 00000000..94b28f58 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/H2.java @@ -0,0 +1,422 @@ +package fr.xephi.authme.datasource; + +import com.google.common.annotations.VisibleForTesting; +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.datasource.columnshandler.AuthMeColumnsHandler; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.properties.DatabaseSettings; + +import java.io.File; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +import static fr.xephi.authme.datasource.SqlDataSourceUtils.getNullableLong; +import static fr.xephi.authme.datasource.SqlDataSourceUtils.logSqlException; + +/** + * H2 data source. + */ +@SuppressWarnings({"checkstyle:AbbreviationAsWordInName"}) // Justification: Class name cannot be changed anymore +public class H2 extends AbstractSqlDataSource { + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(H2.class); + private final Settings settings; + private final File dataFolder; + private final String database; + private final String tableName; + private final Columns col; + private Connection con; + + /** + * Constructor for H2. + * + * @param settings The settings instance + * @param dataFolder The data folder + * @throws SQLException when initialization of a SQL datasource failed + */ + public H2(Settings settings, File dataFolder) throws SQLException { + this.settings = settings; + this.dataFolder = dataFolder; + this.database = settings.getProperty(DatabaseSettings.MYSQL_DATABASE); + this.tableName = settings.getProperty(DatabaseSettings.MYSQL_TABLE); + this.col = new Columns(settings); + + try { + this.connect(); + this.setup(); + } catch (Exception ex) { + logger.logException("Error during H2 initialization:", ex); + throw ex; + } + } + + @VisibleForTesting + H2(Settings settings, File dataFolder, Connection connection) { + this.settings = settings; + this.dataFolder = dataFolder; + this.database = settings.getProperty(DatabaseSettings.MYSQL_DATABASE); + this.tableName = settings.getProperty(DatabaseSettings.MYSQL_TABLE); + this.col = new Columns(settings); + this.con = connection; + this.columnsHandler = AuthMeColumnsHandler.createForH2(con, settings); + } + + /** + * Initializes the connection to the H2 database. + * + * @throws SQLException when an SQL error occurs while connecting + */ + protected void connect() throws SQLException { + try { + Class.forName("org.h2.Driver"); + } catch (ClassNotFoundException e) { + throw new IllegalStateException("Failed to load H2 JDBC class", e); + } + + logger.debug("H2 driver loaded"); + this.con = DriverManager.getConnection(this.getJdbcUrl(this.dataFolder.getAbsolutePath(), "", this.database)); + this.columnsHandler = AuthMeColumnsHandler.createForSqlite(con, settings); + } + + /** + * Creates the table if necessary, or adds any missing columns to the table. + * + * @throws SQLException when an SQL error occurs while initializing the database + */ + @VisibleForTesting + @SuppressWarnings("checkstyle:CyclomaticComplexity") + protected void setup() throws SQLException { + try (Statement st = con.createStatement()) { + // Note: cannot add unique fields later on in SQLite, so we add it on initialization + st.executeUpdate("CREATE TABLE IF NOT EXISTS " + tableName + " (" + + col.ID + " INTEGER AUTO_INCREMENT, " + + col.NAME + " VARCHAR(255) NOT NULL UNIQUE, " + + "CONSTRAINT table_const_prim PRIMARY KEY (" + col.ID + "));"); + + DatabaseMetaData md = con.getMetaData(); + + if (isColumnMissing(md, col.REAL_NAME)) { + st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN IF NOT EXISTS " + + col.REAL_NAME + " VARCHAR(255) NOT NULL DEFAULT 'Player';"); + } + + if (isColumnMissing(md, col.PASSWORD)) { + st.executeUpdate("ALTER TABLE " + tableName + + " ADD COLUMN IF NOT EXISTS " + col.PASSWORD + " VARCHAR(255) NOT NULL DEFAULT '';"); + } + + if (!col.SALT.isEmpty() && isColumnMissing(md, col.SALT)) { + st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN IF NOT EXISTS " + col.SALT + " VARCHAR(255);"); + } + + if (isColumnMissing(md, col.LAST_IP)) { + st.executeUpdate("ALTER TABLE " + tableName + + " ADD COLUMN IF NOT EXISTS " + col.LAST_IP + " VARCHAR(40);"); + } + + if (isColumnMissing(md, col.LAST_LOGIN)) { + st.executeUpdate("ALTER TABLE " + tableName + + " ADD COLUMN IF NOT EXISTS " + col.LAST_LOGIN + " BIGINT;"); + } + + if (isColumnMissing(md, col.REGISTRATION_IP)) { + st.executeUpdate("ALTER TABLE " + tableName + + " ADD COLUMN IF NOT EXISTS " + col.REGISTRATION_IP + " VARCHAR(40);"); + } + + if (isColumnMissing(md, col.REGISTRATION_DATE)) { + addRegistrationDateColumn(st); + } + + if (isColumnMissing(md, col.LASTLOC_X)) { + st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN IF NOT EXISTS " + col.LASTLOC_X + + " DOUBLE NOT NULL DEFAULT '0.0';"); + st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN IF NOT EXISTS " + col.LASTLOC_Y + + " DOUBLE NOT NULL DEFAULT '0.0';"); + st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN IF NOT EXISTS " + col.LASTLOC_Z + + " DOUBLE NOT NULL DEFAULT '0.0';"); + } + + if (isColumnMissing(md, col.LASTLOC_WORLD)) { + st.executeUpdate("ALTER TABLE " + tableName + + " ADD COLUMN IF NOT EXISTS " + col.LASTLOC_WORLD + " VARCHAR(255) NOT NULL DEFAULT 'world';"); + } + + if (isColumnMissing(md, col.LASTLOC_YAW)) { + st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN IF NOT EXISTS " + + col.LASTLOC_YAW + " FLOAT;"); + } + + if (isColumnMissing(md, col.LASTLOC_PITCH)) { + st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN IF NOT EXISTS " + + col.LASTLOC_PITCH + " FLOAT;"); + } + + if (isColumnMissing(md, col.EMAIL)) { + st.executeUpdate("ALTER TABLE " + tableName + + " ADD COLUMN IF NOT EXISTS " + col.EMAIL + " VARCHAR_IGNORECASE(255);"); + } + + if (isColumnMissing(md, col.IS_LOGGED)) { + st.executeUpdate("ALTER TABLE " + tableName + + " ADD COLUMN IF NOT EXISTS " + col.IS_LOGGED + " INT NOT NULL DEFAULT '0';"); + } + + if (isColumnMissing(md, col.HAS_SESSION)) { + st.executeUpdate("ALTER TABLE " + tableName + + " ADD COLUMN IF NOT EXISTS " + col.HAS_SESSION + " INT NOT NULL DEFAULT '0';"); + } + + if (isColumnMissing(md, col.TOTP_KEY)) { + st.executeUpdate("ALTER TABLE " + tableName + + " ADD COLUMN IF NOT EXISTS " + col.TOTP_KEY + " VARCHAR(32);"); + } + + if (!col.PLAYER_UUID.isEmpty() && isColumnMissing(md, col.PLAYER_UUID)) { + st.executeUpdate("ALTER TABLE " + tableName + + " ADD COLUMN IF NOT EXISTS " + col.PLAYER_UUID + " VARCHAR(36)"); + } + } + logger.info("H2 Setup finished"); + } + + /** + * Checks if a column is missing in the specified table. + * @param columnName the name of the column to look for + * @return true if the column is missing, false if it exists + * @throws SQLException if an error occurs while executing SQL or accessing the result set + * @deprecated Not work in H2, it always returns true + */ + @Deprecated + private boolean isColumnMissing(DatabaseMetaData metaData, String columnName) throws SQLException { + String query = "SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = ? AND COLUMN_NAME = ?"; +// try (PreparedStatement preparedStatement = con.prepareStatement(query)) { +// preparedStatement.setString(1, tableName); +// preparedStatement.setString(2, columnName.toUpperCase()); +// try (ResultSet rs = preparedStatement.executeQuery()) { +// return !rs.next(); +// } +// } + return true; + } + + + @Override + public void reload() { + close(con); + try { + this.connect(); + this.setup(); + } catch (SQLException ex) { + logger.logException("Error while reloading H2:", ex); + } + } + + @Override + public PlayerAuth getAuth(String user) { + String sql = "SELECT * FROM " + tableName + " WHERE LOWER(" + col.NAME + ")=LOWER(?);"; + try (PreparedStatement pst = con.prepareStatement(sql)) { + pst.setString(1, user); + try (ResultSet rs = pst.executeQuery()) { + if (rs.next()) { + return buildAuthFromResultSet(rs); + } + } + } catch (SQLException ex) { + logSqlException(ex); + } + return null; + } + + @Override + public Set getRecordsToPurge(long until) { + Set list = new HashSet<>(); + String select = "SELECT " + col.NAME + " FROM " + tableName + " WHERE MAX(" + + " COALESCE(" + col.LAST_LOGIN + ", 0)," + + " COALESCE(" + col.REGISTRATION_DATE + ", 0)" + + ") < ?;"; + try (PreparedStatement selectPst = con.prepareStatement(select)) { + selectPst.setLong(1, until); + try (ResultSet rs = selectPst.executeQuery()) { + while (rs.next()) { + list.add(rs.getString(col.NAME)); + } + } + } catch (SQLException ex) { + logSqlException(ex); + } + + return list; + } + + @Override + public void purgeRecords(Collection toPurge) { + String delete = "DELETE FROM " + tableName + " WHERE " + col.NAME + "=?;"; + try (PreparedStatement deletePst = con.prepareStatement(delete)) { + for (String name : toPurge) { + deletePst.setString(1, name.toLowerCase(Locale.ROOT)); + deletePst.executeUpdate(); + } + } catch (SQLException ex) { + logSqlException(ex); + } + } + + @Override + public boolean removeAuth(String user) { + String sql = "DELETE FROM " + tableName + " WHERE " + col.NAME + "=?;"; + try (PreparedStatement pst = con.prepareStatement(sql)) { + pst.setString(1, user.toLowerCase(Locale.ROOT)); + pst.executeUpdate(); + return true; + } catch (SQLException ex) { + logSqlException(ex); + } + return false; + } + + @Override + public void closeConnection() { + try { + if (con != null && !con.isClosed()) { + con.close(); + } + } catch (SQLException ex) { + logSqlException(ex); + } + } + + @Override + public DataSourceType getType() { + return DataSourceType.H2; + } + + @Override + public List getAllAuths() { + List auths = new ArrayList<>(); + String sql = "SELECT * FROM " + tableName + ";"; + try (PreparedStatement pst = con.prepareStatement(sql); ResultSet rs = pst.executeQuery()) { + while (rs.next()) { + PlayerAuth auth = buildAuthFromResultSet(rs); + auths.add(auth); + } + } catch (SQLException ex) { + logSqlException(ex); + } + return auths; + } + + @Override + public List getLoggedPlayersWithEmptyMail() { + List players = new ArrayList<>(); + String sql = "SELECT " + col.REAL_NAME + " FROM " + tableName + " WHERE " + col.IS_LOGGED + " = 1" + + " AND (" + col.EMAIL + " = 'your@email.com' OR " + col.EMAIL + " IS NULL);"; + try (Statement st = con.createStatement(); ResultSet rs = st.executeQuery(sql)) { + while (rs.next()) { + players.add(rs.getString(1)); + } + } catch (SQLException ex) { + logSqlException(ex); + } + return players; + } + + @Override + public List getRecentlyLoggedInPlayers() { + List players = new ArrayList<>(); + String sql = "SELECT * FROM " + tableName + " ORDER BY " + col.LAST_LOGIN + " DESC LIMIT 10;"; + try (Statement st = con.createStatement(); ResultSet rs = st.executeQuery(sql)) { + while (rs.next()) { + players.add(buildAuthFromResultSet(rs)); + } + } catch (SQLException e) { + logSqlException(e); + } + return players; + } + + + @Override + public boolean setTotpKey(String user, String totpKey) { + String sql = "UPDATE " + tableName + " SET " + col.TOTP_KEY + " = ? WHERE " + col.NAME + " = ?"; + try (PreparedStatement pst = con.prepareStatement(sql)) { + pst.setString(1, totpKey); + pst.setString(2, user.toLowerCase(Locale.ROOT)); + pst.executeUpdate(); + return true; + } catch (SQLException e) { + logSqlException(e); + } + return false; + } + + private PlayerAuth buildAuthFromResultSet(ResultSet row) throws SQLException { + String salt = !col.SALT.isEmpty() ? row.getString(col.SALT) : null; + + return PlayerAuth.builder() + .name(row.getString(col.NAME)) + .email(row.getString(col.EMAIL)) + .realName(row.getString(col.REAL_NAME)) + .password(row.getString(col.PASSWORD), salt) + .totpKey(row.getString(col.TOTP_KEY)) + .lastLogin(getNullableLong(row, col.LAST_LOGIN)) + .lastIp(row.getString(col.LAST_IP)) + .registrationDate(row.getLong(col.REGISTRATION_DATE)) + .registrationIp(row.getString(col.REGISTRATION_IP)) + .locX(row.getDouble(col.LASTLOC_X)) + .locY(row.getDouble(col.LASTLOC_Y)) + .locZ(row.getDouble(col.LASTLOC_Z)) + .locWorld(row.getString(col.LASTLOC_WORLD)) + .locYaw(row.getFloat(col.LASTLOC_YAW)) + .locPitch(row.getFloat(col.LASTLOC_PITCH)) + .build(); + } + + /** + * Creates the column for registration date and sets all entries to the current timestamp. + * We do so in order to avoid issues with purging, where entries with 0 / NULL might get + * purged immediately on startup otherwise. + * + * @param st Statement object to the database + */ + private void addRegistrationDateColumn(Statement st) throws SQLException { + int affect = st.executeUpdate("ALTER TABLE " + tableName + + " ADD COLUMN IF NOT EXISTS " + col.REGISTRATION_DATE + " BIGINT NOT NULL DEFAULT '0';"); + if (affect > 0) { + long currentTimestamp = System.currentTimeMillis(); + int updatedRows = st.executeUpdate(String.format("UPDATE %s SET %s = %d;", + tableName, col.REGISTRATION_DATE, currentTimestamp)); + logger.info("Created column '" + col.REGISTRATION_DATE + "' and set the current timestamp, " + + currentTimestamp + ", to all " + updatedRows + " rows"); + } + } + + @Override + String getJdbcUrl(String dataPath, String ignored, String database) { + return "jdbc:h2:" + dataPath + File.separator + database; + } + + private static void close(Connection con) { + if (con != null) { + try { + con.close(); + } catch (SQLException ex) { + logSqlException(ex); + } + } + } +} + diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/MariaDB.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/MariaDB.java new file mode 100644 index 00000000..8fa35595 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/MariaDB.java @@ -0,0 +1,27 @@ +package fr.xephi.authme.datasource; + +import fr.xephi.authme.datasource.mysqlextensions.MySqlExtensionsFactory; +import fr.xephi.authme.settings.Settings; + +import java.sql.SQLException; + +public class MariaDB extends MySQL { + public MariaDB(Settings settings, MySqlExtensionsFactory extensionsFactory) throws SQLException { + super(settings, extensionsFactory); + } + + @Override + String getJdbcUrl(String host, String port, String database) { + return "jdbc:mariadb://" + host + ":" + port + "/" + database; + } + + @Override + protected String getDriverClassName() { + return "org.mariadb.jdbc.Driver"; + } + + @Override + public DataSourceType getType() { + return DataSourceType.MARIADB; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/MySQL.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/MySQL.java new file mode 100644 index 00000000..9a1cf216 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/MySQL.java @@ -0,0 +1,524 @@ +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.data.auth.PlayerAuth; +import fr.xephi.authme.datasource.columnshandler.AuthMeColumnsHandler; +import fr.xephi.authme.datasource.mysqlextensions.MySqlExtension; +import fr.xephi.authme.datasource.mysqlextensions.MySqlExtensionsFactory; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.properties.DatabaseSettings; +import fr.xephi.authme.settings.properties.HooksSettings; +import fr.xephi.authme.util.UuidUtils; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.UUID; + +import static fr.xephi.authme.datasource.SqlDataSourceUtils.getNullableLong; +import static fr.xephi.authme.datasource.SqlDataSourceUtils.logSqlException; + +/** + * MySQL data source. + */ +@SuppressWarnings({"checkstyle:AbbreviationAsWordInName"}) // Justification: Class name cannot be changed anymore +public class MySQL extends AbstractSqlDataSource { + private final ConsoleLogger logger = ConsoleLoggerFactory.get(MySQL.class); + + private boolean useSsl; + private boolean serverCertificateVerification; + private boolean allowPublicKeyRetrieval; + private String mariaDbSslMode; + private String host; + private String port; + private String username; + private String password; + private String database; + private String tableName; + private int poolSize; + private int maxLifetime; + private List columnOthers; + private Columns col; + private MySqlExtension sqlExtension; + private HikariDataSource ds; + + public MySQL(Settings settings, MySqlExtensionsFactory extensionsFactory) throws SQLException { + setParameters(settings, extensionsFactory); + + // Set the connection arguments (and check if connection is ok) + try { + this.setConnectionArguments(); + } catch (RuntimeException e) { + if (e instanceof IllegalArgumentException) { + logger.warning("Invalid database arguments! Please check your configuration!"); + logger.warning("If this error persists, please report it to the developer!"); + } + if (e instanceof PoolInitializationException) { + logger.warning("Can't initialize database connection! Please check your configuration!"); + logger.warning("If this error persists, please report it to the developer!"); + } + logger.warning("Can't use the Hikari Connection Pool! Please, report this error to the developer!"); + throw e; + } + + // Initialize the database + try { + checkTablesAndColumns(); + } catch (SQLException e) { + closeConnection(); + logger.logException("Can't initialize the MySQL database:", e); + logger.warning("Please check your database settings in the config.yml file!"); + throw e; + } + } + + @VisibleForTesting + MySQL(Settings settings, HikariDataSource hikariDataSource, MySqlExtensionsFactory extensionsFactory) { + ds = hikariDataSource; + setParameters(settings, extensionsFactory); + } + + /** + * Returns the path of the Driver class to use when connecting to the database. + * + * @return the dotted path of the SQL driver class to be used + */ + protected String getDriverClassName() { + return "com.mysql.cj.jdbc.Driver"; + } + + /** + * Retrieves various settings. + * + * @param settings the settings to read properties from + * @param extensionsFactory factory to create the MySQL extension + */ + private void setParameters(Settings settings, MySqlExtensionsFactory extensionsFactory) { + this.host = settings.getProperty(DatabaseSettings.MYSQL_HOST); + this.port = settings.getProperty(DatabaseSettings.MYSQL_PORT); + this.username = settings.getProperty(DatabaseSettings.MYSQL_USERNAME); + this.password = settings.getProperty(DatabaseSettings.MYSQL_PASSWORD); + this.database = settings.getProperty(DatabaseSettings.MYSQL_DATABASE); + this.tableName = settings.getProperty(DatabaseSettings.MYSQL_TABLE); + this.columnOthers = settings.getProperty(HooksSettings.MYSQL_OTHER_USERNAME_COLS); + this.col = new Columns(settings); + this.columnsHandler = AuthMeColumnsHandler.createForMySql(this::getConnection, settings); + this.sqlExtension = extensionsFactory.buildExtension(col); + this.poolSize = settings.getProperty(DatabaseSettings.MYSQL_POOL_SIZE); + this.maxLifetime = settings.getProperty(DatabaseSettings.MYSQL_CONNECTION_MAX_LIFETIME); + this.useSsl = settings.getProperty(DatabaseSettings.MYSQL_USE_SSL); + this.serverCertificateVerification = settings.getProperty(DatabaseSettings.MYSQL_CHECK_SERVER_CERTIFICATE); + this.allowPublicKeyRetrieval = settings.getProperty(DatabaseSettings.MYSQL_ALLOW_PUBLIC_KEY_RETRIEVAL); + this.mariaDbSslMode = settings.getProperty(DatabaseSettings.MARIADB_SSL_MODE); + } + + /** + * Sets up the connection arguments to the database. + */ + private void setConnectionArguments() { + ds = new HikariDataSource(); + ds.setPoolName("AuthMeMYSQLPool"); + + // Pool Settings + ds.setMaximumPoolSize(poolSize); + ds.setMaxLifetime(maxLifetime * 1000L); + + // Database URL + ds.setJdbcUrl(this.getJdbcUrl(this.host, this.port, this.database)); + + // Auth + ds.setUsername(this.username); + ds.setPassword(this.password); + + // Driver + ds.setDriverClassName(this.getDriverClassName()); + + // Request mysql over SSL + if (this instanceof MariaDB) { + ds.addDataSourceProperty("sslMode", mariaDbSslMode); + } else { + ds.addDataSourceProperty("useSSL", String.valueOf(useSsl)); + + // Disabling server certificate verification on need + if (!serverCertificateVerification) { + ds.addDataSourceProperty("verifyServerCertificate", String.valueOf(false)); + } + } + + + // Disabling server certificate verification on need + if (allowPublicKeyRetrieval) { + ds.addDataSourceProperty("allowPublicKeyRetrieval", String.valueOf(true)); + } + + // Encoding + ds.addDataSourceProperty("characterEncoding", "utf8"); + ds.addDataSourceProperty("encoding", "UTF-8"); + ds.addDataSourceProperty("useUnicode", "true"); + + // Random stuff + ds.addDataSourceProperty("rewriteBatchedStatements", "true"); + ds.addDataSourceProperty("jdbcCompliantTruncation", "false"); + + // Caching + ds.addDataSourceProperty("cachePrepStmts", "true"); + ds.addDataSourceProperty("prepStmtCacheSize", "275"); + ds.addDataSourceProperty("prepStmtCacheSqlLimit", "2048"); + + logger.info("Connection arguments loaded, Hikari ConnectionPool ready!"); + } + + @Override + public void reload() { + if (ds != null) { + ds.close(); + } + setConnectionArguments(); + logger.info("Hikari ConnectionPool arguments reloaded!"); + } + + private Connection getConnection() throws SQLException { + return ds.getConnection(); + } + + /** + * Creates the table or any of its required columns if they don't exist. + */ + @SuppressWarnings({"checkstyle:CyclomaticComplexity", "checkstyle:JavaNCSS"}) + private void checkTablesAndColumns() throws SQLException { + try (Connection con = getConnection(); Statement st = con.createStatement()) { + // Create table with ID column if it doesn't exist + String sql = "CREATE TABLE IF NOT EXISTS " + tableName + " (" + + col.ID + " MEDIUMINT(8) UNSIGNED AUTO_INCREMENT," + + "PRIMARY KEY (" + col.ID + ")" + + ") CHARACTER SET = utf8;"; + st.executeUpdate(sql); + + DatabaseMetaData md = con.getMetaData(); + if (isColumnMissing(md, col.NAME)) { + st.executeUpdate("ALTER TABLE " + tableName + + " ADD COLUMN " + col.NAME + " VARCHAR(255) NOT NULL UNIQUE AFTER " + col.ID + ";"); + } + + if (isColumnMissing(md, col.REAL_NAME)) { + st.executeUpdate("ALTER TABLE " + tableName + + " ADD COLUMN " + col.REAL_NAME + " VARCHAR(255) NOT NULL AFTER " + col.NAME + ";"); + } + + if (isColumnMissing(md, col.PASSWORD)) { + st.executeUpdate("ALTER TABLE " + tableName + + " ADD COLUMN " + col.PASSWORD + " VARCHAR(255) CHARACTER SET ascii COLLATE ascii_bin NOT NULL;"); + } + + if (!col.SALT.isEmpty() && isColumnMissing(md, col.SALT)) { + st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.SALT + " VARCHAR(255);"); + } + + if (isColumnMissing(md, col.LAST_IP)) { + st.executeUpdate("ALTER TABLE " + tableName + + " ADD COLUMN " + col.LAST_IP + " VARCHAR(40) CHARACTER SET ascii COLLATE ascii_bin;"); + } else { + MySqlMigrater.migrateLastIpColumn(st, md, tableName, col); + } + + if (isColumnMissing(md, col.LAST_LOGIN)) { + st.executeUpdate("ALTER TABLE " + tableName + + " ADD COLUMN " + col.LAST_LOGIN + " BIGINT;"); + } else { + MySqlMigrater.migrateLastLoginColumn(st, md, tableName, col); + } + + if (isColumnMissing(md, col.REGISTRATION_DATE)) { + MySqlMigrater.addRegistrationDateColumn(st, tableName, col); + } + + if (isColumnMissing(md, col.REGISTRATION_IP)) { + st.executeUpdate("ALTER TABLE " + tableName + + " ADD COLUMN " + col.REGISTRATION_IP + " VARCHAR(40) CHARACTER SET ascii COLLATE ascii_bin;"); + } + + if (isColumnMissing(md, col.LASTLOC_X)) { + st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + + col.LASTLOC_X + " DOUBLE NOT NULL DEFAULT '0.0' AFTER " + col.LAST_LOGIN + " , ADD " + + col.LASTLOC_Y + " DOUBLE NOT NULL DEFAULT '0.0' AFTER " + col.LASTLOC_X + " , ADD " + + col.LASTLOC_Z + " DOUBLE NOT NULL DEFAULT '0.0' AFTER " + col.LASTLOC_Y); + } else { + st.executeUpdate("ALTER TABLE " + tableName + " MODIFY " + + col.LASTLOC_X + " DOUBLE NOT NULL DEFAULT '0.0', MODIFY " + + col.LASTLOC_Y + " DOUBLE NOT NULL DEFAULT '0.0', MODIFY " + + col.LASTLOC_Z + " DOUBLE NOT NULL DEFAULT '0.0';"); + } + + if (isColumnMissing(md, col.LASTLOC_WORLD)) { + st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + + col.LASTLOC_WORLD + " VARCHAR(255) NOT NULL DEFAULT 'world' AFTER " + col.LASTLOC_Z); + } + + if (isColumnMissing(md, col.LASTLOC_YAW)) { + st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + + col.LASTLOC_YAW + " FLOAT;"); + } + + if (isColumnMissing(md, col.LASTLOC_PITCH)) { + st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + + col.LASTLOC_PITCH + " FLOAT;"); + } + + if (isColumnMissing(md, col.EMAIL)) { + st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + + col.EMAIL + " VARCHAR(255);"); + } + + if (isColumnMissing(md, col.IS_LOGGED)) { + st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + + col.IS_LOGGED + " SMALLINT NOT NULL DEFAULT '0' AFTER " + col.EMAIL); + } + + if (isColumnMissing(md, col.HAS_SESSION)) { + st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + + col.HAS_SESSION + " SMALLINT NOT NULL DEFAULT '0' AFTER " + col.IS_LOGGED); + } + + if (isColumnMissing(md, col.TOTP_KEY)) { + st.executeUpdate("ALTER TABLE " + tableName + + " ADD COLUMN " + col.TOTP_KEY + " VARCHAR(32);"); + } else if (SqlDataSourceUtils.getColumnSize(md, tableName, col.TOTP_KEY) != 32) { + st.executeUpdate("ALTER TABLE " + tableName + + " MODIFY " + col.TOTP_KEY + " VARCHAR(32);"); + } + + if (!col.PLAYER_UUID.isEmpty() && isColumnMissing(md, col.PLAYER_UUID)) { + st.executeUpdate("ALTER TABLE " + tableName + + " ADD COLUMN " + col.PLAYER_UUID + " VARCHAR(36)"); + } + } + logger.info("MySQL setup finished"); + } + + private boolean isColumnMissing(DatabaseMetaData metaData, String columnName) throws SQLException { + try (ResultSet rs = metaData.getColumns(database, null, tableName, columnName)) { + return !rs.next(); + } + } + + @Override + public PlayerAuth getAuth(String user) { + String sql = "SELECT * FROM " + tableName + " WHERE " + col.NAME + "=?;"; + PlayerAuth auth; + try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) { + pst.setString(1, user.toLowerCase(Locale.ROOT)); + try (ResultSet rs = pst.executeQuery()) { + if (rs.next()) { + int id = rs.getInt(col.ID); + auth = buildAuthFromResultSet(rs); + sqlExtension.extendAuth(auth, id, con); + return auth; + } + } + } catch (SQLException ex) { + logSqlException(ex); + } + return null; + } + + @Override + public boolean saveAuth(PlayerAuth auth) { + super.saveAuth(auth); + + try (Connection con = getConnection()) { + if (!columnOthers.isEmpty()) { + for (String column : columnOthers) { + try (PreparedStatement pst = con.prepareStatement( + "UPDATE " + tableName + " SET " + column + "=? WHERE " + col.NAME + "=?;")) { + pst.setString(1, auth.getRealName()); + pst.setString(2, auth.getNickname()); + pst.executeUpdate(); + } + } + } + + sqlExtension.saveAuth(auth, con); + return true; + } catch (SQLException ex) { + logSqlException(ex); + } + return false; + } + + @Override + String getJdbcUrl(String host, String port, String database) { + return "jdbc:mysql://" + host + ":" + port + "/" + database; + } + + @Override + public Set getRecordsToPurge(long until) { + Set list = new HashSet<>(); + String select = "SELECT " + col.NAME + " FROM " + tableName + " WHERE GREATEST(" + + " COALESCE(" + col.LAST_LOGIN + ", 0)," + + " COALESCE(" + col.REGISTRATION_DATE + ", 0)" + + ") < ?;"; + try (Connection con = getConnection(); + PreparedStatement selectPst = con.prepareStatement(select)) { + selectPst.setLong(1, until); + try (ResultSet rs = selectPst.executeQuery()) { + while (rs.next()) { + list.add(rs.getString(col.NAME)); + } + } + } catch (SQLException ex) { + logSqlException(ex); + } + + return list; + } + + @Override + public boolean removeAuth(String user) { + user = user.toLowerCase(Locale.ROOT); + String sql = "DELETE FROM " + tableName + " WHERE " + col.NAME + "=?;"; + try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) { + sqlExtension.removeAuth(user, con); + pst.setString(1, user.toLowerCase(Locale.ROOT)); + pst.executeUpdate(); + return true; + } catch (SQLException ex) { + logSqlException(ex); + } + return false; + } + + @Override + public void closeConnection() { + if (ds != null && !ds.isClosed()) { + ds.close(); + } + } + + @Override + public void purgeRecords(Collection toPurge) { + String sql = "DELETE FROM " + tableName + " WHERE " + col.NAME + "=?;"; + try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) { + for (String name : toPurge) { + pst.setString(1, name.toLowerCase(Locale.ROOT)); + pst.executeUpdate(); + } + } catch (SQLException ex) { + logSqlException(ex); + } + } + + @Override + public DataSourceType getType() { + return DataSourceType.MYSQL; + } + + @Override + public List getAllAuths() { + List auths = new ArrayList<>(); + try (Connection con = getConnection(); Statement st = con.createStatement()) { + try (ResultSet rs = st.executeQuery("SELECT * FROM " + tableName)) { + while (rs.next()) { + PlayerAuth auth = buildAuthFromResultSet(rs); + sqlExtension.extendAuth(auth, rs.getInt(col.ID), con); + auths.add(auth); + } + } + } catch (SQLException ex) { + logSqlException(ex); + } + return auths; + } + + @Override + public List getLoggedPlayersWithEmptyMail() { + List players = new ArrayList<>(); + String sql = "SELECT " + col.REAL_NAME + " FROM " + tableName + " WHERE " + col.IS_LOGGED + " = 1" + + " AND (" + col.EMAIL + " = 'your@email.com' OR " + col.EMAIL + " IS NULL);"; + try (Connection con = getConnection(); + Statement st = con.createStatement(); + ResultSet rs = st.executeQuery(sql)) { + while (rs.next()) { + players.add(rs.getString(1)); + } + } catch (SQLException ex) { + logSqlException(ex); + } + return players; + } + + @Override + public List getRecentlyLoggedInPlayers() { + List players = new ArrayList<>(); + String sql = "SELECT * FROM " + tableName + " ORDER BY " + col.LAST_LOGIN + " DESC LIMIT 10;"; + try (Connection con = getConnection(); + Statement st = con.createStatement(); + ResultSet rs = st.executeQuery(sql)) { + while (rs.next()) { + players.add(buildAuthFromResultSet(rs)); + } + } catch (SQLException e) { + logSqlException(e); + } + return players; + } + + @Override + public boolean setTotpKey(String user, String totpKey) { + String sql = "UPDATE " + tableName + " SET " + col.TOTP_KEY + " = ? WHERE " + col.NAME + " = ?"; + try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) { + pst.setString(1, totpKey); + pst.setString(2, user.toLowerCase(Locale.ROOT)); + pst.executeUpdate(); + return true; + } catch (SQLException e) { + logSqlException(e); + } + return false; + } + + /** + * Creates a {@link PlayerAuth} object with the data from the provided result set. + * + * @param row the result set to read from + * @return generated player auth object with the data from the result set + * @throws SQLException . + */ + private PlayerAuth buildAuthFromResultSet(ResultSet row) throws SQLException { + String salt = col.SALT.isEmpty() ? null : row.getString(col.SALT); + int group = col.GROUP.isEmpty() ? -1 : row.getInt(col.GROUP); + UUID uuid = col.PLAYER_UUID.isEmpty() + ? null : UuidUtils.parseUuidSafely(row.getString(col.PLAYER_UUID)); + return PlayerAuth.builder() + .name(row.getString(col.NAME)) + .realName(row.getString(col.REAL_NAME)) + .password(row.getString(col.PASSWORD), salt) + .totpKey(row.getString(col.TOTP_KEY)) + .lastLogin(getNullableLong(row, col.LAST_LOGIN)) + .lastIp(row.getString(col.LAST_IP)) + .email(row.getString(col.EMAIL)) + .registrationDate(row.getLong(col.REGISTRATION_DATE)) + .registrationIp(row.getString(col.REGISTRATION_IP)) + .groupId(group) + .locWorld(row.getString(col.LASTLOC_WORLD)) + .locX(row.getDouble(col.LASTLOC_X)) + .locY(row.getDouble(col.LASTLOC_Y)) + .locZ(row.getDouble(col.LASTLOC_Z)) + .locYaw(row.getFloat(col.LASTLOC_YAW)) + .locPitch(row.getFloat(col.LASTLOC_PITCH)) + .uuid(uuid) + .build(); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/MySqlMigrater.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/MySqlMigrater.java new file mode 100644 index 00000000..fec564d5 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/MySqlMigrater.java @@ -0,0 +1,116 @@ +package fr.xephi.authme.datasource; + +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.output.ConsoleLoggerFactory; + +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.sql.Types; + +/** + * Performs migrations on the MySQL data source if necessary. + */ +final class MySqlMigrater { + + private static ConsoleLogger logger = ConsoleLoggerFactory.get(MySqlMigrater.class); + + private MySqlMigrater() { + } + + /** + * Changes the last IP column to be nullable if it has a NOT NULL constraint without a default value. + * Background: Before 5.2, the last IP column was initialized to be {@code NOT NULL} without a default. + * With the introduction of a registration IP column we no longer want to set the last IP column on registration. + * + * @param st Statement object to the database + * @param metaData column metadata for the table + * @param tableName the MySQL table's name + * @param col the column names configuration + */ + static void migrateLastIpColumn(Statement st, DatabaseMetaData metaData, + String tableName, Columns col) throws SQLException { + final boolean isNotNullWithoutDefault = SqlDataSourceUtils.isNotNullColumn(metaData, tableName, col.LAST_IP) + && SqlDataSourceUtils.getColumnDefaultValue(metaData, tableName, col.LAST_IP) == null; + + if (isNotNullWithoutDefault) { + String sql = String.format("ALTER TABLE %s MODIFY %s VARCHAR(40) CHARACTER SET ascii COLLATE ascii_bin", + tableName, col.LAST_IP); + st.execute(sql); + logger.info("Changed last login column to allow NULL values. Please verify the registration feature " + + "if you are hooking into a forum."); + } + } + + /** + * Checks if the last login column has a type that needs to be migrated. + * + * @param st Statement object to the database + * @param metaData column metadata for the table + * @param tableName the MySQL table's name + * @param col the column names configuration + */ + static void migrateLastLoginColumn(Statement st, DatabaseMetaData metaData, + String tableName, Columns col) throws SQLException { + final int columnType; + try (ResultSet rs = metaData.getColumns(null, null, tableName, col.LAST_LOGIN)) { + if (!rs.next()) { + logger.warning("Could not get LAST_LOGIN meta data. This should never happen!"); + return; + } + columnType = rs.getInt("DATA_TYPE"); + } + + if (columnType == Types.INTEGER) { + migrateLastLoginColumnFromInt(st, tableName, col); + } + } + + /** + * Performs conversion of lastlogin column from int to bigint. + * + * @param st Statement object to the database + * @param tableName the table name + * @param col the column names configuration + * @see + * #887: Migrate lastlogin column from int32 to bigint + */ + private static void migrateLastLoginColumnFromInt(Statement st, String tableName, Columns col) throws SQLException { + // Change from int to bigint + logger.info("Migrating lastlogin column from int to bigint"); + String sql = String.format("ALTER TABLE %s MODIFY %s BIGINT;", tableName, col.LAST_LOGIN); + st.execute(sql); + + // Migrate timestamps in seconds format to milliseconds format if they are plausible + int rangeStart = 1262304000; // timestamp for 2010-01-01 + int rangeEnd = 1514678400; // timestamp for 2017-12-31 + sql = String.format("UPDATE %s SET %s = %s * 1000 WHERE %s > %d AND %s < %d;", + tableName, col.LAST_LOGIN, col.LAST_LOGIN, col.LAST_LOGIN, rangeStart, col.LAST_LOGIN, rangeEnd); + int changedRows = st.executeUpdate(sql); + + logger.warning("You may have entries with invalid timestamps. Please check your data " + + "before purging. " + changedRows + " rows were migrated from seconds to milliseconds."); + } + + /** + * Creates the column for registration date and sets all entries to the current timestamp. + * We do so in order to avoid issues with purging, where entries with 0 / NULL might get + * purged immediately on startup otherwise. + * + * @param st Statement object to the database + * @param tableName the table name + * @param col the column names configuration + */ + static void addRegistrationDateColumn(Statement st, String tableName, Columns col) throws SQLException { + st.executeUpdate("ALTER TABLE " + tableName + + " ADD COLUMN " + col.REGISTRATION_DATE + " BIGINT NOT NULL DEFAULT 0;"); + + // Use the timestamp from Java to avoid timezone issues in case JVM and database are out of sync + long currentTimestamp = System.currentTimeMillis(); + int updatedRows = st.executeUpdate(String.format("UPDATE %s SET %s = %d;", + tableName, col.REGISTRATION_DATE, currentTimestamp)); + logger.info("Created column '" + col.REGISTRATION_DATE + "' and set the current timestamp, " + + currentTimestamp + ", to all " + updatedRows + " rows"); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/PostgreSqlDataSource.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/PostgreSqlDataSource.java new file mode 100644 index 00000000..cc308934 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/PostgreSqlDataSource.java @@ -0,0 +1,472 @@ +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.data.auth.PlayerAuth; +import fr.xephi.authme.datasource.columnshandler.AuthMeColumnsHandler; +import fr.xephi.authme.datasource.mysqlextensions.MySqlExtension; +import fr.xephi.authme.datasource.mysqlextensions.MySqlExtensionsFactory; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.properties.DatabaseSettings; +import fr.xephi.authme.settings.properties.HooksSettings; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +import static fr.xephi.authme.datasource.SqlDataSourceUtils.getNullableLong; +import static fr.xephi.authme.datasource.SqlDataSourceUtils.logSqlException; + +/** + * PostgreSQL data source. + */ +public class PostgreSqlDataSource extends AbstractSqlDataSource { + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(PostgreSqlDataSource.class); + + private String host; + private String port; + private String username; + private String password; + private String database; + private String tableName; + private int poolSize; + private int maxLifetime; + private List columnOthers; + private Columns col; + private MySqlExtension sqlExtension; + private HikariDataSource ds; + + public PostgreSqlDataSource(Settings settings, MySqlExtensionsFactory extensionsFactory) throws SQLException { + setParameters(settings, extensionsFactory); + + // Set the connection arguments (and check if connection is ok) + try { + this.setConnectionArguments(); + } catch (RuntimeException e) { + if (e instanceof IllegalArgumentException) { + logger.warning("Invalid database arguments! Please check your configuration!"); + logger.warning("If this error persists, please report it to the developer!"); + } + if (e instanceof PoolInitializationException) { + logger.warning("Can't initialize database connection! Please check your configuration!"); + logger.warning("If this error persists, please report it to the developer!"); + } + logger.warning("Can't use the Hikari Connection Pool! Please, report this error to the developer!"); + throw e; + } + + // Initialize the database + try { + checkTablesAndColumns(); + } catch (SQLException e) { + closeConnection(); + logger.logException("Can't initialize the PostgreSQL database:", e); + logger.warning("Please check your database settings in the config.yml file!"); + throw e; + } + } + + @VisibleForTesting + PostgreSqlDataSource(Settings settings, HikariDataSource hikariDataSource, + MySqlExtensionsFactory extensionsFactory) { + ds = hikariDataSource; + setParameters(settings, extensionsFactory); + } + + /** + * Retrieves various settings. + * + * @param settings the settings to read properties from + * @param extensionsFactory factory to create the MySQL extension + */ + private void setParameters(Settings settings, MySqlExtensionsFactory extensionsFactory) { + this.host = settings.getProperty(DatabaseSettings.MYSQL_HOST); + this.port = settings.getProperty(DatabaseSettings.MYSQL_PORT); + this.username = settings.getProperty(DatabaseSettings.MYSQL_USERNAME); + this.password = settings.getProperty(DatabaseSettings.MYSQL_PASSWORD); + this.database = settings.getProperty(DatabaseSettings.MYSQL_DATABASE); + this.tableName = settings.getProperty(DatabaseSettings.MYSQL_TABLE); + this.columnOthers = settings.getProperty(HooksSettings.MYSQL_OTHER_USERNAME_COLS); + this.col = new Columns(settings); + this.columnsHandler = AuthMeColumnsHandler.createForMySql(this::getConnection, settings); + this.sqlExtension = extensionsFactory.buildExtension(col); + this.poolSize = settings.getProperty(DatabaseSettings.MYSQL_POOL_SIZE); + this.maxLifetime = settings.getProperty(DatabaseSettings.MYSQL_CONNECTION_MAX_LIFETIME); + } + + /** + * Sets up the connection arguments to the database. + */ + private void setConnectionArguments() { + ds = new HikariDataSource(); + ds.setPoolName("AuthMePostgreSQLPool"); + + // Pool Settings + ds.setMaximumPoolSize(poolSize); + ds.setMaxLifetime(maxLifetime * 1000); + + // Database URL + ds.setDriverClassName("org.postgresql.Driver"); + ds.setJdbcUrl(this.getJdbcUrl(this.host, this.port, this.database)); + + // Auth + ds.setUsername(this.username); + ds.setPassword(this.password); + + // Random stuff + ds.addDataSourceProperty("reWriteBatchedInserts", "true"); + + // Caching + ds.addDataSourceProperty("cachePrepStmts", "true"); + ds.addDataSourceProperty("preparedStatementCacheQueries", "275"); + + logger.info("Connection arguments loaded, Hikari ConnectionPool ready!"); + } + + @Override + public void reload() { + if (ds != null) { + ds.close(); + } + setConnectionArguments(); + logger.info("Hikari ConnectionPool arguments reloaded!"); + } + + private Connection getConnection() throws SQLException { + return ds.getConnection(); + } + + /** + * Creates the table or any of its required columns if they don't exist. + */ + private void checkTablesAndColumns() throws SQLException { + try (Connection con = getConnection(); Statement st = con.createStatement()) { + // Create table with ID column if it doesn't exist + String sql = "CREATE TABLE IF NOT EXISTS " + tableName + " (" + + col.ID + " BIGSERIAL," + + "PRIMARY KEY (" + col.ID + ")" + + ");"; + st.executeUpdate(sql); + + DatabaseMetaData md = con.getMetaData(); + if (isColumnMissing(md, col.NAME)) { + st.executeUpdate("ALTER TABLE " + tableName + + " ADD COLUMN " + col.NAME + " VARCHAR(255) NOT NULL UNIQUE;"); + } + + if (isColumnMissing(md, col.REAL_NAME)) { + st.executeUpdate("ALTER TABLE " + tableName + + " ADD COLUMN " + col.REAL_NAME + " VARCHAR(255) NOT NULL;"); + } + + if (isColumnMissing(md, col.PASSWORD)) { + st.executeUpdate("ALTER TABLE " + tableName + + " ADD COLUMN " + col.PASSWORD + " VARCHAR(255) NOT NULL;"); + } + + if (!col.SALT.isEmpty() && isColumnMissing(md, col.SALT)) { + st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.SALT + " VARCHAR(255);"); + } + + if (isColumnMissing(md, col.LAST_IP)) { + st.executeUpdate("ALTER TABLE " + tableName + + " ADD COLUMN " + col.LAST_IP + " VARCHAR(40);"); + } else { + MySqlMigrater.migrateLastIpColumn(st, md, tableName, col); + } + + if (isColumnMissing(md, col.LAST_LOGIN)) { + st.executeUpdate("ALTER TABLE " + tableName + + " ADD COLUMN " + col.LAST_LOGIN + " BIGINT;"); + } else { + MySqlMigrater.migrateLastLoginColumn(st, md, tableName, col); + } + + if (isColumnMissing(md, col.REGISTRATION_DATE)) { + MySqlMigrater.addRegistrationDateColumn(st, tableName, col); + } + + if (isColumnMissing(md, col.REGISTRATION_IP)) { + st.executeUpdate("ALTER TABLE " + tableName + + " ADD COLUMN " + col.REGISTRATION_IP + " VARCHAR(40);"); + } + + if (isColumnMissing(md, col.LASTLOC_X)) { + st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + + col.LASTLOC_X + " DOUBLE PRECISION NOT NULL DEFAULT '0.0' , ADD " + + col.LASTLOC_Y + " DOUBLE PRECISION NOT NULL DEFAULT '0.0' , ADD " + + col.LASTLOC_Z + " DOUBLE PRECISION NOT NULL DEFAULT '0.0';"); + } + + if (isColumnMissing(md, col.LASTLOC_WORLD)) { + st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + + col.LASTLOC_WORLD + " VARCHAR(255) NOT NULL DEFAULT 'world';"); + } + + if (isColumnMissing(md, col.LASTLOC_YAW)) { + st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + + col.LASTLOC_YAW + " FLOAT;"); + } + + if (isColumnMissing(md, col.LASTLOC_PITCH)) { + st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + + col.LASTLOC_PITCH + " FLOAT;"); + } + + if (isColumnMissing(md, col.EMAIL)) { + st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + + col.EMAIL + " VARCHAR(255);"); + } + + if (isColumnMissing(md, col.IS_LOGGED)) { + st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + + col.IS_LOGGED + " SMALLINT NOT NULL DEFAULT '0';"); + } + + if (isColumnMissing(md, col.HAS_SESSION)) { + st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + + col.HAS_SESSION + " SMALLINT NOT NULL DEFAULT '0';"); + } + + if (isColumnMissing(md, col.TOTP_KEY)) { + st.executeUpdate("ALTER TABLE " + tableName + + " ADD COLUMN " + col.TOTP_KEY + " VARCHAR(32);"); + } else if (SqlDataSourceUtils.getColumnSize(md, tableName, col.TOTP_KEY) != 32) { + st.executeUpdate("ALTER TABLE " + tableName + + " ALTER COLUMN " + col.TOTP_KEY + " TYPE VARCHAR(32);"); + } + + if (!col.PLAYER_UUID.isEmpty() && isColumnMissing(md, col.PLAYER_UUID)) { + st.executeUpdate("ALTER TABLE " + tableName + + " ADD COLUMN " + col.PLAYER_UUID + " VARCHAR(36)"); + } + } + logger.info("PostgreSQL setup finished"); + } + + private boolean isColumnMissing(DatabaseMetaData metaData, String columnName) throws SQLException { + try (ResultSet rs = metaData.getColumns(null, null, tableName, columnName.toLowerCase(Locale.ROOT))) { + return !rs.next(); + } + } + + @Override + public PlayerAuth getAuth(String user) { + String sql = "SELECT * FROM " + tableName + " WHERE " + col.NAME + "=?;"; + PlayerAuth auth; + try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) { + pst.setString(1, user.toLowerCase(Locale.ROOT)); + try (ResultSet rs = pst.executeQuery()) { + if (rs.next()) { + int id = rs.getInt(col.ID); + auth = buildAuthFromResultSet(rs); + sqlExtension.extendAuth(auth, id, con); + return auth; + } + } + } catch (SQLException ex) { + logSqlException(ex); + } + return null; + } + + @Override + public boolean saveAuth(PlayerAuth auth) { + super.saveAuth(auth); + + try (Connection con = getConnection()) { + if (!columnOthers.isEmpty()) { + for (String column : columnOthers) { + try (PreparedStatement pst = con.prepareStatement( + "UPDATE " + tableName + " SET " + column + "=? WHERE " + col.NAME + "=?;")) { + pst.setString(1, auth.getRealName()); + pst.setString(2, auth.getNickname()); + pst.executeUpdate(); + } + } + } + + sqlExtension.saveAuth(auth, con); + return true; + } catch (SQLException ex) { + logSqlException(ex); + } + return false; + } + + @Override + String getJdbcUrl(String host, String port, String database) { + return "jdbc:postgresql://" + host + ":" + port + "/" + database; + } + + @Override + public Set getRecordsToPurge(long until) { + Set list = new HashSet<>(); + String select = "SELECT " + col.NAME + " FROM " + tableName + " WHERE GREATEST(" + + " COALESCE(" + col.LAST_LOGIN + ", 0)," + + " COALESCE(" + col.REGISTRATION_DATE + ", 0)" + + ") < ?;"; + try (Connection con = getConnection(); + PreparedStatement selectPst = con.prepareStatement(select)) { + selectPst.setLong(1, until); + try (ResultSet rs = selectPst.executeQuery()) { + while (rs.next()) { + list.add(rs.getString(col.NAME)); + } + } + } catch (SQLException ex) { + logSqlException(ex); + } + + return list; + } + + @Override + public boolean removeAuth(String user) { + user = user.toLowerCase(Locale.ROOT); + String sql = "DELETE FROM " + tableName + " WHERE " + col.NAME + "=?;"; + try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) { + sqlExtension.removeAuth(user, con); + pst.setString(1, user.toLowerCase(Locale.ROOT)); + pst.executeUpdate(); + return true; + } catch (SQLException ex) { + logSqlException(ex); + } + return false; + } + + @Override + public void closeConnection() { + if (ds != null && !ds.isClosed()) { + ds.close(); + } + } + + @Override + public void purgeRecords(Collection toPurge) { + String sql = "DELETE FROM " + tableName + " WHERE " + col.NAME + "=?;"; + try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) { + for (String name : toPurge) { + pst.setString(1, name.toLowerCase(Locale.ROOT)); + pst.executeUpdate(); + } + } catch (SQLException ex) { + logSqlException(ex); + } + } + + @Override + public DataSourceType getType() { + return DataSourceType.POSTGRESQL; + } + + @Override + public List getAllAuths() { + List auths = new ArrayList<>(); + try (Connection con = getConnection(); Statement st = con.createStatement()) { + try (ResultSet rs = st.executeQuery("SELECT * FROM " + tableName)) { + while (rs.next()) { + PlayerAuth auth = buildAuthFromResultSet(rs); + sqlExtension.extendAuth(auth, rs.getInt(col.ID), con); + auths.add(auth); + } + } + } catch (SQLException ex) { + logSqlException(ex); + } + return auths; + } + + @Override + public List getLoggedPlayersWithEmptyMail() { + List players = new ArrayList<>(); + String sql = "SELECT " + col.REAL_NAME + " FROM " + tableName + " WHERE " + col.IS_LOGGED + " = 1" + + " AND (" + col.EMAIL + " = 'your@email.com' OR " + col.EMAIL + " IS NULL);"; + try (Connection con = getConnection(); + Statement st = con.createStatement(); + ResultSet rs = st.executeQuery(sql)) { + while (rs.next()) { + players.add(rs.getString(1)); + } + } catch (SQLException ex) { + logSqlException(ex); + } + return players; + } + + @Override + public List getRecentlyLoggedInPlayers() { + List players = new ArrayList<>(); + String sql = "SELECT * FROM " + tableName + " ORDER BY " + col.LAST_LOGIN + " DESC LIMIT 10;"; + try (Connection con = getConnection(); + Statement st = con.createStatement(); + ResultSet rs = st.executeQuery(sql)) { + while (rs.next()) { + players.add(buildAuthFromResultSet(rs)); + } + } catch (SQLException e) { + logSqlException(e); + } + return players; + } + + @Override + public boolean setTotpKey(String user, String totpKey) { + String sql = "UPDATE " + tableName + " SET " + col.TOTP_KEY + " = ? WHERE " + col.NAME + " = ?"; + try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) { + pst.setString(1, totpKey); + pst.setString(2, user.toLowerCase(Locale.ROOT)); + pst.executeUpdate(); + return true; + } catch (SQLException e) { + logSqlException(e); + } + return false; + } + + /** + * Creates a {@link PlayerAuth} object with the data from the provided result set. + * + * @param row the result set to read from + * + * @return generated player auth object with the data from the result set + * + * @throws SQLException . + */ + private PlayerAuth buildAuthFromResultSet(ResultSet row) throws SQLException { + String salt = col.SALT.isEmpty() ? null : row.getString(col.SALT); + int group = col.GROUP.isEmpty() ? -1 : row.getInt(col.GROUP); + return PlayerAuth.builder() + .name(row.getString(col.NAME)) + .realName(row.getString(col.REAL_NAME)) + .password(row.getString(col.PASSWORD), salt) + .totpKey(row.getString(col.TOTP_KEY)) + .lastLogin(getNullableLong(row, col.LAST_LOGIN)) + .lastIp(row.getString(col.LAST_IP)) + .email(row.getString(col.EMAIL)) + .registrationDate(row.getLong(col.REGISTRATION_DATE)) + .registrationIp(row.getString(col.REGISTRATION_IP)) + .groupId(group) + .locWorld(row.getString(col.LASTLOC_WORLD)) + .locX(row.getDouble(col.LASTLOC_X)) + .locY(row.getDouble(col.LASTLOC_Y)) + .locZ(row.getDouble(col.LASTLOC_Z)) + .locYaw(row.getFloat(col.LASTLOC_YAW)) + .locPitch(row.getFloat(col.LASTLOC_PITCH)) + .build(); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/SQLite.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/SQLite.java new file mode 100644 index 00000000..b261b5d5 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/SQLite.java @@ -0,0 +1,423 @@ +package fr.xephi.authme.datasource; + +import com.google.common.annotations.VisibleForTesting; +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.datasource.columnshandler.AuthMeColumnsHandler; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.properties.DatabaseSettings; + +import java.io.File; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +import static fr.xephi.authme.datasource.SqlDataSourceUtils.getNullableLong; +import static fr.xephi.authme.datasource.SqlDataSourceUtils.logSqlException; + +/** + * SQLite data source. + */ +@SuppressWarnings({"checkstyle:AbbreviationAsWordInName"}) // Justification: Class name cannot be changed anymore +public class SQLite extends AbstractSqlDataSource { + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(SQLite.class); + private final Settings settings; + private final File dataFolder; + private final String database; + private final String tableName; + private final Columns col; + private Connection con; + + /** + * Constructor for SQLite. + * + * @param settings The settings instance + * @param dataFolder The data folder + * + * @throws SQLException when initialization of a SQL datasource failed + */ + public SQLite(Settings settings, File dataFolder) throws SQLException { + this.settings = settings; + this.dataFolder = dataFolder; + this.database = settings.getProperty(DatabaseSettings.MYSQL_DATABASE); + this.tableName = settings.getProperty(DatabaseSettings.MYSQL_TABLE); + this.col = new Columns(settings); + + try { + this.connect(); + this.setup(); + this.migrateIfNeeded(); + } catch (Exception ex) { + logger.logException("Error during SQLite initialization:", ex); + throw ex; + } + } + + @VisibleForTesting + SQLite(Settings settings, File dataFolder, Connection connection) { + this.settings = settings; + this.dataFolder = dataFolder; + this.database = settings.getProperty(DatabaseSettings.MYSQL_DATABASE); + this.tableName = settings.getProperty(DatabaseSettings.MYSQL_TABLE); + this.col = new Columns(settings); + this.con = connection; + this.columnsHandler = AuthMeColumnsHandler.createForSqlite(con, settings); + } + + /** + * Initializes the connection to the SQLite database. + * + * @throws SQLException when an SQL error occurs while connecting + */ + protected void connect() throws SQLException { + try { + Class.forName("org.sqlite.JDBC"); + } catch (ClassNotFoundException e) { + throw new IllegalStateException("Failed to load SQLite JDBC class", e); + } + + logger.debug("SQLite driver loaded"); + this.con = DriverManager.getConnection(this.getJdbcUrl(this.dataFolder.getAbsolutePath(), "", this.database)); + this.columnsHandler = AuthMeColumnsHandler.createForSqlite(con, settings); + } + + /** + * Creates the table if necessary, or adds any missing columns to the table. + * + * @throws SQLException when an SQL error occurs while initializing the database + */ + @VisibleForTesting + @SuppressWarnings("checkstyle:CyclomaticComplexity") + protected void setup() throws SQLException { + try (Statement st = con.createStatement()) { + // Note: cannot add unique fields later on in SQLite, so we add it on initialization + st.executeUpdate("CREATE TABLE IF NOT EXISTS " + tableName + " (" + + col.ID + " INTEGER AUTO_INCREMENT, " + + col.NAME + " VARCHAR(255) NOT NULL UNIQUE, " + + "CONSTRAINT table_const_prim PRIMARY KEY (" + col.ID + "));"); + + DatabaseMetaData md = con.getMetaData(); + + if (isColumnMissing(md, col.REAL_NAME)) { + st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + + col.REAL_NAME + " VARCHAR(255) NOT NULL DEFAULT 'Player';"); + } + + if (isColumnMissing(md, col.PASSWORD)) { + st.executeUpdate("ALTER TABLE " + tableName + + " ADD COLUMN " + col.PASSWORD + " VARCHAR(255) NOT NULL DEFAULT '';"); + } + + if (!col.SALT.isEmpty() && isColumnMissing(md, col.SALT)) { + st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.SALT + " VARCHAR(255);"); + } + + if (isColumnMissing(md, col.LAST_IP)) { + st.executeUpdate("ALTER TABLE " + tableName + + " ADD COLUMN " + col.LAST_IP + " VARCHAR(40);"); + } + + if (isColumnMissing(md, col.LAST_LOGIN)) { + st.executeUpdate("ALTER TABLE " + tableName + + " ADD COLUMN " + col.LAST_LOGIN + " TIMESTAMP;"); + } + + if (isColumnMissing(md, col.REGISTRATION_IP)) { + st.executeUpdate("ALTER TABLE " + tableName + + " ADD COLUMN " + col.REGISTRATION_IP + " VARCHAR(40);"); + } + + if (isColumnMissing(md, col.REGISTRATION_DATE)) { + addRegistrationDateColumn(st); + } + + if (isColumnMissing(md, col.LASTLOC_X)) { + st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.LASTLOC_X + + " DOUBLE NOT NULL DEFAULT '0.0';"); + st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.LASTLOC_Y + + " DOUBLE NOT NULL DEFAULT '0.0';"); + st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.LASTLOC_Z + + " DOUBLE NOT NULL DEFAULT '0.0';"); + } + + if (isColumnMissing(md, col.LASTLOC_WORLD)) { + st.executeUpdate("ALTER TABLE " + tableName + + " ADD COLUMN " + col.LASTLOC_WORLD + " VARCHAR(255) NOT NULL DEFAULT 'world';"); + } + + if (isColumnMissing(md, col.LASTLOC_YAW)) { + st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + + col.LASTLOC_YAW + " FLOAT;"); + } + + if (isColumnMissing(md, col.LASTLOC_PITCH)) { + st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + + col.LASTLOC_PITCH + " FLOAT;"); + } + + if (isColumnMissing(md, col.EMAIL)) { + st.executeUpdate("ALTER TABLE " + tableName + + " ADD COLUMN " + col.EMAIL + " VARCHAR(255);"); + } + + if (isColumnMissing(md, col.IS_LOGGED)) { + st.executeUpdate("ALTER TABLE " + tableName + + " ADD COLUMN " + col.IS_LOGGED + " INT NOT NULL DEFAULT '0';"); + } + + if (isColumnMissing(md, col.HAS_SESSION)) { + st.executeUpdate("ALTER TABLE " + tableName + + " ADD COLUMN " + col.HAS_SESSION + " INT NOT NULL DEFAULT '0';"); + } + + if (isColumnMissing(md, col.TOTP_KEY)) { + st.executeUpdate("ALTER TABLE " + tableName + + " ADD COLUMN " + col.TOTP_KEY + " VARCHAR(32);"); + } + + if (!col.PLAYER_UUID.isEmpty() && isColumnMissing(md, col.PLAYER_UUID)) { + st.executeUpdate("ALTER TABLE " + tableName + + " ADD COLUMN " + col.PLAYER_UUID + " VARCHAR(36)"); + } + } + logger.info("SQLite Setup finished"); + } + + /** + * Migrates the database if necessary. See {@link SqLiteMigrater} for details. + */ + @VisibleForTesting + void migrateIfNeeded() throws SQLException { + DatabaseMetaData metaData = con.getMetaData(); + if (SqLiteMigrater.isMigrationRequired(metaData, tableName, col)) { + new SqLiteMigrater(settings, dataFolder).performMigration(this); + // Migration deletes the table and recreates it, therefore call connect() again + // to get an up-to-date Connection to the database + connect(); + } + } + + private boolean isColumnMissing(DatabaseMetaData metaData, String columnName) throws SQLException { + try (ResultSet rs = metaData.getColumns(null, null, tableName, columnName)) { + return !rs.next(); + } + } + + @Override + public void reload() { + close(con); + try { + this.connect(); + this.setup(); + this.migrateIfNeeded(); + } catch (SQLException ex) { + logger.logException("Error while reloading SQLite:", ex); + } + } + + @Override + public PlayerAuth getAuth(String user) { + String sql = "SELECT * FROM " + tableName + " WHERE LOWER(" + col.NAME + ")=LOWER(?);"; + try (PreparedStatement pst = con.prepareStatement(sql)) { + pst.setString(1, user); + try (ResultSet rs = pst.executeQuery()) { + if (rs.next()) { + return buildAuthFromResultSet(rs); + } + } + } catch (SQLException ex) { + logSqlException(ex); + } + return null; + } + + @Override + public Set getRecordsToPurge(long until) { + Set list = new HashSet<>(); + String select = "SELECT " + col.NAME + " FROM " + tableName + " WHERE MAX(" + + " COALESCE(" + col.LAST_LOGIN + ", 0)," + + " COALESCE(" + col.REGISTRATION_DATE + ", 0)" + + ") < ?;"; + try (PreparedStatement selectPst = con.prepareStatement(select)) { + selectPst.setLong(1, until); + try (ResultSet rs = selectPst.executeQuery()) { + while (rs.next()) { + list.add(rs.getString(col.NAME)); + } + } + } catch (SQLException ex) { + logSqlException(ex); + } + + return list; + } + + @Override + public void purgeRecords(Collection toPurge) { + String delete = "DELETE FROM " + tableName + " WHERE " + col.NAME + "=?;"; + try (PreparedStatement deletePst = con.prepareStatement(delete)) { + for (String name : toPurge) { + deletePst.setString(1, name.toLowerCase(Locale.ROOT)); + deletePst.executeUpdate(); + } + } catch (SQLException ex) { + logSqlException(ex); + } + } + + @Override + public boolean removeAuth(String user) { + String sql = "DELETE FROM " + tableName + " WHERE " + col.NAME + "=?;"; + try (PreparedStatement pst = con.prepareStatement(sql)) { + pst.setString(1, user.toLowerCase(Locale.ROOT)); + pst.executeUpdate(); + return true; + } catch (SQLException ex) { + logSqlException(ex); + } + return false; + } + + @Override + public void closeConnection() { + try { + if (con != null && !con.isClosed()) { + con.close(); + } + } catch (SQLException ex) { + logSqlException(ex); + } + } + + @Override + public DataSourceType getType() { + return DataSourceType.SQLITE; + } + + @Override + public List getAllAuths() { + List auths = new ArrayList<>(); + String sql = "SELECT * FROM " + tableName + ";"; + try (PreparedStatement pst = con.prepareStatement(sql); ResultSet rs = pst.executeQuery()) { + while (rs.next()) { + PlayerAuth auth = buildAuthFromResultSet(rs); + auths.add(auth); + } + } catch (SQLException ex) { + logSqlException(ex); + } + return auths; + } + + @Override + public List getLoggedPlayersWithEmptyMail() { + List players = new ArrayList<>(); + String sql = "SELECT " + col.REAL_NAME + " FROM " + tableName + " WHERE " + col.IS_LOGGED + " = 1" + + " AND (" + col.EMAIL + " = 'your@email.com' OR " + col.EMAIL + " IS NULL);"; + try (Statement st = con.createStatement(); ResultSet rs = st.executeQuery(sql)) { + while (rs.next()) { + players.add(rs.getString(1)); + } + } catch (SQLException ex) { + logSqlException(ex); + } + return players; + } + + @Override + public List getRecentlyLoggedInPlayers() { + List players = new ArrayList<>(); + String sql = "SELECT * FROM " + tableName + " ORDER BY " + col.LAST_LOGIN + " DESC LIMIT 10;"; + try (Statement st = con.createStatement(); ResultSet rs = st.executeQuery(sql)) { + while (rs.next()) { + players.add(buildAuthFromResultSet(rs)); + } + } catch (SQLException e) { + logSqlException(e); + } + return players; + } + + + @Override + public boolean setTotpKey(String user, String totpKey) { + String sql = "UPDATE " + tableName + " SET " + col.TOTP_KEY + " = ? WHERE " + col.NAME + " = ?"; + try (PreparedStatement pst = con.prepareStatement(sql)) { + pst.setString(1, totpKey); + pst.setString(2, user.toLowerCase(Locale.ROOT)); + pst.executeUpdate(); + return true; + } catch (SQLException e) { + logSqlException(e); + } + return false; + } + + private PlayerAuth buildAuthFromResultSet(ResultSet row) throws SQLException { + String salt = !col.SALT.isEmpty() ? row.getString(col.SALT) : null; + + return PlayerAuth.builder() + .name(row.getString(col.NAME)) + .email(row.getString(col.EMAIL)) + .realName(row.getString(col.REAL_NAME)) + .password(row.getString(col.PASSWORD), salt) + .totpKey(row.getString(col.TOTP_KEY)) + .lastLogin(getNullableLong(row, col.LAST_LOGIN)) + .lastIp(row.getString(col.LAST_IP)) + .registrationDate(row.getLong(col.REGISTRATION_DATE)) + .registrationIp(row.getString(col.REGISTRATION_IP)) + .locX(row.getDouble(col.LASTLOC_X)) + .locY(row.getDouble(col.LASTLOC_Y)) + .locZ(row.getDouble(col.LASTLOC_Z)) + .locWorld(row.getString(col.LASTLOC_WORLD)) + .locYaw(row.getFloat(col.LASTLOC_YAW)) + .locPitch(row.getFloat(col.LASTLOC_PITCH)) + .build(); + } + + /** + * Creates the column for registration date and sets all entries to the current timestamp. + * We do so in order to avoid issues with purging, where entries with 0 / NULL might get + * purged immediately on startup otherwise. + * + * @param st Statement object to the database + */ + private void addRegistrationDateColumn(Statement st) throws SQLException { + st.executeUpdate("ALTER TABLE " + tableName + + " ADD COLUMN " + col.REGISTRATION_DATE + " TIMESTAMP NOT NULL DEFAULT '0';"); + + // Use the timestamp from Java to avoid timezone issues in case JVM and database are out of sync + long currentTimestamp = System.currentTimeMillis(); + int updatedRows = st.executeUpdate(String.format("UPDATE %s SET %s = %d;", + tableName, col.REGISTRATION_DATE, currentTimestamp)); + logger.info("Created column '" + col.REGISTRATION_DATE + "' and set the current timestamp, " + + currentTimestamp + ", to all " + updatedRows + " rows"); + } + + @Override + String getJdbcUrl(String dataPath, String ignored, String database) { + return "jdbc:sqlite:" + dataPath + File.separator + database + ".db"; + } + + private static void close(Connection con) { + if (con != null) { + try { + con.close(); + } catch (SQLException ex) { + logSqlException(ex); + } + } + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/SqLiteMigrater.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/SqLiteMigrater.java new file mode 100644 index 00000000..b4a2a577 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/SqLiteMigrater.java @@ -0,0 +1,147 @@ +package fr.xephi.authme.datasource; + +import com.google.common.io.Files; +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.properties.DatabaseSettings; +import fr.xephi.authme.util.FileUtils; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Field; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.SQLException; +import java.sql.Statement; + +/** + * Migrates the SQLite database when necessary. + */ +class SqLiteMigrater { + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(SqLiteMigrater.class); + private final File dataFolder; + private final String databaseName; + private final String tableName; + private final Columns col; + + SqLiteMigrater(Settings settings, File dataFolder) { + this.dataFolder = dataFolder; + this.databaseName = settings.getProperty(DatabaseSettings.MYSQL_DATABASE); + this.tableName = settings.getProperty(DatabaseSettings.MYSQL_TABLE); + this.col = new Columns(settings); + } + + /** + * Returns whether the database needs to be migrated. + *

+ * Background: Before commit 22911a0 (July 2016), new SQLite databases initialized the last IP column to be NOT NULL + * without a default value. Allowing the last IP to be null (#792) is therefore not compatible. + * + * @param metaData the database meta data + * @param tableName the table name (SQLite file name) + * @param col column names configuration + * @return true if a migration is necessary, false otherwise + */ + static boolean isMigrationRequired(DatabaseMetaData metaData, String tableName, Columns col) throws SQLException { + return SqlDataSourceUtils.isNotNullColumn(metaData, tableName, col.LAST_IP) + && SqlDataSourceUtils.getColumnDefaultValue(metaData, tableName, col.LAST_IP) == null; + } + + /** + * Migrates the given SQLite instance. + * + * @param sqLite the instance to migrate + */ + void performMigration(SQLite sqLite) throws SQLException { + logger.warning("YOUR SQLITE DATABASE NEEDS MIGRATING! DO NOT TURN OFF YOUR SERVER"); + + String backupName = createBackup(); + logger.info("Made a backup of your database at 'backups/" + backupName + "'"); + + recreateDatabaseWithNewDefinitions(sqLite); + logger.info("SQLite database migrated successfully"); + } + + private String createBackup() { + File sqLite = new File(dataFolder, databaseName + ".db"); + File backupDirectory = new File(dataFolder, "backups"); + FileUtils.createDirectory(backupDirectory); + + String backupName = "backup-" + databaseName + FileUtils.createCurrentTimeString() + ".db"; + File backup = new File(backupDirectory, backupName); + try { + Files.copy(sqLite, backup); + return backupName; + } catch (IOException e) { + throw new IllegalStateException("Failed to create SQLite backup before migration", e); + } + } + + /** + * Renames the current database, creates a new database under the name and copies the data + * from the renamed database to the newly created one. This is necessary because SQLite + * does not support dropping or modifying a column. + * + * @param sqLite the SQLite instance to migrate + */ + // cf. https://stackoverflow.com/questions/805363/how-do-i-rename-a-column-in-a-sqlite-database-table + private void recreateDatabaseWithNewDefinitions(SQLite sqLite) throws SQLException { + Connection connection = getConnection(sqLite); + String tempTable = "tmp_" + tableName; + try (Statement st = connection.createStatement()) { + st.execute("ALTER TABLE " + tableName + " RENAME TO " + tempTable + ";"); + } + + sqLite.reload(); + connection = getConnection(sqLite); + + try (Statement st = connection.createStatement()) { + String copySql = "INSERT INTO $table ($id, $name, $realName, $password, $lastIp, $lastLogin, $regIp, " + + "$regDate, $locX, $locY, $locZ, $locWorld, $locPitch, $locYaw, $email, $isLogged)" + + "SELECT $id, $name, $realName," + + " $password, CASE WHEN $lastIp = '127.0.0.1' OR $lastIp = '' THEN NULL else $lastIp END," + + " $lastLogin, $regIp, $regDate, $locX, $locY, $locZ, $locWorld, $locPitch, $locYaw," + + " CASE WHEN $email = 'your@email.com' THEN NULL ELSE $email END, $isLogged" + + " FROM " + tempTable + ";"; + int insertedEntries = st.executeUpdate(replaceColumnVariables(copySql)); + logger.info("Copied over " + insertedEntries + " from the old table to the new one"); + + st.execute("DROP TABLE " + tempTable + ";"); + } + } + + private String replaceColumnVariables(String sql) { + String replacedSql = sql.replace("$table", tableName).replace("$id", col.ID) + .replace("$name", col.NAME).replace("$realName", col.REAL_NAME) + .replace("$password", col.PASSWORD).replace("$lastIp", col.LAST_IP) + .replace("$lastLogin", col.LAST_LOGIN).replace("$regIp", col.REGISTRATION_IP) + .replace("$regDate", col.REGISTRATION_DATE).replace("$locX", col.LASTLOC_X) + .replace("$locY", col.LASTLOC_Y).replace("$locZ", col.LASTLOC_Z) + .replace("$locWorld", col.LASTLOC_WORLD).replace("$locPitch", col.LASTLOC_PITCH) + .replace("$locYaw", col.LASTLOC_YAW).replace("$email", col.EMAIL) + .replace("$isLogged", col.IS_LOGGED); + if (replacedSql.contains("$")) { + throw new IllegalStateException("SQL still statement still has '$' in it - was a tag not replaced?" + + " Replacement result: " + replacedSql); + } + return replacedSql; + } + + /** + * Returns the connection from the given SQLite instance. + * + * @param sqLite the SQLite instance to process + * @return the connection to the SQLite database + */ + private static Connection getConnection(SQLite sqLite) { + try { + Field connectionField = SQLite.class.getDeclaredField("con"); + connectionField.setAccessible(true); + return (Connection) connectionField.get(sqLite); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new IllegalStateException("Failed to get the connection from SQLite", e); + } + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/SqlDataSourceUtils.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/SqlDataSourceUtils.java new file mode 100644 index 00000000..1a936f02 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/SqlDataSourceUtils.java @@ -0,0 +1,109 @@ +package fr.xephi.authme.datasource; + +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.output.ConsoleLoggerFactory; + +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.sql.SQLException; + +/** + * Utilities for SQL data sources. + */ +public final class SqlDataSourceUtils { + + private static final ConsoleLogger logger = ConsoleLoggerFactory.get(SqlDataSourceUtils.class); + + private SqlDataSourceUtils() { + } + + /** + * Logs a SQL exception. + * + * @param e the exception to log + */ + public static void logSqlException(SQLException e) { + logger.logException("Error during SQL operation:", e); + } + + /** + * Returns the long value of a column, or null when appropriate. This method is necessary because + * JDBC's {@link ResultSet#getLong} returns {@code 0} if the entry in the database is {@code null}. + * + * @param rs the result set to read from + * @param columnName the name of the column to retrieve + * @return the value (which may be null) + * @throws SQLException :) + */ + public static Long getNullableLong(ResultSet rs, String columnName) throws SQLException { + long longValue = rs.getLong(columnName); + return rs.wasNull() ? null : longValue; + } + + /** + * Returns whether the given column has a NOT NULL constraint. + * + * @param metaData the database meta data + * @param tableName the name of the table in which the column is + * @param columnName the name of the column to check + * @return true if the column is NOT NULL, false otherwise + * @throws SQLException :) + */ + public static boolean isNotNullColumn(DatabaseMetaData metaData, String tableName, + String columnName) throws SQLException { + try (ResultSet rs = metaData.getColumns(null, null, tableName, columnName)) { + if (!rs.next()) { + throw new IllegalStateException("Did not find meta data for column '" + + columnName + "' while checking for not-null constraint"); + } + + int nullableCode = rs.getInt("NULLABLE"); + if (nullableCode == DatabaseMetaData.columnNoNulls) { + return true; + } else if (nullableCode == DatabaseMetaData.columnNullableUnknown) { + logger.warning("Unknown nullable status for column '" + columnName + "'"); + } + } + return false; + } + + /** + * Returns the default value of a column (as per its SQL definition). + * + * @param metaData the database meta data + * @param tableName the name of the table in which the column is + * @param columnName the name of the column to check + * @return the default value of the column (may be null) + * @throws SQLException :) + */ + public static Object getColumnDefaultValue(DatabaseMetaData metaData, String tableName, + String columnName) throws SQLException { + try (ResultSet rs = metaData.getColumns(null, null, tableName, columnName)) { + if (!rs.next()) { + throw new IllegalStateException("Did not find meta data for column '" + + columnName + "' while checking its default value"); + } + return rs.getObject("COLUMN_DEF"); + } + } + + /** + * Returns the size of a column (as per its SQL definition). + * + * @param metaData the database meta data + * @param tableName the name of the table in which the column is + * @param columnName the name of the column to check + * @return the size of the column + * @throws SQLException :) + */ + public static int getColumnSize(DatabaseMetaData metaData, String tableName, + String columnName) throws SQLException { + try (ResultSet rs = metaData.getColumns(null, null, tableName, columnName)) { + if (!rs.next()) { + throw new IllegalStateException("Did not find meta data for column '" + + columnName + "' while checking its size"); + } + return rs.getInt("COLUMN_SIZE"); + } + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/columnshandler/AuthMeColumns.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/columnshandler/AuthMeColumns.java new file mode 100644 index 00000000..47851a5a --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/columnshandler/AuthMeColumns.java @@ -0,0 +1,86 @@ +package fr.xephi.authme.datasource.columnshandler; + +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.settings.properties.DatabaseSettings; + +import static fr.xephi.authme.datasource.columnshandler.AuthMeColumnsFactory.ColumnOptions.DEFAULT_FOR_NULL; +import static fr.xephi.authme.datasource.columnshandler.AuthMeColumnsFactory.ColumnOptions.OPTIONAL; +import static fr.xephi.authme.datasource.columnshandler.AuthMeColumnsFactory.createDouble; +import static fr.xephi.authme.datasource.columnshandler.AuthMeColumnsFactory.createFloat; +import static fr.xephi.authme.datasource.columnshandler.AuthMeColumnsFactory.createInteger; +import static fr.xephi.authme.datasource.columnshandler.AuthMeColumnsFactory.createLong; +import static fr.xephi.authme.datasource.columnshandler.AuthMeColumnsFactory.createString; + +/** + * Contains column definitions for the AuthMe table. + */ +public final class AuthMeColumns { + + public static final PlayerAuthColumn NAME = createString( + DatabaseSettings.MYSQL_COL_NAME, PlayerAuth::getNickname); + + public static final PlayerAuthColumn NICK_NAME = createString( + DatabaseSettings.MYSQL_COL_REALNAME, PlayerAuth::getRealName); + + public static final PlayerAuthColumn PASSWORD = createString( + DatabaseSettings.MYSQL_COL_PASSWORD, auth -> auth.getPassword().getHash()); + + public static final PlayerAuthColumn SALT = createString( + DatabaseSettings.MYSQL_COL_SALT, auth -> auth.getPassword().getSalt(), OPTIONAL); + + public static final PlayerAuthColumn EMAIL = createString( + DatabaseSettings.MYSQL_COL_EMAIL, PlayerAuth::getEmail, DEFAULT_FOR_NULL); + + public static final PlayerAuthColumn LAST_IP = createString( + DatabaseSettings.MYSQL_COL_LAST_IP, PlayerAuth::getLastIp); + + public static final PlayerAuthColumn GROUP_ID = createInteger( + DatabaseSettings.MYSQL_COL_GROUP, PlayerAuth::getGroupId, OPTIONAL); + + public static final PlayerAuthColumn LAST_LOGIN = createLong( + DatabaseSettings.MYSQL_COL_LASTLOGIN, PlayerAuth::getLastLogin); + + public static final PlayerAuthColumn REGISTRATION_IP = createString( + DatabaseSettings.MYSQL_COL_REGISTER_IP, PlayerAuth::getRegistrationIp); + + public static final PlayerAuthColumn REGISTRATION_DATE = createLong( + DatabaseSettings.MYSQL_COL_REGISTER_DATE, PlayerAuth::getRegistrationDate); + + public static final PlayerAuthColumn UUID = createString( + DatabaseSettings.MYSQL_COL_PLAYER_UUID, + auth -> ( auth.getUuid() == null ? null : auth.getUuid().toString()), + OPTIONAL); + + // -------- + // Location columns + // -------- + public static final PlayerAuthColumn LOCATION_X = createDouble( + DatabaseSettings.MYSQL_COL_LASTLOC_X, PlayerAuth::getQuitLocX); + + public static final PlayerAuthColumn LOCATION_Y = createDouble( + DatabaseSettings.MYSQL_COL_LASTLOC_Y, PlayerAuth::getQuitLocY); + + public static final PlayerAuthColumn LOCATION_Z = createDouble( + DatabaseSettings.MYSQL_COL_LASTLOC_Z, PlayerAuth::getQuitLocZ); + + public static final PlayerAuthColumn LOCATION_WORLD = createString( + DatabaseSettings.MYSQL_COL_LASTLOC_WORLD, PlayerAuth::getWorld); + + public static final PlayerAuthColumn LOCATION_YAW = createFloat( + DatabaseSettings.MYSQL_COL_LASTLOC_YAW, PlayerAuth::getYaw); + + public static final PlayerAuthColumn LOCATION_PITCH = createFloat( + DatabaseSettings.MYSQL_COL_LASTLOC_PITCH, PlayerAuth::getPitch); + + // -------- + // Columns not on PlayerAuth + // -------- + public static final DataSourceColumn IS_LOGGED = createInteger( + DatabaseSettings.MYSQL_COL_ISLOGGED); + + public static final DataSourceColumn HAS_SESSION = createInteger( + DatabaseSettings.MYSQL_COL_HASSESSION); + + private AuthMeColumns() { + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/columnshandler/AuthMeColumnsFactory.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/columnshandler/AuthMeColumnsFactory.java new file mode 100644 index 00000000..3400f76c --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/columnshandler/AuthMeColumnsFactory.java @@ -0,0 +1,83 @@ +package fr.xephi.authme.datasource.columnshandler; + +import ch.jalu.configme.properties.Property; +import ch.jalu.datasourcecolumns.ColumnType; +import ch.jalu.datasourcecolumns.StandardTypes; +import fr.xephi.authme.data.auth.PlayerAuth; + +import java.util.function.Function; + +/** + * Util class for initializing {@link DataSourceColumn} objects. + */ +final class AuthMeColumnsFactory { + + private AuthMeColumnsFactory() { + } + + static DataSourceColumn createInteger(Property nameProperty, + ColumnOptions... options) { + return new DataSourceColumn<>(StandardTypes.INTEGER, nameProperty, + isOptional(options), hasDefaultForNull(options)); + } + + static PlayerAuthColumn createInteger(Property nameProperty, + Function playerAuthGetter, + ColumnOptions... options) { + return createInternal(StandardTypes.INTEGER, nameProperty, playerAuthGetter, options); + } + + static PlayerAuthColumn createLong(Property nameProperty, + Function playerAuthGetter, + ColumnOptions... options) { + return createInternal(StandardTypes.LONG, nameProperty, playerAuthGetter, options); + } + + static PlayerAuthColumn createString(Property nameProperty, + Function playerAuthGetter, + ColumnOptions... options) { + return createInternal(StandardTypes.STRING, nameProperty, playerAuthGetter, options); + } + + static PlayerAuthColumn createDouble(Property nameProperty, + Function playerAuthGetter, + ColumnOptions... options) { + return createInternal(StandardTypes.DOUBLE, nameProperty, playerAuthGetter, options); + } + + static PlayerAuthColumn createFloat(Property nameProperty, + Function playerAuthGetter, + ColumnOptions... options) { + return createInternal(StandardTypes.FLOAT, nameProperty, playerAuthGetter, options); + } + + private static PlayerAuthColumn createInternal(ColumnType type, Property nameProperty, + Function authGetter, + ColumnOptions... options) { + return new PlayerAuthColumn<>(type, nameProperty, isOptional(options), hasDefaultForNull(options), authGetter); + } + + private static boolean isOptional(ColumnOptions[] options) { + return containsInArray(ColumnOptions.OPTIONAL, options); + } + + private static boolean hasDefaultForNull(ColumnOptions[] options) { + return containsInArray(ColumnOptions.DEFAULT_FOR_NULL, options); + } + + private static boolean containsInArray(ColumnOptions needle, ColumnOptions[] haystack) { + for (ColumnOptions option : haystack) { + if (option == needle) { + return true; + } + } + return false; + } + + enum ColumnOptions { + + OPTIONAL, + + DEFAULT_FOR_NULL + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/columnshandler/AuthMeColumnsHandler.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/columnshandler/AuthMeColumnsHandler.java new file mode 100644 index 00000000..b910d36e --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/columnshandler/AuthMeColumnsHandler.java @@ -0,0 +1,227 @@ +package fr.xephi.authme.datasource.columnshandler; + +import ch.jalu.datasourcecolumns.data.DataSourceValue; +import ch.jalu.datasourcecolumns.data.DataSourceValues; +import ch.jalu.datasourcecolumns.data.UpdateValues; +import ch.jalu.datasourcecolumns.predicate.Predicate; +import ch.jalu.datasourcecolumns.sqlimplementation.PredicateSqlGenerator; +import ch.jalu.datasourcecolumns.sqlimplementation.SqlColumnsHandler; +import ch.jalu.datasourcecolumns.sqlimplementation.statementgenerator.ConnectionSupplier; +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.properties.DatabaseSettings; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.List; +import java.util.Locale; + +import static ch.jalu.datasourcecolumns.sqlimplementation.SqlColumnsHandlerConfig.forConnectionPool; +import static ch.jalu.datasourcecolumns.sqlimplementation.SqlColumnsHandlerConfig.forSingleConnection; +import static fr.xephi.authme.datasource.SqlDataSourceUtils.logSqlException; + +/** + * Wrapper of {@link SqlColumnsHandler} for the AuthMe data table. + * Wraps exceptions and provides better support for operations based on a {@link PlayerAuth} object. + */ +public final class AuthMeColumnsHandler { + + private final SqlColumnsHandler internalHandler; + + private AuthMeColumnsHandler(SqlColumnsHandler internalHandler) { + this.internalHandler = internalHandler; + } + + /** + * Creates a column handler for SQLite. + * + * @param connection the connection to the database + * @param settings plugin settings + * @return created column handler + */ + public static AuthMeColumnsHandler createForSqlite(Connection connection, Settings settings) { + ColumnContext columnContext = new ColumnContext(settings, false); + String tableName = settings.getProperty(DatabaseSettings.MYSQL_TABLE); + String nameColumn = settings.getProperty(DatabaseSettings.MYSQL_COL_NAME); + + SqlColumnsHandler sqlColHandler = new SqlColumnsHandler<>( + forSingleConnection(connection, tableName, nameColumn, columnContext) + .setPredicateSqlGenerator(new PredicateSqlGenerator<>(columnContext, true)) + ); + return new AuthMeColumnsHandler(sqlColHandler); + } + + /** + * Creates a column handler for H2. + * + * @param connection the connection to the database + * @param settings plugin settings + * @return created column handler + */ + public static AuthMeColumnsHandler createForH2(Connection connection, Settings settings) { + ColumnContext columnContext = new ColumnContext(settings, false); + String tableName = settings.getProperty(DatabaseSettings.MYSQL_TABLE); + String nameColumn = settings.getProperty(DatabaseSettings.MYSQL_COL_NAME); + + SqlColumnsHandler sqlColHandler = new SqlColumnsHandler<>( + forSingleConnection(connection, tableName, nameColumn, columnContext) + .setPredicateSqlGenerator(new PredicateSqlGenerator<>(columnContext, false)) + ); + return new AuthMeColumnsHandler(sqlColHandler); + } + + + /** + * Creates a column handler for MySQL. + * + * @param connectionSupplier supplier of connections from the connection pool + * @param settings plugin settings + * @return created column handler + */ + public static AuthMeColumnsHandler createForMySql(ConnectionSupplier connectionSupplier, Settings settings) { + ColumnContext columnContext = new ColumnContext(settings, true); + String tableName = settings.getProperty(DatabaseSettings.MYSQL_TABLE); + String nameColumn = settings.getProperty(DatabaseSettings.MYSQL_COL_NAME); + + SqlColumnsHandler sqlColHandler = new SqlColumnsHandler<>( + forConnectionPool(connectionSupplier, tableName, nameColumn, columnContext)); + return new AuthMeColumnsHandler(sqlColHandler); + } + + /** + * Changes a column from a specific row to the given value. + * + * @param name name of the account to modify + * @param column the column to modify + * @param value the value to set the column to + * @param the column type + * @return true upon success, false otherwise + */ + public boolean update(String name, DataSourceColumn column, T value) { + try { + return internalHandler.update(name.toLowerCase(Locale.ROOT), column, value); + } catch (SQLException e) { + logSqlException(e); + return false; + } + } + + /** + * Updates a row to have the values as retrieved from the PlayerAuth object. + * + * @param auth the player auth object to modify and to get values from + * @param columns the columns to update in the row + * @return true upon success, false otherwise + */ + public boolean update(PlayerAuth auth, PlayerAuthColumn... columns) { + try { + return internalHandler.update(auth.getNickname(), auth, columns); + } catch (SQLException e) { + logSqlException(e); + return false; + } + } + + /** + * Updates a row to have the given values. + * + * @param name the name of the account to modify + * @param updateValues the values to set on the row + * @return true upon success, false otherwise + */ + public boolean update(String name, UpdateValues updateValues) { + try { + return internalHandler.update(name.toLowerCase(Locale.ROOT), updateValues); + } catch (SQLException e) { + logSqlException(e); + return false; + } + } + + /** + * Sets the given value to the provided column for all rows which match the predicate. + * + * @param predicate the predicate to filter rows by + * @param column the column to modify on the matched rows + * @param value the new value to set + * @param the column type + * @return number of modified rows + */ + public int update(Predicate predicate, DataSourceColumn column, T value) { + try { + return internalHandler.update(predicate, column, value); + } catch (SQLException e) { + logSqlException(e); + return 0; + } + } + + /** + * Retrieves the given column from a given row. + * + * @param name the account name to look up + * @param column the column whose value should be retrieved + * @param the column type + * @return the result of the lookup + * @throws SQLException . + */ + public DataSourceValue retrieve(String name, DataSourceColumn column) throws SQLException { + return internalHandler.retrieve(name.toLowerCase(Locale.ROOT), column); + } + + /** + * Retrieves multiple values from a given row. + * + * @param name the account name to look up + * @param columns the columns to retrieve + * @return map-like object with the requested values + * @throws SQLException . + */ + public DataSourceValues retrieve(String name, DataSourceColumn... columns) throws SQLException { + return internalHandler.retrieve(name.toLowerCase(Locale.ROOT), columns); + } + + /** + * Retrieves a column's value for all rows that satisfy the given predicate. + * + * @param predicate the predicate to fulfill + * @param column the column to retrieve from the matching rows + * @param the column's value type + * @return the values of the matching rows + * @throws SQLException . + */ + public List retrieve(Predicate predicate, DataSourceColumn column) throws SQLException { + return internalHandler.retrieve(predicate, column); + } + + /** + * Inserts the given values into a new row, as taken from the player auth. + * + * @param auth the player auth to get values from + * @param columns the columns to insert + * @return true upon success, false otherwise + */ + public boolean insert(PlayerAuth auth, PlayerAuthColumn... columns) { + try { + return internalHandler.insert(auth, columns); + } catch (SQLException e) { + logSqlException(e); + return false; + } + } + + /** + * Returns the number of rows that match the provided predicate. + * + * @param predicate the predicate to test the rows for + * @return number of rows fulfilling the predicate + */ + public int count(Predicate predicate) { + try { + return internalHandler.count(predicate); + } catch (SQLException e) { + logSqlException(e); + return 0; + } + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/columnshandler/ColumnContext.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/columnshandler/ColumnContext.java new file mode 100644 index 00000000..483e31e9 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/columnshandler/ColumnContext.java @@ -0,0 +1,35 @@ +package fr.xephi.authme.datasource.columnshandler; + +import fr.xephi.authme.settings.Settings; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Context for resolving the properties of {@link AuthMeColumns} entries. + */ +public class ColumnContext { + + private final Settings settings; + private final Map, String> columnNames = new ConcurrentHashMap<>(); + private final boolean hasDefaultSupport; + + /** + * Constructor. + * + * @param settings plugin settings + * @param hasDefaultSupport whether or not the underlying database has support for the {@code DEFAULT} keyword + */ + public ColumnContext(Settings settings, boolean hasDefaultSupport) { + this.settings = settings; + this.hasDefaultSupport = hasDefaultSupport; + } + + public String getName(DataSourceColumn column) { + return columnNames.computeIfAbsent(column, k -> settings.getProperty(k.getNameProperty())); + } + + public boolean hasDefaultSupport() { + return hasDefaultSupport; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/columnshandler/DataSourceColumn.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/columnshandler/DataSourceColumn.java new file mode 100644 index 00000000..4b7fa4ca --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/columnshandler/DataSourceColumn.java @@ -0,0 +1,57 @@ +package fr.xephi.authme.datasource.columnshandler; + +import ch.jalu.configme.properties.Property; +import ch.jalu.datasourcecolumns.Column; +import ch.jalu.datasourcecolumns.ColumnType; + +/** + * Basic {@link Column} implementation for AuthMe. + * + * @param column type + */ +public class DataSourceColumn implements Column { + + private final ColumnType columnType; + private final Property nameProperty; + private final boolean isOptional; + private final boolean useDefaultForNull; + + /** + * Constructor. + * + * @param type type of the column + * @param nameProperty property defining the column name + * @param isOptional whether or not the column can be skipped (if name is configured to empty string) + * @param useDefaultForNull whether SQL DEFAULT should be used for null values (if supported by the database) + */ + DataSourceColumn(ColumnType type, Property nameProperty, boolean isOptional, boolean useDefaultForNull) { + this.columnType = type; + this.nameProperty = nameProperty; + this.isOptional = isOptional; + this.useDefaultForNull = useDefaultForNull; + } + + public Property getNameProperty() { + return nameProperty; + } + + @Override + public String resolveName(ColumnContext columnContext) { + return columnContext.getName(this); + } + + @Override + public ColumnType getType() { + return columnType; + } + + @Override + public boolean isColumnUsed(ColumnContext columnContext) { + return !isOptional || !resolveName(columnContext).isEmpty(); + } + + @Override + public boolean useDefaultForNullValue(ColumnContext columnContext) { + return useDefaultForNull && columnContext.hasDefaultSupport(); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/columnshandler/PlayerAuthColumn.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/columnshandler/PlayerAuthColumn.java new file mode 100644 index 00000000..43d022b1 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/columnshandler/PlayerAuthColumn.java @@ -0,0 +1,32 @@ +package fr.xephi.authme.datasource.columnshandler; + +import ch.jalu.configme.properties.Property; +import ch.jalu.datasourcecolumns.ColumnType; +import ch.jalu.datasourcecolumns.DependentColumn; +import fr.xephi.authme.data.auth.PlayerAuth; + +import java.util.function.Function; + +/** + * Implementation for columns which can also be retrieved from a {@link PlayerAuth} object. + * + * @param column type + */ +public class PlayerAuthColumn extends DataSourceColumn implements DependentColumn { + + private final Function playerAuthGetter; + + /* + * Constructor. See parent class for details. + */ + PlayerAuthColumn(ColumnType type, Property nameProperty, boolean isOptional, boolean useDefaultForNull, + Function playerAuthGetter) { + super(type, nameProperty, isOptional, useDefaultForNull); + this.playerAuthGetter = playerAuthGetter; + } + + @Override + public T getValueFromDependent(PlayerAuth auth) { + return playerAuthGetter.apply(auth); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/converter/AbstractDataSourceConverter.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/converter/AbstractDataSourceConverter.java new file mode 100644 index 00000000..fdced2fa --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/converter/AbstractDataSourceConverter.java @@ -0,0 +1,85 @@ +package fr.xephi.authme.datasource.converter; + +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.datasource.DataSourceType; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import org.bukkit.command.CommandSender; + +import java.util.ArrayList; +import java.util.List; + +import static fr.xephi.authme.util.Utils.logAndSendMessage; + +/** + * Converts from one AuthMe data source type to another. + * + * @param the source type to convert from + */ +public abstract class AbstractDataSourceConverter implements Converter { + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(MySqlToSqlite.class); + + private final DataSource destination; + private final DataSourceType destinationType; + + /** + * Constructor. + * + * @param destination the data source to convert to + * @param destinationType the data source type of the destination. The given data source is checked that its + * type corresponds to this type before the conversion is started, enabling us to just pass + * the current data source and letting this class check that the types correspond. + */ + public AbstractDataSourceConverter(DataSource destination, DataSourceType destinationType) { + this.destination = destination; + this.destinationType = destinationType; + } + + // Implementation note: Because of ForceFlatToSqlite it is possible that the CommandSender is null, + // which is never the case when a converter is launched from the /authme converter command. + @Override + public void execute(CommandSender sender) { + if (destinationType != destination.getType()) { + if (sender != null) { + sender.sendMessage("Please configure your connection to " + + destinationType + " and re-run this command"); + } + return; + } + + S source; + try { + source = getSource(); + } catch (Exception e) { + logAndSendMessage(sender, "The data source to convert from could not be initialized"); + logger.logException("Could not initialize source:", e); + return; + } + + List skippedPlayers = new ArrayList<>(); + for (PlayerAuth auth : source.getAllAuths()) { + if (destination.isAuthAvailable(auth.getNickname())) { + skippedPlayers.add(auth.getNickname()); + } else { + destination.saveAuth(auth); + destination.updateSession(auth); + destination.updateQuitLoc(auth); + } + } + + if (!skippedPlayers.isEmpty()) { + logAndSendMessage(sender, "Skipped conversion for players which were already in " + + destinationType + ": " + String.join(", ", skippedPlayers)); + } + logAndSendMessage(sender, "Database successfully converted from " + source.getType() + + " to " + destinationType); + } + + /** + * @return the data source to convert from + * @throws Exception during initialization of source + */ + protected abstract S getSource() throws Exception; +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/converter/Converter.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/converter/Converter.java new file mode 100644 index 00000000..7549bb48 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/converter/Converter.java @@ -0,0 +1,16 @@ +package fr.xephi.authme.datasource.converter; + +import org.bukkit.command.CommandSender; + +/** + * Interface for AuthMe converters. + */ +public interface Converter { + + /** + * Execute the conversion. + * + * @param sender the sender who initialized the conversion + */ + void execute(CommandSender sender); +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/converter/CrazyLoginConverter.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/converter/CrazyLoginConverter.java new file mode 100644 index 00000000..a9197223 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/converter/CrazyLoginConverter.java @@ -0,0 +1,82 @@ +package fr.xephi.authme.datasource.converter; + +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.initialization.DataFolder; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.settings.Settings; +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; +import java.util.Locale; + +/** + * Converter for CrazyLogin to AuthMe. + */ +public class CrazyLoginConverter implements Converter { + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(CrazyLoginConverter.class); + + private final DataSource database; + private final Settings settings; + private final File dataFolder; + + @Inject + CrazyLoginConverter(@DataFolder File dataFolder, DataSource dataSource, Settings settings) { + this.dataFolder = dataFolder; + this.database = dataSource; + this.settings = settings; + } + + @Override + 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("|")) { + migrateAccount(line); + } + } + logger.info("CrazyLogin database has been imported correctly"); + } catch (IOException ex) { + logger.warning("Can't open the crazylogin database file! Does it exist?"); + logger.logException("Encountered", ex); + } + } + + /** + * Moves an account from CrazyLogin to the AuthMe database. + * + * @param line line read from the CrazyLogin file (one account) + */ + private void migrateAccount(String line) { + String[] args = line.split("\\|"); + if (args.length < 2 || "name".equalsIgnoreCase(args[0])) { + return; + } + String playerName = args[0]; + String password = args[1]; + if (password != null) { + PlayerAuth auth = PlayerAuth.builder() + .name(playerName.toLowerCase(Locale.ROOT)) + .realName(playerName) + .password(password, null) + .build(); + database.saveAuth(auth); + } + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/converter/H2ToSqlite.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/converter/H2ToSqlite.java new file mode 100644 index 00000000..03520b80 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/converter/H2ToSqlite.java @@ -0,0 +1,33 @@ +package fr.xephi.authme.datasource.converter; + +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.datasource.DataSourceType; +import fr.xephi.authme.datasource.H2; +import fr.xephi.authme.initialization.DataFolder; +import fr.xephi.authme.settings.Settings; + +import javax.inject.Inject; +import java.io.File; +import java.sql.SQLException; + +/** + * Converts H2 to SQLite. + * + */ +public class H2ToSqlite extends AbstractDataSourceConverter

{ + + private final Settings settings; + private final File dataFolder; + + @Inject + H2ToSqlite(Settings settings, DataSource dataSource, @DataFolder File dataFolder) { + super(dataSource, DataSourceType.SQLITE); + this.settings = settings; + this.dataFolder = dataFolder; + } + + @Override + protected H2 getSource() throws SQLException { + return new H2(settings, dataFolder); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/converter/LoginSecurityConverter.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/converter/LoginSecurityConverter.java new file mode 100644 index 00000000..dbdce5df --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/converter/LoginSecurityConverter.java @@ -0,0 +1,210 @@ +package fr.xephi.authme.datasource.converter; + +import com.google.common.annotations.VisibleForTesting; +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.initialization.DataFolder; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.properties.ConverterSettings; +import fr.xephi.authme.util.UuidUtils; +import org.bukkit.command.CommandSender; + +import javax.inject.Inject; +import java.io.File; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static fr.xephi.authme.util.Utils.logAndSendMessage; + +/** + * Converts data from LoginSecurity to AuthMe. + */ +public class LoginSecurityConverter implements Converter { + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(LoginSecurityConverter.class); + private final File dataFolder; + private final DataSource dataSource; + + private final boolean useSqlite; + private final String mySqlHost; + private final String mySqlDatabase; + private final String mySqlUser; + private final String mySqlPassword; + + @Inject + LoginSecurityConverter(@DataFolder File dataFolder, DataSource dataSource, Settings settings) { + this.dataFolder = dataFolder; + this.dataSource = dataSource; + + useSqlite = settings.getProperty(ConverterSettings.LOGINSECURITY_USE_SQLITE); + mySqlHost = settings.getProperty(ConverterSettings.LOGINSECURITY_MYSQL_HOST); + mySqlDatabase = settings.getProperty(ConverterSettings.LOGINSECURITY_MYSQL_DATABASE); + mySqlUser = settings.getProperty(ConverterSettings.LOGINSECURITY_MYSQL_USER); + mySqlPassword = settings.getProperty(ConverterSettings.LOGINSECURITY_MYSQL_PASSWORD); + } + + @Override + public void execute(CommandSender sender) { + try (Connection connection = createConnectionOrInformSender(sender)) { + if (connection != null) { + performConversion(sender, connection); + logger.info("LoginSecurity conversion completed! Please remember to set \"legacyHashes: ['BCRYPT']\" " + + "in your configuration file!"); + } + } catch (SQLException e) { + sender.sendMessage("Failed to convert from SQLite. Please see the log for more info"); + logger.logException("Could not fetch or migrate data:", e); + } + } + + /** + * Performs the conversion from LoginSecurity to AuthMe. + * + * @param sender the command sender who launched the conversion + * @param connection connection to the LoginSecurity data source + */ + @VisibleForTesting + void performConversion(CommandSender sender, Connection connection) throws SQLException { + try (Statement statement = connection.createStatement()) { + statement.execute( + "SELECT * from ls_players LEFT JOIN ls_locations ON ls_locations.id = ls_players.id"); + try (ResultSet resultSet = statement.getResultSet()) { + migrateData(sender, resultSet); + } + } + } + + /** + * Migrates the accounts. + * + * @param sender the command sender + * @param resultSet result set with the account data to migrate + */ + private void migrateData(CommandSender sender, ResultSet resultSet) throws SQLException { + List skippedPlayers = new ArrayList<>(); + long successfulSaves = 0; + while (resultSet.next()) { + String name = resultSet.getString("last_name"); + if (dataSource.isAuthAvailable(name)) { + skippedPlayers.add(name); + } else { + PlayerAuth auth = buildAuthFromLoginSecurity(name, resultSet); + dataSource.saveAuth(auth); + dataSource.updateSession(auth); + ++successfulSaves; + } + } + + logAndSendMessage(sender, "Migrated " + successfulSaves + " accounts successfully from LoginSecurity"); + if (!skippedPlayers.isEmpty()) { + logAndSendMessage(sender, "Skipped conversion for players which were already in AuthMe: " + + String.join(", ", skippedPlayers)); + } + } + + /** + * Creates a PlayerAuth based on data extracted from the given result set. + * + * @param name the name of the player to build + * @param resultSet the result set to extract data from + * @return the created player auth object + */ + private static PlayerAuth buildAuthFromLoginSecurity(String name, ResultSet resultSet) throws SQLException { + Long lastLoginMillis = Optional.ofNullable(resultSet.getTimestamp("last_login")) + .map(Timestamp::getTime).orElse(null); + long regDate = Optional.ofNullable(resultSet.getDate("registration_date")) + .map(Date::getTime).orElse(System.currentTimeMillis()); + UUID uuid = UuidUtils.parseUuidSafely(resultSet.getString("unique_user_id")); + return PlayerAuth.builder() + .name(name) + .realName(name) + .password(resultSet.getString("password"), null) + .lastIp(resultSet.getString("ip_address")) + .lastLogin(lastLoginMillis) + .registrationDate(regDate) + .locX(resultSet.getDouble("x")) + .locY(resultSet.getDouble("y")) + .locZ(resultSet.getDouble("z")) + .locWorld(resultSet.getString("world")) + .locYaw(resultSet.getFloat("yaw")) + .locPitch(resultSet.getFloat("pitch")) + .uuid(uuid) + .build(); + } + + /** + * Creates a {@link Connection} to the LoginSecurity data source based on the settings, + * or informs the sender of the error that occurred. + * + * @param sender the command sender who launched the conversion + * @return the created connection object, or null if it failed + */ + private Connection createConnectionOrInformSender(CommandSender sender) { + Connection connection; + if (useSqlite) { + File sqliteDatabase = new File(dataFolder.getParentFile(), "LoginSecurity/LoginSecurity.db"); + if (!sqliteDatabase.exists()) { + sender.sendMessage("The file '" + sqliteDatabase.getPath() + "' does not exist"); + return null; + } + connection = createSqliteConnection("plugins/LoginSecurity/LoginSecurity.db"); + } else { + if (mySqlDatabase.isEmpty() || mySqlUser.isEmpty()) { + sender.sendMessage("The LoginSecurity database or username is not configured in AuthMe's config.yml"); + return null; + } + connection = createMySqlConnection(); + } + + if (connection == null) { + sender.sendMessage("Could not connect to LoginSecurity using Sqlite = " + + useSqlite + ", see log for more info"); + return null; + } + return connection; + } + + /** + * Creates a connection to SQLite. + * + * @param path the path to the SQLite database + * @return the created connection + */ + @VisibleForTesting + Connection createSqliteConnection(String path) { + try { + Class.forName("org.sqlite.JDBC"); + } catch (ClassNotFoundException e) { + throw new IllegalStateException(e); + } + + try { + return DriverManager.getConnection( + "jdbc:sqlite:" + path, "trump", "donald"); + } catch (SQLException e) { + logger.logException("Could not connect to SQLite database", e); + return null; + } + } + + private Connection createMySqlConnection() { + try { + return DriverManager.getConnection( + "jdbc:mysql://" + mySqlHost + "/" + mySqlDatabase, mySqlUser, mySqlPassword); + } catch (SQLException e) { + logger.logException("Could not connect to SQLite database", e); + return null; + } + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/converter/MySqlToSqlite.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/converter/MySqlToSqlite.java new file mode 100644 index 00000000..b1a485b9 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/converter/MySqlToSqlite.java @@ -0,0 +1,31 @@ +package fr.xephi.authme.datasource.converter; + +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.datasource.DataSourceType; +import fr.xephi.authme.datasource.MySQL; +import fr.xephi.authme.datasource.mysqlextensions.MySqlExtensionsFactory; +import fr.xephi.authme.settings.Settings; + +import javax.inject.Inject; +import java.sql.SQLException; + +/** + * Converts from MySQL to SQLite. + */ +public class MySqlToSqlite extends AbstractDataSourceConverter { + + private final Settings settings; + private final MySqlExtensionsFactory mySqlExtensionsFactory; + + @Inject + MySqlToSqlite(DataSource dataSource, Settings settings, MySqlExtensionsFactory mySqlExtensionsFactory) { + super(dataSource, DataSourceType.SQLITE); + this.settings = settings; + this.mySqlExtensionsFactory = mySqlExtensionsFactory; + } + + @Override + protected MySQL getSource() throws SQLException { + return new MySQL(settings, mySqlExtensionsFactory); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/converter/RoyalAuthConverter.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/converter/RoyalAuthConverter.java new file mode 100644 index 00000000..ebb9080e --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/converter/RoyalAuthConverter.java @@ -0,0 +1,61 @@ +package fr.xephi.authme.datasource.converter; + +import fr.xephi.authme.AuthMe; +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.output.ConsoleLoggerFactory; +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 java.util.Locale; + +import static fr.xephi.authme.util.FileUtils.makePath; + +public class RoyalAuthConverter implements Converter { + + private static final String LAST_LOGIN_PATH = "timestamps.quit"; + private static final String PASSWORD_PATH = "login.password"; + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(RoyalAuthConverter.class); + + private final AuthMe plugin; + private final DataSource dataSource; + + @Inject + RoyalAuthConverter(AuthMe plugin, DataSource dataSource) { + this.plugin = plugin; + this.dataSource = dataSource; + } + + @Override + public void execute(CommandSender sender) { + for (OfflinePlayer player : plugin.getServer().getOfflinePlayers()) { + try { + String name = player.getName().toLowerCase(Locale.ROOT); + File file = new File(makePath(".", "plugins", "RoyalAuth", "userdata", name + ".yml")); + + if (dataSource.isAuthAvailable(name) || !file.exists()) { + continue; + } + FileConfiguration configuration = YamlConfiguration.loadConfiguration(file); + PlayerAuth auth = PlayerAuth.builder() + .name(name) + .password(configuration.getString(PASSWORD_PATH), null) + .lastLogin(configuration.getLong(LAST_LOGIN_PATH)) + .realName(player.getName()) + .build(); + + dataSource.saveAuth(auth); + dataSource.updateSession(auth); + } catch (Exception e) { + logger.logException("Error while trying to import " + player.getName() + " RoyalAuth data", e); + } + } + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/converter/SqliteToH2.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/converter/SqliteToH2.java new file mode 100644 index 00000000..eea2bfca --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/converter/SqliteToH2.java @@ -0,0 +1,33 @@ +package fr.xephi.authme.datasource.converter; + +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.datasource.DataSourceType; +import fr.xephi.authme.datasource.SQLite; +import fr.xephi.authme.initialization.DataFolder; +import fr.xephi.authme.settings.Settings; + +import javax.inject.Inject; +import java.io.File; +import java.sql.SQLException; + +/** + * Converts SQLite to H2 + * + */ +public class SqliteToH2 extends AbstractDataSourceConverter{ + + private final Settings settings; + private final File dataFolder; + + @Inject + SqliteToH2(Settings settings, DataSource dataSource, @DataFolder File dataFolder) { + super(dataSource, DataSourceType.H2); + this.settings = settings; + this.dataFolder = dataFolder; + } + + @Override + protected SQLite getSource() throws SQLException { + return new SQLite(settings, dataFolder); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/converter/SqliteToSql.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/converter/SqliteToSql.java new file mode 100644 index 00000000..cc472da2 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/converter/SqliteToSql.java @@ -0,0 +1,32 @@ +package fr.xephi.authme.datasource.converter; + +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.datasource.DataSourceType; +import fr.xephi.authme.datasource.SQLite; +import fr.xephi.authme.initialization.DataFolder; +import fr.xephi.authme.settings.Settings; + +import javax.inject.Inject; +import java.io.File; +import java.sql.SQLException; + +/** + * Converts from SQLite to MySQL. + */ +public class SqliteToSql extends AbstractDataSourceConverter { + + private final Settings settings; + private final File dataFolder; + + @Inject + SqliteToSql(Settings settings, DataSource dataSource, @DataFolder File dataFolder) { + super(dataSource, DataSourceType.MYSQL); + this.settings = settings; + this.dataFolder = dataFolder; + } + + @Override + protected SQLite getSource() throws SQLException { + return new SQLite(settings, dataFolder); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/converter/XAuthConverter.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/converter/XAuthConverter.java new file mode 100644 index 00000000..0f04310c --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/converter/XAuthConverter.java @@ -0,0 +1,149 @@ +package fr.xephi.authme.datasource.converter; + +import de.luricos.bukkit.xAuth.database.DatabaseTables; +import de.luricos.bukkit.xAuth.utils.xAuthLog; +import de.luricos.bukkit.xAuth.xAuth; +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.initialization.DataFolder; +import fr.xephi.authme.util.Utils; +import org.bukkit.command.CommandSender; +import org.bukkit.plugin.PluginManager; + +import javax.inject.Inject; +import java.io.File; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import static fr.xephi.authme.util.FileUtils.makePath; + +public class XAuthConverter implements Converter { + + @Inject + @DataFolder + private File dataFolder; + @Inject + private DataSource database; + @Inject + private PluginManager pluginManager; + + XAuthConverter() { + } + + @Override + public void execute(CommandSender sender) { + try { + Class.forName("de.luricos.bukkit.xAuth.xAuth"); + convert(sender); + } catch (ClassNotFoundException ce) { + sender.sendMessage("xAuth has not been found, please put xAuth.jar in your plugin folder and restart!"); + } + } + + private void convert(CommandSender sender) { + if (pluginManager.getPlugin("xAuth") == null) { + sender.sendMessage("[AuthMe] xAuth plugin not found"); + return; + } + //TODO ljacqu 20160702: xAuthDb is not used except for the existence check -- is this intended? + File xAuthDb = new File(dataFolder.getParent(), makePath("xAuth", "xAuth.h2.db")); + if (!xAuthDb.exists()) { + sender.sendMessage("[AuthMe] xAuth H2 database not found, checking for MySQL or SQLite data..."); + } + List players = getXAuthPlayers(); + if (Utils.isCollectionEmpty(players)) { + sender.sendMessage("[AuthMe] Error while importing xAuthPlayers: did not find any players"); + return; + } + sender.sendMessage("[AuthMe] Starting import..."); + + for (int id : players) { + String pl = getIdPlayer(id); + String psw = getPassword(id); + if (psw != null && !psw.isEmpty() && pl != null) { + PlayerAuth auth = PlayerAuth.builder() + .name(pl.toLowerCase(Locale.ROOT)) + .realName(pl) + .password(psw, null).build(); + database.saveAuth(auth); + } + } + sender.sendMessage("[AuthMe] Successfully converted from xAuth database"); + } + + private String getIdPlayer(int id) { + String realPass = ""; + Connection conn = xAuth.getPlugin().getDatabaseController().getConnection(); + PreparedStatement ps = null; + ResultSet rs = null; + try { + String sql = String.format("SELECT `playername` FROM `%s` WHERE `id` = ?", + xAuth.getPlugin().getDatabaseController().getTable(DatabaseTables.ACCOUNT)); + ps = conn.prepareStatement(sql); + ps.setInt(1, id); + rs = ps.executeQuery(); + if (!rs.next()) { + return null; + } + realPass = rs.getString("playername").toLowerCase(Locale.ROOT); + } catch (SQLException e) { + xAuthLog.severe("Failed to retrieve name for account: " + id, e); + return null; + } finally { + xAuth.getPlugin().getDatabaseController().close(conn, ps, rs); + } + return realPass; + } + + private List getXAuthPlayers() { + List xP = new ArrayList<>(); + Connection conn = xAuth.getPlugin().getDatabaseController().getConnection(); + PreparedStatement ps = null; + ResultSet rs = null; + try { + String sql = String.format("SELECT * FROM `%s`", + xAuth.getPlugin().getDatabaseController().getTable(DatabaseTables.ACCOUNT)); + ps = conn.prepareStatement(sql); + rs = ps.executeQuery(); + while (rs.next()) { + xP.add(rs.getInt("id")); + } + } catch (SQLException e) { + xAuthLog.severe("Cannot import xAuthPlayers", e); + return new ArrayList<>(); + } finally { + xAuth.getPlugin().getDatabaseController().close(conn, ps, rs); + } + return xP; + } + + private String getPassword(int accountId) { + String realPass = ""; + Connection conn = xAuth.getPlugin().getDatabaseController().getConnection(); + PreparedStatement ps = null; + ResultSet rs = null; + try { + String sql = String.format("SELECT `password`, `pwtype` FROM `%s` WHERE `id` = ?", + xAuth.getPlugin().getDatabaseController().getTable(DatabaseTables.ACCOUNT)); + ps = conn.prepareStatement(sql); + ps.setInt(1, accountId); + rs = ps.executeQuery(); + if (!rs.next()) { + return null; + } + realPass = rs.getString("password"); + } catch (SQLException e) { + xAuthLog.severe("Failed to retrieve password hash for account: " + accountId, e); + return null; + } finally { + xAuth.getPlugin().getDatabaseController().close(conn, ps, rs); + } + return realPass; + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/mysqlextensions/Ipb4Extension.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/mysqlextensions/Ipb4Extension.java new file mode 100644 index 00000000..2f39b8f7 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/mysqlextensions/Ipb4Extension.java @@ -0,0 +1,55 @@ +package fr.xephi.authme.datasource.mysqlextensions; + +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.datasource.Columns; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.properties.HooksSettings; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +/** + * Extension for IPB4. + */ +class Ipb4Extension extends MySqlExtension { + + private final String ipbPrefix; + private final int ipbGroup; + + Ipb4Extension(Settings settings, Columns col) { + super(settings, col); + this.ipbPrefix = settings.getProperty(HooksSettings.IPB_TABLE_PREFIX); + this.ipbGroup = settings.getProperty(HooksSettings.IPB_ACTIVATED_GROUP_ID); + } + + @Override + public void saveAuth(PlayerAuth auth, Connection con) throws SQLException { + // Update player group in core_members + String sql = "UPDATE " + ipbPrefix + tableName + + " SET " + tableName + ".member_group_id=? WHERE " + col.NAME + "=?;"; + try (PreparedStatement pst2 = con.prepareStatement(sql)) { + pst2.setInt(1, ipbGroup); + pst2.setString(2, auth.getNickname()); + pst2.executeUpdate(); + } + // Get current time without ms + long time = System.currentTimeMillis() / 1000; + // update joined date + sql = "UPDATE " + ipbPrefix + tableName + + " SET " + tableName + ".joined=? WHERE " + col.NAME + "=?;"; + try (PreparedStatement pst2 = con.prepareStatement(sql)) { + pst2.setLong(1, time); + pst2.setString(2, auth.getNickname()); + pst2.executeUpdate(); + } + // Update last_visit + sql = "UPDATE " + ipbPrefix + tableName + + " SET " + tableName + ".last_visit=? WHERE " + col.NAME + "=?;"; + try (PreparedStatement pst2 = con.prepareStatement(sql)) { + pst2.setLong(1, time); + pst2.setString(2, auth.getNickname()); + pst2.executeUpdate(); + } + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/mysqlextensions/MySqlExtension.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/mysqlextensions/MySqlExtension.java new file mode 100644 index 00000000..07374dfb --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/mysqlextensions/MySqlExtension.java @@ -0,0 +1,96 @@ +package fr.xephi.authme.datasource.mysqlextensions; + +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.datasource.Columns; +import fr.xephi.authme.security.crypts.HashedPassword; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.properties.DatabaseSettings; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.OptionalInt; + +/** + * Extension for the MySQL data source for forums. For certain password hashes (e.g. phpBB), we want + * to hook into the forum board and execute some actions specific to the forum software. + */ +public abstract class MySqlExtension { + + protected final Columns col; + protected final String tableName; + + MySqlExtension(Settings settings, Columns col) { + this.col = col; + this.tableName = settings.getProperty(DatabaseSettings.MYSQL_TABLE); + } + + /** + * Performs additional actions when a new player is saved. + * + * @param auth the player auth that has been saved + * @param con connection to the sql table + * @throws SQLException . + */ + public void saveAuth(PlayerAuth auth, Connection con) throws SQLException { + // extend for custom behavior + } + + /** + * Writes properties to the given PlayerAuth object that need to be retrieved in a specific manner + * when a PlayerAuth object is read from the table. + * + * @param auth the player auth object to extend + * @param id the database id of the player auth entry + * @param con connection to the sql table + * @throws SQLException . + */ + public void extendAuth(PlayerAuth auth, int id, Connection con) throws SQLException { + // extend for custom behavior + } + + /** + * Performs additional actions when a user's password is changed. + * + * @param user the name of the player (lowercase) + * @param password the new password to set + * @param con connection to the sql table + * @throws SQLException . + */ + public void changePassword(String user, HashedPassword password, Connection con) throws SQLException { + // extend for custom behavior + } + + /** + * Performs additional actions when a player is removed from the database. + * + * @param user the user to remove + * @param con connection to the sql table + * @throws SQLException . + */ + public void removeAuth(String user, Connection con) throws SQLException { + // extend for custom behavior + } + + /** + * Fetches the database ID of the given name from the database. + * + * @param name the name to get the ID for + * @param con connection to the sql table + * @return id of the playerAuth, or empty OptionalInt if the name is not registered + * @throws SQLException . + */ + protected OptionalInt retrieveIdFromTable(String name, Connection con) throws SQLException { + String sql = "SELECT " + col.ID + " FROM " + tableName + " WHERE " + col.NAME + "=?;"; + try (PreparedStatement pst = con.prepareStatement(sql)) { + pst.setString(1, name); + try (ResultSet rs = pst.executeQuery()) { + if (rs.next()) { + return OptionalInt.of(rs.getInt(col.ID)); + } + } + } + return OptionalInt.empty(); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/mysqlextensions/MySqlExtensionsFactory.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/mysqlextensions/MySqlExtensionsFactory.java new file mode 100644 index 00000000..dda94d78 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/mysqlextensions/MySqlExtensionsFactory.java @@ -0,0 +1,39 @@ +package fr.xephi.authme.datasource.mysqlextensions; + +import fr.xephi.authme.datasource.Columns; +import fr.xephi.authme.security.HashAlgorithm; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.properties.SecuritySettings; + +import javax.inject.Inject; + +/** + * Creates the appropriate {@link MySqlExtension}, depending on the configured password hashing algorithm. + */ +public class MySqlExtensionsFactory { + + @Inject + private Settings settings; + + /** + * Creates a new {@link MySqlExtension} object according to the configured hash algorithm. + * + * @param columnsConfig the columns configuration + * @return the extension the MySQL data source should use + */ + public MySqlExtension buildExtension(Columns columnsConfig) { + HashAlgorithm hash = settings.getProperty(SecuritySettings.PASSWORD_HASH); + switch (hash) { + case IPB4: + return new Ipb4Extension(settings, columnsConfig); + case PHPBB: + return new PhpBbExtension(settings, columnsConfig); + case WORDPRESS: + return new WordpressExtension(settings, columnsConfig); + case XFBCRYPT: + return new XfBcryptExtension(settings, columnsConfig); + default: + return new NoOpExtension(settings, columnsConfig); + } + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/mysqlextensions/NoOpExtension.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/mysqlextensions/NoOpExtension.java new file mode 100644 index 00000000..6d15f837 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/mysqlextensions/NoOpExtension.java @@ -0,0 +1,14 @@ +package fr.xephi.authme.datasource.mysqlextensions; + +import fr.xephi.authme.datasource.Columns; +import fr.xephi.authme.settings.Settings; + +/** + * Extension implementation that does not do anything. + */ +class NoOpExtension extends MySqlExtension { + + NoOpExtension(Settings settings, Columns col) { + super(settings, col); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/mysqlextensions/PhpBbExtension.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/mysqlextensions/PhpBbExtension.java new file mode 100644 index 00000000..d78aded1 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/mysqlextensions/PhpBbExtension.java @@ -0,0 +1,83 @@ +package fr.xephi.authme.datasource.mysqlextensions; + +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.datasource.Columns; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.properties.HooksSettings; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.OptionalInt; + +/** + * Extensions for phpBB when MySQL is used as data source. + */ +class PhpBbExtension extends MySqlExtension { + + private final String phpBbPrefix; + private final int phpBbGroup; + + PhpBbExtension(Settings settings, Columns col) { + super(settings, col); + this.phpBbPrefix = settings.getProperty(HooksSettings.PHPBB_TABLE_PREFIX); + this.phpBbGroup = settings.getProperty(HooksSettings.PHPBB_ACTIVATED_GROUP_ID); + } + + @Override + public void saveAuth(PlayerAuth auth, Connection con) throws SQLException { + OptionalInt authId = retrieveIdFromTable(auth.getNickname(), con); + if (authId.isPresent()) { + updateSpecificsOnSave(authId.getAsInt(), auth.getNickname(), con); + } + } + + private void updateSpecificsOnSave(int id, String name, Connection con) throws SQLException { + // Insert player in phpbb_user_group + String sql = "INSERT INTO " + phpBbPrefix + + "user_group (group_id, user_id, group_leader, user_pending) VALUES (?,?,?,?);"; + try (PreparedStatement pst = con.prepareStatement(sql)) { + pst.setInt(1, phpBbGroup); + pst.setInt(2, id); + pst.setInt(3, 0); + pst.setInt(4, 0); + pst.executeUpdate(); + } + // Update username_clean in phpbb_users + sql = "UPDATE " + tableName + " SET " + tableName + ".username_clean=? WHERE " + col.NAME + "=?;"; + try (PreparedStatement pst = con.prepareStatement(sql)) { + pst.setString(1, name); + pst.setString(2, name); + pst.executeUpdate(); + } + // Update player group in phpbb_users + sql = "UPDATE " + tableName + " SET " + tableName + ".group_id=? WHERE " + col.NAME + "=?;"; + try (PreparedStatement pst = con.prepareStatement(sql)) { + pst.setInt(1, phpBbGroup); + pst.setString(2, name); + pst.executeUpdate(); + } + // Get current time without ms + long time = System.currentTimeMillis() / 1000; + // Update user_regdate + sql = "UPDATE " + tableName + " SET " + tableName + ".user_regdate=? WHERE " + col.NAME + "=?;"; + try (PreparedStatement pst = con.prepareStatement(sql)) { + pst.setLong(1, time); + pst.setString(2, name); + pst.executeUpdate(); + } + // Update user_lastvisit + sql = "UPDATE " + tableName + " SET " + tableName + ".user_lastvisit=? WHERE " + col.NAME + "=?;"; + try (PreparedStatement pst = con.prepareStatement(sql)) { + pst.setLong(1, time); + pst.setString(2, name); + pst.executeUpdate(); + } + // Increment num_users + sql = "UPDATE " + phpBbPrefix + + "config SET config_value = config_value + 1 WHERE config_name = 'num_users';"; + try (PreparedStatement pst = con.prepareStatement(sql)) { + pst.executeUpdate(); + } + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/mysqlextensions/WordpressExtension.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/mysqlextensions/WordpressExtension.java new file mode 100644 index 00000000..8105d2e5 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/mysqlextensions/WordpressExtension.java @@ -0,0 +1,84 @@ +package fr.xephi.authme.datasource.mysqlextensions; + +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.datasource.Columns; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.properties.HooksSettings; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.OptionalInt; + +/** + * MySQL extensions for Wordpress. + */ +class WordpressExtension extends MySqlExtension { + + private final String wordpressPrefix; + + WordpressExtension(Settings settings, Columns col) { + super(settings, col); + this.wordpressPrefix = settings.getProperty(HooksSettings.WORDPRESS_TABLE_PREFIX); + } + + @Override + public void saveAuth(PlayerAuth auth, Connection con) throws SQLException { + OptionalInt authId = retrieveIdFromTable(auth.getNickname(), con); + if (authId.isPresent()) { + saveSpecifics(auth, authId.getAsInt(), con); + } + } + + /** + * Saves the required data to Wordpress tables. + * + * @param auth the player data + * @param id the player id + * @param con the sql connection + * @throws SQLException . + */ + private void saveSpecifics(PlayerAuth auth, int id, Connection con) throws SQLException { + String sql = "INSERT INTO " + wordpressPrefix + "usermeta (user_id, meta_key, meta_value) VALUES (?,?,?)"; + try (PreparedStatement pst = con.prepareStatement(sql)) { + + new UserMetaBatchAdder(pst, id) + .addMetaRow("first_name", "") + .addMetaRow("last_name", "") + .addMetaRow("nickname", auth.getNickname()) + .addMetaRow("description", "") + .addMetaRow("rich_editing", "true") + .addMetaRow("comment_shortcuts", "false") + .addMetaRow("admin_color", "fresh") + .addMetaRow("use_ssl", "0") + .addMetaRow("show_admin_bar_front", "true") + .addMetaRow(wordpressPrefix + "capabilities", "a:1:{s:10:\"subscriber\";b:1;}") + .addMetaRow(wordpressPrefix + "user_level", "0") + .addMetaRow("default_password_nag", ""); + + // Execute queries + pst.executeBatch(); + pst.clearBatch(); + } + } + + /** Helper to add batch entries to the wrapped prepared statement. */ + private static final class UserMetaBatchAdder { + + private final PreparedStatement pst; + private final int userId; + + UserMetaBatchAdder(PreparedStatement pst, int userId) { + this.pst = pst; + this.userId = userId; + } + + UserMetaBatchAdder addMetaRow(String metaKey, String metaValue) throws SQLException { + pst.setInt(1, userId); + pst.setString(2, metaKey); + pst.setString(3, metaValue); + pst.addBatch(); + return this; + } + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/mysqlextensions/XfBcryptExtension.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/mysqlextensions/XfBcryptExtension.java new file mode 100644 index 00000000..8d2a1ee9 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/datasource/mysqlextensions/XfBcryptExtension.java @@ -0,0 +1,148 @@ +package fr.xephi.authme.datasource.mysqlextensions; + +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.datasource.Columns; +import fr.xephi.authme.security.crypts.HashedPassword; +import fr.xephi.authme.security.crypts.XfBCrypt; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.properties.HooksSettings; + +import java.sql.Blob; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.OptionalInt; + +/** + * Extension for XFBCRYPT. + */ +class XfBcryptExtension extends MySqlExtension { + + private final String xfPrefix; + private final int xfGroup; + + XfBcryptExtension(Settings settings, Columns col) { + super(settings, col); + this.xfPrefix = settings.getProperty(HooksSettings.XF_TABLE_PREFIX); + this.xfGroup = settings.getProperty(HooksSettings.XF_ACTIVATED_GROUP_ID); + } + + @Override + public void saveAuth(PlayerAuth auth, Connection con) throws SQLException { + OptionalInt authId = retrieveIdFromTable(auth.getNickname(), con); + if (authId.isPresent()) { + updateXenforoTablesOnSave(auth, authId.getAsInt(), con); + } + } + + /** + * Updates the xenforo tables after a player auth has been saved. + * + * @param auth the player auth which was saved + * @param id the account id + * @param con connection to the database + */ + private void updateXenforoTablesOnSave(PlayerAuth auth, int id, Connection con) throws SQLException { + // Insert player password, salt in xf_user_authenticate + String sql = "INSERT INTO " + xfPrefix + "user_authenticate (user_id, scheme_class, data) VALUES (?,?,?)"; + try (PreparedStatement pst = con.prepareStatement(sql)) { + pst.setInt(1, id); + pst.setString(2, XfBCrypt.SCHEME_CLASS); + String serializedHash = XfBCrypt.serializeHash(auth.getPassword().getHash()); + byte[] bytes = serializedHash.getBytes(); + Blob blob = con.createBlob(); + blob.setBytes(1, bytes); + pst.setBlob(3, blob); + pst.executeUpdate(); + } + // Update player group in xf_users + sql = "UPDATE " + tableName + " SET " + tableName + ".user_group_id=? WHERE " + col.NAME + "=?;"; + try (PreparedStatement pst = con.prepareStatement(sql)) { + pst.setInt(1, xfGroup); + pst.setString(2, auth.getNickname()); + pst.executeUpdate(); + } + // Update player permission combination in xf_users + sql = "UPDATE " + tableName + " SET " + tableName + ".permission_combination_id=? WHERE " + col.NAME + "=?;"; + try (PreparedStatement pst = con.prepareStatement(sql)) { + pst.setInt(1, xfGroup); + pst.setString(2, auth.getNickname()); + pst.executeUpdate(); + } + // Insert player privacy combination in xf_user_privacy + sql = "INSERT INTO " + xfPrefix + "user_privacy (user_id, allow_view_profile, allow_post_profile, " + + "allow_send_personal_conversation, allow_view_identities, allow_receive_news_feed) VALUES (?,?,?,?,?,?)"; + try (PreparedStatement pst = con.prepareStatement(sql)) { + pst.setInt(1, id); + pst.setString(2, "everyone"); + pst.setString(3, "members"); + pst.setString(4, "members"); + pst.setString(5, "everyone"); + pst.setString(6, "everyone"); + pst.executeUpdate(); + } + // Insert player group relation in xf_user_group_relation + sql = "INSERT INTO " + xfPrefix + "user_group_relation (user_id, user_group_id, is_primary) VALUES (?,?,?)"; + try (PreparedStatement pst = con.prepareStatement(sql)) { + pst.setInt(1, id); + pst.setInt(2, xfGroup); + pst.setString(3, "1"); + pst.executeUpdate(); + } + } + + @Override + public void extendAuth(PlayerAuth auth, int id, Connection con) throws SQLException { + try (PreparedStatement pst = con.prepareStatement( + "SELECT data FROM " + xfPrefix + "user_authenticate WHERE " + col.ID + "=?;")) { + pst.setInt(1, id); + try (ResultSet rs = pst.executeQuery()) { + if (rs.next()) { + Blob blob = rs.getBlob("data"); + byte[] bytes = blob.getBytes(1, (int) blob.length()); + auth.setPassword(new HashedPassword(XfBCrypt.getHashFromBlob(bytes))); + } + } + } + } + + @Override + public void changePassword(String user, HashedPassword password, Connection con) throws SQLException { + OptionalInt authId = retrieveIdFromTable(user, con); + if (authId.isPresent()) { + final int id = authId.getAsInt(); + // Insert password in the correct table + String sql = "UPDATE " + xfPrefix + "user_authenticate SET data=? WHERE " + col.ID + "=?;"; + try (PreparedStatement pst = con.prepareStatement(sql)) { + String serializedHash = XfBCrypt.serializeHash(password.getHash()); + byte[] bytes = serializedHash.getBytes(); + Blob blob = con.createBlob(); + blob.setBytes(1, bytes); + pst.setBlob(1, blob); + pst.setInt(2, id); + pst.executeUpdate(); + } + + // ... + sql = "UPDATE " + xfPrefix + "user_authenticate SET scheme_class=? WHERE " + col.ID + "=?;"; + try (PreparedStatement pst = con.prepareStatement(sql)) { + pst.setString(1, XfBCrypt.SCHEME_CLASS); + pst.setInt(2, id); + pst.executeUpdate(); + } + } + } + + @Override + public void removeAuth(String user, Connection con) throws SQLException { + OptionalInt authId = retrieveIdFromTable(user, con); + if (authId.isPresent()) { + String sql = "DELETE FROM " + xfPrefix + "user_authenticate WHERE " + col.ID + "=?;"; + try (PreparedStatement xfDelete = con.prepareStatement(sql)) { + xfDelete.setInt(1, authId.getAsInt()); + xfDelete.executeUpdate(); + } + } + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/events/AbstractTeleportEvent.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/events/AbstractTeleportEvent.java new file mode 100644 index 00000000..51d366d9 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/events/AbstractTeleportEvent.java @@ -0,0 +1,78 @@ +package fr.xephi.authme.events; + +import org.bukkit.Location; +import org.bukkit.entity.Player; +import org.bukkit.event.Cancellable; + +/** + * Common supertype for all AuthMe teleport events. + */ +public abstract class AbstractTeleportEvent extends CustomEvent implements Cancellable { + + private final Player player; + private final Location from; + private Location to; + private boolean isCancelled; + + /** + * Constructor. + * + * @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) { + super(isAsync); + this.player = player; + this.from = player.getLocation(); + this.to = to; + } + + /** + * Return the player planned to be teleported. + * + * @return The player + */ + public Player getPlayer() { + return player; + } + + /** + * Return the location the player is being teleported away from. + * + * @return The location prior to the teleport + */ + public Location getFrom() { + return from; + } + + /** + * Set the destination of the teleport. + * + * @param to The location to teleport the player to + */ + public void setTo(Location to) { + this.to = to; + } + + /** + * Return the destination the player is being teleported to. + * + * @return The teleport destination + */ + public Location getTo() { + return to; + } + + @Override + public void setCancelled(boolean isCancelled) { + this.isCancelled = isCancelled; + } + + @Override + public boolean isCancelled() { + return isCancelled; + } + + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/events/AbstractUnregisterEvent.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/events/AbstractUnregisterEvent.java new file mode 100644 index 00000000..ac07b2ce --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/events/AbstractUnregisterEvent.java @@ -0,0 +1,33 @@ +package fr.xephi.authme.events; + +import org.bukkit.entity.Player; + +/** + * Event fired when a player has been unregistered. + */ +public abstract class AbstractUnregisterEvent extends CustomEvent { + + private final Player player; + + /** + * Constructor for a player that has unregistered himself. + * + * @param player the player + * @param isAsync if the event is called asynchronously + */ + public AbstractUnregisterEvent(Player player, boolean isAsync) { + super(isAsync); + this.player = player; + } + + /** + * Returns the player that has been unregistered. + *

+ * This may be {@code null}! Please refer to the implementations of this class for details. + * + * @return the unregistered player, or null + */ + public Player getPlayer() { + return player; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/events/AuthMeAsyncPreLoginEvent.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/events/AuthMeAsyncPreLoginEvent.java new file mode 100644 index 00000000..dd2e21c3 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/events/AuthMeAsyncPreLoginEvent.java @@ -0,0 +1,70 @@ +package fr.xephi.authme.events; + +import org.bukkit.entity.Player; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; + +/** + * 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 { + + private static final HandlerList handlers = new HandlerList(); + private final Player player; + private boolean canLogin = true; + + /** + * Constructor. + * + * @param player The player + * @param isAsync True if the event is async, false otherwise + */ + public AuthMeAsyncPreLoginEvent(Player player, boolean isAsync) { + super(isAsync); + this.player = player; + } + + /** + * Return the player concerned by this event. + * + * @return The player who executed a valid {@code /login} command + */ + public Player getPlayer() { + return player; + } + + /** + * Return whether the player is allowed to log in. + * + * @return True if the player can log in, false otherwise + */ + public boolean canLogin() { + return canLogin; + } + + /** + * Define whether or not the player may log in. + * + * @param canLogin True to allow the player to log in; false to prevent him + */ + public void setCanLogin(boolean canLogin) { + this.canLogin = canLogin; + } + + /** + * Return the list of handlers, equivalent to {@link #getHandlers()} and required by {@link Event}. + * + * @return The list of handlers + */ + public static HandlerList getHandlerList() { + return handlers; + } + + @Override + public HandlerList getHandlers() { + return handlers; + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/events/AuthMeAsyncPreRegisterEvent.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/events/AuthMeAsyncPreRegisterEvent.java new file mode 100644 index 00000000..af26ad51 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/events/AuthMeAsyncPreRegisterEvent.java @@ -0,0 +1,70 @@ +package fr.xephi.authme.events; + +import org.bukkit.entity.Player; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; + +/** + * This event is called when a player uses the register command, + * it's fired even when a user does a /register with invalid arguments. + * {@link #setCanRegister(boolean) event.setCanRegister(false)} prevents the player from registering. + */ +public class AuthMeAsyncPreRegisterEvent extends CustomEvent { + + private static final HandlerList handlers = new HandlerList(); + private final Player player; + private boolean canRegister = true; + + /** + * Constructor. + * + * @param player The player + * @param isAsync True if the event is async, false otherwise + */ + public AuthMeAsyncPreRegisterEvent(Player player, boolean isAsync) { + super(isAsync); + this.player = player; + } + + /** + * Return the player concerned by this event. + * + * @return The player who executed a valid {@code /login} command + */ + public Player getPlayer() { + return player; + } + + /** + * Return whether the player is allowed to register. + * + * @return True if the player can log in, false otherwise + */ + public boolean canRegister() { + return canRegister; + } + + /** + * Define whether or not the player may register. + * + * @param canRegister True to allow the player to log in; false to prevent him + */ + public void setCanRegister(boolean canRegister) { + this.canRegister = canRegister; + } + + /** + * Return the list of handlers, equivalent to {@link #getHandlers()} and required by {@link Event}. + * + * @return The list of handlers + */ + public static HandlerList getHandlerList() { + return handlers; + } + + @Override + public HandlerList getHandlers() { + return handlers; + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/events/AuthMeTeleportEvent.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/events/AuthMeTeleportEvent.java new file mode 100644 index 00000000..ed8a08d3 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/events/AuthMeTeleportEvent.java @@ -0,0 +1,39 @@ +package fr.xephi.authme.events; + +import org.bukkit.Location; +import org.bukkit.entity.Player; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; + +/** + * This event is fired before AuthMe teleports a player for general purposes. + */ +public class AuthMeTeleportEvent extends AbstractTeleportEvent { + + private static final HandlerList handlers = new HandlerList(); + + /** + * Constructor. + * + * @param player The player + * @param to The teleport destination + */ + public AuthMeTeleportEvent(Player player, Location to) { + super(false, player, to); + } + + /** + * Return the list of handlers, equivalent to {@link #getHandlers()} and required by {@link Event}. + * + * @return The list of handlers + */ + public static HandlerList getHandlerList() { + return handlers; + } + + @Override + public HandlerList getHandlers() { + return handlers; + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/events/CustomEvent.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/events/CustomEvent.java new file mode 100644 index 00000000..f647b6d5 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/events/CustomEvent.java @@ -0,0 +1,27 @@ +package fr.xephi.authme.events; + +import org.bukkit.event.Event; + +/** + * The parent of all AuthMe events. + */ +public abstract class CustomEvent extends Event { + + /** + * Constructor. + */ + public CustomEvent() { + super(false); + } + + /** + * Constructor, specifying whether the event is asynchronous or not. + * + * @param isAsync {@code true} to fire the event asynchronously, false otherwise + * @see Event#Event(boolean) + */ + public CustomEvent(boolean isAsync) { + super(isAsync); + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/events/EmailChangedEvent.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/events/EmailChangedEvent.java new file mode 100644 index 00000000..e489c202 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/events/EmailChangedEvent.java @@ -0,0 +1,85 @@ +package fr.xephi.authme.events; + +import org.bukkit.entity.Player; +import org.bukkit.event.Cancellable; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; + +/** + * This event is called when a player adds or changes his email address. + */ +public class EmailChangedEvent extends CustomEvent implements Cancellable { + private static final HandlerList handlers = new HandlerList(); + private final Player player; + private final String oldEmail; + private final String newEmail; + private boolean isCancelled; + + /** + * Constructor + * + * @param player The player that changed email + * @param oldEmail Old email player had on file. Can be null when user adds an email + * @param newEmail New email that player tries to set. In case of adding email, this will contain + * the email is trying to set. + * @param isAsync should this event be called asynchronously? + */ + public EmailChangedEvent(Player player, String oldEmail, String newEmail, boolean isAsync) { + super(isAsync); + this.player = player; + this.oldEmail = oldEmail; + this.newEmail = newEmail; + } + + @Override + public boolean isCancelled() { + return isCancelled; + } + + /** + * Gets the player who changes the email + * + * @return The player who changed the email + */ + public Player getPlayer() { + return player; + } + + /** + * Gets the old email in case user tries to change existing email. + * + * @return old email stored on file. Can be null when user never had an email and adds a new one. + */ + public String getOldEmail() { + return this.oldEmail; + } + + /** + * Gets the new email. + * + * @return the email user is trying to set. If user adds email and never had one before, + * this is where such email can be found. + */ + public String getNewEmail() { + return this.newEmail; + } + + @Override + public void setCancelled(boolean cancelled) { + this.isCancelled = cancelled; + } + + @Override + public HandlerList getHandlers() { + return handlers; + } + + /** + * Return the list of handlers, equivalent to {@link #getHandlers()} and required by {@link Event}. + * + * @return The list of handlers + */ + public static HandlerList getHandlerList() { + return handlers; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/events/FailedLoginEvent.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/events/FailedLoginEvent.java new file mode 100644 index 00000000..c80ce65b --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/events/FailedLoginEvent.java @@ -0,0 +1,46 @@ +package fr.xephi.authme.events; + +import org.bukkit.entity.Player; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; + +/** + * Event fired when a player enters a wrong password. + */ +public class FailedLoginEvent extends CustomEvent { + + private static final HandlerList handlers = new HandlerList(); + private final Player player; + + /** + * Constructor. + * + * @param player The player + * @param isAsync if the event is called asynchronously + */ + public FailedLoginEvent(Player player, boolean isAsync) { + super(isAsync); + this.player = player; + } + + /** + * @return The player entering a wrong password + */ + public Player getPlayer() { + return player; + } + + /** + * Return the list of handlers, equivalent to {@link #getHandlers()} and required by {@link Event}. + * + * @return The list of handlers + */ + public static HandlerList getHandlerList() { + return handlers; + } + + @Override + public HandlerList getHandlers() { + return handlers; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/events/FirstSpawnTeleportEvent.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/events/FirstSpawnTeleportEvent.java new file mode 100644 index 00000000..e5e6868d --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/events/FirstSpawnTeleportEvent.java @@ -0,0 +1,40 @@ +package fr.xephi.authme.events; + +import org.bukkit.Location; +import org.bukkit.entity.Player; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; + +/** + * Event that is called if a player is teleported to the AuthMe first spawn, i.e. to the + * spawn location for players who have never played before. + */ +public class FirstSpawnTeleportEvent extends AbstractTeleportEvent { + + private static final HandlerList handlers = new HandlerList(); + + /** + * Constructor. + * + * @param player The player + * @param to The teleport destination + */ + public FirstSpawnTeleportEvent(Player player, Location to) { + super(false, player, to); + } + + /** + * Return the list of handlers, equivalent to {@link #getHandlers()} and required by {@link Event}. + * + * @return The list of handlers + */ + public static HandlerList getHandlerList() { + return handlers; + } + + @Override + public HandlerList getHandlers() { + return handlers; + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/events/LoginEvent.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/events/LoginEvent.java new file mode 100644 index 00000000..eb38d626 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/events/LoginEvent.java @@ -0,0 +1,58 @@ +package fr.xephi.authme.events; + +import org.bukkit.entity.Player; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; + +/** + * Event fired when a player has successfully logged in or registered. + */ +public class LoginEvent extends CustomEvent { + + private static final HandlerList handlers = new HandlerList(); + private final Player player; + + /** + * Constructor. + * + * @param player The player + */ + public LoginEvent(Player player) { + this.player = player; + } + + /** + * Return the player that has successfully logged in or registered. + * + * @return The player + */ + public Player getPlayer() { + return player; + } + + /** + * Ensures compatibility with plugins like GuiRules. + * + * @return true + * @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}. + * + * @return The list of handlers + */ + public static HandlerList getHandlerList() { + return handlers; + } + + @Override + public HandlerList getHandlers() { + return handlers; + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/events/LogoutEvent.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/events/LogoutEvent.java new file mode 100644 index 00000000..70fae096 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/events/LogoutEvent.java @@ -0,0 +1,49 @@ +package fr.xephi.authme.events; + +import org.bukkit.entity.Player; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; + +/** + * This event is called when a player logs out through AuthMe, i.e. only when the player + * has executed the {@code /logout} command. This event is not fired if a player simply + * leaves the server. + */ +public class LogoutEvent extends CustomEvent { + + private static final HandlerList handlers = new HandlerList(); + private final Player player; + + /** + * Constructor. + * + * @param player The player + */ + public LogoutEvent(Player player) { + this.player = player; + } + + /** + * Return the player who logged out. + * + * @return The player + */ + public Player getPlayer() { + return this.player; + } + + /** + * Return the list of handlers, equivalent to {@link #getHandlers()} and required by {@link Event}. + * + * @return The list of handlers + */ + public static HandlerList getHandlerList() { + return handlers; + } + + @Override + public HandlerList getHandlers() { + return handlers; + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/events/PasswordEncryptionEvent.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/events/PasswordEncryptionEvent.java new file mode 100644 index 00000000..2ad87226 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/events/PasswordEncryptionEvent.java @@ -0,0 +1,57 @@ +package fr.xephi.authme.events; + +import fr.xephi.authme.security.crypts.EncryptionMethod; +import org.bukkit.event.HandlerList; + +/** + * This event is called when we need to compare or hash a password for a player and allows + * third-party listeners to change the encryption method. This is typically + * done with the {@link fr.xephi.authme.security.HashAlgorithm#CUSTOM} setting. + */ +public class PasswordEncryptionEvent extends CustomEvent { + + private static final HandlerList handlers = new HandlerList(); + private EncryptionMethod method; + + /** + * Constructor. + * + * @param method The method used to encrypt the password + */ + public PasswordEncryptionEvent(EncryptionMethod method) { + super(false); + this.method = method; + } + + /** + * Return the list of handlers, equivalent to {@link #getHandlers()} and required by {@link org.bukkit.event.Event}. + * + * @return The list of handlers + */ + public static HandlerList getHandlerList() { + return handlers; + } + + @Override + public HandlerList getHandlers() { + return handlers; + } + + /** + * Return the encryption method used to hash the password. + * + * @return The encryption method + */ + public EncryptionMethod getMethod() { + return method; + } + + /** + * Set the encryption method to hash the password with. + * + * @param method The encryption method to use + */ + public void setMethod(EncryptionMethod method) { + this.method = method; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/events/ProtectInventoryEvent.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/events/ProtectInventoryEvent.java new file mode 100644 index 00000000..f2fba487 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/events/ProtectInventoryEvent.java @@ -0,0 +1,85 @@ +package fr.xephi.authme.events; + +import org.bukkit.entity.Player; +import org.bukkit.event.Cancellable; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; +import org.bukkit.inventory.ItemStack; + +/** + * This event is called before the inventory data of a player is suppressed, + * i.e. the inventory of the player is not displayed until he has authenticated. + */ +public class ProtectInventoryEvent extends CustomEvent implements Cancellable { + + private static final HandlerList handlers = new HandlerList(); + private final ItemStack[] storedInventory; + private final ItemStack[] storedArmor; + private final Player player; + private boolean isCancelled; + + /** + * Constructor. + * + * @param player The player + * @param isAsync True if the event is async, false otherwise + */ + public ProtectInventoryEvent(Player player, boolean isAsync) { + super(isAsync); + this.player = player; + this.storedInventory = player.getInventory().getContents(); + this.storedArmor = player.getInventory().getArmorContents(); + } + + /** + * Return the inventory of the player. + * + * @return The player's inventory + */ + public ItemStack[] getStoredInventory() { + return storedInventory; + } + + /** + * Return the armor of the player. + * + * @return The player's armor + */ + public ItemStack[] getStoredArmor() { + return storedArmor; + } + + /** + * Return the player whose inventory will be hidden. + * + * @return The player associated with this event + */ + public Player getPlayer() { + return player; + } + + @Override + public void setCancelled(boolean isCancelled) { + this.isCancelled = isCancelled; + } + + @Override + public boolean isCancelled() { + return isCancelled; + } + + /** + * Return the list of handlers, equivalent to {@link #getHandlers()} and required by {@link Event}. + * + * @return The list of handlers + */ + public static HandlerList getHandlerList() { + return handlers; + } + + @Override + public HandlerList getHandlers() { + return handlers; + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/events/RegisterEvent.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/events/RegisterEvent.java new file mode 100644 index 00000000..2a98d054 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/events/RegisterEvent.java @@ -0,0 +1,47 @@ +package fr.xephi.authme.events; + +import org.bukkit.entity.Player; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; + +/** + * Event fired when a player has successfully registered. + */ +public class RegisterEvent extends CustomEvent { + + private static final HandlerList handlers = new HandlerList(); + private final Player player; + + /** + * Constructor. + * + * @param player The player + */ + public RegisterEvent(Player player) { + this.player = player; + } + + /** + * Return the player that has successfully logged in or registered. + * + * @return The player + */ + public Player getPlayer() { + return player; + } + + /** + * Return the list of handlers, equivalent to {@link #getHandlers()} and required by {@link Event}. + * + * @return The list of handlers + */ + public static HandlerList getHandlerList() { + return handlers; + } + + @Override + public HandlerList getHandlers() { + return handlers; + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/events/RestoreInventoryEvent.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/events/RestoreInventoryEvent.java new file mode 100644 index 00000000..eb9bf71d --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/events/RestoreInventoryEvent.java @@ -0,0 +1,60 @@ +package fr.xephi.authme.events; + +import org.bukkit.entity.Player; +import org.bukkit.event.Cancellable; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; + +/** + * This event is fired when the inventory of a player is restored + * (the inventory data is no longer hidden from the user). + */ +public class RestoreInventoryEvent extends CustomEvent implements Cancellable { + + private static final HandlerList handlers = new HandlerList(); + private final Player player; + private boolean isCancelled; + + /** + * Constructor. + * + * @param player The player + */ + public RestoreInventoryEvent(Player player) { + this.player = player; + } + + /** + * Return the player whose inventory will be restored. + * + * @return Player + */ + public Player getPlayer() { + return player; + } + + @Override + public boolean isCancelled() { + return isCancelled; + } + + @Override + public void setCancelled(boolean isCancelled) { + this.isCancelled = isCancelled; + } + + /** + * Return the list of handlers, equivalent to {@link #getHandlers()} and required by {@link Event}. + * + * @return The list of handlers + */ + public static HandlerList getHandlerList() { + return handlers; + } + + @Override + public HandlerList getHandlers() { + return handlers; + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/events/RestoreSessionEvent.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/events/RestoreSessionEvent.java new file mode 100644 index 00000000..abfffaa9 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/events/RestoreSessionEvent.java @@ -0,0 +1,51 @@ +package fr.xephi.authme.events; + +import org.bukkit.entity.Player; +import org.bukkit.event.Cancellable; +import org.bukkit.event.HandlerList; + +/** + * Event fired before a session is restored. + */ +public class RestoreSessionEvent extends CustomEvent implements Cancellable { + + private static final HandlerList handlers = new HandlerList(); + private final Player player; + private boolean isCancelled; + + public RestoreSessionEvent(Player player, boolean isAsync) { + super(isAsync); + this.player = player; + } + + @Override + public boolean isCancelled() { + return isCancelled; + } + + @Override + public void setCancelled(boolean isCancelled) { + this.isCancelled = isCancelled; + } + + /** + * @return the player for which the session will be enabled + */ + public Player getPlayer() { + return player; + } + + /** + * Return the list of handlers, equivalent to {@link #getHandlers()} and required by {@link org.bukkit.event.Event}. + * + * @return The list of handlers + */ + public static HandlerList getHandlerList() { + return handlers; + } + + @Override + public HandlerList getHandlers() { + return handlers; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/events/SpawnTeleportEvent.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/events/SpawnTeleportEvent.java new file mode 100644 index 00000000..a1a85d84 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/events/SpawnTeleportEvent.java @@ -0,0 +1,51 @@ +package fr.xephi.authme.events; + +import org.bukkit.Location; +import org.bukkit.entity.Player; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; + +/** + * Called if a player is teleported to a specific spawn upon joining or logging in. + */ +public class SpawnTeleportEvent extends AbstractTeleportEvent { + + private static final HandlerList handlers = new HandlerList(); + private final boolean isAuthenticated; + + /** + * Constructor. + * + * @param player The player + * @param to The teleport destination + * @param isAuthenticated Whether or not the player is logged in + */ + public SpawnTeleportEvent(Player player, Location to, boolean isAuthenticated) { + super(false, player, to); + this.isAuthenticated = isAuthenticated; + } + + /** + * Return whether or not the player is authenticated. + * + * @return true if the player is logged in, false otherwise + */ + public boolean isAuthenticated() { + return isAuthenticated; + } + + /** + * Return the list of handlers, equivalent to {@link #getHandlers()} and required by {@link Event}. + * + * @return The list of handlers + */ + public static HandlerList getHandlerList() { + return handlers; + } + + @Override + public HandlerList getHandlers() { + return handlers; + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/events/UnregisterByAdminEvent.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/events/UnregisterByAdminEvent.java new file mode 100644 index 00000000..f12e5116 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/events/UnregisterByAdminEvent.java @@ -0,0 +1,67 @@ +package fr.xephi.authme.events; + +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.bukkit.event.HandlerList; + +/** + * Event fired after a player has been unregistered from an external source (by an admin or via the API). + *

+ * Note that only the {@code playerName} is guaranteed to be not {@code null} in any case. + *

+ * The {@code player} may be null if a name is supplied which has never been online on the server – + * due to migrations, data removal, etc. it is possible that a user exists in the database for which the + * server knows no {@link Player} object. + *

+ * If a player is unregistered via an API call, the {@code initiator} is null as the action has not been + * started by any {@link CommandSender}. Otherwise, the {@code initiator} is the user who performed the + * command to unregister the player name. + */ +public class UnregisterByAdminEvent extends AbstractUnregisterEvent { + + private static final HandlerList handlers = new HandlerList(); + private final String playerName; + private final CommandSender initiator; + + /** + * Constructor. + * + * @param player the player (may be null - see class JavaDoc) + * @param playerName the name of the player that was unregistered + * @param isAsync whether or not the event is async + * @param initiator the initiator of the unregister process (may be null - see class JavaDoc) + */ + public UnregisterByAdminEvent(Player player, String playerName, boolean isAsync, CommandSender initiator) { + super(player, isAsync); + this.playerName = playerName; + this.initiator = initiator; + } + + /** + * @return the name of the player that was unregistered + */ + public String getPlayerName() { + return playerName; + } + + /** + * @return the user who requested to unregister the name, or null if not applicable + */ + public CommandSender getInitiator() { + return initiator; + } + + /** + * Return the list of handlers, equivalent to {@link #getHandlers()} and required by {@link org.bukkit.event.Event}. + * + * @return The list of handlers + */ + public static HandlerList getHandlerList() { + return handlers; + } + + @Override + public HandlerList getHandlers() { + return handlers; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/events/UnregisterByPlayerEvent.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/events/UnregisterByPlayerEvent.java new file mode 100644 index 00000000..cd96cd91 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/events/UnregisterByPlayerEvent.java @@ -0,0 +1,36 @@ +package fr.xephi.authme.events; + +import org.bukkit.entity.Player; +import org.bukkit.event.HandlerList; + +/** + * Event fired after a player has unregistered himself. + */ +public class UnregisterByPlayerEvent extends AbstractUnregisterEvent { + + private static final HandlerList handlers = new HandlerList(); + + /** + * Constructor. + * + * @param player the player (never null) + * @param isAsync if the event is called asynchronously + */ + public UnregisterByPlayerEvent(Player player, boolean isAsync) { + super(player, isAsync); + } + + /** + * Return the list of handlers, equivalent to {@link #getHandlers()} and required by {@link org.bukkit.event.Event}. + * + * @return The list of handlers + */ + public static HandlerList getHandlerList() { + return handlers; + } + + @Override + public HandlerList getHandlers() { + return handlers; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/initialization/DataFolder.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/initialization/DataFolder.java new file mode 100644 index 00000000..0288f45a --- /dev/null +++ b/plugin/platform-bukkit/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, ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface DataFolder { +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/initialization/DataSourceProvider.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/initialization/DataSourceProvider.java new file mode 100644 index 00000000..41e48f3f --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/initialization/DataSourceProvider.java @@ -0,0 +1,115 @@ +package fr.xephi.authme.initialization; + +import com.alessiodp.libby.Library; +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.data.auth.PlayerCache; +import fr.xephi.authme.datasource.CacheDataSource; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.datasource.DataSourceType; +import fr.xephi.authme.datasource.H2; +import fr.xephi.authme.datasource.MariaDB; +import fr.xephi.authme.datasource.MySQL; +import fr.xephi.authme.datasource.PostgreSqlDataSource; +import fr.xephi.authme.datasource.SQLite; +import fr.xephi.authme.datasource.mysqlextensions.MySqlExtensionsFactory; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.service.BukkitService; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.properties.DatabaseSettings; + +import javax.inject.Inject; +import javax.inject.Provider; +import java.io.File; +import java.sql.SQLException; + +import static fr.xephi.authme.AuthMe.libraryManager; + +/** + * Creates the AuthMe data source. + */ +public class DataSourceProvider implements Provider { + + private static final int SQLITE_MAX_SIZE = 4000; + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(DataSourceProvider.class); + + @Inject + @DataFolder + private File dataFolder; + @Inject + private Settings settings; + @Inject + private BukkitService bukkitService; + @Inject + private PlayerCache playerCache; + @Inject + private MySqlExtensionsFactory mySqlExtensionsFactory; + + DataSourceProvider() { + } + + @Override + public DataSource get() { + try { + return createDataSource(); + } catch (Exception e) { + logger.logException("Could not create data source:", e); + throw new IllegalStateException("Error during initialization of data source", e); + } + } + + /** + * Sets up the data source. + * + * @return the constructed data source + * @throws SQLException when initialization of a SQL data source failed + */ + private DataSource createDataSource() throws SQLException { + DataSourceType dataSourceType = settings.getProperty(DatabaseSettings.BACKEND); + DataSource dataSource; + switch (dataSourceType) { + case MYSQL: + dataSource = new MySQL(settings, mySqlExtensionsFactory); + break; + case MARIADB: + dataSource = new MariaDB(settings, mySqlExtensionsFactory); + break; + case POSTGRESQL: + dataSource = new PostgreSqlDataSource(settings, mySqlExtensionsFactory); + break; + case SQLITE: + dataSource = new SQLite(settings, dataFolder); + break; + case H2: + Library h2 = Library.builder() + .groupId("com.h2database") + .artifactId("h2") + .version("2.2.224") + .build(); + libraryManager.addMavenCentral(); + libraryManager.loadLibrary(h2); + dataSource = new H2(settings, dataFolder); + break; + default: + throw new UnsupportedOperationException("Unknown data source type '" + dataSourceType + "'"); + } + + if (settings.getProperty(DatabaseSettings.USE_CACHING)) { + dataSource = new CacheDataSource(dataSource, playerCache); + } + if (DataSourceType.SQLITE.equals(dataSourceType)) { + checkDataSourceSize(dataSource); + } + return dataSource; + } + + private void checkDataSourceSize(DataSource dataSource) { + bukkitService.runTaskAsynchronously(() -> { + int accounts = dataSource.getAccountsRegistered(); + if (accounts >= SQLITE_MAX_SIZE) { + logger.warning("YOU'RE USING THE SQLITE DATABASE WITH " + + accounts + "+ ACCOUNTS; FOR BETTER PERFORMANCE, PLEASE UPGRADE TO MYSQL!!"); + } + }); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/initialization/HasCleanup.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/initialization/HasCleanup.java new file mode 100644 index 00000000..351df4f6 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/initialization/HasCleanup.java @@ -0,0 +1,16 @@ +package fr.xephi.authme.initialization; + +/** + * Common interface for types which have data that becomes outdated + * and that can be cleaned up periodically. + * + * @see fr.xephi.authme.task.CleanupTask + */ +public interface HasCleanup { + + /** + * Performs the cleanup action. + */ + void performCleanup(); + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/initialization/OnShutdownPlayerSaver.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/initialization/OnShutdownPlayerSaver.java new file mode 100644 index 00000000..261e7dd5 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/initialization/OnShutdownPlayerSaver.java @@ -0,0 +1,75 @@ +package fr.xephi.authme.initialization; + +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.data.auth.PlayerCache; +import fr.xephi.authme.data.limbo.LimboService; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.service.BukkitService; +import fr.xephi.authme.service.ValidationService; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.SpawnLoader; +import fr.xephi.authme.settings.properties.RestrictionSettings; +import fr.xephi.authme.util.PlayerUtils; +import org.bukkit.Location; +import org.bukkit.entity.Player; + +import javax.inject.Inject; +import java.util.Locale; + +/** + * Saves all players' data when the plugin shuts down. + */ +public class OnShutdownPlayerSaver { + + @Inject + private BukkitService bukkitService; + @Inject + private Settings settings; + @Inject + private ValidationService validationService; + @Inject + private DataSource dataSource; + @Inject + private SpawnLoader spawnLoader; + @Inject + private PlayerCache playerCache; + @Inject + private LimboService limboService; + + OnShutdownPlayerSaver() { + } + + /** + * Saves the data of all online players. + */ + public void saveAllPlayers() { + for (Player player : bukkitService.getOnlinePlayers()) { + savePlayer(player); + } + } + + private void savePlayer(Player player) { + String name = player.getName().toLowerCase(Locale.ROOT); + if (PlayerUtils.isNpc(player) || validationService.isUnrestricted(name)) { + return; + } + if (limboService.hasLimboPlayer(name)) { + limboService.restoreData(player); + } else { + saveLoggedinPlayer(player); + } + playerCache.removePlayer(name); + } + + private void saveLoggedinPlayer(Player player) { + if (settings.getProperty(RestrictionSettings.SAVE_QUIT_LOCATION)) { + Location loc = spawnLoader.getPlayerLocationOrSpawn(player); + PlayerAuth auth = PlayerAuth.builder() + .name(player.getName().toLowerCase(Locale.ROOT)) + .realName(player.getName()) + .location(loc).build(); + dataSource.updateQuitLoc(auth); + // TODO: send an update when a messaging service will be implemented (QUITLOC) + } + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/initialization/OnStartupTasks.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/initialization/OnStartupTasks.java new file mode 100644 index 00000000..705f3bcc --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/initialization/OnStartupTasks.java @@ -0,0 +1,114 @@ +package fr.xephi.authme.initialization; + +import com.github.Anon8281.universalScheduler.UniversalRunnable; +import fr.xephi.authme.AuthMe; +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.message.Messages; +import fr.xephi.authme.output.ConsoleFilter; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.output.Log4JFilter; +import fr.xephi.authme.service.BukkitService; +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.PluginSettings; +import org.apache.logging.log4j.LogManager; +import org.bstats.bukkit.Metrics; +import org.bstats.charts.SimplePie; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import javax.inject.Inject; +import java.util.List; +import java.util.logging.Logger; + +import static fr.xephi.authme.service.BukkitService.TICKS_PER_MINUTE; +import static fr.xephi.authme.settings.properties.EmailSettings.RECALL_PLAYERS; + +/** + * Contains actions such as migrations that should be performed on startup. + */ +public class OnStartupTasks { + + private static ConsoleLogger consoleLogger = ConsoleLoggerFactory.get(OnStartupTasks.class); + + @Inject + private DataSource dataSource; + @Inject + private Settings settings; + @Inject + private BukkitService bukkitService; + @Inject + private Messages messages; + + OnStartupTasks() { + } + + /** + * Sends bstats metrics. + * + * @param plugin the plugin instance + * @param settings the settings + */ + public static void sendMetrics(AuthMe plugin, Settings settings) { + final Metrics metrics = new Metrics(plugin, 18479); + + metrics.addCustomChart(new SimplePie("messages_language", + () -> settings.getProperty(PluginSettings.MESSAGES_LANGUAGE))); + metrics.addCustomChart(new SimplePie("database_backend", + () -> settings.getProperty(DatabaseSettings.BACKEND).toString())); + } + + /** + * Sets up the console filter if enabled. + * + * @param logger the plugin logger + */ + public static void setupConsoleFilter(Logger logger) { + // Try to set the log4j filter + try { + Class.forName("org.apache.logging.log4j.core.filter.AbstractFilter"); + setLog4JFilter(); + } catch (ClassNotFoundException | NoClassDefFoundError e) { + // log4j is not available + consoleLogger.info("You're using Minecraft 1.6.x or older, Log4J support will be disabled"); + ConsoleFilter filter = new ConsoleFilter(); + logger.setFilter(filter); + Bukkit.getLogger().setFilter(filter); + Logger.getLogger("Minecraft").setFilter(filter); + } + } + + // Set the console filter to remove the passwords + private static void setLog4JFilter() { + org.apache.logging.log4j.core.Logger logger; + logger = (org.apache.logging.log4j.core.Logger) LogManager.getRootLogger(); + logger.addFilter(new Log4JFilter()); + } + + /** + * Starts a task that regularly reminds players without a defined email to set their email, + * if enabled. + */ + public void scheduleRecallEmailTask() { + if (!settings.getProperty(RECALL_PLAYERS)) { + return; + } + bukkitService.runTaskTimerAsynchronously(new UniversalRunnable() { + @Override + public void run() { + List loggedPlayersWithEmptyMail = dataSource.getLoggedPlayersWithEmptyMail(); + bukkitService.runTask(() -> { + for (String playerWithoutMail : loggedPlayersWithEmptyMail) { + Player player = bukkitService.getPlayerExact(playerWithoutMail); + if (player != null) { + messages.send(player, MessageKey.ADD_EMAIL_MESSAGE); + } + } + }); + } + }, 1, (long) TICKS_PER_MINUTE * settings.getProperty(EmailSettings.DELAY_RECALL)); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/initialization/Reloadable.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/initialization/Reloadable.java new file mode 100644 index 00000000..65c3a337 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/initialization/Reloadable.java @@ -0,0 +1,15 @@ +package fr.xephi.authme.initialization; + +/** + * Interface for reloadable entities. + * + * @see fr.xephi.authme.command.executable.authme.ReloadCommand + */ +public interface Reloadable { + + /** + * Performs the reload action. + */ + void reload(); + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/initialization/SettingsDependent.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/initialization/SettingsDependent.java new file mode 100644 index 00000000..89d18318 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/initialization/SettingsDependent.java @@ -0,0 +1,18 @@ +package fr.xephi.authme.initialization; + +import fr.xephi.authme.settings.Settings; + +/** + * Interface for classes that keep a local copy of certain settings. + * + * @see fr.xephi.authme.command.executable.authme.ReloadCommand + */ +public interface SettingsDependent { + + /** + * Performs a reload with the provided settings instance. + * + * @param settings the settings instance + */ + void reload(Settings settings); +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/initialization/SettingsProvider.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/initialization/SettingsProvider.java new file mode 100644 index 00000000..77133715 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/initialization/SettingsProvider.java @@ -0,0 +1,45 @@ +package fr.xephi.authme.initialization; + +import ch.jalu.configme.configurationdata.ConfigurationData; +import ch.jalu.configme.resource.PropertyResource; +import fr.xephi.authme.service.yaml.YamlFileResourceProvider; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.SettingsMigrationService; +import fr.xephi.authme.settings.properties.AuthMeSettingsRetriever; +import fr.xephi.authme.util.FileUtils; + +import javax.inject.Inject; +import javax.inject.Provider; +import java.io.File; + +/** + * Initializes the settings. + */ +public class SettingsProvider implements Provider { + + @Inject + @DataFolder + private File dataFolder; + @Inject + private SettingsMigrationService migrationService; + + SettingsProvider() { + } + + /** + * Loads the plugin's settings. + * + * @return the settings instance, or null if it could not be constructed + */ + @Override + public Settings get() { + File configFile = new File(dataFolder, "config.yml"); + if (!configFile.exists()) { + FileUtils.create(configFile); + } + PropertyResource resource = YamlFileResourceProvider.loadFromFile(configFile); + ConfigurationData configurationData = AuthMeSettingsRetriever.buildConfigurationData(); + return new Settings(dataFolder, resource, migrationService, configurationData); + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/initialization/TaskCloser.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/initialization/TaskCloser.java new file mode 100644 index 00000000..4cd6daa9 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/initialization/TaskCloser.java @@ -0,0 +1,33 @@ +package fr.xephi.authme.initialization; + +import com.github.Anon8281.universalScheduler.scheduling.schedulers.TaskScheduler; +import fr.xephi.authme.AuthMe; +import fr.xephi.authme.datasource.DataSource; + +/** + * Waits for asynchronous tasks to complete before closing the data source + * so the plugin can shut down properly. + */ +public class TaskCloser implements Runnable { + + private final TaskScheduler scheduler; + private final DataSource dataSource; + + /** + * Constructor. + * + * @param dataSource the data source (nullable) + */ + public TaskCloser(DataSource dataSource) { + this.scheduler = AuthMe.getScheduler(); + this.dataSource = dataSource; + } + + @Override + public void run() { + scheduler.cancelTasks(); + if (dataSource != null) { + dataSource.closeConnection(); + } + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/listener/AdvancedShulkerFixListener.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/listener/AdvancedShulkerFixListener.java new file mode 100644 index 00000000..64045591 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/listener/AdvancedShulkerFixListener.java @@ -0,0 +1,20 @@ +package fr.xephi.authme.listener; + +import org.bukkit.block.Block; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.block.BlockDispenseEvent; + +//This fix is only for Minecraft 1.13- +public class AdvancedShulkerFixListener implements Listener { + + @EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true) + public void onDispenserActivate(BlockDispenseEvent event) { + Block block = event.getBlock(); + + if (block.getY() <= 0 || block.getY() >= block.getWorld().getMaxHeight() - 1) { + event.setCancelled(true); + } + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/listener/BedrockAutoLoginListener.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/listener/BedrockAutoLoginListener.java new file mode 100644 index 00000000..4f63e610 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/listener/BedrockAutoLoginListener.java @@ -0,0 +1,54 @@ +package fr.xephi.authme.listener; +/* Inspired by DongShaoNB/BedrockPlayerSupport **/ + +import fr.xephi.authme.AuthMe; +import fr.xephi.authme.api.v3.AuthMeApi; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.message.Messages; +import fr.xephi.authme.service.BukkitService; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.properties.HooksSettings; +import fr.xephi.authme.settings.properties.SecuritySettings; +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.PlayerJoinEvent; + +import javax.inject.Inject; +import java.util.UUID; + +import static org.bukkit.Bukkit.getServer; + +public class BedrockAutoLoginListener implements Listener { + private final AuthMeApi authmeApi = AuthMeApi.getInstance(); + @Inject + private BukkitService bukkitService; + @Inject + private AuthMe plugin; + @Inject + private Messages messages; + + @Inject + private Settings settings; + + public BedrockAutoLoginListener() { + } + + private boolean isBedrockPlayer(UUID uuid) { + return settings.getProperty(HooksSettings.HOOK_FLOODGATE_PLAYER) && settings.getProperty(SecuritySettings.FORCE_LOGIN_BEDROCK) && org.geysermc.floodgate.api.FloodgateApi.getInstance().isFloodgateId(uuid) && getServer().getPluginManager().getPlugin("floodgate") != null; + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onPlayerJoin(PlayerJoinEvent event) { + Player player = event.getPlayer(); + String name = event.getPlayer().getName(); + UUID uuid = event.getPlayer().getUniqueId(); + bukkitService.runTaskLater(player, () -> { + if (isBedrockPlayer(uuid) && !authmeApi.isAuthenticated(player) && authmeApi.isRegistered(name)) { + authmeApi.forceLogin(player, true); + messages.send(player, MessageKey.BEDROCK_AUTO_LOGGED_IN); + } + },20L); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/listener/BlockListener.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/listener/BlockListener.java new file mode 100644 index 00000000..e40bf8d0 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/listener/BlockListener.java @@ -0,0 +1,28 @@ +package fr.xephi.authme.listener; + +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.event.block.BlockPlaceEvent; + +import javax.inject.Inject; + +public class BlockListener implements Listener { + + @Inject + private ListenerService listenerService; + + @EventHandler(ignoreCancelled = true) + public void onBlockPlace(BlockPlaceEvent event) { + if (listenerService.shouldCancelEvent(event.getPlayer())) { + event.setCancelled(true); + } + } + + @EventHandler(ignoreCancelled = true) + public void onBlockBreak(BlockBreakEvent event) { + if (listenerService.shouldCancelEvent(event.getPlayer())) { + event.setCancelled(true); + } + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/listener/DoubleLoginFixListener.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/listener/DoubleLoginFixListener.java new file mode 100644 index 00000000..2c0370a4 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/listener/DoubleLoginFixListener.java @@ -0,0 +1,35 @@ +package fr.xephi.authme.listener; +//Prevent Ghost Players + +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.service.CommonService; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; + +import javax.inject.Inject; +import java.util.Collection; +import java.util.HashSet; + +public class DoubleLoginFixListener implements Listener { + @Inject + private CommonService service; + + public DoubleLoginFixListener() { + } + + @EventHandler + public void onPlayerJoin(PlayerJoinEvent event) { + Collection PlayerList = Bukkit.getServer().getOnlinePlayers(); + HashSet PlayerSet = new HashSet(); + for (Player ep : PlayerList) { + if (PlayerSet.contains(ep.getName().toLowerCase())) { + ep.kickPlayer(service.retrieveSingleMessage(ep.getPlayer(), MessageKey.DOUBLE_LOGIN_FIX)); + break; + } + PlayerSet.add(ep.getName().toLowerCase()); + } + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/listener/EntityListener.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/listener/EntityListener.java new file mode 100644 index 00000000..35e10920 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/listener/EntityListener.java @@ -0,0 +1,99 @@ +package fr.xephi.authme.listener; + +import org.bukkit.entity.Player; +import org.bukkit.entity.Projectile; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.EntityDamageByEntityEvent; +import org.bukkit.event.entity.EntityDamageEvent; +import org.bukkit.event.entity.EntityInteractEvent; +import org.bukkit.event.entity.EntityRegainHealthEvent; +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 javax.inject.Inject; + +public class EntityListener implements Listener { + + private final ListenerService listenerService; + + @Inject + EntityListener(ListenerService listenerService) { + this.listenerService = listenerService; + } + + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + public void onDamage(EntityDamageEvent event) { + if (listenerService.shouldCancelEvent(event)) { + event.getEntity().setFireTicks(0); + event.setDamage(0); + event.setCancelled(true); + } + } + + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + public void onAttack(EntityDamageByEntityEvent event) { + if (listenerService.shouldCancelEvent(event.getDamager())) { + event.setCancelled(true); + } + } + + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + public void onEntityTarget(EntityTargetEvent event) { + if (listenerService.shouldCancelEvent(event.getTarget())) { + event.setTarget(null); + event.setCancelled(true); + } + } + + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + public void onFoodLevelChange(FoodLevelChangeEvent event) { + if (listenerService.shouldCancelEvent(event)) { + event.setCancelled(true); + } + } + + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + public void entityRegainHealthEvent(EntityRegainHealthEvent event) { + if (listenerService.shouldCancelEvent(event)) { + event.setAmount(0); + event.setCancelled(true); + } + } + + //TODO sgdc3 20190808: We listen at the same event twice, does it make any sense? + @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST) + public void onEntityInteract(EntityInteractEvent event) { + if (listenerService.shouldCancelEvent(event)) { + event.setCancelled(true); + } + } + + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + public void onLowestEntityInteract(EntityInteractEvent event) { + if (listenerService.shouldCancelEvent(event)) { + event.setCancelled(true); + } + } + + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + public void onProjectileLaunch(ProjectileLaunchEvent event) { + final Projectile projectile = event.getEntity(); + + ProjectileSource shooter = projectile.getShooter(); + if (shooter instanceof Player && listenerService.shouldCancelEvent((Player) shooter)) { + event.setCancelled(true); + } + } + + @EventHandler(ignoreCancelled = true, priority = EventPriority.NORMAL) + public void onShoot(EntityShootBowEvent event) { + if (listenerService.shouldCancelEvent(event)) { + event.setCancelled(true); + } + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/listener/FailedVerificationException.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/listener/FailedVerificationException.java new file mode 100644 index 00000000..0b887f92 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/listener/FailedVerificationException.java @@ -0,0 +1,32 @@ +package fr.xephi.authme.listener; + +import fr.xephi.authme.message.MessageKey; + +/** + * Exception thrown when a verification has failed. + */ +public class FailedVerificationException extends Exception { + + private static final long serialVersionUID = 3903242223297960699L; + 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 + + ";args=" + (args == null ? "null" : String.join(", ", args)); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/listener/ListenerService.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/listener/ListenerService.java new file mode 100644 index 00000000..3d582b0e --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/listener/ListenerService.java @@ -0,0 +1,104 @@ +package fr.xephi.authme.listener; + +import fr.xephi.authme.data.auth.PlayerCache; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.initialization.SettingsDependent; +import fr.xephi.authme.service.ValidationService; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.properties.RegistrationSettings; +import fr.xephi.authme.util.PlayerUtils; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import org.bukkit.event.entity.EntityEvent; +import org.bukkit.event.player.PlayerEvent; + +import javax.inject.Inject; + +/** + * Service class for the AuthMe listeners to determine whether an event should be canceled. + */ +class ListenerService implements SettingsDependent { + private final DataSource dataSource; + private final PlayerCache playerCache; + private final ValidationService validationService; + private boolean isRegistrationForced; + + + @Inject + ListenerService(Settings settings, DataSource dataSource, PlayerCache playerCache, + ValidationService validationService) { + this.dataSource = dataSource; + this.playerCache = playerCache; + this.validationService = validationService; + reload(settings); + } + + /** + * 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 + */ + public boolean shouldCancelEvent(EntityEvent event) { + Entity entity = event.getEntity(); + return shouldCancelEvent(entity); + } + + /** + * Returns, based on the entity associated with the event, whether or not the event should be canceled. + * + * @param entity the player entity to verify + * @return true if the associated event should be canceled, false otherwise + */ + public boolean shouldCancelEvent(Entity entity) { + if (entity instanceof Player) { + Player player = (Player) entity; + return shouldCancelEvent(player); + } + return false; + } + + /** + * 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 + */ + public boolean shouldCancelEvent(PlayerEvent event) { + Player player = event.getPlayer(); + return shouldCancelEvent(player); + } + + /** + * 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 + */ + + public boolean shouldCancelEvent(Player player) { + + return player != null && !checkAuth(player.getName()) && !PlayerUtils.isNpc(player); + } + @Override + public void reload(Settings settings) { + isRegistrationForced = settings.getProperty(RegistrationSettings.FORCE); + } + + /** + * 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 (validationService.isUnrestricted(name) || playerCache.isAuthenticated(name)){ + return true; + } + if (!isRegistrationForced && !dataSource.isAuthAvailable(name)) { + return true; + } + return false; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/listener/LoginLocationFixListener.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/listener/LoginLocationFixListener.java new file mode 100644 index 00000000..1c64b68b --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/listener/LoginLocationFixListener.java @@ -0,0 +1,118 @@ +package fr.xephi.authme.listener; + +import fr.xephi.authme.AuthMe; +import fr.xephi.authme.api.v3.AuthMeApi; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.message.Messages; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.properties.SecuritySettings; +import fr.xephi.authme.util.TeleportUtils; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; + +import javax.inject.Inject; +import java.lang.reflect.Method; + + +public class LoginLocationFixListener implements Listener { + @Inject + private AuthMe plugin; + @Inject + private Messages messages; + @Inject + private Settings settings; + private final AuthMeApi authmeApi = AuthMeApi.getInstance(); + + public LoginLocationFixListener() { + } + + private static Material materialPortal = Material.matchMaterial("PORTAL"); + + private static boolean isAvailable; // false: unchecked/method not available true: method is available + BlockFace[] faces = {BlockFace.WEST, BlockFace.EAST, BlockFace.NORTH, BlockFace.SOUTH, BlockFace.SOUTH_EAST, BlockFace.SOUTH_WEST, BlockFace.NORTH_EAST, BlockFace.NORTH_WEST}; + + static { + if (materialPortal == null) { + materialPortal = Material.matchMaterial("PORTAL_BLOCK"); + if (materialPortal == null) { + materialPortal = Material.matchMaterial("NETHER_PORTAL"); + } + } + try { + Method getMinHeightMethod = World.class.getMethod("getMinHeight"); + isAvailable = true; + } catch (NoSuchMethodException e) { + isAvailable = false; + } + } + + private int getMinHeight(World world) { + //This keeps compatibility of 1.16.x and lower + if (isAvailable) { + return world.getMinHeight(); + } else { + return 0; + } + } + + + @EventHandler + public void onPlayerJoin(PlayerJoinEvent event) { + Player player = event.getPlayer(); + Location JoinLocation = player.getLocation(); + if (settings.getProperty(SecuritySettings.LOGIN_LOC_FIX_SUB_PORTAL)) { + if (!JoinLocation.getBlock().getType().equals(materialPortal) && !JoinLocation.getBlock().getRelative(BlockFace.UP).getType().equals(materialPortal)) { + return; + } + Block JoinBlock = JoinLocation.getBlock(); + boolean solved = false; + for (BlockFace face : faces) { + if (JoinBlock.getRelative(face).getType().equals(Material.AIR) && JoinBlock.getRelative(face).getRelative(BlockFace.UP).getType().equals(Material.AIR)) { + TeleportUtils.teleport(player, JoinBlock.getRelative(face).getLocation().add(0.5, 0.1, 0.5)); + solved = true; + break; + } + } + if (!solved) { + JoinBlock.getRelative(BlockFace.UP).breakNaturally(); + JoinBlock.breakNaturally(); + } + messages.send(player, MessageKey.LOCATION_FIX_PORTAL); + } + if (settings.getProperty(SecuritySettings.LOGIN_LOC_FIX_SUB_UNDERGROUND)) { + Material UpType = JoinLocation.getBlock().getRelative(BlockFace.UP).getType(); + World world = player.getWorld(); + int MaxHeight = world.getMaxHeight(); + int MinHeight = getMinHeight(world); + if (!UpType.isOccluding() && !UpType.equals(Material.LAVA)) { + return; + } + for (int i = MinHeight; i <= MaxHeight; i++) { + JoinLocation.setY(i); + Block JoinBlock = JoinLocation.getBlock(); + if ((JoinBlock.getRelative(BlockFace.DOWN).getType().isBlock()) + && JoinBlock.getType().equals(Material.AIR) + && JoinBlock.getRelative(BlockFace.UP).getType().equals(Material.AIR)) { + if (JoinBlock.getRelative(BlockFace.DOWN).getType().equals(Material.LAVA)) { + JoinBlock.getRelative(BlockFace.DOWN).setType(Material.DIRT); + } + TeleportUtils.teleport(player, JoinBlock.getLocation().add(0.5, 0.1, 0.5)); + messages.send(player, MessageKey.LOCATION_FIX_UNDERGROUND); + break; + } + if (i == MaxHeight) { + TeleportUtils.teleport(player, JoinBlock.getLocation().add(0.5, 1.1, 0.5)); + messages.send(player, MessageKey.LOCATION_FIX_UNDERGROUND_CANT_FIX); + } + } + } + + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/listener/OnJoinVerifier.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/listener/OnJoinVerifier.java new file mode 100644 index 00000000..a617c2fa --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/listener/OnJoinVerifier.java @@ -0,0 +1,223 @@ +package fr.xephi.authme.listener; + +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.initialization.Reloadable; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.message.Messages; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.permission.PermissionsManager; +import fr.xephi.authme.permission.PlayerStatePermission; +import fr.xephi.authme.service.AntiBotService; +import fr.xephi.authme.service.BukkitService; +import fr.xephi.authme.service.ValidationService; +import fr.xephi.authme.settings.Settings; +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.StringUtils; +import fr.xephi.authme.util.Utils; +import org.bukkit.Server; +import org.bukkit.entity.Player; +import org.bukkit.event.player.PlayerLoginEvent; + +import javax.annotation.PostConstruct; +import javax.inject.Inject; +import java.util.Collection; +import java.util.Locale; +import java.util.regex.Pattern; + +/** + * Service for performing various verifications when a player joins. + */ +public class OnJoinVerifier implements Reloadable { + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(OnJoinVerifier.class); + + @Inject + private Settings settings; + @Inject + private DataSource dataSource; + @Inject + private Messages messages; + @Inject + private PermissionsManager permissionsManager; + @Inject + private AntiBotService antiBotService; + @Inject + private ValidationService validationService; + @Inject + private BukkitService bukkitService; + @Inject + private Server server; + + private Pattern nicknamePattern; + + OnJoinVerifier() { + } + + + @PostConstruct + @Override + public void reload() { + String nickRegEx = settings.getProperty(RestrictionSettings.ALLOWED_NICKNAME_CHARACTERS); + nicknamePattern = Utils.safePatternCompile(nickRegEx); + } + + /** + * Checks if Antibot is enabled. + * + * @param name the joining player name to check + * @param isAuthAvailable whether or not the player is registered + * @throws FailedVerificationException if the verification fails + */ + public void checkAntibot(String name, boolean isAuthAvailable) throws FailedVerificationException { + if (isAuthAvailable || permissionsManager.hasPermissionOffline(name, PlayerStatePermission.BYPASS_ANTIBOT)) { + return; + } + if (antiBotService.shouldKick()) { + antiBotService.addPlayerKick(name); + 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 + * @throws FailedVerificationException if the verification fails + */ + 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 + * @throws FailedVerificationException if the verification fails + */ + 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(player, 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) { + // AuthMeReReloaded - Folia compatibility + bukkitService.runTaskIfFolia(nonVipPlayer, () -> nonVipPlayer.kickPlayer(messages.retrieveSingle(player, MessageKey.KICK_FOR_VIP))); + event.allow(); + return false; + } else { + logger.info("VIP player " + player.getName() + " tried to join, but the server was full"); + event.setKickMessage(messages.retrieveSingle(player, MessageKey.KICK_FULL_SERVER)); + return true; + } + } + + /** + * Checks that the casing in the username corresponds to the one in the database, if so configured. + * + * @param connectingName the player name to verify + * @param auth the auth object associated with the player + * @throws FailedVerificationException if the verification fails + */ + public void checkNameCasing(String connectingName, PlayerAuth auth) throws FailedVerificationException { + if (auth != null && settings.getProperty(RegistrationSettings.PREVENT_OTHER_CASE)) { + String realName = auth.getRealName(); // might be null or "Player" + + if (StringUtils.isBlank(realName) || "Player".equals(realName)) { + dataSource.updateRealName(connectingName.toLowerCase(Locale.ROOT), connectingName); + } else if (!realName.equals(connectingName)) { + throw new FailedVerificationException(MessageKey.INVALID_NAME_CASE, realName, connectingName); + } + } + } + + /** + * Checks that the player's country is admitted. + * + * @param name the joining player name to verify + * @param address the player address + * @param isAuthAvailable whether or not the user is registered + * @throws FailedVerificationException if the verification fails + */ + public void checkPlayerCountry(String name, String address, + boolean isAuthAvailable) throws FailedVerificationException { + if ((!isAuthAvailable || settings.getProperty(ProtectionSettings.ENABLE_PROTECTION_REGISTERED)) + && settings.getProperty(ProtectionSettings.ENABLE_PROTECTION) + && !permissionsManager.hasPermissionOffline(name, PlayerStatePermission.BYPASS_COUNTRY_CHECK) + && !validationService.isCountryAdmitted(address)) { + 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 name the player name to check + * @throws FailedVerificationException if the verification fails + */ + public void checkSingleSession(String name) throws FailedVerificationException { + if (!settings.getProperty(RestrictionSettings.FORCE_SINGLE_SESSION)) { + return; + } + + Player onlinePlayer = bukkitService.getPlayerExact(name); + if (onlinePlayer != null) { + throw new FailedVerificationException(MessageKey.USERNAME_ALREADY_ONLINE_ERROR); + } + } + + /** + * 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; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/listener/PlayerListener.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/listener/PlayerListener.java new file mode 100644 index 00000000..b24da9da --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/listener/PlayerListener.java @@ -0,0 +1,567 @@ +package fr.xephi.authme.listener; + +import fr.xephi.authme.data.QuickCommandsProtectionManager; +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.message.Messages; +import fr.xephi.authme.permission.PermissionsManager; +import fr.xephi.authme.permission.PlayerStatePermission; +import fr.xephi.authme.process.Management; +import fr.xephi.authme.service.AntiBotService; +import fr.xephi.authme.service.BukkitService; +import fr.xephi.authme.service.JoinMessageService; +import fr.xephi.authme.service.TeleportationService; +import fr.xephi.authme.service.ValidationService; +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; +import fr.xephi.authme.settings.properties.RestrictionSettings; +import fr.xephi.authme.util.TeleportUtils; +import fr.xephi.authme.util.message.I18NUtils; +import fr.xephi.authme.util.message.MiniMessageUtils; +import org.bukkit.ChatColor; +import org.bukkit.Location; +import org.bukkit.entity.HumanEntity; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +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.PlayerCommandPreprocessEvent; +import org.bukkit.event.player.PlayerDropItemEvent; +import org.bukkit.event.player.PlayerEditBookEvent; +import org.bukkit.event.player.PlayerFishEvent; +import org.bukkit.event.player.PlayerInteractAtEntityEvent; +import org.bukkit.event.player.PlayerInteractEntityEvent; +import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.event.player.PlayerItemConsumeEvent; +import org.bukkit.event.player.PlayerItemHeldEvent; +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.PlayerQuitEvent; +import org.bukkit.event.player.PlayerRespawnEvent; +import org.bukkit.event.player.PlayerShearEntityEvent; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.InventoryView; + +import javax.inject.Inject; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +import static fr.xephi.authme.settings.properties.RestrictionSettings.ALLOWED_MOVEMENT_RADIUS; +import static fr.xephi.authme.settings.properties.RestrictionSettings.ALLOW_UNAUTHED_MOVEMENT; +import static org.bukkit.Bukkit.getServer; + +/** + * Listener class for player events. + */ +public class PlayerListener implements Listener { + @Inject + private Settings settings; + @Inject + private Messages messages; + @Inject + private DataSource dataSource; + @Inject + private AntiBotService antiBotService; + @Inject + private Management management; + @Inject + private BukkitService bukkitService; + @Inject + private SpawnLoader spawnLoader; + @Inject + private OnJoinVerifier onJoinVerifier; + @Inject + private ListenerService listenerService; + @Inject + private TeleportationService teleportationService; + @Inject + private ValidationService validationService; + @Inject + private JoinMessageService joinMessageService; + @Inject + private PermissionsManager permissionsManager; + @Inject + private QuickCommandsProtectionManager quickCommandsProtectionManager; + + public static List PENDING_INVENTORIES = new ArrayList<>(); + + // Lowest priority to apply fast protection checks + @EventHandler(priority = EventPriority.LOWEST) + public void onAsyncPlayerPreLoginEventLowest(AsyncPlayerPreLoginEvent event) { + + if (event.getLoginResult() != AsyncPlayerPreLoginEvent.Result.ALLOWED) { + return; + } + final String name = event.getName(); + + // NOTE: getAddress() sometimes returning null, we don't want to handle this race condition + if (event.getAddress() == null) { + event.disallow(AsyncPlayerPreLoginEvent.Result.KICK_OTHER, + messages.retrieveSingle(name, MessageKey.KICK_UNRESOLVED_HOSTNAME)); + return; + } + + if (validationService.isUnrestricted(name)) { + return; + } + if (settings.getProperty(HooksSettings.HOOK_FLOODGATE_PLAYER) && settings.getProperty(HooksSettings.IGNORE_BEDROCK_NAME_CHECK)) { + if (getServer().getPluginManager().getPlugin("floodgate") != null) { + if (org.geysermc.floodgate.api.FloodgateApi.getInstance().isFloodgateId(event.getUniqueId())) return; + } + } + // Non-blocking checks + try { + onJoinVerifier.checkIsValidName(name); + } catch (FailedVerificationException e) { + event.setKickMessage(messages.retrieveSingle(name, e.getReason(), e.getArgs())); + event.setLoginResult(AsyncPlayerPreLoginEvent.Result.KICK_OTHER); + + } + } + + /* + * Login/join/leave events + */ + + // Note: at this stage (HIGHEST priority) the user's permission data should already have been loaded by + // the permission handler, we don't need to call permissionsManager.loadUserData() + + @EventHandler(priority = EventPriority.HIGHEST) + public void onAsyncPlayerPreLoginEventHighest(AsyncPlayerPreLoginEvent event) { + if (event.getLoginResult() != AsyncPlayerPreLoginEvent.Result.ALLOWED) { + return; + } + final String name = event.getName(); + + if (validationService.isUnrestricted(name)) { + return; + } + + // Slow, blocking checks + try { + final PlayerAuth auth = dataSource.getAuth(name); + final boolean isAuthAvailable = auth != null; + onJoinVerifier.checkKickNonRegistered(isAuthAvailable); + onJoinVerifier.checkAntibot(name, isAuthAvailable); + onJoinVerifier.checkNameCasing(name, auth); + final String ip = event.getAddress().getHostAddress(); + onJoinVerifier.checkPlayerCountry(name, ip, isAuthAvailable); + } catch (FailedVerificationException e) { + event.setKickMessage(messages.retrieveSingle(name, e.getReason(), e.getArgs())); + event.setLoginResult(AsyncPlayerPreLoginEvent.Result.KICK_OTHER); + } + } + + // Note: We can't teleport the player in the PlayerLoginEvent listener + // as the new player location will be reverted by the server. + + @EventHandler(priority = EventPriority.LOW) + public void onPlayerLogin(PlayerLoginEvent event) { + final Player player = event.getPlayer(); + final String name = player.getName(); + + try { + onJoinVerifier.checkSingleSession(name); + } catch (FailedVerificationException e) { + event.setKickMessage(messages.retrieveSingle(name, e.getReason(), e.getArgs())); + event.setResult(PlayerLoginEvent.Result.KICK_OTHER); + return; + } + + if (validationService.isUnrestricted(name)) { + return; + } + + onJoinVerifier.refusePlayerForFullServer(event); + } + + @EventHandler(priority = EventPriority.NORMAL) + public void onPlayerJoin(PlayerJoinEvent event) { + final Player player = event.getPlayer(); + if (!PlayerListener19Spigot.isPlayerSpawnLocationEventCalled()) { + teleportationService.teleportOnJoin(player); + } + + quickCommandsProtectionManager.processJoin(player); + + management.performJoin(player); + + teleportationService.teleportNewPlayerToFirstSpawn(player); + + } + + + @EventHandler(priority = EventPriority.HIGH) // HIGH as EssentialsX listens at HIGHEST + public void onJoinMessage(PlayerJoinEvent event) { + final Player player = event.getPlayer(); + + + // Note: join message can be null, despite api documentation says not + if (settings.getProperty(RegistrationSettings.REMOVE_JOIN_MESSAGE)) { + event.setJoinMessage(null); + return; + } + + String customJoinMessage = settings.getProperty(RegistrationSettings.CUSTOM_JOIN_MESSAGE); + if (!customJoinMessage.isEmpty()) { + customJoinMessage = ChatColor.translateAlternateColorCodes('&', MiniMessageUtils.parseMiniMessageToLegacy(customJoinMessage)); + event.setJoinMessage(customJoinMessage + .replace("{PLAYERNAME}", player.getName()) + .replace("{DISPLAYNAME}", player.getDisplayName()) + .replace("{DISPLAYNAMENOCOLOR}", ChatColor.stripColor(player.getDisplayName())) + ); + } + + if (!settings.getProperty(RegistrationSettings.DELAY_JOIN_MESSAGE)) { + return; + } + + String name = player.getName().toLowerCase(Locale.ROOT); + String joinMsg = event.getJoinMessage(); + + // Remove the join message while the player isn't logging in + if (joinMsg != null) { + event.setJoinMessage(null); + joinMessageService.putMessage(name, joinMsg); + } + } + + @EventHandler(priority = EventPriority.HIGHEST) + public void onPlayerQuit(PlayerQuitEvent event) { + Player player = event.getPlayer(); + + // Note: quit message can be null, despite api documentation says not + if (settings.getProperty(RegistrationSettings.REMOVE_LEAVE_MESSAGE)) { + event.setQuitMessage(null); + } else if (settings.getProperty(RegistrationSettings.REMOVE_UNLOGGED_LEAVE_MESSAGE)) { + if (listenerService.shouldCancelEvent(event)) { + event.setQuitMessage(null); + } + } + + // Remove data from locale map when player quit + if (settings.getProperty(PluginSettings.I18N_MESSAGES)) { + I18NUtils.removeLocale(player.getUniqueId()); + } + + if (antiBotService.wasPlayerKicked(player.getName())) { + return; + } + + management.performQuit(player); + } + + @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST) + public void onPlayerKick(PlayerKickEvent event) { + // Note #831: Especially for offline CraftBukkit, we need to catch players being kicked because of + // "logged in from another location" and to cancel their kick + if (settings.getProperty(RestrictionSettings.FORCE_SINGLE_SESSION) + && event.getReason().contains("You logged in from another location")) { + event.setCancelled(true); + return; + } + + final Player player = event.getPlayer(); + if (!antiBotService.wasPlayerKicked(player.getName())) { + management.performQuit(player); + } + } + + /* + * Chat/command events + */ + + private void removeUnauthorizedRecipients(AsyncPlayerChatEvent event) { + if (settings.getProperty(RestrictionSettings.HIDE_CHAT)) { + event.getRecipients().removeIf(listenerService::shouldCancelEvent); + if (event.getRecipients().isEmpty()) { + event.setCancelled(true); + } + } + } + + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + public void onPlayerChat(AsyncPlayerChatEvent event) { + if (settings.getProperty(RestrictionSettings.ALLOW_CHAT)) { + return; + } + + final Player player = event.getPlayer(); + final boolean mayPlayerSendChat = !listenerService.shouldCancelEvent(player) + || permissionsManager.hasPermission(player, PlayerStatePermission.ALLOW_CHAT_BEFORE_LOGIN); + if (mayPlayerSendChat) { + removeUnauthorizedRecipients(event); + } else { + event.setCancelled(true); + messages.send(player, MessageKey.DENIED_CHAT); + } + } + + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + public void onPlayerCommandPreprocess(PlayerCommandPreprocessEvent event) { + String cmd = event.getMessage().split(" ")[0].toLowerCase(Locale.ROOT); + if (settings.getProperty(HooksSettings.USE_ESSENTIALS_MOTD) && "/motd".equals(cmd)) { + return; + } + if (settings.getProperty(RestrictionSettings.ALLOW_COMMANDS).contains(cmd)) { + return; + } + final Player player = event.getPlayer(); + if (!quickCommandsProtectionManager.isAllowed(player.getName())) { + event.setCancelled(true); + bukkitService.runTaskIfFolia(player, () -> player.kickPlayer(messages.retrieveSingle(player, MessageKey.QUICK_COMMAND_PROTECTION_KICK))); + // AuthMeReReloaded - Folia compatibility + return; + } + if (listenerService.shouldCancelEvent(player)) { + event.setCancelled(true); + messages.send(player, MessageKey.DENIED_COMMAND); + } + } + + /* + * Movement events + */ + + @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST) + public void onPlayerMove(PlayerMoveEvent event) { + if (settings.getProperty(ALLOW_UNAUTHED_MOVEMENT) && settings.getProperty(ALLOWED_MOVEMENT_RADIUS) <= 0) { + return; + } + + Location from = event.getFrom(); + Location to = event.getTo(); + if (to == null) { + return; + } + + /* + * Limit player X and Z movements + * Deny player Y+ movements (allows falling) + */ + + if (from.getX() == to.getX() + && from.getZ() == to.getZ() + && from.getY() - to.getY() >= 0) { + return; + } + + Player player = event.getPlayer(); + if (!listenerService.shouldCancelEvent(player)) { + return; + } + + if (!settings.getProperty(RestrictionSettings.ALLOW_UNAUTHED_MOVEMENT)) { + // "cancel" the event + event.setTo(event.getFrom()); + return; + } + + if (settings.getProperty(RestrictionSettings.NO_TELEPORT)) { + return; + } + + Location spawn = spawnLoader.getSpawnLocation(player); + if (spawn != null && spawn.getWorld() != null) { + if (!player.getWorld().equals(spawn.getWorld())) { + TeleportUtils.teleport(player,spawn); + } else if (spawn.distance(player.getLocation()) > settings.getProperty(ALLOWED_MOVEMENT_RADIUS)) { + TeleportUtils.teleport(player,spawn); + } + } + } + + @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST) + public void onPlayerRespawn(PlayerRespawnEvent event) { + if (settings.getProperty(RestrictionSettings.NO_TELEPORT)) { + return; + } + if (!listenerService.shouldCancelEvent(event)) { + return; + } + Location spawn = spawnLoader.getSpawnLocation(event.getPlayer()); + if (spawn != null && spawn.getWorld() != null) { + event.setRespawnLocation(spawn); + } + } + + /* + * Entity/block interaction events + */ + + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + public void onPlayerInteract(PlayerInteractEvent event) { + if (listenerService.shouldCancelEvent(event)) { + event.setCancelled(true); + } + } + + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + public void onPlayerInteractEntity(PlayerInteractEntityEvent event) { + if (listenerService.shouldCancelEvent(event)) { + event.setCancelled(true); + } + } + + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + public void onPlayerInteractAtEntity(PlayerInteractAtEntityEvent event) { + if (listenerService.shouldCancelEvent(event)) { + event.setCancelled(true); + } + } + + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + public void onPlayerHitPlayerEvent(EntityDamageByEntityEvent event) { + if (listenerService.shouldCancelEvent(event)) { + event.setCancelled(true); + } + } + + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + public void onPlayerShear(PlayerShearEntityEvent event) { + if (listenerService.shouldCancelEvent(event)) { + event.setCancelled(true); + } + } + + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + public void onPlayerFish(PlayerFishEvent event) { + if (listenerService.shouldCancelEvent(event)) { + event.setCancelled(true); + } + } + + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + public void onPlayerBedEnter(PlayerBedEnterEvent event) { + if (listenerService.shouldCancelEvent(event)) { + event.setCancelled(true); + } + } + + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + public void onPlayerEditBook(PlayerEditBookEvent event) { + if (listenerService.shouldCancelEvent(event)) { + event.setCancelled(true); + } + } + + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + public void onSignChange(SignChangeEvent event) { + Player player = event.getPlayer(); + if (listenerService.shouldCancelEvent(player)) { + event.setCancelled(true); + } + } + + /* + * Inventory interactions + */ + +// @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) +// public void onPlayerPickupItem(EntityPickupItemEvent event) { +// if (listenerService.shouldCancelEvent(event)) { +// event.setCancelled(true); +// } +// } + + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + public void onPlayerDropItem(PlayerDropItemEvent event) { + if (listenerService.shouldCancelEvent(event)) { + event.setCancelled(true); + } + } + + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + public void onPlayerHeldItem(PlayerItemHeldEvent event) { + if (listenerService.shouldCancelEvent(event)) { + event.setCancelled(true); + } + } + + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + public void onPlayerConsumeItem(PlayerItemConsumeEvent event) { + if (listenerService.shouldCancelEvent(event)) { + event.setCancelled(true); + } + } + + private boolean isInventoryOpenedByApi(Inventory inventory) { + if (inventory == null) { + return false; + } + if (PENDING_INVENTORIES.contains(inventory)) { + PENDING_INVENTORIES.remove(inventory); + return true; + } else { + return false; + } + } + @SuppressWarnings("all") + private boolean isInventoryWhitelisted(InventoryView inventory) { + if (inventory == null) { + return false; + } + Set whitelist = settings.getProperty(RestrictionSettings.UNRESTRICTED_INVENTORIES); + if (whitelist.isEmpty()) { + return false; + } + //append a string for String whitelist + String invName = ChatColor.stripColor(inventory.getTitle()).toLowerCase(Locale.ROOT); + if (settings.getProperty(RestrictionSettings.STRICT_UNRESTRICTED_INVENTORIES_CHECK)) { + return whitelist.contains(invName); + } + for (String wl : whitelist) { + if (invName.contains(wl)) { + return true; + } + } + return false; + } + + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + public void onPlayerInventoryOpen(InventoryOpenEvent event) { + final HumanEntity player = event.getPlayer(); + if (listenerService.shouldCancelEvent(player) + && !isInventoryWhitelisted(event.getView()) + && !isInventoryOpenedByApi(event.getInventory())) { + event.setCancelled(true); + + /* + * @note little hack cause InventoryOpenEvent cannot be cancelled for + * real, cause no packet is sent to server by client for the main inv + */ + bukkitService.scheduleSyncDelayedTask(player::closeInventory, 1); + } + } + + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + public void onPlayerInventoryClick(InventoryClickEvent event) { + if (listenerService.shouldCancelEvent(event.getWhoClicked()) + && !isInventoryWhitelisted(event.getView())) { + event.setCancelled(true); + } + } +// @EventHandler(priority = EventPriority.LOWEST) +// public void onSwitchHand(PlayerSwapHandItemsEvent event) { +// Player player = event.getPlayer(); +// if (!player.isSneaking() || !player.hasPermission("keybindings.use")) +// return; +// event.setCancelled(true); +// Bukkit.dispatchCommand(event.getPlayer(), "help"); +// } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/listener/PlayerListener111.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/listener/PlayerListener111.java new file mode 100644 index 00000000..43b4ca33 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/listener/PlayerListener111.java @@ -0,0 +1,24 @@ +package fr.xephi.authme.listener; + +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.EntityAirChangeEvent; + +import javax.inject.Inject; + +/** + * Listener of player events for events introduced in Minecraft 1.11. + */ +public class PlayerListener111 implements Listener { + + @Inject + private ListenerService listenerService; + + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + public void onPlayerAirChange(EntityAirChangeEvent event) { + if (listenerService.shouldCancelEvent(event)) { + event.setCancelled(true); + } + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/listener/PlayerListener19.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/listener/PlayerListener19.java new file mode 100644 index 00000000..aeb116dc --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/listener/PlayerListener19.java @@ -0,0 +1,24 @@ +package fr.xephi.authme.listener; + +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerSwapHandItemsEvent; + +import javax.inject.Inject; + +/** + * Listener of player events for events introduced in Minecraft 1.9. + */ +public class PlayerListener19 implements Listener { + + @Inject + private ListenerService listenerService; + + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + public void onPlayerSwapHandItems(PlayerSwapHandItemsEvent event) { + if (listenerService.shouldCancelEvent(event)) { + event.setCancelled(true); + } + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/listener/PlayerListener19Spigot.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/listener/PlayerListener19Spigot.java new file mode 100644 index 00000000..a068f27d --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/listener/PlayerListener19Spigot.java @@ -0,0 +1,36 @@ +package fr.xephi.authme.listener; + +import fr.xephi.authme.service.TeleportationService; +import org.bukkit.Location; +import org.bukkit.entity.Player; +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; + +public class PlayerListener19Spigot implements Listener { + + private static boolean isPlayerSpawnLocationEventCalled = false; + + @Inject + private TeleportationService teleportationService; + + + public static boolean isPlayerSpawnLocationEventCalled() { + return isPlayerSpawnLocationEventCalled; + } + + // Note: the following event is called since MC1.9, in older versions we have to fallback on the PlayerJoinEvent + @EventHandler(priority = EventPriority.HIGH) + public void onPlayerSpawn(PlayerSpawnLocationEvent event) { + isPlayerSpawnLocationEventCalled = true; + final Player player = event.getPlayer(); + + Location customSpawnLocation = teleportationService.prepareOnJoinSpawnLocation(player); + if (customSpawnLocation != null) { + event.setSpawnLocation(customSpawnLocation); + } + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/listener/PlayerListenerHigherThan18.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/listener/PlayerListenerHigherThan18.java new file mode 100644 index 00000000..66c408a1 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/listener/PlayerListenerHigherThan18.java @@ -0,0 +1,37 @@ +package fr.xephi.authme.listener; + +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.properties.PluginSettings; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.EntityPickupItemEvent; +import org.bukkit.event.player.PlayerSwapHandItemsEvent; + +import javax.inject.Inject; + +public class PlayerListenerHigherThan18 implements Listener { + @Inject + private ListenerService listenerService; + + @Inject + private Settings settings; + + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + public void onPlayerPickupItem(EntityPickupItemEvent event) { + if (listenerService.shouldCancelEvent(event)) { + event.setCancelled(true); + } + } + + @EventHandler(priority = EventPriority.LOWEST) + public void onSwitchHand(PlayerSwapHandItemsEvent event) { + Player player = event.getPlayer(); + if (player.isSneaking() && player.hasPermission("keybindings.use") && settings.getProperty(PluginSettings.MENU_UNREGISTER_COMPATIBILITY)) { + event.setCancelled(true); + Bukkit.dispatchCommand(event.getPlayer(), "help"); + } + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/listener/PurgeListener.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/listener/PurgeListener.java new file mode 100644 index 00000000..dcc3db4e --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/listener/PurgeListener.java @@ -0,0 +1,89 @@ +package fr.xephi.authme.listener; + +import fr.xephi.authme.AuthMe; +import fr.xephi.authme.api.v3.AuthMeApi; +import fr.xephi.authme.service.BukkitService; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.properties.SecuritySettings; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerQuitEvent; + +import javax.inject.Inject; +import java.io.File; +import java.util.UUID; + +public class PurgeListener implements Listener { + private final AuthMeApi authmeApi = AuthMeApi.getInstance(); + + @Inject + private Settings settings; + @Inject + private BukkitService bukkitService; + @Inject + private AuthMe plugin; + + @EventHandler + public void onPlayerQuit(PlayerQuitEvent event) { + Player player = event.getPlayer(); + String name = player.getName(); + UUID playerUUID = event.getPlayer().getUniqueId(); + if (!authmeApi.isRegistered(name)) { + if (settings.getProperty(SecuritySettings.PURGE_DATA_ON_QUIT)) { + bukkitService.runTaskLater(() -> { + if (!player.isOnline()) { + deletePlayerData(playerUUID); + deletePlayerStats(playerUUID); + deleteAuthMePlayerData(playerUUID); + } + }, 100L); + } + } + } + + private void deletePlayerData(UUID playerUUID) { + // 获取服务器的存储文件夹路径 + File serverFolder = Bukkit.getServer().getWorldContainer(); + String worldFolderName = settings.getProperty(SecuritySettings.DELETE_PLAYER_DATA_WORLD); + // 构建playerdata文件夹路径 + File playerDataFolder = new File(serverFolder, File.separator + worldFolderName + File.separator + "playerdata"); + + // 构建玩家数据文件路径 + File playerDataFile = new File(playerDataFolder, File.separator + playerUUID + ".dat"); + File playerDataOldFile = new File(playerDataFolder, File.separator + playerUUID + ".dat_old"); + + // 删除玩家数据文件 + if (playerDataFile.exists()) { + playerDataFile.delete(); + } + if (playerDataOldFile.exists()) { + playerDataOldFile.delete(); + } + } + + private void deleteAuthMePlayerData(UUID playerUUID) { + File pluginFolder = plugin.getDataFolder(); + File path = new File(pluginFolder, File.separator + "playerdata" + File.separator + playerUUID); + File dataFile = new File(path, File.separator + "data.json"); + if (dataFile.exists()) { + dataFile.delete(); + path.delete(); + } + } + + private void deletePlayerStats(UUID playerUUID) { + // 获取服务器的存储文件夹路径 + File serverFolder = Bukkit.getServer().getWorldContainer(); + String worldFolderName = settings.getProperty(SecuritySettings.DELETE_PLAYER_DATA_WORLD); + // 构建stats文件夹路径 + File statsFolder = new File(serverFolder, File.separator + worldFolderName + File.separator + "stats"); + // 构建玩家统计数据文件路径 + File statsFile = new File(statsFolder, File.separator + playerUUID + ".json"); + // 删除玩家统计数据文件 + if (statsFile.exists()) { + statsFile.delete(); + } + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/listener/ServerListener.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/listener/ServerListener.java new file mode 100644 index 00000000..97cefa04 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/listener/ServerListener.java @@ -0,0 +1,79 @@ +package fr.xephi.authme.listener; + +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.listener.protocollib.ProtocolLibService; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.permission.PermissionsManager; +import fr.xephi.authme.service.PluginHookService; +import fr.xephi.authme.settings.SpawnLoader; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.server.PluginDisableEvent; +import org.bukkit.event.server.PluginEnableEvent; + +import javax.inject.Inject; + +/** + * Listener for server events. + */ +public class ServerListener implements Listener { + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(ServerListener.class); + + @Inject + private PluginHookService pluginHookService; + @Inject + private SpawnLoader spawnLoader; + @Inject + private ProtocolLibService protocolLibService; + @Inject + private PermissionsManager permissionsManager; + + @EventHandler(priority = EventPriority.HIGHEST) + public void onPluginDisable(PluginDisableEvent event) { + final String pluginName = event.getPlugin().getName(); + + // Call the onPluginDisable method in the permissions manager + permissionsManager.onPluginDisable(pluginName); + + if ("Essentials".equalsIgnoreCase(pluginName)) { + pluginHookService.unhookEssentials(); + logger.info("Essentials has been disabled: unhooking"); + } else if ("CMI".equalsIgnoreCase(pluginName)) { + pluginHookService.unhookCmi(); + spawnLoader.unloadCmiSpawn(); + logger.info("CMI has been disabled: unhooking"); + } else if ("Multiverse-Core".equalsIgnoreCase(pluginName)) { + pluginHookService.unhookMultiverse(); + logger.info("Multiverse-Core has been disabled: unhooking"); + } else if ("EssentialsSpawn".equalsIgnoreCase(pluginName)) { + spawnLoader.unloadEssentialsSpawn(); + logger.info("EssentialsSpawn has been disabled: unhooking"); + } else if ("ProtocolLib".equalsIgnoreCase(pluginName)) { + protocolLibService.disable(); + logger.warning("ProtocolLib has been disabled, unhooking packet adapters!"); + } + } + + @EventHandler(priority = EventPriority.HIGHEST) + public void onPluginEnable(PluginEnableEvent event) { + final String pluginName = event.getPlugin().getName(); + + // Call the onPluginEnable method in the permissions manager + permissionsManager.onPluginEnable(pluginName); + + if ("Essentials".equalsIgnoreCase(pluginName)) { + pluginHookService.tryHookToEssentials(); + } else if ("Multiverse-Core".equalsIgnoreCase(pluginName)) { + pluginHookService.tryHookToMultiverse(); + } else if ("EssentialsSpawn".equalsIgnoreCase(pluginName)) { + spawnLoader.loadEssentialsSpawn(); + } else if ("CMI".equalsIgnoreCase(pluginName)) { + pluginHookService.tryHookToCmi(); + spawnLoader.loadCmiSpawn(); + } else if ("ProtocolLib".equalsIgnoreCase(pluginName)) { + protocolLibService.setup(); + } + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/listener/protocollib/I18NGetLocalePacketAdapter.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/listener/protocollib/I18NGetLocalePacketAdapter.java new file mode 100644 index 00000000..60148ba5 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/listener/protocollib/I18NGetLocalePacketAdapter.java @@ -0,0 +1,36 @@ +package fr.xephi.authme.listener.protocollib; + +import com.comphenix.protocol.PacketType; +import com.comphenix.protocol.ProtocolLibrary; +import com.comphenix.protocol.events.ListenerPriority; +import com.comphenix.protocol.events.PacketAdapter; +import com.comphenix.protocol.events.PacketEvent; +import fr.xephi.authme.AuthMe; +import fr.xephi.authme.util.message.I18NUtils; + +import java.util.UUID; + +class I18NGetLocalePacketAdapter extends PacketAdapter { + + I18NGetLocalePacketAdapter(AuthMe plugin) { + super(plugin, ListenerPriority.NORMAL, PacketType.Play.Client.SETTINGS); + } + + @Override + public void onPacketReceiving(PacketEvent event) { + if (event.getPacketType() == PacketType.Play.Client.SETTINGS) { + String locale = event.getPacket().getStrings().read(0).toLowerCase(); + UUID uuid = event.getPlayer().getUniqueId(); + + I18NUtils.addLocale(uuid, locale); + } + } + + public void register() { + ProtocolLibrary.getProtocolManager().addPacketListener(this); + } + + public void unregister() { + ProtocolLibrary.getProtocolManager().removePacketListener(this); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/listener/protocollib/InventoryPacketAdapter.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/listener/protocollib/InventoryPacketAdapter.java new file mode 100644 index 00000000..c83b992b --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/listener/protocollib/InventoryPacketAdapter.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2015 AuthMe-Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package fr.xephi.authme.listener.protocollib; + +import com.comphenix.protocol.PacketType; +import com.comphenix.protocol.ProtocolLibrary; +import com.comphenix.protocol.ProtocolManager; +import com.comphenix.protocol.events.PacketAdapter; +import com.comphenix.protocol.events.PacketContainer; +import com.comphenix.protocol.events.PacketEvent; +import com.comphenix.protocol.reflect.StructureModifier; +import fr.xephi.authme.AuthMe; +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.data.auth.PlayerCache; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.service.BukkitService; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +import java.util.Arrays; +import java.util.List; + +class InventoryPacketAdapter extends PacketAdapter { + + private static final int PLAYER_INVENTORY = 0; + // http://wiki.vg/Inventory#Inventory (0-4 crafting, 5-8 armor, 9-35 main inventory, 36-44 hotbar, 45 off hand) + // +1 because an index starts with 0 + private static final int CRAFTING_SIZE = 5; + private static final int ARMOR_SIZE = 4; + private static final int MAIN_SIZE = 27; + private static final int HOTBAR_SIZE = 9; + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(InventoryPacketAdapter.class); + private final PlayerCache playerCache; + private final DataSource dataSource; + + InventoryPacketAdapter(AuthMe plugin, PlayerCache playerCache, DataSource dataSource) { + super(plugin, PacketType.Play.Server.SET_SLOT, PacketType.Play.Server.WINDOW_ITEMS); + this.playerCache = playerCache; + this.dataSource = dataSource; + } + + @Override + public void onPacketSending(PacketEvent packetEvent) { + Player player = packetEvent.getPlayer(); + PacketContainer packet = packetEvent.getPacket(); + + int windowId = packet.getIntegers().read(0); + if (windowId == PLAYER_INVENTORY && shouldHideInventory(player.getName())) { + packetEvent.setCancelled(true); + } + } + + /** + * Registers itself to ProtocolLib and blanks out the inventory packet to any applicable players. + * + * @param bukkitService the bukkit service (for retrieval of online players) + */ + public void register(BukkitService bukkitService) { + ProtocolLibrary.getProtocolManager().addPacketListener(this); + + bukkitService.getOnlinePlayers().stream() + .filter(player -> shouldHideInventory(player.getName())) + .forEach(this::sendBlankInventoryPacket); + } + + private boolean shouldHideInventory(String playerName) { + return !playerCache.isAuthenticated(playerName) && dataSource.isAuthAvailable(playerName); + } + + public void unregister() { + ProtocolLibrary.getProtocolManager().removePacketListener(this); + } + + /** + * Sends a blanked out packet to the given player in order to hide the inventory. + * + * @param player the player to send the blank inventory packet to + */ + public void sendBlankInventoryPacket(Player player) { + ProtocolManager protocolManager = ProtocolLibrary.getProtocolManager(); + PacketContainer inventoryPacket = protocolManager.createPacket(PacketType.Play.Server.WINDOW_ITEMS); + inventoryPacket.getIntegers().write(0, PLAYER_INVENTORY); + int inventorySize = CRAFTING_SIZE + ARMOR_SIZE + MAIN_SIZE + HOTBAR_SIZE; + + ItemStack[] blankInventory = new ItemStack[inventorySize]; + Arrays.fill(blankInventory, new ItemStack(Material.AIR)); + + //old minecraft versions + StructureModifier itemArrayModifier = inventoryPacket.getItemArrayModifier(); + if (itemArrayModifier.size() > 0) { + itemArrayModifier.write(0, blankInventory); + } else { + //minecraft versions above 1.11 + StructureModifier> itemListModifier = inventoryPacket.getItemListModifier(); + itemListModifier.write(0, Arrays.asList(blankInventory)); + } + + protocolManager.sendServerPacket(player, inventoryPacket, false); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/listener/protocollib/ProtocolLibService.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/listener/protocollib/ProtocolLibService.java new file mode 100644 index 00000000..f0297b33 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/listener/protocollib/ProtocolLibService.java @@ -0,0 +1,159 @@ +package fr.xephi.authme.listener.protocollib; + +import ch.jalu.injector.annotations.NoFieldScan; +import fr.xephi.authme.AuthMe; +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.data.auth.PlayerCache; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.initialization.SettingsDependent; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.service.BukkitService; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.properties.PluginSettings; +import fr.xephi.authme.settings.properties.RestrictionSettings; +import fr.xephi.authme.util.Utils; +import org.bukkit.entity.Player; + +import javax.inject.Inject; + +@NoFieldScan +public class ProtocolLibService implements SettingsDependent { + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(ProtocolLibService.class); + + /* Packet Adapters */ + private InventoryPacketAdapter inventoryPacketAdapter; + private TabCompletePacketAdapter tabCompletePacketAdapter; + private I18NGetLocalePacketAdapter i18nGetLocalePacketAdapter; + + /* Settings */ + private boolean protectInvBeforeLogin; + private boolean denyTabCompleteBeforeLogin; + private boolean i18nMessagesSending; + + /* Service */ + private boolean isEnabled; + private final AuthMe plugin; + private final BukkitService bukkitService; + private final PlayerCache playerCache; + private final DataSource dataSource; + + @Inject + ProtocolLibService(AuthMe plugin, Settings settings, BukkitService bukkitService, PlayerCache playerCache, + DataSource dataSource) { + this.plugin = plugin; + this.bukkitService = bukkitService; + this.playerCache = playerCache; + this.dataSource = dataSource; + reload(settings); + } + + /** + * Set up the ProtocolLib packet adapters. + */ + public void setup() { + // Check if ProtocolLib is enabled on the server. + if (!plugin.getServer().getPluginManager().isPluginEnabled("ProtocolLib")) { + if (protectInvBeforeLogin) { + logger.warning("WARNING! The protectInventory feature requires ProtocolLib! Disabling it..."); + } + + if (denyTabCompleteBeforeLogin) { + logger.warning("WARNING! The denyTabComplete feature requires ProtocolLib! Disabling it..."); + } + + if (i18nMessagesSending) { + logger.warning("WARNING! The i18n Messages feature requires ProtocolLib on lower version (< 1.15.2)! Disabling it..."); + } + + this.isEnabled = false; + return; + } + + // Set up packet adapters + if (protectInvBeforeLogin) { + if (inventoryPacketAdapter == null) { + // register the packet listener and start hiding it for all already online players (reload) + inventoryPacketAdapter = new InventoryPacketAdapter(plugin, playerCache, dataSource); + inventoryPacketAdapter.register(bukkitService); + } + } else if (inventoryPacketAdapter != null) { + inventoryPacketAdapter.unregister(); + inventoryPacketAdapter = null; + } + + if (denyTabCompleteBeforeLogin) { + if (tabCompletePacketAdapter == null) { + tabCompletePacketAdapter = new TabCompletePacketAdapter(plugin, playerCache); + tabCompletePacketAdapter.register(); + } + } else if (tabCompletePacketAdapter != null) { + tabCompletePacketAdapter.unregister(); + tabCompletePacketAdapter = null; + } + + if (i18nMessagesSending) { + if (i18nGetLocalePacketAdapter == null) { + i18nGetLocalePacketAdapter = new I18NGetLocalePacketAdapter(plugin); + i18nGetLocalePacketAdapter.register(); + } + } else if (i18nGetLocalePacketAdapter != null) { + i18nGetLocalePacketAdapter.unregister(); + i18nGetLocalePacketAdapter = null; + } + + this.isEnabled = true; + } + + /** + * Stops all features based on ProtocolLib. + */ + public void disable() { + isEnabled = false; + + if (inventoryPacketAdapter != null) { + inventoryPacketAdapter.unregister(); + inventoryPacketAdapter = null; + } + if (tabCompletePacketAdapter != null) { + tabCompletePacketAdapter.unregister(); + tabCompletePacketAdapter = null; + } + if (i18nGetLocalePacketAdapter != null) { + i18nGetLocalePacketAdapter.unregister(); + i18nGetLocalePacketAdapter = null; + } + } + + /** + * Send a packet to the player to give them a blank inventory. + * + * @param player The player to send the packet to. + */ + public void sendBlankInventoryPacket(Player player) { + if (isEnabled && inventoryPacketAdapter != null) { + inventoryPacketAdapter.sendBlankInventoryPacket(player); + } + } + + @Override + public void reload(Settings settings) { + boolean oldProtectInventory = this.protectInvBeforeLogin; + + this.protectInvBeforeLogin = settings.getProperty(RestrictionSettings.PROTECT_INVENTORY_BEFORE_LOGIN); + this.denyTabCompleteBeforeLogin = settings.getProperty(RestrictionSettings.DENY_TABCOMPLETE_BEFORE_LOGIN); + this.i18nMessagesSending = settings.getProperty(PluginSettings.I18N_MESSAGES) && Utils.MAJOR_VERSION <= 15; + + //it was true and will be deactivated now, so we need to restore the inventory for every player + if (oldProtectInventory && !protectInvBeforeLogin && inventoryPacketAdapter != null) { + inventoryPacketAdapter.unregister(); + for (Player onlinePlayer : bukkitService.getOnlinePlayers()) { + if (!playerCache.isAuthenticated(onlinePlayer.getName())) { + onlinePlayer.updateInventory(); + } + } + } + setup(); + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/listener/protocollib/TabCompletePacketAdapter.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/listener/protocollib/TabCompletePacketAdapter.java new file mode 100644 index 00000000..3f0bb316 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/listener/protocollib/TabCompletePacketAdapter.java @@ -0,0 +1,44 @@ +package fr.xephi.authme.listener.protocollib; + +import com.comphenix.protocol.PacketType; +import com.comphenix.protocol.ProtocolLibrary; +import com.comphenix.protocol.events.ListenerPriority; +import com.comphenix.protocol.events.PacketAdapter; +import com.comphenix.protocol.events.PacketEvent; +import com.comphenix.protocol.reflect.FieldAccessException; +import fr.xephi.authme.AuthMe; +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.data.auth.PlayerCache; +import fr.xephi.authme.output.ConsoleLoggerFactory; + +class TabCompletePacketAdapter extends PacketAdapter { + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(TabCompletePacketAdapter.class); + private final PlayerCache playerCache; + + TabCompletePacketAdapter(AuthMe plugin, PlayerCache playerCache) { + super(plugin, ListenerPriority.NORMAL, PacketType.Play.Client.TAB_COMPLETE); + this.playerCache = playerCache; + } + + @Override + public void onPacketReceiving(PacketEvent event) { + if (event.getPacketType() == PacketType.Play.Client.TAB_COMPLETE) { + try { + if (!playerCache.isAuthenticated(event.getPlayer().getName())) { + event.setCancelled(true); + } + } catch (FieldAccessException e) { + logger.logException("Couldn't access field:", e); + } + } + } + + public void register() { + ProtocolLibrary.getProtocolManager().addPacketListener(this); + } + + public void unregister() { + ProtocolLibrary.getProtocolManager().removePacketListener(this); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/mail/EmailService.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/mail/EmailService.java new file mode 100644 index 00000000..76968fb9 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/mail/EmailService.java @@ -0,0 +1,223 @@ +package fr.xephi.authme.mail; + +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.initialization.DataFolder; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.properties.EmailSettings; +import fr.xephi.authme.settings.properties.PluginSettings; +import fr.xephi.authme.settings.properties.SecuritySettings; +import fr.xephi.authme.util.FileUtils; +import org.apache.commons.mail.EmailException; +import org.apache.commons.mail.HtmlEmail; + +import javax.activation.DataSource; +import javax.activation.FileDataSource; +import javax.imageio.ImageIO; +import javax.inject.Inject; +import java.io.File; +import java.io.IOException; + +/** + * Creates emails and sends them. + */ +public class EmailService { + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(EmailService.class); + + private final File dataFolder; + private final Settings settings; + private final SendMailSsl sendMailSsl; + + @Inject + EmailService(@DataFolder File dataFolder, Settings settings, SendMailSsl sendMailSsl) { + this.dataFolder = dataFolder; + this.settings = settings; + this.sendMailSsl = sendMailSsl; + } + + public boolean hasAllInformation() { + return sendMailSsl.hasAllInformation(); + } + + public boolean sendNewPasswordMail(String name, String mailAddress, String newPass,String ip,String time) { + HtmlEmail email; + try { + email = sendMailSsl.initializeMail(mailAddress); + } catch (EmailException e) { + logger.logException("Failed to create email with the given settings:", e); + return false; + } + + String mailText = replaceTagsForPasswordMail(settings.getNewPasswordEmailMessage(), name, newPass,ip,time); + File file = null; + if (settings.getProperty(EmailSettings.PASSWORD_AS_IMAGE)) { + try { + file = generatePasswordImage(name, newPass); + mailText = embedImageIntoEmailContent(file, email, mailText); + } catch (IOException | EmailException e) { + logger.logException( + "Unable to send new password as image for email " + mailAddress + ":", e); + } + } + + boolean couldSendEmail = sendMailSsl.sendEmail(mailText, email); + FileUtils.delete(file); + return couldSendEmail; + } + + /** + * Sends an email to the user with his new password. + * + * @param name the name of the player + * @param mailAddress the player's email + * @param newPass the new password + * @return true if email could be sent, false otherwise + */ + public boolean sendPasswordMail(String name, String mailAddress, String newPass, String time) { + if (!hasAllInformation()) { + logger.warning("Cannot perform email registration: not all email settings are complete"); + return false; + } + + HtmlEmail email; + try { + email = sendMailSsl.initializeMail(mailAddress); + } catch (EmailException e) { + logger.logException("Failed to create email with the given settings:", e); + return false; + } + + String mailText = replaceTagsForPasswordMail(settings.getPasswordEmailMessage(), name, newPass,time); + // Generate an image? + File file = null; + if (settings.getProperty(EmailSettings.PASSWORD_AS_IMAGE)) { + try { + file = generatePasswordImage(name, newPass); + mailText = embedImageIntoEmailContent(file, email, mailText); + } catch (IOException | EmailException e) { + logger.logException( + "Unable to send new password as image for email " + mailAddress + ":", e); + } + } + + boolean couldSendEmail = sendMailSsl.sendEmail(mailText, email); + FileUtils.delete(file); + return couldSendEmail; + } + /** + * Sends an email to the user with the temporary verification code. + * + * @param name the name of the player + * @param mailAddress the player's email + * @param code the verification code + */ + public void sendVerificationMail(String name, String mailAddress, String code, String time) { + if (!hasAllInformation()) { + logger.warning("Cannot send verification email: not all email settings are complete"); + return; + } + + HtmlEmail email; + try { + email = sendMailSsl.initializeMail(mailAddress); + } catch (EmailException e) { + logger.logException("Failed to create verification email with the given settings:", e); + return; + } + + String mailText = replaceTagsForVerificationEmail(settings.getVerificationEmailMessage(), name, code, + settings.getProperty(SecuritySettings.VERIFICATION_CODE_EXPIRATION_MINUTES),time); + sendMailSsl.sendEmail(mailText, email); + } + + /** + * Sends an email to the user with a recovery code for the password recovery process. + * + * @param name the name of the player + * @param email the player's email address + * @param code the recovery code + * @return true if email could be sent, false otherwise + */ + public boolean sendRecoveryCode(String name, String email, String code, String time) { + HtmlEmail htmlEmail; + try { + htmlEmail = sendMailSsl.initializeMail(email); + } catch (EmailException e) { + logger.logException("Failed to create email for recovery code:", e); + return false; + } + + String message = replaceTagsForRecoveryCodeMail(settings.getRecoveryCodeEmailMessage(), + name, code, settings.getProperty(SecuritySettings.RECOVERY_CODE_HOURS_VALID),time); + return sendMailSsl.sendEmail(message, htmlEmail); + } + + public void sendShutDown(String email, String time) { + HtmlEmail htmlEmail; + try { + htmlEmail = sendMailSsl.initializeMail(email); + } catch (EmailException e) { + logger.logException("Failed to create email for shutdown:", e); + return; + } + + String message = replaceTagsForShutDownMail(settings.getShutdownEmailMessage(), time); + sendMailSsl.sendEmail(message, htmlEmail); + } + + private File generatePasswordImage(String name, String newPass) throws IOException { + ImageGenerator gen = new ImageGenerator(newPass); + File file = new File(dataFolder, name + "_new_pass.jpg"); + ImageIO.write(gen.generateImage(), "jpg", file); + return file; + } + + private static String embedImageIntoEmailContent(File image, HtmlEmail email, String content) + throws EmailException { + DataSource source = new FileDataSource(image); + String tag = email.embed(source, image.getName()); + return content.replace("", ""); + } + + private String replaceTagsForPasswordMail(String mailText, String name, String newPass,String ip,String time) { + return mailText + .replace("", name) + .replace("", settings.getProperty(PluginSettings.SERVER_NAME)) + .replace("", newPass) + .replace("", ip) + .replace("

+ * Only the "XOAUTH2" mechanism is supported. The {@code callbackHandler} is + * passed to the OAuth2SaslClient. Other parameters are ignored. + */ +public class OAuth2SaslClientFactory implements SaslClientFactory { + + private static final Logger logger = Logger.getLogger(OAuth2SaslClientFactory.class.getName()); + + public static final String OAUTH_TOKEN_PROP = "mail.smpt.sasl.mechanisms.oauth2.oauthToken"; + + public SaslClient createSaslClient(String[] mechanisms, + String authorizationId, String protocol, String serverName, + Map props, CallbackHandler callbackHandler) { + boolean matchedMechanism = false; + for (int i = 0; i < mechanisms.length; ++i) { + if ("XOAUTH2".equalsIgnoreCase(mechanisms[i])) { + matchedMechanism = true; + break; + } + } + if (!matchedMechanism) { + logger.info("Failed to match any mechanisms"); + return null; + } + return new OAuth2SaslClient((String) props.get(OAUTH_TOKEN_PROP), callbackHandler); + } + + public String[] getMechanismNames(Map props) { + return new String[] { "XOAUTH2" }; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/mail/SendMailSsl.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/mail/SendMailSsl.java new file mode 100644 index 00000000..b7e9fd41 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/mail/SendMailSsl.java @@ -0,0 +1,158 @@ +package fr.xephi.authme.mail; + +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.output.LogLevel; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.properties.EmailSettings; +import fr.xephi.authme.settings.properties.PluginSettings; +import fr.xephi.authme.util.StringUtils; +import org.apache.commons.mail.EmailConstants; +import org.apache.commons.mail.EmailException; +import org.apache.commons.mail.HtmlEmail; + +import javax.activation.CommandMap; +import javax.activation.MailcapCommandMap; +import javax.inject.Inject; +import javax.mail.Session; +import java.security.Security; +import java.util.Properties; + +import static fr.xephi.authme.settings.properties.EmailSettings.MAIL_ACCOUNT; +import static fr.xephi.authme.settings.properties.EmailSettings.MAIL_PASSWORD; + + +/** + * Sends emails to players on behalf of the server. + */ +public class SendMailSsl { + + private ConsoleLogger logger = ConsoleLoggerFactory.get(SendMailSsl.class); + + @Inject + private Settings settings; + + /** + * Returns whether all necessary settings are set for sending mails. + * + * @return true if the necessary email settings are set, false otherwise + */ + public boolean hasAllInformation() { + return !settings.getProperty(MAIL_ACCOUNT).isEmpty() + && !settings.getProperty(MAIL_PASSWORD).isEmpty(); + } + + /** + * Creates a {@link HtmlEmail} object configured as per the AuthMe config + * with the given email address as recipient. + * + * @param emailAddress the email address the email is destined for + * @return the created HtmlEmail object + * @throws EmailException if the mail is invalid + */ + public HtmlEmail initializeMail(String emailAddress) throws EmailException { + String senderMail = StringUtils.isBlank(settings.getProperty(EmailSettings.MAIL_ADDRESS)) + ? settings.getProperty(EmailSettings.MAIL_ACCOUNT) + : settings.getProperty(EmailSettings.MAIL_ADDRESS); + + String senderName = StringUtils.isBlank(settings.getProperty(EmailSettings.MAIL_SENDER_NAME)) + ? senderMail + : settings.getProperty(EmailSettings.MAIL_SENDER_NAME); + String mailPassword = settings.getProperty(EmailSettings.MAIL_PASSWORD); + int port = settings.getProperty(EmailSettings.SMTP_PORT); + + HtmlEmail email = new HtmlEmail(); + email.setCharset(EmailConstants.UTF_8); + email.setSmtpPort(port); + email.setHostName(settings.getProperty(EmailSettings.SMTP_HOST)); + email.addTo(emailAddress); + email.setFrom(senderMail, senderName); + email.setSubject(settings.getProperty(EmailSettings.RECOVERY_MAIL_SUBJECT)); + email.setAuthentication(settings.getProperty(EmailSettings.MAIL_ACCOUNT), mailPassword); + if (settings.getProperty(PluginSettings.LOG_LEVEL).includes(LogLevel.DEBUG)) { + email.setDebug(true); + } + + setPropertiesForPort(email, port); + return email; + } + + /** + * Sets the given content to the HtmlEmail object and sends it. + * + * @param content the content to set + * @param email the email object to send + * @return true upon success, false otherwise + */ + public boolean sendEmail(String content, HtmlEmail email) { + Thread.currentThread().setContextClassLoader(SendMailSsl.class.getClassLoader()); + // Issue #999: Prevent UnsupportedDataTypeException: no object DCH for MIME type multipart/alternative + // cf. http://stackoverflow.com/questions/21856211/unsupporteddatatypeexception-no-object-dch-for-mime-type + MailcapCommandMap mc = (MailcapCommandMap) CommandMap.getDefaultCommandMap(); + mc.addMailcap("text/html;; x-java-content-handler=com.sun.mail.handlers.text_html"); + mc.addMailcap("text/xml;; x-java-content-handler=com.sun.mail.handlers.text_xml"); + mc.addMailcap("text/plain;; x-java-content-handler=com.sun.mail.handlers.text_plain"); + mc.addMailcap("multipart/*;; x-java-content-handler=com.sun.mail.handlers.multipart_mixed"); + mc.addMailcap("message/rfc822;; x-java-content- handler=com.sun.mail.handlers.message_rfc822"); + + try { + email.setHtmlMsg(content); + email.setTextMsg(content); + } catch (EmailException e) { + logger.logException("Your email.html config contains an error and cannot be sent:", e); + return false; + } + try { + email.send(); + return true; + } catch (EmailException e) { + logger.logException("Failed to send a mail to " + email.getToAddresses() + ":", e); + return false; + } + } + + /** + * Sets properties to the given HtmlEmail object based on the port from which it will be sent. + * + * @param email the email object to configure + * @param port the configured outgoing port + */ + private void setPropertiesForPort(HtmlEmail email, int port) throws EmailException { + switch (port) { + case 587: + String oAuth2Token = settings.getProperty(EmailSettings.OAUTH2_TOKEN); + if (!oAuth2Token.isEmpty()) { + if (Security.getProvider("Google OAuth2 Provider") == null) { + Security.addProvider(new OAuth2Provider()); + } + Properties mailProperties = email.getMailSession().getProperties(); + mailProperties.setProperty("mail.smtp.ssl.enable", "true"); + mailProperties.setProperty("mail.smtp.auth.mechanisms", "XOAUTH2"); + mailProperties.setProperty("mail.smtp.sasl.enable", "true"); + mailProperties.setProperty("mail.smtp.sasl.mechanisms", "XOAUTH2"); + mailProperties.setProperty("mail.smtp.auth.login.disable", "true"); + mailProperties.setProperty("mail.smtp.auth.plain.disable", "true"); + mailProperties.setProperty(OAuth2SaslClientFactory.OAUTH_TOKEN_PROP, oAuth2Token); + email.setMailSession(Session.getInstance(mailProperties)); + } else { + email.setStartTLSEnabled(true); + email.setStartTLSRequired(true); + } + break; + case 25: + if (settings.getProperty(EmailSettings.PORT25_USE_TLS)) { + email.setStartTLSEnabled(true); + email.setSSLCheckServerIdentity(true); + } + break; + case 465: + email.setSslSmtpPort(Integer.toString(port)); + email.setSSLOnConnect(true); + break; + default: + email.setStartTLSEnabled(true); + email.setSSLOnConnect(true); + email.setSSLCheckServerIdentity(true); + } + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/message/AbstractMessageFileHandler.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/message/AbstractMessageFileHandler.java new file mode 100644 index 00000000..8211d53c --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/message/AbstractMessageFileHandler.java @@ -0,0 +1,170 @@ +package fr.xephi.authme.message; + +import com.google.common.annotations.VisibleForTesting; +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.initialization.DataFolder; +import fr.xephi.authme.initialization.Reloadable; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.properties.PluginSettings; +import fr.xephi.authme.util.FileUtils; +import fr.xephi.authme.util.message.I18NUtils; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; + +import javax.annotation.PostConstruct; +import javax.inject.Inject; +import java.io.File; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import static fr.xephi.authme.message.MessagePathHelper.DEFAULT_LANGUAGE; + +/** + * Handles a YAML message file with a default file fallback. + */ +public abstract class AbstractMessageFileHandler implements Reloadable { + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(AbstractMessageFileHandler.class); + + @DataFolder + @Inject + private File dataFolder; + + @Inject + private Settings settings; + + private String filename; + private FileConfiguration configuration; + private Map i18nConfiguration; + private final String defaultFile; + + protected AbstractMessageFileHandler() { + this.defaultFile = createFilePath(DEFAULT_LANGUAGE); + } + + @Override + @PostConstruct + public void reload() { + String language = settings.getProperty(PluginSettings.MESSAGES_LANGUAGE); + filename = createFilePath(language); + File messagesFile = initializeFile(filename); + configuration = YamlConfiguration.loadConfiguration(messagesFile); + i18nConfiguration = null; + } + + protected String getLanguage() { + return settings.getProperty(PluginSettings.MESSAGES_LANGUAGE); + } + + protected File getUserLanguageFile() { + return new File(dataFolder, filename); + } + + protected String getFilename() { + return filename; + } + + /** + * Returns whether the message file configuration has an entry at the given path. + * + * @param path the path to verify + * @return true if an entry exists for the path in the messages file, false otherwise + */ + public boolean hasSection(String path) { + return configuration.get(path) != null; + } + + /** + * Returns the message for the given key. + * + * @param key the key to retrieve the message for + * @return the message + */ + public String getMessage(String key) { + String message = configuration.getString(key); + return message == null + ? "Error retrieving message '" + key + "'" + : message; + } + + /** + * Returns the i18n message for the given key and given locale. + * + * @param key the key to retrieve the message for + * @param locale the locale that player client setting uses + * @return the message + */ + public String getMessageByLocale(String key, String locale) { + if (locale == null || !settings.getProperty(PluginSettings.I18N_MESSAGES)) { + return getMessage(key); + } + + String message = getI18nConfiguration(locale).getString(key); + return message == null + ? "Error retrieving message '" + key + "'" + : message; + } + + /** + * Returns the message for the given key only if it exists, + * i.e. without falling back to the default file. + * + * @param key the key to retrieve the message for + * @return the message, or {@code null} if not available + */ + public String getMessageIfExists(String key) { + return configuration.getString(key); + } + + public FileConfiguration getI18nConfiguration(String locale) { + if (i18nConfiguration == null) { + i18nConfiguration = new ConcurrentHashMap<>(); + } + + locale = I18NUtils.localeToCode(locale, settings); + + if (i18nConfiguration.containsKey(locale)) { + return i18nConfiguration.get(locale); + } else { + // Sync with reload(); + String i18nFilename = createFilePath(locale); + File i18nMessagesFile = initializeFile(i18nFilename); + FileConfiguration config = YamlConfiguration.loadConfiguration(i18nMessagesFile); + + i18nConfiguration.put(locale, config); + + return config; + } + } + + /** + * Creates the path to the messages file for the given language code. + * + * @param language the language code + * @return path to the message file for the given language + */ + protected abstract String createFilePath(String language); + + /** + * Copies the messages file from the JAR to the local messages/ folder if it doesn't exist. + * + * @param filePath path to the messages file to use + * @return the messages file to use + */ + @VisibleForTesting + File initializeFile(String filePath) { + File file = new File(dataFolder, filePath); + // Check that JAR file exists to avoid logging an error + if (FileUtils.getResourceFromJar(filePath) != null && FileUtils.copyFileFromResource(file, filePath)) { + return file; + } + + if (FileUtils.copyFileFromResource(file, defaultFile)) { + return file; + } else { + logger.warning("Wanted to copy default messages file '" + defaultFile + "' from JAR but it didn't exist"); + return null; + } + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/message/HelpMessagesFileHandler.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/message/HelpMessagesFileHandler.java new file mode 100644 index 00000000..89dc5224 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/message/HelpMessagesFileHandler.java @@ -0,0 +1,67 @@ +package fr.xephi.authme.message; + +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.util.FileUtils; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; + +import javax.inject.Inject; +import java.io.InputStream; +import java.io.InputStreamReader; + +import static fr.xephi.authme.message.MessagePathHelper.DEFAULT_LANGUAGE; + +/** + * File handler for the help_xx.yml resource. + */ +public class HelpMessagesFileHandler extends AbstractMessageFileHandler { + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(HelpMessagesFileHandler.class); + + private FileConfiguration defaultConfiguration; + + @Inject // Trigger injection in the superclass + HelpMessagesFileHandler() { + } + + /** + * Returns the message for the given key. + * + * @param key the key to retrieve the message for + * @return the message + */ + @Override + public String getMessage(String key) { + String message = getMessageIfExists(key); + + if (message == null) { + logger.warning("Error getting message with key '" + key + "'. " + + "Please update your config file '" + getFilename() + "' or run /authme messages help"); + return getDefault(key); + } + return message; + } + + /** + * Gets the message from the default file. + * + * @param key the key to retrieve the message for + * @return the message from the default file + */ + private String getDefault(String key) { + if (defaultConfiguration == null) { + InputStream stream = FileUtils.getResourceFromJar(createFilePath(DEFAULT_LANGUAGE)); + defaultConfiguration = YamlConfiguration.loadConfiguration(new InputStreamReader(stream)); + } + String message = defaultConfiguration.getString(key); + return message == null + ? "Error retrieving message '" + key + "'" + : message; + } + + @Override + protected String createFilePath(String language) { + return MessagePathHelper.createHelpMessageFilePath(language); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/message/MessageKey.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/message/MessageKey.java new file mode 100644 index 00000000..ab05738d --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/message/MessageKey.java @@ -0,0 +1,399 @@ +package fr.xephi.authme.message; + +/** + * Keys for translatable messages managed by {@link Messages}. + */ +public enum MessageKey { + /** + * You have been disconnected due to doubled login. + */ + DOUBLE_LOGIN_FIX("double_login_fix.fix_message"), + + /** + * You are stuck in portal during Login. + */ + LOCATION_FIX_PORTAL("login_location_fix.fix_portal"), + + /** + * You are stuck underground during Login. + */ + LOCATION_FIX_UNDERGROUND("login_location_fix.fix_underground"), + + /** + * You are stuck underground during Login, but we cant fix it. + */ + LOCATION_FIX_UNDERGROUND_CANT_FIX("login_location_fix.cannot_fix_underground"), + + /** + * Bedrock auto login success! + */ + BEDROCK_AUTO_LOGGED_IN("bedrock_auto_login.success"), + + /** + * In order to use this command you must be authenticated! + */ + DENIED_COMMAND("error.denied_command"), + + /** + * A player with the same IP is already in game! + */ + SAME_IP_ONLINE("on_join_validation.same_ip_online"), + + /** + * In order to chat you must be authenticated! + */ + DENIED_CHAT("error.denied_chat"), + + /** AntiBot protection mode is enabled! You have to wait some minutes before joining the server. */ + KICK_ANTIBOT("antibot.kick_antibot"), + + /** This user isn't registered! */ + UNKNOWN_USER("error.unregistered_user"), + + /** You're not logged in! */ + NOT_LOGGED_IN("error.not_logged_in"), + + /** Usage: /login <password> */ + USAGE_LOGIN("login.command_usage"), + + /** Wrong password! */ + WRONG_PASSWORD("login.wrong_password"), + + /** Successfully unregistered! */ + UNREGISTERED_SUCCESS("unregister.success"), + + /** In-game registration is disabled! */ + REGISTRATION_DISABLED("registration.disabled"), + + /** Logged-in due to Session Reconnection. */ + SESSION_RECONNECTION("session.valid_session"), + + /** Successful login! */ + LOGIN_SUCCESS("login.success"), + + /** Your account isn't activated yet, please check your emails! */ + ACCOUNT_NOT_ACTIVATED("misc.account_not_activated"), + + /** You already have registered this username! */ + NAME_ALREADY_REGISTERED("registration.name_taken"), + + /** You don't have the permission to perform this action! */ + NO_PERMISSION("error.no_permission"), + + /** An unexpected error occurred, please contact an administrator! */ + ERROR("error.unexpected_error"), + + /** Please, login with the command: /login <password> */ + LOGIN_MESSAGE("login.login_request"), + + /** Please, register to the server with the command: /register <password> <ConfirmPassword> */ + REGISTER_MESSAGE("registration.register_request"), + + /** You have exceeded the maximum number of registrations (%reg_count/%max_acc %reg_names) for your connection! */ + MAX_REGISTER_EXCEEDED("error.max_registration", "%max_acc", "%reg_count", "%reg_names"), + + /** Usage: /register <password> <ConfirmPassword> */ + USAGE_REGISTER("registration.command_usage"), + + /** Usage: /unregister <password> */ + USAGE_UNREGISTER("unregister.command_usage"), + + /** Password changed successfully! */ + PASSWORD_CHANGED_SUCCESS("misc.password_changed"), + + /** Passwords didn't match, check them again! */ + PASSWORD_MATCH_ERROR("password.match_error"), + + /** You can't use your name as password, please choose another one... */ + PASSWORD_IS_USERNAME_ERROR("password.name_in_password"), + + /** The chosen password isn't safe, please choose another one... */ + PASSWORD_UNSAFE_ERROR("password.unsafe_password"), + + /** Your chosen password is not secure. It was used %pwned_count times already! Please use a stronger password... */ + PASSWORD_PWNED_ERROR("password.pwned_password", "%pwned_count"), + + /** Your password contains illegal characters. Allowed chars: %valid_chars */ + PASSWORD_CHARACTERS_ERROR("password.forbidden_characters", "%valid_chars"), + + /** Your IP has been changed and your session data has expired! */ + SESSION_EXPIRED("session.invalid_session"), + + /** Only registered users can join the server! Please visit http://example.com to register yourself! */ + MUST_REGISTER_MESSAGE("registration.reg_only"), + + /** You're already logged in! */ + ALREADY_LOGGED_IN_ERROR("error.logged_in"), + + /** Logged out successfully! */ + LOGOUT_SUCCESS("misc.logout"), + + /** The same username is already playing on the server! */ + USERNAME_ALREADY_ONLINE_ERROR("on_join_validation.same_nick_online"), + + /** Successfully registered! */ + REGISTER_SUCCESS("registration.success"), + + /** Your password is too short or too long! Please try with another one! */ + INVALID_PASSWORD_LENGTH("password.wrong_length"), + + /** Configuration and database have been reloaded correctly! */ + CONFIG_RELOAD_SUCCESS("misc.reload"), + + /** Login timeout exceeded, you have been kicked from the server, please try again! */ + LOGIN_TIMEOUT_ERROR("login.timeout_error"), + + /** Usage: /changepassword <oldPassword> <newPassword> */ + USAGE_CHANGE_PASSWORD("misc.usage_change_password"), + + /** Your username is either too short or too long! */ + INVALID_NAME_LENGTH("on_join_validation.name_length"), + + /** Your username contains illegal characters. Allowed chars: %valid_chars */ + INVALID_NAME_CHARACTERS("on_join_validation.characters_in_name", "%valid_chars"), + + /** Please add your email to your account with the command: /email add <yourEmail> <confirmEmail> */ + ADD_EMAIL_MESSAGE("email.add_email_request"), + + /** Forgot your password? Please use the command: /email recovery <yourEmail> */ + FORGOT_PASSWORD_MESSAGE("recovery.forgot_password_hint"), + + /** To log in you have to solve a captcha code, please use the command: /captcha %captcha_code */ + USAGE_CAPTCHA("captcha.usage_captcha", "%captcha_code"), + + /** Wrong captcha, please type "/captcha %captcha_code" into the chat! */ + CAPTCHA_WRONG_ERROR("captcha.wrong_captcha", "%captcha_code"), + + /** Captcha code solved correctly! */ + CAPTCHA_SUCCESS("captcha.valid_captcha"), + + /** To register you have to solve a captcha first, please use the command: /captcha %captcha_code */ + CAPTCHA_FOR_REGISTRATION_REQUIRED("captcha.captcha_for_registration", "%captcha_code"), + + /** Valid captcha! You may now register with /register */ + REGISTER_CAPTCHA_SUCCESS("captcha.register_captcha_valid"), + + /** A VIP player has joined the server when it was full! */ + KICK_FOR_VIP("error.kick_for_vip"), + + /** The server is full, try again later! */ + KICK_FULL_SERVER("on_join_validation.kick_full_server"), + + /** An error occurred: unresolved player hostname! **/ + KICK_UNRESOLVED_HOSTNAME("error.kick_unresolved_hostname"), + + /** Usage: /email add <email> <confirmEmail> */ + USAGE_ADD_EMAIL("email.usage_email_add"), + + /** Usage: /email change <oldEmail> <newEmail> */ + USAGE_CHANGE_EMAIL("email.usage_email_change"), + + /** Usage: /email recovery <Email> */ + USAGE_RECOVER_EMAIL("recovery.command_usage"), + + /** Invalid new email, try again! */ + INVALID_NEW_EMAIL("email.new_email_invalid"), + + /** Invalid old email, try again! */ + INVALID_OLD_EMAIL("email.old_email_invalid"), + + /** Invalid email address, try again! */ + INVALID_EMAIL("email.invalid"), + + /** Email address successfully added to your account! */ + EMAIL_ADDED_SUCCESS("email.added"), + + /** Adding email was not allowed */ + EMAIL_ADD_NOT_ALLOWED("email.add_not_allowed"), + + /** Please confirm your email address! */ + CONFIRM_EMAIL_MESSAGE("email.request_confirmation"), + + /** Email address changed correctly! */ + EMAIL_CHANGED_SUCCESS("email.changed"), + + /** Changing email was not allowed */ + EMAIL_CHANGE_NOT_ALLOWED("email.change_not_allowed"), + + /** Your current email address is: %email */ + EMAIL_SHOW("email.email_show", "%email"), + + /** You currently don't have email address associated with this account. */ + SHOW_NO_EMAIL("email.no_email_for_account"), + + /** Recovery email sent successfully! Please check your email inbox! */ + RECOVERY_EMAIL_SENT_MESSAGE("recovery.email_sent"), + + /** Your country is banned from this server! */ + COUNTRY_BANNED_ERROR("on_join_validation.country_banned"), + + /** [AntiBotService] AntiBot enabled due to the huge number of connections! */ + ANTIBOT_AUTO_ENABLED_MESSAGE("antibot.auto_enabled"), + + /** [AntiBotService] AntiBot disabled after %m minutes! */ + ANTIBOT_AUTO_DISABLED_MESSAGE("antibot.auto_disabled", "%m"), + + /** The email address is already being used */ + EMAIL_ALREADY_USED_ERROR("email.already_used"), + + /** Your secret code is %code. You can scan it from here %url */ + TWO_FACTOR_CREATE("two_factor.code_created", "%code", "%url"), + + /** Please confirm your code with /2fa confirm <code> */ + TWO_FACTOR_CREATE_CONFIRMATION_REQUIRED("two_factor.confirmation_required"), + + /** Please submit your two-factor authentication code with /2fa code <code> */ + TWO_FACTOR_CODE_REQUIRED("two_factor.code_required"), + + /** Two-factor authentication is already enabled for your account! */ + TWO_FACTOR_ALREADY_ENABLED("two_factor.already_enabled"), + + /** No 2fa key has been generated for you or it has expired. Please run /2fa add */ + TWO_FACTOR_ENABLE_ERROR_NO_CODE("two_factor.enable_error_no_code"), + + /** Successfully enabled two-factor authentication for your account */ + TWO_FACTOR_ENABLE_SUCCESS("two_factor.enable_success"), + + /** Wrong code or code has expired. Please run /2fa add */ + TWO_FACTOR_ENABLE_ERROR_WRONG_CODE("two_factor.enable_error_wrong_code"), + + /** Two-factor authentication is not enabled for your account. Run /2fa add */ + TWO_FACTOR_NOT_ENABLED_ERROR("two_factor.not_enabled_error"), + + /** Successfully removed two-factor auth from your account */ + TWO_FACTOR_REMOVED_SUCCESS("two_factor.removed_success"), + + /** Invalid code! */ + TWO_FACTOR_INVALID_CODE("two_factor.invalid_code"), + + /** You are not the owner of this account. Please choose another name! */ + NOT_OWNER_ERROR("on_join_validation.not_owner_error"), + + /** You should join using username %valid, not %invalid. */ + INVALID_NAME_CASE("on_join_validation.invalid_name_case", "%valid", "%invalid"), + + /** You have been temporarily banned for failing to log in too many times. */ + TEMPBAN_MAX_LOGINS("error.tempban_max_logins"), + + /** You own %count accounts: */ + ACCOUNTS_OWNED_SELF("misc.accounts_owned_self", "%count"), + + /** The player %name has %count accounts: */ + ACCOUNTS_OWNED_OTHER("misc.accounts_owned_other", "%name", "%count"), + + /** An admin just registered you; please log in again */ + KICK_FOR_ADMIN_REGISTER("registration.kicked_admin_registered"), + + /** Error: not all required settings are set for sending emails. Please contact an admin. */ + INCOMPLETE_EMAIL_SETTINGS("email.incomplete_settings"), + + /** The email could not be sent. Please contact an administrator. */ + EMAIL_SEND_FAILURE("email.send_failure"), + + /** A recovery code to reset your password has been sent to your email. */ + RECOVERY_CODE_SENT("recovery.code.code_sent"), + + /** The recovery code is not correct! You have %count tries remaining. */ + INCORRECT_RECOVERY_CODE("recovery.code.incorrect", "%count"), + + /** + * You have exceeded the maximum number of attempts to enter the recovery code. + * Use "/email recovery [email]" to generate a new one. + */ + RECOVERY_TRIES_EXCEEDED("recovery.code.tries_exceeded"), + + /** Recovery code entered correctly! */ + RECOVERY_CODE_CORRECT("recovery.code.correct"), + + /** Please use the command /email setpassword to change your password immediately. */ + RECOVERY_CHANGE_PASSWORD("recovery.code.change_password"), + + /** You cannot change your password using this command anymore. */ + CHANGE_PASSWORD_EXPIRED("email.change_password_expired"), + + /** An email was already sent recently. You must wait %time before you can send a new one. */ + EMAIL_COOLDOWN_ERROR("email.email_cooldown_error", "%time"), + + /** + * This command is sensitive and requires an email verification! + * Check your inbox and follow the email's instructions. + */ + VERIFICATION_CODE_REQUIRED("verification.code_required"), + + /** Usage: /verification <code> */ + USAGE_VERIFICATION_CODE("verification.command_usage"), + + /** Incorrect code, please type "/verification <code>" into the chat, using the code you received by email */ + INCORRECT_VERIFICATION_CODE("verification.incorrect_code"), + + /** Your identity has been verified! You can now execute all commands within the current session! */ + VERIFICATION_CODE_VERIFIED("verification.success"), + + /** You can already execute every sensitive command within the current session! */ + VERIFICATION_CODE_ALREADY_VERIFIED("verification.already_verified"), + + /** Your code has expired! Execute another sensitive command to get a new code! */ + VERIFICATION_CODE_EXPIRED("verification.code_expired"), + + /** To verify your identity you need to link an email address with your account! */ + VERIFICATION_CODE_EMAIL_NEEDED("verification.email_needed"), + + /** You used a command too fast! Please, join the server again and wait more before using any command. */ + QUICK_COMMAND_PROTECTION_KICK("on_join_validation.quick_command"), + + /** second */ + SECOND("time.second"), + + /** seconds */ + SECONDS("time.seconds"), + + /** minute */ + MINUTE("time.minute"), + + /** minutes */ + MINUTES("time.minutes"), + + /** hour */ + HOUR("time.hour"), + + /** hours */ + HOURS("time.hours"), + + /** day */ + DAY("time.day"), + + /** days */ + DAYS("time.days"); + + + private String key; + private String[] tags; + + MessageKey(String key, String... tags) { + this.key = key; + this.tags = tags; + } + + /** + * Return the key used in the messages file. + * + * @return The key + */ + public String getKey() { + return key; + } + + /** + * Return a list of tags (texts) that are replaced with actual content in AuthMe. + * + * @return List of tags + */ + public String[] getTags() { + return tags; + } + + @Override + public String toString() { + return key; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/message/MessagePathHelper.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/message/MessagePathHelper.java new file mode 100644 index 00000000..82c829d1 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/message/MessagePathHelper.java @@ -0,0 +1,77 @@ +package fr.xephi.authme.message; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Helper for creating and processing paths to message files. + */ +public final class MessagePathHelper { + + /** The default language (used as fallback, assumed to be complete, etc.). */ + public static final String DEFAULT_LANGUAGE = "en"; + /** Local path to the folder containing the message files. */ + public static final String MESSAGES_FOLDER = "messages/"; + /** Local path to the default messages file (messages/messages_en.yml). */ + public static final String DEFAULT_MESSAGES_FILE = createMessageFilePath(DEFAULT_LANGUAGE); + + private static final Pattern MESSAGE_FILE_PATTERN = Pattern.compile("messages_([a-z]+)\\.yml"); + private static final Pattern HELP_MESSAGES_FILE = Pattern.compile("help_[a-z]+\\.yml"); + + private MessagePathHelper() { + } + + /** + * Creates the local path to the messages file for the provided language code. + * + * @param languageCode the language code + * @return local path to the messages file of the given language + */ + public static String createMessageFilePath(String languageCode) { + return "messages/messages_" + languageCode + ".yml"; + } + + /** + * Creates the local path to the help messages file for the provided language code. + * + * @param languageCode the language code + * @return local path to the help messages file of the given language + */ + public static String createHelpMessageFilePath(String languageCode) { + return "messages/help_" + languageCode + ".yml"; + } + + /** + * Returns whether the given file name is a messages file. + * + * @param filename the file name to test + * @return true if it is a messages file, false otherwise + */ + public static boolean isMessagesFile(String filename) { + return MESSAGE_FILE_PATTERN.matcher(filename).matches(); + } + + /** + * Returns the language code the given file name is for if it is a messages file, otherwise null is returned. + * + * @param filename the file name to process + * @return the language code the file name is a messages file for, or null if not applicable + */ + public static String getLanguageIfIsMessagesFile(String filename) { + Matcher matcher = MESSAGE_FILE_PATTERN.matcher(filename); + if (matcher.matches()) { + return matcher.group(1); + } + return null; + } + + /** + * Returns whether the given file name is a help messages file. + * + * @param filename the file name to test + * @return true if it is a help messages file, false otherwise + */ + public static boolean isHelpFile(String filename) { + return HELP_MESSAGES_FILE.matcher(filename).matches(); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/message/Messages.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/message/Messages.java new file mode 100644 index 00000000..0d7ec317 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/message/Messages.java @@ -0,0 +1,201 @@ +package fr.xephi.authme.message; + +import com.google.common.collect.ImmutableMap; +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.mail.EmailService; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.util.expiring.Duration; +import fr.xephi.authme.util.message.I18NUtils; +import fr.xephi.authme.util.message.MiniMessageUtils; +import org.bukkit.ChatColor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import javax.inject.Inject; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * Class for retrieving and sending translatable messages to players. + */ +public class Messages { + + // Custom Authme tag replaced to new line + private static final String NEWLINE_TAG = "%nl%"; + + // Global tag replacements + private static final String USERNAME_TAG = "%username%"; + private static final String DISPLAYNAME_TAG = "%displayname%"; + + /** Contains the keys of the singular messages for time units. */ + private static final Map TIME_UNIT_SINGULARS = ImmutableMap.builder() + .put(TimeUnit.SECONDS, MessageKey.SECOND) + .put(TimeUnit.MINUTES, MessageKey.MINUTE) + .put(TimeUnit.HOURS, MessageKey.HOUR) + .put(TimeUnit.DAYS, MessageKey.DAY).build(); + + /** Contains the keys of the plural messages for time units. */ + private static final Map TIME_UNIT_PLURALS = ImmutableMap.builder() + .put(TimeUnit.SECONDS, MessageKey.SECONDS) + .put(TimeUnit.MINUTES, MessageKey.MINUTES) + .put(TimeUnit.HOURS, MessageKey.HOURS) + .put(TimeUnit.DAYS, MessageKey.DAYS).build(); + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(EmailService.class); + + private MessagesFileHandler messagesFileHandler; + + /* + * Constructor. + */ + @Inject + Messages(MessagesFileHandler messagesFileHandler) { + this.messagesFileHandler = messagesFileHandler; + } + + /** + * Send the given message code to the player. + * + * @param sender The entity to send the message to + * @param key The key of the message to send + */ + public void send(CommandSender sender, MessageKey key) { + String[] lines = retrieve(key, sender); + for (String line : lines) { + sender.sendMessage(line); + } + } + + /** + * Send the given message code to the player with the given tag replacements. Note that this method + * logs an error if the number of supplied replacements doesn't correspond to the number of tags + * the message key contains. + * + * @param sender The entity to send the message to + * @param key The key of the message to send + * @param replacements The replacements to apply for the tags + */ + public void send(CommandSender sender, MessageKey key, String... replacements) { + String message = retrieveSingle(sender, key, replacements); + for (String line : message.split("\n")) { + sender.sendMessage(line); + } + } + + /** + * Retrieve the message from the text file and return it split by new line as an array. + * + * @param key The message key to retrieve + * @param sender The entity to send the message to + * @return The message split by new lines + */ + public String[] retrieve(MessageKey key, CommandSender sender) { + String message = retrieveMessage(key, sender); + if (message.isEmpty()) { + // Return empty array instead of array with 1 empty string as entry + return new String[0]; + } + return message.split("\n"); + } + + /** + * Returns the textual representation for the given duration. + * Note that this class only supports the time units days, hour, minutes and seconds. + * + * @param duration the duration to build a text of + * @return text of the duration + */ + public String formatDuration(Duration duration) { + long value = duration.getDuration(); + MessageKey timeUnitKey = value == 1 + ? TIME_UNIT_SINGULARS.get(duration.getTimeUnit()) + : TIME_UNIT_PLURALS.get(duration.getTimeUnit()); + + return value + " " + retrieveMessage(timeUnitKey, ""); + } + + /** + * Retrieve the message from the text file. + * + * @param key The message key to retrieve + * @param sender The entity to send the message to + * @return The message from the file + */ + private String retrieveMessage(MessageKey key, CommandSender sender) { + String locale = sender instanceof Player + ? I18NUtils.getLocale((Player) sender) + : null; + String message = messagesFileHandler.getMessageByLocale(key.getKey(), locale); + String displayName = sender.getName(); + if (sender instanceof Player) { + displayName = ((Player) sender).getDisplayName(); + } + + return ChatColor.translateAlternateColorCodes('&', MiniMessageUtils.parseMiniMessageToLegacy(message)) + .replace(NEWLINE_TAG, "\n") + .replace(USERNAME_TAG, sender.getName()) + .replace(DISPLAYNAME_TAG, displayName); + } + + /** + * Retrieve the message from the text file. + * + * @param key The message key to retrieve + * @param name The name of the entity to send the message to + * @return The message from the file + */ + private String retrieveMessage(MessageKey key, String name) { + String message = messagesFileHandler.getMessage(key.getKey()); + + return ChatColor.translateAlternateColorCodes('&', MiniMessageUtils.parseMiniMessageToLegacy(message)) + .replace(NEWLINE_TAG, "\n") + .replace(USERNAME_TAG, name) + .replace(DISPLAYNAME_TAG, name); + } + + /** + * Retrieve the given message code with the given tag replacements. Note that this method + * logs an error if the number of supplied replacements doesn't correspond to the number of tags + * the message key contains. + * + * @param sender The entity to send the message to + * @param key The key of the message to send + * @param replacements The replacements to apply for the tags + * @return The message from the file with replacements + */ + public String retrieveSingle(CommandSender sender, MessageKey key, String... replacements) { + String message = retrieveMessage(key, sender); + String[] tags = key.getTags(); + if (replacements.length == tags.length) { + for (int i = 0; i < tags.length; ++i) { + message = message.replace(tags[i], replacements[i]); + } + } else { + logger.warning("Invalid number of replacements for message key '" + key + "'"); + } + return message; + } + + /** + * Retrieve the given message code with the given tag replacements. Note that this method + * logs an error if the number of supplied replacements doesn't correspond to the number of tags + * the message key contains. + * + * @param name The name of the entity to send the message to + * @param key The key of the message to send + * @param replacements The replacements to apply for the tags + * @return The message from the file with replacements + */ + public String retrieveSingle(String name, MessageKey key, String... replacements) { + String message = retrieveMessage(key, name); + String[] tags = key.getTags(); + if (replacements.length == tags.length) { + for (int i = 0; i < tags.length; ++i) { + message = message.replace(tags[i], replacements[i]); + } + } else { + logger.warning("Invalid number of replacements for message key '" + key + "'"); + } + return message; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/message/MessagesFileHandler.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/message/MessagesFileHandler.java new file mode 100644 index 00000000..b62be93f --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/message/MessagesFileHandler.java @@ -0,0 +1,48 @@ +package fr.xephi.authme.message; + +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.message.updater.MessageUpdater; +import fr.xephi.authme.output.ConsoleLoggerFactory; + +import javax.inject.Inject; + +import static fr.xephi.authme.message.MessagePathHelper.DEFAULT_LANGUAGE; + +/** + * File handler for the messages_xx.yml resource. + */ +public class MessagesFileHandler extends AbstractMessageFileHandler { + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(MessagesFileHandler.class); + + @Inject + private MessageUpdater messageUpdater; + + MessagesFileHandler() { + } + + @Override + public void reload() { + reloadInternal(false); + } + + private void reloadInternal(boolean isFromReload) { + super.reload(); + + String language = getLanguage(); + boolean hasChange = messageUpdater.migrateAndSave( + getUserLanguageFile(), createFilePath(language), createFilePath(DEFAULT_LANGUAGE)); + if (hasChange) { + if (isFromReload) { + logger.warning("Migration after reload attempt"); + } else { + reloadInternal(true); + } + } + } + + @Override + protected String createFilePath(String language) { + return MessagePathHelper.createMessageFilePath(language); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/message/updater/JarMessageSource.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/message/updater/JarMessageSource.java new file mode 100644 index 00000000..73075f4e --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/message/updater/JarMessageSource.java @@ -0,0 +1,59 @@ +package fr.xephi.authme.message.updater; + +import ch.jalu.configme.properties.Property; +import ch.jalu.configme.resource.PropertyReader; +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.util.FileUtils; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Returns messages from the JAR's message files. Favors a local JAR (e.g. messages_nl.yml) + * before falling back to the default language (messages_en.yml). + */ +public class JarMessageSource { + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(JarMessageSource.class); + private final PropertyReader localJarMessages; + private final PropertyReader defaultJarMessages; + + /** + * Constructor. + * + * @param localJarPath path to the messages file of the language the plugin is configured to use (may not exist) + * @param defaultJarPath path to the default messages file in the JAR (must exist) + */ + public JarMessageSource(String localJarPath, String defaultJarPath) { + localJarMessages = localJarPath.equals(defaultJarPath) ? null : loadJarFile(localJarPath); + defaultJarMessages = loadJarFile(defaultJarPath); + + if (defaultJarMessages == null) { + throw new IllegalStateException("Default JAR file '" + defaultJarPath + "' could not be loaded"); + } + } + + public String getMessageFromJar(Property property) { + String key = property.getPath(); + String message = getString(key, localJarMessages); + return message == null ? getString(key, defaultJarMessages) : message; + } + + private static String getString(String path, PropertyReader reader) { + return reader == null ? null : reader.getString(path); + } + + private MessageMigraterPropertyReader loadJarFile(String jarPath) { + try (InputStream stream = FileUtils.getResourceFromJar(jarPath)) { + if (stream == null) { + logger.debug("Could not load '" + jarPath + "' from JAR"); + return null; + } + return MessageMigraterPropertyReader.loadFromStream(stream); + } catch (IOException e) { + logger.logException("Exception while handling JAR path '" + jarPath + "'", e); + } + return null; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/message/updater/MessageKeyConfigurationData.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/message/updater/MessageKeyConfigurationData.java new file mode 100644 index 00000000..cf3c1c78 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/message/updater/MessageKeyConfigurationData.java @@ -0,0 +1,53 @@ +package fr.xephi.authme.message.updater; + +import ch.jalu.configme.configurationdata.ConfigurationDataImpl; +import ch.jalu.configme.properties.Property; +import ch.jalu.configme.properties.convertresult.PropertyValue; +import ch.jalu.configme.resource.PropertyReader; +import fr.xephi.authme.message.MessageKey; + +import java.util.List; +import java.util.Map; + +public class MessageKeyConfigurationData extends ConfigurationDataImpl { + + /** + * Constructor. + * + * @param propertyListBuilder property list builder for message key properties + * @param allComments registered comments + */ + public MessageKeyConfigurationData(MessageUpdater.MessageKeyPropertyListBuilder propertyListBuilder, + Map> allComments) { + super(propertyListBuilder.getAllProperties(), allComments); + } + + @Override + public void initializeValues(PropertyReader reader) { + for (Property property : getAllMessageProperties()) { + PropertyValue value = property.determineValue(reader); + if (value.isValidInResource()) { + setValue(property, value.getValue()); + } + } + } + + @Override + public T getValue(Property property) { + // Override to silently return null if property is unknown + return (T) getValues().get(property.getPath()); + } + + @SuppressWarnings("unchecked") + public List> getAllMessageProperties() { + return (List) getProperties(); + } + + public String getMessage(MessageKey messageKey) { + return getValue(new MessageUpdater.MessageKeyProperty(messageKey)); + } + + public void setMessage(MessageKey messageKey, String message) { + setValue(new MessageUpdater.MessageKeyProperty(messageKey), message); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/message/updater/MessageMigraterPropertyReader.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/message/updater/MessageMigraterPropertyReader.java new file mode 100644 index 00000000..a994df89 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/message/updater/MessageMigraterPropertyReader.java @@ -0,0 +1,126 @@ +package fr.xephi.authme.message.updater; + +import ch.jalu.configme.exception.ConfigMeException; +import ch.jalu.configme.resource.PropertyReader; +import org.yaml.snakeyaml.Yaml; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Implementation of {@link PropertyReader} which can read a file or a stream with + * a specified charset. + */ +final class MessageMigraterPropertyReader implements PropertyReader { + + private static final Charset CHARSET = StandardCharsets.UTF_8; + + private Map root; + + private MessageMigraterPropertyReader(Map valuesMap) { + root = valuesMap; + } + + /** + * Creates a new property reader for the given file. + * + * @param file the file to load + * @return the created property reader + */ + public static MessageMigraterPropertyReader loadFromFile(File file) { + try (InputStream is = new FileInputStream(file)) { + return loadFromStream(is); + } catch (IOException e) { + throw new IllegalStateException("Error while reading file '" + file + "'", e); + } + } + + public static MessageMigraterPropertyReader loadFromStream(InputStream inputStream) { + Map valuesMap = readStreamToMap(inputStream); + return new MessageMigraterPropertyReader(valuesMap); + } + + @Override + public boolean contains(String path) { + return getObject(path) != null; + } + + @Override + public Set getKeys(boolean b) { + throw new UnsupportedOperationException(); + } + + @Override + public Set getChildKeys(String s) { + throw new UnsupportedOperationException(); + } + + @Override + public Object getObject(String path) { + if (path.isEmpty()) { + return root.get(""); + } + Object node = root; + String[] keys = path.split("\\."); + for (String key : keys) { + node = getIfIsMap(key, node); + if (node == null) { + return null; + } + } + return node; + } + + @Override + public String getString(String path) { + Object o = getObject(path); + return o instanceof String ? (String) o : null; + } + + @Override + public Integer getInt(String path) { + throw new UnsupportedOperationException(); + } + + @Override + public Double getDouble(String path) { + throw new UnsupportedOperationException(); + } + + @Override + public Boolean getBoolean(String path) { + throw new UnsupportedOperationException(); + } + + @Override + public List getList(String path) { + throw new UnsupportedOperationException(); + } + + private static Map readStreamToMap(InputStream inputStream) { + try (InputStreamReader isr = new InputStreamReader(inputStream, CHARSET)) { + Object obj = new Yaml().load(isr); + return obj == null ? new HashMap<>() : (Map) obj; + } catch (IOException e) { + throw new ConfigMeException("Could not read stream", e); + } catch (ClassCastException e) { + throw new ConfigMeException("Top-level is not a map", e); + } + } + + private static Object getIfIsMap(String key, Object value) { + if (value instanceof Map) { + return ((Map) value).get(key); + } + return null; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/message/updater/MessageUpdater.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/message/updater/MessageUpdater.java new file mode 100644 index 00000000..f3f718c8 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/message/updater/MessageUpdater.java @@ -0,0 +1,202 @@ +package fr.xephi.authme.message.updater; + +import ch.jalu.configme.configurationdata.ConfigurationData; +import ch.jalu.configme.configurationdata.PropertyListBuilder; +import ch.jalu.configme.properties.Property; +import ch.jalu.configme.properties.StringProperty; +import ch.jalu.configme.properties.convertresult.ConvertErrorRecorder; +import ch.jalu.configme.resource.PropertyReader; +import ch.jalu.configme.resource.PropertyResource; +import com.google.common.collect.ImmutableMap; +import com.google.common.io.Files; +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.util.FileUtils; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import static java.util.Collections.singletonList; + +/** + * Migrates the used messages file to a complete, up-to-date version when necessary. + */ +public class MessageUpdater { + + private ConsoleLogger logger = ConsoleLoggerFactory.get(MessageUpdater.class); + + /** + * Applies any necessary migrations to the user's messages file and saves it if it has been modified. + * + * @param userFile the user's messages file (yml file in the plugin's folder) + * @param localJarPath path to the messages file in the JAR for the same language (may not exist) + * @param defaultJarPath path to the messages file in the JAR for the default language + * @return true if the file has been migrated and saved, false if it is up-to-date + */ + public boolean migrateAndSave(File userFile, String localJarPath, String defaultJarPath) { + JarMessageSource jarMessageSource = new JarMessageSource(localJarPath, defaultJarPath); + return migrateAndSave(userFile, jarMessageSource); + } + + /** + * Performs the migration. + * + * @param userFile the file to verify and migrate + * @param jarMessageSource jar message source to get texts from if missing + * @return true if the file has been migrated and saved, false if it is up-to-date + */ + private boolean migrateAndSave(File userFile, JarMessageSource jarMessageSource) { + // YamlConfiguration escapes all special characters when saving, making the file hard to use, so use ConfigMe + MessageKeyConfigurationData configurationData = createConfigurationData(); + PropertyResource userResource = new MigraterYamlFileResource(userFile); + + PropertyReader reader = userResource.createReader(); + configurationData.initializeValues(reader); + + // Step 1: Migrate any old keys in the file to the new paths + boolean movedOldKeys = migrateOldKeys(reader, configurationData); + // Step 2: Perform newer migrations + boolean movedNewerKeys = migrateKeys(reader, configurationData); + // Step 3: Take any missing messages from the message files shipped in the AuthMe JAR + boolean addedMissingKeys = addMissingKeys(jarMessageSource, configurationData); + + if (movedOldKeys || movedNewerKeys || addedMissingKeys) { + backupMessagesFile(userFile); + + userResource.exportProperties(configurationData); + logger.debug("Successfully saved {0}", userFile); + return true; + } + return false; + } + + private boolean migrateKeys(PropertyReader propertyReader, MessageKeyConfigurationData configurationData) { + return moveIfApplicable(propertyReader, configurationData, + "misc.two_factor_create", MessageKey.TWO_FACTOR_CREATE); + } + + private static boolean moveIfApplicable(PropertyReader reader, MessageKeyConfigurationData configurationData, + String oldPath, MessageKey messageKey) { + if (configurationData.getMessage(messageKey) == null && reader.getString(oldPath) != null) { + configurationData.setMessage(messageKey, reader.getString(oldPath)); + return true; + } + return false; + } + + private boolean migrateOldKeys(PropertyReader propertyReader, MessageKeyConfigurationData configurationData) { + boolean hasChange = OldMessageKeysMigrater.migrateOldPaths(propertyReader, configurationData); + if (hasChange) { + logger.info("Old keys have been moved to the new ones in your messages_xx.yml file"); + } + return hasChange; + } + + private boolean addMissingKeys(JarMessageSource jarMessageSource, MessageKeyConfigurationData configurationData) { + List addedKeys = new ArrayList<>(); + for (Property property : configurationData.getAllMessageProperties()) { + final String key = property.getPath(); + if (configurationData.getValue(property) == null) { + configurationData.setValue(property, jarMessageSource.getMessageFromJar(property)); + addedKeys.add(key); + } + } + if (!addedKeys.isEmpty()) { + logger.info( + "Added " + addedKeys.size() + " missing keys to your messages_xx.yml file: " + addedKeys); + return true; + } + return false; + } + + private static void backupMessagesFile(File messagesFile) { + String backupName = FileUtils.createBackupFilePath(messagesFile); + File backupFile = new File(backupName); + try { + Files.copy(messagesFile, backupFile); + } catch (IOException e) { + throw new IllegalStateException("Could not back up '" + messagesFile + "' to '" + backupFile + "'", e); + } + } + + /** + * Constructs the {@link ConfigurationData} for exporting a messages file in its entirety. + * + * @return the configuration data to export with + */ + public static MessageKeyConfigurationData createConfigurationData() { + Map comments = ImmutableMap.builder() + .put("registration", "Registration") + .put("password", "Password errors on registration") + .put("login", "Login") + .put("error", "Errors") + .put("antibot", "AntiBot") + .put("unregister", "Unregister") + .put("misc", "Other messages") + .put("session", "Session messages") + .put("on_join_validation", "Error messages when joining") + .put("email", "Email") + .put("recovery", "Password recovery by email") + .put("captcha", "Captcha") + .put("verification", "Verification code") + .put("time", "Time units") + .put("two_factor", "Two-factor authentication") + .put("bedrock_auto_login", "3rd party features: Bedrock Auto Login") + .put("login_location_fix", "3rd party features: Login Location Fix") + .put("double_login_fix", "3rd party features: Double Login Fix") + .build(); + + Set addedKeys = new HashSet<>(); + MessageKeyPropertyListBuilder builder = new MessageKeyPropertyListBuilder(); + // Add one key per section based on the comments map above so that the order is clear + for (String path : comments.keySet()) { + MessageKey key = Arrays.stream(MessageKey.values()).filter(p -> p.getKey().startsWith(path + ".")) + .findFirst().orElseThrow(() -> new IllegalStateException(path)); + builder.addMessageKey(key); + addedKeys.add(key.getKey()); + } + // Add all remaining keys to the property list builder + Arrays.stream(MessageKey.values()) + .filter(key -> !addedKeys.contains(key.getKey())) + .forEach(builder::addMessageKey); + + // Create ConfigurationData instance + Map> commentsMap = comments.entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, e -> singletonList(e.getValue()))); + return new MessageKeyConfigurationData(builder, commentsMap); + } + + static final class MessageKeyProperty extends StringProperty { + + MessageKeyProperty(MessageKey messageKey) { + super(messageKey.getKey(), ""); + } + + @Override + protected String getFromReader(PropertyReader reader, ConvertErrorRecorder errorRecorder) { + return reader.getString(getPath()); + } + } + + static final class MessageKeyPropertyListBuilder { + + private PropertyListBuilder propertyListBuilder = new PropertyListBuilder(); + + void addMessageKey(MessageKey key) { + propertyListBuilder.add(new MessageKeyProperty(key)); + } + + @SuppressWarnings("unchecked") + List getAllProperties() { + return (List) propertyListBuilder.create(); + } + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/message/updater/MigraterYamlFileResource.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/message/updater/MigraterYamlFileResource.java new file mode 100644 index 00000000..b0122215 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/message/updater/MigraterYamlFileResource.java @@ -0,0 +1,47 @@ +package fr.xephi.authme.message.updater; + +import ch.jalu.configme.resource.PropertyReader; +import ch.jalu.configme.resource.YamlFileResource; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.Yaml; + +import java.io.File; + +/** + * Extension of {@link YamlFileResource} to fine-tune the export style. + */ +public class MigraterYamlFileResource extends YamlFileResource { + + private Yaml singleQuoteYaml; + + public MigraterYamlFileResource(File file) { + super(file); + } + + @Override + public PropertyReader createReader() { + return MessageMigraterPropertyReader.loadFromFile(getFile()); + } + + @Override + protected Yaml createNewYaml() { + if (singleQuoteYaml == null) { + DumperOptions options = new DumperOptions(); + options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); + options.setAllowUnicode(true); + options.setDefaultScalarStyle(DumperOptions.ScalarStyle.SINGLE_QUOTED); + // Overridden setting: don't split lines + options.setSplitLines(false); + singleQuoteYaml = new Yaml(options); + } + return singleQuoteYaml; + } + + // Because we set the YAML object to put strings in single quotes, this method by default uses that YAML object + // and also puts all paths as single quotes. Override to just always return the same string since we know those + // are only message names (so never any conflicting strings like "true" or "0"). + @Override + protected String escapePathElementIfNeeded(String path) { + return path; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/message/updater/OldMessageKeysMigrater.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/message/updater/OldMessageKeysMigrater.java new file mode 100644 index 00000000..739a449d --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/message/updater/OldMessageKeysMigrater.java @@ -0,0 +1,170 @@ +package fr.xephi.authme.message.updater; + +import ch.jalu.configme.resource.PropertyReader; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableMap; +import fr.xephi.authme.message.MessageKey; + +import java.util.Map; + +import static com.google.common.collect.ImmutableMap.of; + +/** + * Migrates message files from the old keys (before 5.5) to the new ones. + * + * @see Issue #1467 + */ +final class OldMessageKeysMigrater { + + @VisibleForTesting + static final Map KEYS_TO_OLD_PATH = ImmutableMap.builder() + .put(MessageKey.LOGIN_SUCCESS, "login") + .put(MessageKey.ERROR, "error") + .put(MessageKey.DENIED_COMMAND, "denied_command") + .put(MessageKey.SAME_IP_ONLINE, "same_ip_online") + .put(MessageKey.DENIED_CHAT, "denied_chat") + .put(MessageKey.KICK_ANTIBOT, "kick_antibot") + .put(MessageKey.UNKNOWN_USER, "unknown_user") + .put(MessageKey.NOT_LOGGED_IN, "not_logged_in") + .put(MessageKey.USAGE_LOGIN, "usage_log") + .put(MessageKey.WRONG_PASSWORD, "wrong_pwd") + .put(MessageKey.UNREGISTERED_SUCCESS, "unregistered") + .put(MessageKey.REGISTRATION_DISABLED, "reg_disabled") + .put(MessageKey.SESSION_RECONNECTION, "valid_session") + .put(MessageKey.ACCOUNT_NOT_ACTIVATED, "vb_nonActiv") + .put(MessageKey.NAME_ALREADY_REGISTERED, "user_regged") + .put(MessageKey.NO_PERMISSION, "no_perm") + .put(MessageKey.LOGIN_MESSAGE, "login_msg") + .put(MessageKey.REGISTER_MESSAGE, "reg_msg") + .put(MessageKey.MAX_REGISTER_EXCEEDED, "max_reg") + .put(MessageKey.USAGE_REGISTER, "usage_reg") + .put(MessageKey.USAGE_UNREGISTER, "usage_unreg") + .put(MessageKey.PASSWORD_CHANGED_SUCCESS, "pwd_changed") + .put(MessageKey.PASSWORD_MATCH_ERROR, "password_error") + .put(MessageKey.PASSWORD_IS_USERNAME_ERROR, "password_error_nick") + .put(MessageKey.PASSWORD_UNSAFE_ERROR, "password_error_unsafe") + .put(MessageKey.PASSWORD_CHARACTERS_ERROR, "password_error_chars") + .put(MessageKey.SESSION_EXPIRED, "invalid_session") + .put(MessageKey.MUST_REGISTER_MESSAGE, "reg_only") + .put(MessageKey.ALREADY_LOGGED_IN_ERROR, "logged_in") + .put(MessageKey.LOGOUT_SUCCESS, "logout") + .put(MessageKey.USERNAME_ALREADY_ONLINE_ERROR, "same_nick") + .put(MessageKey.REGISTER_SUCCESS, "registered") + .put(MessageKey.INVALID_PASSWORD_LENGTH, "pass_len") + .put(MessageKey.CONFIG_RELOAD_SUCCESS, "reload") + .put(MessageKey.LOGIN_TIMEOUT_ERROR, "timeout") + .put(MessageKey.USAGE_CHANGE_PASSWORD, "usage_changepassword") + .put(MessageKey.INVALID_NAME_LENGTH, "name_len") + .put(MessageKey.INVALID_NAME_CHARACTERS, "regex") + .put(MessageKey.ADD_EMAIL_MESSAGE, "add_email") + .put(MessageKey.FORGOT_PASSWORD_MESSAGE, "recovery_email") + .put(MessageKey.USAGE_CAPTCHA, "usage_captcha") + .put(MessageKey.CAPTCHA_WRONG_ERROR, "wrong_captcha") + .put(MessageKey.CAPTCHA_SUCCESS, "valid_captcha") + .put(MessageKey.CAPTCHA_FOR_REGISTRATION_REQUIRED, "captcha_for_registration") + .put(MessageKey.REGISTER_CAPTCHA_SUCCESS, "register_captcha_valid") + .put(MessageKey.KICK_FOR_VIP, "kick_forvip") + .put(MessageKey.KICK_FULL_SERVER, "kick_fullserver") + .put(MessageKey.USAGE_ADD_EMAIL, "usage_email_add") + .put(MessageKey.USAGE_CHANGE_EMAIL, "usage_email_change") + .put(MessageKey.USAGE_RECOVER_EMAIL, "usage_email_recovery") + .put(MessageKey.INVALID_NEW_EMAIL, "new_email_invalid") + .put(MessageKey.INVALID_OLD_EMAIL, "old_email_invalid") + .put(MessageKey.INVALID_EMAIL, "email_invalid") + .put(MessageKey.EMAIL_ADDED_SUCCESS, "email_added") + .put(MessageKey.CONFIRM_EMAIL_MESSAGE, "email_confirm") + .put(MessageKey.EMAIL_CHANGED_SUCCESS, "email_changed") + .put(MessageKey.EMAIL_SHOW, "email_show") + .put(MessageKey.SHOW_NO_EMAIL, "show_no_email") + .put(MessageKey.RECOVERY_EMAIL_SENT_MESSAGE, "email_send") + .put(MessageKey.COUNTRY_BANNED_ERROR, "country_banned") + .put(MessageKey.ANTIBOT_AUTO_ENABLED_MESSAGE, "antibot_auto_enabled") + .put(MessageKey.ANTIBOT_AUTO_DISABLED_MESSAGE, "antibot_auto_disabled") + .put(MessageKey.EMAIL_ALREADY_USED_ERROR, "email_already_used") + .put(MessageKey.TWO_FACTOR_CREATE, "two_factor_create") + .put(MessageKey.NOT_OWNER_ERROR, "not_owner_error") + .put(MessageKey.INVALID_NAME_CASE, "invalid_name_case") + .put(MessageKey.TEMPBAN_MAX_LOGINS, "tempban_max_logins") + .put(MessageKey.ACCOUNTS_OWNED_SELF, "accounts_owned_self") + .put(MessageKey.ACCOUNTS_OWNED_OTHER, "accounts_owned_other") + .put(MessageKey.KICK_FOR_ADMIN_REGISTER, "kicked_admin_registered") + .put(MessageKey.INCOMPLETE_EMAIL_SETTINGS, "incomplete_email_settings") + .put(MessageKey.EMAIL_SEND_FAILURE, "email_send_failure") + .put(MessageKey.RECOVERY_CODE_SENT, "recovery_code_sent") + .put(MessageKey.INCORRECT_RECOVERY_CODE, "recovery_code_incorrect") + .put(MessageKey.RECOVERY_TRIES_EXCEEDED, "recovery_tries_exceeded") + .put(MessageKey.RECOVERY_CODE_CORRECT, "recovery_code_correct") + .put(MessageKey.RECOVERY_CHANGE_PASSWORD, "recovery_change_password") + .put(MessageKey.CHANGE_PASSWORD_EXPIRED, "change_password_expired") + .put(MessageKey.EMAIL_COOLDOWN_ERROR, "email_cooldown_error") + .put(MessageKey.VERIFICATION_CODE_REQUIRED, "verification_code_required") + .put(MessageKey.USAGE_VERIFICATION_CODE, "usage_verification_code") + .put(MessageKey.INCORRECT_VERIFICATION_CODE, "incorrect_verification_code") + .put(MessageKey.VERIFICATION_CODE_VERIFIED, "verification_code_verified") + .put(MessageKey.VERIFICATION_CODE_ALREADY_VERIFIED, "verification_code_already_verified") + .put(MessageKey.VERIFICATION_CODE_EXPIRED, "verification_code_expired") + .put(MessageKey.VERIFICATION_CODE_EMAIL_NEEDED, "verification_code_email_needed") + .put(MessageKey.SECOND, "second") + .put(MessageKey.SECONDS, "seconds") + .put(MessageKey.MINUTE, "minute") + .put(MessageKey.MINUTES, "minutes") + .put(MessageKey.HOUR, "hour") + .put(MessageKey.HOURS, "hours") + .put(MessageKey.DAY, "day") + .put(MessageKey.DAYS, "days") + .build(); + + private static final Map> PLACEHOLDER_REPLACEMENTS = + ImmutableMap.>builder() + .put(MessageKey.PASSWORD_CHARACTERS_ERROR, of("REG_EX", "%valid_chars")) + .put(MessageKey.INVALID_NAME_CHARACTERS, of("REG_EX", "%valid_chars")) + .put(MessageKey.USAGE_CAPTCHA, of("", "%captcha_code")) + .put(MessageKey.CAPTCHA_FOR_REGISTRATION_REQUIRED, of("", "%captcha_code")) + .put(MessageKey.CAPTCHA_WRONG_ERROR, of("THE_CAPTCHA", "%captcha_code")) + .build(); + + private OldMessageKeysMigrater() { + } + + /** + * Migrates any existing old key paths to their new paths if no text has been defined for the new key. + * + * @param reader the property reader to get values from + * @param configurationData the configuration data to write to + * @return true if at least one message could be migrated, false otherwise + */ + static boolean migrateOldPaths(PropertyReader reader, MessageKeyConfigurationData configurationData) { + boolean wasPropertyMoved = false; + for (Map.Entry migrationEntry : KEYS_TO_OLD_PATH.entrySet()) { + wasPropertyMoved |= moveIfApplicable(reader, configurationData, + migrationEntry.getKey(), migrationEntry.getValue()); + } + return wasPropertyMoved; + } + + private static boolean moveIfApplicable(PropertyReader reader, MessageKeyConfigurationData configurationData, + MessageKey messageKey, String oldPath) { + if (configurationData.getMessage(messageKey) == null) { + String textAtOldPath = reader.getString(oldPath); + if (textAtOldPath != null) { + textAtOldPath = replaceOldPlaceholders(messageKey, textAtOldPath); + configurationData.setMessage(messageKey, textAtOldPath); + return true; + } + } + return false; + } + + private static String replaceOldPlaceholders(MessageKey key, String text) { + Map replacements = PLACEHOLDER_REPLACEMENTS.get(key); + if (replacements == null) { + return text; + } + + String newText = text; + for (Map.Entry replacement : replacements.entrySet()) { + newText = newText.replace(replacement.getKey(), replacement.getValue()); + } + return newText; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/output/ConsoleFilter.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/output/ConsoleFilter.java new file mode 100644 index 00000000..975cc4cc --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/output/ConsoleFilter.java @@ -0,0 +1,26 @@ +package fr.xephi.authme.output; + +import java.util.logging.Filter; +import java.util.logging.LogRecord; + +/** + * Console filter to replace sensitive AuthMe commands with a generic message. + * + * @author Xephi59 + */ +public class ConsoleFilter implements Filter { + + @Override + public boolean isLoggable(LogRecord record) { + if (record == null || record.getMessage() == null) { + return true; + } + + if (LogFilterHelper.isSensitiveAuthMeCommand(record.getMessage())) { + String playerName = record.getMessage().split(" ")[0]; + record.setMessage(playerName + " issued an AuthMe command"); + } + return true; + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/output/ConsoleLoggerFactory.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/output/ConsoleLoggerFactory.java new file mode 100644 index 00000000..fb51043d --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/output/ConsoleLoggerFactory.java @@ -0,0 +1,55 @@ +package fr.xephi.authme.output; + +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.settings.Settings; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Creates and keeps track of {@link ConsoleLogger} instances. + */ +public final class ConsoleLoggerFactory { + + private static final Map consoleLoggers = new ConcurrentHashMap<>(); + private static Settings settings; + + private ConsoleLoggerFactory() { + } + + /** + * Creates or returns the already existing logger associated with the given class. + * + * @param owningClass the class whose logger should be retrieved + * @return logger for the given class + */ + public static ConsoleLogger get(Class owningClass) { + String name = owningClass.getCanonicalName(); + return consoleLoggers.computeIfAbsent(name, ConsoleLoggerFactory::createLogger); + } + + /** + * Sets up all loggers according to the properties returned by the settings instance. + * + * @param settings the settings instance + */ + public static void reloadSettings(Settings settings) { + ConsoleLoggerFactory.settings = settings; + ConsoleLogger.initializeSharedSettings(settings); + + consoleLoggers.values() + .forEach(logger -> logger.initializeSettings(settings)); + } + + public static int getTotalLoggers() { + return consoleLoggers.size(); + } + + private static ConsoleLogger createLogger(String name) { + ConsoleLogger logger = new ConsoleLogger(name); + if (settings != null) { + logger.initializeSettings(settings); + } + return logger; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/output/Log4JFilter.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/output/Log4JFilter.java new file mode 100644 index 00000000..1ebf5141 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/output/Log4JFilter.java @@ -0,0 +1,75 @@ +package fr.xephi.authme.output; + +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.Marker; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.Logger; +import org.apache.logging.log4j.core.filter.AbstractFilter; +import org.apache.logging.log4j.message.Message; + +/** + * Implements a filter for Log4j to skip sensitive AuthMe commands. + * + * @author Xephi59 + */ +public class Log4JFilter extends AbstractFilter { + + private static final long serialVersionUID = -5594073755007974254L; + + /** + * Validates a Message instance and returns the {@link Result} value + * depending on whether the message contains sensitive AuthMe data. + * + * @param message The Message object to verify + * + * @return The Result value + */ + private static Result validateMessage(Message message) { + if (message == null) { + return Result.NEUTRAL; + } + return validateMessage(message.getFormattedMessage()); + } + + /** + * Validates a message and returns the {@link Result} value depending + * on whether the message contains sensitive AuthMe data. + * + * @param message The message to verify + * + * @return The Result value + */ + private static Result validateMessage(String message) { + return LogFilterHelper.isSensitiveAuthMeCommand(message) + ? Result.DENY + : Result.NEUTRAL; + } + + @Override + public Result filter(LogEvent event) { + Message candidate = null; + if (event != null) { + candidate = event.getMessage(); + } + return validateMessage(candidate); + } + + @Override + public Result filter(Logger logger, Level level, Marker marker, Message msg, Throwable t) { + return validateMessage(msg); + } + + @Override + public Result filter(Logger logger, Level level, Marker marker, String msg, Object... params) { + return validateMessage(msg); + } + + @Override + public Result filter(Logger logger, Level level, Marker marker, Object msg, Throwable t) { + String candidate = null; + if (msg != null) { + candidate = msg.toString(); + } + return validateMessage(candidate); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/output/LogFilterHelper.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/output/LogFilterHelper.java new file mode 100644 index 00000000..6058584a --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/output/LogFilterHelper.java @@ -0,0 +1,51 @@ +package fr.xephi.authme.output; + +import com.google.common.annotations.VisibleForTesting; +import fr.xephi.authme.util.StringUtils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; + +/** + * Service class for the log filters. + */ +final class LogFilterHelper { + + @VisibleForTesting + static final List COMMANDS_TO_SKIP = withAndWithoutAuthMePrefix( + "/login ", "/l ", "/log ", "/register ", "/reg ", "/unregister ", "/unreg ", + "/changepassword ", "/cp ", "/changepass ", "/authme register ", "/authme reg ", "/authme r ", + "/authme changepassword ", "/authme password ", "/authme changepass ", "/authme cp ", "/email setpassword "); + + private static final String ISSUED_COMMAND_TEXT = "issued server command:"; + + private LogFilterHelper() { + // Util class + } + + /** + * Validate a message and return whether the message contains a sensitive AuthMe command. + * + * @param message The message to verify + * + * @return True if it is a sensitive AuthMe command, false otherwise + */ + static boolean isSensitiveAuthMeCommand(String message) { + if (message == null) { + return false; + } + String lowerMessage = message.toLowerCase(Locale.ROOT); + return lowerMessage.contains(ISSUED_COMMAND_TEXT) && StringUtils.containsAny(lowerMessage, COMMANDS_TO_SKIP); + } + + private static List withAndWithoutAuthMePrefix(String... commands) { + List commandList = new ArrayList<>(commands.length * 2); + for (String command : commands) { + commandList.add(command); + commandList.add(command.substring(0, 1) + "authme:" + command.substring(1)); + } + return Collections.unmodifiableList(commandList); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/output/LogLevel.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/output/LogLevel.java new file mode 100644 index 00000000..f958e6d4 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/output/LogLevel.java @@ -0,0 +1,38 @@ +package fr.xephi.authme.output; + +/** + * Log level. + */ +public enum LogLevel { + + /** Info: general messages. */ + INFO(3), + + /** Fine: more detailed messages that may still be interesting to plugin users. */ + FINE(2), + + /** Debug: very detailed messages for debugging. */ + DEBUG(1); + + private int value; + + /** + * Constructor. + * + * @param value the log level; the higher the number the more "important" the level. + * A log level enables its number and all above. + */ + LogLevel(int value) { + this.value = value; + } + + /** + * Return whether the current log level includes the given log level. + * + * @param level the level to process + * @return true if the level is enabled, false otherwise + */ + public boolean includes(LogLevel level) { + return value <= level.value; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/permission/AdminPermission.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/permission/AdminPermission.java new file mode 100644 index 00000000..20e27f2f --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/permission/AdminPermission.java @@ -0,0 +1,166 @@ +package fr.xephi.authme.permission; + +/** + * AuthMe admin command permissions. + */ +public enum AdminPermission implements PermissionNode { + + /** + * Administrator command to register a new user. + */ + REGISTER("authme.admin.register"), + + /** + * Administrator command to unregister an existing user. + */ + UNREGISTER("authme.admin.unregister"), + + /** + * Administrator command to force-login an existing user. + */ + FORCE_LOGIN("authme.admin.forcelogin"), + + /** + * Administrator command to change the password of a user. + */ + CHANGE_PASSWORD("authme.admin.changepassword"), + + /** + * Administrator command to see the last login date and time of a user. + */ + LAST_LOGIN("authme.admin.lastlogin"), + + /** + * Administrator command to see all accounts associated with a user. + */ + ACCOUNTS("authme.admin.accounts"), + + /** + * Administrator command to get the email address of a user, if set. + */ + GET_EMAIL("authme.admin.getemail"), + + /** + * Administrator command to set or change the email address of a user. + */ + CHANGE_EMAIL("authme.admin.changemail"), + + /** + * Administrator command to see whether a player has enabled two-factor authentication. + */ + VIEW_TOTP_STATUS("authme.admin.totpviewstatus"), + + /** + * Administrator command to disable the two-factor auth of a user. + */ + DISABLE_TOTP("authme.admin.totpdisable"), + + /** + * Administrator command to get the last known IP of a user. + */ + GET_IP("authme.admin.getip"), + + /** + * Administrator command to see the last recently logged in players. + */ + SEE_RECENT_PLAYERS("authme.admin.seerecent"), + + /** + * Administrator command to teleport to the AuthMe spawn. + */ + SPAWN("authme.admin.spawn"), + + /** + * Administrator command to set the AuthMe spawn. + */ + SET_SPAWN("authme.admin.setspawn"), + + /** + * Administrator command to teleport to the first AuthMe spawn. + */ + FIRST_SPAWN("authme.admin.firstspawn"), + + /** + * Administrator command to set the first AuthMe spawn. + */ + SET_FIRST_SPAWN("authme.admin.setfirstspawn"), + + /** + * Administrator command to purge old user data. + */ + PURGE("authme.admin.purge"), + + /** + * Administrator command to purge the last position of a user. + */ + PURGE_LAST_POSITION("authme.admin.purgelastpos"), + + /** + * Administrator command to purge all data associated with banned players. + */ + PURGE_BANNED_PLAYERS("authme.admin.purgebannedplayers"), + + /** + * Administrator command to purge a given player. + */ + PURGE_PLAYER("authme.admin.purgeplayer"), + + /** + * Administrator command to toggle the AntiBot protection status. + */ + SWITCH_ANTIBOT("authme.admin.switchantibot"), + + /** + * Administrator command to convert old or other data to AuthMe data. + */ + CONVERTER("authme.admin.converter"), + + /** + * Administrator command to reload the plugin configuration. + */ + RELOAD("authme.admin.reload"), + + /** + * Permission to see Antibot messages. + */ + ANTIBOT_MESSAGES("authme.admin.antibotmessages"), + + /** + * Permission to use the update messages command. + */ + UPDATE_MESSAGES("authme.admin.updatemessages"), + + /** + * Permission to see the other accounts of the players that log in. + */ + SEE_OTHER_ACCOUNTS("authme.admin.seeotheraccounts"), + + /** + * Allows to use the backup command. + */ + BACKUP("authme.admin.backup"); + + /** + * The permission node. + */ + private String node; + + /** + * Constructor. + * + * @param node Permission node. + */ + AdminPermission(String node) { + this.node = node; + } + + @Override + public String getNode() { + return node; + } + + @Override + public DefaultPermission getDefaultPermission() { + return DefaultPermission.OP_ONLY; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/permission/DebugSectionPermissions.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/permission/DebugSectionPermissions.java new file mode 100644 index 00000000..6461c29a --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/permission/DebugSectionPermissions.java @@ -0,0 +1,61 @@ +package fr.xephi.authme.permission; + +/** + * Permissions for the debug sections (/authme debug). + */ +public enum DebugSectionPermissions implements PermissionNode { + + /** General permission to use the /authme debug command. */ + DEBUG_COMMAND("authme.debug.command"), + + /** Permission to use the country lookup section. */ + COUNTRY_LOOKUP("authme.debug.country"), + + /** Permission to use the stats section. */ + DATA_STATISTICS("authme.debug.stats"), + + /** Permission to use the permission checker. */ + HAS_PERMISSION_CHECK("authme.debug.perm"), + + /** Permission to use sample validation. */ + INPUT_VALIDATOR("authme.debug.valid"), + + /** Permission to use the limbo data viewer. */ + LIMBO_PLAYER_VIEWER("authme.debug.limbo"), + + /** Permission to view permission groups. */ + PERM_GROUPS("authme.debug.group"), + + /** Permission to view data from the database. */ + PLAYER_AUTH_VIEWER("authme.debug.db"), + + /** Permission to change nullable status of MySQL columns. */ + MYSQL_DEFAULT_CHANGER("authme.debug.mysqldef"), + + /** Permission to view spawn information. */ + SPAWN_LOCATION("authme.debug.spawn"), + + /** Permission to use the test email sender. */ + TEST_EMAIL("authme.debug.mail"); + + private final String node; + + /** + * Constructor. + * + * @param node the permission node + */ + DebugSectionPermissions(String node) { + this.node = node; + } + + @Override + public String getNode() { + return node; + } + + @Override + public DefaultPermission getDefaultPermission() { + return DefaultPermission.OP_ONLY; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/permission/DefaultPermission.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/permission/DefaultPermission.java new file mode 100644 index 00000000..1d0e0f3e --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/permission/DefaultPermission.java @@ -0,0 +1,42 @@ +package fr.xephi.authme.permission; + +import org.bukkit.permissions.ServerOperator; + +/** + * The default permission to fall back to if there is no support for permission nodes. + */ +public enum DefaultPermission { + + /** No one has permission. */ + NOT_ALLOWED { + @Override + public boolean evaluate(ServerOperator sender) { + return false; + } + }, + + /** Only players with OP status have permission. */ + OP_ONLY { + @Override + public boolean evaluate(ServerOperator sender) { + return sender != null && sender.isOp(); + } + }, + + /** Everyone is granted permission. */ + ALLOWED { + @Override + public boolean evaluate(ServerOperator sender) { + return true; + } + }; + + /** + * 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(ServerOperator sender); + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/permission/PermissionNode.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/permission/PermissionNode.java new file mode 100644 index 00000000..189f97b5 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/permission/PermissionNode.java @@ -0,0 +1,21 @@ +package fr.xephi.authme.permission; + +/** + * Common interface for AuthMe permission nodes. + */ +public interface PermissionNode { + + /** + * Return the node of the permission, e.g. "authme.player.unregister". + * + * @return The name of the permission node + */ + String getNode(); + + /** + * Return the default permission for this node, e.g. "OP_ONLY" + * + * @return The default level of permission + */ + DefaultPermission getDefaultPermission(); +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/permission/PermissionsManager.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/permission/PermissionsManager.java new file mode 100644 index 00000000..7399d0ca --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/permission/PermissionsManager.java @@ -0,0 +1,466 @@ +package fr.xephi.authme.permission; + +import com.google.common.annotations.VisibleForTesting; +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.data.limbo.UserGroup; +import fr.xephi.authme.initialization.Reloadable; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.permission.handlers.LuckPermsHandler; +import fr.xephi.authme.permission.handlers.PermissionHandler; +import fr.xephi.authme.permission.handlers.PermissionHandlerException; +import fr.xephi.authme.permission.handlers.PermissionLoadUserException; +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.settings.properties.PluginSettings; +import fr.xephi.authme.util.StringUtils; +import org.bukkit.OfflinePlayer; +import org.bukkit.Server; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.PluginManager; + +import javax.annotation.PostConstruct; +import javax.inject.Inject; +import java.util.Collection; +import java.util.Collections; +import java.util.UUID; + +/** + * PermissionsManager. + *

+ * A permissions manager, to manage and use various permissions systems. + * This manager supports dynamic plugin hooking and various other features. + *

+ * Written by Tim Visée. + * + * @author Tim Visée, http://timvisee.com + * @version 0.3 + */ +public class PermissionsManager implements Reloadable { + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(PermissionsManager.class); + private final Server server; + private final PluginManager pluginManager; + private final Settings settings; + + /** + * The permission handler that is currently in use. + * Null if no permission system is hooked. + */ + private PermissionHandler handler = null; + + @Inject + PermissionsManager(Server server, PluginManager pluginManager, Settings settings) { + this.server = server; + this.pluginManager = pluginManager; + this.settings = settings; + } + + /** + * Check if the permissions manager is currently hooked into any of the supported permissions systems. + * + * @return False if there isn't any permissions system used. + */ + public boolean isEnabled() { + return handler != null; + } + + /** + * Setup and hook into the permissions systems. + */ + @PostConstruct + @VisibleForTesting + void setup() { + if (settings.getProperty(PluginSettings.FORCE_VAULT_HOOK)) { + try { + PermissionHandler handler = createPermissionHandler(PermissionsSystemType.VAULT); + if (handler != null) { + // Show a success message and return + this.handler = handler; + logger.info("Hooked into " + PermissionsSystemType.VAULT.getDisplayName() + "!"); + return; + } + } catch (PermissionHandlerException e) { + logger.logException("Failed to create Vault hook (forced):", e); + } + } else { + // Loop through all the available permissions system types + for (PermissionsSystemType type : PermissionsSystemType.values()) { + try { + PermissionHandler handler = createPermissionHandler(type); + if (handler != null) { + // Show a success message and return + this.handler = handler; + logger.info("Hooked into " + type.getDisplayName() + "!"); + return; + } + } catch (Exception ex) { + // An error occurred, show a warning message + logger.logException("Error while hooking into " + type.getDisplayName(), ex); + } + } + } + + // No recognized permissions system found, show a message and return + logger.info("No supported permissions system found! Permissions are disabled!"); + } + + /** + * Creates a permission handler for the provided permission systems if possible. + * + * @param type the permission systems type for which to create a corresponding permission handler + * + * @return the permission handler, or {@code null} if not possible + * + * @throws PermissionHandlerException during initialization of the permission handler + */ + private PermissionHandler createPermissionHandler(PermissionsSystemType type) throws PermissionHandlerException { + // Try to find the plugin for the current permissions system + Plugin plugin = pluginManager.getPlugin(type.getPluginName()); + + if (plugin == null) { + return null; + } + + // Make sure the plugin is enabled before hooking + if (!plugin.isEnabled()) { + logger.info("Not hooking into " + type.getDisplayName() + " because it's disabled!"); + return null; + } + + switch (type) { + case LUCK_PERMS: + return new LuckPermsHandler(); + case PERMISSIONS_EX: + return new PermissionsExHandler(); + case Z_PERMISSIONS: + return new ZPermissionsHandler(); + case VAULT: + return new VaultHandler(server); + default: + throw new IllegalStateException("Unhandled permission type '" + type + "'"); + } + } + + /** + * Break the hook with all permission systems. + */ + private void unhook() { + // Reset the current used permissions system + this.handler = null; + + // Print a status message to the console + logger.info("Unhooked from Permissions!"); + } + + /** + * Reload the permissions manager, and re-hook all permission plugins. + */ + @Override + public void reload() { + // Unhook all permission plugins + unhook(); + + // Set up the permissions manager again + setup(); + } + + /** + * Method called when a plugin is being enabled. + * + * @param pluginName The name of the plugin being enabled. + */ + public void onPluginEnable(String pluginName) { + // Check if any known permissions system is enabling + if (PermissionsSystemType.isPermissionSystem(pluginName)) { + logger.info(pluginName + " plugin enabled, dynamically updating permissions hooks!"); + setup(); + } + } + + /** + * Method called when a plugin is being disabled. + * + * @param pluginName The name of the plugin being disabled. + */ + public void onPluginDisable(String pluginName) { + // Check if any known permission system is being disabled + if (PermissionsSystemType.isPermissionSystem(pluginName)) { + logger.info(pluginName + " plugin disabled, updating hooks!"); + setup(); + } + } + + /** + * Return the permissions system that is hooked into. + * + * @return The permissions system, or null. + */ + public PermissionsSystemType getPermissionSystem() { + return isEnabled() ? handler.getPermissionSystem() : null; + } + + /** + * Check if the command sender has permission for the given permissions node. If no permissions system is used or + * if the sender is not a player (e.g. console user), the player has to be OP in order to have the permission. + * + * @param sender The command sender. + * @param permissionNode The permissions node to verify. + * + * @return True if the sender has the permission, false otherwise. + */ + public boolean hasPermission(CommandSender sender, PermissionNode permissionNode) { + // Check if the permission node is null + if (permissionNode == null) { + return true; + } + + // Return default if sender is not a player or no permission system is in use + if (!(sender instanceof Player) || !isEnabled()) { + return permissionNode.getDefaultPermission().evaluate(sender); + } + + Player player = (Player) sender; + return player.hasPermission(permissionNode.getNode()); + } + + /** + * Check if a player has permission for the given permission node. This is for offline player checks. + * If no permissions system is used, then the player will not have permission. + * + * @param player The offline player + * @param permissionNode The permission node to verify + * + * @return true if the player has permission, false otherwise + */ + public boolean hasPermissionOffline(OfflinePlayer player, PermissionNode permissionNode) { + // Check if the permission node is null + if (permissionNode == null) { + return true; + } + + if (!isEnabled()) { + return permissionNode.getDefaultPermission().evaluate(player); + } + + return handler.hasPermissionOffline(player.getName(), permissionNode); + } + + /** + * Check whether the offline player with the given name has permission for the given permission node. + * This method is used as a last resort when nothing besides the name is known. + * + * @param name The name of the player + * @param permissionNode The permission node to verify + * + * @return true if the player has permission, false otherwise + */ + public boolean hasPermissionOffline(String name, PermissionNode permissionNode) { + if (permissionNode == null) { + return true; + } + if (!isEnabled()) { + return permissionNode.getDefaultPermission().evaluate(null); + } + + return handler.hasPermissionOffline(name, permissionNode); + } + + /** + * 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. + */ + public boolean hasGroupSupport() { + return isEnabled() && handler.hasGroupSupport(); + } + + /** + * Get the permission groups of a player, if available. + * + * @param player The player. + * + * @return Permission groups, or an empty collection if this feature is not supported. + */ + public Collection getGroups(OfflinePlayer player) { + return isEnabled() ? handler.getGroups(player) : Collections.emptyList(); + } + + /** + * Get the primary group of a player, if available. + * + * @param player The player. + * + * @return The name of the primary permission group. Or null. + */ + public UserGroup getPrimaryGroup(OfflinePlayer player) { + return isEnabled() ? handler.getPrimaryGroup(player) : null; + } + + /** + * Check whether the player is in the specified group. + * + * @param player The player. + * @param groupName 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. + */ + public boolean isInGroup(OfflinePlayer player, UserGroup groupName) { + return isEnabled() && handler.isInGroup(player, groupName); + } + + /** + * Add the permission group of a player, if supported. + * + * @param player The player + * @param groupName 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. + */ + public boolean addGroup(OfflinePlayer player, UserGroup groupName) { + if (!isEnabled() || StringUtils.isBlank(groupName.getGroupName())) { + return false; + } + return handler.addToGroup(player, groupName); + } + + /** + * Add the permission groups of a player, if supported. + * + * @param player The player + * @param groupNames The name of the groups to add. + * + * @return True if at least one group was added, false otherwise. + * False is also returned if this feature isn't supported for the current permissions system. + */ + public boolean addGroups(OfflinePlayer player, Collection groupNames) { + // If no permissions system is used, return false + if (!isEnabled()) { + return false; + } + + // Add each group to the user + boolean result = false; + for (UserGroup group : groupNames) { + if (!group.getGroupName().isEmpty()) { + result |= handler.addToGroup(player, group); + } + } + + // Return the result + return result; + } + + /** + * 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. + */ + public boolean removeGroup(OfflinePlayer player, UserGroup group) { + return isEnabled() && handler.removeFromGroup(player, group); + } + + /** + * Remove the permission groups of a player, if supported. + * + * @param player The player + * @param groupNames The name of the groups to remove. + * + * @return True if at least one group was removed, false otherwise. + * False is also returned if this feature isn't supported for the current permissions system. + */ + public boolean removeGroups(OfflinePlayer player, Collection groupNames) { + // If no permissions system is used, return false + if (!isEnabled()) { + return false; + } + + // Add each group to the user + boolean result = false; + for (UserGroup group : groupNames) { + if (!group.getGroupName().isEmpty()) { + result |= handler.removeFromGroup(player, group); + } + } + + // Return the result + return result; + } + + /** + * 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. + */ + public boolean setGroup(OfflinePlayer player, UserGroup group) { + return isEnabled() && handler.setGroup(player, group); + } + + /** + * 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 its primary group. All the subgroups are removed just fine. + * + * @param player The player to remove all groups from. + * + * @return True if succeed, false otherwise. + * False will also be returned if this feature isn't supported for the used permissions system. + */ + public boolean removeAllGroups(OfflinePlayer player) { + // If no permissions system is used, return false + if (!isEnabled()) { + return false; + } + + // Get a list of current groups + Collection groups = getGroups(player); + + // Remove each group + return removeGroups(player, groups); + } + + /** + * Loads the permission data of the given player. + * + * @param offlinePlayer the offline player. + * @return true if the load was successful. + */ + public boolean loadUserData(OfflinePlayer offlinePlayer) { + try { + loadUserData(offlinePlayer.getUniqueId()); + } catch (PermissionLoadUserException e) { + logger.logException("Unable to load the permission data of user " + offlinePlayer.getName(), e); + return false; + } + return true; + } + + /** + * Loads the permission data of the given player unique identifier. + * + * @param uuid the {@link UUID} of the player. + * @throws PermissionLoadUserException if the action failed. + */ + public void loadUserData(UUID uuid) throws PermissionLoadUserException { + if (!isEnabled()) { + return; + } + handler.loadUserData(uuid); + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/permission/PermissionsSystemType.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/permission/PermissionsSystemType.java new file mode 100644 index 00000000..00f01036 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/permission/PermissionsSystemType.java @@ -0,0 +1,91 @@ +package fr.xephi.authme.permission; + +/** + * Enum representing the permissions systems AuthMe supports. + */ +public enum PermissionsSystemType { + + /** + * LuckPerms. + */ + LUCK_PERMS("LuckPerms", "LuckPerms"), + + /** + * Permissions Ex. + */ + PERMISSIONS_EX("PermissionsEx", "PermissionsEx"), + + /** + * zPermissions. + */ + Z_PERMISSIONS("zPermissions", "zPermissions"), + + /** + * Vault. + */ + VAULT("Vault", "Vault"); + + /** + * The display name of the permissions system. + */ + private String displayName; + + /** + * The name of the permissions system plugin. + */ + private String pluginName; + + /** + * Constructor for PermissionsSystemType. + * + * @param displayName Display name of the permissions system. + * @param pluginName Name of the plugin. + */ + PermissionsSystemType(String displayName, String pluginName) { + this.displayName = displayName; + this.pluginName = pluginName; + } + + /** + * Get the display name of the permissions system. + * + * @return Display name. + */ + public String getDisplayName() { + return this.displayName; + } + + /** + * Return the plugin name. + * + * @return Plugin name. + */ + public String getPluginName() { + return this.pluginName; + } + + /** + * Cast the permissions system type to a string. + * + * @return The display name of the permissions system. + */ + @Override + public String toString() { + return getDisplayName(); + } + + /** + * 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 static boolean isPermissionSystem(String name) { + for (PermissionsSystemType permissionsSystemType : values()) { + if (permissionsSystemType.pluginName.equals(name)) { + return true; + } + } + return false; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/permission/PlayerPermission.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/permission/PlayerPermission.java new file mode 100644 index 00000000..d7427f46 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/permission/PlayerPermission.java @@ -0,0 +1,112 @@ +package fr.xephi.authme.permission; + +/** + * AuthMe player permission nodes, for regular players. + */ +public enum PlayerPermission implements PermissionNode { + + /** + * Command permission to login. + */ + LOGIN("authme.player.login"), + + /** + * Command permission to logout. + */ + LOGOUT("authme.player.logout"), + + /** + * Command permission to register. + */ + REGISTER("authme.player.register"), + + /** + * Command permission to unregister. + */ + UNREGISTER("authme.player.unregister"), + + /** + * Command permission to change the password. + */ + CHANGE_PASSWORD("authme.player.changepassword"), + + /** + * Command permission to see the own email address. + */ + SEE_EMAIL("authme.player.email.see"), + + /** + * Command permission to add an email address. + */ + ADD_EMAIL("authme.player.email.add"), + + /** + * Command permission to change the email address. + */ + CHANGE_EMAIL("authme.player.email.change"), + + /** + * Command permission to recover an account using its email address. + */ + RECOVER_EMAIL("authme.player.email.recover"), + + /** + * Command permission to use captcha. + */ + CAPTCHA("authme.player.captcha"), + + /** + * Permission for users a login can be forced to. + */ + CAN_LOGIN_BE_FORCED("authme.player.canbeforced"), + + /** + * Permission to use to see own other accounts. + */ + SEE_OWN_ACCOUNTS("authme.player.seeownaccounts"), + + /** + * Permission to use the email verification codes feature. + */ + VERIFICATION_CODE("authme.player.security.verificationcode"), + + /** + * Permission that enables on join quick commands checks for the player. + */ + QUICK_COMMANDS_PROTECTION("authme.player.protection.quickcommandsprotection"), + + /** + * Permission to enable two-factor authentication. + */ + ENABLE_TWO_FACTOR_AUTH("authme.player.totpadd"), + + /** + * Permission to disable two-factor authentication. + */ + DISABLE_TWO_FACTOR_AUTH("authme.player.totpremove"); + + /** + * The permission node. + */ + private String node; + + /** + * Constructor. + * + * @param node Permission node. + */ + PlayerPermission(String node) { + this.node = node; + } + + @Override + public String getNode() { + return node; + } + + @Override + public DefaultPermission getDefaultPermission() { + return DefaultPermission.ALLOWED; + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/permission/PlayerStatePermission.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/permission/PlayerStatePermission.java new file mode 100644 index 00000000..b9fe17e7 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/permission/PlayerStatePermission.java @@ -0,0 +1,79 @@ +package fr.xephi.authme.permission; + +/** + * Permission nodes that give a player a status (e.g. VIP) + * or grant them more freedom (e.g. less restrictions). + */ +public enum PlayerStatePermission implements PermissionNode { + + /** + * Permission node to bypass AntiBot protection. + */ + BYPASS_ANTIBOT("authme.bypassantibot", DefaultPermission.OP_ONLY), + + /** + * Permission node to bypass BungeeCord server teleportation. + */ + BYPASS_BUNGEE_SEND("authme.bypassbungeesend", DefaultPermission.NOT_ALLOWED), + + /** + * Permission for users to bypass force-survival mode. + */ + BYPASS_FORCE_SURVIVAL("authme.bypassforcesurvival", DefaultPermission.OP_ONLY), + + /** + * When the server is full and someone with this permission joins the server, someone will be kicked. + */ + IS_VIP("authme.vip", DefaultPermission.NOT_ALLOWED), + + /** + * Permission to be able to register multiple accounts. + */ + ALLOW_MULTIPLE_ACCOUNTS("authme.allowmultipleaccounts", DefaultPermission.OP_ONLY), + + /** + * Permission to bypass the purging process. + */ + BYPASS_PURGE("authme.bypasspurge", DefaultPermission.NOT_ALLOWED), + + /** + * Permission to bypass the GeoIp country code check. + */ + BYPASS_COUNTRY_CHECK("authme.bypasscountrycheck", DefaultPermission.NOT_ALLOWED), + + /** + * Permission to send chat messages before being logged in. + */ + ALLOW_CHAT_BEFORE_LOGIN("authme.allowchatbeforelogin", DefaultPermission.NOT_ALLOWED); + + /** + * The permission node. + */ + private String node; + + /** + * The default permission level. + */ + private DefaultPermission defaultPermission; + + /** + * Constructor. + * + * @param node Permission node + * @param defaultPermission The default permission + */ + 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/plugin/platform-bukkit/src/main/java/fr/xephi/authme/permission/handlers/LuckPermGroup.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/permission/handlers/LuckPermGroup.java new file mode 100644 index 00000000..81a34a4e --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/permission/handlers/LuckPermGroup.java @@ -0,0 +1,23 @@ +package fr.xephi.authme.permission.handlers; + +import net.luckperms.api.context.ImmutableContextSet; +import net.luckperms.api.model.group.Group; + +public class LuckPermGroup { + private Group group; + private ImmutableContextSet contexts; + + public LuckPermGroup(Group group, ImmutableContextSet contexts) { + this.group = group; + + this.contexts = contexts; + } + + public Group getGroup() { + return group; + } + + public ImmutableContextSet getContexts() { + return contexts; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/permission/handlers/LuckPermsHandler.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/permission/handlers/LuckPermsHandler.java new file mode 100644 index 00000000..3e786f33 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/permission/handlers/LuckPermsHandler.java @@ -0,0 +1,227 @@ +package fr.xephi.authme.permission.handlers; + +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.data.limbo.UserGroup; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.permission.PermissionNode; +import fr.xephi.authme.permission.PermissionsSystemType; +import net.luckperms.api.LuckPerms; +import net.luckperms.api.LuckPermsProvider; +import net.luckperms.api.cacheddata.CachedPermissionData; +import net.luckperms.api.context.ContextSetFactory; +import net.luckperms.api.model.data.DataMutateResult; +import net.luckperms.api.model.group.Group; +import net.luckperms.api.model.user.User; +import net.luckperms.api.node.NodeEqualityPredicate; +import net.luckperms.api.node.types.InheritanceNode; +import net.luckperms.api.query.QueryMode; +import net.luckperms.api.query.QueryOptions; +import org.bukkit.OfflinePlayer; +import org.jetbrains.annotations.NotNull; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.stream.Collectors; + +/** + * Handler for LuckPerms. + * + * @see LuckPerms SpigotMC page + * @see LuckPerms on Github + */ +public class LuckPermsHandler implements PermissionHandler { + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(LuckPermsHandler.class); + private LuckPerms luckPerms; + + public LuckPermsHandler() throws PermissionHandlerException { + try { + luckPerms = LuckPermsProvider.get(); + } catch (IllegalStateException e) { + throw new PermissionHandlerException("Could not get api of LuckPerms", e); + } + } + + @Override + public boolean addToGroup(OfflinePlayer player, UserGroup group) { + Group newGroup = luckPerms.getGroupManager().getGroup(group.getGroupName()); + if (newGroup == null) { + return false; + } + + String playerName = player.getName(); + if (playerName == null) { + return false; + } + User user = luckPerms.getUserManager().getUser(playerName); + if (user == null) { + return false; + } + + InheritanceNode node = buildGroupNode(group); + + DataMutateResult result = user.data().add(node); + if (result == DataMutateResult.FAIL) { + return false; + } + + luckPerms.getUserManager().saveUser(user); + return true; + } + + @Override + public boolean hasGroupSupport() { + return true; + } + + @Override + public boolean hasPermissionOffline(String name, PermissionNode node) { + User user = luckPerms.getUserManager().getUser(name); + if (user == null) { + logger.warning("LuckPermsHandler: tried to check permission for offline user " + + name + " but it isn't loaded!"); + return false; + } + + CachedPermissionData permissionData = user.getCachedData() + .getPermissionData(QueryOptions.builder(QueryMode.CONTEXTUAL).build()); + return permissionData.checkPermission(node.getNode()).asBoolean(); + } + + @Override + public boolean isInGroup(OfflinePlayer player, UserGroup group) { + String playerName = player.getName(); + if (playerName == null) { + return false; + } + User user = luckPerms.getUserManager().getUser(playerName); + if (user == null) { + logger.warning("LuckPermsHandler: tried to check group for offline user " + + player.getName() + " but it isn't loaded!"); + return false; + } + + InheritanceNode inheritanceNode = InheritanceNode.builder(group.getGroupName()).build(); + return user.data().contains(inheritanceNode, NodeEqualityPredicate.EXACT).asBoolean(); + } + + @Override + public boolean removeFromGroup(OfflinePlayer player, UserGroup group) { + String playerName = player.getName(); + if (playerName == null) { + return false; + } + User user = luckPerms.getUserManager().getUser(playerName); + if (user == null) { + logger.warning("LuckPermsHandler: tried to remove group for offline user " + + player.getName() + " but it isn't loaded!"); + return false; + } + + InheritanceNode groupNode = InheritanceNode.builder(group.getGroupName()).build(); + boolean result = user.data().remove(groupNode) != DataMutateResult.FAIL; + + luckPerms.getUserManager().saveUser(user); + return result; + } + + @Override + public boolean setGroup(OfflinePlayer player, UserGroup group) { + String playerName = player.getName(); + if (playerName == null) { + return false; + } + User user = luckPerms.getUserManager().getUser(playerName); + if (user == null) { + logger.warning("LuckPermsHandler: tried to set group for offline user " + + player.getName() + " but it isn't loaded!"); + return false; + } + + InheritanceNode groupNode = buildGroupNode(group); + + DataMutateResult result = user.data().add(groupNode); + if (result == DataMutateResult.FAIL) { + return false; + } + user.data().clear(node -> { + if (!(node instanceof InheritanceNode)) { + return false; + } + InheritanceNode inheritanceNode = (InheritanceNode) node; + return !inheritanceNode.equals(groupNode); + }); + + luckPerms.getUserManager().saveUser(user); + return true; + } + + @Override + public List getGroups(OfflinePlayer player) { + String playerName = player.getName(); + if (playerName == null) { + return Collections.emptyList(); + } + User user = luckPerms.getUserManager().getUser(playerName); + if (user == null) { + logger.warning("LuckPermsHandler: tried to get groups for offline user " + + player.getName() + " but it isn't loaded!"); + return Collections.emptyList(); + } + + return user.getDistinctNodes().stream() + .filter(node -> node instanceof InheritanceNode) + .map(node -> (InheritanceNode) node) + .map(node -> { + Group group = luckPerms.getGroupManager().getGroup(node.getGroupName()); + if (group == null) { + return null; + } + return new LuckPermGroup(group, node.getContexts()); + }) + .filter(Objects::nonNull) + .sorted((o1, o2) -> sortGroups(user, o1, o2)) + .map(g -> new UserGroup(g.getGroup().getName(), g.getContexts().toFlattenedMap())) + .collect(Collectors.toList()); + } + + @Override + public PermissionsSystemType getPermissionSystem() { + return PermissionsSystemType.LUCK_PERMS; + } + + @Override + public void loadUserData(UUID uuid) throws PermissionLoadUserException { + try { + luckPerms.getUserManager().loadUser(uuid).get(5, TimeUnit.SECONDS); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + throw new PermissionLoadUserException("Unable to load the permission data of the user " + uuid, e); + } + } + + @NotNull + private InheritanceNode buildGroupNode(UserGroup group) { + ContextSetFactory contextSetFactory = luckPerms.getContextManager().getContextSetFactory(); + InheritanceNode.Builder builder = InheritanceNode.builder(group.getGroupName()); + if (group.getContextMap() != null) { + group.getContextMap().forEach((k, v) -> builder.withContext((contextSetFactory.immutableOf(k, v)))); + } + return builder.build(); + } + + private int sortGroups(User user, LuckPermGroup o1, LuckPermGroup o2) { + Group group1 = o1.getGroup(); + Group group2 = o2.getGroup(); + if (group1.getName().equals(user.getPrimaryGroup()) || group2.getName().equals(user.getPrimaryGroup())) { + return group1.getName().equals(user.getPrimaryGroup()) ? 1 : -1; + } + + int i = Integer.compare(group2.getWeight().orElse(0), group1.getWeight().orElse(0)); + return i != 0 ? i : group1.getName().compareToIgnoreCase(group2.getName()); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/permission/handlers/PermissionHandler.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/permission/handlers/PermissionHandler.java new file mode 100644 index 00000000..efdc989e --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/permission/handlers/PermissionHandler.java @@ -0,0 +1,114 @@ +package fr.xephi.authme.permission.handlers; + +import fr.xephi.authme.data.limbo.UserGroup; +import fr.xephi.authme.permission.PermissionNode; +import fr.xephi.authme.permission.PermissionsSystemType; +import fr.xephi.authme.util.Utils; +import org.bukkit.OfflinePlayer; + +import java.util.Collection; +import java.util.UUID; + +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(OfflinePlayer player, UserGroup 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 by their name. + * Used to check an offline player's permission. + * + * @param name The player's name. + * @param node The permission node. + * + * @return True if the player has permission. + */ + boolean hasPermissionOffline(String name, PermissionNode node); + + /** + * 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. + */ + default boolean isInGroup(OfflinePlayer player, UserGroup group) { + return getGroups(player).contains(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(OfflinePlayer player, UserGroup 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(OfflinePlayer player, UserGroup 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. + */ + Collection getGroups(OfflinePlayer player); + + /** + * Get the primary group of a player, if available. + * + * @param player The player. + * + * @return The name of the primary permission group. Or null. + */ + default UserGroup getPrimaryGroup(OfflinePlayer player) { + Collection groups = getGroups(player); + if (Utils.isCollectionEmpty(groups)) { + return null; + } + return groups.iterator().next(); + } + + /** + * Get the permission system that is being used. + * + * @return The permission system. + */ + PermissionsSystemType getPermissionSystem(); + + default void loadUserData(UUID uuid) throws PermissionLoadUserException { + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/permission/handlers/PermissionHandlerException.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/permission/handlers/PermissionHandlerException.java new file mode 100644 index 00000000..a0de9ada --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/permission/handlers/PermissionHandlerException.java @@ -0,0 +1,16 @@ +package fr.xephi.authme.permission.handlers; + +/** + * Exception during the instantiation of a {@link PermissionHandler}. + */ +@SuppressWarnings("serial") +public class PermissionHandlerException extends Exception { + + public PermissionHandlerException(String message) { + super(message); + } + + public PermissionHandlerException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/permission/handlers/PermissionLoadUserException.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/permission/handlers/PermissionLoadUserException.java new file mode 100644 index 00000000..697b4918 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/permission/handlers/PermissionLoadUserException.java @@ -0,0 +1,13 @@ +package fr.xephi.authme.permission.handlers; + +import java.util.UUID; + +/** + * Exception thrown when a {@link PermissionHandler#loadUserData(UUID uuid)} request fails. + */ +public class PermissionLoadUserException extends Exception { + + public PermissionLoadUserException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/permission/handlers/PermissionsExHandler.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/permission/handlers/PermissionsExHandler.java new file mode 100644 index 00000000..db834ca4 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/permission/handlers/PermissionsExHandler.java @@ -0,0 +1,90 @@ +package fr.xephi.authme.permission.handlers; + +import fr.xephi.authme.data.limbo.UserGroup; +import fr.xephi.authme.permission.PermissionNode; +import fr.xephi.authme.permission.PermissionsSystemType; +import org.bukkit.OfflinePlayer; +import ru.tehkode.permissions.PermissionManager; +import ru.tehkode.permissions.PermissionUser; +import ru.tehkode.permissions.bukkit.PermissionsEx; + +import java.util.ArrayList; +import java.util.List; + +import static java.util.stream.Collectors.toList; + +/** + * Handler for PermissionsEx. + * + * @see PermissionsEx Bukkit page + * @see PermissionsEx on Github + */ +public class PermissionsExHandler implements PermissionHandler { + + private PermissionManager permissionManager; + + public PermissionsExHandler() throws PermissionHandlerException { + permissionManager = PermissionsEx.getPermissionManager(); + if (permissionManager == null) { + throw new PermissionHandlerException("Could not get manager of PermissionsEx"); + } + } + + @Override + public boolean addToGroup(OfflinePlayer player, UserGroup group) { + if (!PermissionsEx.getPermissionManager().getGroupNames().contains(group)) { + return false; + } + + PermissionUser user = PermissionsEx.getUser(player.getName()); + user.addGroup(group.getGroupName()); + return true; + } + + @Override + public boolean hasGroupSupport() { + return true; + } + + @Override + public boolean hasPermissionOffline(String name, PermissionNode node) { + PermissionUser user = permissionManager.getUser(name); + return user.has(node.getNode()); + } + + @Override + public boolean isInGroup(OfflinePlayer player, UserGroup group) { + PermissionUser user = permissionManager.getUser(player.getName()); + return user.inGroup(group.getGroupName()); + } + + @Override + public boolean removeFromGroup(OfflinePlayer player, UserGroup group) { + PermissionUser user = permissionManager.getUser(player.getName()); + user.removeGroup(group.getGroupName()); + return true; + } + + @Override + public boolean setGroup(OfflinePlayer player, UserGroup group) { + List groups = new ArrayList<>(); + groups.add(group.getGroupName()); + + PermissionUser user = permissionManager.getUser(player.getName()); + user.setParentsIdentifier(groups); + return true; + } + + @Override + public List getGroups(OfflinePlayer player) { + PermissionUser user = permissionManager.getUser(player.getName()); + return user.getParentIdentifiers(null).stream() + .map(i -> new UserGroup(i, null)) + .collect(toList()); + } + + @Override + public PermissionsSystemType getPermissionSystem() { + return PermissionsSystemType.PERMISSIONS_EX; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/permission/handlers/VaultHandler.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/permission/handlers/VaultHandler.java new file mode 100644 index 00000000..87f5fc27 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/permission/handlers/VaultHandler.java @@ -0,0 +1,105 @@ +package fr.xephi.authme.permission.handlers; + +import com.google.common.annotations.VisibleForTesting; +import fr.xephi.authme.data.limbo.UserGroup; +import fr.xephi.authme.permission.PermissionNode; +import fr.xephi.authme.permission.PermissionsSystemType; +import net.milkbowl.vault.permission.Permission; +import org.bukkit.OfflinePlayer; +import org.bukkit.Server; +import org.bukkit.plugin.RegisteredServiceProvider; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static java.util.stream.Collectors.toList; + +/** + * Handler for permissions via Vault. + * + * @see Vault Bukkit page + * @see Vault on Github + */ +public class VaultHandler implements PermissionHandler { + + private Permission vaultProvider; + + public VaultHandler(Server server) throws PermissionHandlerException { + this.vaultProvider = getVaultPermission(server); + } + + /** + * Returns the Vault Permission interface. + * + * @param server the bukkit server instance + * @return the vault permission instance + * @throws PermissionHandlerException if the vault permission instance cannot be retrieved + */ + @VisibleForTesting + Permission getVaultPermission(Server server) throws PermissionHandlerException { + // Get the permissions provider service + RegisteredServiceProvider permissionProvider = server + .getServicesManager().getRegistration(Permission.class); + if (permissionProvider == null) { + throw new PermissionHandlerException("Could not load permissions provider service"); + } + + // Get the Vault provider and make sure it's valid + Permission vaultPerms = permissionProvider.getProvider(); + if (vaultPerms == null) { + throw new PermissionHandlerException("Could not load Vault permissions provider"); + } + return vaultPerms; + } + + @Override + public boolean addToGroup(OfflinePlayer player, UserGroup group) { + return vaultProvider.playerAddGroup(null, player, group.getGroupName()); + } + + @Override + public boolean hasGroupSupport() { + return vaultProvider.hasGroupSupport(); + } + + @Override + public boolean hasPermissionOffline(String name, PermissionNode node) { + return vaultProvider.has((String) null, name, node.getNode()); + } + + @Override + public boolean isInGroup(OfflinePlayer player, UserGroup group) { + return vaultProvider.playerInGroup(null, player, group.getGroupName()); + } + + @Override + public boolean removeFromGroup(OfflinePlayer player, UserGroup group) { + return vaultProvider.playerRemoveGroup(null, player, group.getGroupName()); + } + + @Override + public boolean setGroup(OfflinePlayer player, UserGroup group) { + for (UserGroup g : getGroups(player)) { + removeFromGroup(player, g); + } + + return vaultProvider.playerAddGroup(null, player, group.getGroupName()); + } + + @Override + public List getGroups(OfflinePlayer player) { + String[] groups = vaultProvider.getPlayerGroups(null, player); + return groups == null ? Collections.emptyList() : Arrays.stream(groups).map(UserGroup::new).collect(toList()); + } + + @Override + public UserGroup getPrimaryGroup(OfflinePlayer player) { + return new UserGroup(vaultProvider.getPrimaryGroup(null, player)); + } + + @Override + public PermissionsSystemType getPermissionSystem() { + return PermissionsSystemType.VAULT; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/permission/handlers/ZPermissionsHandler.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/permission/handlers/ZPermissionsHandler.java new file mode 100644 index 00000000..0b632de6 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/permission/handlers/ZPermissionsHandler.java @@ -0,0 +1,79 @@ +package fr.xephi.authme.permission.handlers; + +import fr.xephi.authme.data.limbo.UserGroup; +import fr.xephi.authme.permission.PermissionNode; +import fr.xephi.authme.permission.PermissionsSystemType; +import org.bukkit.Bukkit; +import org.bukkit.OfflinePlayer; +import org.tyrannyofheaven.bukkit.zPermissions.ZPermissionsService; + +import java.util.Collection; +import java.util.Map; + +import static java.util.stream.Collectors.toList; + +/** + * Handler for zPermissions. + * + * @see zPermissions Bukkit page + * @see zPermissions on Github + */ +public class ZPermissionsHandler implements PermissionHandler { + + private ZPermissionsService zPermissionsService; + + public ZPermissionsHandler() throws PermissionHandlerException { + // Set the zPermissions service and make sure it's valid + ZPermissionsService zPermissionsService = Bukkit.getServicesManager().load(ZPermissionsService.class); + if (zPermissionsService == null) { + throw new PermissionHandlerException("Failed to get the ZPermissions service!"); + } + this.zPermissionsService = zPermissionsService; + } + + @Override + public boolean addToGroup(OfflinePlayer player, UserGroup group) { + return Bukkit.dispatchCommand(Bukkit.getConsoleSender(), + "permissions player " + player.getName() + " addgroup " + group.getGroupName()); + } + + @Override + public boolean hasGroupSupport() { + return true; + } + + @Override + public boolean hasPermissionOffline(String name, PermissionNode node) { + Map perms = zPermissionsService.getPlayerPermissions(null, null, name); + return perms.getOrDefault(node.getNode(), false); + } + + @Override + public boolean removeFromGroup(OfflinePlayer player, UserGroup group) { + return Bukkit.dispatchCommand(Bukkit.getConsoleSender(), + "permissions player " + player.getName() + " removegroup " + group.getGroupName()); + } + + @Override + public boolean setGroup(OfflinePlayer player, UserGroup group) { + return Bukkit.dispatchCommand(Bukkit.getConsoleSender(), + "permissions player " + player.getName() + " setgroup " + group.getGroupName()); + } + + @Override + public Collection getGroups(OfflinePlayer player) { + return zPermissionsService.getPlayerGroups(player.getName()).stream() + .map(UserGroup::new) + .collect(toList()); + } + + @Override + public UserGroup getPrimaryGroup(OfflinePlayer player) { + return new UserGroup(zPermissionsService.getPlayerPrimaryGroup(player.getName())); + } + + @Override + public PermissionsSystemType getPermissionSystem() { + return PermissionsSystemType.Z_PERMISSIONS; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/AsynchronousProcess.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/AsynchronousProcess.java new file mode 100644 index 00000000..80c06313 --- /dev/null +++ b/plugin/platform-bukkit/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/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/Management.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/Management.java new file mode 100644 index 00000000..454c0c18 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/Management.java @@ -0,0 +1,107 @@ +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; +import fr.xephi.authme.process.login.AsynchronousLogin; +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.register.executors.RegistrationMethod; +import fr.xephi.authme.process.register.executors.RegistrationParameters; +import fr.xephi.authme.process.unregister.AsynchronousUnregister; +import fr.xephi.authme.service.BukkitService; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import javax.inject.Inject; + +/** + * Performs auth actions, e.g. when a player joins, registers or wants to change his password. + */ +public class Management { + + @Inject + private BukkitService bukkitService; + + // Processes + @Inject + private AsyncAddEmail asyncAddEmail; + @Inject + private AsyncChangeEmail asyncChangeEmail; + @Inject + private AsynchronousLogout asynchronousLogout; + @Inject + private AsynchronousQuit asynchronousQuit; + @Inject + private AsynchronousJoin asynchronousJoin; + @Inject + private AsyncRegister asyncRegister; + @Inject + private AsynchronousLogin asynchronousLogin; + @Inject + private AsynchronousUnregister asynchronousUnregister; + @Inject + private AsyncChangePassword asyncChangePassword; + + Management() { + } + + + public void performLogin(Player player, String password) { + runTask(() -> asynchronousLogin.login(player, password)); + } + + public void forceLogin(Player player) { + runTask(() -> asynchronousLogin.forceLogin(player)); + } + + public void forceLogin(Player player, boolean quiet) { + runTask(() -> asynchronousLogin.forceLogin(player, quiet)); + } + + public void performLogout(Player player) { + runTask(() -> asynchronousLogout.logout(player)); + } + + public

void performRegister(RegistrationMethod

variant, P parameters) { + runTask(() -> asyncRegister.register(variant, parameters)); + } + + public void performUnregister(Player player, String password) { + runTask(() -> asynchronousUnregister.unregister(player, password)); + } + + public void performUnregisterByAdmin(CommandSender initiator, String name, Player player) { + runTask(() -> asynchronousUnregister.adminUnregister(initiator, name, player)); + } + + public void performJoin(Player player) { + runTask(() -> asynchronousJoin.processJoin(player)); + } + + public void performQuit(Player player) { + runTask(() -> asynchronousQuit.processQuit(player)); + } + + public void performAddEmail(Player player, String newEmail) { + runTask(() -> asyncAddEmail.addEmail(player, newEmail)); + } + + public void performChangeEmail(Player player, String oldEmail, String newEmail) { + runTask(() -> asyncChangeEmail.changeEmail(player, oldEmail, newEmail)); + } + + public void performPasswordChange(Player player, String oldPassword, String newPassword) { + runTask(() -> asyncChangePassword.changePassword(player, oldPassword, newPassword)); + } + + public void performPasswordChangeAsAdmin(CommandSender sender, String playerName, String newPassword) { + runTask(() -> asyncChangePassword.changePasswordAsAdmin(sender, playerName, newPassword)); + } + + private void runTask(Runnable runnable) { + bukkitService.runTaskOptionallyAsync(runnable); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/SyncProcessManager.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/SyncProcessManager.java new file mode 100644 index 00000000..0fdfdde3 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/SyncProcessManager.java @@ -0,0 +1,62 @@ +package fr.xephi.authme.process; + +import fr.xephi.authme.process.login.ProcessSyncPlayerLogin; +import fr.xephi.authme.process.logout.ProcessSyncPlayerLogout; +import fr.xephi.authme.process.quit.ProcessSyncPlayerQuit; +import fr.xephi.authme.process.register.ProcessSyncEmailRegister; +import fr.xephi.authme.process.register.ProcessSyncPasswordRegister; +import fr.xephi.authme.service.BukkitService; +import org.bukkit.entity.Player; + +import javax.inject.Inject; +import java.util.List; + +/** + * 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 ProcessSyncPlayerLogout processSyncPlayerLogout; + @Inject + private ProcessSyncPlayerQuit processSyncPlayerQuit; + + + public void processSyncEmailRegister(Player player) { + runTask(() -> processSyncEmailRegister.processEmailRegister(player)); + } + + public void processSyncPasswordRegister(Player player) { + runTask(() -> processSyncPasswordRegister.processPasswordRegister(player)); + } + + public void processSyncPlayerLogout(Player player) { + runTask(() -> processSyncPlayerLogout.processSyncLogout(player)); + } + + public void processSyncPlayerLogin(Player player, boolean isFirstLogin, List authsWithSameIp) { + runTask(() -> processSyncPlayerLogin.processPlayerLogin(player, isFirstLogin, authsWithSameIp)); + } + + public void processSyncPlayerQuit(Player player, boolean wasLoggedIn) { + runTask(() -> processSyncPlayerQuit.processSyncQuit(player, wasLoggedIn)); + } + + private void runTask(Runnable runnable) { + bukkitService.scheduleSyncTaskFromOptionallyAsyncTask(runnable); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/SynchronousProcess.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/SynchronousProcess.java new file mode 100644 index 00000000..6c23a0f8 --- /dev/null +++ b/plugin/platform-bukkit/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/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/changepassword/AsyncChangePassword.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/changepassword/AsyncChangePassword.java new file mode 100644 index 00000000..aa45d228 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/changepassword/AsyncChangePassword.java @@ -0,0 +1,102 @@ +package fr.xephi.authme.process.changepassword; + +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.data.auth.PlayerCache; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.process.AsynchronousProcess; +import fr.xephi.authme.security.PasswordSecurity; +import fr.xephi.authme.security.crypts.HashedPassword; +import fr.xephi.authme.service.CommonService; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import javax.inject.Inject; +import java.util.Locale; + +public class AsyncChangePassword implements AsynchronousProcess { + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(AsyncChangePassword.class); + + @Inject + private DataSource dataSource; + + @Inject + private CommonService commonService; + + @Inject + private PasswordSecurity passwordSecurity; + + @Inject + private PlayerCache playerCache; + + AsyncChangePassword() { + } + + /** + * Change password for an online player + * + * @param player the player + * @param oldPassword the old password used by the player + * @param newPassword the new password chosen by the player + */ + public void changePassword(Player player, String oldPassword, String newPassword) { + String name = player.getName().toLowerCase(Locale.ROOT); + PlayerAuth auth = playerCache.getAuth(name); + if (passwordSecurity.comparePassword(oldPassword, auth.getPassword(), player.getName())) { + HashedPassword hashedPassword = passwordSecurity.computeHash(newPassword, name); + auth.setPassword(hashedPassword); + + if (!dataSource.updatePassword(auth)) { + commonService.send(player, MessageKey.ERROR); + return; + } + + // TODO: send an update when a messaging service will be implemented (PASSWORD_CHANGED) + + playerCache.updatePlayer(auth); + commonService.send(player, MessageKey.PASSWORD_CHANGED_SUCCESS); + logger.info(player.getName() + " changed his password"); + } else { + commonService.send(player, MessageKey.WRONG_PASSWORD); + } + } + + /** + * Change a user's password as an administrator, without asking for the previous one + * + * @param sender who is performing the operation, null if called by other plugins + * @param playerName the player name + * @param newPassword the new password chosen for the player + */ + public void changePasswordAsAdmin(CommandSender sender, String playerName, String newPassword) { + String lowerCaseName = playerName.toLowerCase(Locale.ROOT); + if (!(playerCache.isAuthenticated(lowerCaseName) || dataSource.isAuthAvailable(lowerCaseName))) { + if (sender == null) { + logger.warning("Tried to change password for user " + lowerCaseName + " but it doesn't exist!"); + } else { + commonService.send(sender, MessageKey.UNKNOWN_USER); + } + return; + } + + HashedPassword hashedPassword = passwordSecurity.computeHash(newPassword, lowerCaseName); + if (dataSource.updatePassword(lowerCaseName, hashedPassword)) { + // TODO: send an update when a messaging service will be implemented (PASSWORD_CHANGED) + + if (sender != null) { + commonService.send(sender, MessageKey.PASSWORD_CHANGED_SUCCESS); + logger.info(sender.getName() + " changed password of " + lowerCaseName); + } else { + logger.info("Changed password of " + lowerCaseName); + } + } else { + if (sender != null) { + commonService.send(sender, MessageKey.ERROR); + } + logger.warning("An error occurred while changing password for user " + lowerCaseName + "!"); + } + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/email/AsyncAddEmail.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/email/AsyncAddEmail.java new file mode 100644 index 00000000..4001420c --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/email/AsyncAddEmail.java @@ -0,0 +1,95 @@ +package fr.xephi.authme.process.email; + +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.data.auth.PlayerCache; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.events.EmailChangedEvent; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.process.AsynchronousProcess; +import fr.xephi.authme.service.BukkitService; +import fr.xephi.authme.service.CommonService; +import fr.xephi.authme.service.ValidationService; +import fr.xephi.authme.util.Utils; +import org.bukkit.entity.Player; + +import javax.inject.Inject; +import java.util.Locale; + +/** + * Async task to add an email to an account. + */ +public class AsyncAddEmail implements AsynchronousProcess { + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(AsyncAddEmail.class); + + @Inject + private CommonService service; + + @Inject + private DataSource dataSource; + + @Inject + private PlayerCache playerCache; + + @Inject + private ValidationService validationService; + + @Inject + private BukkitService bukkitService; + + AsyncAddEmail() { + } + + /** + * Handles the request to add the given email to the player's account. + * + * @param player the player to add the email to + * @param email the email to add + */ + public void addEmail(Player player, String email) { + String playerName = player.getName().toLowerCase(Locale.ROOT); + + if (playerCache.isAuthenticated(playerName)) { + PlayerAuth auth = playerCache.getAuth(playerName); + String currentEmail = auth.getEmail(); + + if (!Utils.isEmailEmpty(currentEmail)) { + service.send(player, MessageKey.USAGE_CHANGE_EMAIL); + } else if (!validationService.validateEmail(email)) { + service.send(player, MessageKey.INVALID_EMAIL); + } else if (!validationService.isEmailFreeForRegistration(email, player)) { + service.send(player, MessageKey.EMAIL_ALREADY_USED_ERROR); + } else { + EmailChangedEvent event = bukkitService.createAndCallEvent(isAsync + -> new EmailChangedEvent(player, null, email, isAsync)); + if (event.isCancelled()) { + logger.info("Could not add email to player '" + player + "' – event was cancelled"); + service.send(player, MessageKey.EMAIL_ADD_NOT_ALLOWED); + return; + } + auth.setEmail(email); + if (dataSource.updateEmail(auth)) { + playerCache.updatePlayer(auth); + // TODO: send an update when a messaging service will be implemented (ADD_MAIL) + service.send(player, MessageKey.EMAIL_ADDED_SUCCESS); + } else { + logger.warning("Could not save email for player '" + player + "'"); + service.send(player, MessageKey.ERROR); + } + } + } else { + sendUnloggedMessage(player); + } + } + + private void sendUnloggedMessage(Player player) { + if (dataSource.isAuthAvailable(player.getName())) { + service.send(player, MessageKey.LOGIN_MESSAGE); + } else { + service.send(player, MessageKey.REGISTER_MESSAGE); + } + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/email/AsyncChangeEmail.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/email/AsyncChangeEmail.java new file mode 100644 index 00000000..74d9c39b --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/email/AsyncChangeEmail.java @@ -0,0 +1,107 @@ +package fr.xephi.authme.process.email; + +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.data.auth.PlayerCache; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.events.EmailChangedEvent; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.process.AsynchronousProcess; +import fr.xephi.authme.service.BukkitService; +import fr.xephi.authme.service.CommonService; +import fr.xephi.authme.service.ValidationService; +import org.bukkit.entity.Player; + +import javax.inject.Inject; +import java.util.Locale; + +/** + * Async task for changing the email. + */ +public class AsyncChangeEmail implements AsynchronousProcess { + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(AsyncChangeEmail.class); + + @Inject + private CommonService service; + + @Inject + private PlayerCache playerCache; + + @Inject + private DataSource dataSource; + + @Inject + private ValidationService validationService; + + @Inject + private BukkitService bukkitService; + + AsyncChangeEmail() { + } + + /** + * Handles the request to change the player's email address. + * + * @param player the player to change the email for + * @param oldEmail provided old email + * @param newEmail provided new email + */ + public void changeEmail(Player player, String oldEmail, String newEmail) { + String playerName = player.getName().toLowerCase(Locale.ROOT); + if (playerCache.isAuthenticated(playerName)) { + PlayerAuth auth = playerCache.getAuth(playerName); + String currentEmail = auth.getEmail(); + + if (currentEmail == null) { + service.send(player, MessageKey.USAGE_ADD_EMAIL); + } else if (newEmail == null || !validationService.validateEmail(newEmail)) { + service.send(player, MessageKey.INVALID_NEW_EMAIL); + } else if (!oldEmail.equalsIgnoreCase(currentEmail)) { + service.send(player, MessageKey.INVALID_OLD_EMAIL); + } else if (!validationService.isEmailFreeForRegistration(newEmail, player)) { + service.send(player, MessageKey.EMAIL_ALREADY_USED_ERROR); + } else { + saveNewEmail(auth, player, oldEmail, newEmail); + } + } else { + outputUnloggedMessage(player); + } + } + + /** + * Saves the new email value into the database and informs services. + * + * @param auth the player auth object + * @param player the player object + * @param oldEmail the old email value + * @param newEmail the new email value + */ + private void saveNewEmail(PlayerAuth auth, Player player, String oldEmail, String newEmail) { + EmailChangedEvent event = bukkitService.createAndCallEvent(isAsync + -> new EmailChangedEvent(player, oldEmail, newEmail, isAsync)); + if (event.isCancelled()) { + logger.info("Could not change email for player '" + player + "' – event was cancelled"); + service.send(player, MessageKey.EMAIL_CHANGE_NOT_ALLOWED); + return; + } + + auth.setEmail(newEmail); + if (dataSource.updateEmail(auth)) { + playerCache.updatePlayer(auth); + // TODO: send an update when a messaging service will be implemented (CHANGE_MAIL) + service.send(player, MessageKey.EMAIL_CHANGED_SUCCESS); + } else { + service.send(player, MessageKey.ERROR); + } + } + + private void outputUnloggedMessage(Player player) { + if (dataSource.isAuthAvailable(player.getName())) { + service.send(player, MessageKey.LOGIN_MESSAGE); + } else { + service.send(player, MessageKey.REGISTER_MESSAGE); + } + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java new file mode 100644 index 00000000..424c4989 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java @@ -0,0 +1,246 @@ +package fr.xephi.authme.process.join; + +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.data.ProxySessionManager; +import fr.xephi.authme.data.limbo.LimboService; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.events.ProtectInventoryEvent; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.permission.PlayerStatePermission; +import fr.xephi.authme.process.AsynchronousProcess; +import fr.xephi.authme.process.login.AsynchronousLogin; +import fr.xephi.authme.service.BukkitService; +import fr.xephi.authme.service.CommonService; +import fr.xephi.authme.service.PluginHookService; +import fr.xephi.authme.service.SessionService; +import fr.xephi.authme.service.ValidationService; +import fr.xephi.authme.service.bungeecord.BungeeSender; +import fr.xephi.authme.service.bungeecord.MessageType; +import fr.xephi.authme.service.velocity.VMessageType; +import fr.xephi.authme.service.velocity.VelocitySender; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.commandconfig.CommandManager; +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.InternetProtocolUtils; +import fr.xephi.authme.util.PlayerUtils; +import org.bukkit.GameMode; +import org.bukkit.Server; +import org.bukkit.entity.Player; + +import javax.inject.Inject; +import java.util.Locale; + +import static fr.xephi.authme.service.BukkitService.TICKS_PER_SECOND; +import static fr.xephi.authme.settings.properties.RestrictionSettings.PROTECT_INVENTORY_BEFORE_LOGIN; + +/** + * Asynchronous process for when a player joins. + */ +public class AsynchronousJoin implements AsynchronousProcess { + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(AsynchronousJoin.class); + + @Inject + private Server server; + + @Inject + private Settings settings; + + @Inject + private DataSource database; + + @Inject + private CommonService service; + + @Inject + private LimboService limboService; + + @Inject + private PluginHookService pluginHookService; + + @Inject + private BukkitService bukkitService; + + @Inject + private AsynchronousLogin asynchronousLogin; + + @Inject + private CommandManager commandManager; + + @Inject + private ValidationService validationService; + + @Inject + private SessionService sessionService; + + @Inject + private BungeeSender bungeeSender; + + @Inject + private VelocitySender velocitySender; + + @Inject + private ProxySessionManager proxySessionManager; + + AsynchronousJoin() { + } + + /** + * Processes the given player that has just joined. + * + * @param player the player to process + */ + public void processJoin(Player player) { + String name = player.getName().toLowerCase(Locale.ROOT); + String ip = PlayerUtils.getPlayerIp(player); + + if (!validationService.fulfillsNameRestrictions(player)) { + handlePlayerWithUnmetNameRestriction(player, ip); + return; + } + + if (service.getProperty(RestrictionSettings.UNRESTRICTED_NAMES).contains(name)) { + return; + } + + if (service.getProperty(RestrictionSettings.FORCE_SURVIVAL_MODE) + && player.getGameMode() != GameMode.SURVIVAL + && !service.hasPermission(player, PlayerStatePermission.BYPASS_FORCE_SURVIVAL)) { + bukkitService.scheduleSyncTaskFromOptionallyAsyncTask(() -> player.setGameMode(GameMode.SURVIVAL)); + } + + if (service.getProperty(HooksSettings.DISABLE_SOCIAL_SPY)) { + pluginHookService.setEssentialsSocialSpyStatus(player, false); + } + + if (!validatePlayerCountForIp(player, ip)) { + return; + } + + boolean isAuthAvailable = database.isAuthAvailable(name); + + if (isAuthAvailable) { + // Protect inventory + if (service.getProperty(PROTECT_INVENTORY_BEFORE_LOGIN)) { + ProtectInventoryEvent ev = bukkitService.createAndCallEvent( + isAsync -> new ProtectInventoryEvent(player, isAsync)); + if (ev.isCancelled()) { + player.updateInventory(); + logger.fine("ProtectInventoryEvent has been cancelled for " + player.getName() + "..."); + } + } + + // Session logic + if (sessionService.canResumeSession(player)) { + if (velocitySender.isEnabled()) { + bukkitService.scheduleSyncDelayedTask(() -> + velocitySender.sendAuthMeVelocityMessage(player, VMessageType.LOGIN), service.getProperty(HooksSettings.PROXY_SEND_DELAY)); + } + service.send(player, MessageKey.SESSION_RECONNECTION); + // Run commands + bukkitService.scheduleSyncTaskFromOptionallyAsyncTask( + () -> commandManager.runCommandsOnSessionLogin(player)); + bukkitService.runTaskOptionallyAsync(() -> asynchronousLogin.forceLogin(player)); + return; + } else if (proxySessionManager.shouldResumeSession(name)) { + service.send(player, MessageKey.SESSION_RECONNECTION); + // Run commands + bukkitService.scheduleSyncTaskFromOptionallyAsyncTask( + () -> commandManager.runCommandsOnSessionLogin(player)); + bukkitService.runTaskOptionallyAsync(() -> asynchronousLogin.forceLogin(player)); + logger.info("The user " + player.getName() + " has been automatically logged in, " + + "as present in autologin queue."); + return; + } + } else if (!service.getProperty(RegistrationSettings.FORCE)) { + + // Skip if registration is optional + + if (bungeeSender.isEnabled()) { + // As described at https://www.spigotmc.org/wiki/bukkit-bungee-plugin-messaging-channel/ + // "Keep in mind that you can't send plugin messages directly after a player joins." + bukkitService.scheduleSyncDelayedTask(() -> + bungeeSender.sendAuthMeBungeecordMessage(player, MessageType.LOGIN), settings.getProperty(HooksSettings.PROXY_SEND_DELAY)); + } + if (velocitySender.isEnabled()) { + bukkitService.scheduleSyncDelayedTask(() -> + velocitySender.sendAuthMeVelocityMessage(player, VMessageType.LOGIN), settings.getProperty(HooksSettings.PROXY_SEND_DELAY)); + } + return; + } + + processJoinSync(player, isAuthAvailable); + } + + private void handlePlayerWithUnmetNameRestriction(Player player, String ip) { + bukkitService.scheduleSyncTaskFromOptionallyAsyncTask(() -> { + player.kickPlayer(service.retrieveSingleMessage(player, MessageKey.NOT_OWNER_ERROR)); + if (service.getProperty(RestrictionSettings.BAN_UNKNOWN_IP)) { + server.banIP(ip); + } + }); + } + + /** + * Performs various operations in sync mode for an unauthenticated player (such as blindness effect and + * limbo player creation). + * + * @param player the player to process + * @param isAuthAvailable true if the player is registered, false otherwise + */ + private void processJoinSync(Player player, boolean isAuthAvailable) { + int registrationTimeout = service.getProperty(RestrictionSettings.TIMEOUT) * TICKS_PER_SECOND; + + bukkitService.scheduleSyncTaskFromOptionallyAsyncTask(() -> { + limboService.createLimboPlayer(player, isAuthAvailable); + + player.setNoDamageTicks(registrationTimeout); + if (pluginHookService.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; + + // AuthMeReReloaded - Fix potion apply on Folia + bukkitService.runTaskIfFolia(player, () -> player.addPotionEffect(bukkitService.createBlindnessEffect(blindTimeOut))); + } + commandManager.runCommandsOnJoin(player); + }); + } + + /** + * 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(Player player, String ip) { + if (service.getProperty(RestrictionSettings.MAX_JOIN_PER_IP) > 0 + && !service.hasPermission(player, PlayerStatePermission.ALLOW_MULTIPLE_ACCOUNTS) + && !InternetProtocolUtils.isLoopbackAddress(ip) + && countOnlinePlayersByIp(ip) > service.getProperty(RestrictionSettings.MAX_JOIN_PER_IP)) { + + bukkitService.scheduleSyncTaskFromOptionallyAsyncTask( + () -> player.kickPlayer(service.retrieveSingleMessage(player, MessageKey.SAME_IP_ONLINE))); + return false; + } + return true; + } + + private int countOnlinePlayersByIp(String ip) { + int count = 0; + for (Player player : bukkitService.getOnlinePlayers()) { + if (ip.equalsIgnoreCase(PlayerUtils.getPlayerIp(player))) { + ++count; + } + } + return count; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java new file mode 100644 index 00000000..988124d9 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java @@ -0,0 +1,395 @@ +package fr.xephi.authme.process.login; + +import com.google.common.annotations.VisibleForTesting; +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.data.TempbanManager; +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.data.auth.PlayerCache; +import fr.xephi.authme.data.captcha.LoginCaptchaManager; +import fr.xephi.authme.data.limbo.LimboMessageType; +import fr.xephi.authme.data.limbo.LimboPlayerState; +import fr.xephi.authme.data.limbo.LimboService; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.events.AuthMeAsyncPreLoginEvent; +import fr.xephi.authme.events.FailedLoginEvent; +import fr.xephi.authme.mail.EmailService; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.permission.AdminPermission; +import fr.xephi.authme.permission.PlayerPermission; +import fr.xephi.authme.permission.PlayerStatePermission; +import fr.xephi.authme.process.AsynchronousProcess; +import fr.xephi.authme.process.SyncProcessManager; +import fr.xephi.authme.security.PasswordSecurity; +import fr.xephi.authme.service.BukkitService; +import fr.xephi.authme.service.CommonService; +import fr.xephi.authme.service.SessionService; +import fr.xephi.authme.service.bungeecord.BungeeSender; +import fr.xephi.authme.service.bungeecord.MessageType; +import fr.xephi.authme.service.velocity.VelocitySender; +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.HooksSettings; +import fr.xephi.authme.settings.properties.PluginSettings; +import fr.xephi.authme.settings.properties.RestrictionSettings; +import fr.xephi.authme.util.InternetProtocolUtils; +import fr.xephi.authme.util.PlayerUtils; +import fr.xephi.authme.util.Utils; +import org.bukkit.ChatColor; +import org.bukkit.entity.Player; + +import javax.inject.Inject; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +/** + * Asynchronous task for a player login. + */ +public class AsynchronousLogin implements AsynchronousProcess { + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(AsynchronousLogin.class); + + @Inject + private DataSource dataSource; + + @Inject + private CommonService service; + + @Inject + private PlayerCache playerCache; + + @Inject + private SyncProcessManager syncProcessManager; + + @Inject + private BukkitService bukkitService; + + @Inject + private PasswordSecurity passwordSecurity; + + @Inject + private LoginCaptchaManager loginCaptchaManager; + + @Inject + private TempbanManager tempbanManager; + + @Inject + private LimboService limboService; + + @Inject + private EmailService emailService; + + @Inject + private SessionService sessionService; + @Inject + private Settings settings; + @Inject + private BungeeSender bungeeSender; + @Inject + private VelocitySender velocitySender; + + AsynchronousLogin() { + } + + /** + * Processes a player's login request. + * + * @param player the player to log in + * @param password the password to log in with + */ + public void login(Player player, String password) { + PlayerAuth auth = getPlayerAuth(player); + if (auth != null && checkPlayerInfo(player, auth, password)) { + if (auth.getTotpKey() != null) { + limboService.resetMessageTask(player, LimboMessageType.TOTP_CODE); + limboService.getLimboPlayer(player.getName()).setState(LimboPlayerState.TOTP_REQUIRED); + // TODO #1141: Check if we should check limbo state before processing password + } else { + performLogin(player, auth); + } + } + } + + /** + * Logs a player in without requiring a password. + * + * @param player the player to log in + */ + public synchronized void forceLogin(Player player) { + PlayerAuth auth = getPlayerAuth(player); + if (auth != null) { + performLogin(player, auth); + } + } + + /** + * Logs a player in without requiring a password. + * + * @param player the player to log in + * @param quiet if true no messages will be sent + */ + public void forceLogin(Player player, boolean quiet) { + PlayerAuth auth = getPlayerAuth(player, quiet); + if (auth != null) { + performLogin(player, auth); + } + } + + /** + * Checks the precondition for authentication (like user known) and returns + * the player's {@link PlayerAuth} object. + * + * @param player the player to check + * @return the PlayerAuth object, or {@code null} if the player doesn't exist or may not log in + * (e.g. because he is already logged in) + */ + private PlayerAuth getPlayerAuth(Player player) { + return getPlayerAuth(player, false); + } + + /** + * Checks the precondition for authentication (like user known) and returns + * the player's {@link PlayerAuth} object. + * + * @param player the player to check + * @param quiet don't send messages + * @return the PlayerAuth object, or {@code null} if the player doesn't exist or may not log in + * (e.g. because he is already logged in) + */ + private PlayerAuth getPlayerAuth(Player player, boolean quiet) { + String name = player.getName().toLowerCase(Locale.ROOT); + if (playerCache.isAuthenticated(name)) { + if (!quiet) { + service.send(player, MessageKey.ALREADY_LOGGED_IN_ERROR); + } + return null; + } + + PlayerAuth auth = dataSource.getAuth(name); + if (auth == null) { + if (!quiet) { + service.send(player, MessageKey.UNKNOWN_USER); + } + // Recreate the message task to immediately send the message again as response + limboService.resetMessageTask(player, LimboMessageType.REGISTER); + return null; + } + + if (!service.getProperty(DatabaseSettings.MYSQL_COL_GROUP).isEmpty() + && auth.getGroupId() == service.getProperty(HooksSettings.NON_ACTIVATED_USERS_GROUP)) { + if (!quiet) { + service.send(player, MessageKey.ACCOUNT_NOT_ACTIVATED); + } + return null; + } + + String ip = PlayerUtils.getPlayerIp(player); + if (hasReachedMaxLoggedInPlayersForIp(player, ip)) { + if (!quiet) { + service.send(player, MessageKey.ALREADY_LOGGED_IN_ERROR); + } + return null; + } + + boolean isAsync = service.getProperty(PluginSettings.USE_ASYNC_TASKS); + AuthMeAsyncPreLoginEvent event = new AuthMeAsyncPreLoginEvent(player, isAsync); + bukkitService.callEvent(event); + if (!event.canLogin()) { + return null; + } + return auth; + } + + /** + * Checks various conditions for regular player login (not used in force login). + * + * @param player the player requesting to log in + * @param auth the PlayerAuth object of the player + * @param password the password supplied by the player + * @return true if the password matches and all other conditions are met (e.g. no captcha required), + * false otherwise + */ + private boolean checkPlayerInfo(Player player, PlayerAuth auth, String password) { + String name = player.getName().toLowerCase(Locale.ROOT); + + // If captcha is required send a message to the player and deny to log in + if (loginCaptchaManager.isCaptchaRequired(name)) { + service.send(player, MessageKey.USAGE_CAPTCHA, loginCaptchaManager.getCaptchaCodeOrGenerateNew(name)); + return false; + } + + String ip = PlayerUtils.getPlayerIp(player); + + // Increase the counts here before knowing the result of the login. + loginCaptchaManager.increaseLoginFailureCount(name); + tempbanManager.increaseCount(ip, name); + + if (passwordSecurity.comparePassword(password, auth.getPassword(), player.getName())) { + return true; + } else { + handleWrongPassword(player, auth, ip); + return false; + } + } + + /** + * Handles a login with wrong password. + * + * @param player the player who attempted to log in + * @param auth the PlayerAuth object of the player + * @param ip the ip address of the player + */ + private void handleWrongPassword(Player player, PlayerAuth auth, String ip) { + logger.fine(player.getName() + " used the wrong password"); + + bukkitService.createAndCallEvent(isAsync -> new FailedLoginEvent(player, isAsync)); + if (tempbanManager.shouldTempban(ip)) { + tempbanManager.tempbanPlayer(player); + } else if (service.getProperty(RestrictionSettings.KICK_ON_WRONG_PASSWORD)) { + bukkitService.scheduleSyncTaskFromOptionallyAsyncTask( + () -> player.kickPlayer(service.retrieveSingleMessage(player, MessageKey.WRONG_PASSWORD))); + } else { + service.send(player, MessageKey.WRONG_PASSWORD); + + // If the authentication fails check if Captcha is required and send a message to the player + if (loginCaptchaManager.isCaptchaRequired(player.getName())) { + limboService.muteMessageTask(player); + service.send(player, MessageKey.USAGE_CAPTCHA, + loginCaptchaManager.getCaptchaCodeOrGenerateNew(player.getName())); + } else if (emailService.hasAllInformation() && !Utils.isEmailEmpty(auth.getEmail())) { + service.send(player, MessageKey.FORGOT_PASSWORD_MESSAGE); + } + } + } + + /** + * Sets the player to the logged in state. + * + * @param player the player to log in + * @param auth the associated PlayerAuth object + */ + public void performLogin(Player player, PlayerAuth auth) { + if (player.isOnline()) { + boolean isFirstLogin = (auth.getLastLogin() == null); + + // Update auth to reflect this new login + String ip = PlayerUtils.getPlayerIp(player); + auth.setRealName(player.getName()); + auth.setLastLogin(System.currentTimeMillis()); + auth.setLastIp(ip); + dataSource.updateSession(auth); + + // TODO: send an update when a messaging service will be implemented (SESSION) + + // Successful login, so reset the captcha & temp ban count + String name = player.getName(); + loginCaptchaManager.resetLoginFailureCount(name); + tempbanManager.resetCount(ip, name); + player.setNoDamageTicks(0); + + service.send(player, MessageKey.LOGIN_SUCCESS); + + // Other auths + List auths = dataSource.getAllAuthsByIp(auth.getLastIp()); + displayOtherAccounts(auths, player); + + String email = auth.getEmail(); + if (service.getProperty(EmailSettings.RECALL_PLAYERS) && Utils.isEmailEmpty(email)) { + service.send(player, MessageKey.ADD_EMAIL_MESSAGE); + } + + logger.fine(player.getName() + " logged in " + ip); + + // makes player loggedin + playerCache.updatePlayer(auth); + dataSource.setLogged(name); + sessionService.grantSession(name); + + if (bungeeSender.isEnabled()) { + // As described at https://www.spigotmc.org/wiki/bukkit-bungee-plugin-messaging-channel/ + // "Keep in mind that you can't send plugin messages directly after a player joins." + bukkitService.scheduleSyncDelayedTask(() -> + bungeeSender.sendAuthMeBungeecordMessage(player, MessageType.LOGIN), settings.getProperty(HooksSettings.PROXY_SEND_DELAY)); + } + + // As the scheduling executes the Task most likely after the current + // 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. + syncProcessManager.processSyncPlayerLogin(player, isFirstLogin, auths); + } else { + logger.warning("Player '" + player.getName() + "' wasn't online during login process, aborted..."); + } + } + + /** + * Sends info about the other accounts owned by the given player to the configured users. + * + * @param auths the names of the accounts also owned by the player + * @param player the player + */ + private void displayOtherAccounts(List auths, Player player) { + if (!service.getProperty(RestrictionSettings.DISPLAY_OTHER_ACCOUNTS) || auths.size() <= 1) { + return; + } + + List formattedNames = new ArrayList<>(auths.size()); + for (String currentName : auths) { + Player currentPlayer = bukkitService.getPlayerExact(currentName); + if (currentPlayer != null && currentPlayer.isOnline()) { + formattedNames.add(ChatColor.GREEN + currentPlayer.getName() + ChatColor.GRAY); + } else { + formattedNames.add(currentName); + } + } + + String message = ChatColor.GRAY + String.join(", ", formattedNames) + "."; + + logger.fine("The user " + player.getName() + " has " + auths.size() + " accounts:"); + logger.fine(message); + + for (Player onlinePlayer : bukkitService.getOnlinePlayers()) { + if (onlinePlayer.getName().equalsIgnoreCase(player.getName()) + && service.hasPermission(onlinePlayer, PlayerPermission.SEE_OWN_ACCOUNTS)) { + service.send(onlinePlayer, MessageKey.ACCOUNTS_OWNED_SELF, Integer.toString(auths.size())); + onlinePlayer.sendMessage(message); + } else if (service.hasPermission(onlinePlayer, AdminPermission.SEE_OTHER_ACCOUNTS)) { + service.send(onlinePlayer, MessageKey.ACCOUNTS_OWNED_OTHER, + player.getName(), Integer.toString(auths.size())); + onlinePlayer.sendMessage(message); + } + } + } + + /** + * Checks whether the maximum threshold of logged in player per IP address has been reached + * for the given player and IP address. + * + * @param player the player to process + * @param ip the associated ip address + * @return true if the threshold has been reached, false otherwise + */ + @VisibleForTesting + boolean hasReachedMaxLoggedInPlayersForIp(Player player, String ip) { + // Do not perform the check if player has multiple accounts permission or if IP is localhost + if (service.getProperty(RestrictionSettings.MAX_LOGIN_PER_IP) <= 0 + || service.hasPermission(player, PlayerStatePermission.ALLOW_MULTIPLE_ACCOUNTS) + || InternetProtocolUtils.isLoopbackAddress(ip)) { + return false; + } + + // Count logged in players with same IP address + String name = player.getName(); + int count = 0; + for (Player onlinePlayer : bukkitService.getOnlinePlayers()) { + if (ip.equalsIgnoreCase(PlayerUtils.getPlayerIp(onlinePlayer)) + && !onlinePlayer.getName().equals(name) + && dataSource.isLogged(onlinePlayer.getName().toLowerCase(Locale.ROOT))) { + ++count; + } + } + return count >= service.getProperty(RestrictionSettings.MAX_LOGIN_PER_IP); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/login/ProcessSyncPlayerLogin.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/login/ProcessSyncPlayerLogin.java new file mode 100644 index 00000000..ac022405 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/login/ProcessSyncPlayerLogin.java @@ -0,0 +1,132 @@ +package fr.xephi.authme.process.login; + +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.data.auth.PlayerCache; +import fr.xephi.authme.data.limbo.LimboPlayer; +import fr.xephi.authme.data.limbo.LimboService; +import fr.xephi.authme.events.LoginEvent; +import fr.xephi.authme.events.RestoreInventoryEvent; +import fr.xephi.authme.permission.PermissionsManager; +import fr.xephi.authme.permission.PlayerStatePermission; +import fr.xephi.authme.process.SynchronousProcess; +import fr.xephi.authme.service.BukkitService; +import fr.xephi.authme.service.CommonService; +import fr.xephi.authme.service.JoinMessageService; +import fr.xephi.authme.service.TeleportationService; +import fr.xephi.authme.service.bungeecord.BungeeSender; +import fr.xephi.authme.service.velocity.VMessageType; +import fr.xephi.authme.service.velocity.VelocitySender; +import fr.xephi.authme.settings.commandconfig.CommandManager; +import fr.xephi.authme.settings.properties.HooksSettings; +import fr.xephi.authme.settings.properties.RegistrationSettings; +import org.bukkit.entity.Player; +import org.bukkit.potion.PotionEffectType; + +import javax.inject.Inject; +import java.util.List; +import java.util.Locale; + +import static fr.xephi.authme.settings.properties.RestrictionSettings.PROTECT_INVENTORY_BEFORE_LOGIN; + +public class ProcessSyncPlayerLogin implements SynchronousProcess { + + @Inject + private BungeeSender bungeeSender; + + @Inject + private VelocitySender velocitySender; + + @Inject + private LimboService limboService; + + @Inject + private BukkitService bukkitService; + + @Inject + private TeleportationService teleportationService; + + @Inject + private PlayerCache playerCache; + + @Inject + private CommandManager commandManager; + + @Inject + private CommonService commonService; + + @Inject + private JoinMessageService joinMessageService; + + @Inject + private PermissionsManager permissionsManager; + + ProcessSyncPlayerLogin() { + } + + private void restoreInventory(Player player) { + RestoreInventoryEvent event = new RestoreInventoryEvent(player); + bukkitService.callEvent(event); + if (!event.isCancelled()) { + player.updateInventory(); + } + } + + /** + * Performs operations in sync mode for a player that has just logged in. + * + * @param player the player that was logged in + * @param isFirstLogin true if this is the first time the player logged in + * @param authsWithSameIp registered names with the same IP address as the player's + */ + public void processPlayerLogin(Player player, boolean isFirstLogin, List authsWithSameIp) { + final String name = player.getName().toLowerCase(Locale.ROOT); + final LimboPlayer limbo = limboService.getLimboPlayer(name); + + // Limbo contains the State of the Player before /login + if (limbo != null) { + limboService.restoreData(player); + } + + if (commonService.getProperty(PROTECT_INVENTORY_BEFORE_LOGIN)) { + restoreInventory(player); + } + + final PlayerAuth auth = playerCache.getAuth(name); + + // AuthMeReReloaded start - Fix #57 + if (isFirstLogin) { + auth.setQuitLocation(player.getLocation()); + } + // AuthMeReReloaded end - Fix #57 + + teleportationService.teleportOnLogin(player, auth, limbo); + + // We can now display the join message (if delayed) + joinMessageService.sendMessage(name); + + if (commonService.getProperty(RegistrationSettings.APPLY_BLIND_EFFECT)) { + player.removePotionEffect(PotionEffectType.BLINDNESS); + } + + // AuthMeVelocity start - send on player login + if (velocitySender.isEnabled()) { + bukkitService.scheduleSyncDelayedTask(() -> + velocitySender.sendAuthMeVelocityMessage(player, VMessageType.LOGIN), commonService.getProperty(HooksSettings.PROXY_SEND_DELAY)); + } + // AuthMeVelocity end + + // The Login event now fires (as intended) after everything is processed + bukkitService.callEvent(new LoginEvent(player)); + + // Login is now finished; we can force all commands + if (isFirstLogin) { + commandManager.runCommandsOnFirstLogin(player, authsWithSameIp); + } + commandManager.runCommandsOnLogin(player, authsWithSameIp); + + if (!permissionsManager.hasPermission(player, PlayerStatePermission.BYPASS_BUNGEE_SEND)) { + // Send Bungee stuff. The service will check if it is enabled or not. + bungeeSender.connectPlayerOnLogin(player); + } + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/logout/AsynchronousLogout.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/logout/AsynchronousLogout.java new file mode 100644 index 00000000..4b6af1e5 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/logout/AsynchronousLogout.java @@ -0,0 +1,81 @@ +package fr.xephi.authme.process.logout; + +import fr.xephi.authme.data.VerificationCodeManager; +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.data.auth.PlayerCache; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.process.AsynchronousProcess; +import fr.xephi.authme.process.SyncProcessManager; +import fr.xephi.authme.service.CommonService; +import fr.xephi.authme.service.SessionService; +import fr.xephi.authme.service.bungeecord.BungeeSender; +import fr.xephi.authme.service.bungeecord.MessageType; +import fr.xephi.authme.service.velocity.VMessageType; +import fr.xephi.authme.service.velocity.VelocitySender; +import org.bukkit.entity.Player; + +import javax.inject.Inject; +import java.util.Locale; + +/** + * Async task when a player wants to log out. + */ +public class AsynchronousLogout implements AsynchronousProcess { + + @Inject + private DataSource database; + + @Inject + private CommonService service; + + @Inject + private PlayerCache playerCache; + + @Inject + private VerificationCodeManager codeManager; + + @Inject + private SyncProcessManager syncProcessManager; + + @Inject + private SessionService sessionService; + + @Inject + private BungeeSender bungeeSender; + @Inject + private VelocitySender velocitySender; + + AsynchronousLogout() { + } + + /** + * Handles a player's request to log out. + * + * @param player the player wanting to log out + */ + public void logout(Player player) { + String name = player.getName().toLowerCase(Locale.ROOT); + if (!playerCache.isAuthenticated(name)) { + service.send(player, MessageKey.NOT_LOGGED_IN); + return; + } + + PlayerAuth auth = playerCache.getAuth(name); + database.updateSession(auth); + // TODO: send an update when a messaging service will be implemented (SESSION) + //if (service.getProperty(RestrictionSettings.SAVE_QUIT_LOCATION)) { + auth.setQuitLocation(player.getLocation()); + database.updateQuitLoc(auth); + // TODO: send an update when a messaging service will be implemented (QUITLOC) + //} AuthMeReReloaded - Always save quit location + + playerCache.removePlayer(name); + codeManager.unverify(name); + database.setUnlogged(name); + sessionService.revokeSession(name); + bungeeSender.sendAuthMeBungeecordMessage(player, MessageType.LOGOUT); + velocitySender.sendAuthMeVelocityMessage(player, VMessageType.LOGOUT); + syncProcessManager.processSyncPlayerLogout(player); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/logout/ProcessSyncPlayerLogout.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/logout/ProcessSyncPlayerLogout.java new file mode 100644 index 00000000..96865e21 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/logout/ProcessSyncPlayerLogout.java @@ -0,0 +1,83 @@ +package fr.xephi.authme.process.logout; + +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.data.limbo.LimboService; +import fr.xephi.authme.events.LogoutEvent; +import fr.xephi.authme.listener.protocollib.ProtocolLibService; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.process.SynchronousProcess; +import fr.xephi.authme.service.BukkitService; +import fr.xephi.authme.service.CommonService; +import fr.xephi.authme.service.TeleportationService; +import fr.xephi.authme.settings.commandconfig.CommandManager; +import fr.xephi.authme.settings.properties.RegistrationSettings; +import fr.xephi.authme.settings.properties.RestrictionSettings; +import org.bukkit.entity.Player; + +import javax.inject.Inject; + +import static fr.xephi.authme.service.BukkitService.TICKS_PER_SECOND; + + +public class ProcessSyncPlayerLogout implements SynchronousProcess { + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(ProcessSyncPlayerLogout.class); + + @Inject + private CommonService service; + + @Inject + private BukkitService bukkitService; + + @Inject + private ProtocolLibService protocolLibService; + + @Inject + private LimboService limboService; + + @Inject + private TeleportationService teleportationService; + + @Inject + private CommandManager commandManager; + + ProcessSyncPlayerLogout() { + } + + /** + * Processes a player which has been logged out. + * + * @param player the player logging out + */ + public void processSyncLogout(Player player) { + if (service.getProperty(RestrictionSettings.PROTECT_INVENTORY_BEFORE_LOGIN)) { + protocolLibService.sendBlankInventoryPacket(player); + } + + applyLogoutEffect(player); + commandManager.runCommandsOnLogout(player); + + // Player is now logout... Time to fire event ! + bukkitService.callEvent(new LogoutEvent(player)); + + service.send(player, MessageKey.LOGOUT_SUCCESS); + logger.info(player.getName() + " logged out"); + } + + private void applyLogoutEffect(Player player) { + // dismount player + player.leaveVehicle(); + teleportationService.teleportOnJoin(player); + + // Apply Blindness effect + if (service.getProperty(RegistrationSettings.APPLY_BLIND_EFFECT)) { + int timeout = service.getProperty(RestrictionSettings.TIMEOUT) * TICKS_PER_SECOND; + player.addPotionEffect(bukkitService.createBlindnessEffect(timeout)); + } + + // Set player's data to unauthenticated + limboService.createLimboPlayer(player, true); + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/quit/AsynchronousQuit.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/quit/AsynchronousQuit.java new file mode 100644 index 00000000..31bb7078 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/quit/AsynchronousQuit.java @@ -0,0 +1,113 @@ +package fr.xephi.authme.process.quit; + +import fr.xephi.authme.AuthMe; +import fr.xephi.authme.data.VerificationCodeManager; +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.data.auth.PlayerCache; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.process.AsynchronousProcess; +import fr.xephi.authme.process.SyncProcessManager; +import fr.xephi.authme.service.CommonService; +import fr.xephi.authme.service.SessionService; +import fr.xephi.authme.service.ValidationService; +import fr.xephi.authme.settings.SpawnLoader; +import fr.xephi.authme.settings.properties.PluginSettings; +import fr.xephi.authme.util.PlayerUtils; +import org.bukkit.Location; +import org.bukkit.entity.Player; + +import javax.inject.Inject; +import java.util.Locale; + +/** + * Async process called when a player quits the server. + */ +public class AsynchronousQuit implements AsynchronousProcess { + + @Inject + private AuthMe plugin; + + @Inject + private DataSource database; + + @Inject + private CommonService service; + + @Inject + private PlayerCache playerCache; + + @Inject + private SyncProcessManager syncProcessManager; + + @Inject + private SpawnLoader spawnLoader; + + @Inject + private ValidationService validationService; + + @Inject + private VerificationCodeManager codeManager; + + @Inject + private SessionService sessionService; + + AsynchronousQuit() { + } + + /** + * Processes that the given player has quit the server. + * + * @param player the player who left + */ + public void processQuit(Player player) { + if (player == null || validationService.isUnrestricted(player.getName())) { + return; + } + String name = player.getName().toLowerCase(Locale.ROOT); + boolean wasLoggedIn = playerCache.isAuthenticated(name); + + if (wasLoggedIn) { + //if (service.getProperty(RestrictionSettings.SAVE_QUIT_LOCATION)) { + // AuthMeReReloaded - Always save quit location on quit + Location loc = spawnLoader.getPlayerLocationOrSpawn(player); + PlayerAuth authLoc = PlayerAuth.builder() + .name(name).location(loc) + .realName(player.getName()).build(); + database.updateQuitLoc(authLoc); + // AuthMeReReloaded - Fix AuthMe#2769 -1 + //} + + + String ip = PlayerUtils.getPlayerIp(player); + PlayerAuth auth = PlayerAuth.builder() + .name(name) + .realName(player.getName()) + .lastIp(ip) + .lastLogin(System.currentTimeMillis()) + .build(); + database.updateSession(auth); + + // TODO: send an update when a messaging service will be implemented (QUITLOC) + } + + //always unauthenticate the player - use session only for auto logins on the same ip + playerCache.removePlayer(name); + codeManager.unverify(name); + + //always update the database when the player quit the game (if sessions are disabled) + if (wasLoggedIn) { + database.setUnlogged(name); + if (!service.getProperty(PluginSettings.SESSIONS_ENABLED)) { + sessionService.revokeSession(name); + } + } + + if (plugin.isEnabled()) { + syncProcessManager.processSyncPlayerQuit(player, wasLoggedIn); + } + + // remove player from cache + database.invalidateCache(name); + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/quit/ProcessSyncPlayerQuit.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/quit/ProcessSyncPlayerQuit.java new file mode 100644 index 00000000..42048ef1 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/quit/ProcessSyncPlayerQuit.java @@ -0,0 +1,37 @@ +package fr.xephi.authme.process.quit; + +import com.github.Anon8281.universalScheduler.UniversalScheduler; +import fr.xephi.authme.data.limbo.LimboService; +import fr.xephi.authme.process.SynchronousProcess; +import fr.xephi.authme.settings.commandconfig.CommandManager; +import org.bukkit.entity.Player; + +import javax.inject.Inject; + + +public class ProcessSyncPlayerQuit implements SynchronousProcess { + + @Inject + private LimboService limboService; + + @Inject + private CommandManager commandManager; + + /** + * Processes a player having quit. + * + * @param player the player that left + * @param wasLoggedIn true if the player was logged in when leaving, false otherwise + */ + public void processSyncQuit(Player player, boolean wasLoggedIn) { + if (wasLoggedIn) { + commandManager.runCommandsOnLogout(player); + } else { + limboService.restoreData(player); + if (!UniversalScheduler.isFolia) { // AuthMeReReloaded - Fix #146 (Very stupid solution, but works) + player.saveData(); // #1238: Speed is sometimes not restored properly + } + } + player.leaveVehicle(); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/register/AsyncRegister.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/register/AsyncRegister.java new file mode 100644 index 00000000..a13bcf3c --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/register/AsyncRegister.java @@ -0,0 +1,131 @@ +package fr.xephi.authme.process.register; + +import ch.jalu.injector.factory.SingletonStore; +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.data.auth.PlayerCache; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.events.AuthMeAsyncPreRegisterEvent; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.process.AsynchronousProcess; +import fr.xephi.authme.process.register.executors.RegistrationExecutor; +import fr.xephi.authme.process.register.executors.RegistrationMethod; +import fr.xephi.authme.process.register.executors.RegistrationParameters; +import fr.xephi.authme.service.BukkitService; +import fr.xephi.authme.service.CommonService; +import fr.xephi.authme.settings.properties.RegistrationSettings; +import fr.xephi.authme.settings.properties.RestrictionSettings; +import fr.xephi.authme.util.InternetProtocolUtils; +import fr.xephi.authme.util.PlayerUtils; +import org.bukkit.entity.Player; + +import javax.inject.Inject; +import java.util.List; +import java.util.Locale; + +import static fr.xephi.authme.permission.PlayerStatePermission.ALLOW_MULTIPLE_ACCOUNTS; + +/** + * Asynchronous processing of a request for registration. + */ +public class AsyncRegister implements AsynchronousProcess { + + @Inject + private DataSource database; + @Inject + private PlayerCache playerCache; + @Inject + private BukkitService bukkitService; + @Inject + private CommonService service; + @Inject + private SingletonStore registrationExecutorFactory; + + AsyncRegister() { + } + + /** + * Performs the registration process for the given player. + * + * @param variant the registration method + * @param parameters the parameters + * @param

parameters type + */ + public synchronized

void register(RegistrationMethod

variant, P parameters) { + if (preRegisterCheck(variant, parameters.getPlayer())) { + RegistrationExecutor

executor = registrationExecutorFactory.getSingleton(variant.getExecutorClass()); + if (executor.isRegistrationAdmitted(parameters)) { + executeRegistration(parameters, executor); + } + } + } + + /** + * Checks if the player is able to register, in that case the {@link AuthMeAsyncPreRegisterEvent} is invoked. + * + * @param variant the registration type variant. + * @param player the player which is trying to register. + * + * @return true if the checks are successful and the event hasn't marked the action as denied, false otherwise. + */ + private boolean preRegisterCheck(RegistrationMethod variant, Player player) { + String name = player.getName().toLowerCase(Locale.ROOT); + if (playerCache.isAuthenticated(name)) { + service.send(player, MessageKey.ALREADY_LOGGED_IN_ERROR); + return false; + } else if (!service.getProperty(RegistrationSettings.IS_ENABLED)) { + service.send(player, MessageKey.REGISTRATION_DISABLED); + return false; + } else if (database.isAuthAvailable(name)) { + service.send(player, MessageKey.NAME_ALREADY_REGISTERED); + return false; + } + + AuthMeAsyncPreRegisterEvent event = bukkitService.createAndCallEvent( + isAsync -> new AuthMeAsyncPreRegisterEvent(player, isAsync)); + if (!event.canRegister()) { + return false; + } + + return variant == RegistrationMethod.API_REGISTRATION || isPlayerIpAllowedToRegister(player); + } + + /** + * Executes the registration. + * + * @param parameters the registration parameters + * @param executor the executor to perform the registration process with + * @param

registration params type + */ + private

+ void executeRegistration(P parameters, RegistrationExecutor

executor) { + PlayerAuth auth = executor.buildPlayerAuth(parameters); + if (database.saveAuth(auth)) { + executor.executePostPersistAction(parameters); + } else { + service.send(parameters.getPlayer(), MessageKey.ERROR); + } + } + + /** + * Checks whether the registration threshold has been exceeded for the given player's IP address. + * + * @param player the player to check + * + * @return true if registration may take place, false otherwise (IP check failed) + */ + private boolean isPlayerIpAllowedToRegister(Player player) { + int maxRegPerIp = service.getProperty(RestrictionSettings.MAX_REGISTRATION_PER_IP); + String ip = PlayerUtils.getPlayerIp(player); + if (maxRegPerIp > 0 + && !InternetProtocolUtils.isLoopbackAddress(ip) + && !service.hasPermission(player, ALLOW_MULTIPLE_ACCOUNTS)) { + List otherAccounts = database.getAllAuthsByIp(ip); + if (otherAccounts.size() >= maxRegPerIp) { + service.send(player, MessageKey.MAX_REGISTER_EXCEEDED, Integer.toString(maxRegPerIp), + Integer.toString(otherAccounts.size()), String.join(", ", otherAccounts)); + return false; + } + } + return true; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/register/ProcessSyncEmailRegister.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/register/ProcessSyncEmailRegister.java new file mode 100644 index 00000000..0fe19acf --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/register/ProcessSyncEmailRegister.java @@ -0,0 +1,52 @@ +package fr.xephi.authme.process.register; + +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.data.limbo.LimboService; +import fr.xephi.authme.events.RegisterEvent; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.process.SynchronousProcess; +import fr.xephi.authme.service.BukkitService; +import fr.xephi.authme.service.CommonService; +import fr.xephi.authme.service.velocity.VMessageType; +import fr.xephi.authme.service.velocity.VelocitySender; +import fr.xephi.authme.util.PlayerUtils; +import org.bukkit.entity.Player; + +import javax.inject.Inject; + +/** + * Performs synchronous tasks after a successful {@link RegistrationType#EMAIL email registration}. + */ +public class ProcessSyncEmailRegister implements SynchronousProcess { + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(ProcessSyncEmailRegister.class); + + @Inject + private BukkitService bukkitService; + + @Inject + private CommonService service; + + @Inject + private LimboService limboService; + @Inject + private VelocitySender velocitySender; + + ProcessSyncEmailRegister() { + } + + /** + * Performs sync tasks for a player which has just registered by email. + * + * @param player the recently registered player + */ + public void processEmailRegister(Player player) { + service.send(player, MessageKey.ACCOUNT_NOT_ACTIVATED); + limboService.replaceTasksAfterRegistration(player); + velocitySender.sendAuthMeVelocityMessage(player, VMessageType.REGISTER); + bukkitService.callEvent(new RegisterEvent(player)); + logger.fine(player.getName() + " registered " + PlayerUtils.getPlayerIp(player)); + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/register/ProcessSyncPasswordRegister.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/register/ProcessSyncPasswordRegister.java new file mode 100644 index 00000000..f68ba015 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/register/ProcessSyncPasswordRegister.java @@ -0,0 +1,96 @@ +package fr.xephi.authme.process.register; + +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.data.limbo.LimboService; +import fr.xephi.authme.events.RegisterEvent; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.process.SynchronousProcess; +import fr.xephi.authme.service.BukkitService; +import fr.xephi.authme.service.CommonService; +import fr.xephi.authme.service.bungeecord.BungeeSender; +import fr.xephi.authme.service.velocity.VMessageType; +import fr.xephi.authme.service.velocity.VelocitySender; +import fr.xephi.authme.settings.commandconfig.CommandManager; +import fr.xephi.authme.settings.properties.EmailSettings; +import fr.xephi.authme.settings.properties.RegistrationSettings; +import fr.xephi.authme.util.PlayerUtils; +import org.bukkit.entity.Player; + +import javax.inject.Inject; + +/** + * Performs synchronous tasks after a successful {@link RegistrationType#PASSWORD password registration}. + */ +public class ProcessSyncPasswordRegister implements SynchronousProcess { + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(ProcessSyncPasswordRegister.class); + + @Inject + private BungeeSender bungeeSender; + + @Inject + private VelocitySender velocitySender; + + @Inject + private CommonService service; + + @Inject + private LimboService limboService; + + @Inject + private CommandManager commandManager; + + @Inject + private BukkitService bukkitService; + + ProcessSyncPasswordRegister() { + } + + /** + * Request that the player log in. + * + * @param player the player + */ + private void requestLogin(Player player) { + limboService.replaceTasksAfterRegistration(player); + + if (player.isInsideVehicle() && player.getVehicle() != null) { + player.getVehicle().eject(); + } + } + + /** + * Processes a player having registered with a password. + * + * @param player the newly registered player + */ + public void processPasswordRegister(Player player) { + service.send(player, MessageKey.REGISTER_SUCCESS); + + if (!service.getProperty(EmailSettings.MAIL_ACCOUNT).isEmpty()) { + service.send(player, MessageKey.ADD_EMAIL_MESSAGE); + } + velocitySender.sendAuthMeVelocityMessage(player, VMessageType.REGISTER); + bukkitService.callEvent(new RegisterEvent(player)); + logger.fine(player.getName() + " registered " + PlayerUtils.getPlayerIp(player)); + + // Kick Player after Registration is enabled, kick the player + if (service.getProperty(RegistrationSettings.FORCE_KICK_AFTER_REGISTER)) { + player.kickPlayer(service.retrieveSingleMessage(player, MessageKey.REGISTER_SUCCESS)); + return; + } + + // Register is now finished; we can force all commands + commandManager.runCommandsOnRegister(player); + + // Request login after registration + if (service.getProperty(RegistrationSettings.FORCE_LOGIN_AFTER_REGISTER)) { + requestLogin(player); + return; + } + + // Send Bungee stuff. The service will check if it is enabled or not. + bungeeSender.connectPlayerOnLogin(player); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/register/RegisterSecondaryArgument.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/register/RegisterSecondaryArgument.java new file mode 100644 index 00000000..4ac4f2d5 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/register/RegisterSecondaryArgument.java @@ -0,0 +1,20 @@ +package fr.xephi.authme.process.register; + +/** + * Type of the second argument of the {@code /register} command. + */ +public enum RegisterSecondaryArgument { + + /** No second argument. */ + NONE, + + /** Confirmation of the first argument. */ + CONFIRMATION, + + /** For password registration, mandatory secondary argument is email. */ + EMAIL_MANDATORY, + + /** For password registration, optional secondary argument is email. */ + EMAIL_OPTIONAL + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/register/RegistrationType.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/register/RegistrationType.java new file mode 100644 index 00000000..7b03b1c2 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/register/RegistrationType.java @@ -0,0 +1,19 @@ +package fr.xephi.authme.process.register; + +/** + * Registration type. + */ +public enum RegistrationType { + + /** + * Password registration: account is registered with a password supplied by the player. + */ + PASSWORD, + + /** + * Email registration: account is registered with an email supplied by the player. A password + * is generated and sent to the email address. + */ + EMAIL + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/register/executors/AbstractPasswordRegisterExecutor.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/register/executors/AbstractPasswordRegisterExecutor.java new file mode 100644 index 00000000..179aa59d --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/register/executors/AbstractPasswordRegisterExecutor.java @@ -0,0 +1,99 @@ +package fr.xephi.authme.process.register.executors; + +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.process.SyncProcessManager; +import fr.xephi.authme.process.login.AsynchronousLogin; +import fr.xephi.authme.security.PasswordSecurity; +import fr.xephi.authme.security.crypts.HashedPassword; +import fr.xephi.authme.service.BukkitService; +import fr.xephi.authme.service.CommonService; +import fr.xephi.authme.service.ValidationService; +import fr.xephi.authme.settings.properties.PluginSettings; +import fr.xephi.authme.settings.properties.RegistrationSettings; +import org.bukkit.entity.Player; + +import javax.inject.Inject; + +/** + * Registration executor for registration methods where the password + * is supplied by the user. + * + * @param

the parameters type + */ +abstract class AbstractPasswordRegisterExecutor

+ implements RegistrationExecutor

{ + + /** + * Number of ticks to wait before running the login action when it is run synchronously. + * A small delay is necessary or the database won't return the newly saved PlayerAuth object + * and the login process thinks the user is not registered. + */ + private static final int SYNC_LOGIN_DELAY = 5; + + @Inject + private ValidationService validationService; + + @Inject + private CommonService commonService; + + @Inject + private PasswordSecurity passwordSecurity; + + @Inject + private BukkitService bukkitService; + + @Inject + private SyncProcessManager syncProcessManager; + + @Inject + private AsynchronousLogin asynchronousLogin; + + @Override + public boolean isRegistrationAdmitted(P params) { + ValidationService.ValidationResult passwordValidation = validationService.validatePassword( + params.getPassword(), params.getPlayer().getName()); + if (passwordValidation.hasError()) { + commonService.send(params.getPlayer(), passwordValidation.getMessageKey(), passwordValidation.getArgs()); + return false; + } + return true; + } + + @Override + public PlayerAuth buildPlayerAuth(P params) { + HashedPassword hashedPassword = passwordSecurity.computeHash(params.getPassword(), params.getPlayerName()); + params.setHashedPassword(hashedPassword); + return createPlayerAuthObject(params); + } + + /** + * Creates the PlayerAuth object to store into the database, based on the registration parameters. + * + * @param params the parameters + * @return the PlayerAuth representing the new account to register + */ + protected abstract PlayerAuth createPlayerAuthObject(P params); + + /** + * Returns whether the player should be automatically logged in after registration. + * + * @param params the registration parameters + * @return true if the player should be logged in, false otherwise + */ + protected boolean performLoginAfterRegister(P params) { + return !commonService.getProperty(RegistrationSettings.FORCE_LOGIN_AFTER_REGISTER); + } + + @Override + public void executePostPersistAction(P params) { + final Player player = params.getPlayer(); + if (performLoginAfterRegister(params)) { + if (commonService.getProperty(PluginSettings.USE_ASYNC_TASKS)) { + bukkitService.runTaskAsynchronously(() -> asynchronousLogin.forceLogin(player)); + } else { + bukkitService.scheduleSyncDelayedTask(() -> asynchronousLogin.forceLogin(player), SYNC_LOGIN_DELAY); + } + } + syncProcessManager.processSyncPasswordRegister(player); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/register/executors/AbstractPasswordRegisterParams.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/register/executors/AbstractPasswordRegisterParams.java new file mode 100644 index 00000000..0c7a1d51 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/register/executors/AbstractPasswordRegisterParams.java @@ -0,0 +1,48 @@ +package fr.xephi.authme.process.register.executors; + +import fr.xephi.authme.security.crypts.HashedPassword; +import org.bukkit.entity.Player; + +/** + * Common params type for implementors of {@link AbstractPasswordRegisterExecutor}. + * Password must be supplied on creation and cannot be changed later on. The {@link HashedPassword} + * is stored on the params object for later use. + */ +public abstract class AbstractPasswordRegisterParams extends RegistrationParameters { + + private final String password; + private HashedPassword hashedPassword; + + /** + * Constructor. + * + * @param player the player to register + * @param password the password to use + */ + public AbstractPasswordRegisterParams(Player player, String password) { + super(player); + this.password = password; + } + + /** + * Constructor with no defined password. Use for registration methods which + * have no implicit password (like two factor authentication). + * + * @param player the player to register + */ + public AbstractPasswordRegisterParams(Player player) { + this(player, null); + } + + public String getPassword() { + return password; + } + + void setHashedPassword(HashedPassword hashedPassword) { + this.hashedPassword = hashedPassword; + } + + HashedPassword getHashedPassword() { + return hashedPassword; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/register/executors/ApiPasswordRegisterExecutor.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/register/executors/ApiPasswordRegisterExecutor.java new file mode 100644 index 00000000..6005c8e4 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/register/executors/ApiPasswordRegisterExecutor.java @@ -0,0 +1,20 @@ +package fr.xephi.authme.process.register.executors; + +import fr.xephi.authme.data.auth.PlayerAuth; + +/** + * Executor for password registration via API call. + */ +class ApiPasswordRegisterExecutor extends AbstractPasswordRegisterExecutor { + + @Override + protected PlayerAuth createPlayerAuthObject(ApiPasswordRegisterParams params) { + return PlayerAuthBuilderHelper + .createPlayerAuth(params.getPlayer(), params.getHashedPassword(), null); + } + + @Override + protected boolean performLoginAfterRegister(ApiPasswordRegisterParams params) { + return params.getLoginAfterRegister(); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/register/executors/ApiPasswordRegisterParams.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/register/executors/ApiPasswordRegisterParams.java new file mode 100644 index 00000000..357d32c2 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/register/executors/ApiPasswordRegisterParams.java @@ -0,0 +1,35 @@ +package fr.xephi.authme.process.register.executors; + +import org.bukkit.entity.Player; + +/** + * Parameters for {@link ApiPasswordRegisterExecutor}. + */ +public class ApiPasswordRegisterParams extends PasswordRegisterParams { + + private final boolean loginAfterRegister; + + protected ApiPasswordRegisterParams(Player player, String password, boolean loginAfterRegister) { + super(player, password, null); + this.loginAfterRegister = loginAfterRegister; + } + + /** + * Creates a parameters object. + * + * @param player the player to register + * @param password the password to register with + * @param loginAfterRegister whether the player should be logged in after registration + * @return params object with the given data + */ + public static ApiPasswordRegisterParams of(Player player, String password, boolean loginAfterRegister) { + return new ApiPasswordRegisterParams(player, password, loginAfterRegister); + } + + /** + * @return true if the player should be logged in after being registered, false otherwise + */ + public boolean getLoginAfterRegister() { + return loginAfterRegister; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/register/executors/EmailRegisterExecutor.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/register/executors/EmailRegisterExecutor.java new file mode 100644 index 00000000..2224c23c --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/register/executors/EmailRegisterExecutor.java @@ -0,0 +1,81 @@ +package fr.xephi.authme.process.register.executors; + +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.mail.EmailService; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.process.SyncProcessManager; +import fr.xephi.authme.security.PasswordSecurity; +import fr.xephi.authme.security.crypts.HashedPassword; +import fr.xephi.authme.service.CommonService; +import fr.xephi.authme.settings.properties.EmailSettings; +import fr.xephi.authme.util.PlayerUtils; +import fr.xephi.authme.util.RandomStringUtils; +import org.bukkit.entity.Player; + +import javax.inject.Inject; +import java.text.SimpleDateFormat; +import java.util.Date; + +import static fr.xephi.authme.permission.PlayerStatePermission.ALLOW_MULTIPLE_ACCOUNTS; +import static fr.xephi.authme.process.register.executors.PlayerAuthBuilderHelper.createPlayerAuth; +import static fr.xephi.authme.settings.properties.EmailSettings.RECOVERY_PASSWORD_LENGTH; + +/** + * Executor for email registration: the player only provides his email address, + * to which a generated password is sent. + */ +class EmailRegisterExecutor implements RegistrationExecutor { + + @Inject + private DataSource dataSource; + + @Inject + private CommonService commonService; + + @Inject + private EmailService emailService; + + @Inject + private SyncProcessManager syncProcessManager; + + @Inject + private PasswordSecurity passwordSecurity; + + @Override + public boolean isRegistrationAdmitted(EmailRegisterParams params) { + final int maxRegPerEmail = commonService.getProperty(EmailSettings.MAX_REG_PER_EMAIL); + if (maxRegPerEmail > 0 && !commonService.hasPermission(params.getPlayer(), ALLOW_MULTIPLE_ACCOUNTS)) { + int otherAccounts = dataSource.countAuthsByEmail(params.getEmail()); + if (otherAccounts >= maxRegPerEmail) { + commonService.send(params.getPlayer(), MessageKey.MAX_REGISTER_EXCEEDED, + Integer.toString(maxRegPerEmail), Integer.toString(otherAccounts), "@"); + return false; + } + } + return true; + } + + @Override + public PlayerAuth buildPlayerAuth(EmailRegisterParams params) { + String password = RandomStringUtils.generate(commonService.getProperty(RECOVERY_PASSWORD_LENGTH)); + HashedPassword hashedPassword = passwordSecurity.computeHash(password, params.getPlayer().getName()); + params.setPassword(password); + return createPlayerAuth(params.getPlayer(), hashedPassword, params.getEmail()); + } + + @Override + public void executePostPersistAction(EmailRegisterParams params) { + Player player = params.getPlayer(); + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy'年'MM'月'dd'日' HH:mm:ss"); + Date date = new Date(System.currentTimeMillis()); + boolean couldSendMail = emailService.sendNewPasswordMail( + player.getName(), params.getEmail(), params.getPassword(), PlayerUtils.getPlayerIp(player), dateFormat.format(date)); + if (couldSendMail) { + syncProcessManager.processSyncEmailRegister(player); + } else { + commonService.send(player, MessageKey.EMAIL_SEND_FAILURE); + } + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/register/executors/EmailRegisterParams.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/register/executors/EmailRegisterParams.java new file mode 100644 index 00000000..94d03acc --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/register/executors/EmailRegisterParams.java @@ -0,0 +1,43 @@ +package fr.xephi.authme.process.register.executors; + +import org.bukkit.entity.Player; + +/** + * Parameters for email registration. + */ +public class EmailRegisterParams extends RegistrationParameters { + + private final String email; + private String password; + + protected EmailRegisterParams(Player player, String email) { + super(player); + this.email = email; + } + + /** + * Creates a params object for email registration. + * + * @param player the player to register + * @param email the player's email + * @return params object with the given data + */ + public static EmailRegisterParams of(Player player, String email) { + return new EmailRegisterParams(player, email); + } + + public String getEmail() { + return email; + } + + void setPassword(String password) { + this.password = password; + } + + /** + * @return the password generated for the player + */ + String getPassword() { + return password; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/register/executors/PasswordRegisterExecutor.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/register/executors/PasswordRegisterExecutor.java new file mode 100644 index 00000000..5b1558bc --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/register/executors/PasswordRegisterExecutor.java @@ -0,0 +1,17 @@ +package fr.xephi.authme.process.register.executors; + +import fr.xephi.authme.data.auth.PlayerAuth; + +import static fr.xephi.authme.process.register.executors.PlayerAuthBuilderHelper.createPlayerAuth; + +/** + * Registration executor for password registration. + */ +class PasswordRegisterExecutor extends AbstractPasswordRegisterExecutor { + + @Override + public synchronized PlayerAuth createPlayerAuthObject(PasswordRegisterParams params) { + return createPlayerAuth(params.getPlayer(), params.getHashedPassword(), params.getEmail()); + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/register/executors/PasswordRegisterParams.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/register/executors/PasswordRegisterParams.java new file mode 100644 index 00000000..f21861bf --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/register/executors/PasswordRegisterParams.java @@ -0,0 +1,32 @@ +package fr.xephi.authme.process.register.executors; + +import org.bukkit.entity.Player; + +/** + * Parameters for registration with a given password, and optionally an email address. + */ +public class PasswordRegisterParams extends AbstractPasswordRegisterParams { + + private final String email; + + protected PasswordRegisterParams(Player player, String password, String email) { + super(player, password); + this.email = email; + } + + /** + * Creates a params object. + * + * @param player the player to register + * @param password the password to register with + * @param email the email of the player (may be null) + * @return params object with the given data + */ + public static PasswordRegisterParams of(Player player, String password, String email) { + return new PasswordRegisterParams(player, password, email); + } + + public String getEmail() { + return email; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/register/executors/PlayerAuthBuilderHelper.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/register/executors/PlayerAuthBuilderHelper.java new file mode 100644 index 00000000..9b29dac2 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/register/executors/PlayerAuthBuilderHelper.java @@ -0,0 +1,37 @@ +package fr.xephi.authme.process.register.executors; + +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.security.crypts.HashedPassword; +import fr.xephi.authme.util.PlayerUtils; +import org.bukkit.entity.Player; + +import java.util.Locale; + +/** + * Helper for constructing PlayerAuth objects. + */ +final class PlayerAuthBuilderHelper { + + private PlayerAuthBuilderHelper() { + } + + /** + * Creates a {@link PlayerAuth} object with the given data. + * + * @param player the player to create a PlayerAuth for + * @param hashedPassword the hashed password + * @param email the email address (nullable) + * @return the generated PlayerAuth object + */ + static PlayerAuth createPlayerAuth(Player player, HashedPassword hashedPassword, String email) { + return PlayerAuth.builder() + .name(player.getName().toLowerCase(Locale.ROOT)) + .realName(player.getName()) + .password(hashedPassword) + .email(email) + .registrationIp(PlayerUtils.getPlayerIp(player)) + .registrationDate(System.currentTimeMillis()) + .uuid(player.getUniqueId()) + .build(); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/register/executors/RegistrationExecutor.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/register/executors/RegistrationExecutor.java new file mode 100644 index 00000000..32bfd951 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/register/executors/RegistrationExecutor.java @@ -0,0 +1,39 @@ +package fr.xephi.authme.process.register.executors; + +import fr.xephi.authme.data.auth.PlayerAuth; + +/** + * Performs the registration action. + * + * @param

the registration parameters type + */ +public interface RegistrationExecutor

{ + + /** + * Returns whether the registration may take place. Use this method to execute + * checks specific to the registration method. + *

+ * If this method returns {@code false}, it is expected that the executor inform + * the player about the error within this method call. + * + * @param params the parameters for the registration + * @return true if registration may be performed, false otherwise + */ + boolean isRegistrationAdmitted(P params); + + /** + * Constructs the PlayerAuth object to persist into the database. + * + * @param params the parameters for the registration + * @return the player auth to register in the data source + */ + PlayerAuth buildPlayerAuth(P params); + + /** + * Follow-up method called after the player auth could be added into the database. + * + * @param params the parameters for the registration + */ + void executePostPersistAction(P params); + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/register/executors/RegistrationMethod.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/register/executors/RegistrationMethod.java new file mode 100644 index 00000000..f5f38b86 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/register/executors/RegistrationMethod.java @@ -0,0 +1,53 @@ +package fr.xephi.authme.process.register.executors; + +/** + * Methods with which a player can be registered. + *

+ * These constants each define a different way of registering a player and define the + * {@link RegistrationParameters parameters} and {@link RegistrationExecutor executor} + * classes which perform this registration method. This is essentially a typed enum + * as passing a constant of this class along with a parameters object to a method can + * be restricted to the correct parameters type. + * + * @param

the registration parameters type the method uses + */ +public final class RegistrationMethod

{ + + /** + * Password registration. + */ + public static final RegistrationMethod PASSWORD_REGISTRATION = + new RegistrationMethod<>(PasswordRegisterExecutor.class); + + /** + * Registration with two-factor authentication as login means. + */ + public static final RegistrationMethod TWO_FACTOR_REGISTRATION = + new RegistrationMethod<>(TwoFactorRegisterExecutor.class); + + /** + * Email registration: an email address is provided, to which a generated password is sent. + */ + public static final RegistrationMethod EMAIL_REGISTRATION = + new RegistrationMethod<>(EmailRegisterExecutor.class); + + /** + * API registration: player and password are provided via an API method. + */ + public static final RegistrationMethod API_REGISTRATION = + new RegistrationMethod<>(ApiPasswordRegisterExecutor.class); + + + private final Class> executorClass; + + private RegistrationMethod(Class> executorClass) { + this.executorClass = executorClass; + } + + /** + * @return the executor class to perform the registration method + */ + public Class> getExecutorClass() { + return executorClass; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/register/executors/RegistrationParameters.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/register/executors/RegistrationParameters.java new file mode 100644 index 00000000..c92d57ff --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/register/executors/RegistrationParameters.java @@ -0,0 +1,28 @@ +package fr.xephi.authme.process.register.executors; + +import org.bukkit.entity.Player; + +/** + * Parent of all registration parameters. + */ +public abstract class RegistrationParameters { + + private final Player player; + + /** + * Constructor. + * + * @param player the player to perform the registration for + */ + public RegistrationParameters(Player player) { + this.player = player; + } + + public Player getPlayer() { + return player; + } + + public String getPlayerName() { + return player.getName(); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/register/executors/TwoFactorRegisterExecutor.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/register/executors/TwoFactorRegisterExecutor.java new file mode 100644 index 00000000..c84a70ab --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/register/executors/TwoFactorRegisterExecutor.java @@ -0,0 +1,43 @@ +package fr.xephi.authme.process.register.executors; + +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.security.crypts.TwoFactor; +import fr.xephi.authme.service.CommonService; +import org.bukkit.Bukkit; + +import javax.inject.Inject; + +import static fr.xephi.authme.process.register.executors.PlayerAuthBuilderHelper.createPlayerAuth; + +/** + * Executor for two-factor registration. + */ +class TwoFactorRegisterExecutor extends AbstractPasswordRegisterExecutor { + + @Inject + private CommonService commonService; + + @Override + public boolean isRegistrationAdmitted(TwoFactorRegisterParams params) { + // nothing to check + return true; + } + + @Override + protected PlayerAuth createPlayerAuthObject(TwoFactorRegisterParams params) { + return createPlayerAuth(params.getPlayer(), params.getHashedPassword(), null); + } + + @Override + public void executePostPersistAction(TwoFactorRegisterParams params) { + super.executePostPersistAction(params); + + // Note ljacqu 20170317: This two-factor registration type is only invoked when the password hash is configured + // to two-factor authentication. Therefore, the hashed password is the result of the TwoFactor EncryptionMethod + // implementation (contains the TOTP secret). + String hash = params.getHashedPassword().getHash(); + String qrCodeUrl = TwoFactor.getQrBarcodeUrl(params.getPlayerName(), Bukkit.getIp(), hash); + commonService.send(params.getPlayer(), MessageKey.TWO_FACTOR_CREATE, hash, qrCodeUrl); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/register/executors/TwoFactorRegisterParams.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/register/executors/TwoFactorRegisterParams.java new file mode 100644 index 00000000..a7a75875 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/register/executors/TwoFactorRegisterParams.java @@ -0,0 +1,23 @@ +package fr.xephi.authme.process.register.executors; + +import org.bukkit.entity.Player; + +/** + * Parameters for registration with two-factor authentication. + */ +public class TwoFactorRegisterParams extends AbstractPasswordRegisterParams { + + protected TwoFactorRegisterParams(Player player) { + super(player); + } + + /** + * Creates a parameters object. + * + * @param player the player to register + * @return params object with the given player + */ + public static TwoFactorRegisterParams of(Player player) { + return new TwoFactorRegisterParams(player); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/unregister/AsynchronousUnregister.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/unregister/AsynchronousUnregister.java new file mode 100644 index 00000000..caecd295 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/process/unregister/AsynchronousUnregister.java @@ -0,0 +1,155 @@ +package fr.xephi.authme.process.unregister; + +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.data.auth.PlayerCache; +import fr.xephi.authme.data.limbo.LimboService; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.events.UnregisterByAdminEvent; +import fr.xephi.authme.events.UnregisterByPlayerEvent; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.process.AsynchronousProcess; +import fr.xephi.authme.security.PasswordSecurity; +import fr.xephi.authme.service.BukkitService; +import fr.xephi.authme.service.CommonService; +import fr.xephi.authme.service.TeleportationService; +import fr.xephi.authme.service.bungeecord.BungeeSender; +import fr.xephi.authme.service.bungeecord.MessageType; +import fr.xephi.authme.service.velocity.VMessageType; +import fr.xephi.authme.service.velocity.VelocitySender; +import fr.xephi.authme.settings.commandconfig.CommandManager; +import fr.xephi.authme.settings.properties.RegistrationSettings; +import fr.xephi.authme.settings.properties.RestrictionSettings; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import javax.inject.Inject; + +import static fr.xephi.authme.service.BukkitService.TICKS_PER_SECOND; + +public class AsynchronousUnregister implements AsynchronousProcess { + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(AsynchronousUnregister.class); + + @Inject + private DataSource dataSource; + + @Inject + private CommonService service; + + @Inject + private PasswordSecurity passwordSecurity; + + @Inject + private PlayerCache playerCache; + + @Inject + private BukkitService bukkitService; + + @Inject + private LimboService limboService; + + @Inject + private TeleportationService teleportationService; + + @Inject + private CommandManager commandManager; + + @Inject + private VelocitySender velocitySender; + + @Inject + private BungeeSender bungeeSender; + + AsynchronousUnregister() { + } + + /** + * Processes a player's request to unregister himself. Unregisters the player after + * successful password check. + * + * @param player the player + * @param password the input password to check before unregister + */ + public void unregister(Player player, String password) { + String name = player.getName(); + PlayerAuth cachedAuth = playerCache.getAuth(name); + if (passwordSecurity.comparePassword(password, cachedAuth.getPassword(), name)) { + if (dataSource.removeAuth(name)) { + performPostUnregisterActions(name, player); + logger.info(name + " unregistered himself"); + velocitySender.sendAuthMeVelocityMessage(player, VMessageType.UNREGISTER); + bukkitService.createAndCallEvent(isAsync -> new UnregisterByPlayerEvent(player, isAsync)); + } else { + service.send(player, MessageKey.ERROR); + } + } else { + service.send(player, MessageKey.WRONG_PASSWORD); + } + } + + /** + * Unregisters a player as administrator or console. + * + * @param initiator the initiator of this process (nullable) + * @param name the name of the player + * @param player the according Player object (nullable) + */ + // We need to have the name and the player separate because Player might be null in this case: + // we might have some player in the database that has never been online on the server + public void adminUnregister(CommandSender initiator, String name, Player player) { + if (dataSource.removeAuth(name)) { + performPostUnregisterActions(name, player); + if (player != null) velocitySender.sendAuthMeVelocityMessage(player, VMessageType.FORCE_UNREGISTER); + bukkitService.createAndCallEvent(isAsync -> new UnregisterByAdminEvent(player, name, isAsync, initiator)); + if (initiator == null) { + logger.info(name + " was unregistered"); + } else { + logger.info(name + " was unregistered by " + initiator.getName()); + service.send(initiator, MessageKey.UNREGISTERED_SUCCESS); + } + } else if (initiator != null) { + service.send(initiator, MessageKey.ERROR); + } + } + + /** + * Process the post unregister actions. Makes the user status consistent. + * + * @param name the name of the player + * @param player the according Player object (nullable) + */ + private void performPostUnregisterActions(String name, Player player) { + if (player != null && playerCache.isAuthenticated(name)) { + bungeeSender.sendAuthMeBungeecordMessage(player, MessageType.LOGOUT); + } + playerCache.removePlayer(name); + + // TODO: send an update when a messaging service will be implemented (UNREGISTER) + + if (player == null || !player.isOnline()) { + return; + } + bukkitService.scheduleSyncTaskFromOptionallyAsyncTask(() -> + commandManager.runCommandsOnUnregister(player)); + + if (service.getProperty(RegistrationSettings.FORCE)) { + teleportationService.teleportOnJoin(player); + + bukkitService.scheduleSyncTaskFromOptionallyAsyncTask(() -> { + limboService.createLimboPlayer(player, false); + applyBlindEffect(player); + }); + } + service.send(player, MessageKey.UNREGISTERED_SUCCESS); + } + + private void applyBlindEffect(Player player) { + if (service.getProperty(RegistrationSettings.APPLY_BLIND_EFFECT)) { + int timeout = service.getProperty(RestrictionSettings.TIMEOUT) * TICKS_PER_SECOND; + bukkitService.runTaskIfFolia(player, () -> player.addPotionEffect(bukkitService.createBlindnessEffect(timeout))); + } + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/HashAlgorithm.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/HashAlgorithm.java new file mode 100644 index 00000000..d1085fde --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/HashAlgorithm.java @@ -0,0 +1,60 @@ +package fr.xephi.authme.security; + +import fr.xephi.authme.security.crypts.EncryptionMethod; + +/** + * Hash algorithms supported by AuthMe. + */ +public enum HashAlgorithm { + + ARGON2(fr.xephi.authme.security.crypts.Argon2.class), + BCRYPT(fr.xephi.authme.security.crypts.BCrypt.class), + BCRYPT2Y(fr.xephi.authme.security.crypts.BCrypt2y.class), + CMW(fr.xephi.authme.security.crypts.CmwCrypt.class), + CRAZYCRYPT1(fr.xephi.authme.security.crypts.CrazyCrypt1.class), + IPB3(fr.xephi.authme.security.crypts.Ipb3.class), + IPB4(fr.xephi.authme.security.crypts.Ipb4.class), + JOOMLA(fr.xephi.authme.security.crypts.Joomla.class), + MD5VB(fr.xephi.authme.security.crypts.Md5vB.class), + MYBB(fr.xephi.authme.security.crypts.MyBB.class), + PBKDF2(fr.xephi.authme.security.crypts.Pbkdf2.class), + PBKDF2DJANGO(fr.xephi.authme.security.crypts.Pbkdf2Django.class), + PHPBB(fr.xephi.authme.security.crypts.PhpBB.class), + PHPFUSION(fr.xephi.authme.security.crypts.PhpFusion.class), + ROYALAUTH(fr.xephi.authme.security.crypts.RoyalAuth.class), + SALTED2MD5(fr.xephi.authme.security.crypts.Salted2Md5.class), + SALTEDSHA512(fr.xephi.authme.security.crypts.SaltedSha512.class), + SHA256(fr.xephi.authme.security.crypts.Sha256.class), + SMF(fr.xephi.authme.security.crypts.Smf.class), + TWO_FACTOR(fr.xephi.authme.security.crypts.TwoFactor.class), + WBB3(fr.xephi.authme.security.crypts.Wbb3.class), + WBB4(fr.xephi.authme.security.crypts.Wbb4.class), + WORDPRESS(fr.xephi.authme.security.crypts.Wordpress.class), + XAUTH(fr.xephi.authme.security.crypts.XAuth.class), + XFBCRYPT(fr.xephi.authme.security.crypts.XfBCrypt.class), + NOCRYPT(fr.xephi.authme.security.crypts.NoCrypt.class), + CUSTOM(null), + + @Deprecated DOUBLEMD5(fr.xephi.authme.security.crypts.DoubleMd5.class), + @Deprecated MD5(fr.xephi.authme.security.crypts.Md5.class), + @Deprecated PLAINTEXT(null), + @Deprecated SHA1(fr.xephi.authme.security.crypts.Sha1.class), + @Deprecated SHA512(fr.xephi.authme.security.crypts.Sha512.class), + @Deprecated WHIRLPOOL(fr.xephi.authme.security.crypts.Whirlpool.class); + + private final Class clazz; + + /** + * Constructor for HashAlgorithm. + * + * @param clazz The class of the hash implementation. + */ + HashAlgorithm(Class clazz) { + this.clazz = clazz; + } + + public Class getClazz() { + return clazz; + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/HashUtils.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/HashUtils.java new file mode 100644 index 00000000..e8088078 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/HashUtils.java @@ -0,0 +1,121 @@ +package fr.xephi.authme.security; + +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * Hashing utilities (interface for common hashing algorithms). + */ +public final class HashUtils { + + private HashUtils() { + } + + /** + * Generate the SHA-1 digest of the given message. + * + * @param message The message to hash + * @return The resulting SHA-1 digest + */ + public static String sha1(String message) { + return hash(message, MessageDigestAlgorithm.SHA1); + } + + /** + * Generate the SHA-256 digest of the given message. + * + * @param message The message to hash + * @return The resulting SHA-256 digest + */ + public static String sha256(String message) { + return hash(message, MessageDigestAlgorithm.SHA256); + } + + /** + * Generate the SHA-512 digest of the given message. + * + * @param message The message to hash + * @return The resulting SHA-512 digest + */ + public static String sha512(String message) { + return hash(message, MessageDigestAlgorithm.SHA512); + } + + /** + * Generate the MD5 digest of the given message. + * + * @param message The message to hash + * @return The resulting MD5 digest + */ + public static String md5(String message) { + return hash(message, MessageDigestAlgorithm.MD5); + } + + /** + * Return a {@link MessageDigest} instance for the given algorithm. + * + * @param algorithm The desired algorithm + * @return MessageDigest instance for the given algorithm + */ + public static MessageDigest getDigest(MessageDigestAlgorithm algorithm) { + try { + return MessageDigest.getInstance(algorithm.getKey()); + } catch (NoSuchAlgorithmException e) { + throw new UnsupportedOperationException("Your system seems not to support the hash algorithm '" + + algorithm.getKey() + "'"); + } + } + + /** + * Return whether the given hash starts like a BCrypt hash. Checking with this method + * beforehand prevents the BCryptHasher from throwing certain exceptions. + * + * @param hash The salt to verify + * @return True if the salt is valid, false otherwise + */ + public static boolean isValidBcryptHash(String hash) { + return hash.length() == 60 && hash.substring(0, 2).equals("$2"); + } + + /** + * Checks whether the two strings are equal to each other in a time-constant manner. + * This helps to avoid timing side channel attacks, + * cf. issue #1561. + * + * @param string1 first string + * @param string2 second string + * @return true if the strings are equal to each other, false otherwise + */ + public static boolean isEqual(String string1, String string2) { + return MessageDigest.isEqual( + string1.getBytes(StandardCharsets.UTF_8), string2.getBytes(StandardCharsets.UTF_8)); + } + + /** + * Hash the message with the given algorithm and return the hash in its hexadecimal notation. + * + * @param message The message to hash + * @param algorithm The algorithm to hash the message with + * @return The digest in its hexadecimal representation + */ + public static String hash(String message, MessageDigest algorithm) { + algorithm.reset(); + algorithm.update(message.getBytes()); + byte[] digest = algorithm.digest(); + return String.format("%0" + (digest.length << 1) + "x", new BigInteger(1, digest)); + } + + /** + * Hash the message with the given algorithm and return the hash in its hexadecimal notation. + * + * @param message The message to hash + * @param algorithm The algorithm to hash the message with + * @return The digest in its hexadecimal representation + */ + private static String hash(String message, MessageDigestAlgorithm algorithm) { + return hash(message, getDigest(algorithm)); + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/MessageDigestAlgorithm.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/MessageDigestAlgorithm.java new file mode 100644 index 00000000..51ff2a5d --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/MessageDigestAlgorithm.java @@ -0,0 +1,30 @@ +package fr.xephi.authme.security; + +import java.security.MessageDigest; + +/** + * The Java-supported names to get a {@link MessageDigest} instance with. + * + * @see + * Crypto Spec Appendix A: Standard Names + */ +public enum MessageDigestAlgorithm { + + MD5("MD5"), + + SHA1("SHA-1"), + + SHA256("SHA-256"), + + SHA512("SHA-512"); + + private final String key; + + MessageDigestAlgorithm(String key) { + this.key = key; + } + + public String getKey() { + return key; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/PasswordSecurity.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/PasswordSecurity.java new file mode 100644 index 00000000..012fd3e5 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/PasswordSecurity.java @@ -0,0 +1,164 @@ +package fr.xephi.authme.security; + +import ch.jalu.injector.factory.Factory; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.events.PasswordEncryptionEvent; +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.Settings; +import fr.xephi.authme.settings.properties.SecuritySettings; +import org.bukkit.plugin.PluginManager; + +import javax.annotation.PostConstruct; +import javax.inject.Inject; +import java.util.Collection; +import java.util.Locale; + +/** + * Manager class for password-related operations. + */ +public class PasswordSecurity implements Reloadable { + + @Inject + private Settings settings; + + @Inject + private DataSource dataSource; + + @Inject + private PluginManager pluginManager; + + @Inject + private Factory encryptionMethodFactory; + + private EncryptionMethod encryptionMethod; + private Collection legacyAlgorithms; + + /** + * Load or reload the configuration. + */ + @PostConstruct + @Override + public void reload() { + HashAlgorithm algorithm = settings.getProperty(SecuritySettings.PASSWORD_HASH); + this.encryptionMethod = initializeEncryptionMethodWithEvent(algorithm); + this.legacyAlgorithms = settings.getProperty(SecuritySettings.LEGACY_HASHES); + } + + /** + * Compute the hash of the configured algorithm for the given password and username. + * + * @param password The password to hash + * @param playerName The player's name + * + * @return The password hash + */ + public HashedPassword computeHash(String password, String playerName) { + String playerLowerCase = playerName.toLowerCase(Locale.ROOT); + return encryptionMethod.computeHash(password, playerLowerCase); + } + + /** + * Check if the given password matches the player's stored password. + * + * @param password The password to check + * @param playerName The player to check for + * + * @return True if the password is correct, false otherwise + */ + public boolean comparePassword(String password, String playerName) { + HashedPassword auth = dataSource.getPassword(playerName); + return auth != null && comparePassword(password, auth, playerName); + } + + /** + * Check if the given password matches the given hashed password. + * + * @param password The password to check + * @param hashedPassword The hashed password to check against + * @param playerName The player to check for + * + * @return True if the password matches, false otherwise + */ + public boolean comparePassword(String password, HashedPassword hashedPassword, String playerName) { + String playerLowerCase = playerName.toLowerCase(Locale.ROOT); + return methodMatches(encryptionMethod, password, hashedPassword, playerLowerCase) + || compareWithLegacyHashes(password, hashedPassword, playerLowerCase); + } + + /** + * Compare the given hash with the configured legacy encryption methods to support + * the migration to a new encryption method. Upon a successful match, the password + * will be hashed with the new encryption method and persisted. + * + * @param password The clear-text password to check + * @param hashedPassword The encrypted password to test the clear-text password against + * @param playerName The name of the player + * + * @return True if there was a password match with a configured legacy encryption method, false otherwise + */ + private boolean compareWithLegacyHashes(String password, HashedPassword hashedPassword, String playerName) { + for (HashAlgorithm algorithm : legacyAlgorithms) { + EncryptionMethod method = initializeEncryptionMethod(algorithm); + if (methodMatches(method, password, hashedPassword, playerName)) { + hashAndSavePasswordWithNewAlgorithm(password, playerName); + return true; + } + } + return false; + } + + /** + * Verify with the given encryption method whether the password matches the hash after checking that + * the method can be called safely with the given data. + * + * @param method The encryption method to use + * @param password The password to check + * @param hashedPassword The hash to check against + * @param playerName The name of the player + * + * @return True if the password matched, false otherwise + */ + private static boolean methodMatches(EncryptionMethod method, String password, + HashedPassword hashedPassword, String playerName) { + return method != null && (!method.hasSeparateSalt() || hashedPassword.getSalt() != null) + && method.comparePassword(password, hashedPassword, playerName); + } + + /** + * Get the encryption method from the given {@link HashAlgorithm} value and emit a + * {@link PasswordEncryptionEvent}. The encryption method from the event is then returned, + * which may have been changed by an external listener. + * + * @param algorithm The algorithm to retrieve the encryption method for + * + * @return The encryption method + */ + private EncryptionMethod initializeEncryptionMethodWithEvent(HashAlgorithm algorithm) { + EncryptionMethod method = initializeEncryptionMethod(algorithm); + PasswordEncryptionEvent event = new PasswordEncryptionEvent(method); + pluginManager.callEvent(event); + return event.getMethod(); + } + + /** + * Initialize the encryption method associated with the given hash algorithm. + * + * @param algorithm The algorithm to retrieve the encryption method for + * + * @return The associated encryption method, or null if CUSTOM / deprecated + */ + private EncryptionMethod initializeEncryptionMethod(HashAlgorithm algorithm) { + if (HashAlgorithm.CUSTOM.equals(algorithm) || HashAlgorithm.PLAINTEXT.equals(algorithm)) { + return null; + } + return encryptionMethodFactory.newInstance(algorithm.getClazz()); + } + + private void hashAndSavePasswordWithNewAlgorithm(String password, String playerName) { + HashedPassword hashedPassword = encryptionMethod.computeHash(password, playerName); + dataSource.updatePassword(playerName, hashedPassword); + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/Argon2.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/Argon2.java new file mode 100644 index 00000000..e002886b --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/Argon2.java @@ -0,0 +1,51 @@ +package fr.xephi.authme.security.crypts; + +import de.mkammerer.argon2.Argon2Constants; +import de.mkammerer.argon2.Argon2Factory; +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.security.crypts.description.HasSalt; +import fr.xephi.authme.security.crypts.description.Recommendation; +import fr.xephi.authme.security.crypts.description.SaltType; +import fr.xephi.authme.security.crypts.description.Usage; + +@Recommendation(Usage.RECOMMENDED) +@HasSalt(value = SaltType.TEXT, length = Argon2Constants.DEFAULT_SALT_LENGTH) +// Note: Argon2 is actually a salted algorithm but salt generation is handled internally +// and isn't exposed to the outside, so we treat it as an unsalted implementation +public class Argon2 extends UnsaltedMethod { + + private static ConsoleLogger logger = ConsoleLoggerFactory.get(Argon2.class); + + private de.mkammerer.argon2.Argon2 argon2; + + public Argon2() { + argon2 = Argon2Factory.create(); + } + + /** + * Checks if the argon2 library is available in java.library.path. + * + * @return true if the library is present, false otherwise + */ + public static boolean isLibraryLoaded() { + try { + System.loadLibrary("argon2"); + return true; + } catch (UnsatisfiedLinkError e) { + logger.logException( + "Cannot find argon2 library: https://github.com/AuthMe/AuthMeReloaded/wiki/Argon2-as-Password-Hash", e); + } + return false; + } + + @Override + public String computeHash(String password) { + return argon2.hash(2, 65536, 1, password); + } + + @Override + public boolean comparePassword(String password, HashedPassword hashedPassword, String name) { + return argon2.verify(hashedPassword.getHash(), password); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/BCrypt.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/BCrypt.java new file mode 100644 index 00000000..5b75c89a --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/BCrypt.java @@ -0,0 +1,23 @@ +package fr.xephi.authme.security.crypts; + +import at.favre.lib.crypto.bcrypt.BCrypt.Version; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.properties.HooksSettings; + +import javax.inject.Inject; + +/** + * BCrypt hash algorithm with configurable cost factor. + */ +public class BCrypt extends BCryptBasedHash { + + @Inject + public BCrypt(Settings settings) { + super(createHasher(settings)); + } + + private static BCryptHasher createHasher(Settings settings) { + int bCryptLog2Rounds = settings.getProperty(HooksSettings.BCRYPT_LOG2_ROUND); + return new BCryptHasher(Version.VERSION_2A, bCryptLog2Rounds); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/BCrypt2y.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/BCrypt2y.java new file mode 100644 index 00000000..2558fa98 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/BCrypt2y.java @@ -0,0 +1,20 @@ +package fr.xephi.authme.security.crypts; + +import at.favre.lib.crypto.bcrypt.BCrypt; + +import javax.inject.Inject; + +/** + * Hash for BCrypt in the $2y$ variant. Uses a fixed cost factor of 10. + */ +public class BCrypt2y extends BCryptBasedHash { + + @Inject + public BCrypt2y() { + this(10); + } + + public BCrypt2y(int cost) { + super(new BCryptHasher(BCrypt.Version.VERSION_2Y, cost)); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/BCryptBasedHash.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/BCryptBasedHash.java new file mode 100644 index 00000000..919c9bf2 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/BCryptBasedHash.java @@ -0,0 +1,48 @@ +package fr.xephi.authme.security.crypts; + +import fr.xephi.authme.security.crypts.description.HasSalt; +import fr.xephi.authme.security.crypts.description.Recommendation; +import fr.xephi.authme.security.crypts.description.SaltType; +import fr.xephi.authme.security.crypts.description.Usage; + +import static fr.xephi.authme.security.crypts.BCryptHasher.SALT_LENGTH_ENCODED; +import static java.nio.charset.StandardCharsets.UTF_8; + +/** + * Abstract parent for BCrypt-based hash algorithms. + */ +@Recommendation(Usage.RECOMMENDED) +@HasSalt(value = SaltType.TEXT, length = SALT_LENGTH_ENCODED) +public abstract class BCryptBasedHash implements EncryptionMethod { + + private final BCryptHasher bCryptHasher; + + public BCryptBasedHash(BCryptHasher bCryptHasher) { + this.bCryptHasher = bCryptHasher; + } + + @Override + public HashedPassword computeHash(String password, String name) { + return bCryptHasher.hash(password); + } + + @Override + public String computeHash(String password, String salt, String name) { + return bCryptHasher.hashWithRawSalt(password, salt.getBytes(UTF_8)); + } + + @Override + public boolean comparePassword(String password, HashedPassword hashedPassword, String name) { + return BCryptHasher.comparePassword(password, hashedPassword.getHash()); + } + + @Override + public String generateSalt() { + return BCryptHasher.generateSalt(); + } + + @Override + public boolean hasSeparateSalt() { + return false; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/BCryptHasher.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/BCryptHasher.java new file mode 100644 index 00000000..ffa1064d --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/BCryptHasher.java @@ -0,0 +1,78 @@ +package fr.xephi.authme.security.crypts; + +import at.favre.lib.crypto.bcrypt.BCrypt; +import fr.xephi.authme.security.HashUtils; +import fr.xephi.authme.util.RandomStringUtils; + +import static java.nio.charset.StandardCharsets.UTF_8; + +/** + * Wraps a {@link BCrypt.Hasher} instance and provides methods suitable for use in AuthMe. + */ +public class BCryptHasher { + + /** Number of bytes in a BCrypt salt (not encoded). */ + public static final int BYTES_IN_SALT = 16; + /** Number of characters of the salt in its radix64-encoded form. */ + public static final int SALT_LENGTH_ENCODED = 22; + + private final BCrypt.Hasher hasher; + private final int costFactor; + + /** + * Constructor. + * + * @param version the BCrypt version the instance should generate + * @param costFactor the log2 cost factor to use + */ + public BCryptHasher(BCrypt.Version version, int costFactor) { + this.hasher = BCrypt.with(version); + this.costFactor = costFactor; + } + + public HashedPassword hash(String password) { + byte[] hash = hasher.hash(costFactor, password.getBytes(UTF_8)); + return new HashedPassword(new String(hash, UTF_8)); + } + + public String hashWithRawSalt(String password, byte[] rawSalt) { + byte[] hash = hasher.hash(costFactor, rawSalt, password.getBytes(UTF_8)); + return new String(hash, UTF_8); + } + + /** + * Verifies that the given password is correct for the provided BCrypt hash. + * + * @param password the password to check with + * @param hash the hash to check against + * @return true if the password matches the hash, false otherwise + */ + public static boolean comparePassword(String password, String hash) { + if (HashUtils.isValidBcryptHash(hash)) { + BCrypt.Result result = BCrypt.verifyer().verify(password.getBytes(UTF_8), hash.getBytes(UTF_8)); + return result.verified; + } + return false; + } + + /** + * Generates a salt for usage in BCrypt. The returned salt is not yet encoded. + *

+ * Internally, the BCrypt library in {@link BCrypt.Hasher#hash(int, byte[])} uses the following: + * {@code Bytes.random(16, secureRandom).encodeUtf8();} + *

+ * Because our {@link EncryptionMethod} interface works with {@code String} types we need to make sure that the + * generated bytes in the salt are suitable for conversion into a String, such that calling String#getBytes will + * yield the same number of bytes again. Thus, we are forced to limit the range of characters we use. Ideally we'd + * only have to pass the salt in its encoded form so that we could make use of the entire "spectrum" of values, + * which proves difficult to achieve with the underlying BCrypt library. However, the salt needs to be generated + * manually only for testing purposes; production code should always hash passwords using + * {@link EncryptionMethod#computeHash(String, String)}, which internally may represent salts in more suitable + * formats. + * + * @return the salt for a BCrypt hash + */ + public static String generateSalt() { + return RandomStringUtils.generateLowerUpper(BYTES_IN_SALT); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/CmwCrypt.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/CmwCrypt.java new file mode 100644 index 00000000..2b94dc03 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/CmwCrypt.java @@ -0,0 +1,14 @@ +package fr.xephi.authme.security.crypts; + +import fr.xephi.authme.security.HashUtils; + +/** + * Hash algorithm to hook into the CMS Craft My Website. + */ +public class CmwCrypt extends UnsaltedMethod { + + @Override + public String computeHash(String password) { + return HashUtils.md5(HashUtils.sha1(password)); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/CrazyCrypt1.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/CrazyCrypt1.java new file mode 100644 index 00000000..6130c6a1 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/CrazyCrypt1.java @@ -0,0 +1,31 @@ +package fr.xephi.authme.security.crypts; + +import fr.xephi.authme.security.HashUtils; +import fr.xephi.authme.security.MessageDigestAlgorithm; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; + +public class CrazyCrypt1 extends UsernameSaltMethod { + + private static final char[] CRYPTCHARS = + {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; + + private static String byteArrayToHexString(final byte... args) { + final char[] chars = new char[args.length * 2]; + for (int i = 0; i < args.length; i++) { + chars[i * 2] = CRYPTCHARS[(args[i] >> 4) & 0xF]; + chars[i * 2 + 1] = CRYPTCHARS[(args[i]) & 0xF]; + } + return new String(chars); + } + + @Override + public HashedPassword computeHash(String password, String name) { + final String text = "ÜÄaeut//&/=I " + password + "7421€547" + name + "__+IÄIH§%NK " + password; + final MessageDigest md = HashUtils.getDigest(MessageDigestAlgorithm.SHA512); + md.update(text.getBytes(StandardCharsets.UTF_8), 0, text.length()); + return new HashedPassword(byteArrayToHexString(md.digest())); + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/DoubleMd5.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/DoubleMd5.java new file mode 100644 index 00000000..12e96808 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/DoubleMd5.java @@ -0,0 +1,17 @@ +package fr.xephi.authme.security.crypts; + +import fr.xephi.authme.security.crypts.description.Recommendation; +import fr.xephi.authme.security.crypts.description.Usage; + +import static fr.xephi.authme.security.HashUtils.md5; + +@Deprecated +@Recommendation(Usage.DEPRECATED) +public class DoubleMd5 extends UnsaltedMethod { + + @Override + public String computeHash(String password) { + return md5(md5(password)); + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/EncryptionMethod.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/EncryptionMethod.java new file mode 100644 index 00000000..83cc6c8c --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/EncryptionMethod.java @@ -0,0 +1,61 @@ +package fr.xephi.authme.security.crypts; + +/** + * Public interface for custom password encryption methods. + *

+ * Instantiation of these methods is done via automatic dependency injection. + */ +public interface EncryptionMethod { + + /** + * Hash the given password for the given player name. + * + * @param password The password to hash + * @param name The name of the player (sometimes required to generate a salt with) + * + * @return The hash result for the password. + * @see HashedPassword + */ + HashedPassword computeHash(String password, String name); + + /** + * Hash the given password with the given salt for the given player. + * + * @param password The password to hash + * @param salt The salt to add to the hash + * @param name The player's name (sometimes required to generate a salt with) + * + * @return The hashed password + * @see #hasSeparateSalt() + */ + String computeHash(String password, String salt, String name); + + /** + * Check whether the given hash matches the clear-text password. + * + * @param password The clear-text password to verify + * @param hashedPassword The hash to check the password against + * @param name The player name to do the check for (sometimes required for generating the salt) + * + * @return True if the password matches, false otherwise + */ + boolean comparePassword(String password, HashedPassword hashedPassword, String name); + + /** + * Generate a new salt to hash a password with. + * + * @return The generated salt, null if the method does not use a random text-based salt + */ + String generateSalt(); + + /** + * Return whether the encryption method requires the salt to be stored separately and + * passed again to {@link #comparePassword(String, HashedPassword, String)}. Note that + * an encryption method returning {@code false} does not imply that it uses no salt; it + * may be embedded into the hash or it may use the username as salt. + * + * @return True if the salt has to be stored and retrieved separately, false otherwise + */ + boolean hasSeparateSalt(); + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/HashedPassword.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/HashedPassword.java new file mode 100644 index 00000000..f2e40aa4 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/HashedPassword.java @@ -0,0 +1,48 @@ +package fr.xephi.authme.security.crypts; + +/** + * The result of a hash computation. See {@link #salt} for details. + */ +public class HashedPassword { + + /** The generated hash. */ + private final String hash; + /** + * The generated salt; may be null if no salt is used or if the salt is included + * in the hash output. The salt is only not null if {@link EncryptionMethod#hasSeparateSalt()} + * returns true for the associated encryption method. + *

+ * When the field is not null, it must be stored into the salt column of the data source + * and retrieved again to compare a password with the hash. + */ + private final String salt; + + /** + * Constructor. + * + * @param hash The computed hash + * @param salt The generated salt + */ + public HashedPassword(String hash, String salt) { + this.hash = hash; + this.salt = salt; + } + + /** + * Constructor for a hash with no separate salt. + * + * @param hash The computed hash + */ + public HashedPassword(String hash) { + this(hash, null); + } + + public String getHash() { + return hash; + } + + public String getSalt() { + return salt; + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/HexSaltedMethod.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/HexSaltedMethod.java new file mode 100644 index 00000000..81e76772 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/HexSaltedMethod.java @@ -0,0 +1,40 @@ +package fr.xephi.authme.security.crypts; + +import fr.xephi.authme.security.crypts.description.HasSalt; +import fr.xephi.authme.security.crypts.description.Recommendation; +import fr.xephi.authme.security.crypts.description.SaltType; +import fr.xephi.authme.security.crypts.description.Usage; +import fr.xephi.authme.util.RandomStringUtils; + +/** + * Common type for encryption methods which use a random String of hexadecimal characters + * and store the salt with the hash itself. + */ +@Recommendation(Usage.ACCEPTABLE) +@HasSalt(SaltType.TEXT) // See getSaltLength() for length +public abstract class HexSaltedMethod implements EncryptionMethod { + + public abstract int getSaltLength(); + + @Override + public abstract String computeHash(String password, String salt, String name); + + @Override + public HashedPassword computeHash(String password, String name) { + String salt = generateSalt(); + return new HashedPassword(computeHash(password, salt, null)); + } + + @Override + public abstract boolean comparePassword(String password, HashedPassword hashedPassword, String name); + + @Override + public String generateSalt() { + return RandomStringUtils.generateHex(getSaltLength()); + } + + @Override + public boolean hasSeparateSalt() { + return false; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/Ipb3.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/Ipb3.java new file mode 100644 index 00000000..b29df4fc --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/Ipb3.java @@ -0,0 +1,25 @@ +package fr.xephi.authme.security.crypts; + +import fr.xephi.authme.security.crypts.description.HasSalt; +import fr.xephi.authme.security.crypts.description.Recommendation; +import fr.xephi.authme.security.crypts.description.SaltType; +import fr.xephi.authme.security.crypts.description.Usage; +import fr.xephi.authme.util.RandomStringUtils; + +import static fr.xephi.authme.security.HashUtils.md5; + +@Recommendation(Usage.ACCEPTABLE) +@HasSalt(value = SaltType.TEXT, length = 5) +public class Ipb3 extends SeparateSaltMethod { + + @Override + public String computeHash(String password, String salt, String name) { + return md5(md5(salt) + md5(password)); + } + + @Override + public String generateSalt() { + return RandomStringUtils.generateHex(5); + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/Ipb4.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/Ipb4.java new file mode 100644 index 00000000..e176d0aa --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/Ipb4.java @@ -0,0 +1,67 @@ +package fr.xephi.authme.security.crypts; + +import at.favre.lib.crypto.bcrypt.BCrypt; +import at.favre.lib.crypto.bcrypt.IllegalBCryptFormatException; +import fr.xephi.authme.security.crypts.description.HasSalt; +import fr.xephi.authme.security.crypts.description.Recommendation; +import fr.xephi.authme.security.crypts.description.SaltType; +import fr.xephi.authme.security.crypts.description.Usage; +import fr.xephi.authme.util.RandomStringUtils; + +import static java.nio.charset.StandardCharsets.UTF_8; + + +/** + * Implementation for Ipb4 (Invision Power Board 4). + *

+ * The hash uses standard BCrypt with 13 as log2 number of rounds. Additionally, + * Ipb4 requires that the salt be stored in the column "members_pass_hash" as well + * (even though BCrypt hashes already contain the salt within themselves). + */ +@Recommendation(Usage.DOES_NOT_WORK) +@HasSalt(value = SaltType.TEXT, length = BCryptHasher.SALT_LENGTH_ENCODED) +public class Ipb4 implements EncryptionMethod { + + private BCryptHasher bCryptHasher = new BCryptHasher(BCrypt.Version.VERSION_2A, 13); + + @Override + public String computeHash(String password, String salt, String name) { + // Since the radix64-encoded salt is necessary to be stored separately as well, the incoming salt here is + // radix64-encoded (see #generateSalt()). This means we first need to decode it before passing into the + // bcrypt hasher... We cheat by inserting the encoded salt into a dummy bcrypt hash so that we can parse it + // with the BCrypt utilities. + // This method (with specific salt) is only used for testing purposes, so this approach should be OK. + + String dummyHash = "$2a$10$" + salt + "3Cfb5GnwvKhJ20r.hMjmcNkIT9.Uh9K"; + try { + BCrypt.HashData parseResult = BCrypt.Version.VERSION_2A.parser.parse(dummyHash.getBytes(UTF_8)); + return bCryptHasher.hashWithRawSalt(password, parseResult.rawSalt); + } catch (IllegalBCryptFormatException |IllegalArgumentException e) { + throw new IllegalStateException("Cannot parse hash with salt '" + salt + "'", e); + } + } + + @Override + public HashedPassword computeHash(String password, String name) { + HashedPassword hash = bCryptHasher.hash(password); + + // 7 chars prefix, then 22 chars which is the encoded salt, which we need again + String salt = hash.getHash().substring(7, 29); + return new HashedPassword(hash.getHash(), salt); + } + + @Override + public boolean comparePassword(String password, HashedPassword hashedPassword, String name) { + return BCryptHasher.comparePassword(password, hashedPassword.getHash()); + } + + @Override + public String generateSalt() { + return RandomStringUtils.generateLowerUpper(22); + } + + @Override + public boolean hasSeparateSalt() { + return true; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/Joomla.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/Joomla.java new file mode 100644 index 00000000..2ecc1d8d --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/Joomla.java @@ -0,0 +1,29 @@ +package fr.xephi.authme.security.crypts; + +import fr.xephi.authme.security.HashUtils; +import fr.xephi.authme.security.crypts.description.Recommendation; +import fr.xephi.authme.security.crypts.description.Usage; + +import static fr.xephi.authme.security.HashUtils.isEqual; + +@Recommendation(Usage.ACCEPTABLE) +public class Joomla extends HexSaltedMethod { + + @Override + public String computeHash(String password, String salt, String name) { + return HashUtils.md5(password + salt) + ":" + salt; + } + + @Override + public boolean comparePassword(String password, HashedPassword hashedPassword, String unusedName) { + String hash = hashedPassword.getHash(); + String[] hashParts = hash.split(":"); + return hashParts.length == 2 && isEqual(hash, computeHash(password, hashParts[1], null)); + } + + @Override + public int getSaltLength() { + return 32; + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/Md5.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/Md5.java new file mode 100644 index 00000000..55e07a17 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/Md5.java @@ -0,0 +1,16 @@ +package fr.xephi.authme.security.crypts; + +import fr.xephi.authme.security.HashUtils; +import fr.xephi.authme.security.crypts.description.Recommendation; +import fr.xephi.authme.security.crypts.description.Usage; + +@Deprecated +@Recommendation(Usage.DEPRECATED) +public class Md5 extends UnsaltedMethod { + + @Override + public String computeHash(String password) { + return HashUtils.md5(password); + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/Md5vB.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/Md5vB.java new file mode 100644 index 00000000..00656964 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/Md5vB.java @@ -0,0 +1,25 @@ +package fr.xephi.authme.security.crypts; + +import static fr.xephi.authme.security.HashUtils.isEqual; +import static fr.xephi.authme.security.HashUtils.md5; + +public class Md5vB extends HexSaltedMethod { + + @Override + public String computeHash(String password, String salt, String name) { + return "$MD5vb$" + salt + "$" + md5(md5(password) + salt); + } + + @Override + public boolean comparePassword(String password, HashedPassword hashedPassword, String name) { + String hash = hashedPassword.getHash(); + String[] line = hash.split("\\$"); + return line.length == 4 && isEqual(hash, computeHash(password, line[2], name)); + } + + @Override + public int getSaltLength() { + return 16; + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/MyBB.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/MyBB.java new file mode 100644 index 00000000..3ff0ee5e --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/MyBB.java @@ -0,0 +1,26 @@ +package fr.xephi.authme.security.crypts; + +import fr.xephi.authme.security.crypts.description.HasSalt; +import fr.xephi.authme.security.crypts.description.Recommendation; +import fr.xephi.authme.security.crypts.description.SaltType; +import fr.xephi.authme.security.crypts.description.Usage; +import fr.xephi.authme.util.RandomStringUtils; + +import static fr.xephi.authme.security.HashUtils.md5; + +@Recommendation(Usage.ACCEPTABLE) +@HasSalt(value = SaltType.TEXT, length = 8) +@SuppressWarnings({"checkstyle:AbbreviationAsWordInName"}) +public class MyBB extends SeparateSaltMethod { + + @Override + public String computeHash(String password, String salt, String name) { + return md5(md5(salt) + md5(password)); + } + + @Override + public String generateSalt() { + return RandomStringUtils.generateLowerUpper(8); + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/NoCrypt.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/NoCrypt.java new file mode 100644 index 00000000..afd298ec --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/NoCrypt.java @@ -0,0 +1,15 @@ +package fr.xephi.authme.security.crypts; + +import fr.xephi.authme.security.crypts.description.Recommendation; +import fr.xephi.authme.security.crypts.description.Usage; + +@Deprecated +@Recommendation(Usage.DEPRECATED) +public class NoCrypt extends UnsaltedMethod { + + @Override + public String computeHash(String password) { + return password; + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/Pbkdf2.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/Pbkdf2.java new file mode 100644 index 00000000..c79647ab --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/Pbkdf2.java @@ -0,0 +1,62 @@ +package fr.xephi.authme.security.crypts; + +import com.google.common.primitives.Ints; +import de.rtner.misc.BinTools; +import de.rtner.security.auth.spi.PBKDF2Engine; +import de.rtner.security.auth.spi.PBKDF2Parameters; +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.security.crypts.description.Recommendation; +import fr.xephi.authme.security.crypts.description.Usage; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.properties.SecuritySettings; + +import javax.inject.Inject; + +@Recommendation(Usage.RECOMMENDED) +public class Pbkdf2 extends HexSaltedMethod { + + private static final int DEFAULT_ROUNDS = 10_000; + private final ConsoleLogger logger = ConsoleLoggerFactory.get(Pbkdf2.class); + private int numberOfRounds; + + @Inject + Pbkdf2(Settings settings) { + int configuredRounds = settings.getProperty(SecuritySettings.PBKDF2_NUMBER_OF_ROUNDS); + this.numberOfRounds = configuredRounds > 0 ? configuredRounds : DEFAULT_ROUNDS; + } + + @Override + public String computeHash(String password, String salt, String name) { + String result = "pbkdf2_sha256$" + numberOfRounds + "$" + salt + "$"; + PBKDF2Parameters params = new PBKDF2Parameters("HmacSHA256", "UTF-8", salt.getBytes(), numberOfRounds); + PBKDF2Engine engine = new PBKDF2Engine(params); + + return result + BinTools.bin2hex(engine.deriveKey(password, 64)); + } + + @Override + public boolean comparePassword(String password, HashedPassword hashedPassword, String unusedName) { + String[] line = hashedPassword.getHash().split("\\$"); + if (line.length != 4) { + return false; + } + Integer iterations = Ints.tryParse(line[1]); + if (iterations == null) { + logger.warning("Cannot read number of rounds for Pbkdf2: '" + line[1] + "'"); + return false; + } + + String salt = line[2]; + byte[] derivedKey = BinTools.hex2bin(line[3]); + PBKDF2Parameters params = new PBKDF2Parameters("HmacSHA256", "UTF-8", salt.getBytes(), iterations, derivedKey); + PBKDF2Engine engine = new PBKDF2Engine(params); + return engine.verifyKey(password); + } + + @Override + public int getSaltLength() { + return 16; + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/Pbkdf2Django.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/Pbkdf2Django.java new file mode 100644 index 00000000..a0877cd4 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/Pbkdf2Django.java @@ -0,0 +1,51 @@ +package fr.xephi.authme.security.crypts; + +import com.google.common.primitives.Ints; +import de.rtner.security.auth.spi.PBKDF2Engine; +import de.rtner.security.auth.spi.PBKDF2Parameters; +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.security.crypts.description.AsciiRestricted; + +import java.util.Base64; + +@AsciiRestricted +public class Pbkdf2Django extends HexSaltedMethod { + + private static final int DEFAULT_ITERATIONS = 24000; + private final ConsoleLogger logger = ConsoleLoggerFactory.get(Pbkdf2Django.class); + + @Override + public String computeHash(String password, String salt, String name) { + String result = "pbkdf2_sha256$" + DEFAULT_ITERATIONS + "$" + salt + "$"; + PBKDF2Parameters params = new PBKDF2Parameters("HmacSHA256", "ASCII", salt.getBytes(), DEFAULT_ITERATIONS); + PBKDF2Engine engine = new PBKDF2Engine(params); + + return result + Base64.getEncoder().encodeToString(engine.deriveKey(password, 32)); + } + + @Override + public boolean comparePassword(String password, HashedPassword hashedPassword, String unusedName) { + String[] line = hashedPassword.getHash().split("\\$"); + if (line.length != 4) { + return false; + } + Integer iterations = Ints.tryParse(line[1]); + if (iterations == null) { + logger.warning("Cannot read number of rounds for Pbkdf2Django: '" + line[1] + "'"); + return false; + } + + String salt = line[2]; + byte[] derivedKey = Base64.getDecoder().decode(line[3]); + PBKDF2Parameters params = new PBKDF2Parameters("HmacSHA256", "ASCII", salt.getBytes(), iterations, derivedKey); + PBKDF2Engine engine = new PBKDF2Engine(params); + return engine.verifyKey(password); + } + + @Override + public int getSaltLength() { + return 12; + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/PhpBB.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/PhpBB.java new file mode 100644 index 00000000..c4e27e9d --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/PhpBB.java @@ -0,0 +1,162 @@ +package fr.xephi.authme.security.crypts; + +import fr.xephi.authme.security.HashUtils; +import fr.xephi.authme.security.MessageDigestAlgorithm; +import fr.xephi.authme.security.crypts.description.HasSalt; +import fr.xephi.authme.security.crypts.description.Recommendation; +import fr.xephi.authme.security.crypts.description.SaltType; +import fr.xephi.authme.security.crypts.description.Usage; + +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; + +import static fr.xephi.authme.security.HashUtils.isEqual; +import static fr.xephi.authme.security.crypts.BCryptHasher.SALT_LENGTH_ENCODED; + +/** + * Encryption method compatible with phpBB3. + *

+ * As tested with phpBB 3.2.1, by default new passwords are encrypted with BCrypt $2y$. + * For backwards compatibility, phpBB3 supports other hashes for comparison. This implementation + * successfully checks against phpBB's salted MD5 hashing algorithm (adaptation of phpass), + * as well as plain MD5. + */ +@Recommendation(Usage.ACCEPTABLE) +@HasSalt(value = SaltType.TEXT, length = SALT_LENGTH_ENCODED) +public class PhpBB implements EncryptionMethod { + + private final BCrypt2y bCrypt2y = new BCrypt2y(); + + @Override + public HashedPassword computeHash(String password, String name) { + return bCrypt2y.computeHash(password, name); + } + + @Override + public String computeHash(String password, String salt, String name) { + return bCrypt2y.computeHash(password, salt, name); + } + + @Override + public boolean comparePassword(String password, HashedPassword hashedPassword, String name) { + final String hash = hashedPassword.getHash(); + if (HashUtils.isValidBcryptHash(hash)) { + return bCrypt2y.comparePassword(password, hashedPassword, name); + } else if (hash.length() == 34) { + return PhpassSaltedMd5.phpbb_check_hash(password, hash); + } else { + return isEqual(hash, PhpassSaltedMd5.md5(password)); + } + } + + @Override + public String generateSalt() { + // Salt length 22, as seen in https://github.com/phpbb/phpbb/blob/master/phpBB/phpbb/passwords/driver/bcrypt.php + // Ours generates 16 chars because the salt must not yet be encoded. + return BCryptHasher.generateSalt(); + } + + @Override + public boolean hasSeparateSalt() { + return false; + } + + /** + * Java implementation of the salted MD5 as used in phpBB (adapted from phpass). + * + * @see phpBB's salted_md5.php + * @see phpass + */ + private static final class PhpassSaltedMd5 { + + private static final String itoa64 = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + + private static String md5(String data) { + try { + byte[] bytes = data.getBytes("ISO-8859-1"); + MessageDigest md5er = HashUtils.getDigest(MessageDigestAlgorithm.MD5); + byte[] hash = md5er.digest(bytes); + return bytes2hex(hash); + } catch (UnsupportedEncodingException e) { + throw new UnsupportedOperationException(e); + } + } + + private static int hexToInt(char ch) { + if (ch >= '0' && ch <= '9') + return ch - '0'; + ch = Character.toUpperCase(ch); + if (ch >= 'A' && ch <= 'F') + return ch - 'A' + 0xA; + throw new IllegalArgumentException("Not a hex character: " + ch); + } + + private static String bytes2hex(byte[] bytes) { + StringBuilder r = new StringBuilder(32); + for (byte b : bytes) { + String x = Integer.toHexString(b & 0xff); + if (x.length() < 2) + r.append('0'); + r.append(x); + } + return r.toString(); + } + + private static String pack(String hex) { + StringBuilder buf = new StringBuilder(); + for (int i = 0; i < hex.length(); i += 2) { + char c1 = hex.charAt(i); + char c2 = hex.charAt(i + 1); + char packed = (char) (hexToInt(c1) * 16 + hexToInt(c2)); + buf.append(packed); + } + return buf.toString(); + } + + private static String _hash_encode64(String input, int count) { + StringBuilder output = new StringBuilder(); + int i = 0; + do { + int value = input.charAt(i++); + output.append(itoa64.charAt(value & 0x3f)); + if (i < count) + value |= input.charAt(i) << 8; + output.append(itoa64.charAt((value >> 6) & 0x3f)); + if (i++ >= count) + break; + if (i < count) + value |= input.charAt(i) << 16; + output.append(itoa64.charAt((value >> 12) & 0x3f)); + if (i++ >= count) + break; + output.append(itoa64.charAt((value >> 18) & 0x3f)); + } while (i < count); + return output.toString(); + } + + private static String _hash_crypt_private(String password, String setting) { + String output = "*"; + if (!setting.substring(0, 3).equals("$H$")) + return output; + int count_log2 = itoa64.indexOf(setting.charAt(3)); + if (count_log2 < 7 || count_log2 > 30) + return output; + int count = 1 << count_log2; + String salt = setting.substring(4, 12); + if (salt.length() != 8) + return output; + String m1 = md5(salt + password); + String hash = pack(m1); + do { + hash = pack(md5(hash + password)); + } while (--count > 0); + output = setting.substring(0, 12); + output += _hash_encode64(hash, 16); + return output; + } + + private static boolean phpbb_check_hash(String password, String hash) { + return isEqual(hash, _hash_crypt_private(password, hash)); // #1561: fix timing issue + } + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/PhpFusion.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/PhpFusion.java new file mode 100644 index 00000000..b9744117 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/PhpFusion.java @@ -0,0 +1,48 @@ +package fr.xephi.authme.security.crypts; + +import fr.xephi.authme.security.HashUtils; +import fr.xephi.authme.security.crypts.description.AsciiRestricted; +import fr.xephi.authme.security.crypts.description.Recommendation; +import fr.xephi.authme.security.crypts.description.Usage; +import fr.xephi.authme.util.RandomStringUtils; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.io.UnsupportedEncodingException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; + +@Recommendation(Usage.DO_NOT_USE) +@AsciiRestricted +public class PhpFusion extends SeparateSaltMethod { + + @Override + public String computeHash(String password, String salt, String name) { + String algo = "HmacSHA256"; + String keyString = HashUtils.sha1(salt); + try { + SecretKeySpec key = new SecretKeySpec(keyString.getBytes("UTF-8"), algo); + Mac mac = Mac.getInstance(algo); + mac.init(key); + byte[] bytes = mac.doFinal(password.getBytes("ASCII")); + StringBuilder hash = new StringBuilder(); + for (byte aByte : bytes) { + String hex = Integer.toHexString(0xFF & aByte); + if (hex.length() == 1) { + hash.append('0'); + } + hash.append(hex); + } + return hash.toString(); + } catch (UnsupportedEncodingException | InvalidKeyException | NoSuchAlgorithmException e) { + throw new UnsupportedOperationException("Cannot create PHPFUSION hash for " + name, e); + } + } + + @Override + public String generateSalt() { + return RandomStringUtils.generateHex(12); + } + + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/RoyalAuth.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/RoyalAuth.java new file mode 100644 index 00000000..9f557438 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/RoyalAuth.java @@ -0,0 +1,19 @@ +package fr.xephi.authme.security.crypts; + +import fr.xephi.authme.security.HashUtils; +import fr.xephi.authme.security.MessageDigestAlgorithm; + +import java.security.MessageDigest; + +public class RoyalAuth extends UnsaltedMethod { + + @Override + public String computeHash(String password) { + MessageDigest algorithm = HashUtils.getDigest(MessageDigestAlgorithm.SHA512); + for (int i = 0; i < 25; ++i) { + password = HashUtils.hash(password, algorithm); + } + return password; + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/Salted2Md5.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/Salted2Md5.java new file mode 100644 index 00000000..3d2166a7 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/Salted2Md5.java @@ -0,0 +1,37 @@ +package fr.xephi.authme.security.crypts; + +import fr.xephi.authme.security.crypts.description.HasSalt; +import fr.xephi.authme.security.crypts.description.Recommendation; +import fr.xephi.authme.security.crypts.description.SaltType; +import fr.xephi.authme.security.crypts.description.Usage; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.properties.SecuritySettings; +import fr.xephi.authme.util.RandomStringUtils; + +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 the doubleMd5SaltLength setting +public class Salted2Md5 extends SeparateSaltMethod { + + private final int saltLength; + + @Inject + public Salted2Md5(Settings settings) { + saltLength = settings.getProperty(SecuritySettings.DOUBLE_MD5_SALT_LENGTH); + } + + @Override + public String computeHash(String password, String salt, String name) { + return md5(md5(password) + salt); + } + + @Override + public String generateSalt() { + return RandomStringUtils.generateHex(saltLength); + } + + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/SaltedSha512.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/SaltedSha512.java new file mode 100644 index 00000000..b5578b93 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/SaltedSha512.java @@ -0,0 +1,20 @@ +package fr.xephi.authme.security.crypts; + +import fr.xephi.authme.security.HashUtils; +import fr.xephi.authme.security.crypts.description.Recommendation; +import fr.xephi.authme.security.crypts.description.Usage; +import fr.xephi.authme.util.RandomStringUtils; + +@Recommendation(Usage.RECOMMENDED) +public class SaltedSha512 extends SeparateSaltMethod { + + @Override + public String computeHash(String password, String salt, String name) { + return HashUtils.sha512(password + salt); + } + + @Override + public String generateSalt() { + return RandomStringUtils.generateHex(32); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/SeparateSaltMethod.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/SeparateSaltMethod.java new file mode 100644 index 00000000..c0ec13dd --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/SeparateSaltMethod.java @@ -0,0 +1,32 @@ +package fr.xephi.authme.security.crypts; + +import static fr.xephi.authme.security.HashUtils.isEqual; + +/** + * Common supertype for encryption methods which store their salt separately from the hash. + */ +public abstract class SeparateSaltMethod implements EncryptionMethod { + + @Override + public abstract String computeHash(String password, String salt, String name); + + @Override + public HashedPassword computeHash(String password, String name) { + String salt = generateSalt(); + return new HashedPassword(computeHash(password, salt, name), salt); + } + + @Override + public abstract String generateSalt(); + + @Override + public boolean comparePassword(String password, HashedPassword hashedPassword, String name) { + return isEqual(hashedPassword.getHash(), computeHash(password, hashedPassword.getSalt(), null)); + } + + @Override + public boolean hasSeparateSalt() { + return true; + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/Sha1.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/Sha1.java new file mode 100644 index 00000000..6d095b4a --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/Sha1.java @@ -0,0 +1,16 @@ +package fr.xephi.authme.security.crypts; + +import fr.xephi.authme.security.HashUtils; +import fr.xephi.authme.security.crypts.description.Recommendation; +import fr.xephi.authme.security.crypts.description.Usage; + +@Deprecated +@Recommendation(Usage.DEPRECATED) +public class Sha1 extends UnsaltedMethod { + + @Override + public String computeHash(String password) { + return HashUtils.sha1(password); + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/Sha256.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/Sha256.java new file mode 100644 index 00000000..ce6b2549 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/Sha256.java @@ -0,0 +1,29 @@ +package fr.xephi.authme.security.crypts; + +import fr.xephi.authme.security.crypts.description.Recommendation; +import fr.xephi.authme.security.crypts.description.Usage; + +import static fr.xephi.authme.security.HashUtils.isEqual; +import static fr.xephi.authme.security.HashUtils.sha256; + +@Recommendation(Usage.RECOMMENDED) +public class Sha256 extends HexSaltedMethod { + + @Override + public String computeHash(String password, String salt, String name) { + return "$SHA$" + salt + "$" + sha256(sha256(password) + salt); + } + + @Override + public boolean comparePassword(String password, HashedPassword hashedPassword, String name) { + String hash = hashedPassword.getHash(); + String[] line = hash.split("\\$"); + return line.length == 4 && isEqual(hash, computeHash(password, line[2], name)); + } + + @Override + public int getSaltLength() { + return 16; + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/Sha512.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/Sha512.java new file mode 100644 index 00000000..36dc5a34 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/Sha512.java @@ -0,0 +1,16 @@ +package fr.xephi.authme.security.crypts; + +import fr.xephi.authme.security.HashUtils; +import fr.xephi.authme.security.crypts.description.Recommendation; +import fr.xephi.authme.security.crypts.description.Usage; + +@Deprecated +@Recommendation(Usage.DEPRECATED) +public class Sha512 extends UnsaltedMethod { + + @Override + public String computeHash(String password) { + return HashUtils.sha512(password); + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/Smf.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/Smf.java new file mode 100644 index 00000000..19193d63 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/Smf.java @@ -0,0 +1,51 @@ +package fr.xephi.authme.security.crypts; + +import fr.xephi.authme.security.HashUtils; +import fr.xephi.authme.security.crypts.description.HasSalt; +import fr.xephi.authme.security.crypts.description.Recommendation; +import fr.xephi.authme.security.crypts.description.SaltType; +import fr.xephi.authme.security.crypts.description.Usage; +import fr.xephi.authme.util.RandomStringUtils; + +import java.util.Locale; + +import static fr.xephi.authme.security.HashUtils.isEqual; + +/** + * Hashing algorithm for SMF forums. + *

+ * The hash algorithm is {@code sha1(strtolower($username) . $password)}. However, an additional four-character + * salt is generated for each user, used to generate the login cookie. Therefore, this implementation generates a salt + * and declares that it requires a separate salt (the SMF members table has a not-null constraint on the salt column). + * + * @see Simple Machines Forum + */ +@Recommendation(Usage.DO_NOT_USE) +@HasSalt(SaltType.USERNAME) +public class Smf implements EncryptionMethod { + + @Override + public HashedPassword computeHash(String password, String name) { + return new HashedPassword(computeHash(password, null, name), generateSalt()); + } + + @Override + public String computeHash(String password, String salt, String name) { + return HashUtils.sha1(name.toLowerCase(Locale.ROOT) + password); + } + + @Override + public boolean comparePassword(String password, HashedPassword hashedPassword, String name) { + return isEqual(hashedPassword.getHash(), computeHash(password, null, name)); + } + + @Override + public String generateSalt() { + return RandomStringUtils.generate(4); + } + + @Override + public boolean hasSeparateSalt() { + return true; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/TwoFactor.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/TwoFactor.java new file mode 100644 index 00000000..67b22c78 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/TwoFactor.java @@ -0,0 +1,140 @@ +package fr.xephi.authme.security.crypts; + +import com.google.common.escape.Escaper; +import com.google.common.io.BaseEncoding; +import com.google.common.net.UrlEscapers; +import com.google.common.primitives.Ints; +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.security.crypts.description.HasSalt; +import fr.xephi.authme.security.crypts.description.Recommendation; +import fr.xephi.authme.security.crypts.description.SaltType; +import fr.xephi.authme.security.crypts.description.Usage; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.Calendar; +import java.util.concurrent.TimeUnit; + +/** + * Two factor authentication. + * + * @see Original source + */ +@Recommendation(Usage.DOES_NOT_WORK) +@HasSalt(SaltType.NONE) +public class TwoFactor extends UnsaltedMethod { + + private static final int SCRET_BYTE = 10; + private static final int SCRATCH_CODES = 5; + private static final int BYTES_PER_SCRATCH_CODE = 4; + + private static final int TIME_PRECISION = 3; + private static final String CRYPTO_ALGO = "HmacSHA1"; + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(TwoFactor.class); + + /** + * Creates a link to a QR barcode with the provided secret. + * + * @param user the player's name + * @param host the server host + * @param secret the TOTP secret + * @return URL leading to a QR code + */ + public static String getQrBarcodeUrl(String user, String host, String secret) { + String format = "https://www.google.com/chart?chs=130x130&chld=M%%7C0&cht=qr&chl=" + + "otpauth://totp/" + + "%s@%s%%3Fsecret%%3D%s"; + Escaper urlEscaper = UrlEscapers.urlFragmentEscaper(); + return String.format(format, urlEscaper.escape(user), urlEscaper.escape(host), secret); + } + + @Override + public String computeHash(String password) { + // Allocating the buffer + byte[] buffer = new byte[SCRET_BYTE + SCRATCH_CODES * BYTES_PER_SCRATCH_CODE]; + + // Filling the buffer with random numbers. + // Notice: you want to reuse the same random generator + // while generating larger random number sequences. + new SecureRandom().nextBytes(buffer); + + // Getting the key and converting it to Base32 + byte[] secretKey = Arrays.copyOf(buffer, SCRET_BYTE); + return BaseEncoding.base32().encode(secretKey); + } + + @Override + public boolean comparePassword(String password, HashedPassword hashedPassword, String name) { + try { + return checkPassword(hashedPassword.getHash(), password); + } catch (Exception e) { + logger.logException("Failed to verify two auth code:", e); + return false; + } + } + + private boolean checkPassword(String secretKey, String userInput) + throws NoSuchAlgorithmException, InvalidKeyException { + Integer code = Ints.tryParse(userInput); + if (code == null) { + //code is not an integer + return false; + } + + long currentTime = Calendar.getInstance().getTimeInMillis() / TimeUnit.SECONDS.toMillis(30); + return checkCode(secretKey, code, currentTime); + } + + private boolean checkCode(String secret, long code, long t) throws NoSuchAlgorithmException, InvalidKeyException { + byte[] decodedKey = BaseEncoding.base32().decode(secret); + + // Window is used to check codes generated in the near past. + // You can use this value to tune how far you're willing to go. + int window = TIME_PRECISION; + for (int i = -window; i <= window; ++i) { + long hash = verifyCode(decodedKey, t + i); + + if (hash == code) { + return true; + } + } + + // The validation code is invalid. + return false; + } + + private int verifyCode(byte[] key, long t) throws NoSuchAlgorithmException, InvalidKeyException { + byte[] data = new byte[8]; + long value = t; + for (int i = 8; i-- > 0; value >>>= 8) { + data[i] = (byte) value; + } + + SecretKeySpec signKey = new SecretKeySpec(key, CRYPTO_ALGO); + Mac mac = Mac.getInstance(CRYPTO_ALGO); + mac.init(signKey); + byte[] hash = mac.doFinal(data); + + int offset = hash[20 - 1] & 0xF; + + // We're using a long because Java hasn't got unsigned int. + long truncatedHash = 0; + for (int i = 0; i < 4; ++i) { + truncatedHash <<= 8; + // We are dealing with signed bytes: + // we just keep the first byte. + truncatedHash |= (hash[offset + i] & 0xFF); + } + + truncatedHash &= 0x7FFF_FFFF; + truncatedHash %= 1_000_000; + + return (int) truncatedHash; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/UnsaltedMethod.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/UnsaltedMethod.java new file mode 100644 index 00000000..33815ec7 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/UnsaltedMethod.java @@ -0,0 +1,43 @@ +package fr.xephi.authme.security.crypts; + +import fr.xephi.authme.security.crypts.description.HasSalt; +import fr.xephi.authme.security.crypts.description.Recommendation; +import fr.xephi.authme.security.crypts.description.SaltType; +import fr.xephi.authme.security.crypts.description.Usage; + +import static fr.xephi.authme.security.HashUtils.isEqual; + +/** + * Common type for encryption methods which do not use any salt whatsoever. + */ +@Recommendation(Usage.DO_NOT_USE) +@HasSalt(SaltType.NONE) +public abstract class UnsaltedMethod implements EncryptionMethod { + + public abstract String computeHash(String password); + + @Override + public HashedPassword computeHash(String password, String name) { + return new HashedPassword(computeHash(password)); + } + + @Override + public String computeHash(String password, String salt, String name) { + return computeHash(password); + } + + @Override + public boolean comparePassword(String password, HashedPassword hashedPassword, String name) { + return isEqual(hashedPassword.getHash(), computeHash(password)); + } + + @Override + public String generateSalt() { + return null; + } + + @Override + public boolean hasSeparateSalt() { + return false; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/UsernameSaltMethod.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/UsernameSaltMethod.java new file mode 100644 index 00000000..f5930fcf --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/UsernameSaltMethod.java @@ -0,0 +1,41 @@ +package fr.xephi.authme.security.crypts; + +import fr.xephi.authme.security.crypts.description.HasSalt; +import fr.xephi.authme.security.crypts.description.Recommendation; +import fr.xephi.authme.security.crypts.description.SaltType; +import fr.xephi.authme.security.crypts.description.Usage; + +import static fr.xephi.authme.security.HashUtils.isEqual; + +/** + * Common supertype of encryption methods that use a player's username + * (or something based on it) as embedded salt. + */ +@Recommendation(Usage.DO_NOT_USE) +@HasSalt(SaltType.USERNAME) +public abstract class UsernameSaltMethod implements EncryptionMethod { + + @Override + public abstract HashedPassword computeHash(String password, String name); + + @Override + public String computeHash(String password, String salt, String name) { + return computeHash(password, name).getHash(); + } + + @Override + public boolean comparePassword(String password, HashedPassword hashedPassword, String name) { + return isEqual(hashedPassword.getHash(), computeHash(password, name).getHash()); + } + + @Override + public String generateSalt() { + return null; + } + + @Override + public boolean hasSeparateSalt() { + return false; + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/Wbb3.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/Wbb3.java new file mode 100644 index 00000000..8e26463b --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/Wbb3.java @@ -0,0 +1,25 @@ +package fr.xephi.authme.security.crypts; + +import fr.xephi.authme.security.crypts.description.HasSalt; +import fr.xephi.authme.security.crypts.description.Recommendation; +import fr.xephi.authme.security.crypts.description.SaltType; +import fr.xephi.authme.security.crypts.description.Usage; +import fr.xephi.authme.util.RandomStringUtils; + +import static fr.xephi.authme.security.HashUtils.sha1; + +@Recommendation(Usage.ACCEPTABLE) +@HasSalt(value = SaltType.TEXT, length = 40) +public class Wbb3 extends SeparateSaltMethod { + + @Override + public String computeHash(String password, String salt, String name) { + return sha1(salt.concat(sha1(salt.concat(sha1(password))))); + } + + @Override + public String generateSalt() { + return RandomStringUtils.generateHex(40); + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/Wbb4.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/Wbb4.java new file mode 100644 index 00000000..a55a2d48 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/Wbb4.java @@ -0,0 +1,74 @@ +package fr.xephi.authme.security.crypts; + +import at.favre.lib.crypto.bcrypt.BCrypt; +import at.favre.lib.crypto.bcrypt.IllegalBCryptFormatException; +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.security.crypts.description.HasSalt; +import fr.xephi.authme.security.crypts.description.Recommendation; +import fr.xephi.authme.security.crypts.description.SaltType; +import fr.xephi.authme.security.crypts.description.Usage; + +import java.security.SecureRandom; + +import static fr.xephi.authme.security.HashUtils.isEqual; +import static fr.xephi.authme.security.crypts.BCryptHasher.BYTES_IN_SALT; +import static fr.xephi.authme.security.crypts.BCryptHasher.SALT_LENGTH_ENCODED; +import static java.nio.charset.StandardCharsets.UTF_8; + +@Recommendation(Usage.RECOMMENDED) +@HasSalt(value = SaltType.TEXT, length = SALT_LENGTH_ENCODED) +public class Wbb4 implements EncryptionMethod { + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(Wbb4.class); + private BCryptHasher bCryptHasher = new BCryptHasher(BCrypt.Version.VERSION_2A, 8); + private SecureRandom random = new SecureRandom(); + + @Override + public HashedPassword computeHash(String password, String name) { + byte[] salt = new byte[BYTES_IN_SALT]; + random.nextBytes(salt); + + String hash = hashInternal(password, salt); + return new HashedPassword(hash); + } + + @Override + public String computeHash(String password, String salt, String name) { + return hashInternal(password, salt.getBytes(UTF_8)); + } + + @Override + public boolean comparePassword(String password, HashedPassword hashedPassword, String name) { + try { + BCrypt.HashData hashData = BCrypt.Version.VERSION_2A.parser.parse(hashedPassword.getHash().getBytes(UTF_8)); + byte[] salt = hashData.rawSalt; + String computedHash = hashInternal(password, salt); + return isEqual(hashedPassword.getHash(), computedHash); + } catch (IllegalBCryptFormatException | IllegalArgumentException e) { + logger.logException("Invalid WBB4 hash:", e); + } + return false; + } + + /** + * Hashes the given password with the provided salt twice: hash(hash(password, salt), salt). + * + * @param password the password to hash + * @param rawSalt the salt to use + * @return WBB4-compatible hash + */ + private String hashInternal(String password, byte[] rawSalt) { + return bCryptHasher.hashWithRawSalt(bCryptHasher.hashWithRawSalt(password, rawSalt), rawSalt); + } + + @Override + public String generateSalt() { + return BCryptHasher.generateSalt(); + } + + @Override + public boolean hasSeparateSalt() { + return false; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/Whirlpool.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/Whirlpool.java new file mode 100644 index 00000000..1a450d56 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/Whirlpool.java @@ -0,0 +1,399 @@ +package fr.xephi.authme.security.crypts; + +/** + * The Whirlpool hashing function. + *

+ *

+ * References + *

+ *

+ * The Whirlpool algorithm was developed by Paulo S. L. M. Barreto and Vincent Rijmen. + *

+ * See P.S.L.M. Barreto, V. Rijmen, ``The Whirlpool hashing function,'' First + * NESSIE workshop, 2000 (tweaked version, 2003), + * + * + * @author Paulo S.L.M. Barreto + * @author Vincent Rijmen. + * @version 3.0 (2003.03.12) + *

+ * ==================================================================== + * ========= + *

+ * Differences from version 2.1: + *

+ * - Suboptimal diffusion matrix replaced by cir(1, 1, 4, 1, 8, 5, 2, + * 9). + *

+ * ==================================================================== + * ========= + *

+ * Differences from version 2.0: + *

+ * - Generation of ISO/IEC 10118-3 test vectors. - Bug fix: nonzero + * carry was ignored when tallying the data length (this bug apparently + * only manifested itself when feeding data in pieces rather than in a + * single chunk at once). + *

+ * Differences from version 1.0: + *

+ * - Original S-box replaced by the tweaked, hardware-efficient + * version. + *

+ * ==================================================================== + * ========= + *

+ * THIS SOFTWARE IS PROVIDED BY THE AUTHORS ''AS IS'' AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR + * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import fr.xephi.authme.security.crypts.description.Recommendation; +import fr.xephi.authme.security.crypts.description.Usage; + +import java.util.Arrays; + +@Deprecated +@Recommendation(Usage.DEPRECATED) +public class Whirlpool extends UnsaltedMethod { + + /** + * The message digest size (in bits) + */ + public static final int DIGESTBITS = 512; + + /** + * The message digest size (in bytes) + */ + public static final int DIGESTBYTES = DIGESTBITS >>> 3; + + /** + * The number of rounds of the internal dedicated block cipher. + */ + protected static final int R = 10; + + /** + * The substitution box. + */ + private static final String sbox = "\u1823\uc6E8\u87B8\u014F\u36A6\ud2F5\u796F\u9152" + "\u60Bc\u9B8E\uA30c\u7B35\u1dE0\ud7c2\u2E4B\uFE57" + "\u1577\u37E5\u9FF0\u4AdA\u58c9\u290A\uB1A0\u6B85" + "\uBd5d\u10F4\ucB3E\u0567\uE427\u418B\uA77d\u95d8" + "\uFBEE\u7c66\udd17\u479E\ucA2d\uBF07\uAd5A\u8333" + "\u6302\uAA71\uc819\u49d9\uF2E3\u5B88\u9A26\u32B0" + "\uE90F\ud580\uBEcd\u3448\uFF7A\u905F\u2068\u1AAE" + "\uB454\u9322\u64F1\u7312\u4008\uc3Ec\udBA1\u8d3d" + "\u9700\ucF2B\u7682\ud61B\uB5AF\u6A50\u45F3\u30EF" + "\u3F55\uA2EA\u65BA\u2Fc0\udE1c\uFd4d\u9275\u068A" + "\uB2E6\u0E1F\u62d4\uA896\uF9c5\u2559\u8472\u394c" + "\u5E78\u388c\ud1A5\uE261\uB321\u9c1E\u43c7\uFc04" + "\u5199\u6d0d\uFAdF\u7E24\u3BAB\ucE11\u8F4E\uB7EB" + "\u3c81\u94F7\uB913\u2cd3\uE76E\uc403\u5644\u7FA9" + "\u2ABB\uc153\udc0B\u9d6c\u3174\uF646\uAc89\u14E1" + "\u163A\u6909\u70B6\ud0Ed\ucc42\u98A4\u285c\uF886"; + + private static final long[][] C = new long[8][256]; + private static final long[] rc = new long[R + 1]; + + static { + for (int x = 0; x < 256; x++) { + char c = sbox.charAt(x / 2); + long v1 = ((x & 1) == 0) ? c >>> 8 : c & 0xff; + long v2 = v1 << 1; + if (v2 >= 0x100L) { + v2 ^= 0x11dL; + } + long v4 = v2 << 1; + if (v4 >= 0x100L) { + v4 ^= 0x11dL; + } + long v5 = v4 ^ v1; + long v8 = v4 << 1; + if (v8 >= 0x100L) { + v8 ^= 0x11dL; + } + long v9 = v8 ^ v1; + /* + * build the circulant table C[0][x] = S[x].[1, 1, 4, 1, 8, 5, 2, + * 9]: + */ + C[0][x] = (v1 << 56) | (v1 << 48) | (v4 << 40) | (v1 << 32) | (v8 << 24) | (v5 << 16) | (v2 << 8) | (v9); + /* + * build the remaining circulant tables C[t][x] = C[0][x] rotr t + */ + for (int t = 1; t < 8; t++) { + C[t][x] = (C[t - 1][x] >>> 8) | ((C[t - 1][x] << 56)); + } + } + /* + * build the round constants: + */ + rc[0] = 0L; /* + * not used (assigment kept only to properly initialize all + * variables) + */ + for (int r = 1; r <= R; r++) { + int i = 8 * (r - 1); + rc[r] = (C[0][i] & 0xff00000000000000L) ^ (C[1][i + 1] & 0x00ff000000000000L) ^ (C[2][i + 2] & 0x0000ff0000000000L) ^ (C[3][i + 3] & 0x000000ff00000000L) ^ (C[4][i + 4] & 0x00000000ff000000L) ^ (C[5][i + 5] & 0x0000000000ff0000L) ^ (C[6][i + 6] & 0x000000000000ff00L) ^ (C[7][i + 7] & 0x00000000000000ffL); + } + } + + /** + * Global number of hashed bits (256-bit counter). + */ + protected final byte[] bitLength = new byte[32]; + + /** + * Buffer of data to hash. + */ + protected final byte[] buffer = new byte[64]; + + /** + * Current number of bits on the buffer. + */ + protected int bufferBits = 0; + + /** + * Current (possibly incomplete) byte slot on the buffer. + */ + protected int bufferPos = 0; + + /** + * The hashing state. + */ + protected final long[] hash = new long[8]; + protected final long[] K = new long[8]; + protected final long[] L = new long[8]; + protected final long[] block = new long[8]; + protected final long[] state = new long[8]; + + public Whirlpool() { + } + + protected static String display(byte[] array) { + char[] val = new char[2 * array.length]; + String hex = "0123456789ABCDEF"; + for (int i = 0; i < array.length; i++) { + int b = array[i] & 0xff; + val[2 * i] = hex.charAt(b >>> 4); + val[2 * i + 1] = hex.charAt(b & 15); + } + return String.valueOf(val); + } + + /** + * The core Whirlpool transform. + */ + protected void processBuffer() { + /* + * map the buffer to a block: + */ + for (int i = 0, j = 0; i < 8; i++, j += 8) { + block[i] = (((long) buffer[j]) << 56) ^ (((long) buffer[j + 1] & 0xffL) << 48) ^ (((long) buffer[j + 2] & 0xffL) << 40) ^ (((long) buffer[j + 3] & 0xffL) << 32) ^ (((long) buffer[j + 4] & 0xffL) << 24) ^ (((long) buffer[j + 5] & 0xffL) << 16) ^ (((long) buffer[j + 6] & 0xffL) << 8) ^ (((long) buffer[j + 7] & 0xffL)); + } + /* + * compute and apply K^0 to the cipher state: + */ + for (int i = 0; i < 8; i++) { + state[i] = block[i] ^ (K[i] = hash[i]); + } + /* + * iterate over all rounds: + */ + for (int r = 1; r <= R; r++) { + /* + * compute K^r from K^{r-1}: + */ + for (int i = 0; i < 8; i++) { + L[i] = 0L; + for (int t = 0, s = 56; t < 8; t++, s -= 8) { + L[i] ^= C[t][(int) (K[(i - t) & 7] >>> s) & 0xff]; + } + } + for (int i = 0; i < 8; i++) { + K[i] = L[i]; + } + K[0] ^= rc[r]; + /* + * apply the r-th round transformation: + */ + for (int i = 0; i < 8; i++) { + L[i] = K[i]; + for (int t = 0, s = 56; t < 8; t++, s -= 8) { + L[i] ^= C[t][(int) (state[(i - t) & 7] >>> s) & 0xff]; + } + } + for (int i = 0; i < 8; i++) { + state[i] = L[i]; + } + } + /* + * apply the Miyaguchi-Preneel compression function: + */ + for (int i = 0; i < 8; i++) { + hash[i] ^= state[i] ^ block[i]; + } + } + + /** + * Initialize the hashing state. + */ + public void NESSIEinit() { + Arrays.fill(bitLength, (byte) 0); + bufferBits = bufferPos = 0; + buffer[0] = 0; + Arrays.fill(hash, 0L); + } + + /** + * Delivers input data to the hashing algorithm. + * + * @param source plaintext data to hash. + * @param sourceBits how many bits of plaintext to process. + *

+ * This method maintains the invariant: bufferBits < 512 + *

+ */ + public void NESSIEadd(byte[] source, long sourceBits) { + /* + * sourcePos | +-------+-------+------- ||||||||||||||||||||| source + * +-------+-------+------- + * +-------+-------+-------+-------+-------+------- + * |||||||||||||||||||||| buffer + * +-------+-------+-------+-------+-------+------- | bufferPos + */ + int sourcePos = 0; // index of leftmost source byte containing data (1 + // to 8 bits). + int sourceGap = (8 - ((int) sourceBits & 7)) & 7; // space on + // source[sourcePos]. + int bufferRem = bufferBits & 7; // occupied bits on buffer[bufferPos]. + int b; + // tally the length of the added data: + long value = sourceBits; + for (int i = 31, carry = 0; i >= 0; i--) { + carry += (bitLength[i] & 0xff) + ((int) value & 0xff); + bitLength[i] = (byte) carry; + carry >>>= 8; + value >>>= 8; + } + // process data in chunks of 8 bits: + while (sourceBits > 8) { // at least source[sourcePos] and + // source[sourcePos+1] contain data. + // take a byte from the source: + b = ((source[sourcePos] << sourceGap) & 0xff) | ((source[sourcePos + 1] & 0xff) >>> (8 - sourceGap)); + if (b < 0 || b >= 256) { + throw new RuntimeException("LOGIC ERROR"); + } + // process this byte: + buffer[bufferPos++] |= b >>> bufferRem; + bufferBits += 8 - bufferRem; // bufferBits = 8*bufferPos; + if (bufferBits == 512) { + // process data block: + processBuffer(); + // reset buffer: + bufferBits = bufferPos = 0; + } + buffer[bufferPos] = (byte) ((b << (8 - bufferRem)) & 0xff); + bufferBits += bufferRem; + // proceed to remaining data: + sourceBits -= 8; + sourcePos++; + } + // now 0 <= sourceBits <= 8; + // furthermore, all data (if any is left) is in source[sourcePos]. + if (sourceBits > 0) { + b = (source[sourcePos] << sourceGap) & 0xff; // bits are + // left-justified on b. + // process the remaining bits: + buffer[bufferPos] |= b >>> bufferRem; + } else { + b = 0; + } + if (bufferRem + sourceBits < 8) { + // all remaining data fits on buffer[bufferPos], and there still + // remains some space. + bufferBits += (int) sourceBits; + } else { + // buffer[bufferPos] is full: + bufferPos++; + bufferBits += 8 - bufferRem; // bufferBits = 8*bufferPos; + sourceBits -= 8 - bufferRem; + // now 0 <= sourceBits < 8; furthermore, all data is in + // source[sourcePos]. + if (bufferBits == 512) { + // process data block: + processBuffer(); + // reset buffer: + bufferBits = bufferPos = 0; + } + buffer[bufferPos] = (byte) ((b << (8 - bufferRem)) & 0xff); + bufferBits += (int) sourceBits; + } + } + + /** + *

+ * Get the hash value from the hashing state. + *

+ *

+ * This method uses the invariant: bufferBits < 512 + *

+ * @param digest byte[] + */ + public void NESSIEfinalize(byte[] digest) { + // append a '1'-bit: + buffer[bufferPos] |= 0x80 >>> (bufferBits & 7); + bufferPos++; // all remaining bits on the current byte are set to zero. + // pad with zero bits to complete 512N + 256 bits: + if (bufferPos > 32) { + while (bufferPos < 64) { + buffer[bufferPos++] = 0; + } + // process data block: + processBuffer(); + // reset buffer: + bufferPos = 0; + } + while (bufferPos < 32) { + buffer[bufferPos++] = 0; + } + // append bit length of hashed data: + System.arraycopy(bitLength, 0, buffer, 32, 32); + // process data block: + processBuffer(); + // return the completed message digest: + for (int i = 0, j = 0; i < 8; i++, j += 8) { + long h = hash[i]; + digest[j] = (byte) (h >>> 56); + digest[j + 1] = (byte) (h >>> 48); + digest[j + 2] = (byte) (h >>> 40); + digest[j + 3] = (byte) (h >>> 32); + digest[j + 4] = (byte) (h >>> 24); + digest[j + 5] = (byte) (h >>> 16); + digest[j + 6] = (byte) (h >>> 8); + digest[j + 7] = (byte) (h); + } + } + + /** + * Delivers string input data to the hashing algorithm. + * + * @param source plaintext data to hash (ASCII text string). + * This method maintains the invariant: bufferBits < 512 + */ + public void NESSIEadd(String source) { + if (source.length() > 0) { + byte[] data = new byte[source.length()]; + for (int i = 0; i < source.length(); i++) { + data[i] = (byte) source.charAt(i); + } + NESSIEadd(data, 8 * data.length); + } + } + + @Override + public String computeHash(String password) { + byte[] digest = new byte[DIGESTBYTES]; + NESSIEinit(); + NESSIEadd(password); + NESSIEfinalize(digest); + return display(digest); + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/Wordpress.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/Wordpress.java new file mode 100644 index 00000000..f70c0949 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/Wordpress.java @@ -0,0 +1,123 @@ +package fr.xephi.authme.security.crypts; + +import fr.xephi.authme.security.HashUtils; +import fr.xephi.authme.security.MessageDigestAlgorithm; +import fr.xephi.authme.security.crypts.description.HasSalt; +import fr.xephi.authme.security.crypts.description.Recommendation; +import fr.xephi.authme.security.crypts.description.SaltType; +import fr.xephi.authme.security.crypts.description.Usage; + +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.security.SecureRandom; +import java.util.Arrays; + +import static fr.xephi.authme.security.HashUtils.isEqual; + +@Recommendation(Usage.ACCEPTABLE) +@HasSalt(value = SaltType.TEXT, length = 9) +// Note ljacqu 20151228: Wordpress is actually a salted algorithm but salt generation is handled internally +// and isn't exposed to the outside, so we treat it as an unsalted implementation +public class Wordpress extends UnsaltedMethod { + + private static final String itoa64 = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + private final SecureRandom randomGen = new SecureRandom(); + + private String encode64(byte[] src, int count) { + int i, value; + StringBuilder output = new StringBuilder(); + i = 0; + + if (src.length < count) { + byte[] t = new byte[count]; + System.arraycopy(src, 0, t, 0, src.length); + Arrays.fill(t, src.length, count - 1, (byte) 0); + src = t; + } + + do { + value = src[i] + (src[i] < 0 ? 256 : 0); + ++i; + output.append(itoa64.charAt(value & 63)); + if (i < count) { + value |= (src[i] + (src[i] < 0 ? 256 : 0)) << 8; + } + output.append(itoa64.charAt((value >> 6) & 63)); + if (i++ >= count) { + break; + } + if (i < count) { + value |= (src[i] + (src[i] < 0 ? 256 : 0)) << 16; + } + output.append(itoa64.charAt((value >> 12) & 63)); + if (i++ >= count) { + break; + } + output.append(itoa64.charAt((value >> 18) & 63)); + } while (i < count); + return output.toString(); + } + + private String crypt(String password, String setting) { + String output = "*0"; + if (((setting.length() < 2) ? setting : setting.substring(0, 2)).equalsIgnoreCase(output)) { + output = "*1"; + } + String id = (setting.length() < 3) ? setting : setting.substring(0, 3); + if (!(id.equals("$P$") || id.equals("$H$"))) { + return output; + } + int countLog2 = itoa64.indexOf(setting.charAt(3)); + if (countLog2 < 7 || countLog2 > 30) { + return output; + } + int count = 1 << countLog2; + String salt = setting.substring(4, 4 + 8); + if (salt.length() != 8) { + return output; + } + MessageDigest md = HashUtils.getDigest(MessageDigestAlgorithm.MD5); + byte[] pass = stringToUtf8(password); + byte[] hash = md.digest(stringToUtf8(salt + password)); + do { + byte[] t = new byte[hash.length + pass.length]; + System.arraycopy(hash, 0, t, 0, hash.length); + System.arraycopy(pass, 0, t, hash.length, pass.length); + hash = md.digest(t); + } while (--count > 0); + output = setting.substring(0, 12); + output += encode64(hash, 16); + return output; + } + + private String gensaltPrivate(byte[] input) { + String output = "$P$"; + int iterationCountLog2 = 8; + output += itoa64.charAt(Math.min(iterationCountLog2 + 5, 30)); + output += encode64(input, 6); + return output; + } + + private byte[] stringToUtf8(String string) { + try { + return string.getBytes("UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new UnsupportedOperationException("This system doesn't support UTF-8!", e); + } + } + + @Override + public String computeHash(String password) { + byte random[] = new byte[6]; + randomGen.nextBytes(random); + return crypt(password, gensaltPrivate(stringToUtf8(new String(random)))); + } + + @Override + public boolean comparePassword(String password, HashedPassword hashedPassword, String name) { + String hash = hashedPassword.getHash(); + String comparedHash = crypt(password, hash); + return isEqual(hash, comparedHash); + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/XAuth.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/XAuth.java new file mode 100644 index 00000000..3cd4ceab --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/XAuth.java @@ -0,0 +1,45 @@ +package fr.xephi.authme.security.crypts; + +import fr.xephi.authme.security.crypts.description.Recommendation; +import fr.xephi.authme.security.crypts.description.Usage; + +import java.util.Locale; + +import static fr.xephi.authme.security.HashUtils.isEqual; + +@Recommendation(Usage.RECOMMENDED) +public class XAuth extends HexSaltedMethod { + + private static String getWhirlpool(String message) { + Whirlpool w = new Whirlpool(); + byte[] digest = new byte[Whirlpool.DIGESTBYTES]; + w.NESSIEinit(); + w.NESSIEadd(message); + w.NESSIEfinalize(digest); + return Whirlpool.display(digest); + } + + @Override + public String computeHash(String password, String salt, String name) { + String hash = getWhirlpool(salt + password).toLowerCase(Locale.ROOT); + int saltPos = password.length() >= hash.length() ? hash.length() - 1 : password.length(); + return hash.substring(0, saltPos) + salt + hash.substring(saltPos); + } + + @Override + public boolean comparePassword(String password, HashedPassword hashedPassword, String name) { + String hash = hashedPassword.getHash(); + int saltPos = password.length() >= hash.length() ? hash.length() - 1 : password.length(); + if (saltPos + 12 > hash.length()) { + return false; + } + String salt = hash.substring(saltPos, saltPos + 12); + return isEqual(hash, computeHash(password, salt, name)); + } + + @Override + public int getSaltLength() { + return 12; + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/XfBCrypt.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/XfBCrypt.java new file mode 100644 index 00000000..749b5f57 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/XfBCrypt.java @@ -0,0 +1,35 @@ +package fr.xephi.authme.security.crypts; + +import at.favre.lib.crypto.bcrypt.BCrypt; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class XfBCrypt extends BCryptBasedHash { + + public static final String SCHEME_CLASS = "XenForo_Authentication_Core12"; + private static final Pattern HASH_PATTERN = Pattern.compile("\"hash\";s.*\"(.*)?\""); + + XfBCrypt() { + super(new BCryptHasher(BCrypt.Version.VERSION_2A, 10)); + } + + /** + * Extracts the password hash from the given BLOB. + * + * @param blob the blob to process + * @return the extracted hash + */ + public static String getHashFromBlob(byte[] blob) { + String line = new String(blob); + Matcher m = HASH_PATTERN.matcher(line); + if (m.find()) { + return m.group(1); + } + return "*"; // what? + } + + public static String serializeHash(String hash) { + return "a:1:{s:4:\"hash\";s:" + hash.length() + ":\""+hash+"\";}"; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/description/AsciiRestricted.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/description/AsciiRestricted.java new file mode 100644 index 00000000..bf179fff --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/description/AsciiRestricted.java @@ -0,0 +1,15 @@ +package fr.xephi.authme.security.crypts.description; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Denotes a hashing algorithm that is restricted to the ASCII charset. + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface AsciiRestricted { + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/description/HasSalt.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/description/HasSalt.java new file mode 100644 index 00000000..0723a4dd --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/description/HasSalt.java @@ -0,0 +1,30 @@ +package fr.xephi.authme.security.crypts.description; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Describes the type of salt the encryption algorithm uses. This is purely for documentation + * purposes and is ignored by the code. + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface HasSalt { + + /** + * The type of the salt. + * + * @return The salt type + */ + SaltType value(); + + /** + * For text salts, the length of the salt. + * + * @return The length of the salt the algorithm uses, or 0 if not defined or not applicable. + */ + int length() default 0; + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/description/Recommendation.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/description/Recommendation.java new file mode 100644 index 00000000..4b832a68 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/description/Recommendation.java @@ -0,0 +1,24 @@ +package fr.xephi.authme.security.crypts.description; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to mark a hash algorithm with the usage recommendation. + * + * @see Usage + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface Recommendation { + + /** + * The recommendation for using the hash algorithm. + * + * @return The recommended usage + */ + Usage value(); + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/description/SaltType.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/description/SaltType.java new file mode 100644 index 00000000..40b923fa --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/description/SaltType.java @@ -0,0 +1,17 @@ +package fr.xephi.authme.security.crypts.description; + +/** + * The type of salt used by an encryption algorithm. + */ +public enum SaltType { + + /** Random, newly generated text. */ + TEXT, + + /** Salt is based on the username, including variations and repetitions thereof. */ + USERNAME, + + /** No salt. */ + NONE + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/description/Usage.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/description/Usage.java new file mode 100644 index 00000000..9ccc52c4 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/crypts/description/Usage.java @@ -0,0 +1,29 @@ +package fr.xephi.authme.security.crypts.description; + +/** + * Usage recommendation that can be provided for a hash algorithm. + *

+ * Use the following rules of thumb: + *

    + *
  • Hashes using MD5 may be {@link #ACCEPTABLE} but never {@link #RECOMMENDED}.
  • + *
  • Hashes using no salt or one based on the username should be {@link #DO_NOT_USE}.
  • + *
+ */ +public enum Usage { + + /** The hash algorithm appears to be cryptographically secure and is one of the algorithms recommended by AuthMe. */ + RECOMMENDED, + + /** There are safer algorithms that can be chosen but using the algorithm is generally OK. */ + ACCEPTABLE, + + /** Hash algorithm is not recommended to be used. Use only if required by another system. */ + DO_NOT_USE, + + /** Algorithm that is or will be no longer supported actively. */ + DEPRECATED, + + /** The algorithm does not work properly; do not use. */ + DOES_NOT_WORK + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/totp/GenerateTotpService.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/totp/GenerateTotpService.java new file mode 100644 index 00000000..8c689dde --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/totp/GenerateTotpService.java @@ -0,0 +1,70 @@ +package fr.xephi.authme.security.totp; + +import fr.xephi.authme.initialization.HasCleanup; +import fr.xephi.authme.security.totp.TotpAuthenticator.TotpGenerationResult; +import fr.xephi.authme.util.expiring.ExpiringMap; +import org.bukkit.entity.Player; + +import javax.inject.Inject; +import java.util.Locale; +import java.util.concurrent.TimeUnit; + +/** + * Handles the generation of new TOTP secrets for players. + */ +public class GenerateTotpService implements HasCleanup { + + private static final int NEW_TOTP_KEY_EXPIRATION_MINUTES = 5; + + private final ExpiringMap totpKeys; + + @Inject + private TotpAuthenticator totpAuthenticator; + + GenerateTotpService() { + this.totpKeys = new ExpiringMap<>(NEW_TOTP_KEY_EXPIRATION_MINUTES, TimeUnit.MINUTES); + } + + /** + * Generates a new TOTP key and returns the corresponding QR code. + * + * @param player the player to save the TOTP key for + * @return TOTP generation result + */ + public TotpGenerationResult generateTotpKey(Player player) { + TotpGenerationResult credentials = totpAuthenticator.generateTotpKey(player); + totpKeys.put(player.getName().toLowerCase(Locale.ROOT), credentials); + return credentials; + } + + /** + * Returns the generated TOTP secret of a player, if available and not yet expired. + * + * @param player the player to retrieve the TOTP key for + * @return TOTP generation result + */ + public TotpGenerationResult getGeneratedTotpKey(Player player) { + return totpKeys.get(player.getName().toLowerCase(Locale.ROOT)); + } + + public void removeGenerateTotpKey(Player player) { + totpKeys.remove(player.getName().toLowerCase(Locale.ROOT)); + } + + /** + * Returns whether the given totp code is correct for the generated TOTP key of the player. + * + * @param player the player to verify the code for + * @param totpCode the totp code to verify with the generated secret + * @return true if the input code is correct, false if the code is invalid or no unexpired totp key is available + */ + public boolean isTotpCodeCorrectForGeneratedTotpKey(Player player, String totpCode) { + TotpGenerationResult totpDetails = totpKeys.get(player.getName().toLowerCase(Locale.ROOT)); + return totpDetails != null && totpAuthenticator.checkCode(player.getName(), totpDetails.getTotpKey(), totpCode); + } + + @Override + public void performCleanup() { + totpKeys.removeExpiredEntries(); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/totp/TotpAuthenticator.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/totp/TotpAuthenticator.java new file mode 100644 index 00000000..2c787306 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/security/totp/TotpAuthenticator.java @@ -0,0 +1,98 @@ +package fr.xephi.authme.security.totp; + +import com.google.common.collect.HashBasedTable; +import com.google.common.collect.Table; +import com.google.common.primitives.Ints; +import com.warrenstrange.googleauth.GoogleAuthenticator; +import com.warrenstrange.googleauth.GoogleAuthenticatorKey; +import com.warrenstrange.googleauth.GoogleAuthenticatorQRGenerator; +import com.warrenstrange.googleauth.IGoogleAuthenticator; +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.initialization.HasCleanup; +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.Locale; + +import static fr.xephi.authme.util.Utils.MILLIS_PER_MINUTE; + +/** + * Provides TOTP functions (wrapping a third-party TOTP implementation). + */ +public class TotpAuthenticator implements HasCleanup { + + private static final int CODE_RETENTION_MINUTES = 5; + + private final IGoogleAuthenticator authenticator; + private final Settings settings; + private final Table usedCodes = HashBasedTable.create(); + + @Inject + TotpAuthenticator(Settings settings) { + this.authenticator = createGoogleAuthenticator(); + this.settings = settings; + } + + /** + * @return new Google Authenticator instance + */ + protected IGoogleAuthenticator createGoogleAuthenticator() { + return new GoogleAuthenticator(); + } + + public boolean checkCode(PlayerAuth auth, String totpCode) { + return checkCode(auth.getNickname(), auth.getTotpKey(), totpCode); + } + + /** + * Returns whether the given input code matches for the provided TOTP key. + * + * @param playerName the player name + * @param totpKey the key to check with + * @param inputCode the input code to verify + * @return true if code is valid, false otherwise + */ + public boolean checkCode(String playerName, String totpKey, String inputCode) { + String nameLower = playerName.toLowerCase(Locale.ROOT); + Integer totpCode = Ints.tryParse(inputCode); + if (totpCode != null && !usedCodes.contains(nameLower, totpCode) + && authenticator.authorize(totpKey, totpCode)) { + usedCodes.put(nameLower, totpCode, System.currentTimeMillis()); + return true; + } + return false; + } + + public TotpGenerationResult generateTotpKey(Player player) { + GoogleAuthenticatorKey credentials = authenticator.createCredentials(); + String qrCodeUrl = GoogleAuthenticatorQRGenerator.getOtpAuthURL( + settings.getProperty(PluginSettings.SERVER_NAME), player.getName(), credentials); + return new TotpGenerationResult(credentials.getKey(), qrCodeUrl); + } + + @Override + public void performCleanup() { + long threshold = System.currentTimeMillis() - CODE_RETENTION_MINUTES * MILLIS_PER_MINUTE; + usedCodes.values().removeIf(value -> value < threshold); + } + + public static final class TotpGenerationResult { + private final String totpKey; + private final String authenticatorQrCodeUrl; + + public TotpGenerationResult(String totpKey, String authenticatorQrCodeUrl) { + this.totpKey = totpKey; + this.authenticatorQrCodeUrl = authenticatorQrCodeUrl; + } + + public String getTotpKey() { + return totpKey; + } + + public String getAuthenticatorQrCodeUrl() { + return authenticatorQrCodeUrl; + } + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/service/AntiBotService.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/service/AntiBotService.java new file mode 100644 index 00000000..f902a28f --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/service/AntiBotService.java @@ -0,0 +1,199 @@ +package fr.xephi.authme.service; + +import com.github.Anon8281.universalScheduler.scheduling.tasks.MyScheduledTask; +import fr.xephi.authme.initialization.SettingsDependent; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.message.Messages; +import fr.xephi.authme.permission.AdminPermission; +import fr.xephi.authme.permission.PermissionsManager; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.properties.ProtectionSettings; +import fr.xephi.authme.util.AtomicIntervalCounter; + +import javax.inject.Inject; +import java.util.Locale; +import java.util.concurrent.CopyOnWriteArrayList; + +import static fr.xephi.authme.service.BukkitService.TICKS_PER_MINUTE; +import static fr.xephi.authme.service.BukkitService.TICKS_PER_SECOND; + +/** + * The AntiBot Service Management class. + */ +public class AntiBotService implements SettingsDependent { + + // Instances + private final Messages messages; + private final PermissionsManager permissionsManager; + private final BukkitService bukkitService; + private final CopyOnWriteArrayList antibotKicked = new CopyOnWriteArrayList<>(); + // Settings + private int duration; + // Service status + private AntiBotStatus antiBotStatus; + private boolean startup; + private MyScheduledTask disableTask; + private AtomicIntervalCounter flaggedCounter; + + @Inject + AntiBotService(Settings settings, Messages messages, PermissionsManager permissionsManager, + BukkitService bukkitService) { + // Instances + this.messages = messages; + this.permissionsManager = permissionsManager; + this.bukkitService = bukkitService; + // Initial status + disableTask = null; + antiBotStatus = AntiBotStatus.DISABLED; + startup = true; + // Load settings and start if required + reload(settings); + } + + @Override + public void reload(Settings settings) { + // Load settings + duration = settings.getProperty(ProtectionSettings.ANTIBOT_DURATION); + int sensibility = settings.getProperty(ProtectionSettings.ANTIBOT_SENSIBILITY); + int interval = settings.getProperty(ProtectionSettings.ANTIBOT_INTERVAL); + flaggedCounter = new AtomicIntervalCounter(sensibility, interval * 1000); + + // Stop existing protection + stopProtection(); + antiBotStatus = AntiBotStatus.DISABLED; + + // If antibot is disabled, just stop + if (!settings.getProperty(ProtectionSettings.ENABLE_ANTIBOT)) { + return; + } + + // Bot activation task + Runnable enableTask = () -> antiBotStatus = AntiBotStatus.LISTENING; + + // Delay the schedule on first start + if (startup) { + int delay = settings.getProperty(ProtectionSettings.ANTIBOT_DELAY); + bukkitService.scheduleSyncDelayedTask(enableTask, (long) delay * TICKS_PER_SECOND); + startup = false; + } else { + enableTask.run(); + } + } + + /** + * Transitions the anti bot service to an active status. + */ + private void startProtection() { + if (antiBotStatus == AntiBotStatus.ACTIVE) { + return; // Already activating/active + } + if (disableTask != null) { + disableTask.cancel(); + } + // Schedule auto-disable + disableTask = bukkitService.runTaskLater(this::stopProtection, (long) duration * TICKS_PER_MINUTE); + antiBotStatus = AntiBotStatus.ACTIVE; + bukkitService.scheduleSyncTaskFromOptionallyAsyncTask(() -> { + // Inform admins + bukkitService.getOnlinePlayers().stream() + .filter(player -> permissionsManager.hasPermission(player, AdminPermission.ANTIBOT_MESSAGES)) + .forEach(player -> messages.send(player, MessageKey.ANTIBOT_AUTO_ENABLED_MESSAGE)); + }); + } + + /** + * Transitions the anti bot service from active status back to listening. + */ + private void stopProtection() { + if (antiBotStatus != AntiBotStatus.ACTIVE) { + return; + } + + // Change status + antiBotStatus = AntiBotStatus.LISTENING; + flaggedCounter.reset(); + antibotKicked.clear(); + + // Cancel auto-disable task + disableTask.cancel(); + disableTask = null; + + // Inform admins + String durationString = Integer.toString(duration); + bukkitService.getOnlinePlayers().stream() + .filter(player -> permissionsManager.hasPermission(player, AdminPermission.ANTIBOT_MESSAGES)) + .forEach(player -> messages.send(player, MessageKey.ANTIBOT_AUTO_DISABLED_MESSAGE, durationString)); + } + + /** + * Returns the status of the AntiBot service. + * + * @return status of the antibot service + */ + public AntiBotStatus getAntiBotStatus() { + return antiBotStatus; + } + + /** + * Allows to override the status of the protection. + * + * @param started the new protection status + */ + public void overrideAntiBotStatus(boolean started) { + if (antiBotStatus != AntiBotStatus.DISABLED) { + if (started) { + startProtection(); + } else { + stopProtection(); + } + } + } + + /** + * Returns if a player should be kicked due to antibot service. + * + * @return if the player should be kicked + */ + public boolean shouldKick() { + if (antiBotStatus == AntiBotStatus.DISABLED) { + return false; + } else if (antiBotStatus == AntiBotStatus.ACTIVE) { + return true; + } + + if (flaggedCounter.handle()) { + startProtection(); + return true; + } + return false; + } + + /** + * Returns whether the player was kicked because of activated antibot. The list is reset + * when antibot is deactivated. + * + * @param name the name to check + * + * @return true if the given name has been kicked because of Antibot + */ + public boolean wasPlayerKicked(String name) { + return antibotKicked.contains(name.toLowerCase(Locale.ROOT)); + } + + /** + * Adds a name to the list of players kicked by antibot. Should only be used when a player + * is determined to be kicked because of failed antibot verification. + * + * @param name the name to add + */ + public void addPlayerKick(String name) { + antibotKicked.addIfAbsent(name.toLowerCase(Locale.ROOT)); + } + + public enum AntiBotStatus { + LISTENING, + DISABLED, + ACTIVE + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/service/BackupService.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/service/BackupService.java new file mode 100644 index 00000000..3e570c3d --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/service/BackupService.java @@ -0,0 +1,219 @@ +package fr.xephi.authme.service; + +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.datasource.DataSourceType; +import fr.xephi.authme.initialization.DataFolder; +import fr.xephi.authme.mail.EmailService; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.properties.BackupSettings; +import fr.xephi.authme.settings.properties.DatabaseSettings; +import fr.xephi.authme.util.FileUtils; +import org.bukkit.command.CommandSender; + +import javax.inject.Inject; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Locale; + +import static fr.xephi.authme.util.Utils.logAndSendMessage; +import static fr.xephi.authme.util.Utils.logAndSendWarning; + +/** + * Performs a backup of the data source. + */ +public class BackupService { + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(EmailService.class); + + private final File dataFolder; + private final File backupFolder; + private final Settings settings; + + + /** + * Constructor. + * + * @param dataFolder the data folder + * @param settings the plugin settings + */ + @Inject + public BackupService(@DataFolder File dataFolder, Settings settings) { + this.dataFolder = dataFolder; + this.backupFolder = new File(dataFolder, "backups"); + this.settings = settings; + } + + /** + * Performs a backup for the given reason. + * + * @param cause backup reason + */ + public void doBackup(BackupCause cause) { + doBackup(cause, null); + } + + /** + * Performs a backup for the given reason. + * + * @param cause backup reason + * @param sender the command sender (nullable) + */ + public void doBackup(BackupCause cause, CommandSender sender) { + if (!settings.getProperty(BackupSettings.ENABLED)) { + // Print a warning if the backup was requested via command or by another plugin + if (cause == BackupCause.COMMAND || cause == BackupCause.OTHER) { + logAndSendWarning(sender, + "Can't perform a backup: disabled in configuration. Cause of the backup: " + cause.name()); + } + return; + } else if (BackupCause.START == cause && !settings.getProperty(BackupSettings.ON_SERVER_START) + || BackupCause.STOP == cause && !settings.getProperty(BackupSettings.ON_SERVER_STOP)) { + // Don't perform backup on start or stop if so configured + return; + } + + // Do backup and check return value! + if (doBackup()) { + logAndSendMessage(sender, + "A backup has been performed successfully. Cause of the backup: " + cause.name()); + } else { + logAndSendWarning(sender, "Error while performing a backup! Cause of the backup: " + cause.name()); + } + } + + private boolean doBackup() { + DataSourceType dataSourceType = settings.getProperty(DatabaseSettings.BACKEND); + switch (dataSourceType) { + case MYSQL: + return performMySqlBackup(); + case SQLITE: + String dbName = settings.getProperty(DatabaseSettings.MYSQL_DATABASE); + return performFileBackup(dbName + ".db"); + case H2: + String h2dbName = settings.getProperty(DatabaseSettings.MYSQL_DATABASE); + return performFileBackup(h2dbName + ".mv.db"); + default: + logger.warning("Unknown data source type '" + dataSourceType + "' for backup"); + } + + return false; + } + + /** + * Performs a backup for the MySQL data source. + * + * @return true if successful, false otherwise + */ + private boolean performMySqlBackup() { + FileUtils.createDirectory(backupFolder); + File sqlBackupFile = constructBackupFile("sql"); + + String backupWindowsPath = settings.getProperty(BackupSettings.MYSQL_WINDOWS_PATH); + boolean isUsingWindows = useWindowsCommand(backupWindowsPath); + String backupCommand = isUsingWindows + ? backupWindowsPath + "\\bin\\mysqldump.exe" + buildMysqlDumpArguments(sqlBackupFile) + : "mysqldump" + buildMysqlDumpArguments(sqlBackupFile); + + try { + Process runtimeProcess = Runtime.getRuntime().exec(backupCommand); + int processComplete = runtimeProcess.waitFor(); + if (processComplete == 0) { + logger.info("Backup created successfully. (Using Windows = " + isUsingWindows + ")"); + return true; + } else { + logger.warning("Could not create the backup! (Using Windows = " + isUsingWindows + ")"); + } + } catch (IOException | InterruptedException e) { + logger.logException("Error during backup (using Windows = " + isUsingWindows + "):", e); + } + return false; + } + + private boolean performFileBackup(String filename) { + FileUtils.createDirectory(backupFolder); + File backupFile = constructBackupFile("db"); + + try { + copy(new File(dataFolder, filename), backupFile); + return true; + } catch (IOException ex) { + logger.logException("Encountered an error during file backup:", ex); + } + return false; + } + + /** + * Check if we are under Windows and correct location of mysqldump.exe + * otherwise return error. + * + * @param windowsPath The path to check + * @return True if the path is correct, false if it is incorrect or the OS is not Windows + */ + private boolean useWindowsCommand(String windowsPath) { + String isWin = System.getProperty("os.name").toLowerCase(Locale.ROOT); + if (isWin.contains("win")) { + if (new File(windowsPath + "\\bin\\mysqldump.exe").exists()) { + return true; + } else { + logger.warning("Mysql Windows Path is incorrect. Please check it"); + return false; + } + } + return false; + } + + /** + * Builds the command line arguments to pass along when running the {@code mysqldump} command. + * + * @param sqlBackupFile the file to back up to + * @return the mysqldump command line arguments + */ + private String buildMysqlDumpArguments(File sqlBackupFile) { + String dbUsername = settings.getProperty(DatabaseSettings.MYSQL_USERNAME); + String dbPassword = settings.getProperty(DatabaseSettings.MYSQL_PASSWORD); + String dbName = settings.getProperty(DatabaseSettings.MYSQL_DATABASE); + String tableName = settings.getProperty(DatabaseSettings.MYSQL_TABLE); + + return " -u " + dbUsername + " -p" + dbPassword + " " + dbName + + " --tables " + tableName + " -r " + sqlBackupFile.getPath() + ".sql"; + } + + /** + * Constructs the file name to back up the data source to. + * + * @param fileExtension the file extension to use (e.g. sql) + * @return the file to back up the data to + */ + private File constructBackupFile(String fileExtension) { + String dateString = FileUtils.createCurrentTimeString(); + return new File(backupFolder, "backup" + dateString + "." + fileExtension); + } + + private static void copy(File src, File dst) throws IOException { + try (InputStream in = new FileInputStream(src); + OutputStream out = new FileOutputStream(dst)) { + // Transfer bytes from in to out + byte[] buf = new byte[1024]; + int len; + while ((len = in.read(buf)) > 0) { + out.write(buf, 0, len); + } + } + } + + /** + * Possible backup causes. + */ + public enum BackupCause { + START, + STOP, + COMMAND, + OTHER + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/service/BukkitService.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/service/BukkitService.java new file mode 100644 index 00000000..8a899795 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/service/BukkitService.java @@ -0,0 +1,440 @@ +package fr.xephi.authme.service; + +import com.github.Anon8281.universalScheduler.UniversalRunnable; +import com.github.Anon8281.universalScheduler.UniversalScheduler; +import com.github.Anon8281.universalScheduler.scheduling.tasks.MyScheduledTask; +import fr.xephi.authme.AuthMe; +import fr.xephi.authme.initialization.SettingsDependent; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.properties.PluginSettings; +import org.bukkit.BanEntry; +import org.bukkit.BanList; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.OfflinePlayer; +import org.bukkit.World; +import org.bukkit.command.CommandSender; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import org.bukkit.event.Event; +import org.bukkit.potion.PotionEffect; +import org.bukkit.potion.PotionEffectType; + +import javax.inject.Inject; +import java.util.Collection; +import java.util.Date; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; + +import static fr.xephi.authme.AuthMe.getScheduler; + +/** + * Service for operations requiring the Bukkit API, such as for scheduling. + */ +public class BukkitService implements SettingsDependent { + + /** Number of ticks per second in the Bukkit main thread. */ + public static final int TICKS_PER_SECOND = 20; + /** Number of ticks per minute. */ + public static final int TICKS_PER_MINUTE = 60 * TICKS_PER_SECOND; + /** Whether the server is running Folia. */ + private static final boolean isFolia = UniversalScheduler.isFolia; + private final AuthMe authMe; + private boolean useAsyncTasks; + + @Inject + BukkitService(AuthMe authMe, Settings settings) { + this.authMe = authMe; + reload(settings); + } + + /** + * Schedules a once off task to occur as soon as possible. + *

+ * This task will be executed by the main server thread. + * + * @param task Task to be executed + */ + public void scheduleSyncDelayedTask(Runnable task) { + runTask(task); + } + + /** + * Schedules a once off task to occur after a delay. + *

+ * This task will be executed by the main server thread. + * + * @param task Task to be executed + * @param delay Delay in server ticks before executing task + */ + public void scheduleSyncDelayedTask(Runnable task, long delay) { + if (isFolia) { + runTaskLater(task, delay); + } else { + Bukkit.getScheduler().runTaskLater(authMe, task, delay); // We must do this to keep compatibility + } + } + + /** + * Schedules a synchronous task if we are currently on a async thread; if not, it runs the task immediately. + * Use this when {@link #runTaskOptionallyAsync(Runnable) optionally asynchronous tasks} have to + * run something synchronously. + * + * @param task the task to be run + */ + public void scheduleSyncTaskFromOptionallyAsyncTask(Runnable task) { + if (Bukkit.isPrimaryThread()) { + runTask(task); + } else { + scheduleSyncDelayedTask(task); + } + } + + /** + * Returns a task that will run on the next server tick. + * + * @param task the task to be run + * @throws IllegalArgumentException if plugin is null + * @throws IllegalArgumentException if task is null + */ + public void runTask(Runnable task) { + if (isFolia) { + getScheduler().runTask(task); + } else { + Bukkit.getScheduler().runTask(authMe, task); + } + } + + public void runTask(Entity entity, Runnable task) { + if (isFolia) { + getScheduler().runTask(entity, task); + } else { + Bukkit.getScheduler().runTask(authMe, task); + } + } + + public void runTask(Location location, Runnable task) { + getScheduler().runTask(location, task); + } + + /** + * Runs the task synchronously if we are running Folia, else do nothing but run it. + * @param task the task to be run + */ + public void runTaskIfFolia(Runnable task) { + if (isFolia) { + runTask(task); + } else { + task.run(); + } + } + + /** + * Runs the task synchronously if we are running Folia, else do nothing but run it. + * @param task the task to be run + */ + public void runTaskIfFolia(Entity entity, Runnable task) { + if (isFolia) { + runTask(entity, task); + } else { + task.run(); + } + } + + /** + * Runs the task synchronously if we are running Folia, else do nothing but run it. + * @param task the task to be run + */ + public void runTaskIfFolia(Location location, Runnable task) { + if (isFolia) { + runTask(location, task); + } else { + task.run(); + } + } + + /** + * Returns a task that will run after the specified number of server + * ticks. + * + * @param task the task to be run + * @param delay the ticks to wait before running the task + * @return a BukkitTask that contains the id number + * @throws IllegalArgumentException if plugin is null + * @throws IllegalArgumentException if task is null + */ + public MyScheduledTask runTaskLater(Runnable task, long delay) { + return getScheduler().runTaskLater(task, delay); + } + + public MyScheduledTask runTaskLater(Entity entity, Runnable task, long delay) { + return getScheduler().runTaskLater(entity, task, delay); + } + + /** + * Schedules this task to run asynchronously or immediately executes it based on + * AuthMe's configuration. + * + * @param task the task to run + */ + public void runTaskOptionallyAsync(Runnable task) { + if (useAsyncTasks) { + runTaskAsynchronously(task); + } else { + runTask(task); + } + } + + /** + * Asynchronous tasks should never access any API in Bukkit. Great care + * should be taken to assure the thread-safety of asynchronous tasks. + *

+ * Returns a task that will run asynchronously. + * + * @param task the task to be run + * @throws IllegalArgumentException if plugin is null + * @throws IllegalArgumentException if task is null + */ + public void runTaskAsynchronously(Runnable task) { + if (isFolia) { + getScheduler().runTaskAsynchronously(task); + } else { + Bukkit.getScheduler().runTaskAsynchronously(authMe, task); + } + } + + /** + * Asynchronous tasks should never access any API in Bukkit. Great care + * should be taken to assure the thread-safety of asynchronous tasks. + *

+ * Returns a task that will repeatedly run asynchronously until cancelled, + * starting after the specified number of server ticks. + * + * @param task the task to be run + * @param delay the ticks to wait before running the task for the first + * time + * @param period the ticks to wait between runs + * @return a BukkitTask that contains the id number + * @throws IllegalArgumentException if task is null + * @throws IllegalStateException if this was already scheduled + */ + public MyScheduledTask runTaskTimerAsynchronously(UniversalRunnable task, long delay, long period) { + return task.runTaskTimerAsynchronously(authMe, delay, period); + } + + /** + * Schedules the given task to repeatedly run until cancelled, starting after the + * specified number of server ticks. + * + * @param task the task to schedule + * @param delay the ticks to wait before running the task + * @param period the ticks to wait between runs + * @return a BukkitTask that contains the id number + * @throws IllegalArgumentException if plugin is null + * @throws IllegalStateException if this was already scheduled + */ + public MyScheduledTask runTaskTimer(UniversalRunnable task, long delay, long period) { + return task.runTaskTimer(authMe, delay, period); + } + + /** + * Broadcast a message to all players. + * + * @param message the message + * @return the number of players + */ + public int broadcastMessage(String message) { + return Bukkit.broadcastMessage(message); + } + + /** + * Gets the player with the exact given name, case insensitive. + * + * @param name Exact name of the player to retrieve + * @return a player object if one was found, null otherwise + */ + public Player getPlayerExact(String name) { + return authMe.getServer().getPlayerExact(name); + } + + /** + * Gets the player by the given name, regardless if they are offline or + * online. + *

+ * This method may involve a blocking web request to get the UUID for the + * given name. + *

+ * This will return an object even if the player does not exist. To this + * method, all players will exist. + * + * @param name the name the player to retrieve + * @return an offline player + */ + public OfflinePlayer getOfflinePlayer(String name) { + return authMe.getServer().getOfflinePlayer(name); + } + + /** + * Gets a set containing all banned players. + * + * @return a set containing banned players + */ + public Set getBannedPlayers() { + return Bukkit.getBannedPlayers(); + } + + /** + * Gets every player that has ever played on this server. + * + * @return an array containing all previous players + */ + public OfflinePlayer[] getOfflinePlayers() { + return Bukkit.getOfflinePlayers(); + } + + /** + * Gets a view of all currently online players. + * + * @return collection of online players + */ + @SuppressWarnings("unchecked") + public Collection getOnlinePlayers() { + return (Collection) Bukkit.getOnlinePlayers(); + } + + /** + * 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); + } + + /** + * Creates an event with the provided function and emits it. + * + * @param eventSupplier the event supplier: function taking a boolean specifying whether AuthMe is configured + * in async mode or not + * @param the event type + * @return the event that was created and emitted + */ + public E createAndCallEvent(Function eventSupplier) { + E event = eventSupplier.apply(useAsyncTasks); + callEvent(event); + return event; + } + + /** + * Creates a PotionEffect with blindness for the given duration in ticks. + * + * @param timeoutInTicks duration of the effect in ticks + * @return blindness potion effect + */ + public PotionEffect createBlindnessEffect(int timeoutInTicks) { + return new PotionEffect(PotionEffectType.BLINDNESS, timeoutInTicks, 2); + } + + /** + * 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); + } + + /** + * Dispatches a command on this server, and executes it if found. + * + * @param sender the apparent sender of the command + * @param commandLine the command + arguments. Example: test abc 123 + * @return returns false if no target is found + */ + public boolean dispatchCommand(CommandSender sender, String commandLine) { + return Bukkit.dispatchCommand(sender, commandLine); + } + + /** + * Dispatches a command to be run as console user on this server, and executes it if found. + * + * @param commandLine the command + arguments. Example: test abc 123 + * @return returns false if no target is found + */ + public boolean dispatchConsoleCommand(String commandLine) { + return Bukkit.dispatchCommand(Bukkit.getConsoleSender(), commandLine); + } + + @Override + public void reload(Settings settings) { + useAsyncTasks = settings.getProperty(PluginSettings.USE_ASYNC_TASKS); + } + + /** + * Send the specified bytes to bungeecord using the specified player connection. + * + * @param player the player + * @param bytes the message + */ + public void sendBungeeMessage(Player player, byte[] bytes) { + player.sendPluginMessage(authMe, "BungeeCord", bytes); + } + + /** + * Send the specified bytes to bungeecord using the specified player connection. + * + * @param player the player + * @param bytes the message + */ + public void sendVelocityMessage(Player player, byte[] bytes) { + if (player != null) { + player.sendPluginMessage(authMe, "authmevelocity:main", bytes); + } else { + Bukkit.getServer().sendPluginMessage(authMe, "authmevelocity:main", bytes); + } + } + + + /** + * Adds a ban to the 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); + } + + /** + * Returns an optional with a boolean indicating whether bungeecord is enabled or not if the + * server implementation is Spigot. Otherwise returns an empty optional. + * + * @return Optional with configuration value for Spigot, empty optional otherwise + */ + public Optional isBungeeCordConfiguredForSpigot() { + try { + YamlConfiguration spigotConfig = Bukkit.spigot().getConfig(); + return Optional.of(spigotConfig.getBoolean("settings.bungeecord")); + } catch (NoSuchMethodError e) { + return Optional.empty(); + } + } + + /** + * @return the IP string that this server is bound to, otherwise empty string + */ + public String getIp() { + return Bukkit.getServer().getIp(); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/service/CommonService.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/service/CommonService.java new file mode 100644 index 00000000..92a49267 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/service/CommonService.java @@ -0,0 +1,84 @@ +package fr.xephi.authme.service; + +import ch.jalu.configme.properties.Property; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.message.Messages; +import fr.xephi.authme.permission.PermissionNode; +import fr.xephi.authme.permission.PermissionsManager; +import fr.xephi.authme.settings.Settings; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import javax.inject.Inject; + +/** + * Service for the most common operations regarding settings, messages and permissions. + */ +public class CommonService { + + @Inject + private Settings settings; + + @Inject + private Messages messages; + + @Inject + private PermissionsManager permissionsManager; + + CommonService() { + } + + /** + * Retrieves a property's value. + * + * @param property the property to retrieve + * @param the property type + * @return the property's value + */ + public T getProperty(Property property) { + return settings.getProperty(property); + } + + /** + * Sends a message to the command sender. + * + * @param sender the command sender + * @param key the message key + */ + public void send(CommandSender sender, MessageKey key) { + messages.send(sender, key); + } + + /** + * Sends a message to the command sender with the given replacements. + * + * @param sender the command sender + * @param key the message key + * @param replacements the replacements to apply to the message + */ + public void send(CommandSender sender, MessageKey key, String... replacements) { + messages.send(sender, key, replacements); + } + + /** + * Retrieves a message in one piece. + * + * @param sender The entity to send the message to + * @param key the key of the message + * @return the message + */ + public String retrieveSingleMessage(CommandSender sender, MessageKey key) { + return messages.retrieveSingle(sender, key); + } + + /** + * Checks whether the player has the given permission. + * + * @param player the player + * @param node the permission node to check + * @return true if player has permission, false otherwise + */ + public boolean hasPermission(Player player, PermissionNode node) { + return permissionsManager.hasPermission(player, node); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/service/GeoIpService.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/service/GeoIpService.java new file mode 100644 index 00000000..bf7252ff --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/service/GeoIpService.java @@ -0,0 +1,188 @@ +package fr.xephi.authme.service; + +import com.google.common.annotations.VisibleForTesting; +import com.maxmind.db.GeoIp2Provider; +import com.maxmind.db.Reader; +import com.maxmind.db.Reader.FileMode; +import com.maxmind.db.cache.CHMCache; +import com.maxmind.db.model.Country; +import com.maxmind.db.model.CountryResponse; +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.initialization.DataFolder; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.util.InternetProtocolUtils; + +import javax.inject.Inject; +import java.io.File; +import java.io.IOException; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Optional; + +public class GeoIpService { + + //private static final String LICENSE = + //"[LICENSE] This product includes GeoLite2 data created by MaxMind, available at https://www.maxmind.com"; + + private static final String DATABASE_NAME = "GeoLite2-Country"; + private static final String DATABASE_FILE = DATABASE_NAME + ".mmdb"; + + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(GeoIpService.class); + private final Path dataFile; + + private GeoIp2Provider databaseReader; + private volatile boolean downloading; + + @Inject + GeoIpService(@DataFolder File dataFolder){ + this.dataFile = dataFolder.toPath().resolve(DATABASE_FILE); + + // Fires download of recent data or the initialization of the look up service + isDataAvailable(); + } + + @VisibleForTesting + GeoIpService(@DataFolder File dataFolder, GeoIp2Provider reader) { + this.dataFile = dataFolder.toPath().resolve(DATABASE_FILE); + + this.databaseReader = reader; + } + + /** + * Download (if absent or old) the GeoIpLite data file and then try to load it. + * + * @return True if the data is available, false otherwise. + */ + private synchronized boolean isDataAvailable() { + if (downloading) { + // we are currently downloading the database + return false; + } + + if (databaseReader != null) { + // everything is initialized + return true; + } + + if (Files.exists(dataFile)) { + try { + startReading(); + return true; + } catch (IOException ioEx) { + logger.logException("Failed to load GeoLiteAPI database", ioEx); + return false; + } + } + + // File is outdated or doesn't exist - let's try to download the data file! + // use bukkit's cached threads + return false; + } + + /** + * + */ + + private void startReading() throws IOException { + databaseReader = new Reader(dataFile.toFile(), FileMode.MEMORY, new CHMCache()); + + // clear downloading flag, because we now have working reader instance + downloading = false; + } + + /** + * Downloads the archive to the destination file if it's newer than the locally version. + * + * @param lastModified modification timestamp of the already present file + * @param destination save file + * @return null if no updates were found, the MD5 hash of the downloaded archive if successful + * @throws IOException if failed during downloading and writing to destination file + */ + + /** + * Downloads the archive to the destination file if it's newer than the locally version. + * + * @param destination save file + * @return null if no updates were found, the MD5 hash of the downloaded archive if successful + * @throws IOException if failed during downloading and writing to destination file + */ + + /** + * Verify if the expected checksum is equal to the checksum of the given file. + * + * @param function the checksum function like MD5, SHA256 used to generate the checksum from the file + * @param file the file we want to calculate the checksum from + * @param expectedChecksum the expected checksum + * @throws IOException on I/O error reading the file or the checksum verification failed + */ + + /** + * Extract the database from gzipped data. Existing outputFile will be replaced if it already exists. + * + * @param inputFile gzipped database input file + * @param outputFile destination file for the database + * @throws IOException on I/O error reading the archive, or writing the output + */ + + /** + * Get the country code of the given IP address. + * + * @param ip textual IP address to lookup. + * @return two-character ISO 3166-1 alpha code for the country, "LOCALHOST" for local addresses + * or "--" if it cannot be fetched. + */ + public String getCountryCode(String ip) { + if (InternetProtocolUtils.isLocalAddress(ip)) { + return "LOCALHOST"; + } + return getCountry(ip).map(Country::getIsoCode).orElse("--"); + } + + /** + * Get the country name of the given IP address. + * + * @param ip textual IP address to lookup. + * @return The name of the country, "LocalHost" for local addresses, or "N/A" if it cannot be fetched. + */ + public String getCountryName(String ip) { + if (InternetProtocolUtils.isLocalAddress(ip)) { + return "LocalHost"; + } + return getCountry(ip).map(Country::getName).orElse("N/A"); + } + + /** + * Get the country of the given IP address + * + * @param ip textual IP address to lookup + * @return the wrapped Country model or {@link Optional#empty()} if + *

    + *
  • Database reader isn't initialized
  • + *
  • MaxMind has no record about this IP address
  • + *
  • IP address is local
  • + *
  • Textual representation is not a valid IP address
  • + *
+ */ + private Optional getCountry(String ip) { + if (ip == null || ip.isEmpty() || !isDataAvailable()) { + return Optional.empty(); + } + + try { + InetAddress address = InetAddress.getByName(ip); + + // Reader.getCountry() can be null for unknown addresses + return Optional.ofNullable(databaseReader.getCountry(address)).map(CountryResponse::getCountry); + } catch (UnknownHostException e) { + // Ignore invalid ip addresses + // Legacy GEO IP Database returned a unknown country object with Country-Code: '--' and Country-Name: 'N/A' + } catch (IOException ioEx) { + logger.logException("Cannot lookup country for " + ip + " at GEO IP database", ioEx); + } + + return Optional.empty(); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/service/HelpTranslationGenerator.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/service/HelpTranslationGenerator.java new file mode 100644 index 00000000..e1c8c5ba --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/service/HelpTranslationGenerator.java @@ -0,0 +1,126 @@ +package fr.xephi.authme.service; + +import com.google.common.collect.ImmutableMap; +import fr.xephi.authme.command.CommandArgumentDescription; +import fr.xephi.authme.command.CommandDescription; +import fr.xephi.authme.command.CommandInitializer; +import fr.xephi.authme.command.help.HelpMessage; +import fr.xephi.authme.command.help.HelpMessagesService; +import fr.xephi.authme.command.help.HelpSection; +import fr.xephi.authme.initialization.DataFolder; +import fr.xephi.authme.message.MessagePathHelper; +import fr.xephi.authme.permission.DefaultPermission; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.properties.PluginSettings; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.Yaml; + +import javax.inject.Inject; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.StandardOpenOption; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Generates the full command structure for the help translation and saves it to the current help file, + * preserving already existing entries. + */ +public class HelpTranslationGenerator { + + @Inject + private CommandInitializer commandInitializer; + + @Inject + private HelpMessagesService helpMessagesService; + + @Inject + private Settings settings; + + @DataFolder + @Inject + private File dataFolder; + + /** + * Updates the help file to contain entries for all commands. + * + * @return the help file that has been updated + * @throws IOException if the help file cannot be written to + */ + public File updateHelpFile() throws IOException { + String languageCode = settings.getProperty(PluginSettings.MESSAGES_LANGUAGE); + File helpFile = new File(dataFolder, MessagePathHelper.createHelpMessageFilePath(languageCode)); + Map helpEntries = generateHelpMessageEntries(); + + String helpEntriesYaml = exportToYaml(helpEntries); + Files.write(helpFile.toPath(), helpEntriesYaml.getBytes(), StandardOpenOption.TRUNCATE_EXISTING); + return helpFile; + } + + private static String exportToYaml(Map helpEntries) { + DumperOptions options = new DumperOptions(); + options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); + options.setAllowUnicode(true); + return new Yaml(options).dump(helpEntries); + } + + /** + * Generates entries for a complete help text file. + * + * @return help text entries to save + */ + private Map generateHelpMessageEntries() { + Map messageEntries = new LinkedHashMap<>(HelpMessage.values().length); + for (HelpMessage message : HelpMessage.values()) { + messageEntries.put(message.getEntryKey(), helpMessagesService.getMessage(message)); + } + + Map defaultPermissions = new LinkedHashMap<>(); + for (DefaultPermission defaultPermission : DefaultPermission.values()) { + defaultPermissions.put(HelpMessagesService.getDefaultPermissionsSubPath(defaultPermission), + helpMessagesService.getMessage(defaultPermission)); + } + messageEntries.put("defaultPermissions", defaultPermissions); + + Map sectionEntries = new LinkedHashMap<>(HelpSection.values().length); + for (HelpSection section : HelpSection.values()) { + sectionEntries.put(section.getEntryKey(), helpMessagesService.getMessage(section)); + } + + Map commandEntries = new LinkedHashMap<>(); + for (CommandDescription command : commandInitializer.getCommands()) { + generateCommandEntries(command, commandEntries); + } + + return ImmutableMap.of( + "common", messageEntries, + "section", sectionEntries, + "commands", commandEntries); + } + + /** + * Adds YAML entries for the provided command its children to the given map. + * + * @param command the command to process (including its children) + * @param commandEntries the map to add the generated entries to + */ + private void generateCommandEntries(CommandDescription command, Map commandEntries) { + CommandDescription translatedCommand = helpMessagesService.buildLocalizedDescription(command); + Map commandData = new LinkedHashMap<>(); + commandData.put("description", translatedCommand.getDescription()); + commandData.put("detailedDescription", translatedCommand.getDetailedDescription()); + + int i = 1; + for (CommandArgumentDescription argument : translatedCommand.getArguments()) { + Map argumentData = new LinkedHashMap<>(2); + argumentData.put("label", argument.getName()); + argumentData.put("description", argument.getDescription()); + commandData.put("arg" + i, argumentData); + ++i; + } + + commandEntries.put(HelpMessagesService.getCommandSubPath(translatedCommand), commandData); + translatedCommand.getChildren().forEach(child -> generateCommandEntries(child, commandEntries)); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/service/JoinMessageService.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/service/JoinMessageService.java new file mode 100644 index 00000000..d4c5b4c6 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/service/JoinMessageService.java @@ -0,0 +1,45 @@ +package fr.xephi.authme.service; + +import fr.xephi.authme.util.StringUtils; + +import javax.inject.Inject; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * The JoinMessageService class. + */ +public class JoinMessageService { + + private BukkitService bukkitService; + + private Map joinMessages; + + @Inject + JoinMessageService(BukkitService bukkitService) { + this.bukkitService = bukkitService; + joinMessages = new ConcurrentHashMap<>(); + } + + /** + * Store a join message. + * + * @param playerName the player name + * @param string the join message + */ + public void putMessage(String playerName, String string) { + joinMessages.put(playerName, string); + } + + /** + * Broadcast the join message of the specified player. + * + * @param playerName the player name + */ + public void sendMessage(String playerName) { + String joinMessage = joinMessages.remove(playerName); + if (!StringUtils.isBlank(joinMessage)) { + bukkitService.broadcastMessage(joinMessage); + } + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/service/MigrationService.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/service/MigrationService.java new file mode 100644 index 00000000..de8d88a7 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/service/MigrationService.java @@ -0,0 +1,54 @@ +package fr.xephi.authme.service; + +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.security.HashAlgorithm; +import fr.xephi.authme.security.crypts.HashedPassword; +import fr.xephi.authme.security.crypts.Sha256; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.properties.SecuritySettings; + +import java.util.List; + +/** + * Migrations to perform during the initialization of AuthMe. + */ +public final class MigrationService { + + private static ConsoleLogger logger = ConsoleLoggerFactory.get(MigrationService.class); + + private MigrationService() { + } + + /** + * Hash all passwords to Sha256 and updated the setting if the password hash is set to the deprecated PLAINTEXT. + * + * @param settings The settings instance + * @param dataSource The data source + * @param authmeSha256 Instance to the AuthMe Sha256 encryption method implementation + */ + public static void changePlainTextToSha256(Settings settings, DataSource dataSource, + Sha256 authmeSha256) { + if (HashAlgorithm.PLAINTEXT == settings.getProperty(SecuritySettings.PASSWORD_HASH)) { + logger.warning("Your HashAlgorithm has been detected as plaintext and is now deprecated;" + + " it will be changed and hashed now to the AuthMe default hashing method"); + logger.warning("Don't stop your server; wait for the conversion to have been completed!"); + List allAuths = dataSource.getAllAuths(); + for (PlayerAuth auth : allAuths) { + String hash = auth.getPassword().getHash(); + if (hash.startsWith("$SHA$")) { + logger.warning("Skipping conversion for " + auth.getNickname() + "; detected SHA hash"); + } else { + HashedPassword hashedPassword = authmeSha256.computeHash(hash, auth.getNickname()); + auth.setPassword(hashedPassword); + dataSource.updatePassword(auth); + } + } + settings.setProperty(SecuritySettings.PASSWORD_HASH, HashAlgorithm.SHA256); + settings.save(); + logger.info("Migrated " + allAuths.size() + " accounts from plaintext to SHA256"); + } + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/service/PasswordRecoveryService.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/service/PasswordRecoveryService.java new file mode 100644 index 00000000..01f96300 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/service/PasswordRecoveryService.java @@ -0,0 +1,187 @@ +package fr.xephi.authme.service; + +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.initialization.HasCleanup; +import fr.xephi.authme.initialization.Reloadable; +import fr.xephi.authme.mail.EmailService; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.message.Messages; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.security.PasswordSecurity; +import fr.xephi.authme.security.crypts.HashedPassword; +import fr.xephi.authme.settings.properties.SecuritySettings; +import fr.xephi.authme.util.PlayerUtils; +import fr.xephi.authme.util.RandomStringUtils; +import fr.xephi.authme.util.expiring.Duration; +import fr.xephi.authme.util.expiring.ExpiringMap; +import fr.xephi.authme.util.expiring.ExpiringSet; +import org.bukkit.entity.Player; + +import javax.annotation.PostConstruct; +import javax.inject.Inject; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.concurrent.TimeUnit; + +import static fr.xephi.authme.settings.properties.EmailSettings.RECOVERY_PASSWORD_LENGTH; + +/** + * Manager for password recovery. + */ +public class PasswordRecoveryService implements Reloadable, HasCleanup { + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(PasswordRecoveryService.class); + + @Inject + private CommonService commonService; + + @Inject + private DataSource dataSource; + + @Inject + private EmailService emailService; + + @Inject + private PasswordSecurity passwordSecurity; + + @Inject + private RecoveryCodeService recoveryCodeService; + + @Inject + private Messages messages; + + private ExpiringSet emailCooldown; + private ExpiringMap successfulRecovers; + + @PostConstruct + private void initEmailCooldownSet() { + emailCooldown = new ExpiringSet<>( + commonService.getProperty(SecuritySettings.EMAIL_RECOVERY_COOLDOWN_SECONDS), TimeUnit.SECONDS); + successfulRecovers = new ExpiringMap<>( + commonService.getProperty(SecuritySettings.PASSWORD_CHANGE_TIMEOUT), TimeUnit.MINUTES); + } + + /** + * Create a new recovery code and send it to the player + * via email. + * + * @param player The player getting the code. + * @param email The email to send the code to. + */ + public void createAndSendRecoveryCode(Player player, String email) { + if (!checkEmailCooldown(player)) { + return; + } + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy'年'MM'月'dd'日' HH:mm:ss"); + Date date = new Date(System.currentTimeMillis()); + String recoveryCode = recoveryCodeService.generateCode(player.getName()); + boolean couldSendMail = emailService.sendRecoveryCode(player.getName(), email, recoveryCode, dateFormat.format(date)); + if (couldSendMail) { + commonService.send(player, MessageKey.RECOVERY_CODE_SENT); + emailCooldown.add(player.getName().toLowerCase(Locale.ROOT)); + } else { + commonService.send(player, MessageKey.EMAIL_SEND_FAILURE); + } + } + + /** + * Generate a new password and send it to the player via + * email. This will update the database with the new password. + * + * @param player The player recovering their password. + * @param email The email to send the password to. + */ + public void generateAndSendNewPassword(Player player, String email) { + if (!checkEmailCooldown(player)) { + return; + } + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy'年'MM'月'dd'日' HH:mm:ss"); + Date date = new Date(System.currentTimeMillis()); + + String name = player.getName(); + String thePass = RandomStringUtils.generate(commonService.getProperty(RECOVERY_PASSWORD_LENGTH)); + HashedPassword hashNew = passwordSecurity.computeHash(thePass, name); + + logger.info("Generating new password for '" + name + "'"); + + dataSource.updatePassword(name, hashNew); + boolean couldSendMail = emailService.sendPasswordMail(name, email, thePass, dateFormat.format(date)); + if (couldSendMail) { + commonService.send(player, MessageKey.RECOVERY_EMAIL_SENT_MESSAGE); + emailCooldown.add(player.getName().toLowerCase(Locale.ROOT)); + } else { + commonService.send(player, MessageKey.EMAIL_SEND_FAILURE); + } + } + + /** + * Allows a player to change their password after + * correctly entering a recovery code. + * + * @param player The player recovering their password. + */ + public void addSuccessfulRecovery(Player player) { + String name = player.getName(); + String address = PlayerUtils.getPlayerIp(player); + + successfulRecovers.put(name, address); + commonService.send(player, MessageKey.RECOVERY_CHANGE_PASSWORD); + } + + /** + * Removes a player from the list of successful recovers so that he can + * no longer use the /email setpassword command. + * + * @param player The player to remove. + */ + public void removeFromSuccessfulRecovery(Player player) { + successfulRecovers.remove(player.getName()); + } + + /** + * Check if a player is able to have emails sent. + * + * @param player The player to check. + * @return True if the player is not on cooldown. + */ + private boolean checkEmailCooldown(Player player) { + Duration waitDuration = emailCooldown.getExpiration(player.getName().toLowerCase(Locale.ROOT)); + if (waitDuration.getDuration() > 0) { + String durationText = messages.formatDuration(waitDuration); + messages.send(player, MessageKey.EMAIL_COOLDOWN_ERROR, durationText); + return false; + } + return true; + } + + /** + * Checks if a player can change their password after recovery + * using the /email setpassword command. + * + * @param player The player to check. + * @return True if the player can change their password. + */ + public boolean canChangePassword(Player player) { + String name = player.getName(); + String playerAddress = PlayerUtils.getPlayerIp(player); + String storedAddress = successfulRecovers.get(name); + + return storedAddress != null && playerAddress.equals(storedAddress); + } + + @Override + public void reload() { + emailCooldown.setExpiration( + commonService.getProperty(SecuritySettings.EMAIL_RECOVERY_COOLDOWN_SECONDS), TimeUnit.SECONDS); + successfulRecovers.setExpiration( + commonService.getProperty(SecuritySettings.PASSWORD_CHANGE_TIMEOUT), TimeUnit.MINUTES); + } + + @Override + public void performCleanup() { + emailCooldown.removeExpiredEntries(); + successfulRecovers.removeExpiredEntries(); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/service/PluginHookService.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/service/PluginHookService.java new file mode 100644 index 00000000..3615aa73 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/service/PluginHookService.java @@ -0,0 +1,197 @@ +package fr.xephi.authme.service; + +import ch.jalu.injector.annotations.NoFieldScan; +import com.earth2me.essentials.Essentials; +import com.onarandombox.MultiverseCore.MultiverseCore; +import com.onarandombox.MultiverseCore.api.MVWorldManager; +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.PluginManager; + +import javax.inject.Inject; +import java.io.File; + +/** + * Hooks into third-party plugins and allows to perform actions on them. + */ +@NoFieldScan +public class PluginHookService { + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(PluginHookService.class); + private final PluginManager pluginManager; + private Essentials essentials; + private Plugin cmi; + private MultiverseCore multiverse; + + /** + * Constructor. + * + * @param pluginManager The server's plugin manager + */ + @Inject + public PluginHookService(PluginManager pluginManager) { + this.pluginManager = pluginManager; + tryHookToEssentials(); + tryHookToCmi(); + tryHookToMultiverse(); + } + + /** + * Enable or disable the social spy status of the given user if Essentials is available. + * + * @param player The player to modify + * @param socialSpyStatus The social spy status (enabled/disabled) to set + */ + public void setEssentialsSocialSpyStatus(Player player, boolean socialSpyStatus) { + if (essentials != null) { + essentials.getUser(player).setSocialSpyEnabled(socialSpyStatus); + } + } + + /** + * If Essentials is hooked into, return Essentials' data folder. + * + * @return The Essentials data folder, or null if unavailable + */ + public File getEssentialsDataFolder() { + if (essentials != null) { + return essentials.getDataFolder(); + } + return null; + } + + /** + * If CMI is hooked into, return CMI' data folder. + * + * @return The CMI data folder, or null if unavailable + */ + public File getCmiDataFolder() { + Plugin plugin = pluginManager.getPlugin("CMI"); + if (plugin == null) { + return null; + } + return plugin.getDataFolder(); + } + + /** + * Return the spawn of the given world as defined by Multiverse (if available). + * + * @param world The world to get the Multiverse spawn for + * @return The spawn location from Multiverse, or null if unavailable + */ + public Location getMultiverseSpawn(World world) { + if (multiverse != null) { + MVWorldManager manager = multiverse.getMVWorldManager(); + if (manager.isMVWorld(world)) { + return manager.getMVWorld(world).getSpawnLocation(); + } + } + return null; + } + + // ------ + // "Is plugin available" methods + // ------ + + /** + * @return true if we have a hook to Essentials, false otherwise + */ + public boolean isEssentialsAvailable() { + return essentials != null; + } + + /** + * @return true if we have a hook to CMI, false otherwise + */ + public boolean isCmiAvailable() { + return cmi != null; + } + + /** + * @return true if we have a hook to Multiverse, false otherwise + */ + public boolean isMultiverseAvailable() { + return multiverse != null; + } + + // ------ + // Hook methods + // ------ + + /** + * Attempts to create a hook into Essentials. + */ + public void tryHookToEssentials() { + try { + essentials = getPlugin(pluginManager, "Essentials", Essentials.class); + } catch (Exception | NoClassDefFoundError ignored) { + essentials = null; + } + } + + /** + * Attempts to create a hook into CMI. + */ + public void tryHookToCmi() { + try { + cmi = getPlugin(pluginManager, "CMI", Plugin.class); + } catch (Exception | NoClassDefFoundError ignored) { + cmi = null; + } + } + + /** + * Attempts to create a hook into Multiverse. + */ + public void tryHookToMultiverse() { + try { + multiverse = getPlugin(pluginManager, "Multiverse-Core", MultiverseCore.class); + } catch (Exception | NoClassDefFoundError ignored) { + multiverse = null; + } + } + + // ------ + // Unhook methods + // ------ + + /** + * Unhooks from Essentials. + */ + public void unhookEssentials() { + essentials = null; + } + + /** + * Unhooks from CMI. + */ + public void unhookCmi() { + cmi = null; + } + + /** + * Unhooks from Multiverse. + */ + public void unhookMultiverse() { + multiverse = null; + } + + // ------ + // Helpers + // ------ + + private T getPlugin(PluginManager pluginManager, String name, Class clazz) + throws Exception, NoClassDefFoundError { + if (pluginManager.isPluginEnabled(name)) { + T plugin = clazz.cast(pluginManager.getPlugin(name)); + logger.info("Hooked successfully into " + name); + return plugin; + } + return null; + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/service/RecoveryCodeService.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/service/RecoveryCodeService.java new file mode 100644 index 00000000..dcccb36d --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/service/RecoveryCodeService.java @@ -0,0 +1,111 @@ +package fr.xephi.authme.service; + +import fr.xephi.authme.initialization.HasCleanup; +import fr.xephi.authme.initialization.SettingsDependent; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.properties.SecuritySettings; +import fr.xephi.authme.util.RandomStringUtils; +import fr.xephi.authme.util.expiring.ExpiringMap; +import fr.xephi.authme.util.expiring.TimedCounter; + +import javax.inject.Inject; +import java.util.concurrent.TimeUnit; + +/** + * Manager for recovery codes. + */ +public class RecoveryCodeService implements SettingsDependent, HasCleanup { + + private final ExpiringMap recoveryCodes; + private final TimedCounter playerTries; + private int recoveryCodeLength; + private int recoveryCodeExpiration; + private int recoveryCodeMaxTries; + + @Inject + RecoveryCodeService(Settings settings) { + recoveryCodeLength = settings.getProperty(SecuritySettings.RECOVERY_CODE_LENGTH); + recoveryCodeExpiration = settings.getProperty(SecuritySettings.RECOVERY_CODE_HOURS_VALID); + recoveryCodeMaxTries = settings.getProperty(SecuritySettings.RECOVERY_CODE_MAX_TRIES); + recoveryCodes = new ExpiringMap<>(recoveryCodeExpiration, TimeUnit.HOURS); + playerTries = new TimedCounter<>(recoveryCodeExpiration, TimeUnit.HOURS); + } + + /** + * @return whether recovery codes are enabled or not + */ + public boolean isRecoveryCodeNeeded() { + return recoveryCodeLength > 0 && recoveryCodeExpiration > 0; + } + + /** + * Generates the recovery code for the given player. + * + * @param player the player to generate a code for + * @return the generated code + */ + public String generateCode(String player) { + String code = RandomStringUtils.generateHex(recoveryCodeLength); + + playerTries.put(player, recoveryCodeMaxTries); + recoveryCodes.put(player, code); + return code; + } + + /** + * Checks whether the supplied code is valid for the given player. + * + * @param player the player to check for + * @param code the code to check + * @return true if the code matches and has not expired, false otherwise + */ + public boolean isCodeValid(String player, String code) { + String storedCode = recoveryCodes.get(player); + playerTries.decrement(player); + return storedCode != null && storedCode.equals(code); + } + + /** + * Checks whether a player has tries remaining to enter a code. + * + * @param player The player to check for. + * @return True if the player has tries left. + */ + public boolean hasTriesLeft(String player) { + return playerTries.get(player) > 0; + } + + /** + * Get the number of attempts a player has to enter a code. + * + * @param player The player to check for. + * @return The number of tries left. + */ + public int getTriesLeft(String player) { + return playerTries.get(player); + } + + /** + * Removes the player's recovery code if present. + * + * @param player the player + */ + public void removeCode(String player) { + recoveryCodes.remove(player); + playerTries.remove(player); + } + + @Override + public void reload(Settings settings) { + recoveryCodeLength = settings.getProperty(SecuritySettings.RECOVERY_CODE_LENGTH); + recoveryCodeExpiration = settings.getProperty(SecuritySettings.RECOVERY_CODE_HOURS_VALID); + recoveryCodeMaxTries = settings.getProperty(SecuritySettings.RECOVERY_CODE_MAX_TRIES); + recoveryCodes.setExpiration(recoveryCodeExpiration, TimeUnit.HOURS); + } + + @Override + public void performCleanup() { + recoveryCodes.removeExpiredEntries(); + playerTries.removeExpiredEntries(); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/service/SessionService.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/service/SessionService.java new file mode 100644 index 00000000..b4092b45 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/service/SessionService.java @@ -0,0 +1,105 @@ +package fr.xephi.authme.service; + +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.events.RestoreSessionEvent; +import fr.xephi.authme.initialization.Reloadable; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.settings.properties.PluginSettings; +import fr.xephi.authme.util.PlayerUtils; +import org.bukkit.entity.Player; + +import javax.inject.Inject; + +import static fr.xephi.authme.util.Utils.MILLIS_PER_MINUTE; + +/** + * Handles the user sessions. + */ +public class SessionService implements Reloadable { + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(SessionService.class); + private final CommonService service; + private final BukkitService bukkitService; + private final DataSource database; + + private boolean isEnabled; + + @Inject + SessionService(CommonService service, BukkitService bukkitService, DataSource database) { + this.service = service; + this.bukkitService = bukkitService; + this.database = database; + reload(); + } + + /** + * Returns whether the player has a session he can resume. + * + * @param player the player to check + * @return true if there is a current session, false otherwise + */ + public boolean canResumeSession(Player player) { + final String name = player.getName(); + if (isEnabled && database.hasSession(name)) { + database.setUnlogged(name); + database.revokeSession(name); + PlayerAuth auth = database.getAuth(name); + + SessionState state = fetchSessionStatus(auth, player); + if (state.equals(SessionState.VALID)) { + RestoreSessionEvent event = bukkitService.createAndCallEvent( + isAsync -> new RestoreSessionEvent(player, isAsync)); + return !event.isCancelled(); + } else if (state.equals(SessionState.IP_CHANGED)) { + service.send(player, MessageKey.SESSION_EXPIRED); + } + } + return false; + } + + /** + * Checks if the given Player has a current session by comparing its properties + * with the given PlayerAuth's. + * + * @param auth the player auth + * @param player the associated player + * @return SessionState based on the state of the session (VALID, NOT_VALID, OUTDATED, IP_CHANGED) + */ + private SessionState fetchSessionStatus(PlayerAuth auth, Player player) { + if (auth == null) { + logger.warning("No PlayerAuth in database for '" + player.getName() + "' during session check"); + return SessionState.NOT_VALID; + } else if (auth.getLastLogin() == null) { + return SessionState.NOT_VALID; + } + long timeSinceLastLogin = System.currentTimeMillis() - auth.getLastLogin(); + + if (timeSinceLastLogin > 0 + && timeSinceLastLogin < service.getProperty(PluginSettings.SESSIONS_TIMEOUT) * MILLIS_PER_MINUTE) { + if (PlayerUtils.getPlayerIp(player).equals(auth.getLastIp())) { + return SessionState.VALID; + } else { + return SessionState.IP_CHANGED; + } + } + return SessionState.OUTDATED; + } + + public void grantSession(String name) { + if (isEnabled) { + database.grantSession(name); + } + } + + public void revokeSession(String name) { + database.revokeSession(name); + } + + @Override + public void reload() { + this.isEnabled = service.getProperty(PluginSettings.SESSIONS_ENABLED); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/service/SessionState.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/service/SessionState.java new file mode 100644 index 00000000..801f36bc --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/service/SessionState.java @@ -0,0 +1,13 @@ +package fr.xephi.authme.service; + +public enum SessionState { + + VALID, + + NOT_VALID, + + OUTDATED, + + IP_CHANGED + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/service/TeleportationService.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/service/TeleportationService.java new file mode 100644 index 00000000..c50b0129 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/service/TeleportationService.java @@ -0,0 +1,200 @@ +package fr.xephi.authme.service; + +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.data.auth.PlayerCache; +import fr.xephi.authme.data.limbo.LimboPlayer; +import fr.xephi.authme.datasource.DataSource; +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.ConsoleLoggerFactory; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.SpawnLoader; +import fr.xephi.authme.settings.properties.RestrictionSettings; +import fr.xephi.authme.util.TeleportUtils; +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 { + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(TeleportationService.class); + + @Inject + private Settings settings; + + @Inject + private BukkitService bukkitService; + + @Inject + private SpawnLoader spawnLoader; + + @Inject + private PlayerCache playerCache; + + @Inject + private DataSource dataSource; + + 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)); + } + + /** + * Teleports the player according to the settings when he joins. + * + * @param player the player to process + */ + public void teleportOnJoin(final Player player) { + if (!settings.getProperty(RestrictionSettings.NO_TELEPORT) + && settings.getProperty(TELEPORT_UNAUTHED_TO_SPAWN)) { + logger.debug("Teleport on join for player `{0}`", player.getName()); + teleportToSpawn(player, playerCache.isAuthenticated(player.getName())); + } + } + + /** + * Returns the player's custom on join location. + * + * @param player the player to process + * + * @return the custom spawn location, null if the player should spawn at the original location + */ + public Location prepareOnJoinSpawnLocation(final Player player) { + if (!settings.getProperty(RestrictionSettings.NO_TELEPORT) + && settings.getProperty(TELEPORT_UNAUTHED_TO_SPAWN)) { + final Location location = spawnLoader.getSpawnLocation(player); + + SpawnTeleportEvent event = new SpawnTeleportEvent(player, location, + playerCache.isAuthenticated(player.getName())); + bukkitService.callEvent(event); + if (!isEventValid(event)) { + return null; + } + + logger.debug("Returning custom location for >1.9 join event for player `{0}`", player.getName()); + return location; + } + return null; + } + + /** + * Teleports the player to the first spawn if he is new and the first spawn is configured. + * + * @param player the player to process + */ + public void teleportNewPlayerToFirstSpawn(final Player player) { + if (settings.getProperty(RestrictionSettings.NO_TELEPORT)) { + return; + } + + Location firstSpawn = spawnLoader.getFirstSpawn(); + if (firstSpawn == null) { + return; + } + + if (!player.hasPlayedBefore() || !dataSource.isAuthAvailable(player.getName())) { + logger.debug("Attempting to teleport player `{0}` to first spawn", player.getName()); + performTeleportation(player, new FirstSpawnTeleportEvent(player, firstSpawn)); + } + } + + /** + * Teleports the player according to the settings after having successfully logged in. + * + * @param player the player + * @param auth corresponding PlayerAuth object + * @param limbo corresponding LimboPlayer object + */ + public void teleportOnLogin(final Player player, PlayerAuth auth, LimboPlayer limbo) { + if (settings.getProperty(RestrictionSettings.NO_TELEPORT)) { + return; + } + + // #856: If LimboPlayer comes from a persisted file, the Location might be null + String worldName = (limbo != null && limbo.getLocation() != null) + ? limbo.getLocation().getWorld().getName() + : null; + + // The world in LimboPlayer is from where the player comes, before any teleportation by AuthMe + if (mustForceSpawnAfterLogin(worldName)) { + logger.debug("Teleporting `{0}` to spawn because of 'force-spawn after login'", player.getName()); + teleportToSpawn(player, true); + } else if (settings.getProperty(TELEPORT_UNAUTHED_TO_SPAWN)) { + if (settings.getProperty(RestrictionSettings.SAVE_QUIT_LOCATION)) { + Location location = buildLocationFromAuth(player, auth); + Location playerLoc = player.getLocation(); + if (location.getX() == playerLoc.getX() && location.getY() == location.getY() && location.getZ() == playerLoc.getZ() + && location.getWorld() == playerLoc.getWorld()) return; + logger.debug("Teleporting `{0}` after login, based on the player auth", player.getName()); + teleportBackFromSpawn(player, location); + } else if (limbo != null && limbo.getLocation() != null) { + logger.debug("Teleporting `{0}` after login, based on the limbo player", player.getName()); + teleportBackFromSpawn(player, limbo.getLocation()); + } + } + } + + private boolean mustForceSpawnAfterLogin(String worldName) { + return worldName != null && settings.getProperty(RestrictionSettings.FORCE_SPAWN_LOCATION_AFTER_LOGIN) + && spawnOnLoginWorlds.contains(worldName); + } + + 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(), + auth.getYaw(), auth.getPitch()); + } + + 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, spawnLoc, isAuthenticated)); + } + + /** + * Emits the teleportation event and performs teleportation according to it (potentially modified + * by external listeners). Note that no 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.scheduleSyncTaskFromOptionallyAsyncTask(() -> { + bukkitService.callEvent(event); + if (player.isOnline() && isEventValid(event)) { + TeleportUtils.teleport(player, event.getTo()); + } + }); + } + + private static boolean isEventValid(AbstractTeleportEvent event) { + return !event.isCancelled() && event.getTo() != null && event.getTo().getWorld() != null; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/service/ValidationService.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/service/ValidationService.java new file mode 100644 index 00000000..fca9eb53 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/service/ValidationService.java @@ -0,0 +1,339 @@ +package fr.xephi.authme.service; + +import ch.jalu.configme.properties.Property; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimap; +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.initialization.Reloadable; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.permission.PermissionsManager; +import fr.xephi.authme.permission.PlayerStatePermission; +import fr.xephi.authme.security.HashUtils; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.properties.EmailSettings; +import fr.xephi.authme.settings.properties.ProtectionSettings; +import fr.xephi.authme.settings.properties.RestrictionSettings; +import fr.xephi.authme.settings.properties.SecuritySettings; +import fr.xephi.authme.util.PlayerUtils; +import fr.xephi.authme.util.Utils; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import javax.annotation.PostConstruct; +import javax.inject.Inject; +import java.io.DataInputStream; +import java.net.HttpURLConnection; +import java.net.InetSocketAddress; +import java.net.URL; +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.regex.Pattern; + +import static fr.xephi.authme.util.StringUtils.isInsideString; + +/** + * Validation service. + */ +public class ValidationService implements Reloadable { + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(ValidationService.class); + + @Inject + private Settings settings; + @Inject + private DataSource dataSource; + @Inject + private PermissionsManager permissionsManager; + @Inject + private GeoIpService geoIpService; + + private Pattern passwordRegex; + private Pattern emailRegex; + private Multimap restrictedNames; + + ValidationService() { + } + + @PostConstruct + @Override + public void reload() { + passwordRegex = Utils.safePatternCompile(settings.getProperty(RestrictionSettings.ALLOWED_PASSWORD_REGEX)); + restrictedNames = settings.getProperty(RestrictionSettings.ENABLE_RESTRICTED_USERS) + ? loadNameRestrictions(settings.getProperty(RestrictionSettings.RESTRICTED_USERS)) + : HashMultimap.create(); + + emailRegex = Utils.safePatternCompile(settings.getProperty(RestrictionSettings.ALLOWED_EMAIL_REGEX)); + } + + /** + * 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 the validation result + */ + public ValidationResult validatePassword(String password, String username) { + String passLow = password.toLowerCase(Locale.ROOT); + if (!passwordRegex.matcher(passLow).matches()) { + return new ValidationResult(MessageKey.PASSWORD_CHARACTERS_ERROR, passwordRegex.pattern()); + } else if (passLow.equalsIgnoreCase(username)) { + 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 new ValidationResult(MessageKey.INVALID_PASSWORD_LENGTH); + } else if (settings.getProperty(SecuritySettings.UNSAFE_PASSWORDS).contains(passLow)) { + return new ValidationResult(MessageKey.PASSWORD_UNSAFE_ERROR); + } else if (settings.getProperty(SecuritySettings.HAVE_I_BEEN_PWNED_CHECK)) { + HaveIBeenPwnedResults results = validatePasswordHaveIBeenPwned(password); + if (results != null + && results.isPwned() + && results.getPwnCount() > settings.getProperty(SecuritySettings.HAVE_I_BEEN_PWNED_LIMIT)) { + return new ValidationResult(MessageKey.PASSWORD_PWNED_ERROR, String.valueOf(results.getPwnCount())); + } + } + + return new ValidationResult(); + } + + /** + * Verifies whether the email is valid and admitted for use according to the plugin settings. + * + * @param email the email to verify + * @return true if the email is valid, false otherwise + */ + public boolean validateEmail(String email) { + return emailRegex.matcher(email).matches(); + } + + /** + * Queries the database whether the email is still free for registration, i.e. whether the given + * command sender may use the email to register a new account (as defined by settings and permissions). + * + * @param email the email to verify + * @param sender the command sender + * @return true if the email may be used, false otherwise (registration threshold has been exceeded) + */ + public boolean isEmailFreeForRegistration(String email, CommandSender sender) { + return permissionsManager.hasPermission(sender, PlayerStatePermission.ALLOW_MULTIPLE_ACCOUNTS) + || dataSource.countAuthsByEmail(email) < settings.getProperty(EmailSettings.MAX_REG_PER_EMAIL); + } + + /** + * Checks whether the player's country is allowed to join the server, based on the given IP address + * and the configured country whitelist or blacklist. + * + * @param hostAddress the IP address to verify + * @return true if the IP address' country is allowed, false otherwise + */ + public boolean isCountryAdmitted(String hostAddress) { + // Check if we have restrictions on country, if not return true and avoid the country lookup + if (settings.getProperty(ProtectionSettings.COUNTRIES_WHITELIST).isEmpty() + && settings.getProperty(ProtectionSettings.COUNTRIES_BLACKLIST).isEmpty()) { + return true; + } + + String countryCode = geoIpService.getCountryCode(hostAddress); + boolean isCountryAllowed = validateWhitelistAndBlacklist(countryCode, + ProtectionSettings.COUNTRIES_WHITELIST, ProtectionSettings.COUNTRIES_BLACKLIST); + logger.debug("Country code `{0}` for `{1}` is allowed: {2}", countryCode, hostAddress, isCountryAllowed); + return isCountryAllowed; + } + + /** + * Checks if the name is unrestricted according to the configured settings. + * + * @param name the name to verify + * @return true if unrestricted, false otherwise + */ + public boolean isUnrestricted(String name) { + return settings.getProperty(RestrictionSettings.UNRESTRICTED_NAMES).contains(name.toLowerCase(Locale.ROOT)); + } + + /** + * Checks that the player meets any name restriction if present (IP/domain-based). + * + * @param player the player to check + * @return true if the player may join, false if the player does not satisfy the name restrictions + */ + public boolean fulfillsNameRestrictions(Player player) { + Collection restrictions = restrictedNames.get(player.getName().toLowerCase(Locale.ROOT)); + if (Utils.isCollectionEmpty(restrictions)) { + return true; + } + + String ip = PlayerUtils.getPlayerIp(player); + String domain = getHostName(player.getAddress()); + for (String restriction : restrictions) { + if (restriction.startsWith("regex:")) { + restriction = restriction.replace("regex:", ""); + } else { + restriction = restriction.replace("*", "(.*)"); + } + if (ip.matches(restriction)) { + return true; + } + if (domain.matches(restriction)) { + return true; + } + } + return false; + } + + @VisibleForTesting + protected String getHostName(InetSocketAddress inetSocketAddr) { + return inetSocketAddr.getHostName(); + } + + /** + * Verifies whether the given value is allowed according to the given whitelist and blacklist settings. + * Whitelist has precedence over blacklist: if a whitelist is set, the value is rejected if not present + * in the whitelist. + * + * @param value the value to verify + * @param whitelist the whitelist property + * @param blacklist the blacklist property + * @return true if the value is admitted by the lists, false otherwise + */ + private boolean validateWhitelistAndBlacklist(String value, Property> whitelist, + Property> blacklist) { + List whitelistValue = settings.getProperty(whitelist); + if (!Utils.isCollectionEmpty(whitelistValue)) { + return containsIgnoreCase(whitelistValue, value); + } + List blacklistValue = settings.getProperty(blacklist); + return Utils.isCollectionEmpty(blacklistValue) || !containsIgnoreCase(blacklistValue, value); + } + + private static boolean containsIgnoreCase(Collection coll, String needle) { + for (String entry : coll) { + if (entry.equalsIgnoreCase(needle)) { + return true; + } + } + return false; + } + + /** + * Loads the configured name restrictions into a Multimap by player name (all-lowercase). + * + * @param configuredRestrictions the restriction rules to convert to a map + * @return map of allowed IPs/domain names by player name + */ + private Multimap loadNameRestrictions(Set configuredRestrictions) { + Multimap restrictions = HashMultimap.create(); + for (String restriction : configuredRestrictions) { + if (isInsideString(';', restriction)) { + String[] data = restriction.split(";"); + restrictions.put(data[0].toLowerCase(Locale.ROOT), data[1]); + } else { + logger.warning("Restricted user rule must have a ';' separating name from restriction," + + " but found: '" + restriction + "'"); + } + } + return restrictions; + } + /** + * Check haveibeenpwned.com for the given password. + * + * @param password password to check for + * @return Results of the check + */ + public HaveIBeenPwnedResults validatePasswordHaveIBeenPwned(String password) { + String hash = HashUtils.sha1(password); + + String hashPrefix = hash.substring(0, 5); + + try { + String url = String.format("https://api.pwnedpasswords.com/range/%s", hashPrefix); + HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); + connection.setRequestMethod("GET"); + connection.setRequestProperty("User-Agent", "AuthMeReloaded"); + connection.setConnectTimeout(5000); + connection.setReadTimeout(5000); + connection.setDoInput(true); + StringBuilder outStr = new StringBuilder(); + + try (DataInputStream input = new DataInputStream(connection.getInputStream())) { + for (int c = input.read(); c != -1; c = input.read()) + outStr.append((char) c); + } + + String[] hashes = outStr.toString().split("\n"); + for (String hashSuffix : hashes) { + String[] hashSuffixParts = hashSuffix.trim().split(":"); + if (hashSuffixParts[0].equalsIgnoreCase(hash.substring(5))) { + return new HaveIBeenPwnedResults(true, Integer.parseInt(hashSuffixParts[1])); + } + } + return new HaveIBeenPwnedResults(false, 0); + } catch (java.io.IOException e) { + logger.warning("Error occurred while checking password online, check your connection.\nWhen this error shows, the player's password won't be check"); + return null; + } + } + + + 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; + } + } + + public static final class HaveIBeenPwnedResults { + private final boolean isPwned; + private final int pwnCount; + + public HaveIBeenPwnedResults(boolean isPwned, int pwnCount) { + this.isPwned = isPwned; + this.pwnCount = pwnCount; + } + + public boolean isPwned() { + return isPwned; + } + + public int getPwnCount() { + return pwnCount; + } + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/service/bungeecord/BungeeReceiver.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/service/bungeecord/BungeeReceiver.java new file mode 100644 index 00000000..05c7c944 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/service/bungeecord/BungeeReceiver.java @@ -0,0 +1,160 @@ +package fr.xephi.authme.service.bungeecord; + +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.data.ProxySessionManager; +import fr.xephi.authme.initialization.SettingsDependent; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.process.Management; +import fr.xephi.authme.service.BukkitService; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.properties.HooksSettings; +import org.bukkit.entity.Player; +import org.bukkit.plugin.messaging.Messenger; +import org.bukkit.plugin.messaging.PluginMessageListener; + +import javax.inject.Inject; +import java.util.Optional; + +public class BungeeReceiver implements PluginMessageListener, SettingsDependent { + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(BungeeReceiver.class); + + private final AuthMe plugin; + private final BukkitService bukkitService; + private final ProxySessionManager proxySessionManager; + private final Management management; + + private boolean isEnabled; + + @Inject + BungeeReceiver(AuthMe plugin, BukkitService bukkitService, ProxySessionManager proxySessionManager, + Management management, Settings settings) { + this.plugin = plugin; + this.bukkitService = bukkitService; + this.proxySessionManager = proxySessionManager; + this.management = management; + reload(settings); + } + + @Override + public void reload(Settings settings) { + this.isEnabled = settings.getProperty(HooksSettings.BUNGEECORD); + if (this.isEnabled) { + this.isEnabled = bukkitService.isBungeeCordConfiguredForSpigot().orElse(false); + } + if (this.isEnabled) { + final Messenger messenger = plugin.getServer().getMessenger(); + if (!messenger.isIncomingChannelRegistered(plugin, "BungeeCord")) { + messenger.registerIncomingPluginChannel(plugin, "BungeeCord", this); + } + } + } + + /** + * Processes the given data input and attempts to translate it to a message for the "AuthMe.v2.Broadcast" channel. + * + * @param in the input to handle + */ + private void handleBroadcast(ByteArrayDataInput in) { + // Read data byte array + short dataLength = in.readShort(); + byte[] dataBytes = new byte[dataLength]; + in.readFully(dataBytes); + ByteArrayDataInput dataIn = ByteStreams.newDataInput(dataBytes); + + // Parse type + String typeId = dataIn.readUTF(); + Optional type = MessageType.fromId(typeId); + if (!type.isPresent()) { + logger.debug("Received unsupported forwarded bungeecord message type! ({0})", typeId); + return; + } + + // Parse argument + String argument; + try { + argument = dataIn.readUTF(); + } catch (IllegalStateException e) { + logger.warning("Received invalid forwarded plugin message of type " + type.get().name() + + ": argument is missing!"); + return; + } + + // Handle type + switch (type.get()) { + case LOGIN: + case LOGOUT: + // TODO: unused + break; + default: + } + } + + /** + * Processes the given data input and attempts to translate it to a message for the "AuthMe.v2" channel. + * + * @param in the input to handle + */ + private void handle(ByteArrayDataInput in) { + // Parse type + String typeId = in.readUTF(); + Optional type = MessageType.fromId(typeId); + if (!type.isPresent()) { + logger.debug("Received unsupported bungeecord message type! ({0})", typeId); + return; + } + + // Parse argument + String argument; + try { + argument = in.readUTF(); + } catch (IllegalStateException e) { + logger.warning("Received invalid plugin message of type " + type.get().name() + + ": argument is missing!"); + return; + } + + // Handle type + switch (type.get()) { + case PERFORM_LOGIN: + performLogin(argument); + break; + default: + } + } + + @Override + public void onPluginMessageReceived(String channel, Player player, byte[] data) { + if (!isEnabled) { + return; + } + + ByteArrayDataInput in = ByteStreams.newDataInput(data); + + // Check subchannel + String subChannel = in.readUTF(); + if ("AuthMe.v2.Broadcast".equals(subChannel)) { + handleBroadcast(in); + } else if ("AuthMe.v2".equals(subChannel)) { + handle(in); + } + } + + private void performLogin(String name) { + Player player = bukkitService.getPlayerExact(name); + if (player != null && player.isOnline()) { + management.forceLogin(player, true); + logger.info("The user " + player.getName() + " has been automatically logged in, " + + "as requested via plugin messaging."); + } else { + proxySessionManager.processProxySessionMessage(name); + logger.info("The user " + name + " should be automatically logged in, " + + "as requested via plugin messaging but has not been detected, nickname has been" + + " added to autologin queue."); + } + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/service/bungeecord/BungeeSender.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/service/bungeecord/BungeeSender.java new file mode 100644 index 00000000..3d43605b --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/service/bungeecord/BungeeSender.java @@ -0,0 +1,113 @@ +package fr.xephi.authme.service.bungeecord; + +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.initialization.SettingsDependent; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.service.BukkitService; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.properties.HooksSettings; +import org.bukkit.entity.Player; +import org.bukkit.plugin.messaging.Messenger; + +import javax.inject.Inject; +import java.util.Locale; + +public class BungeeSender implements SettingsDependent { + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(BungeeSender.class); + private final AuthMe plugin; + private final BukkitService bukkitService; + + private boolean isEnabled; + private String destinationServerOnLogin; + + /* + * Constructor. + */ + @Inject + BungeeSender(AuthMe plugin, BukkitService bukkitService, Settings settings) { + this.plugin = plugin; + this.bukkitService = bukkitService; + reload(settings); + } + + @Override + public void reload(Settings settings) { + this.isEnabled = settings.getProperty(HooksSettings.BUNGEECORD); + this.destinationServerOnLogin = settings.getProperty(HooksSettings.BUNGEECORD_SERVER); + + if (this.isEnabled) { + Messenger messenger = plugin.getServer().getMessenger(); + if (!messenger.isOutgoingChannelRegistered(plugin, "BungeeCord")) { + messenger.registerOutgoingPluginChannel(plugin, "BungeeCord"); + } + } + } + + public boolean isEnabled() { + return isEnabled; + } + + private void sendBungeecordMessage(Player player, String... data) { + ByteArrayDataOutput out = ByteStreams.newDataOutput(); + for (String element : data) { + out.writeUTF(element); + } + bukkitService.sendBungeeMessage(player, out.toByteArray()); + } + + private void sendForwardedBungeecordMessage(Player player, String subChannel, String... data) { + ByteArrayDataOutput out = ByteStreams.newDataOutput(); + out.writeUTF("Forward"); + out.writeUTF("ONLINE"); + out.writeUTF(subChannel); + ByteArrayDataOutput dataOut = ByteStreams.newDataOutput(); + for (String element : data) { + dataOut.writeUTF(element); + } + byte[] dataBytes = dataOut.toByteArray(); + out.writeShort(dataBytes.length); + out.write(dataBytes); + bukkitService.sendBungeeMessage(player, out.toByteArray()); + } + + /** + * Send a player to a specified server. If no server is configured, this will + * do nothing. + * + * @param player The player to send. + */ + public void connectPlayerOnLogin(Player player) { + if (!isEnabled || destinationServerOnLogin.isEmpty()) { + return; + } + // Add a small delay, just in case... + bukkitService.scheduleSyncDelayedTask(() -> + sendBungeecordMessage(player, "Connect", destinationServerOnLogin), 10L); + } + + /** + * Sends a message to the AuthMe plugin messaging channel, if enabled. + * + * @param player The player related to the message + * @param type The message type, See {@link MessageType} + */ + public void sendAuthMeBungeecordMessage(Player player, MessageType type) { + if (!isEnabled) { + return; + } + if (!plugin.isEnabled()) { + logger.debug("Tried to send a " + type + " bungeecord message but the plugin was disabled!"); + return; + } + if (type.isBroadcast()) { + sendForwardedBungeecordMessage(player, "AuthMe.v2.Broadcast", type.getId(), player.getName().toLowerCase(Locale.ROOT)); + } else { + sendBungeecordMessage(player, "AuthMe.v2", type.getId(), player.getName().toLowerCase(Locale.ROOT)); + } + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/service/bungeecord/MessageType.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/service/bungeecord/MessageType.java new file mode 100644 index 00000000..ac63e41e --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/service/bungeecord/MessageType.java @@ -0,0 +1,42 @@ +package fr.xephi.authme.service.bungeecord; + +import java.util.Optional; + +public enum MessageType { + LOGIN("login", true), + LOGOUT("logout", true), + PERFORM_LOGIN("perform.login", false); + + private final String id; + private final boolean broadcast; + + MessageType(String id, boolean broadcast) { + this.id = id; + this.broadcast = broadcast; + } + + public String getId() { + return id; + } + + public boolean isBroadcast() { + return broadcast; + } + + /** + * Returns the MessageType with the given ID. + * + * @param id the message type id. + * + * @return the MessageType with the given id, empty if invalid. + */ + public static Optional fromId(String id) { + for (MessageType current : values()) { + if (current.getId().equals(id)) { + return Optional.of(current); + } + } + return Optional.empty(); + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/service/velocity/VMessageType.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/service/velocity/VMessageType.java new file mode 100644 index 00000000..0e65db42 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/service/velocity/VMessageType.java @@ -0,0 +1,5 @@ +package fr.xephi.authme.service.velocity; + +public enum VMessageType { + LOGIN, REGISTER, LOGOUT, FORCE_UNREGISTER, UNREGISTER +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/service/velocity/VelocityReceiver.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/service/velocity/VelocityReceiver.java new file mode 100644 index 00000000..caa04e5f --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/service/velocity/VelocityReceiver.java @@ -0,0 +1,89 @@ +package fr.xephi.authme.service.velocity; + +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.data.ProxySessionManager; +import fr.xephi.authme.initialization.SettingsDependent; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.process.Management; +import fr.xephi.authme.service.BukkitService; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.properties.HooksSettings; +import org.bukkit.entity.Player; +import org.bukkit.plugin.messaging.Messenger; +import org.bukkit.plugin.messaging.PluginMessageListener; + +import javax.inject.Inject; + +public class VelocityReceiver implements PluginMessageListener, SettingsDependent { + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(VelocityReceiver.class); + + private final AuthMe plugin; + private final BukkitService bukkitService; + private final ProxySessionManager proxySessionManager; + private final Management management; + + private boolean isEnabled; + + @Inject + VelocityReceiver(AuthMe plugin, BukkitService bukkitService, ProxySessionManager proxySessionManager, + Management management, Settings settings) { + this.plugin = plugin; + this.bukkitService = bukkitService; + this.proxySessionManager = proxySessionManager; + this.management = management; + reload(settings); + } + + @Override + public void reload(Settings settings) { + this.isEnabled = settings.getProperty(HooksSettings.VELOCITY); + if (this.isEnabled) { + final Messenger messenger = plugin.getServer().getMessenger(); + if (!messenger.isIncomingChannelRegistered(plugin, "authmevelocity:main")) { + messenger.registerIncomingPluginChannel(plugin, "authmevelocity:main", this); + } + } + } + + + @Override + public void onPluginMessageReceived(String channel, Player player, byte[] bytes) { + if (!isEnabled) { + return; + } + + if (channel.equals("authmevelocity:main")) { + final ByteArrayDataInput in = ByteStreams.newDataInput(bytes); + + final String data = in.readUTF(); + final String username = in.readUTF(); + processData(username, data); + logger.debug("PluginMessage | AuthMeVelocity identifier processed"); + } + } + + private void processData(String username, String data) { + if (VMessageType.LOGIN.toString().equals(data)) { + performLogin(username); + } + } + + private void performLogin(String name) { + Player player = bukkitService.getPlayerExact(name); + if (player != null && player.isOnline()) { + management.forceLogin(player, true); + logger.info("The user " + player.getName() + " has been automatically logged in, " + + "as requested via plugin messaging."); + } else { + proxySessionManager.processProxySessionMessage(name); + logger.info("The user " + name + " should be automatically logged in, " + + "as requested via plugin messaging but has not been detected, nickname has been" + + " added to autologin queue."); + } + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/service/velocity/VelocitySender.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/service/velocity/VelocitySender.java new file mode 100644 index 00000000..16a0f2cb --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/service/velocity/VelocitySender.java @@ -0,0 +1,76 @@ +package fr.xephi.authme.service.velocity; + +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.initialization.SettingsDependent; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.service.BukkitService; +import fr.xephi.authme.service.bungeecord.MessageType; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.properties.HooksSettings; +import org.bukkit.entity.Player; +import org.bukkit.plugin.messaging.Messenger; + +import javax.inject.Inject; + +public class VelocitySender implements SettingsDependent { + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(VelocitySender.class); + private final AuthMe plugin; + private final BukkitService bukkitService; + + private boolean isEnabled; + + /* + * Constructor. + */ + @Inject + VelocitySender(AuthMe plugin, BukkitService bukkitService, Settings settings) { + this.plugin = plugin; + this.bukkitService = bukkitService; + reload(settings); + } + + @Override + public void reload(Settings settings) { + this.isEnabled = settings.getProperty(HooksSettings.VELOCITY); + + if (this.isEnabled) { + Messenger messenger = plugin.getServer().getMessenger(); + if (!messenger.isOutgoingChannelRegistered(plugin, "authmevelocity:main")) { + messenger.registerOutgoingPluginChannel(plugin, "authmevelocity:main"); + } + } + } + + public boolean isEnabled() { + return isEnabled; + } + + private void sendForwardedVelocityMessage(Player player, VMessageType type, String playerName) { + ByteArrayDataOutput out = ByteStreams.newDataOutput(); + out.writeUTF(type.toString()); + out.writeUTF(playerName); + bukkitService.sendVelocityMessage(player, out.toByteArray()); + } + + /** + * Sends a message to the AuthMe plugin messaging channel, if enabled. + * + * @param player The player related to the message + * @param type The message type, See {@link MessageType} + */ + public void sendAuthMeVelocityMessage(Player player, VMessageType type) { + if (!isEnabled) { + return; + } + if (!plugin.isEnabled()) { + logger.debug("Tried to send a " + type + " velocity message but the plugin was disabled!"); + return; + } + sendForwardedVelocityMessage(player, type, player.getName()); + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/service/yaml/YamlFileResourceProvider.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/service/yaml/YamlFileResourceProvider.java new file mode 100644 index 00000000..9509d730 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/service/yaml/YamlFileResourceProvider.java @@ -0,0 +1,47 @@ +package fr.xephi.authme.service.yaml; + +import ch.jalu.configme.exception.ConfigMeException; +import ch.jalu.configme.resource.PropertyReader; +import ch.jalu.configme.resource.YamlFileResource; + +import java.io.File; + +/** + * Creates {@link YamlFileResource} objects. + */ +public final class YamlFileResourceProvider { + + private YamlFileResourceProvider() { + } + + /** + * Creates a {@link YamlFileResource} instance for the given file. Wraps SnakeYAML's parse exception + * thrown when a reader is created into an AuthMe exception. + * + * @param file the file to load + * @return the generated resource + */ + public static YamlFileResource loadFromFile(File file) { + return new AuthMeYamlFileResource(file); + } + + /** + * Extension of {@link YamlFileResource} which wraps SnakeYAML's parse exception into a custom + * exception when a reader is created. + */ + private static final class AuthMeYamlFileResource extends YamlFileResource { + + AuthMeYamlFileResource(File file) { + super(file); + } + + @Override + public PropertyReader createReader() { + try { + return super.createReader(); + } catch (ConfigMeException e) { + throw new YamlParseException(getFile().getPath(), e); + } + } + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/service/yaml/YamlParseException.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/service/yaml/YamlParseException.java new file mode 100644 index 00000000..eba631d3 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/service/yaml/YamlParseException.java @@ -0,0 +1,28 @@ +package fr.xephi.authme.service.yaml; + +import ch.jalu.configme.exception.ConfigMeException; + +import java.util.Optional; + +/** + * Exception when a YAML file could not be parsed. + */ +public class YamlParseException extends RuntimeException { + + private final String file; + + /** + * Constructor. + * + * @param file the file a parsing exception occurred with + * @param configMeException the caught exception from ConfigMe + */ + public YamlParseException(String file, ConfigMeException configMeException) { + super(Optional.ofNullable(configMeException.getCause()).orElse(configMeException)); + this.file = file; + } + + public String getFile() { + return file; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/EnumSetProperty.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/EnumSetProperty.java new file mode 100644 index 00000000..583be062 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/EnumSetProperty.java @@ -0,0 +1,56 @@ +package fr.xephi.authme.settings; + +import ch.jalu.configme.properties.BaseProperty; +import ch.jalu.configme.properties.convertresult.ConvertErrorRecorder; +import ch.jalu.configme.resource.PropertyReader; + +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.stream.Collectors; + +import static com.google.common.collect.Sets.newHashSet; + +/** + * Property whose value is a set of entries of a given enum. + * + * @param the enum type + */ +public class EnumSetProperty> extends BaseProperty> { + + private final Class enumClass; + + @SafeVarargs + public EnumSetProperty(Class enumClass, String path, E... values) { + super(path, newHashSet(values)); + this.enumClass = enumClass; + } + + @Override + protected Set getFromReader(PropertyReader reader, ConvertErrorRecorder errorRecorder) { + Object entry = reader.getObject(getPath()); + if (entry instanceof Collection) { + return ((Collection) entry).stream() + .map(val -> toEnum(String.valueOf(val))) + .filter(e -> e != null) + .collect(Collectors.toCollection(LinkedHashSet::new)); + } + return null; + } + + private E toEnum(String str) { + for (E e : enumClass.getEnumConstants()) { + if (str.equalsIgnoreCase(e.name())) { + return e; + } + } + return null; + } + + @Override + public Object toExportValue(Set value) { + return value.stream() + .map(Enum::name) + .collect(Collectors.toList()); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/Settings.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/Settings.java new file mode 100644 index 00000000..ad0afa6f --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/Settings.java @@ -0,0 +1,113 @@ +package fr.xephi.authme.settings; + +import ch.jalu.configme.SettingsManagerImpl; +import ch.jalu.configme.configurationdata.ConfigurationData; +import ch.jalu.configme.migration.MigrationService; +import ch.jalu.configme.resource.PropertyResource; +import com.google.common.io.Files; +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.output.ConsoleLoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import static fr.xephi.authme.util.FileUtils.copyFileFromResource; + +/** + * The AuthMe settings manager. + */ +public class Settings extends SettingsManagerImpl { + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(Settings.class); + private final File pluginFolder; + private String passwordEmailMessage; + private String verificationEmailMessage; + private String recoveryCodeEmailMessage; + private String shutdownEmailMessage; + private String newPasswordEmailMessage; + + + /** + * Constructor. + * + * @param pluginFolder the AuthMe plugin folder + * @param resource the property resource to read and write properties to + * @param migrationService migration service to check the settings file with + * @param configurationData configuration data (properties and comments) + */ + public Settings(File pluginFolder, PropertyResource resource, MigrationService migrationService, + ConfigurationData configurationData) { + super(resource, configurationData, migrationService); + this.pluginFolder = pluginFolder; + loadSettingsFromFiles(); + } + + /** + * Return the text to use in email registrations. + * + * @return The email message + */ + public String getPasswordEmailMessage() { + return passwordEmailMessage; + } + + /** + * Return the text for verification emails (before sensitive commands can be used). + * + * @return The email message + */ + public String getVerificationEmailMessage() { + return verificationEmailMessage; + } + + /** + * Return the text to use when someone requests to receive a recovery code. + * + * @return The email message + */ + public String getRecoveryCodeEmailMessage() { + return recoveryCodeEmailMessage; + } + + public String getShutdownEmailMessage() {return shutdownEmailMessage;} + + public String getNewPasswordEmailMessage() { + return newPasswordEmailMessage; + } + + private void loadSettingsFromFiles() { + newPasswordEmailMessage = readFile("new_email.html"); + passwordEmailMessage = readFile("email.html"); + verificationEmailMessage = readFile("verification_code_email.html"); + recoveryCodeEmailMessage = readFile("recovery_code_email.html"); + shutdownEmailMessage = readFile("shutdown.html"); + String country = readFile("GeoLite2-Country.mmdb"); + } + + @Override + public void reload() { + super.reload(); + loadSettingsFromFiles(); + } + + /** + * Reads a file from the plugin folder or copies it from the JAR to the plugin folder. + * + * @param filename the file to read + * @return the file's contents + */ + private String readFile(String filename) { + final File file = new File(pluginFolder, filename); + if (copyFileFromResource(file, filename)) { + try { + return Files.asCharSource(file, StandardCharsets.UTF_8).read(); + } catch (IOException e) { + logger.logException("Failed to read file '" + filename + "':", e); + } + } else { + logger.warning("Failed to copy file '" + filename + "' from JAR"); + } + return ""; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/SettingsMigrationService.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/SettingsMigrationService.java new file mode 100644 index 00000000..fe035a8b --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/SettingsMigrationService.java @@ -0,0 +1,400 @@ +package fr.xephi.authme.settings; + +import ch.jalu.configme.configurationdata.ConfigurationData; +import ch.jalu.configme.migration.PlainMigrationService; +import ch.jalu.configme.properties.Property; +import ch.jalu.configme.properties.convertresult.PropertyValue; +import ch.jalu.configme.resource.PropertyReader; +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.datasource.DataSourceType; +import fr.xephi.authme.initialization.DataFolder; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.output.LogLevel; +import fr.xephi.authme.process.register.RegisterSecondaryArgument; +import fr.xephi.authme.process.register.RegistrationType; +import fr.xephi.authme.security.HashAlgorithm; +import fr.xephi.authme.settings.properties.DatabaseSettings; +import fr.xephi.authme.settings.properties.PluginSettings; +import fr.xephi.authme.settings.properties.RegistrationSettings; +import fr.xephi.authme.settings.properties.SecuritySettings; +import fr.xephi.authme.util.StringUtils; + +import javax.inject.Inject; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import static ch.jalu.configme.properties.PropertyInitializer.newListProperty; +import static ch.jalu.configme.properties.PropertyInitializer.newProperty; +import static fr.xephi.authme.settings.properties.DatabaseSettings.MYSQL_POOL_SIZE; +import static fr.xephi.authme.settings.properties.RegistrationSettings.DELAY_JOIN_MESSAGE; +import static fr.xephi.authme.settings.properties.RegistrationSettings.REMOVE_JOIN_MESSAGE; +import static fr.xephi.authme.settings.properties.RegistrationSettings.REMOVE_LEAVE_MESSAGE; +import static fr.xephi.authme.settings.properties.RestrictionSettings.ALLOWED_NICKNAME_CHARACTERS; +import static fr.xephi.authme.settings.properties.RestrictionSettings.FORCE_SPAWN_LOCATION_AFTER_LOGIN; +import static fr.xephi.authme.settings.properties.RestrictionSettings.FORCE_SPAWN_ON_WORLDS; + +/** + * Service for verifying that the configuration is up-to-date. + */ +public class SettingsMigrationService extends PlainMigrationService { + + private static ConsoleLogger logger = ConsoleLoggerFactory.get(SettingsMigrationService.class); + private final File pluginFolder; + + // Stores old "other accounts command" config if present. + // We need to store it in here for retrieval when we build the CommandConfig. Retrieving it from the config.yml is + // not possible since this migration service may trigger the config.yml to be resaved. As the old command settings + // don't exist in the code anymore, as soon as config.yml is resaved we lose this information. + private String oldOtherAccountsCommand; + private int oldOtherAccountsCommandThreshold; + + @Inject + SettingsMigrationService(@DataFolder File pluginFolder) { + this.pluginFolder = pluginFolder; + } + + @Override + @SuppressWarnings("checkstyle:BooleanExpressionComplexity") + protected boolean performMigrations(PropertyReader reader, ConfigurationData configurationData) { + boolean changes = false; + + if ("[a-zA-Z0-9_?]*".equals(reader.getString(ALLOWED_NICKNAME_CHARACTERS.getPath()))) { + configurationData.setValue(ALLOWED_NICKNAME_CHARACTERS, "[a-zA-Z0-9_]*"); + changes = true; + } + + String driverClass = reader.getString("DataSource.mySQLDriverClassName"); + if ("fr.xephi.authme.libs.org.mariadb.jdbc.Driver".equals(driverClass)) { + configurationData.setValue(DatabaseSettings.BACKEND, DataSourceType.MARIADB); + changes = true; + } + + setOldOtherAccountsCommandFieldsIfSet(reader); + + // Note ljacqu 20160211: Concatenating migration methods with | instead of the usual || + // ensures that all migrations will be performed + return changes + | performMailTextToFileMigration(reader) + | migrateJoinLeaveMessages(reader, configurationData) + | migrateForceSpawnSettings(reader, configurationData) + | migratePoolSizeSetting(reader, configurationData) + | changeBooleanSettingToLogLevelProperty(reader, configurationData) + | hasOldHelpHeaderProperty(reader) + | hasSupportOldPasswordProperty(reader) + | convertToRegistrationType(reader, configurationData) + | mergeAndMovePermissionGroupSettings(reader, configurationData) + | moveDeprecatedHashAlgorithmIntoLegacySection(reader, configurationData) + | moveSaltColumnConfigWithOtherColumnConfigs(reader, configurationData) + || hasDeprecatedProperties(reader); + } + + private static boolean hasDeprecatedProperties(PropertyReader reader) { + String[] deprecatedProperties = { + "Converter.Rakamak.newPasswordHash", "Hooks.chestshop", "Hooks.legacyChestshop", "Hooks.notifications", + "Passpartu", "Performances", "settings.restrictions.enablePasswordVerifier", "Xenoforo.predefinedSalt", + "VeryGames", "settings.restrictions.allowAllCommandsIfRegistrationIsOptional", "DataSource.mySQLWebsite", + "Hooks.customAttributes", "Security.stop.kickPlayersBeforeStopping", + "settings.restrictions.keepCollisionsDisabled", "settings.forceCommands", "settings.forceCommandsAsConsole", + "settings.forceRegisterCommands", "settings.forceRegisterCommandsAsConsole", + "settings.sessions.sessionExpireOnIpChange", "settings.restrictions.otherAccountsCmd", + "settings.restrictions.otherAccountsCmdThreshold, DataSource.mySQLDriverClassName"}; + for (String deprecatedPath : deprecatedProperties) { + if (reader.contains(deprecatedPath)) { + return true; + } + } + return false; + } + + // -------- + // Old other accounts + // -------- + public boolean hasOldOtherAccountsCommand() { + return !StringUtils.isBlank(oldOtherAccountsCommand); + } + + public String getOldOtherAccountsCommand() { + return oldOtherAccountsCommand; + } + + public int getOldOtherAccountsCommandThreshold() { + return oldOtherAccountsCommandThreshold; + } + + // -------- + // Specific migrations + // -------- + + /** + * Check if {@code Email.mailText} is present and move it to the Email.html file if it doesn't exist yet. + * + * @param reader The property reader + * @return True if a migration has been completed, false otherwise + */ + private boolean performMailTextToFileMigration(PropertyReader reader) { + final String oldSettingPath = "Email.mailText"; + final String oldMailText = reader.getString(oldSettingPath); + if (oldMailText == null) { + return false; + } + + final File emailFile = new File(pluginFolder, "email.html"); + final String mailText = oldMailText + .replace("", "").replace("%playername%", "") + .replace("", "").replace("%servername%", "") + .replace("", "").replace("%generatedpass%", "") + .replace("", "").replace("%image%", ""); + if (!emailFile.exists()) { + try (FileWriter fw = new FileWriter(emailFile)) { + fw.write(mailText); + } catch (IOException e) { + logger.logException("Could not create email.html configuration file:", e); + } + } + return true; + } + + /** + * Detect deprecated {@code settings.delayJoinLeaveMessages} and inform user of new "remove join messages" + * and "remove leave messages" settings. + * + * @param reader The property reader + * @param configData Configuration data + * @return True if the configuration has changed, false otherwise + */ + private static boolean migrateJoinLeaveMessages(PropertyReader reader, ConfigurationData configData) { + Property oldDelayJoinProperty = newProperty("settings.delayJoinLeaveMessages", false); + boolean hasMigrated = moveProperty(oldDelayJoinProperty, DELAY_JOIN_MESSAGE, reader, configData); + + if (hasMigrated) { + logger.info(String.format("Note that we now also have the settings %s and %s", + REMOVE_JOIN_MESSAGE.getPath(), REMOVE_LEAVE_MESSAGE.getPath())); + } + return hasMigrated; + } + + /** + * Detects old "force spawn loc on join" and "force spawn on these worlds" settings and moves them + * to the new paths. + * + * @param reader The property reader + * @param configData Configuration data + * @return True if the configuration has changed, false otherwise + */ + private static boolean migrateForceSpawnSettings(PropertyReader reader, ConfigurationData configData) { + Property oldForceLocEnabled = newProperty( + "settings.restrictions.ForceSpawnLocOnJoinEnabled", false); + Property> oldForceWorlds = newListProperty( + "settings.restrictions.ForceSpawnOnTheseWorlds", "world", "world_nether", "world_the_ed"); + + return moveProperty(oldForceLocEnabled, FORCE_SPAWN_LOCATION_AFTER_LOGIN, reader, configData) + | moveProperty(oldForceWorlds, FORCE_SPAWN_ON_WORLDS, reader, configData); + } + + /** + * Detects the old auto poolSize value and replaces it with the default value. + * + * @param reader The property reader + * @param configData Configuration data + * @return True if the configuration has changed, false otherwise + */ + private static boolean migratePoolSizeSetting(PropertyReader reader, ConfigurationData configData) { + Integer oldValue = reader.getInt(MYSQL_POOL_SIZE.getPath()); + if (oldValue == null || oldValue > 0) { + return false; + } + configData.setValue(MYSQL_POOL_SIZE, 10); + return true; + } + + /** + * Changes the old boolean property "hide spam from console" to the new property specifying + * the log level. + * + * @param reader The property reader + * @param configData Configuration data + * @return True if the configuration has changed, false otherwise + */ + private static boolean changeBooleanSettingToLogLevelProperty(PropertyReader reader, + ConfigurationData configData) { + final String oldPath = "Security.console.noConsoleSpam"; + final Property newProperty = PluginSettings.LOG_LEVEL; + if (!newProperty.isValidInResource(reader) && reader.contains(oldPath)) { + logger.info("Moving '" + oldPath + "' to '" + newProperty.getPath() + "'"); + boolean oldValue = Optional.ofNullable(reader.getBoolean(oldPath)).orElse(false); + LogLevel level = oldValue ? LogLevel.INFO : LogLevel.FINE; + configData.setValue(newProperty, level); + return true; + } + return false; + } + + private static boolean hasOldHelpHeaderProperty(PropertyReader reader) { + if (reader.contains("settings.helpHeader")) { + logger.warning("Help header setting is now in messages/help_xx.yml, " + + "please check the file to set it again"); + return true; + } + return false; + } + + private static boolean hasSupportOldPasswordProperty(PropertyReader reader) { + String path = "settings.security.supportOldPasswordHash"; + if (reader.contains(path)) { + logger.warning("Property '" + path + "' is no longer supported. " + + "Use '" + SecuritySettings.LEGACY_HASHES.getPath() + "' instead."); + return true; + } + return false; + } + + /** + * Converts old boolean configurations for registration to the new enum properties, if applicable. + * + * @param reader The property reader + * @param configData Configuration data + * @return True if the configuration has changed, false otherwise + */ + private static boolean convertToRegistrationType(PropertyReader reader, ConfigurationData configData) { + String oldEmailRegisterPath = "settings.registration.enableEmailRegistrationSystem"; + if (RegistrationSettings.REGISTRATION_TYPE.isValidInResource(reader) + || !reader.contains(oldEmailRegisterPath)) { + return false; + } + + boolean useEmail = newProperty(oldEmailRegisterPath, false).determineValue(reader).getValue(); + RegistrationType registrationType = useEmail ? RegistrationType.EMAIL : RegistrationType.PASSWORD; + + String useConfirmationPath = useEmail + ? "settings.registration.doubleEmailCheck" + : "settings.restrictions.enablePasswordConfirmation"; + boolean hasConfirmation = newProperty(useConfirmationPath, false).determineValue(reader).getValue(); + RegisterSecondaryArgument secondaryArgument = hasConfirmation + ? RegisterSecondaryArgument.CONFIRMATION + : RegisterSecondaryArgument.NONE; + + logger.warning("Merging old registration settings into '" + + RegistrationSettings.REGISTRATION_TYPE.getPath() + "'"); + configData.setValue(RegistrationSettings.REGISTRATION_TYPE, registrationType); + configData.setValue(RegistrationSettings.REGISTER_SECOND_ARGUMENT, secondaryArgument); + return true; + } + + /** + * Migrates old permission group settings to the new configurations. + * + * @param reader The property reader + * @param configData Configuration data + * @return True if the configuration has changed, false otherwise + */ + private static boolean mergeAndMovePermissionGroupSettings(PropertyReader reader, ConfigurationData configData) { + boolean performedChanges; + + // We have two old settings replaced by only one: move the first non-empty one + Property oldUnloggedInGroup = newProperty("settings.security.unLoggedinGroup", ""); + Property oldRegisteredGroup = newProperty("GroupOptions.RegisteredPlayerGroup", ""); + if (!oldUnloggedInGroup.determineValue(reader).getValue().isEmpty()) { + performedChanges = moveProperty(oldUnloggedInGroup, PluginSettings.REGISTERED_GROUP, reader, configData); + } else { + performedChanges = moveProperty(oldRegisteredGroup, PluginSettings.REGISTERED_GROUP, reader, configData); + } + + // Move paths of other old options + performedChanges |= moveProperty(newProperty("GroupOptions.UnregisteredPlayerGroup", ""), + PluginSettings.UNREGISTERED_GROUP, reader, configData); + performedChanges |= moveProperty(newProperty("permission.EnablePermissionCheck", false), + PluginSettings.ENABLE_PERMISSION_CHECK, reader, configData); + return performedChanges; + } + + /** + * If a deprecated hash is used, it is added to the legacy hashes option and the active hash + * is changed to SHA256. + * + * @param reader The property reader + * @param configData Configuration data + * @return True if the configuration has changed, false otherwise + */ + private static boolean moveDeprecatedHashAlgorithmIntoLegacySection(PropertyReader reader, + ConfigurationData configData) { + HashAlgorithm currentHash = SecuritySettings.PASSWORD_HASH.determineValue(reader).getValue(); + // Skip CUSTOM (has no class) and PLAINTEXT (is force-migrated later on in the startup process) + if (currentHash != HashAlgorithm.CUSTOM && currentHash != HashAlgorithm.PLAINTEXT) { + Class encryptionClass = currentHash.getClazz(); + if (encryptionClass.isAnnotationPresent(Deprecated.class)) { + configData.setValue(SecuritySettings.PASSWORD_HASH, HashAlgorithm.SHA256); + Set legacyHashes = SecuritySettings.LEGACY_HASHES.determineValue(reader).getValue(); + legacyHashes.add(currentHash); + configData.setValue(SecuritySettings.LEGACY_HASHES, legacyHashes); + logger.warning("The hash algorithm '" + currentHash + + "' is no longer supported for active use. New hashes will be in SHA256."); + return true; + } + } + return false; + } + + /** + * Moves the property for the password salt column name to the same path as all other column name properties. + * + * @param reader The property reader + * @param configData Configuration data + * @return True if the configuration has changed, false otherwise + */ + private static boolean moveSaltColumnConfigWithOtherColumnConfigs(PropertyReader reader, + ConfigurationData configData) { + Property oldProperty = newProperty("ExternalBoardOptions.mySQLColumnSalt", + DatabaseSettings.MYSQL_COL_SALT.getDefaultValue()); + return moveProperty(oldProperty, DatabaseSettings.MYSQL_COL_SALT, reader, configData); + } + + /** + * Retrieves the old config to run a command when alt accounts are detected and sets them to this instance + * for further processing. + * + * @param reader The property reader + */ + private void setOldOtherAccountsCommandFieldsIfSet(PropertyReader reader) { + Property commandProperty = newProperty("settings.restrictions.otherAccountsCmd", ""); + Property commandThresholdProperty = newProperty("settings.restrictions.otherAccountsCmdThreshold", 0); + + PropertyValue commandPropValue = commandProperty.determineValue(reader); + int commandThreshold = commandThresholdProperty.determineValue(reader).getValue(); + if (commandPropValue.isValidInResource() && commandThreshold >= 2) { + oldOtherAccountsCommand = commandPropValue.getValue(); + oldOtherAccountsCommandThreshold = commandThreshold; + } + } + + /** + * Checks for an old property path and moves it to a new path if it is present and the new path is not yet set. + * + * @param oldProperty The old property (create a temporary {@link Property} object with the path) + * @param newProperty The new property to move the value to + * @param reader The property reader + * @param configData Configuration data + * @param The type of the property + * @return True if a migration has been done, false otherwise + */ + protected static boolean moveProperty(Property oldProperty, + Property newProperty, + PropertyReader reader, + ConfigurationData configData) { + PropertyValue oldPropertyValue = oldProperty.determineValue(reader); + if (oldPropertyValue.isValidInResource()) { + if (reader.contains(newProperty.getPath())) { + logger.info("Detected deprecated property " + oldProperty.getPath()); + } else { + logger.info("Renaming " + oldProperty.getPath() + " to " + newProperty.getPath()); + configData.setValue(newProperty, oldPropertyValue.getValue()); + } + return true; + } + return false; + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/SettingsWarner.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/SettingsWarner.java new file mode 100644 index 00000000..c63ffe71 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/SettingsWarner.java @@ -0,0 +1,90 @@ +package fr.xephi.authme.settings; + +import fr.xephi.authme.AuthMe; +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.security.HashAlgorithm; +import fr.xephi.authme.security.crypts.Argon2; +import fr.xephi.authme.service.BukkitService; +import fr.xephi.authme.settings.properties.EmailSettings; +import fr.xephi.authme.settings.properties.HooksSettings; +import fr.xephi.authme.settings.properties.PluginSettings; +import fr.xephi.authme.settings.properties.RestrictionSettings; +import fr.xephi.authme.settings.properties.SecuritySettings; + +import javax.inject.Inject; +import java.util.Optional; + +/** + * Logs warning messages in cases where the configured values suggest a misconfiguration. + *

+ * Note that this class does not modify any settings and it is called after the settings have been fully loaded. + * For actual migrations (= verifications which trigger changes and a resave of the settings), + * see {@link SettingsMigrationService}. + */ +public class SettingsWarner { + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(SettingsWarner.class); + + @Inject + private Settings settings; + + @Inject + private AuthMe authMe; + + @Inject + private BukkitService bukkitService; + + SettingsWarner() { + } + + /** + * Logs warning when necessary to notify the user about misconfigurations. + */ + public void logWarningsForMisconfigurations() { + // Force single session disabled + if (!settings.getProperty(RestrictionSettings.FORCE_SINGLE_SESSION)) { + logger.warning("WARNING!!! By disabling ForceSingleSession, your server protection is inadequate!"); + } + + // Use TLS property only affects port 25 + if (!settings.getProperty(EmailSettings.PORT25_USE_TLS) + && settings.getProperty(EmailSettings.SMTP_PORT) != 25) { + logger.warning("Note: You have set Email.useTls to false but this only affects mail over port 25"); + } + + // Output hint if sessions are enabled that the timeout must be positive + if (settings.getProperty(PluginSettings.SESSIONS_ENABLED) + && settings.getProperty(PluginSettings.SESSIONS_TIMEOUT) <= 0) { + logger.warning("Warning: Session timeout needs to be positive in order to work!"); + } + + // Warn if spigot.yml has settings.bungeecord set to true but config.yml has Hooks.bungeecord set to false + if (isTrue(bukkitService.isBungeeCordConfiguredForSpigot()) + && !settings.getProperty(HooksSettings.BUNGEECORD) && !settings.getProperty(HooksSettings.VELOCITY)) { + logger.warning("Note: Hooks.bungeecord is set to false but your server appears to be running in" + + " bungeecord mode (see your spigot.yml). In order to allow the datasource caching and the" + + " AuthMeBungee add-on to work properly you have to enable this option!"); + } + + if (!isTrue(bukkitService.isBungeeCordConfiguredForSpigot()) + && settings.getProperty(HooksSettings.BUNGEECORD)) { + logger.warning("Note: Hooks.bungeecord is set to true but your server appears to be running in" + + " non-bungeecord mode (see your spigot.yml). In order to prevent untrusted payload attack, " + + "BungeeCord hook will be automatically disabled!"); + } + + + // Check if argon2 library is present and can be loaded + if (settings.getProperty(SecuritySettings.PASSWORD_HASH).equals(HashAlgorithm.ARGON2) + && !Argon2.isLibraryLoaded()) { + logger.warning("WARNING!!! You use Argon2 Hash Algorithm method but we can't find the Argon2 " + + "library on your system! See https://github.com/AuthMe/AuthMeReloaded/wiki/Argon2-as-Password-Hash"); + authMe.stopOrUnload(); + } + } + + private static boolean isTrue(Optional value) { + return value.isPresent() && value.get(); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/SpawnLoader.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/SpawnLoader.java new file mode 100644 index 00000000..171a999b --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/SpawnLoader.java @@ -0,0 +1,378 @@ +package fr.xephi.authme.settings; + +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.initialization.DataFolder; +import fr.xephi.authme.initialization.Reloadable; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.service.PluginHookService; +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; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.World; +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; +import java.util.Locale; + +/** + * Manager for spawn points. It loads spawn definitions from AuthMe and third-party plugins + * and is responsible for returning the correct spawn point as per the settings. + *

+ * The spawn priority setting defines from which sources and in which order the spawn point + * 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 Reloadable { + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(SpawnLoader.class); + + private final File authMeConfigurationFile; + private final Settings settings; + private final PluginHookService pluginHookService; + private Configuration authMeConfiguration; + private String[] spawnPriority; + private Location essentialsSpawn; + private Location cmiSpawn; + + /** + * Constructor. + * + * @param pluginFolder The AuthMe data folder + * @param settings The setting instance + * @param pluginHookService The plugin hooks instance + */ + @Inject + SpawnLoader(@DataFolder File pluginFolder, Settings settings, PluginHookService pluginHookService) { + File spawnFile = new File(pluginFolder, "spawn.yml"); + FileUtils.copyFileFromResource(spawnFile, "spawn.yml"); + this.authMeConfigurationFile = spawnFile; + this.settings = settings; + this.pluginHookService = pluginHookService; + reload(); + } + + /** + * (Re)loads the spawn file and relevant settings. + */ + @Override + public void reload() { + spawnPriority = settings.getProperty(RestrictionSettings.SPAWN_PRIORITY).split(","); + authMeConfiguration = YamlConfiguration.loadConfiguration(authMeConfigurationFile); + loadEssentialsSpawn(); + } + + /** + * Return the AuthMe spawn location. + * + * @return The location of the regular AuthMe spawn point + */ + public Location getSpawn() { + return getLocationFromConfiguration(authMeConfiguration, "spawn"); + } + + /** + * Set the AuthMe spawn point. + * + * @param location The location to use + * + * @return True upon success, false otherwise + */ + public boolean setSpawn(Location location) { + return setLocation("spawn", location); + } + + /** + * Return the AuthMe first spawn location. + * + * @return The location of the AuthMe spawn point for first timers + */ + public Location getFirstSpawn() { + return getLocationFromConfiguration(authMeConfiguration, "firstspawn"); + } + + /** + * Set the AuthMe first spawn location. + * + * @param location The location to use + * + * @return True upon success, false otherwise + */ + public boolean setFirstSpawn(Location location) { + return setLocation("firstspawn", location); + } + + /** + * Load the spawn point defined in EssentialsSpawn. + */ + public void loadEssentialsSpawn() { + // EssentialsSpawn cannot run without Essentials, so it's fine to get the Essentials data folder + File essentialsFolder = pluginHookService.getEssentialsDataFolder(); + if (essentialsFolder == null) { + return; + } + + File essentialsSpawnFile = new File(essentialsFolder, "spawn.yml"); + if (essentialsSpawnFile.exists()) { + essentialsSpawn = getLocationFromConfiguration( + YamlConfiguration.loadConfiguration(essentialsSpawnFile), "spawns.default"); + } else { + essentialsSpawn = null; + logger.info("Essentials spawn file not found: '" + essentialsSpawnFile.getAbsolutePath() + "'"); + } + } + + /** + * Unset the spawn point defined in EssentialsSpawn. + */ + public void unloadEssentialsSpawn() { + essentialsSpawn = null; + } + + /** + * Load the spawn point defined in CMI. + */ + public void loadCmiSpawn() { + File cmiFolder = pluginHookService.getCmiDataFolder(); + if (cmiFolder == null) { + return; + } + + File cmiConfig = new File(cmiFolder, "config.yml"); + if (cmiConfig.exists()) { + cmiSpawn = getLocationFromCmiConfiguration(YamlConfiguration.loadConfiguration(cmiConfig)); + } else { + cmiSpawn = null; + logger.info("CMI config file not found: '" + cmiConfig.getAbsolutePath() + "'"); + } + } + + /** + * Unset the spawn point defined in CMI. + */ + public void unloadCmiSpawn() { + cmiSpawn = null; + } + + /** + * Return the spawn location for the given player. The source of the spawn location varies + * depending on the spawn priority setting. + * + * @param player The player to retrieve the spawn point for + * + * @return The spawn location, or the default spawn location upon failure + * + * @see RestrictionSettings#SPAWN_PRIORITY + */ + public Location getSpawnLocation(Player player) { + if (player == null || player.getWorld() == null) { + return null; + } + + World world = player.getWorld(); + Location spawnLoc = null; + for (String priority : spawnPriority) { + switch (priority.toLowerCase(Locale.ROOT).trim()) { + case "default": + if (world.getSpawnLocation() != null) { + if (!isValidSpawnPoint(world.getSpawnLocation())) { + for (World spawnWorld : Bukkit.getWorlds()) { + if (isValidSpawnPoint(spawnWorld.getSpawnLocation())) { + world = spawnWorld; + break; + } + } + logger.warning("Seems like AuthMe is unable to find a proper spawn location. " + + "Set a location with the command '/authme setspawn'"); + } + spawnLoc = world.getSpawnLocation(); + } + break; + case "multiverse": + if (settings.getProperty(HooksSettings.MULTIVERSE)) { + spawnLoc = pluginHookService.getMultiverseSpawn(world); + } + break; + case "essentials": + spawnLoc = essentialsSpawn; + break; + case "cmi": + spawnLoc = cmiSpawn; + break; + case "authme": + spawnLoc = getSpawn(); + break; + default: + // ignore + } + if (spawnLoc != null) { + logger.debug("Spawn location determined as `{0}` for world `{1}`", spawnLoc, world.getName()); + return spawnLoc; + } + } + logger.debug("Fall back to default world spawn location. World: `{0}`", world.getName()); + + return world.getSpawnLocation(); // return default location + } + + /** + * Checks if a given location is a valid spawn point [!= (0,0,0)]. + * + * @param location The location to check + * + * @return True upon success, false otherwise + */ + private boolean isValidSpawnPoint(Location location) { + if (location.getX() == 0 && location.getY() == 0 && location.getZ() == 0) { + return false; + } + return true; + } + + /** + * Save the location under the given prefix. + * + * @param prefix The prefix to save the spawn under + * @param location The location to persist + * + * @return True upon success, false otherwise + */ + private boolean setLocation(String prefix, Location location) { + if (location != null && location.getWorld() != null) { + authMeConfiguration.set(prefix + ".world", location.getWorld().getName()); + authMeConfiguration.set(prefix + ".x", location.getX()); + authMeConfiguration.set(prefix + ".y", location.getY()); + authMeConfiguration.set(prefix + ".z", location.getZ()); + authMeConfiguration.set(prefix + ".yaw", location.getYaw()); + authMeConfiguration.set(prefix + ".pitch", location.getPitch()); + return saveAuthMeConfig(); + } + return false; + } + + private boolean saveAuthMeConfig() { + try { + authMeConfiguration.save(authMeConfigurationFile); + return true; + } catch (IOException e) { + logger.logException("Could not save spawn config (" + authMeConfigurationFile + ")", e); + } + return false; + } + + /** + * Return player's location if player is alive, or player's spawn location if dead. + * + * @param player player to retrieve + * + * @return location of the given player if alive, spawn location if dead. + */ + public Location getPlayerLocationOrSpawn(Player player) { + if (player.getHealth() <= 0.0) { + return getSpawnLocation(player); + } + return player.getLocation(); + } + + /** + * Build a {@link Location} object from the given path in the file configuration. + * + * @param configuration The file configuration to read from + * @param pathPrefix The path to get the spawn point from + * + * @return Location corresponding to the values in the path + */ + private static Location getLocationFromConfiguration(FileConfiguration configuration, String pathPrefix) { + if (containsAllSpawnFields(configuration, pathPrefix)) { + String prefix = pathPrefix + "."; + String worldName = configuration.getString(prefix + "world"); + World world = Bukkit.getWorld(worldName); + if (!StringUtils.isBlank(worldName) && world != null) { + return new Location(world, configuration.getDouble(prefix + "x"), + configuration.getDouble(prefix + "y"), configuration.getDouble(prefix + "z"), + getFloat(configuration, prefix + "yaw"), getFloat(configuration, prefix + "pitch")); + } + } + return null; + } + + /** + * Build a {@link Location} object based on the CMI configuration. + * + * @param configuration The CMI file configuration to read from + * + * @return Location corresponding to the values in the path + */ + private static Location getLocationFromCmiConfiguration(FileConfiguration configuration) { + final String pathPrefix = "Spawn.Main"; + if (isLocationCompleteInCmiConfig(configuration, pathPrefix)) { + String prefix = pathPrefix + "."; + String worldName = configuration.getString(prefix + "World"); + World world = Bukkit.getWorld(worldName); + if (!StringUtils.isBlank(worldName) && world != null) { + return new Location(world, configuration.getDouble(prefix + "X"), + configuration.getDouble(prefix + "Y"), configuration.getDouble(prefix + "Z"), + getFloat(configuration, prefix + "Yaw"), getFloat(configuration, prefix + "Pitch")); + } + } + return null; + } + + /** + * Return whether the file configuration contains all fields necessary to define a spawn + * under the given path. + * + * @param configuration The file configuration to use + * @param pathPrefix The path to verify + * + * @return True if all spawn fields are present, false otherwise + */ + private static boolean containsAllSpawnFields(FileConfiguration configuration, String pathPrefix) { + String[] fields = {"world", "x", "y", "z", "yaw", "pitch"}; + for (String field : fields) { + if (!configuration.contains(pathPrefix + "." + field)) { + return false; + } + } + return true; + } + + /** + * Return whether the CMI file configuration contains all spawn fields under the given path. + * + * @param cmiConfiguration The file configuration from CMI + * @param pathPrefix The path to verify + * + * @return True if all spawn fields are present, false otherwise + */ + private static boolean isLocationCompleteInCmiConfig(FileConfiguration cmiConfiguration, String pathPrefix) { + String[] fields = {"World", "X", "Y", "Z", "Yaw", "Pitch"}; + for (String field : fields) { + if (!cmiConfiguration.contains(pathPrefix + "." + field)) { + return false; + } + } + return true; + } + + /** + * Retrieve a property as a float from the given file configuration. + * + * @param configuration The file configuration to use + * @param path The path of the property to retrieve + * + * @return The float + */ + private static float getFloat(FileConfiguration configuration, String path) { + Object value = configuration.get(path); + // This behavior is consistent with FileConfiguration#getDouble + return (value instanceof Number) ? ((Number) value).floatValue() : 0; + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/commandconfig/Command.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/commandconfig/Command.java new file mode 100644 index 00000000..5ac922b1 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/commandconfig/Command.java @@ -0,0 +1,67 @@ +package fr.xephi.authme.settings.commandconfig; + +/** + * Command to be run. + */ +public class Command { + + /** The command to execute. */ + private String command; + /** The executor of the command. */ + private Executor executor = Executor.PLAYER; + /** Delay before executing the command (in ticks) */ + private long delay = 0; + + /** + * Default constructor (for bean mapping). + */ + public Command() { + } + + /** + * Creates a copy of this Command object, setting the given command text on the copy. + * + * @param command the command text to use in the copy + * @return copy of the source with the new command + */ + public Command copyWithCommand(String command) { + Command copy = new Command(); + setValuesToCopyWithNewCommand(copy, command); + return copy; + } + + protected void setValuesToCopyWithNewCommand(Command copy, String newCommand) { + copy.command = newCommand; + copy.executor = this.executor; + copy.delay = this.delay; + } + + public String getCommand() { + return command; + } + + public void setCommand(String command) { + this.command = command; + } + + public Executor getExecutor() { + return executor; + } + + public void setExecutor(Executor executor) { + this.executor = executor; + } + + public long getDelay() { + return delay; + } + + public void setDelay(long delay) { + this.delay = delay; + } + + @Override + public String toString() { + return command + " (" + executor + ")"; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/commandconfig/CommandConfig.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/commandconfig/CommandConfig.java new file mode 100644 index 00000000..29484ccd --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/commandconfig/CommandConfig.java @@ -0,0 +1,76 @@ +package fr.xephi.authme.settings.commandconfig; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Command configuration. + * + * @see CommandManager + */ +public class CommandConfig { + + private Map onJoin = new LinkedHashMap<>(); + private Map onLogin = new LinkedHashMap<>(); + private Map onSessionLogin = new LinkedHashMap<>(); + private Map onFirstLogin = new LinkedHashMap<>(); + private Map onRegister = new LinkedHashMap<>(); + private Map onUnregister = new LinkedHashMap<>(); + private Map onLogout = new LinkedHashMap<>(); + + public Map getOnJoin() { + return onJoin; + } + + public void setOnJoin(Map onJoin) { + this.onJoin = onJoin; + } + + public Map getOnLogin() { + return onLogin; + } + + public void setOnLogin(Map onLogin) { + this.onLogin = onLogin; + } + + public Map getOnSessionLogin() { + return onSessionLogin; + } + + public void setOnSessionLogin(Map onSessionLogin) { + this.onSessionLogin = onSessionLogin; + } + + public Map getOnFirstLogin() { + return onFirstLogin; + } + + public void setOnFirstLogin(Map onFirstLogin) { + this.onFirstLogin = onFirstLogin; + } + + public Map getOnRegister() { + return onRegister; + } + + public void setOnRegister(Map onRegister) { + this.onRegister = onRegister; + } + + public Map getOnUnregister() { + return onUnregister; + } + + public void setOnUnregister(Map onUnregister) { + this.onUnregister = onUnregister; + } + + public Map getOnLogout() { + return onLogout; + } + + public void setOnLogout(Map onLogout) { + this.onLogout = onLogout; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/commandconfig/CommandManager.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/commandconfig/CommandManager.java new file mode 100644 index 00000000..86f06aa8 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/commandconfig/CommandManager.java @@ -0,0 +1,192 @@ +package fr.xephi.authme.settings.commandconfig; + +import ch.jalu.configme.SettingsManager; +import ch.jalu.configme.SettingsManagerBuilder; +import fr.xephi.authme.initialization.DataFolder; +import fr.xephi.authme.initialization.Reloadable; +import fr.xephi.authme.service.BukkitService; +import fr.xephi.authme.service.GeoIpService; +import fr.xephi.authme.service.yaml.YamlFileResourceProvider; +import fr.xephi.authme.util.FileUtils; +import fr.xephi.authme.util.PlayerUtils; +import fr.xephi.authme.util.lazytags.Tag; +import fr.xephi.authme.util.lazytags.WrappedTagReplacer; +import org.bukkit.entity.Player; + +import javax.inject.Inject; +import java.io.File; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.function.Predicate; + +import static fr.xephi.authme.util.lazytags.TagBuilder.createTag; + +/** + * Manages configurable commands to be run when various events occur. + */ +public class CommandManager implements Reloadable { + + private final File dataFolder; + private final BukkitService bukkitService; + private final GeoIpService geoIpService; + private final CommandMigrationService commandMigrationService; + private final List> availableTags = buildAvailableTags(); + + private WrappedTagReplacer onJoinCommands; + private WrappedTagReplacer onLoginCommands; + private WrappedTagReplacer onSessionLoginCommands; + private WrappedTagReplacer onFirstLoginCommands; + private WrappedTagReplacer onRegisterCommands; + private WrappedTagReplacer onUnregisterCommands; + private WrappedTagReplacer onLogoutCommands; + + @Inject + CommandManager(@DataFolder File dataFolder, BukkitService bukkitService, GeoIpService geoIpService, + CommandMigrationService commandMigrationService) { + this.dataFolder = dataFolder; + this.bukkitService = bukkitService; + this.geoIpService = geoIpService; + this.commandMigrationService = commandMigrationService; + reload(); + } + + /** + * Runs the configured commands for when a player has joined. + * + * @param player the joining player + */ + public void runCommandsOnJoin(Player player) { + executeCommands(player, onJoinCommands.getAdaptedItems(player)); + } + + /** + * Runs the configured commands for when a player has successfully registered. + * + * @param player the player who has registered + */ + public void runCommandsOnRegister(Player player) { + executeCommands(player, onRegisterCommands.getAdaptedItems(player)); + } + + /** + * Runs the configured commands for when a player has logged in successfully. + * + * @param player the player that logged in + * @param otherAccounts account names whose IP is the same as the player's + */ + public void runCommandsOnLogin(Player player, List otherAccounts) { + final int numberOfOtherAccounts = otherAccounts.size(); + executeCommands(player, onLoginCommands.getAdaptedItems(player), + cmd -> shouldCommandBeRun(cmd, numberOfOtherAccounts)); + } + + /** + * Runs the configured commands for when a player has logged in successfully due to session. + * + * @param player the player that logged in + */ + public void runCommandsOnSessionLogin(Player player) { + executeCommands(player, onSessionLoginCommands.getAdaptedItems(player)); + } + + /** + * Runs the configured commands for when a player logs in the first time. + * + * @param player the player that has logged in for the first time + * @param otherAccounts account names whose IP is the same as the player's + */ + public void runCommandsOnFirstLogin(Player player, List otherAccounts) { + final int numberOfOtherAccounts = otherAccounts.size(); + executeCommands(player, onFirstLoginCommands.getAdaptedItems(player), + cmd -> shouldCommandBeRun(cmd, numberOfOtherAccounts)); + } + + /** + * Runs the configured commands for when a player has been unregistered. + * + * @param player the player that has been unregistered + */ + public void runCommandsOnUnregister(Player player) { + executeCommands(player, onUnregisterCommands.getAdaptedItems(player)); + } + + /** + * Runs the configured commands for when a player logs out (by command or by quitting the server). + * + * @param player the player that is no longer logged in + */ + public void runCommandsOnLogout(Player player) { + executeCommands(player, onLogoutCommands.getAdaptedItems(player)); + } + + private void executeCommands(Player player, List commands) { + executeCommands(player, commands, c -> true); + } + + private void executeCommands(Player player, List commands, Predicate predicate) { + for (T cmd : commands) { + if (predicate.test(cmd)) { + long delay = cmd.getDelay(); + if (delay > 0) { + bukkitService.runTaskLater(player, () -> dispatchCommand(player, cmd), delay); + } else { + bukkitService.runTaskIfFolia(player, () -> dispatchCommand(player, cmd)); + } + } + } + } + + private void dispatchCommand(Player player, Command command) { + if (Executor.CONSOLE.equals(command.getExecutor())) { + bukkitService.dispatchConsoleCommand(command.getCommand()); + } else { + bukkitService.dispatchCommand(player, command.getCommand()); + } + } + + private static boolean shouldCommandBeRun(OnLoginCommand command, int numberOfOtherAccounts) { + return (!command.getIfNumberOfAccountsAtLeast().isPresent() + || command.getIfNumberOfAccountsAtLeast().get() <= numberOfOtherAccounts) + && (!command.getIfNumberOfAccountsLessThan().isPresent() + || command.getIfNumberOfAccountsLessThan().get() > numberOfOtherAccounts); + } + + @Override + public void reload() { + File file = new File(dataFolder, "commands.yml"); + FileUtils.copyFileFromResource(file, "commands.yml"); + + SettingsManager settingsManager = SettingsManagerBuilder + .withResource(YamlFileResourceProvider.loadFromFile(file)) + .configurationData(CommandSettingsHolder.class) + .migrationService(commandMigrationService) + .create(); + CommandConfig commandConfig = settingsManager.getProperty(CommandSettingsHolder.COMMANDS); + onJoinCommands = newReplacer(commandConfig.getOnJoin()); + onLoginCommands = newOnLoginCmdReplacer(commandConfig.getOnLogin()); + onFirstLoginCommands = newOnLoginCmdReplacer(commandConfig.getOnFirstLogin()); + onSessionLoginCommands = newReplacer(commandConfig.getOnSessionLogin()); + onRegisterCommands = newReplacer(commandConfig.getOnRegister()); + onUnregisterCommands = newReplacer(commandConfig.getOnUnregister()); + onLogoutCommands = newReplacer(commandConfig.getOnLogout()); + } + + private WrappedTagReplacer newReplacer(Map commands) { + return new WrappedTagReplacer<>(availableTags, commands.values(), Command::getCommand, + Command::copyWithCommand); + } + + private WrappedTagReplacer newOnLoginCmdReplacer(Map commands) { + return new WrappedTagReplacer<>(availableTags, commands.values(), Command::getCommand, + OnLoginCommand::copyWithCommand); + } + + private List> buildAvailableTags() { + return Arrays.asList( + createTag("%p", Player::getName), + createTag("%nick", Player::getDisplayName), + createTag("%ip", PlayerUtils::getPlayerIp), + createTag("%country", pl -> geoIpService.getCountryName(PlayerUtils.getPlayerIp(pl)))); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/commandconfig/CommandMigrationService.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/commandconfig/CommandMigrationService.java new file mode 100644 index 00000000..0822616d --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/commandconfig/CommandMigrationService.java @@ -0,0 +1,66 @@ +package fr.xephi.authme.settings.commandconfig; + +import ch.jalu.configme.configurationdata.ConfigurationData; +import ch.jalu.configme.migration.MigrationService; +import ch.jalu.configme.resource.PropertyReader; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableList; +import fr.xephi.authme.settings.SettingsMigrationService; +import fr.xephi.authme.util.RandomStringUtils; + +import javax.inject.Inject; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * Migrates the commands from their old location, in config.yml, to the dedicated commands configuration file. + */ +class CommandMigrationService implements MigrationService { + + /** List of all properties in {@link CommandConfig}. */ + @VisibleForTesting + static final List COMMAND_CONFIG_PROPERTIES = ImmutableList.of( + "onJoin", "onLogin", "onSessionLogin", "onFirstLogin", "onRegister", "onUnregister", "onLogout"); + + @Inject + private SettingsMigrationService settingsMigrationService; + + CommandMigrationService() { + } + + @Override + public boolean checkAndMigrate(PropertyReader reader, ConfigurationData configurationData) { + final CommandConfig commandConfig = CommandSettingsHolder.COMMANDS.determineValue(reader).getValue(); + if (moveOtherAccountsConfig(commandConfig) || isAnyCommandMissing(reader)) { + configurationData.setValue(CommandSettingsHolder.COMMANDS, commandConfig); + return true; + } + return false; + } + + private boolean moveOtherAccountsConfig(CommandConfig commandConfig) { + if (settingsMigrationService.hasOldOtherAccountsCommand()) { + OnLoginCommand command = new OnLoginCommand(); + command.setCommand(replaceOldPlaceholdersWithNew(settingsMigrationService.getOldOtherAccountsCommand())); + command.setExecutor(Executor.CONSOLE); + command.setIfNumberOfAccountsAtLeast( + Optional.of(settingsMigrationService.getOldOtherAccountsCommandThreshold())); + + Map onLoginCommands = commandConfig.getOnLogin(); + onLoginCommands.put(RandomStringUtils.generate(10), command); + return true; + } + return false; + } + + private static String replaceOldPlaceholdersWithNew(String oldOtherAccountsCommand) { + return oldOtherAccountsCommand + .replace("%playername%", "%p") + .replace("%playerip%", "%ip"); + } + + private static boolean isAnyCommandMissing(PropertyReader reader) { + return COMMAND_CONFIG_PROPERTIES.stream().anyMatch(property -> reader.getObject(property) == null); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/commandconfig/CommandSettingsHolder.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/commandconfig/CommandSettingsHolder.java new file mode 100644 index 00000000..ab8e9849 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/commandconfig/CommandSettingsHolder.java @@ -0,0 +1,77 @@ +package fr.xephi.authme.settings.commandconfig; + +import ch.jalu.configme.SettingsHolder; +import ch.jalu.configme.configurationdata.CommentsConfiguration; +import ch.jalu.configme.properties.BeanProperty; +import ch.jalu.configme.properties.Property; + +/** + * Settings holder class for the commands.yml settings. + */ +public final class CommandSettingsHolder implements SettingsHolder { + + public static final Property COMMANDS = + new BeanProperty<>(CommandConfig.class, "", new CommandConfig()); + + private CommandSettingsHolder() { + } + + @Override + public void registerComments(CommentsConfiguration conf) { + String[] rootComments = { + "This configuration file allows you to execute commands on various events.", + "Supported placeholders in commands:", + " %p is replaced with the player name.", + " %nick is replaced with the player's nick name", + " %ip is replaced with the player's IP address", + " %country is replaced with the player's country", + "", + "For example, if you want to send a welcome message to a player who just registered:", + "onRegister:", + " welcome:", + " command: 'msg %p Welcome to the server!'", + " executor: CONSOLE", + "", + "This will make the console execute the msg command to the player.", + "Each command under an event has a name you can choose freely (e.g. 'welcome' as above),", + "after which a mandatory 'command' field defines the command to run,", + "and 'executor' defines who will run the command (either PLAYER or CONSOLE). Longer example:", + "onLogin:", + " welcome:", + " command: 'msg %p Welcome back!'", + " executor: PLAYER", + " broadcast:", + " command: 'broadcast %p has joined, welcome back!'", + " executor: CONSOLE", + "", + "You can also add delay to command. It will run after the specified ticks. Example:", + "onLogin:", + " rules:", + " command: 'rules'", + " executor: PLAYER", + " delay: 200", + "", + "Supported command events: onLogin, onSessionLogin, onFirstLogin, onJoin, onLogout, onRegister, " + + "onUnregister", + "", + "For onLogin and onFirstLogin, you can use 'ifNumberOfAccountsLessThan' and 'ifNumberOfAccountsAtLeast'", + "to specify limits to how many accounts a player can have (matched by IP) for a command to be run:", + "onLogin:", + " warnOnManyAccounts:", + " command: 'say Uh oh! %p has many alt accounts!'", + " executor: CONSOLE", + " ifNumberOfAccountsAtLeast: 5" + }; + + conf.setComment("", rootComments); + conf.setComment("onFirstLogin", + "Commands to run for players logging in whose 'last login date' was empty"); + conf.setComment("onUnregister", + "Commands to run whenever a player is unregistered (by himself, or by an admin)"); + conf.setComment("onLogout", + "These commands are called whenever a logged in player uses /logout or quits.", + "The commands are not run if a player that was not logged in quits the server.", + "Note: if your server crashes, these commands won't be run, so don't rely on them to undo", + "'onLogin' commands that would be dangerous for non-logged in players to have!"); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/commandconfig/Executor.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/commandconfig/Executor.java new file mode 100644 index 00000000..c7043de2 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/commandconfig/Executor.java @@ -0,0 +1,14 @@ +package fr.xephi.authme.settings.commandconfig; + +/** + * The executor of the command. + */ +public enum Executor { + + /** The player of the event. */ + PLAYER, + + /** The console user. */ + CONSOLE + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/commandconfig/OnLoginCommand.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/commandconfig/OnLoginCommand.java new file mode 100644 index 00000000..13cc30fc --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/commandconfig/OnLoginCommand.java @@ -0,0 +1,49 @@ +package fr.xephi.authme.settings.commandconfig; + +import java.util.Optional; + +/** + * Configurable command for when a player logs in. + */ +public class OnLoginCommand extends Command { + + private Optional ifNumberOfAccountsAtLeast = Optional.empty(); + private Optional ifNumberOfAccountsLessThan = Optional.empty(); + + /** + * Default constructor (for bean mapping). + */ + public OnLoginCommand() { + } + + /** + * Creates a copy of this object, using the given command as new {@link Command#command command}. + * + * @param command the command text to use in the copy + * @return copy of the source with the new command + */ + @Override + public OnLoginCommand copyWithCommand(String command) { + OnLoginCommand copy = new OnLoginCommand(); + setValuesToCopyWithNewCommand(copy, command); + copy.ifNumberOfAccountsAtLeast = this.ifNumberOfAccountsAtLeast; + copy.ifNumberOfAccountsLessThan = this.ifNumberOfAccountsLessThan; + return copy; + } + + public Optional getIfNumberOfAccountsAtLeast() { + return ifNumberOfAccountsAtLeast; + } + + public void setIfNumberOfAccountsAtLeast(Optional ifNumberOfAccountsAtLeast) { + this.ifNumberOfAccountsAtLeast = ifNumberOfAccountsAtLeast; + } + + public Optional getIfNumberOfAccountsLessThan() { + return ifNumberOfAccountsLessThan; + } + + public void setIfNumberOfAccountsLessThan(Optional ifNumberOfAccountsLessThan) { + this.ifNumberOfAccountsLessThan = ifNumberOfAccountsLessThan; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/properties/AuthMeSettingsRetriever.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/properties/AuthMeSettingsRetriever.java new file mode 100644 index 00000000..c3793485 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/properties/AuthMeSettingsRetriever.java @@ -0,0 +1,28 @@ +package fr.xephi.authme.settings.properties; + +import ch.jalu.configme.SettingsHolder; +import ch.jalu.configme.configurationdata.ConfigurationData; +import ch.jalu.configme.configurationdata.ConfigurationDataBuilder; +import ch.jalu.configme.properties.Property; + +/** + * Utility class responsible for retrieving all {@link Property} fields from {@link SettingsHolder} classes. + */ +public final class AuthMeSettingsRetriever { + + private AuthMeSettingsRetriever() { + } + + /** + * Builds the configuration data for all property fields in AuthMe {@link SettingsHolder} classes. + * + * @return configuration data + */ + public static ConfigurationData buildConfigurationData() { + return ConfigurationDataBuilder.createConfiguration( + DatabaseSettings.class, PluginSettings.class, RestrictionSettings.class, + EmailSettings.class, HooksSettings.class, ProtectionSettings.class, + PurgeSettings.class, SecuritySettings.class, RegistrationSettings.class, + LimboSettings.class, BackupSettings.class, ConverterSettings.class); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/properties/BackupSettings.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/properties/BackupSettings.java new file mode 100644 index 00000000..190c5b7d --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/properties/BackupSettings.java @@ -0,0 +1,29 @@ +package fr.xephi.authme.settings.properties; + +import ch.jalu.configme.Comment; +import ch.jalu.configme.SettingsHolder; +import ch.jalu.configme.properties.Property; + +import static ch.jalu.configme.properties.PropertyInitializer.newProperty; + +public final class BackupSettings implements SettingsHolder { + + @Comment("General configuration for backups: if false, no backups are possible") + public static final Property ENABLED = + newProperty("BackupSystem.ActivateBackup", false); + + @Comment("Create backup at every start of server") + public static final Property ON_SERVER_START = + newProperty("BackupSystem.OnServerStart", false); + + @Comment("Create backup at every stop of server") + public static final Property ON_SERVER_STOP = + newProperty("BackupSystem.OnServerStop", true); + + @Comment("Windows only: MySQL installation path") + public static final Property MYSQL_WINDOWS_PATH = + newProperty("BackupSystem.MysqlWindowsPath", "C:\\Program Files\\MySQL\\MySQL Server 5.1\\"); + + private BackupSettings() { + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/properties/ConverterSettings.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/properties/ConverterSettings.java new file mode 100644 index 00000000..2f677827 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/properties/ConverterSettings.java @@ -0,0 +1,43 @@ +package fr.xephi.authme.settings.properties; + +import ch.jalu.configme.Comment; +import ch.jalu.configme.SettingsHolder; +import ch.jalu.configme.configurationdata.CommentsConfiguration; +import ch.jalu.configme.properties.Property; + +import static ch.jalu.configme.properties.PropertyInitializer.newProperty; + +public final class ConverterSettings implements SettingsHolder { + @Comment("CrazyLogin database file name") + public static final Property CRAZYLOGIN_FILE_NAME = + newProperty("Converter.CrazyLogin.fileName", "accounts.db"); + + @Comment("LoginSecurity: convert from SQLite; if false we use MySQL") + public static final Property LOGINSECURITY_USE_SQLITE = + newProperty("Converter.loginSecurity.useSqlite", true); + + @Comment("LoginSecurity MySQL: database host") + public static final Property LOGINSECURITY_MYSQL_HOST = + newProperty("Converter.loginSecurity.mySql.host", ""); + + @Comment("LoginSecurity MySQL: database name") + public static final Property LOGINSECURITY_MYSQL_DATABASE = + newProperty("Converter.loginSecurity.mySql.database", ""); + + @Comment("LoginSecurity MySQL: database user") + public static final Property LOGINSECURITY_MYSQL_USER = + newProperty("Converter.loginSecurity.mySql.user", ""); + + @Comment("LoginSecurity MySQL: password for database user") + public static final Property LOGINSECURITY_MYSQL_PASSWORD = + newProperty("Converter.loginSecurity.mySql.password", ""); + + private ConverterSettings() { + } + + @Override + public void registerComments(CommentsConfiguration conf) { + conf.setComment("Converter", + "Converter settings: see https://github.com/AuthMe/AuthMeReloaded/wiki/Converters"); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/properties/DatabaseSettings.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/properties/DatabaseSettings.java new file mode 100644 index 00000000..6103183d --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/properties/DatabaseSettings.java @@ -0,0 +1,172 @@ +package fr.xephi.authme.settings.properties; + +import ch.jalu.configme.Comment; +import ch.jalu.configme.SettingsHolder; +import ch.jalu.configme.properties.Property; +import fr.xephi.authme.datasource.DataSourceType; + +import static ch.jalu.configme.properties.PropertyInitializer.newProperty; + +public final class DatabaseSettings implements SettingsHolder { + + @Comment({"What type of database do you want to use?", + "Valid values: H2, SQLITE, MARIADB, MYSQL, POSTGRESQL"}) + public static final Property BACKEND = + newProperty(DataSourceType.class, "DataSource.backend", DataSourceType.SQLITE); + + @Comment({"Enable the database caching system, should be disabled on bungeecord environments", + "or when a website integration is being used."}) + public static final Property USE_CACHING = + newProperty("DataSource.caching", true); + + @Comment("Should we try to use VirtualThreads(Java 21+) for database cache loader?") + public static final Property USE_VIRTUAL_THREADS = + newProperty("DataSource.useVirtualThreadsCache", false); + + @Comment("Database host address") + public static final Property MYSQL_HOST = + newProperty("DataSource.mySQLHost", "127.0.0.1"); + + @Comment("Database port") + public static final Property MYSQL_PORT = + newProperty("DataSource.mySQLPort", "3306"); + + @Comment({"Replacement of Mysql's useSsl (for MariaDB only).", + "- disable: No SSL", + "- trust: Trust blindly (no validation)", + "- verify_ca: Encryption, certificates validation, BUT no hostname verification", + "- verify_full: Encryption, certificate validation and hostname validation", + "Read more: https://bit.ly/mariadb-sslmode"}) + public static final Property MARIADB_SSL_MODE = + newProperty("DataSource.MariaDbSslMode", "disabled"); + + @Comment({"Connect to MySQL database over SSL", + "If you're using MariaDB, use sslMode instead"}) + public static final Property MYSQL_USE_SSL = + newProperty("DataSource.mySQLUseSSL", true); + + @Comment({"Verification of server's certificate.", + "We would not recommend to set this option to false.", + "Set this option to false at your own risk if and only if you know what you're doing"}) + public static final Property MYSQL_CHECK_SERVER_CERTIFICATE = + newProperty( "DataSource.mySQLCheckServerCertificate", true); + + @Comment({"Authorize client to retrieve RSA server public key.", + "Advanced option, ignore if you don't know what it means.", + "If you are using MariaDB, use MariaDbSslMode instead."}) + public static final Property MYSQL_ALLOW_PUBLIC_KEY_RETRIEVAL = + newProperty( "DataSource.mySQLAllowPublicKeyRetrieval", true); + + @Comment("Username to connect to the MySQL database") + public static final Property MYSQL_USERNAME = + newProperty("DataSource.mySQLUsername", "authme"); + + @Comment("Password to connect to the MySQL database") + public static final Property MYSQL_PASSWORD = + newProperty("DataSource.mySQLPassword", "12345"); + + @Comment("Database Name, use with converters or as SQLITE database name") + public static final Property MYSQL_DATABASE = + newProperty("DataSource.mySQLDatabase", "authme"); + + @Comment("Table of the database") + public static final Property MYSQL_TABLE = + newProperty("DataSource.mySQLTablename", "authme"); + + @Comment("Column of IDs to sort data") + public static final Property MYSQL_COL_ID = + newProperty("DataSource.mySQLColumnId", "id"); + + @Comment("Column for storing or checking players nickname") + public static final Property MYSQL_COL_NAME = + newProperty("DataSource.mySQLColumnName", "username"); + + @Comment("Column for storing or checking players RealName") + public static final Property MYSQL_COL_REALNAME = + newProperty("DataSource.mySQLRealName", "realname"); + + @Comment("Column for storing players passwords") + public static final Property MYSQL_COL_PASSWORD = + newProperty("DataSource.mySQLColumnPassword", "password"); + + @Comment("Column for storing players passwords salts") + public static final Property MYSQL_COL_SALT = + newProperty("DataSource.mySQLColumnSalt", ""); + + @Comment("Column for storing players emails") + public static final Property MYSQL_COL_EMAIL = + newProperty("DataSource.mySQLColumnEmail", "email"); + + @Comment("Column for storing if a player is logged in or not") + public static final Property MYSQL_COL_ISLOGGED = + newProperty("DataSource.mySQLColumnLogged", "isLogged"); + + @Comment("Column for storing if a player has a valid session or not") + public static final Property MYSQL_COL_HASSESSION = + newProperty("DataSource.mySQLColumnHasSession", "hasSession"); + + @Comment("Column for storing a player's TOTP key (for two-factor authentication)") + public static final Property MYSQL_COL_TOTP_KEY = + newProperty("DataSource.mySQLtotpKey", "totp"); + + @Comment("Column for storing the player's last IP") + public static final Property MYSQL_COL_LAST_IP = + newProperty("DataSource.mySQLColumnIp", "ip"); + + @Comment("Column for storing players lastlogins") + public static final Property MYSQL_COL_LASTLOGIN = + newProperty("DataSource.mySQLColumnLastLogin", "lastlogin"); + + @Comment("Column storing the registration date") + public static final Property MYSQL_COL_REGISTER_DATE = + newProperty("DataSource.mySQLColumnRegisterDate", "regdate"); + + @Comment("Column for storing the IP address at the time of registration") + public static final Property MYSQL_COL_REGISTER_IP = + newProperty("DataSource.mySQLColumnRegisterIp", "regip"); + + @Comment("Column for storing player LastLocation - X") + public static final Property MYSQL_COL_LASTLOC_X = + newProperty("DataSource.mySQLlastlocX", "x"); + + @Comment("Column for storing player LastLocation - Y") + public static final Property MYSQL_COL_LASTLOC_Y = + newProperty("DataSource.mySQLlastlocY", "y"); + + @Comment("Column for storing player LastLocation - Z") + public static final Property MYSQL_COL_LASTLOC_Z = + newProperty("DataSource.mySQLlastlocZ", "z"); + + @Comment("Column for storing player LastLocation - World Name") + public static final Property MYSQL_COL_LASTLOC_WORLD = + newProperty("DataSource.mySQLlastlocWorld", "world"); + + @Comment("Column for storing player LastLocation - Yaw") + public static final Property MYSQL_COL_LASTLOC_YAW = + newProperty("DataSource.mySQLlastlocYaw", "yaw"); + + @Comment("Column for storing player LastLocation - Pitch") + public static final Property MYSQL_COL_LASTLOC_PITCH = + newProperty("DataSource.mySQLlastlocPitch", "pitch"); + + @Comment("Column for storing players uuids (optional)") + public static final Property MYSQL_COL_PLAYER_UUID = + newProperty( "DataSource.mySQLPlayerUUID", "" ); + + @Comment("Column for storing players groups") + public static final Property MYSQL_COL_GROUP = + newProperty("ExternalBoardOptions.mySQLColumnGroup", ""); + + @Comment("Overrides the size of the DB Connection Pool, default = 10") + public static final Property MYSQL_POOL_SIZE = + newProperty("DataSource.poolSize", 10); + + @Comment({"The maximum lifetime of a connection in the pool, default = 1800 seconds", + "You should set this at least 30 seconds less than mysql server wait_timeout"}) + public static final Property MYSQL_CONNECTION_MAX_LIFETIME = + newProperty("DataSource.maxLifetime", 1800); + + private DatabaseSettings() { + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/properties/EmailSettings.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/properties/EmailSettings.java new file mode 100644 index 00000000..a0cc03e2 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/properties/EmailSettings.java @@ -0,0 +1,76 @@ +package fr.xephi.authme.settings.properties; + +import ch.jalu.configme.Comment; +import ch.jalu.configme.SettingsHolder; +import ch.jalu.configme.properties.Property; + +import static ch.jalu.configme.properties.PropertyInitializer.newProperty; + +public final class EmailSettings implements SettingsHolder { + + @Comment("Email SMTP server host") + public static final Property SMTP_HOST = + newProperty("Email.mailSMTP", "smtp.163.com"); + + @Comment("Email SMTP server port") + public static final Property SMTP_PORT = + newProperty("Email.mailPort", 465); + + @Comment("Only affects port 25: enable TLS/STARTTLS?") + public static final Property PORT25_USE_TLS = + newProperty("Email.useTls", true); + + @Comment("Email account which sends the mails") + public static final Property MAIL_ACCOUNT = + newProperty("Email.mailAccount", ""); + + @Comment("Email account password") + public static final Property MAIL_PASSWORD = + newProperty("Email.mailPassword", ""); + + @Comment("Email address, fill when mailAccount is not the email address of the account") + public static final Property MAIL_ADDRESS = + newProperty("Email.mailAddress", ""); + + @Comment("Custom sender name, replacing the mailAccount name in the email") + public static final Property MAIL_SENDER_NAME = + newProperty("Email.mailSenderName", ""); + + @Comment("Recovery password length") + public static final Property RECOVERY_PASSWORD_LENGTH = + newProperty("Email.RecoveryPasswordLength", 12); + + @Comment("Mail Subject") + public static final Property RECOVERY_MAIL_SUBJECT = + newProperty("Email.mailSubject", "Your new AuthMe password"); + + @Comment("Like maxRegPerIP but with email") + public static final Property MAX_REG_PER_EMAIL = + newProperty("Email.maxRegPerEmail", 1); + + @Comment("Recall players to add an email?") + public static final Property RECALL_PLAYERS = + newProperty("Email.recallPlayers", false); + + @Comment("Delay in minute for the recall scheduler") + public static final Property DELAY_RECALL = + newProperty("Email.delayRecall", 5); + + @Comment("Send the new password drawn in an image?") + public static final Property PASSWORD_AS_IMAGE = + newProperty("Email.generateImage", false); + + @Comment("The OAuth2 token") + public static final Property OAUTH2_TOKEN = + newProperty("Email.emailOauth2Token", ""); + @Comment("Email notifications when the server shuts down") + public static final Property SHUTDOWN_MAIL = + newProperty("Email.shutDownEmail", false); + @Comment("Email notification address when the server is shut down") + public static final Property SHUTDOWN_MAIL_ADDRESS = + newProperty("Email.shutDownEmailAddress", "your@mail.com"); + + private EmailSettings() { + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/properties/HooksSettings.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/properties/HooksSettings.java new file mode 100644 index 00000000..08c91c6a --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/properties/HooksSettings.java @@ -0,0 +1,101 @@ +package fr.xephi.authme.settings.properties; + +import ch.jalu.configme.Comment; +import ch.jalu.configme.SettingsHolder; +import ch.jalu.configme.properties.Property; + +import java.util.List; + +import static ch.jalu.configme.properties.PropertyInitializer.newListProperty; +import static ch.jalu.configme.properties.PropertyInitializer.newProperty; + +public final class HooksSettings implements SettingsHolder { + + @Comment("Do we need to hook with multiverse for spawn checking?") + public static final Property MULTIVERSE = + newProperty("Hooks.multiverse", true); + + @Comment("Do we need to hook with BungeeCord?") + public static final Property BUNGEECORD = + newProperty("Hooks.bungeecord", false); + @Comment("Do we need to hook with Velocity?") + public static final Property VELOCITY = + newProperty("Hooks.velocity", false); + + @Comment({"How many ticks should we wait before sending login info to proxy?", + "Change this to higher if your player has high ping.", + "See: https://www.spigotmc.org/wiki/bukkit-bungee-plugin-messaging-channel/"}) + public static final Property PROXY_SEND_DELAY = + newProperty("Hooks.proxySendDelay", 10L); + + @Comment({"Hook into floodgate.", + "This must be true if you want to use other bedrock features." + }) + public static final Property HOOK_FLOODGATE_PLAYER = + newProperty("Hooks.floodgate", false); + + @Comment("Allow bedrock players join without check isValidName?") + public static final Property IGNORE_BEDROCK_NAME_CHECK = + newProperty("Hooks.ignoreBedrockNameCheck", true); + + + @Comment("Send player to this BungeeCord server after register/login") + public static final Property BUNGEECORD_SERVER = + newProperty("Hooks.sendPlayerTo", ""); + + @Comment("Do we need to disable Essentials SocialSpy on join?") + public static final Property DISABLE_SOCIAL_SPY = + newProperty("Hooks.disableSocialSpy", false); + + @Comment("Do we need to force /motd Essentials command on join?") + public static final Property USE_ESSENTIALS_MOTD = + newProperty("Hooks.useEssentialsMotd", false); + + @Comment({ + "-1 means disabled. If you want that only activated players", + "can log into your server, you can set here the group number", + "of unactivated users, needed for some forum/CMS support"}) + public static final Property NON_ACTIVATED_USERS_GROUP = + newProperty("ExternalBoardOptions.nonActivedUserGroup", -1); + + @Comment("Other MySQL columns where we need to put the username (case-sensitive)") + public static final Property> MYSQL_OTHER_USERNAME_COLS = + newListProperty("ExternalBoardOptions.mySQLOtherUsernameColumns"); + + @Comment("How much log2 rounds needed in BCrypt (do not change if you do not know what it does)") + public static final Property BCRYPT_LOG2_ROUND = + newProperty("ExternalBoardOptions.bCryptLog2Round", 12); + + @Comment("phpBB table prefix defined during the phpBB installation process") + public static final Property PHPBB_TABLE_PREFIX = + newProperty("ExternalBoardOptions.phpbbTablePrefix", "phpbb_"); + + @Comment("phpBB activated group ID; 2 is the default registered group defined by phpBB") + public static final Property PHPBB_ACTIVATED_GROUP_ID = + newProperty("ExternalBoardOptions.phpbbActivatedGroupId", 2); + + @Comment("IP Board table prefix defined during the IP Board installation process") + public static final Property IPB_TABLE_PREFIX = + newProperty("ExternalBoardOptions.IPBTablePrefix", "ipb_"); + + @Comment("IP Board default group ID; 3 is the default registered group defined by IP Board") + public static final Property IPB_ACTIVATED_GROUP_ID = + newProperty("ExternalBoardOptions.IPBActivatedGroupId", 3); + + @Comment("Xenforo table prefix defined during the Xenforo installation process") + public static final Property XF_TABLE_PREFIX = + newProperty("ExternalBoardOptions.XFTablePrefix", "xf_"); + + @Comment("XenForo default group ID; 2 is the default registered group defined by Xenforo") + public static final Property XF_ACTIVATED_GROUP_ID = + newProperty("ExternalBoardOptions.XFActivatedGroupId", 2); + + @Comment("Wordpress prefix defined during WordPress installation") + public static final Property WORDPRESS_TABLE_PREFIX = + newProperty("ExternalBoardOptions.wordpressTablePrefix", "wp_"); + + + private HooksSettings() { + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/properties/LimboSettings.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/properties/LimboSettings.java new file mode 100644 index 00000000..309b6923 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/properties/LimboSettings.java @@ -0,0 +1,83 @@ +package fr.xephi.authme.settings.properties; + +import ch.jalu.configme.Comment; +import ch.jalu.configme.SettingsHolder; +import ch.jalu.configme.configurationdata.CommentsConfiguration; +import ch.jalu.configme.properties.Property; +import fr.xephi.authme.data.limbo.AllowFlightRestoreType; +import fr.xephi.authme.data.limbo.WalkFlySpeedRestoreType; +import fr.xephi.authme.data.limbo.persistence.LimboPersistenceType; +import fr.xephi.authme.data.limbo.persistence.SegmentSize; + +import static ch.jalu.configme.properties.PropertyInitializer.newProperty; + +/** + * Settings for the LimboPlayer feature. + */ +public final class LimboSettings implements SettingsHolder { + + @Comment({ + "Besides storing the data in memory, you can define if/how the data should be persisted", + "on disk. This is useful in case of a server crash, so next time the server starts we can", + "properly restore things like OP status, ability to fly, and walk/fly speed.", + "DISABLED: no disk storage,", + "INDIVIDUAL_FILES: each player data in its own file,", + "DISTRIBUTED_FILES: distributes players into different files based on their UUID, see below" + }) + public static final Property LIMBO_PERSISTENCE_TYPE = + newProperty(LimboPersistenceType.class, "limbo.persistence.type", LimboPersistenceType.INDIVIDUAL_FILES); + + @Comment({ + "This setting only affects DISTRIBUTED_FILES persistence. The distributed file", + "persistence attempts to reduce the number of files by distributing players into various", + "buckets based on their UUID. This setting defines into how many files the players should", + "be distributed. Possible values: ONE, FOUR, EIGHT, SIXTEEN, THIRTY_TWO, SIXTY_FOUR,", + "ONE_TWENTY for 128, TWO_FIFTY for 256.", + "For example, if you expect 100 non-logged in players, setting to SIXTEEN will average", + "6.25 players per file (100 / 16).", + "Note: if you change this setting all data will be migrated. If you have a lot of data,", + "change this setting only on server restart, not with /authme reload." + }) + public static final Property DISTRIBUTION_SIZE = + newProperty(SegmentSize.class, "limbo.persistence.distributionSize", SegmentSize.SIXTEEN); + + @Comment({ + "Whether the player is allowed to fly: RESTORE, ENABLE, DISABLE, NOTHING.", + "RESTORE sets back the old property from the player. NOTHING will prevent AuthMe", + "from modifying the 'allow flight' property on the player." + }) + public static final Property RESTORE_ALLOW_FLIGHT = + newProperty(AllowFlightRestoreType.class, "limbo.restoreAllowFlight", AllowFlightRestoreType.RESTORE); + + @Comment({ + "Restore fly speed: RESTORE, DEFAULT, MAX_RESTORE, RESTORE_NO_ZERO.", + "RESTORE: restore the speed the player had;", + "DEFAULT: always set to default speed;", + "MAX_RESTORE: take the maximum of the player's current speed and the previous one", + "RESTORE_NO_ZERO: Like 'restore' but sets speed to default if the player's speed was 0" + }) + public static final Property RESTORE_FLY_SPEED = + newProperty(WalkFlySpeedRestoreType.class, "limbo.restoreFlySpeed", WalkFlySpeedRestoreType.RESTORE_NO_ZERO); + + @Comment({ + "Restore walk speed: RESTORE, DEFAULT, MAX_RESTORE, RESTORE_NO_ZERO.", + "See above for a description of the values." + }) + public static final Property RESTORE_WALK_SPEED = + newProperty(WalkFlySpeedRestoreType.class, "limbo.restoreWalkSpeed", WalkFlySpeedRestoreType.RESTORE_NO_ZERO); + + private LimboSettings() { + } + + @Override + public void registerComments(CommentsConfiguration conf) { + String[] limboExplanation = { + "Before a user logs in, various properties are temporarily removed from the player,", + "such as OP status, ability to fly, and walk/fly speed.", + "Once the user is logged in, we add back the properties we previously saved.", + "In this section, you may define how these properties should be handled.", + "Read more at https://github.com/AuthMe/AuthMeReloaded/wiki/Limbo-players" + }; + conf.setComment("limbo", limboExplanation); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/properties/PluginSettings.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/properties/PluginSettings.java new file mode 100644 index 00000000..221496b3 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/properties/PluginSettings.java @@ -0,0 +1,122 @@ +package fr.xephi.authme.settings.properties; + +import ch.jalu.configme.Comment; +import ch.jalu.configme.SettingsHolder; +import ch.jalu.configme.properties.Property; +import fr.xephi.authme.output.LogLevel; + +import java.util.Set; + +import static ch.jalu.configme.properties.PropertyInitializer.newLowercaseStringSetProperty; +import static ch.jalu.configme.properties.PropertyInitializer.newProperty; + +public final class PluginSettings implements SettingsHolder { + @Comment({ + "Should we execute /help command when unregistered players press Shift+F?", + "This keeps compatibility with some menu plugins", + "If you are using TrMenu, don't enable this because TrMenu already implemented this." + }) + public static final Property MENU_UNREGISTER_COMPATIBILITY = + newProperty("3rdPartyFeature.compatibility.menuPlugins", false); + + @Comment({ + "Send i18n messages to player based on their client settings, this option will override `settings.messagesLanguage`", + "(Requires Protocollib or Packetevents)", + "This will not affect language of AuthMe help command." + }) + public static final Property I18N_MESSAGES = + newProperty("3rdPartyFeature.features.i18nMessages.enabled", false); + + @Comment({"Redirect locale code to certain AuthMe language code as you want", + "Minecraft locale list: https://minecraft.wiki/w/Language", + "AuthMe language code: https://github.com/HaHaWTH/AuthMeReReloaded/blob/master/docs/translations.md", + "For example, if you want to show Russian messages to player using language Tatar(tt_ru),", + "and show Chinese Simplified messages to player using language Classical Chinese(lzh), then:", + "locale-code-redirect:", + "- 'tt_ru:ru'", + "- 'lzh:zhcn'"}) + public static final Property> I18N_CODE_REDIRECT = + newLowercaseStringSetProperty("3rdPartyFeature.features.i18nMessages.locale-code-redirect", + "tt_ru:ru", "lzh:zhcn"); + + @Comment({ + "Do you want to enable the session feature?", + "If enabled, when a player authenticates successfully,", + "his IP and his nickname is saved.", + "The next time the player joins the server, if his IP", + "is the same as last time and the timeout hasn't", + "expired, he will not need to authenticate." + }) + public static final Property SESSIONS_ENABLED = + newProperty("settings.sessions.enabled", true); + + @Comment({ + "After how many minutes should a session expire?", + "A player's session ends after the timeout or if his IP has changed" + }) + public static final Property SESSIONS_TIMEOUT = + newProperty("settings.sessions.timeout", 43200); + + @Comment({ + "Message language, available languages:", + "https://github.com/AuthMe/AuthMeReloaded/blob/master/docs/translations.md", + "Example: zhcn, en" + }) + public static final Property MESSAGES_LANGUAGE = + newProperty("settings.messagesLanguage", "en"); + + @Comment({ + "Enables switching a player to defined permission groups before they log in.", + "See below for a detailed explanation." + }) + public static final Property ENABLE_PERMISSION_CHECK = + newProperty("GroupOptions.enablePermissionCheck", false); + + @Comment({ + "This is a very important option: if a registered player joins the server", + "AuthMe will switch him to unLoggedInGroup. This should prevent all major exploits.", + "You can set up your permission plugin with this special group to have no permissions,", + "or only permission to chat (or permission to send private messages etc.).", + "The better way is to set up this group with few permissions, so if a player", + "tries to exploit an account they can do only what you've defined for the group.", + "After login, the player will be moved to his correct permissions group!", + "Please note that the group name is case-sensitive, so 'admin' is different from 'Admin'", + "Otherwise your group will be wiped and the player will join in the default group []!", + "Example: registeredPlayerGroup: 'NotLogged'" + }) + public static final Property REGISTERED_GROUP = + newProperty("GroupOptions.registeredPlayerGroup", ""); + + @Comment({ + "Similar to above, unregistered players can be set to the following", + "permissions group" + }) + public static final Property UNREGISTERED_GROUP = + newProperty("GroupOptions.unregisteredPlayerGroup", ""); + + @Comment("Forces authme to hook into Vault instead of a specific permission handler system.") + public static final Property FORCE_VAULT_HOOK = + newProperty("settings.forceVaultHook", false); + + @Comment({ + "Log level: INFO, FINE, DEBUG. Use INFO for general messages,", + "FINE for some additional detailed ones (like password failed),", + "and DEBUG for debugging" + }) + public static final Property LOG_LEVEL = + newProperty(LogLevel.class, "settings.logLevel", LogLevel.FINE); + + @Comment({ + "By default we schedule async tasks when talking to the database. If you want", + "typical communication with the database to happen synchronously, set this to false" + }) + public static final Property USE_ASYNC_TASKS = + newProperty("settings.useAsyncTasks", true); + + @Comment("The name of the server, used in some placeholders.") + public static final Property SERVER_NAME = newProperty("settings.serverName", "Your Minecraft Server"); + + private PluginSettings() { + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/properties/ProtectionSettings.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/properties/ProtectionSettings.java new file mode 100644 index 00000000..7becd390 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/properties/ProtectionSettings.java @@ -0,0 +1,66 @@ +package fr.xephi.authme.settings.properties; + +import ch.jalu.configme.Comment; +import ch.jalu.configme.SettingsHolder; +import ch.jalu.configme.properties.Property; + +import java.util.List; + +import static ch.jalu.configme.properties.PropertyInitializer.newListProperty; +import static ch.jalu.configme.properties.PropertyInitializer.newProperty; + + +public final class ProtectionSettings implements SettingsHolder { + + @Comment("Enable some servers protection (country based login, antibot)") + public static final Property ENABLE_PROTECTION = + newProperty("Protection.enableProtection", false); + + @Comment("Apply the protection also to registered usernames") + public static final Property ENABLE_PROTECTION_REGISTERED = + newProperty("Protection.enableProtectionRegistered", false); + + @Comment({ + "Countries allowed to join the server and register. For country codes, see", + "https://dev.maxmind.com/geoip/legacy/codes/iso3166/", + "Use \"LOCALHOST\" for local addresses.", + "PLEASE USE QUOTES!"}) + public static final Property> COUNTRIES_WHITELIST = + newListProperty("Protection.countries", "LOCALHOST"); + + @Comment({ + "Countries not allowed to join the server and register", + "PLEASE USE QUOTES!"}) + public static final Property> COUNTRIES_BLACKLIST = + newListProperty("Protection.countriesBlacklist", "A1"); + + @Comment("Do we need to enable automatic antibot system?") + public static final Property ENABLE_ANTIBOT = + newProperty("Protection.enableAntiBot", true); + + @Comment("The interval in seconds") + public static final Property ANTIBOT_INTERVAL = + newProperty("Protection.antiBotInterval", 5); + + @Comment({ + "Max number of players allowed to login in the interval", + "before the AntiBot system is enabled automatically"}) + public static final Property ANTIBOT_SENSIBILITY = + newProperty("Protection.antiBotSensibility", 10); + + @Comment("Duration in minutes of the antibot automatic system") + public static final Property ANTIBOT_DURATION = + newProperty("Protection.antiBotDuration", 10); + + @Comment("Delay in seconds before the antibot activation") + public static final Property ANTIBOT_DELAY = + newProperty("Protection.antiBotDelay", 60); + + @Comment("Kicks the player that issued a command before the defined time after the join process") + public static final Property QUICK_COMMANDS_DENIED_BEFORE_MILLISECONDS = + newProperty("Protection.quickCommands.denyCommandsBeforeMilliseconds", 1000); + + private ProtectionSettings() { + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/properties/PurgeSettings.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/properties/PurgeSettings.java new file mode 100644 index 00000000..0064fd40 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/properties/PurgeSettings.java @@ -0,0 +1,46 @@ +package fr.xephi.authme.settings.properties; + +import ch.jalu.configme.Comment; +import ch.jalu.configme.SettingsHolder; +import ch.jalu.configme.properties.Property; + +import static ch.jalu.configme.properties.PropertyInitializer.newProperty; + +public final class PurgeSettings implements SettingsHolder { + + @Comment("If enabled, AuthMe automatically purges old, unused accounts") + public static final Property USE_AUTO_PURGE = + newProperty("Purge.useAutoPurge", false); + + @Comment("Number of days after which an account should be purged") + public static final Property DAYS_BEFORE_REMOVE_PLAYER = + newProperty("Purge.daysBeforeRemovePlayer", 60); + + @Comment("Do we need to remove the player.dat file during purge process?") + public static final Property REMOVE_PLAYER_DAT = + newProperty("Purge.removePlayerDat", false); + + @Comment("Do we need to remove the Essentials/userdata/player.yml file during purge process?") + public static final Property REMOVE_ESSENTIALS_FILES = + newProperty("Purge.removeEssentialsFile", false); + + @Comment("World in which the players.dat are stored") + public static final Property DEFAULT_WORLD = + newProperty("Purge.defaultWorld", "world"); + + @Comment("Remove LimitedCreative/inventories/player.yml, player_creative.yml files during purge?") + public static final Property REMOVE_LIMITED_CREATIVE_INVENTORIES = + newProperty("Purge.removeLimitedCreativesInventories", false); + + @Comment("Do we need to remove the AntiXRayData/PlayerData/player file during purge process?") + public static final Property REMOVE_ANTI_XRAY_FILE = + newProperty("Purge.removeAntiXRayFile", false); + + @Comment("Do we need to remove permissions?") + public static final Property REMOVE_PERMISSIONS = + newProperty("Purge.removePermissions", false); + + private PurgeSettings() { + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/properties/RegistrationSettings.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/properties/RegistrationSettings.java new file mode 100644 index 00000000..b15132d5 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/properties/RegistrationSettings.java @@ -0,0 +1,108 @@ +package fr.xephi.authme.settings.properties; + +import ch.jalu.configme.Comment; +import ch.jalu.configme.SettingsHolder; +import ch.jalu.configme.properties.Property; +import fr.xephi.authme.process.register.RegisterSecondaryArgument; +import fr.xephi.authme.process.register.RegistrationType; + +import static ch.jalu.configme.properties.PropertyInitializer.newProperty; + +public final class RegistrationSettings implements SettingsHolder { + + @Comment("Enable registration on the server?") + public static final Property IS_ENABLED = + newProperty("settings.registration.enabled", true); + + @Comment({ + "Send every X seconds a message to a player to", + "remind him that he has to login/register"}) + public static final Property MESSAGE_INTERVAL = + newProperty("settings.registration.messageInterval", 5); + + @Comment({ + "Only registered and logged in players can play.", + "See restrictions for exceptions"}) + public static final Property FORCE = + newProperty("settings.registration.force", true); + + @Comment({ + "Type of registration: PASSWORD or EMAIL", + "PASSWORD = account is registered with a password supplied by the user;", + "EMAIL = password is generated and sent to the email provided by the user.", + "More info at https://github.com/AuthMe/AuthMeReloaded/wiki/Registration" + }) + public static final Property REGISTRATION_TYPE = + newProperty(RegistrationType.class, "settings.registration.type", RegistrationType.PASSWORD); + + @Comment({ + "Second argument the /register command should take: ", + "NONE = no 2nd argument", + "CONFIRMATION = must repeat first argument (pass or email)", + "EMAIL_OPTIONAL = for password register: 2nd argument can be empty or have email address", + "EMAIL_MANDATORY = for password register: 2nd argument MUST be an email address" + }) + public static final Property REGISTER_SECOND_ARGUMENT = + newProperty(RegisterSecondaryArgument.class, "settings.registration.secondArg", + RegisterSecondaryArgument.CONFIRMATION); + + @Comment({ + "Should we unregister the player when he didn't verify the email?", + "This only works if you enabled email registration."}) + public static final Property UNREGISTER_ON_EMAIL_VERIFICATION_FAILURE = + newProperty("settings.registration.email.unregisterOnEmailVerificationFailure", false); + + @Comment({"How many minutes should we wait before unregister the player", + "when he didn't verify the email?"}) + public static final Property UNREGISTER_AFTER_MINUTES = + newProperty("settings.registration.email.unregisterAfterMinutes", 10L); + @Comment({ + "Do we force kick a player after a successful registration?", + "Do not use with login feature below"}) + public static final Property FORCE_KICK_AFTER_REGISTER = + newProperty("settings.registration.forceKickAfterRegister", false); + + @Comment("Does AuthMe need to enforce a /login after a successful registration?") + public static final Property FORCE_LOGIN_AFTER_REGISTER = + newProperty("settings.registration.forceLoginAfterRegister", false); + @Comment("Should we delay the join message and display it once the player has logged in?") + public static final Property DELAY_JOIN_MESSAGE = + newProperty("settings.delayJoinMessage", true); + + @Comment({ + "The custom join message that will be sent after a successful login,", + "keep empty to use the original one.", + "Available variables:", + "{PLAYERNAME}: the player name (no colors)", + "{DISPLAYNAME}: the player display name (with colors)", + "{DISPLAYNAMENOCOLOR}: the player display name (without colors)"}) + public static final Property CUSTOM_JOIN_MESSAGE = + newProperty("settings.customJoinMessage", ""); + + @Comment("Should we remove the leave messages of unlogged users?") + public static final Property REMOVE_UNLOGGED_LEAVE_MESSAGE = + newProperty("settings.removeUnloggedLeaveMessage", true); + + @Comment("Should we remove join messages altogether?") + public static final Property REMOVE_JOIN_MESSAGE = + newProperty("settings.removeJoinMessage", true); + + @Comment("Should we remove leave messages altogether?") + public static final Property REMOVE_LEAVE_MESSAGE = + newProperty("settings.removeLeaveMessage", true); + + @Comment("Do we need to add potion effect Blinding before login/register?") + public static final Property APPLY_BLIND_EFFECT = + newProperty("settings.applyBlindEffect", false); + + @Comment({ + "Do we need to prevent people to login with another case?", + "If Xephi is registered, then Xephi can login, but not XEPHI/xephi/XePhI"}) + public static final Property PREVENT_OTHER_CASE = + newProperty("settings.preventOtherCase", true); + + + private RegistrationSettings() { + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/properties/RestrictionSettings.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/properties/RestrictionSettings.java new file mode 100644 index 00000000..1fc8341f --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/properties/RestrictionSettings.java @@ -0,0 +1,217 @@ +package fr.xephi.authme.settings.properties; + +import ch.jalu.configme.Comment; +import ch.jalu.configme.SettingsHolder; +import ch.jalu.configme.properties.Property; + +import java.util.List; +import java.util.Set; + +import static ch.jalu.configme.properties.PropertyInitializer.newListProperty; +import static ch.jalu.configme.properties.PropertyInitializer.newLowercaseStringSetProperty; +import static ch.jalu.configme.properties.PropertyInitializer.newProperty; + +public final class RestrictionSettings implements SettingsHolder { + + @Comment({ + "Can not authenticated players chat?", + "Keep in mind that this feature also blocks all commands not", + "listed in the list below."}) + public static final Property ALLOW_CHAT = + newProperty("settings.restrictions.allowChat", false); + + @Comment("Hide the chat log from players who are not authenticated?") + public static final Property HIDE_CHAT = + newProperty("settings.restrictions.hideChat", false); + + @Comment("Allowed commands for unauthenticated players") + public static final Property> ALLOW_COMMANDS = + newLowercaseStringSetProperty("settings.restrictions.allowCommands", + "/login", "/log", "/l", "/register", "/reg", "/email", "/captcha", "/2fa", "/totp"); + + @Comment({ + "Max number of allowed registrations per IP", + "The value 0 means an unlimited number of registrations!"}) + public static final Property MAX_REGISTRATION_PER_IP = + newProperty("settings.restrictions.maxRegPerIp", 3); + + @Comment("Minimum allowed username length") + public static final Property MIN_NICKNAME_LENGTH = + newProperty("settings.restrictions.minNicknameLength", 3); + + @Comment("Maximum allowed username length") + public static final Property MAX_NICKNAME_LENGTH = + newProperty("settings.restrictions.maxNicknameLength", 16); + + @Comment({ + "When this setting is enabled, online players can't be kicked out", + "due to \"Logged in from another Location\"", + "This setting will prevent potential security exploits."}) + public static final Property FORCE_SINGLE_SESSION = + newProperty("settings.restrictions.ForceSingleSession", true); + + @Comment({ + "If enabled, every player that spawn in one of the world listed in", + "\"ForceSpawnLocOnJoin.worlds\" will be teleported to the spawnpoint after successful", + "authentication. The quit location of the player will be overwritten.", + "This is different from \"teleportUnAuthedToSpawn\" that teleport player", + "to the spawnpoint on join."}) + public static final Property FORCE_SPAWN_LOCATION_AFTER_LOGIN = + newProperty("settings.restrictions.ForceSpawnLocOnJoin.enabled", false); + + @Comment({ + "WorldNames where we need to force the spawn location", + "Case-sensitive!"}) + public static final Property> FORCE_SPAWN_ON_WORLDS = + newListProperty("settings.restrictions.ForceSpawnLocOnJoin.worlds", + "world", "world_nether", "world_the_end"); + + @Comment("This option will save the quit location of the players.") + public static final Property SAVE_QUIT_LOCATION = + newProperty("settings.restrictions.SaveQuitLocation", false); + + @Comment({ + "To activate the restricted user feature you need", + "to enable this option and configure the AllowedRestrictedUser field."}) + public static final Property ENABLE_RESTRICTED_USERS = + newProperty("settings.restrictions.AllowRestrictedUser", true); + + @Comment({ + "The restricted user feature will kick players listed below", + "if they don't match the defined IP address. Names are case-insensitive.", + "You can use * as wildcard (127.0.0.*), or regex with a \"regex:\" prefix regex:127\\.0\\.0\\..*", + "Example:", + " AllowedRestrictedUser:", + " - playername;127.0.0.1", + " - playername;regex:127\\.0\\.0\\..*"}) + public static final Property> RESTRICTED_USERS = + newLowercaseStringSetProperty("settings.restrictions.AllowedRestrictedUser", + "server_land;127.0.0.1","server;127.0.0.1","bukkit;127.0.0.1","purpur;127.0.0.1", + "system;127.0.0.1","admin;127.0.0.1","md_5;127.0.0.1","administrator;127.0.0.1","notch;127.0.0.1", + "spigot;127.0.0.1","bukkit;127.0.0.1","bukkitcraft;127.0.0.1","paperclip;127.0.0.1","papermc;127.0.0.1", + "spigotmc;127.0.0.1","root;127.0.0.1","console;127.0.0.1","purpur;127.0.0.1","authme;127.0.0.1", + "owner;127.0.0.1"); + + @Comment("Ban unknown IPs trying to log in with a restricted username?") + public static final Property BAN_UNKNOWN_IP = + newProperty("settings.restrictions.banUnsafedIP", false); + + @Comment("Should unregistered players be kicked immediately?") + public static final Property KICK_NON_REGISTERED = + newProperty("settings.restrictions.kickNonRegistered", false); + + @Comment("Should players be kicked on wrong password?") + public static final Property KICK_ON_WRONG_PASSWORD = + newProperty("settings.restrictions.kickOnWrongPassword", false); + + @Comment({ + "Should not logged in players be teleported to the spawn?", + "After the authentication they will be teleported back to", + "their normal position."}) + public static final Property TELEPORT_UNAUTHED_TO_SPAWN = + newProperty("settings.restrictions.teleportUnAuthedToSpawn", false); + + @Comment("Can unregistered players walk around?") + public static final Property ALLOW_UNAUTHED_MOVEMENT = + newProperty("settings.restrictions.allowMovement", false); + + @Comment({ + "After how many seconds should players who fail to login or register", + "be kicked? Set to 0 to disable."}) + public static final Property TIMEOUT = + newProperty("settings.restrictions.timeout", 120); + + @Comment("Regex pattern of allowed characters in the player name.") + public static final Property ALLOWED_NICKNAME_CHARACTERS = + newProperty("settings.restrictions.allowedNicknameCharacters", "[a-zA-Z0-9_]*"); + + + @Comment({ + "How far can unregistered players walk?", + "Set to 0 for unlimited radius" + }) + public static final Property ALLOWED_MOVEMENT_RADIUS = + newProperty("settings.restrictions.allowedMovementRadius", 0); + + @Comment("Should we protect the player inventory before logging in? Requires ProtocolLib.") + public static final Property PROTECT_INVENTORY_BEFORE_LOGIN = + newProperty("settings.restrictions.ProtectInventoryBeforeLogIn", false); + + @Comment("Should we deny the tabcomplete feature before logging in? Requires ProtocolLib.") + public static final Property DENY_TABCOMPLETE_BEFORE_LOGIN = + newProperty("settings.restrictions.DenyTabCompleteBeforeLogin", false); + + @Comment({ + "Should we display all other accounts from a player when he joins?", + "permission: /authme.admin.accounts"}) + public static final Property DISPLAY_OTHER_ACCOUNTS = + newProperty("settings.restrictions.displayOtherAccounts", false); + + @Comment("Spawn priority; values: authme, essentials, cmi, multiverse, default") + public static final Property SPAWN_PRIORITY = + newProperty("settings.restrictions.spawnPriority", "authme,essentials,cmi,multiverse,default"); + + @Comment("Maximum Login authorized by IP") + public static final Property MAX_LOGIN_PER_IP = + newProperty("settings.restrictions.maxLoginPerIp", 3); + + @Comment("Maximum Join authorized by IP") + public static final Property MAX_JOIN_PER_IP = + newProperty("settings.restrictions.maxJoinPerIp", 3); + + @Comment("AuthMe will NEVER teleport players if set to true!") + public static final Property NO_TELEPORT = + newProperty("settings.restrictions.noTeleport", false); + + @Comment({ + "Regex syntax for allowed chars in passwords. The default [!-~] allows all visible ASCII", + "characters, which is what we recommend. See also http://asciitable.com", + "You can test your regex with https://regex101.com" + }) + public static final Property ALLOWED_PASSWORD_REGEX = + newProperty("settings.restrictions.allowedPasswordCharacters", "[!-~]*"); + + @Comment("Regex syntax for allowed chars in email.") + public static final Property ALLOWED_EMAIL_REGEX = + newProperty("settings.restrictions.allowedEmailCharacters", "^[A-Za-z0-9_.]{3,20}@(qq|outlook|163|gmail|icloud)\\.com$"); + + + @Comment("Force survival gamemode when player joins?") + public static final Property FORCE_SURVIVAL_MODE = + newProperty("settings.GameMode.ForceSurvivalMode", false); + + @Comment({ + "Below you can list all account names that AuthMe will ignore", + "for registration or login. Configure it at your own risk!!", + "This option adds compatibility with BuildCraft and some other mods.", + "It is case-insensitive! Example:", + "UnrestrictedName:", + "- 'npcPlayer'", + "- 'npcPlayer2'" + }) + public static final Property> UNRESTRICTED_NAMES = + newLowercaseStringSetProperty("settings.unrestrictions.UnrestrictedName"); + + + @Comment({ + "Below you can list all inventories names that AuthMe will ignore", + "for registration or login. Configure it at your own risk!!", + "This option adds compatibility with some mods.", + "It is case-insensitive! Example:", + "UnrestrictedInventories:", + "- 'myCustomInventory1'", + "- 'myCustomInventory2'" + }) + public static final Property> UNRESTRICTED_INVENTORIES = + newLowercaseStringSetProperty("settings.unrestrictions.UnrestrictedInventories"); + + @Comment("Should we check unrestricted inventories strictly? (Original behavior)") + public static final Property STRICT_UNRESTRICTED_INVENTORIES_CHECK = + newProperty("settings.unrestrictions.StrictUnrestrictedInventoriesCheck", true); + + + private RestrictionSettings() { + + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/properties/SecuritySettings.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/properties/SecuritySettings.java new file mode 100644 index 00000000..b1d7c4ae --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/settings/properties/SecuritySettings.java @@ -0,0 +1,203 @@ +package fr.xephi.authme.settings.properties; + +import ch.jalu.configme.Comment; +import ch.jalu.configme.SettingsHolder; +import ch.jalu.configme.properties.Property; +import fr.xephi.authme.security.HashAlgorithm; +import fr.xephi.authme.settings.EnumSetProperty; + +import java.util.Set; + +import static ch.jalu.configme.properties.PropertyInitializer.newLowercaseStringSetProperty; +import static ch.jalu.configme.properties.PropertyInitializer.newProperty; + +public final class SecuritySettings implements SettingsHolder { + + @Comment({"Stop the server if we can't contact the sql database", + "Take care with this, if you set this to false,", + "AuthMe will automatically disable and the server won't be protected!"}) + public static final Property STOP_SERVER_ON_PROBLEM = + newProperty("Security.SQLProblem.stopServer", false); + + @Comment({"Should we let Bedrock players login automatically?", + "(Requires hookFloodgate to be true & floodgate loaded)", + "(**THIS IS SAFE DO NOT WORRY**)"}) + public static final Property FORCE_LOGIN_BEDROCK = + newProperty("3rdPartyFeature.features.bedrockAutoLogin", false); + + @Comment("Should we purge data on non-registered players quit?") + public static final Property PURGE_DATA_ON_QUIT = + newProperty("3rdPartyFeature.features.purgeData.purgeOnQuit", false); + + @Comment("Which world's player data should be deleted?(Enter the world *FOLDER* name where your players first logged in)") + public static final Property DELETE_PLAYER_DATA_WORLD = + newProperty("3rdPartyFeature.features.purgeData.purgeWorldFolderName", "world"); + @Comment("Enable the new feature to prevent ghost players?") + public static final Property ANTI_GHOST_PLAYERS = + newProperty("3rdPartyFeature.fixes.antiGhostPlayer", false); + + @Comment({"(MC1.13- only)", + "Should we fix the shulker crash bug with advanced method?"}) + public static final Property ADVANCED_SHULKER_FIX = + newProperty("3rdPartyFeature.fixes.advancedShulkerFix", false); + + @Comment("Should we fix the location when players logged in the portal?") + public static final Property LOGIN_LOC_FIX_SUB_PORTAL = + newProperty("3rdPartyFeature.fixes.loginLocationFix.fixPortalStuck", false); + + @Comment("Should we fix the location when players logged underground?") + public static final Property LOGIN_LOC_FIX_SUB_UNDERGROUND = + newProperty("3rdPartyFeature.fixes.loginLocationFix.fixGroundStuck", false); + + @Comment("Copy AuthMe log output in a separate file as well?") + public static final Property USE_LOGGING = + newProperty("Security.console.logConsole", true); + + @Comment({"Query haveibeenpwned.com with a hashed version of the password.", + "This is used to check whether it is safe."}) + public static final Property HAVE_I_BEEN_PWNED_CHECK = + newProperty("Security.account.haveIBeenPwned.check", false); + + @Comment({"If the password is used more than this number of times, it is considered unsafe."}) + public static final Property HAVE_I_BEEN_PWNED_LIMIT = + newProperty("Security.account.haveIBeenPwned.limit", 0); + + @Comment("Enable captcha when a player uses wrong password too many times") + public static final Property ENABLE_LOGIN_FAILURE_CAPTCHA = + newProperty("Security.captcha.useCaptcha", false); + + @Comment("Check for updates on enabled from GitHub?") + public static final Property CHECK_FOR_UPDATES = + newProperty("Plugin.updates.checkForUpdates", true); + + @Comment("Should we show the AuthMe banner on startup?") + public static final Property SHOW_STARTUP_BANNER = + newProperty("Plugin.banners.showBanners", true); + + @Comment("Max allowed tries before a captcha is required") + public static final Property MAX_LOGIN_TRIES_BEFORE_CAPTCHA = + newProperty("Security.captcha.maxLoginTry", 8); + + @Comment("Captcha length") + public static final Property CAPTCHA_LENGTH = + newProperty("Security.captcha.captchaLength", 6); + + @Comment("Minutes after which login attempts count is reset for a player") + public static final Property CAPTCHA_COUNT_MINUTES_BEFORE_RESET = + newProperty("Security.captcha.captchaCountReset", 120); + + @Comment("Require captcha before a player may register?") + public static final Property ENABLE_CAPTCHA_FOR_REGISTRATION = + newProperty("Security.captcha.requireForRegistration", false); + + @Comment("Minimum length of password") + public static final Property MIN_PASSWORD_LENGTH = + newProperty("settings.security.minPasswordLength", 8); + + @Comment("Maximum length of password") + public static final Property MAX_PASSWORD_LENGTH = + newProperty("settings.security.passwordMaxLength", 26); + + @Comment({ + "Possible values: SHA256, BCRYPT, BCRYPT2Y, PBKDF2, SALTEDSHA512,", + "MYBB, IPB3, PHPBB, PHPFUSION, SMF, XENFORO, XAUTH, JOOMLA, WBB3, WBB4, MD5VB,", + "PBKDF2DJANGO, WORDPRESS, ROYALAUTH, ARGON2, NOCRYPT, CUSTOM (for developers only). See full list at", + "https://github.com/AuthMe/AuthMeReloaded/blob/master/docs/hash_algorithms.md", + "If you use ARGON2, check that you have the argon2 c library on your system" + }) + public static final Property PASSWORD_HASH = + newProperty(HashAlgorithm.class, "settings.security.passwordHash", HashAlgorithm.SHA256); + + @Comment({ + "If a password check fails, AuthMe will also try to check with the following hash methods.", + "Use this setting when you change from one hash method to another.", + "AuthMe will update the password to the new hash. Example:", + "legacyHashes:", + "- 'SHA1'" + }) + public static final Property> LEGACY_HASHES = + new EnumSetProperty<>(HashAlgorithm.class, "settings.security.legacyHashes"); + + @Comment("Salt length for the SALTED2MD5 MD5(MD5(password)+salt)") + public static final Property DOUBLE_MD5_SALT_LENGTH = + newProperty("settings.security.doubleMD5SaltLength", 8); + + @Comment("Number of rounds to use if passwordHash is set to PBKDF2. Default is 10000") + public static final Property PBKDF2_NUMBER_OF_ROUNDS = + newProperty("settings.security.pbkdf2Rounds", 10000); + + @Comment({"Prevent unsafe passwords from being used; put them in lowercase!", + "You should always set 'help' as unsafePassword due to possible conflicts.", + "unsafePasswords:", + "- '123456'", + "- 'password'", + "- 'help'"}) + public static final Property> UNSAFE_PASSWORDS = + newLowercaseStringSetProperty("settings.security.unsafePasswords", + "12345678", "password", "qwertyui", "123456789", "87654321", "1234567890", "asdfghjkl","zxcvbnm,","asdfghjk","12312312","123123123","32132132","321321321"); + + @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 their IP being tempbanned") + public static final Property MAX_LOGIN_TEMPBAN = + newProperty("Security.tempban.maxLoginTries", 8); + + @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); + + @Comment({"How many minutes before resetting the count for failed logins by IP and username", + "Default: 480 minutes (8 hours)"}) + public static final Property TEMPBAN_MINUTES_BEFORE_RESET = + newProperty("Security.tempban.minutesBeforeCounterReset", 480); + + @Comment({"The command to execute instead of using the internal ban system, empty if disabled.", + "Available placeholders: %player%, %ip%"}) + public static final Property TEMPBAN_CUSTOM_COMMAND = + newProperty("Security.tempban.customCommand", ""); + + @Comment("Number of characters a recovery code should have (0 to disable)") + public static final Property RECOVERY_CODE_LENGTH = + newProperty("Security.recoveryCode.length", 8); + + @Comment("How many hours is a recovery code valid for?") + public static final Property RECOVERY_CODE_HOURS_VALID = + newProperty("Security.recoveryCode.validForHours", 6); + + @Comment("Max number of tries to enter recovery code") + public static final Property RECOVERY_CODE_MAX_TRIES = + newProperty("Security.recoveryCode.maxTries", 4); + + @Comment({"How long a player has after password recovery to change their password", + "without logging in. This is in minutes.", + "Default: 2 minutes"}) + public static final Property PASSWORD_CHANGE_TIMEOUT = + newProperty("Security.recoveryCode.passwordChangeTimeout", 5); + + @Comment({ + "Seconds a user has to wait for before a password recovery mail may be sent again", + "This prevents an attacker from abusing AuthMe's email feature." + }) + public static final Property EMAIL_RECOVERY_COOLDOWN_SECONDS = + newProperty("Security.emailRecovery.cooldown", 60); + + @Comment({ + "The mail shown using /email show will be partially hidden", + "E.g. (if enabled)", + " original email: my.email@example.com", + " hidden email: my.***@***mple.com" + }) + public static final Property USE_EMAIL_MASKING = + newProperty("Security.privacy.enableEmailMasking", false); + + @Comment("Minutes after which a verification code will expire") + public static final Property VERIFICATION_CODE_EXPIRATION_MINUTES = + newProperty("Security.privacy.verificationCodeExpiration", 10); + + private SecuritySettings() { + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/task/CleanupTask.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/task/CleanupTask.java new file mode 100644 index 00000000..4f40ddb2 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/task/CleanupTask.java @@ -0,0 +1,25 @@ +package fr.xephi.authme.task; + +import ch.jalu.injector.factory.SingletonStore; +import com.github.Anon8281.universalScheduler.UniversalRunnable; +import fr.xephi.authme.initialization.HasCleanup; + +import javax.inject.Inject; + +/** + * Task run periodically to invoke the cleanup task on services. + */ +public class CleanupTask extends UniversalRunnable { + + @Inject + private SingletonStore hasCleanupStore; + + CleanupTask() { + } + + @Override + public void run() { + hasCleanupStore.retrieveAllOfType() + .forEach(HasCleanup::performCleanup); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/task/MessageTask.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/task/MessageTask.java new file mode 100644 index 00000000..dd11e8ef --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/task/MessageTask.java @@ -0,0 +1,34 @@ +package fr.xephi.authme.task; + +import com.github.Anon8281.universalScheduler.UniversalRunnable; +import org.bukkit.entity.Player; + +/** + * Message shown to a player in a regular interval as long as he is not logged in. + */ +public class MessageTask extends UniversalRunnable { + + private final Player player; + private final String[] message; + private boolean isMuted; + + /* + * Constructor. + */ + public MessageTask(Player player, String[] lines) { + this.player = player; + this.message = lines; + isMuted = false; + } + + public void setMuted(boolean isMuted) { + this.isMuted = isMuted; + } + + @Override + public void run() { + if (!isMuted) { + player.sendMessage(message); + } + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/task/TimeoutTask.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/task/TimeoutTask.java new file mode 100644 index 00000000..60aac741 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/task/TimeoutTask.java @@ -0,0 +1,34 @@ +package fr.xephi.authme.task; + +import fr.xephi.authme.data.auth.PlayerCache; +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 Player player; + private final String message; + private final PlayerCache playerCache; + + /** + * Constructor for TimeoutTask. + * + * @param player the player to check + * @param message the kick message + * @param playerCache player cache instance + */ + public TimeoutTask(Player player, String message, PlayerCache playerCache) { + this.message = message; + this.player = player; + this.playerCache = playerCache; + } + + @Override + public void run() { + if (!playerCache.isAuthenticated(player.getName())) { + player.kickPlayer(message); + } + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/task/Updater.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/task/Updater.java new file mode 100644 index 00000000..5616d88a --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/task/Updater.java @@ -0,0 +1,67 @@ +package fr.xephi.authme.task; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URL; +import java.util.Scanner; + +public class Updater { + private final String currentVersion; + private String latestVersion; + private static boolean isUpdateAvailable = false; + private static final String owner = "HaHaWTH"; + private static final String repo = "AuthMeReReloaded"; + private static final String UPDATE_URL = "https://api.github.com/repos/" + owner + "/" + repo + "/releases/latest"; + + public Updater(String currentVersion) { + this.currentVersion = currentVersion; + } + + + /** + * Check if there is an update available + * Note: This method will perform a network request! + * + * @return true if there is an update available, false otherwise + */ + public boolean isUpdateAvailable() { + URI uri = URI.create(UPDATE_URL); + try { + URL url = uri.toURL(); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setConnectTimeout(10000); + conn.setReadTimeout(10000); + Scanner scanner = new Scanner(conn.getInputStream()); + String response = scanner.useDelimiter("\\Z").next(); + scanner.close(); + String latestVersion = response.substring(response.indexOf("tag_name") + 11); + latestVersion = latestVersion.substring(0, latestVersion.indexOf("\"")); + this.latestVersion = latestVersion; + isUpdateAvailable = !currentVersion.equals(latestVersion); + return isUpdateAvailable; + } catch (IOException ignored) { + this.latestVersion = null; + isUpdateAvailable = false; + return false; + } + } + + public String getLatestVersion() { + return latestVersion; + } + + public String getCurrentVersion() { + return currentVersion; + } + + /** + * Returns true if there is an update available, false otherwise + * Must be called after {@link Updater#isUpdateAvailable()} + * + * @return A boolean indicating whether there is an update available + */ + public static boolean hasUpdate() { + return isUpdateAvailable; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/task/purge/PurgeExecutor.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/task/purge/PurgeExecutor.java new file mode 100644 index 00000000..ea009604 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/task/purge/PurgeExecutor.java @@ -0,0 +1,227 @@ +package fr.xephi.authme.task.purge; + +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.permission.PermissionsManager; +import fr.xephi.authme.service.BukkitService; +import fr.xephi.authme.service.PluginHookService; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.properties.PurgeSettings; +import org.bukkit.ChatColor; +import org.bukkit.OfflinePlayer; +import org.bukkit.Server; + +import javax.inject.Inject; +import java.io.File; +import java.util.Collection; +import java.util.Locale; + +import static fr.xephi.authme.util.FileUtils.makePath; + +/** + * Executes the purge operations. + */ +public class PurgeExecutor { + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(PurgeExecutor.class); + + @Inject + private Settings settings; + + @Inject + private DataSource dataSource; + + @Inject + private PermissionsManager permissionsManager; + + @Inject + private PluginHookService pluginHookService; + + @Inject + private BukkitService bukkitService; + + @Inject + private Server server; + + PurgeExecutor() { + } + + /** + * Performs the purge operations, i.e. deletes data and removes the files associated with the given + * players and names. + * + * @param players the players to purge + * @param names names to purge + */ + public void executePurge(Collection players, Collection names) { + // Purge other data + purgeFromAuthMe(names); + purgeEssentials(players); + purgeDat(players); + purgeLimitedCreative(names); + purgeAntiXray(names); + purgePermissions(players); + } + + /** + * Purges data from the AntiXray plugin. + * + * @param cleared the players whose data should be cleared + */ + synchronized void purgeAntiXray(Collection cleared) { + if (!settings.getProperty(PurgeSettings.REMOVE_ANTI_XRAY_FILE)) { + return; + } + + int i = 0; + File dataFolder = new File(makePath(".", "plugins", "AntiXRayData", "PlayerData")); + if (!dataFolder.exists() || !dataFolder.isDirectory()) { + return; + } + + for (String file : dataFolder.list()) { + if (cleared.contains(file.toLowerCase(Locale.ROOT))) { + File playerFile = new File(dataFolder, file); + if (playerFile.exists() && playerFile.delete()) { + i++; + } + } + } + + logger.info("AutoPurge: Removed " + i + " AntiXRayData Files"); + } + + /** + * Deletes the given accounts from AuthMe. + * + * @param names the name of the accounts to delete + */ + synchronized void purgeFromAuthMe(Collection names) { + dataSource.purgeRecords(names); + //TODO ljacqu 20160717: We shouldn't output namedBanned.size() but the actual total that was deleted + logger.info(ChatColor.GOLD + "Deleted " + names.size() + " user accounts"); + } + + /** + * Purges data from the LimitedCreative plugin. + * + * @param cleared the players whose data should be cleared + */ + synchronized void purgeLimitedCreative(Collection cleared) { + if (!settings.getProperty(PurgeSettings.REMOVE_LIMITED_CREATIVE_INVENTORIES)) { + return; + } + + int i = 0; + File dataFolder = new File(makePath(".", "plugins", "LimitedCreative", "inventories")); + if (!dataFolder.exists() || !dataFolder.isDirectory()) { + return; + } + for (String file : dataFolder.list()) { + String name = file; + int idx; + idx = file.lastIndexOf("_creative.yml"); + if (idx != -1) { + name = name.substring(0, idx); + } else { + idx = file.lastIndexOf("_adventure.yml"); + if (idx != -1) { + name = name.substring(0, idx); + } else { + idx = file.lastIndexOf(".yml"); + if (idx != -1) { + name = name.substring(0, idx); + } + } + } + if (name.equals(file)) { + continue; + } + if (cleared.contains(name.toLowerCase(Locale.ROOT))) { + File dataFile = new File(dataFolder, file); + if (dataFile.exists() && dataFile.delete()) { + i++; + } + } + } + logger.info("AutoPurge: Removed " + i + " LimitedCreative Survival, Creative and Adventure files"); + } + + /** + * Removes the .dat file of the given players. + * + * @param cleared list of players to clear + */ + synchronized void purgeDat(Collection cleared) { + if (!settings.getProperty(PurgeSettings.REMOVE_PLAYER_DAT)) { + return; + } + + int i = 0; + File dataFolder = new File(server.getWorldContainer(), + makePath(settings.getProperty(PurgeSettings.DEFAULT_WORLD), "players")); + + for (OfflinePlayer offlinePlayer : cleared) { + File playerFile = new File(dataFolder, offlinePlayer.getUniqueId() + ".dat"); + if (playerFile.delete()) { + i++; + } + } + + logger.info("AutoPurge: Removed " + i + " .dat Files"); + } + + /** + * Removes the Essentials userdata file of each given player. + * + * @param cleared list of players to clear + */ + synchronized void purgeEssentials(Collection cleared) { + if (!settings.getProperty(PurgeSettings.REMOVE_ESSENTIALS_FILES)) { + return; + } + + File essentialsDataFolder = pluginHookService.getEssentialsDataFolder(); + if (essentialsDataFolder == null) { + logger.info("Cannot purge Essentials: plugin is not loaded"); + return; + } + + final File userDataFolder = new File(essentialsDataFolder, "userdata"); + if (!userDataFolder.exists() || !userDataFolder.isDirectory()) { + return; + } + + int deletedFiles = 0; + for (OfflinePlayer offlinePlayer : cleared) { + File playerFile = new File(userDataFolder, offlinePlayer.getUniqueId() + ".yml"); + if (playerFile.exists() && playerFile.delete()) { + deletedFiles++; + } + } + + logger.info("AutoPurge: Removed " + deletedFiles + " EssentialsFiles"); + } + + /** + * Removes permission data (groups a user belongs to) for the given players. + * + * @param cleared the players to remove data for + */ + synchronized void purgePermissions(Collection cleared) { + if (!settings.getProperty(PurgeSettings.REMOVE_PERMISSIONS)) { + return; + } + + for (OfflinePlayer offlinePlayer : cleared) { + if (!permissionsManager.loadUserData(offlinePlayer)) { + logger.warning("Unable to purge the permissions of user " + offlinePlayer + "!"); + continue; + } + permissionsManager.removeAllGroups(offlinePlayer); + } + + logger.info("AutoPurge: Removed permissions from " + cleared.size() + " player(s)."); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/task/purge/PurgeService.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/task/purge/PurgeService.java new file mode 100644 index 00000000..880d5118 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/task/purge/PurgeService.java @@ -0,0 +1,123 @@ +package fr.xephi.authme.task.purge; + +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.permission.PermissionsManager; +import fr.xephi.authme.service.BukkitService; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.properties.PurgeSettings; +import fr.xephi.authme.util.Utils; +import org.bukkit.OfflinePlayer; +import org.bukkit.command.CommandSender; + +import javax.inject.Inject; +import java.util.Calendar; +import java.util.Collection; +import java.util.Set; + +import static fr.xephi.authme.util.Utils.logAndSendMessage; + +/** + * Initiates purge tasks. + */ +public class PurgeService { + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(PurgeService.class); + + @Inject + private BukkitService bukkitService; + + @Inject + private DataSource dataSource; + + @Inject + private Settings settings; + + @Inject + private PermissionsManager permissionsManager; + + @Inject + private PurgeExecutor purgeExecutor; + + /** Keeps track of whether a purge task is currently running. */ + private boolean isPurging = false; + + PurgeService() { + } + + /** + * Purges players from the database. Runs on startup if enabled. + */ + public void runAutoPurge() { + int daysBeforePurge = settings.getProperty(PurgeSettings.DAYS_BEFORE_REMOVE_PLAYER); + if (!settings.getProperty(PurgeSettings.USE_AUTO_PURGE)) { + return; + } else if (daysBeforePurge <= 0) { + logger.warning("Did not run auto purge: configured days before purging must be positive"); + return; + } + + logger.info("Automatically purging the database..."); + Calendar calendar = Calendar.getInstance(); + calendar.add(Calendar.DATE, -daysBeforePurge); + long until = calendar.getTimeInMillis(); + + runPurge(null, until); + } + + /** + * Runs a purge with a specified last login threshold. Players who haven't logged in since the threshold + * will be purged. + * + * @param sender Sender running the command + * @param until The last login threshold in milliseconds + */ + public void runPurge(CommandSender sender, long until) { + //todo: note this should may run async because it may executes a SQL-Query + Set toPurge = dataSource.getRecordsToPurge(until); + if (Utils.isCollectionEmpty(toPurge)) { + logAndSendMessage(sender, "No players to purge"); + return; + } + + purgePlayers(sender, toPurge, bukkitService.getOfflinePlayers()); + } + + /** + * Purges the given list of player names. + * + * @param sender Sender running the command + * @param names The names to remove + * @param players Collection of OfflinePlayers (including those with the given names) + */ + public void purgePlayers(CommandSender sender, Set names, OfflinePlayer[] players) { + if (isPurging) { + logAndSendMessage(sender, "Purge is already in progress! Aborting purge request"); + return; + } + + isPurging = true; + PurgeTask purgeTask = new PurgeTask(this, permissionsManager, sender, names, players); + bukkitService.runTaskTimerAsynchronously(purgeTask, 0, 1); + } + + /** + * Set if a purge is currently in progress. + * + * @param purging True if purging. + */ + void setPurging(boolean purging) { + this.isPurging = purging; + } + + /** + * Perform purge operations for the given players and names. + * + * @param players the players (associated with the names) + * @param names the lowercase names + */ + void executePurge(Collection players, Collection names) { + purgeExecutor.executePurge(players, names); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/task/purge/PurgeTask.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/task/purge/PurgeTask.java new file mode 100644 index 00000000..15ab6552 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/task/purge/PurgeTask.java @@ -0,0 +1,130 @@ +package fr.xephi.authme.task.purge; + +import com.github.Anon8281.universalScheduler.UniversalRunnable; +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.permission.PermissionsManager; +import fr.xephi.authme.permission.PlayerStatePermission; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.OfflinePlayer; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import java.util.HashSet; +import java.util.Locale; +import java.util.Set; +import java.util.UUID; + +class PurgeTask extends UniversalRunnable { + + //how many players we should check for each tick + private static final int INTERVAL_CHECK = 5; + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(PurgeTask.class); + private final PurgeService purgeService; + private final PermissionsManager permissionsManager; + private final UUID sender; + private final Set toPurge; + + private final OfflinePlayer[] offlinePlayers; + private final int totalPurgeCount; + + private int currentPage = 0; + + /** + * Constructor. + * + * @param service the purge service + * @param permissionsManager the permissions manager + * @param sender the sender who initiated the purge, or null + * @param toPurge lowercase names to purge + * @param offlinePlayers offline players to map to the names + */ + PurgeTask(PurgeService service, PermissionsManager permissionsManager, CommandSender sender, + Set toPurge, OfflinePlayer[] offlinePlayers) { + this.purgeService = service; + this.permissionsManager = permissionsManager; + + if (sender instanceof Player) { + this.sender = ((Player) sender).getUniqueId(); + } else { + this.sender = null; + } + + this.toPurge = toPurge; + this.totalPurgeCount = toPurge.size(); + this.offlinePlayers = offlinePlayers; + } + + @Override + public void run() { + if (toPurge.isEmpty()) { + //everything was removed + finish(); + return; + } + + Set playerPortion = new HashSet<>(INTERVAL_CHECK); + Set namePortion = new HashSet<>(INTERVAL_CHECK); + for (int i = 0; i < INTERVAL_CHECK; i++) { + int nextPosition = (currentPage * INTERVAL_CHECK) + i; + if (offlinePlayers.length <= nextPosition) { + //no more offline players on this page + break; + } + + OfflinePlayer offlinePlayer = offlinePlayers[nextPosition]; + if (offlinePlayer.getName() != null && toPurge.remove(offlinePlayer.getName().toLowerCase(Locale.ROOT))) { + if (!permissionsManager.loadUserData(offlinePlayer)) { + logger.warning("Unable to check if the user " + offlinePlayer.getName() + " can be purged!"); + continue; + } + if (!permissionsManager.hasPermissionOffline(offlinePlayer, PlayerStatePermission.BYPASS_PURGE)) { + playerPortion.add(offlinePlayer); + namePortion.add(offlinePlayer.getName()); + } + } + } + + if (!toPurge.isEmpty() && playerPortion.isEmpty()) { + logger.info("Finished lookup of offlinePlayers. Begin looking purging player names only"); + + //we went through all offlineplayers but there are still names remaining + for (String name : toPurge) { + if (!permissionsManager.hasPermissionOffline(name, PlayerStatePermission.BYPASS_PURGE)) { + namePortion.add(name); + } + } + toPurge.clear(); + } + + currentPage++; + purgeService.executePurge(playerPortion, namePortion); + if (currentPage % 20 == 0) { + int completed = totalPurgeCount - toPurge.size(); + sendMessage("[AuthMe] Purge progress " + completed + '/' + totalPurgeCount); + } + } + + private void finish() { + cancel(); + + // Show a status message + sendMessage(ChatColor.GREEN + "[AuthMe] Database has been purged successfully"); + + logger.info("Purge finished!"); + purgeService.setPurging(false); + } + + private void sendMessage(String message) { + if (sender == null) { + Bukkit.getConsoleSender().sendMessage(message); + } else { + Player player = Bukkit.getPlayer(sender); + if (player != null) { + player.sendMessage(message); + } + } + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/util/AtomicIntervalCounter.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/util/AtomicIntervalCounter.java new file mode 100644 index 00000000..2a50e660 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/util/AtomicIntervalCounter.java @@ -0,0 +1,53 @@ +package fr.xephi.authme.util; + +/** + * A thread-safe interval counter, allows to detect if an event happens more than 'threshold' times + * in the given 'interval'. + */ +public class AtomicIntervalCounter { + private final int threshold; + private final int interval; + private int count; + private long lastInsert; + + /** + * Constructs a new counter. + * + * @param threshold the threshold value of the counter. + * @param interval the counter interval in milliseconds. + */ + public AtomicIntervalCounter(int threshold, int interval) { + this.threshold = threshold; + this.interval = interval; + reset(); + } + + /** + * Resets the counter count. + */ + public synchronized void reset() { + count = 0; + lastInsert = 0; + } + + /** + * Increments the counter and returns true if the current count has reached the threshold value + * in the given interval, this will also reset the count value. + * + * @return true if the count has reached the threshold value. + */ + public synchronized boolean handle() { + long now = System.currentTimeMillis(); + if (now - lastInsert > interval) { + count = 1; + } else { + count++; + } + if (count > threshold) { + reset(); + return true; + } + lastInsert = now; + return false; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/util/ExceptionUtils.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/util/ExceptionUtils.java new file mode 100644 index 00000000..fd5ae885 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/util/ExceptionUtils.java @@ -0,0 +1,46 @@ +package fr.xephi.authme.util; + +import com.google.common.collect.Sets; + +import java.util.Set; + +/** + * Utilities for exceptions. + */ +public final class ExceptionUtils { + + private ExceptionUtils() { + } + + /** + * Returns the first throwable of the given {@code wantedThrowableType} by visiting the provided + * throwable and its causes recursively. + * + * @param wantedThrowableType the throwable type to find + * @param throwable the throwable to start with + * @param the desired throwable subtype + * @return the first throwable found of the given type, or null if none found + */ + public static T findThrowableInCause(Class wantedThrowableType, Throwable throwable) { + Set visitedObjects = Sets.newIdentityHashSet(); + Throwable currentThrowable = throwable; + while (currentThrowable != null && !visitedObjects.contains(currentThrowable)) { + if (wantedThrowableType.isInstance(currentThrowable)) { + return wantedThrowableType.cast(currentThrowable); + } + visitedObjects.add(currentThrowable); + currentThrowable = currentThrowable.getCause(); + } + return null; + } + + /** + * Format the information from a Throwable as string, retaining the type and its message. + * + * @param th the throwable to process + * @return string with the type of the Throwable and its message, e.g. "[IOException]: Could not open stream" + */ + public static String formatException(Throwable th) { + return "[" + th.getClass().getSimpleName() + "]: " + th.getMessage(); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/util/FileUtils.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/util/FileUtils.java new file mode 100644 index 00000000..7f7ee4aa --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/util/FileUtils.java @@ -0,0 +1,173 @@ +package fr.xephi.authme.util; + +import com.google.common.io.Files; +import fr.xephi.authme.AuthMe; +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.output.ConsoleLoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +import static java.lang.String.format; + +/** + * File utilities. + */ +public final class FileUtils { + + private static final DateTimeFormatter CURRENT_DATE_STRING_FORMATTER = + DateTimeFormatter.ofPattern("yyyyMMdd_HHmm"); + + private static ConsoleLogger logger = ConsoleLoggerFactory.get(FileUtils.class); + + // Utility class + private FileUtils() { + } + + /** + * Copy a resource file (from the JAR) to the given file if it doesn't exist. + * + * @param destinationFile The file to check and copy to (outside of JAR) + * @param resourcePath Local path to the resource file (path to file within JAR) + * + * @return False if the file does not exist and could not be copied, true otherwise + */ + public static boolean copyFileFromResource(File destinationFile, String resourcePath) { + if (destinationFile.exists()) { + return true; + } else if (!createDirectory(destinationFile.getParentFile())) { + logger.warning("Cannot create parent directories for '" + destinationFile + "'"); + return false; + } + + try (InputStream is = getResourceFromJar(resourcePath)) { + if (is == null) { + logger.warning(format("Cannot copy resource '%s' to file '%s': cannot load resource", + resourcePath, destinationFile.getPath())); + } else { + java.nio.file.Files.copy(is, destinationFile.toPath()); + return true; + } + } catch (IOException e) { + logger.logException(format("Cannot copy resource '%s' to file '%s':", + resourcePath, destinationFile.getPath()), e); + } + return false; + } + + /** + * Creates the given directory. + * + * @param dir the directory to create + * @return true upon success, false otherwise + */ + public static boolean createDirectory(File dir) { + if (!dir.exists() && !dir.mkdirs()) { + logger.warning("Could not create directory '" + dir + "'"); + return false; + } + return dir.isDirectory(); + } + + /** + * Returns a JAR file as stream. Returns null if it doesn't exist. + * + * @param path the local path (starting from resources project, e.g. "config.yml" for 'resources/config.yml') + * @return the stream if the file exists, or false otherwise + */ + public static InputStream getResourceFromJar(String path) { + // ClassLoader#getResourceAsStream does not deal with the '\' path separator: replace to '/' + final String normalizedPath = path.replace("\\", "/"); + return AuthMe.class.getClassLoader().getResourceAsStream(normalizedPath); + } + + /** + * Delete a given directory and all its content. + * + * @param directory The directory to remove + */ + public static void purgeDirectory(File directory) { + if (!directory.isDirectory()) { + return; + } + File[] files = directory.listFiles(); + if (files == null) { + return; + } + for (File target : files) { + if (target.isDirectory()) { + purgeDirectory(target); + } + delete(target); + } + } + + /** + * Delete the given file or directory and log a message if it was unsuccessful. + * Method is null safe and does nothing when null is passed. + * + * @param file the file to delete + */ + public static void delete(File file) { + if (file != null) { + boolean result = file.delete(); + if (!result) { + logger.warning("Could not delete file '" + file + "'"); + } + } + } + + /** + * Creates the given file or throws an exception. + * + * @param file the file to create + */ + public static void create(File file) { + try { + boolean result = file.createNewFile(); + if (!result) { + throw new IllegalStateException("Could not create file '" + file + "'"); + } + } catch (IOException e) { + throw new IllegalStateException("Error while creating file '" + file + "'", e); + } + } + + /** + * Construct a file path from the given elements, i.e. separate the given elements by the file separator. + * + * @param elements The elements to create a path with + * + * @return The created path + */ + public static String makePath(String... elements) { + return String.join(File.separator, elements); + } + + /** + * Creates a textual representation of the current time (including minutes), e.g. useful for + * automatically generated backup files. + * + * @return string of the current time for use in file names + */ + public static String createCurrentTimeString() { + return LocalDateTime.now().format(CURRENT_DATE_STRING_FORMATTER); + } + + /** + * Returns a path to a new file (which doesn't exist yet) with a timestamp in the name in the same + * folder as the given file and containing the given file's filename. + * + * @param file the file based on which a new file path should be created + * @return path to a file suitably named for storing a backup + */ + public static String createBackupFilePath(File file) { + String filename = "backup_" + Files.getNameWithoutExtension(file.getName()) + + "_" + createCurrentTimeString() + + "." + Files.getFileExtension(file.getName()); + return makePath(file.getParent(), filename); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/util/InternetProtocolUtils.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/util/InternetProtocolUtils.java new file mode 100644 index 00000000..03942154 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/util/InternetProtocolUtils.java @@ -0,0 +1,72 @@ +package fr.xephi.authme.util; + +import java.net.InetAddress; +import java.net.UnknownHostException; + +/** + * Utility class about the InternetProtocol + */ +public final class InternetProtocolUtils { + + // Utility class + private InternetProtocolUtils() { + } + + /** + * Checks if the specified address is a private or loopback address + * + * @param address address to check + * @return true if the address is a local (site and link) or loopback address, false otherwise + */ + public static boolean isLocalAddress(String address) { + try { + InetAddress inetAddress = InetAddress.getByName(address); + + // Examples: 127.0.0.1, localhost or [::1] + return isLoopbackAddress(address) + // Example: 10.0.0.0, 172.16.0.0, 192.168.0.0, fec0::/10 (deprecated) + // Ref: https://en.wikipedia.org/wiki/IP_address#Private_addresses + || inetAddress.isSiteLocalAddress() + // Example: 169.254.0.0/16, fe80::/10 + // Ref: https://en.wikipedia.org/wiki/IP_address#Address_autoconfiguration + || inetAddress.isLinkLocalAddress() + // non deprecated unique site-local that java doesn't check yet -> fc00::/7 + || isIPv6UniqueSiteLocal(inetAddress); + } catch (UnknownHostException e) { + return false; + } + } + + /** + * Checks if the specified address is a loopback address. This can be one of the following: + *

    + *
  • 127.0.0.1
  • + *
  • localhost
  • + *
  • [::1]
  • + *
+ * + * @param address address to check + * @return true if the address is a loopback one + */ + public static boolean isLoopbackAddress(String address) { + try { + InetAddress inetAddress = InetAddress.getByName(address); + return inetAddress.isLoopbackAddress(); + } catch (UnknownHostException e) { + return false; + } + } + + private static boolean isLoopbackAddress(InetAddress address) { + return address.isLoopbackAddress(); + } + + private static boolean isIPv6UniqueSiteLocal(InetAddress address) { + // ref: https://en.wikipedia.org/wiki/Unique_local_address + + // currently undefined but could be used in the near future fc00::/8 + return (address.getAddress()[0] & 0xFF) == 0xFC + // in use for unique site-local fd00::/8 + || (address.getAddress()[0] & 0xFF) == 0xFD; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/util/PlayerUtils.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/util/PlayerUtils.java new file mode 100644 index 00000000..3c9b6f4f --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/util/PlayerUtils.java @@ -0,0 +1,39 @@ +package fr.xephi.authme.util; + +import org.bukkit.entity.Player; + +/** + * Player utilities. + */ +public final class PlayerUtils { + + // Utility class + private PlayerUtils() { + } + private static final boolean isLeavesServer = Utils.isClassLoaded("top.leavesmc.leaves.LeavesConfig") || Utils.isClassLoaded("org.leavesmc.leaves.LeavesConfig"); + + /** + * Returns the IP of the given player. + * + * @param player The player to return the IP address for + * @return The player's IP address + */ + public static String getPlayerIp(Player player) { + return player.getAddress().getAddress().getHostAddress(); + } + + /** + * Returns if the player is an NPC or not. + * + * @param player The player to check + * @return True if the player is an NPC, false otherwise + */ + public static boolean isNpc(Player player) { + if (isLeavesServer) { + return player.hasMetadata("NPC") || player.getAddress() == null; + } else { + return player.hasMetadata("NPC"); + } + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/util/RandomStringUtils.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/util/RandomStringUtils.java new file mode 100644 index 00000000..58018477 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/util/RandomStringUtils.java @@ -0,0 +1,75 @@ +package fr.xephi.authme.util; + +import java.security.SecureRandom; +import java.util.Random; + +/** + * Utility for generating random strings. + */ +public final class RandomStringUtils { + + private static final char[] CHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray(); + private static final Random RANDOM = new SecureRandom(); + private static final int NUM_INDEX = 10; + private static final int LOWER_ALPHANUMERIC_INDEX = 36; + private static final int HEX_MAX_INDEX = 16; + + // Utility class + private RandomStringUtils() { + } + + /** + * Generate a string of the given length consisting of random characters within the range [0-9a-z]. + * + * @param length The length of the random string to generate + * @return The random string + */ + public static String generate(int length) { + return generateString(length, LOWER_ALPHANUMERIC_INDEX); + } + + /** + * Generate a random hexadecimal string of the given length. In other words, the generated string + * contains characters only within the range [0-9a-f]. + * + * @param length The length of the random string to generate + * @return The random hexadecimal string + */ + public static String generateHex(int length) { + return generateString(length, HEX_MAX_INDEX); + } + + /** + * Generate a random numbers string of the given length. In other words, the generated string + * contains characters only within the range [0-9]. + * + * @param length The length of the random string to generate + * @return The random numbers string + */ + public static String generateNum(int length) { + return generateString(length, NUM_INDEX); + } + + /** + * Generate a random string with digits and lowercase and uppercase letters. The result of this + * method matches the pattern [0-9a-zA-Z]. + * + * @param length The length of the random string to generate + * @return The random string + */ + public static String generateLowerUpper(int length) { + return generateString(length, CHARS.length); + } + + private static String generateString(int length, int maxIndex) { + if (length < 0) { + throw new IllegalArgumentException("Length must be positive but was " + length); + } + StringBuilder sb = new StringBuilder(length); + for (int i = 0; i < length; ++i) { + sb.append(CHARS[RANDOM.nextInt(maxIndex)]); + } + return sb.toString(); + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/util/StringUtils.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/util/StringUtils.java new file mode 100644 index 00000000..8a863678 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/util/StringUtils.java @@ -0,0 +1,80 @@ +package fr.xephi.authme.util; + +import net.ricecode.similarity.LevenshteinDistanceStrategy; +import net.ricecode.similarity.StringSimilarityService; +import net.ricecode.similarity.StringSimilarityServiceImpl; + +/** + * Utility class for String operations. + */ +public final class StringUtils { + + // Utility class + private StringUtils() { + } + + /** + * Calculates the difference of two strings. + * + * @param first first string + * @param second second string + * @return the difference value + */ + public static double getDifference(String first, String second) { + // Make sure the strings are valid. + if (first == null || second == null) { + return 1.0; + } + + // Create a string similarity service instance, to allow comparison + StringSimilarityService service = new StringSimilarityServiceImpl(new LevenshteinDistanceStrategy()); + + // Determine the difference value, return the result + return Math.abs(service.score(first, second) - 1.0); + } + + /** + * Returns whether the given string contains any of the provided elements. + * + * @param str the string to analyze + * @param pieces the items to check the string for + * @return true if the string contains at least one of the items + */ + public static boolean containsAny(String str, Iterable pieces) { + if (str == null) { + return false; + } + for (String piece : pieces) { + if (piece != null && str.contains(piece)) { + return true; + } + } + return false; + } + + /** + * Null-safe method for checking whether a string is empty. Note that the string + * is trimmed, so this method also considers a string with whitespace as empty. + * + * @param str the string to verify + * @return true if the string is empty, false otherwise + */ + public static boolean isBlank(String str) { + return str == null || str.trim().isEmpty(); + } + + /** + * Checks that the given needle is in the middle of the haystack, i.e. that the haystack + * contains the needle and that it is not at the very start or end. + * + * @param needle the needle to search for + * @param haystack the haystack to search in + * @return true if the needle is in the middle of the word, false otherwise + */ + // Note ljacqu 20170314: `needle` is restricted to char type intentionally because something like + // isInsideString("11", "2211") would unexpectedly return true... + public static boolean isInsideString(char needle, String haystack) { + int index = haystack.indexOf(needle); + return index > 0 && index < haystack.length() - 1; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/util/TeleportUtils.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/util/TeleportUtils.java new file mode 100644 index 00000000..4a043b17 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/util/TeleportUtils.java @@ -0,0 +1,44 @@ +package fr.xephi.authme.util; + +import org.bukkit.Location; +import org.bukkit.entity.Player; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.util.concurrent.CompletableFuture; + +/** + * This class is a utility class for handling async teleportation of players in game. + */ +public class TeleportUtils { + private static MethodHandle teleportAsyncMethodHandle; + + static { + try { + MethodHandles.Lookup lookup = MethodHandles.lookup(); + teleportAsyncMethodHandle = lookup.findVirtual(Player.class, "teleportAsync", MethodType.methodType(CompletableFuture.class, Location.class)); + } catch (NoSuchMethodException | IllegalAccessException e) { + teleportAsyncMethodHandle = null; + // if not, set method handle to null + } + } + + /** + * Teleport a player to a specified location. + * + * @param player The player to be teleported + * @param location Where should the player be teleported + */ + public static void teleport(Player player, Location location) { + if (teleportAsyncMethodHandle != null) { + try { + teleportAsyncMethodHandle.invoke(player, location); + } catch (Throwable throwable) { + player.teleport(location); + } + } else { + player.teleport(location); + } + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/util/Utils.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/util/Utils.java new file mode 100644 index 00000000..324a8eed --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/util/Utils.java @@ -0,0 +1,119 @@ +package fr.xephi.authme.util; + +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.ConsoleCommandSender; + +import java.util.Collection; +import java.util.regex.Pattern; + +/** + * Utility class for various operations used in the codebase. + */ +public final class Utils { + + /** Number of milliseconds in a minute. */ + public static final long MILLIS_PER_MINUTE = 60_000L; + + private static ConsoleLogger logger = ConsoleLoggerFactory.get(Utils.class); + + // Utility class + private Utils() { + } + + /** + * Compile Pattern sneaky without throwing Exception. + * + * @param pattern pattern string to compile + * + * @return the given regex compiled into Pattern object. + */ + public static Pattern safePatternCompile(String pattern) { + try { + return Pattern.compile(pattern); + } catch (Exception e) { + logger.warning("Failed to compile pattern '" + pattern + "' - defaulting to allowing everything"); + return Pattern.compile(".*?"); + } + } + + /** + * Returns whether the class exists in the current class loader. + * + * @param className the class name to check + * + * @return true if the class is loaded, false otherwise + */ + public static boolean isClassLoaded(String className) { + try { + Class.forName(className); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } + + /** + * Sends a message to the given sender (null safe), and logs the message to the console. + * This method is aware that the command sender might be the console sender and avoids + * displaying the message twice in this case. + * + * @param sender the sender to inform + * @param message the message to log and send + */ + public static void logAndSendMessage(CommandSender sender, String message) { + logger.info(message); + // Make sure sender is not console user, which will see the message from ConsoleLogger already + if (sender != null && !(sender instanceof ConsoleCommandSender)) { + sender.sendMessage(message); + } + } + + /** + * Sends a warning to the given sender (null safe), and logs the warning to the console. + * This method is aware that the command sender might be the console sender and avoids + * displaying the message twice in this case. + * + * @param sender the sender to inform + * @param message the warning to log and send + */ + public static void logAndSendWarning(CommandSender sender, String message) { + logger.warning(message); + // Make sure sender is not console user, which will see the message from ConsoleLogger already + if (sender != null && !(sender instanceof ConsoleCommandSender)) { + sender.sendMessage(ChatColor.RED + message); + } + } + + /** + * Null-safe way to check whether a collection is empty or not. + * + * @param coll The collection to verify + * @return True if the collection is null or empty, false otherwise + */ + public static boolean isCollectionEmpty(Collection coll) { + return coll == null || coll.isEmpty(); + } + + /** + * Returns whether the given email is empty or equal to the standard "undefined" email address. + * + * @param email the email to check + * + * @return true if the email is empty + */ + public static boolean isEmailEmpty(String email) { + return StringUtils.isBlank(email) || "your@email.com".equalsIgnoreCase(email); + } + + private final static String[] serverVersion = Bukkit.getServer().getBukkitVersion() + .substring(0, Bukkit.getServer().getBukkitVersion().indexOf("-")) + .split("\\."); + + private final static int FIRST_VERSION = Integer.parseInt(serverVersion[0]); + public final static int MAJOR_VERSION = Integer.parseInt(serverVersion[1]); + public final static int MINOR_VERSION = serverVersion.length == 3 ? Integer.parseInt(serverVersion[2]) : 0; +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/util/UuidUtils.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/util/UuidUtils.java new file mode 100644 index 00000000..be35a935 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/util/UuidUtils.java @@ -0,0 +1,27 @@ +package fr.xephi.authme.util; + +import java.util.UUID; + +/** + * Utility class for various operations on UUID. + */ +public final class UuidUtils { + + // Utility class + private UuidUtils() { + } + + /** + * Returns the given string as an UUID or null. + * + * @param string the uuid to parse + * @return parsed UUID if succeeded or null + */ + public static UUID parseUuidSafely(String string) { + try { + return string == null ? null : UUID.fromString(string); + } catch (IllegalArgumentException ex) { + return null; + } + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/util/expiring/Duration.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/util/expiring/Duration.java new file mode 100644 index 00000000..ecc86299 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/util/expiring/Duration.java @@ -0,0 +1,72 @@ +package fr.xephi.authme.util.expiring; + +import java.util.concurrent.TimeUnit; + +/** + * Represents a duration in time, defined by a time unit and a duration. + */ +public class Duration { + + private final long duration; + private final TimeUnit unit; + + /** + * Constructor. + * + * @param duration the duration + * @param unit the time unit in which {@code duration} is expressed + */ + public Duration(long duration, TimeUnit unit) { + this.duration = duration; + this.unit = unit; + } + + /** + * Creates a Duration object for the given duration and unit in the most suitable time unit. + * For example, {@code createWithSuitableUnit(120, TimeUnit.SECONDS)} will return a Duration + * object of 2 minutes. + *

+ * This method only considers the time units days, hours, minutes, and seconds for the objects + * it creates. Conversion is done with {@link TimeUnit#convert} and so always rounds the + * results down. + *

+ * Further examples: + * createWithSuitableUnit(299, TimeUnit.MINUTES); // 4 hours + * createWithSuitableUnit(700, TimeUnit.MILLISECONDS); // 0 seconds + * + * @param sourceDuration the duration + * @param sourceUnit the time unit the duration is expressed in + * @return Duration object using the most suitable time unit + */ + public static Duration createWithSuitableUnit(long sourceDuration, TimeUnit sourceUnit) { + long durationMillis = Math.abs(TimeUnit.MILLISECONDS.convert(sourceDuration, sourceUnit)); + + TimeUnit targetUnit; + if (durationMillis > 1000L * 60L * 60L * 24L) { + targetUnit = TimeUnit.DAYS; + } else if (durationMillis > 1000L * 60L * 60L) { + targetUnit = TimeUnit.HOURS; + } else if (durationMillis > 1000L * 60L) { + targetUnit = TimeUnit.MINUTES; + } else { + targetUnit = TimeUnit.SECONDS; + } + + long durationInTargetUnit = targetUnit.convert(sourceDuration, sourceUnit); + return new Duration(durationInTargetUnit, targetUnit); + } + + /** + * @return the duration + */ + public long getDuration() { + return duration; + } + + /** + * @return the time unit in which the duration is expressed + */ + public TimeUnit getTimeUnit() { + return unit; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/util/expiring/ExpiringMap.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/util/expiring/ExpiringMap.java new file mode 100644 index 00000000..3bf19351 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/util/expiring/ExpiringMap.java @@ -0,0 +1,138 @@ +package fr.xephi.authme.util.expiring; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; + +/** + * Map with expiring entries. Following a configured amount of time after + * an entry has been inserted, the map will act as if the entry does not + * exist. + *

+ * Time starts counting directly after insertion. Inserting a new entry with + * a key that already has a value will "reset" the expiration. Although the + * expiration can be redefined later on, only entries which are inserted + * afterwards will use the new expiration. + *

+ * An expiration of {@code <= 0} will make the map expire all entries + * immediately after insertion. Note that the map does not remove expired + * entries automatically; this is only done when calling + * {@link #removeExpiredEntries()}. + * + * @param the key type + * @param the value type + */ +public class ExpiringMap { + + private final Map> entries = new ConcurrentHashMap<>(); + private long expirationMillis; + + /** + * Constructor. + * + * @param duration the duration of time after which entries expire + * @param unit the time unit in which {@code duration} is expressed + */ + public ExpiringMap(long duration, TimeUnit unit) { + setExpiration(duration, unit); + } + + /** + * Returns the value associated with the given key, + * if available and not expired. + * + * @param key the key to look up + * @return the associated value, or {@code null} if not available + */ + public V get(K key) { + ExpiringEntry value = entries.get(key); + if (value == null) { + return null; + } else if (System.currentTimeMillis() > value.getExpiration()) { + entries.remove(key); + return null; + } + return value.getValue(); + } + + /** + * Inserts a value for the given key. Overwrites a previous value + * for the key if it exists. + * + * @param key the key to insert a value for + * @param value the value to insert + */ + public void put(K key, V value) { + long expiration = System.currentTimeMillis() + expirationMillis; + entries.put(key, new ExpiringEntry<>(value, expiration)); + } + + /** + * Removes the value for the given key, if available. + * + * @param key the key to remove the value for + */ + public void remove(K key) { + entries.remove(key); + } + + /** + * Removes all entries which have expired from the internal structure. + */ + public void removeExpiredEntries() { + entries.entrySet().removeIf(entry -> System.currentTimeMillis() > entry.getValue().getExpiration()); + } + + /** + * Sets a new expiration duration. Note that already present entries + * will still make use of the old expiration. + * + * @param duration the duration of time after which entries expire + * @param unit the time unit in which {@code duration} is expressed + */ + public void setExpiration(long duration, TimeUnit unit) { + this.expirationMillis = unit.toMillis(duration); + } + + /** + * Returns whether this map is empty. This reflects the state of the + * internal map, which may contain expired entries only. The result + * may change after running {@link #removeExpiredEntries()}. + * + * @return true if map is really empty, false otherwise + */ + public boolean isEmpty() { + return entries.isEmpty(); + } + + /** + * @return the internal map + */ + protected Map> getEntries() { + return entries; + } + + /** + * Class holding a value paired with an expiration timestamp. + * + * @param the value type + */ + protected static final class ExpiringEntry { + + private final V value; + private final long expiration; + + ExpiringEntry(V value, long expiration) { + this.value = value; + this.expiration = expiration; + } + + V getValue() { + return value; + } + + long getExpiration() { + return expiration; + } + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/util/expiring/ExpiringSet.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/util/expiring/ExpiringSet.java new file mode 100644 index 00000000..fea8fb31 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/util/expiring/ExpiringSet.java @@ -0,0 +1,125 @@ +package fr.xephi.authme.util.expiring; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; + +/** + * Set whose entries expire after a configurable amount of time. Once an entry + * has expired, the set will act as if the entry no longer exists. Time starts + * counting after the entry has been inserted. + *

+ * Internally, expired entries are not guaranteed to be cleared automatically. + * A cleanup of all expired entries may be triggered with + * {@link #removeExpiredEntries()}. Adding an entry that is already present + * effectively resets its expiration. + * + * @param the type of the entries + */ +public class ExpiringSet { + + private Map entries = new ConcurrentHashMap<>(); + private long expirationMillis; + + /** + * Constructor. + * + * @param duration the duration of time after which entries expire + * @param unit the time unit in which {@code duration} is expressed + */ + public ExpiringSet(long duration, TimeUnit unit) { + setExpiration(duration, unit); + } + + /** + * Adds an entry to the set. + * + * @param entry the entry to add + */ + public void add(E entry) { + entries.put(entry, System.currentTimeMillis() + expirationMillis); + } + + /** + * Returns whether this set contains the given entry, if it hasn't expired. + * + * @param entry the entry to check + * @return true if the entry is present and not expired, false otherwise + */ + public boolean contains(E entry) { + Long expiration = entries.get(entry); + if (expiration == null) { + return false; + } else if (expiration > System.currentTimeMillis()) { + return true; + } else { + entries.remove(entry); + return false; + } + } + + /** + * Removes the given entry from the set (if present). + * + * @param entry the entry to remove + */ + public void remove(E entry) { + entries.remove(entry); + } + + /** + * Removes all entries from the set. + */ + public void clear() { + entries.clear(); + } + + /** + * Removes all entries which have expired from the internal structure. + */ + public void removeExpiredEntries() { + entries.entrySet().removeIf(entry -> System.currentTimeMillis() > entry.getValue()); + } + + /** + * Returns the duration of the entry until it expires (provided it is not removed or re-added). + * If the entry does not exist, a duration of -1 seconds is returned. + * + * @param entry the entry whose duration before it expires should be returned + * @return duration the entry will remain in the set (if there are not modifications) + */ + public Duration getExpiration(E entry) { + Long expiration = entries.get(entry); + if (expiration == null) { + return new Duration(-1, TimeUnit.SECONDS); + } + long stillPresentMillis = expiration - System.currentTimeMillis(); + if (stillPresentMillis < 0) { + entries.remove(entry); + return new Duration(-1, TimeUnit.SECONDS); + } + return Duration.createWithSuitableUnit(stillPresentMillis, TimeUnit.MILLISECONDS); + } + + /** + * Sets a new expiration duration. Note that already present entries + * will still make use of the old expiration. + * + * @param duration the duration of time after which entries expire + * @param unit the time unit in which {@code duration} is expressed + */ + public void setExpiration(long duration, TimeUnit unit) { + this.expirationMillis = unit.toMillis(duration); + } + + /** + * Returns whether this map is empty. This reflects the state of the + * internal map, which may contain expired entries only. The result + * may change after running {@link #removeExpiredEntries()}. + * + * @return true if map is really empty, false otherwise + */ + public boolean isEmpty() { + return entries.isEmpty(); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/util/expiring/TimedCounter.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/util/expiring/TimedCounter.java new file mode 100644 index 00000000..d6d8bba1 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/util/expiring/TimedCounter.java @@ -0,0 +1,70 @@ +package fr.xephi.authme.util.expiring; + +import java.util.concurrent.TimeUnit; + +/** + * Keeps a count per key which expires after a configurable amount of time. + *

+ * Once the expiration of an entry has been reached, the counter resets + * to 0. The counter returns 0 rather than {@code null} for any given key. + * + * @param the type of the key + */ +public class TimedCounter extends ExpiringMap { + + /** + * Constructor. + * + * @param duration the duration of time after which entries expire + * @param unit the time unit in which {@code duration} is expressed + */ + public TimedCounter(long duration, TimeUnit unit) { + super(duration, unit); + } + + @Override + public Integer get(K key) { + Integer value = super.get(key); + return value == null ? 0 : value; + } + + /** + * Increments the value stored for the provided key. + * + * @param key the key to increment the counter for + */ + public void increment(K key) { + put(key, get(key) + 1); + } + + /** + * Decrements the value stored for the provided key. + * This method will NOT update the expiration. + * + * @param key the key to increment the counter for + */ + public void decrement(K key) { + ExpiringEntry e = getEntries().get(key); + + if (e != null) { + if (e.getValue() <= 0) { + remove(key); + } else { + getEntries().put(key, new ExpiringEntry<>(e.getValue() - 1, e.getExpiration())); + } + } + } + + /** + * Calculates the total of all non-expired entries in this counter. + * + * @return the total of all valid entries + */ + public int total() { + long currentTime = System.currentTimeMillis(); + return getEntries().values().stream() + .filter(entry -> currentTime <= entry.getExpiration()) + .map(ExpiringEntry::getValue) + .reduce(0, Integer::sum); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/util/lazytags/DependentTag.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/util/lazytags/DependentTag.java new file mode 100644 index 00000000..5fb0cd3d --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/util/lazytags/DependentTag.java @@ -0,0 +1,35 @@ +package fr.xephi.authme.util.lazytags; + +import java.util.function.Function; + +/** + * Replaceable tag whose value depends on an argument. + * + * @param the argument type + */ +public class DependentTag implements Tag { + + private final String name; + private final Function replacementFunction; + + /** + * Constructor. + * + * @param name the tag (placeholder) that will be replaced + * @param replacementFunction the function producing the replacement + */ + public DependentTag(String name, Function replacementFunction) { + this.name = name; + this.replacementFunction = replacementFunction; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getValue(A argument) { + return replacementFunction.apply(argument); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/util/lazytags/SimpleTag.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/util/lazytags/SimpleTag.java new file mode 100644 index 00000000..a5bb58a2 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/util/lazytags/SimpleTag.java @@ -0,0 +1,29 @@ +package fr.xephi.authme.util.lazytags; + +import java.util.function.Supplier; + +/** + * Tag to be replaced that does not depend on an argument. + * + * @param type of the argument (not used in this implementation) + */ +public class SimpleTag implements Tag { + + private final String name; + private final Supplier replacementFunction; + + public SimpleTag(String name, Supplier replacementFunction) { + this.name = name; + this.replacementFunction = replacementFunction; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getValue(A argument) { + return replacementFunction.get(); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/util/lazytags/Tag.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/util/lazytags/Tag.java new file mode 100644 index 00000000..2c7c6ba5 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/util/lazytags/Tag.java @@ -0,0 +1,22 @@ +package fr.xephi.authme.util.lazytags; + +/** + * Represents a tag in a text to be replaced with a value (which may depend on some argument). + * + * @param argument type the replacement may depend on + */ +public interface Tag { + + /** + * @return the tag to replace + */ + String getName(); + + /** + * Returns the value to replace the tag with for the given argument. + * + * @param argument the argument to evaluate the replacement for + * @return the replacement + */ + String getValue(A argument); +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/util/lazytags/TagBuilder.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/util/lazytags/TagBuilder.java new file mode 100644 index 00000000..677b30e2 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/util/lazytags/TagBuilder.java @@ -0,0 +1,21 @@ +package fr.xephi.authme.util.lazytags; + +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * Utility class for creating tags. + */ +public final class TagBuilder { + + private TagBuilder() { + } + + public static Tag createTag(String name, Function replacementFunction) { + return new DependentTag<>(name, replacementFunction); + } + + public static Tag createTag(String name, Supplier replacementFunction) { + return new SimpleTag<>(name, replacementFunction); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/util/lazytags/TagReplacer.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/util/lazytags/TagReplacer.java new file mode 100644 index 00000000..a9d19319 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/util/lazytags/TagReplacer.java @@ -0,0 +1,102 @@ +package fr.xephi.authme.util.lazytags; + +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Replaces tags lazily by first determining which tags are being used + * and only applying those replacements afterwards. + * + * @param the argument type + */ +public final class TagReplacer { + + private final List> tags; + private final Collection messages; + + /** + * Private constructor. Use {@link #newReplacer(Collection, Collection)}. + * + * @param tags the tags that are being used in the messages + * @param messages the messages + */ + private TagReplacer(List> tags, Collection messages) { + this.tags = tags; + this.messages = messages; + } + + /** + * Creates a new instance of this class, which will provide the given + * messages adapted with the provided tags. + * + * @param allTags all available tags + * @param messages the messages to use + * @param the argument type + * @return new tag replacer instance + */ + public static TagReplacer newReplacer(Collection> allTags, Collection messages) { + List> usedTags = determineUsedTags(allTags, messages); + return new TagReplacer<>(usedTags, messages); + } + + /** + * Returns the messages with the tags applied for the given argument. + * + * @param argument the argument to get the messages for + * @return the adapted messages + */ + public List getAdaptedMessages(A argument) { + // Note ljacqu 20170121: Using a Map might seem more natural here but we avoid doing so for performance + // Although the performance gain here is probably minimal... + List tagValues = new LinkedList<>(); + for (Tag tag : tags) { + tagValues.add(new TagValue(tag.getName(), tag.getValue(argument))); + } + + List adaptedMessages = new LinkedList<>(); + for (String line : messages) { + String adaptedLine = line; + for (TagValue tagValue : tagValues) { + adaptedLine = adaptedLine.replace(tagValue.tag, tagValue.value); + } + adaptedMessages.add(adaptedLine); + } + return adaptedMessages; + } + + /** + * Determines which tags are used somewhere in the given list of messages. + * + * @param allTags all available tags + * @param messages the messages + * @param argument type + * @return tags used at least once + */ + private static List> determineUsedTags(Collection> allTags, Collection messages) { + return allTags.stream() + .filter(tag -> messages.stream().anyMatch(msg -> msg.contains(tag.getName()))) + .collect(Collectors.toList()); + } + + /** (Tag, value) pair. */ + private static final class TagValue { + + /** The tag to replace. */ + private final String tag; + /** The value to replace with. */ + private final String value; + + TagValue(String tag, String value) { + this.tag = tag; + this.value = value; + } + + @Override + public String toString() { + return "TagValue[tag='" + tag + "', value='" + value + "']"; + } + } + +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/util/lazytags/WrappedTagReplacer.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/util/lazytags/WrappedTagReplacer.java new file mode 100644 index 00000000..ce9487e2 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/util/lazytags/WrappedTagReplacer.java @@ -0,0 +1,61 @@ +package fr.xephi.authme.util.lazytags; + +import java.util.Collection; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * Applies tags lazily to the String property of an item. This class wraps + * a {@link TagReplacer} with the extraction of the String property and + * the creation of new items with the adapted string property. + * + * @param the item type + * @param the argument type to evaluate the replacements + */ +public class WrappedTagReplacer { + + private final Collection items; + private final BiFunction itemCreator; + private final TagReplacer tagReplacer; + + /** + * Constructor. + * + * @param allTags all available tags + * @param items the items to apply the replacements on + * @param stringGetter getter of the String property to adapt on the items + * @param itemCreator a function taking (T, String): the original item and the adapted String, returning a new item + */ + public WrappedTagReplacer(Collection> allTags, + Collection items, + Function stringGetter, + BiFunction itemCreator) { + this.items = items; + this.itemCreator = itemCreator; + + List stringItems = items.stream().map(stringGetter).collect(Collectors.toList()); + tagReplacer = TagReplacer.newReplacer(allTags, stringItems); + } + + /** + * Creates adapted items for the given argument. + * + * @param argument the argument to adapt the items for + * @return the adapted items + */ + public List getAdaptedItems(A argument) { + List adaptedStrings = tagReplacer.getAdaptedMessages(argument); + List adaptedItems = new LinkedList<>(); + + Iterator originalItemsIter = items.iterator(); + Iterator newStringsIter = adaptedStrings.iterator(); + while (originalItemsIter.hasNext() && newStringsIter.hasNext()) { + adaptedItems.add(itemCreator.apply(originalItemsIter.next(), newStringsIter.next())); + } + return adaptedItems; + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/util/message/I18NUtils.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/util/message/I18NUtils.java new file mode 100644 index 00000000..da1d6769 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/util/message/I18NUtils.java @@ -0,0 +1,102 @@ +package fr.xephi.authme.util.message; + +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.properties.PluginSettings; +import fr.xephi.authme.util.Utils; +import org.bukkit.entity.Player; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +public class I18NUtils { + + private static Map PLAYER_LOCALE = new ConcurrentHashMap<>(); + private static final Map LOCALE_MAP = new HashMap<>(); + private static final List LOCALE_LIST = Arrays.asList( + "en", "bg", "de", "eo", "es", "et", "eu", "fi", "fr", "gl", "hu", "id", "it", "ja", "ko", "lt", "nl", "pl", + "pt", "ro", "ru", "sk", "sr", "tr", "uk" + ); + + static { + LOCALE_MAP.put("pt_br", "br"); + LOCALE_MAP.put("cs_cz", "cz"); + LOCALE_MAP.put("nds_de", "de"); + LOCALE_MAP.put("sxu", "de"); + LOCALE_MAP.put("swg", "de"); + LOCALE_MAP.put("rpr", "ru"); + LOCALE_MAP.put("sl_si", "si"); + LOCALE_MAP.put("vi_vn", "vn"); + LOCALE_MAP.put("lzh", "zhcn"); + LOCALE_MAP.put("zh_cn", "zhcn"); + LOCALE_MAP.put("zh_hk", "zhhk"); + LOCALE_MAP.put("zh_tw", "zhtw"); + //LOCALE_MAP.put("zhmc", "zhmc"); + } + + /** + * Returns the locale that player uses. + * + * @param player The player + */ + public static String getLocale(Player player) { + if (Utils.MAJOR_VERSION > 15) { + return player.getLocale().toLowerCase(); + } else if (PLAYER_LOCALE.containsKey(player.getUniqueId())) { + return PLAYER_LOCALE.get(player.getUniqueId()); + } + + return null; + } + + public static void addLocale(UUID uuid, String locale) { + if (PLAYER_LOCALE == null) { + PLAYER_LOCALE = new ConcurrentHashMap<>(); + } + + PLAYER_LOCALE.put(uuid, locale); + } + + public static void removeLocale(UUID uuid) { + PLAYER_LOCALE.remove(uuid); + } + + /** + * Returns the AuthMe messages file language code, by given locale and settings. + * Dreeam - Hard mapping, based on mc1.20.6, 5/29/2024 + * + * @param locale The locale that player client setting uses. + * @param settings The AuthMe settings, for default/fallback language usage. + */ + public static String localeToCode(String locale, Settings settings) { + // Certain locale code to AuthMe language code redirect + if (!settings.getProperty(PluginSettings.I18N_CODE_REDIRECT).isEmpty()) { + for (String raw : settings.getProperty(PluginSettings.I18N_CODE_REDIRECT)) { + String[] split = raw.split(":"); + + if (split.length == 2 && locale.equalsIgnoreCase(split[0])) { + return split[1]; + } + } + } + + // Match certain locale code + if (LOCALE_MAP.containsKey(locale)) { + return LOCALE_MAP.get(locale); + } + + if (locale.contains("_")) { + locale = locale.substring(0, locale.indexOf("_")); + } + + // Match locale code with "_" + if (LOCALE_LIST.contains(locale)) { + return locale; + } + + return settings.getProperty(PluginSettings.MESSAGES_LANGUAGE); + } +} diff --git a/plugin/platform-bukkit/src/main/java/fr/xephi/authme/util/message/MiniMessageUtils.java b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/util/message/MiniMessageUtils.java new file mode 100644 index 00000000..36756c62 --- /dev/null +++ b/plugin/platform-bukkit/src/main/java/fr/xephi/authme/util/message/MiniMessageUtils.java @@ -0,0 +1,22 @@ +package fr.xephi.authme.util.message; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; + +public class MiniMessageUtils { + private static final MiniMessage miniMessage = MiniMessage.miniMessage(); + + /** + * Parse a MiniMessage string into a legacy string. + * + * @param message The message to parse. + * @return The parsed message. + */ + public static String parseMiniMessageToLegacy(String message) { + Component component = miniMessage.deserialize(message); + return LegacyComponentSerializer.legacyAmpersand().serialize(component); + } + private MiniMessageUtils() { + } +} diff --git a/plugin/platform-bukkit/src/main/resources/GeoLite2-Country.mmdb b/plugin/platform-bukkit/src/main/resources/GeoLite2-Country.mmdb new file mode 100644 index 00000000..9f580bdf Binary files /dev/null and b/plugin/platform-bukkit/src/main/resources/GeoLite2-Country.mmdb differ diff --git a/plugin/platform-bukkit/src/main/resources/commands.yml b/plugin/platform-bukkit/src/main/resources/commands.yml new file mode 100644 index 00000000..0ba270d6 --- /dev/null +++ b/plugin/platform-bukkit/src/main/resources/commands.yml @@ -0,0 +1,55 @@ + +# This configuration file allows you to execute commands on various events. +# Supported placeholders in commands: +# %p is replaced with the player name. +# %nick is replaced with the player's nick name +# %ip is replaced with the player's IP address +# %country is replaced with the player's country +# +# For example, if you want to send a welcome message to a player who just registered: +# onRegister: +# welcome: +# command: 'msg %p Welcome to the server!' +# executor: CONSOLE +# +# This will make the console execute the msg command to the player. +# Each command under an event has a name you can choose freely (e.g. 'welcome' as above), +# after which a mandatory 'command' field defines the command to run, +# and 'executor' defines who will run the command (either PLAYER or CONSOLE). Longer example: +# onLogin: +# welcome: +# command: 'msg %p Welcome back!' +# executor: PLAYER +# broadcast: +# command: 'broadcast %p has joined, welcome back!' +# executor: CONSOLE +# +# You can also add delay to command. It will run after the specified ticks. Example: +# onLogin: +# rules: +# command: 'rules' +# executor: PLAYER +# delay: 200 +# +# Supported command events: onLogin, onSessionLogin, onFirstLogin, onJoin, onLogout, onRegister, onUnregister +# +# For onLogin and onFirstLogin, you can use 'ifNumberOfAccountsLessThan' and 'ifNumberOfAccountsAtLeast' +# to specify limits to how many accounts a player can have (matched by IP) for a command to be run: +# onLogin: +# warnOnManyAccounts: +# command: 'say Uh oh! %p has many alt accounts!' +# executor: CONSOLE +# ifNumberOfAccountsAtLeast: 5 +# Commands to run for players logging in whose 'last login date' was empty +onFirstLogin: {} +onJoin: {} +onLogin: {} +# These commands are called whenever a logged in player uses /logout or quits. +# The commands are not run if a player that was not logged in quits the server. +# Note: if your server crashes, these commands won't be run, so don't rely on them to undo +# 'onLogin' commands that would be dangerous for non-logged in players to have! +onLogout: {} +onRegister: {} +onSessionLogin: {} +# Commands to run whenever a player is unregistered (by himself, or by an admin) +onUnregister: {} diff --git a/plugin/platform-bukkit/src/main/resources/email.html b/plugin/platform-bukkit/src/main/resources/email.html new file mode 100644 index 00000000..7b9e77fa --- /dev/null +++ b/plugin/platform-bukkit/src/main/resources/email.html @@ -0,0 +1,121 @@ +

diff --git a/plugin/platform-bukkit/src/main/resources/messages/help_bg.yml b/plugin/platform-bukkit/src/main/resources/messages/help_bg.yml new file mode 100644 index 00000000..e530a5aa --- /dev/null +++ b/plugin/platform-bukkit/src/main/resources/messages/help_bg.yml @@ -0,0 +1,335 @@ +common: + header: ==========[ AuthMeReloaded - Помощ ]========== + optional: Опция + hasPermission: Имате права + noPermission: Нямате права + default: Стандартно + result: Резултат + defaultPermissions: + notAllowed: Не е разрешено + opOnly: Само за Оператори (OP) + allowed: Позволено за всички +section: + command: Команда + description: Кратко описание + detailedDescription: Подробно описание + arguments: Аргументи + alternatives: Алтернативи + permissions: Права + children: Команди +commands: + authme: + description: AuthMe op commands + detailedDescription: The main AuthMeReloaded command. The root for all admin commands. + authme.register: + description: Регистриране на играч + detailedDescription: Регистриране на играч с дадена парола. + arg1: + label: Играч + description: Име на играча + arg2: + label: парола + description: Парола + authme.unregister: + description: Премахване на регистрацията на играча + detailedDescription: Премахване на регистрацията на даден играч от базата данни. + arg1: + label: играч + description: име на играча + authme.forcelogin: + description: Удостоверяване на играча + detailedDescription: Удостоверяване на даденият играч. + arg1: + label: играч + description: име на играча + authme.password: + description: Промяна на парола + detailedDescription: Променя паролата на дадения играч. + arg1: + label: играч + description: име на играча + arg2: + label: парола + description: нова парола + authme.lastlogin: + description: Последен вход на играча + detailedDescription: Показва дата и час на последното влизане на дадения играч. + arg1: + label: играч + description: име на играча + authme.accounts: + description: Акаунти на играча + detailedDescription: Изброява всички акаунти на играча, според името и IP-адреса + arg1: + label: играч + description: име на играча или IP-адрес + authme.email: + description: Имейл адрес на играча + detailedDescription: Показва имейл адреса на посочения играч. + arg1: + label: играч + description: име на играча + authme.setemail: + description: Смяна на имейл на играча. + detailedDescription: Променя имейл адреса на дадения играч. + arg1: + label: играч + description: име на играча + arg2: + label: имейл + description: имейл на играча + authme.getip: + description: IP-адрес + detailedDescription: Показва текущият IP-адрес на посочения играч. + arg1: + label: играч + description: име на играча + authme.spawn: + description: Място на съживяване + detailedDescription: Телепортиране на мястото на съживяване. + authme.setspawn: + description: Задаване на място на съживяване. + detailedDescription: Задава точката на съживяване на текущото местонахождение. + authme.firstspawn: + description: Първоначално място на съживяване. + detailedDescription: Избира се първоначалното място на съживяване. + authme.setfirstspawn: + description: Задаване на първоначално място на съживяване. + detailedDescription: Задава първоначалната точка на съживяване на текущото местонахождение. + authme.purge: + description: Изтриване на стари данни. + detailedDescription: Изтрива стари данни според указаното количество дни. + arg1: + label: дни + description: брой дни + authme.purgeplayer: + description: Изтрива данните на даден играч. + detailedDescription: Изтрива всички данни на даден играч. + arg1: + label: Играч + description: Името на играча, за което трябва да се изтрият данните. + arg2: + label: Опции + description: '''force'' за да се изтрие без проверка дали играча е регистриран.' + authme.backup: + description: Създаване на резервно копие + detailedDescription: Създава резервно копие на всички регистрирани играчи. + authme.resetpos: + description: Рестартиране позиция на играч + detailedDescription: Рестартира последното известно местонахождение на указания + играч, или на всички играчи. + arg1: + label: играч|* + description: име на играча/всички играчи + authme.purgebannedplayers: + description: Изтриване на данни за блокирани играчи. + detailedDescription: Изтрива всички данни за блокираните играчи от базата данни. + authme.switchantibot: + description: Промяна на AntiBot-режима + detailedDescription: Променя режима на AntiBot системата според указанието. + arg1: + label: ON|OFF + description: включено/изключено + authme.reload: + description: Презареждане на плъгина. + detailedDescription: Презарежда плъгина AuthMeReloaded. + authme.version: + description: Информация за версията + detailedDescription: Показва подробна информация за версията на AuthMeReloaded, + неговите разработчици, помощници и лиценз. + authme.converter: + description: Преобразувател + detailedDescription: Преубразовател за базата данни. + arg1: + label: тип + description: 'тип преобразуване: xauth / crazylogin / rakamak / royalauth / + vauth / sqliteToSql / mysqlToSqlite' + authme.messages: + description: Добавяне на липсващи съобщения в помощния файл. + detailedDescription: Добавя всички стойности (на англ. език) в помощният файл. + authme.recent: + description: Проверка на скоро влизалите играчи. + detailedDescription: Показва последните играчи, които са били успешно влезли в играта. + authme.debug: + description: Отстраняване на грешки. + detailedDescription: Позволява различни опции за записване на грешки от плъгина. + arg1: + label: секция + description: Секцията за отстраняване на грешки, която да бъде изпълнена. + arg2: + label: аргумент + description: Аргумент (Зависи от избора на секция за отстраняване на грешки) + arg3: + label: аргумент + description: Аргумент (Зависи от избора на секция за отстраняване на грешки) + authme.help: + description: Помощ + detailedDescription: Показва помощ за командите започващи с /authme. + arg1: + label: команда + description: команда, за която е необходимо да се покаже помощ. + email: + description: Добавя имейл или възстановява парола. + detailedDescription: Основната команда на AuthMeReloaded свързана с имейлите. + email.show: + description: Показва Имейл + detailedDescription: Показва текущият Ви имейл. + email.add: + description: Добавяне на имейл. + detailedDescription: Добавя нов имейл адрес към Вашият акаунт. + arg1: + label: имейл + description: Имейл адрес + arg2: + label: Потвърждение + description: Потвърждение на имейл адреса. + email.change: + description: Смяна на Имейл + detailedDescription: Смяна на имейл адреса на Вашият акаунт. + arg1: + label: стар-имейл + description: Текущия имейл използван към Вашият акаунт + arg2: + label: нов-имейл + description: Новия имейл, който да бъде задеден към акаунта. + email.recover: + description: Възстановяване на парола чрез имейл. + detailedDescription: Възстановяване на Вашият акаунт, използвайки Имейл с нова парола. + arg1: + label: имейл + description: Имейл адреса на Вашият акаунт. + email.code: + description: Изпращане на код за възстановяване на парола. + detailedDescription: Възстановяване на Вашият акаунт чрез код, изпратен по имейл. + arg1: + label: код + description: Код за възстановяване + email.setpassword: + description: Задаване на нова парола след възстановяване. + detailedDescription: Задаване на нова парола след успешно възстановяване на Вашият акаунт. + arg1: + label: парола + description: Нова парола + email.help: + description: Помощ + detailedDescription: Подробна помощ за /email командите. + arg1: + label: команда + description: Командата за която да се покаже помощ (или за всички). + login: + description: вход + detailedDescription: Командата която се използва за идентификация в сървъра на Вашият акаунт. + arg1: + label: парола + description: Паролата на акаунта Ви. + login.help: + description: Помощ + detailedDescription: Подробна помощ за /login командите. + arg1: + label: команда + description: Командата за която да се покаже помощ (или за всички). + logout: + description: Изход + detailedDescription: Командата за изход от Вашият акаунт. + logout.help: + description: помощ + detailedDescription: КПодробна помощ за /logout командите. + arg1: + label: команда + description: Командата за която да се покаже помощ (или за всички). + register: + description: Регистрация на акаунт. + detailedDescription: Команда за регистриране на Вашият акаунт. + arg1: + label: парола + description: Парола + arg2: + label: Потвърждение + description: Потвърждение на паролата + register.help: + description: Помощ + detailedDescription: Детайлна помощ за /register командите. + arg1: + label: команда + description: Командата за която да се покаже помощ (или за всички). + unregister: + description: Дерегистриране на акаунт. + detailedDescription: Команда за премахване на регистрацията на Вашият акаунт. + arg1: + label: парола + description: Вашата парола. + unregister.help: + description: Помощ + detailedDescription: Детайлна помощ за /unregister командите. + arg1: + label: команда + description: Командата за която да се покаже помощ (или за всички). + changepassword: + description: Смяна на парола + detailedDescription: Смяна на паролата за Вашият акаунт. + arg1: + label: текуща-парола + description: Текущата парола на Вашият акаунт. + arg2: + label: Нова парола + description: Новата парола, която да бъде използвана за Вашият акаунт. + changepassword.help: + description: Помощ + detailedDescription: Детайлна помощ за /changepassword командите. + arg1: + label: команда + description: Командата за която да се покаже помощ (или за всички). + totp: + description: TOTP команди + detailedDescription: Команди свързани с допълнителната сигурност на акаунта Ви чрез секретен код. + totp.code: + description: Команда за вход + detailedDescription: Изпълнява проверката на секретния код на Вашият акаунт при влизане. + arg1: + label: код + description: Секретният код, използван за влизане. + totp.add: + description: Включване на TOTP + detailedDescription: Включване на защитата със секретен код за Вашият акаунт. + totp.confirm: + description: Активиране на TOTP след въвеждане на правилен секретен код. + detailedDescription: Запазва генерираният секретен код след потвърждение. + arg1: + label: код + description: Секретният код след изпълняване на командата /totp add + totp.remove: + description: Изключване на TOTP + detailedDescription: Изключване на защитата със секретен код за Вашият акаунт. + arg1: + label: код + description: Текущ секретен код + totp.help: + description: Помощ + detailedDescription: Детайлна помощ за /totp командите. + arg1: + label: команда + description: Командата за която да се покаже помощ (или за всички). + captcha: + description: Код за сигурност + detailedDescription: Команда за код за сигурност. + arg1: + label: код + description: Код за сигурност + captcha.help: + description: Помощ + detailedDescription: Детайлна помощ за /captcha командите. + arg1: + label: команда + description: Командата за която да се покаже помощ (или за всички). + verification: + description: Команда за потвърждение. + detailedDescription: Команда за потвърждение. + arg1: + label: код + description: Код за потвърждение. + verification.help: + description: Помощ + detailedDescription: Детайлна помощ за /verification командите. + arg1: + label: команда + description: Командата за която да се покаже помощ (или за всички). diff --git a/plugin/platform-bukkit/src/main/resources/messages/help_br.yml b/plugin/platform-bukkit/src/main/resources/messages/help_br.yml new file mode 100644 index 00000000..818daf57 --- /dev/null +++ b/plugin/platform-bukkit/src/main/resources/messages/help_br.yml @@ -0,0 +1,46 @@ +# Arquivo de tradução da ajuda do AuthMe (comando /authme help) +# Tradução por Frani (PotterCraft_) e RenanYudi + +# ------------------------------------------------------- +# Lista de textos usados na seção de ajuda: +common: + header: '==========[ Ajuda AuthMeReloaded ]==========' + optional: 'Opcional' + hasPermission: 'Você tem permissão' + noPermission: 'Sem permissão' + default: 'Padrão' + result: 'Resultado' + defaultPermissions: + notAllowed: 'Sem permissão' + opOnly: 'Apenas jogadores com OP' + allowed: 'Todos tem permissão' + +# ------------------------------------------------------- +# Títulos das seções individuais de ajuda +# Deixe o texto traduzido vazio para desativá-lo/esconde-lo +# alternatives: '' +section: + command: 'Comando' + description: 'Descrição breve' + detailedDescription: 'Descrição detalhada' + arguments: 'Argumentos' + permissions: 'Permissões' + alternatives: 'Alternativas' + children: 'Comandos' + +# ------------------------------------------------------- +# Você pode traduzir o help de qualquer comando usando o padrão abaixo +# Por exemplo, para traduzir /authme reload, crie uma seção "authme.reload", ou "login" para /login +# Se o comando usa argumentos, você pode usar arg1 para traduzir o primeiro argumento e assim em diante, como exemplificado abaixo +# As traduções não precisam ser completas, qualquer parte faltante será obtida da configuração padrão. +# OBS: Coloque os comandos principais, como "authme" antes de seus subcomandos (como "authme.reload") +commands: + authme.register: + description: 'Registra um jogador' + detailedDescription: 'Registra um jogador específico com uma senha específica.' + arg1: + label: 'player' + description: 'Nome do jogador' + arg2: + label: 'password' + description: 'Senha' diff --git a/plugin/platform-bukkit/src/main/resources/messages/help_cz.yml b/plugin/platform-bukkit/src/main/resources/messages/help_cz.yml new file mode 100644 index 00000000..4d16d551 --- /dev/null +++ b/plugin/platform-bukkit/src/main/resources/messages/help_cz.yml @@ -0,0 +1,45 @@ +# Translation config for the AuthMe help, e.g. when /authme help or /authme help register is called + +# ------------------------------------------------------- +# List of texts used in the help section +common: + header: '==========[ AuthMeReloaded HELP ]==========' + optional: 'Volitelné' + hasPermission: 'Máš oprávnění' + noPermission: 'Bez oprávnění' + default: 'Defaultní' + result: 'Výsledek' + defaultPermissions: + notAllowed: 'Bez oprávnění' + opOnly: 'Pouze OP' + allowed: 'Povolené pro všechny' + +# ------------------------------------------------------- +# Titles of the individual help sections +# Set the translation text to empty text to disable the section, e.g. to hide alternatives: +# alternatives: '' +section: + command: 'Příkaz' + description: 'Krátký popis' + detailedDescription: 'Detailní popis' + arguments: 'Argumenty' + permissions: 'Oprávnění' + alternatives: 'Alternativy' + children: 'Příkazy' + +# ------------------------------------------------------- +# You can translate the data for all commands using the below pattern. +# For example to translate /authme reload, create a section "authme.reload", or "login" for /login +# If the command has arguments, you can use arg1 as below to translate the first argument, and so forth +# Translations don't need to be complete; any missing section will be taken from the default silently +# Important: Put main commands like "authme" before their children (e.g. "authme.reload") +commands: + authme.register: + description: 'Registrovat hráče' + detailedDescription: 'Registrovat daného hráče s daným heslem.' + arg1: + label: 'hráč' + description: 'Jméno hráče' + arg2: + label: 'heslo' + description: 'Heslo' diff --git a/plugin/platform-bukkit/src/main/resources/messages/help_de.yml b/plugin/platform-bukkit/src/main/resources/messages/help_de.yml new file mode 100644 index 00000000..b1841ef4 --- /dev/null +++ b/plugin/platform-bukkit/src/main/resources/messages/help_de.yml @@ -0,0 +1,29 @@ +common: + header: '==========[ AuthMeReloaded Hilfe ]==========' + optional: 'Optional' + hasPermission: 'Du hast Berechtigung' + noPermission: 'Keine Berechtigung' + default: 'Default' + result: 'Resultat' + defaultPermissions: + notAllowed: 'Kein Recht' + opOnly: 'Nur OP''s' + allowed: 'Allen erlaubt' +section: + command: 'Kommando' + description: 'Beschreibung' + detailedDescription: 'Detaillierte Beschreibung' + arguments: 'Argumente' + permissions: 'Rechte' + alternatives: 'Alternativen' + children: 'Kommandos' +commands: + authme.register: + description: 'Registriert einen Benutzer' + detailedDescription: 'Registriert den Benutzer mit dem gegebenen Passwort.' + arg1: + label: 'spieler' + description: 'Name des Spielers' + arg2: + label: 'passwort' + description: 'Passwort' diff --git a/plugin/platform-bukkit/src/main/resources/messages/help_en.yml b/plugin/platform-bukkit/src/main/resources/messages/help_en.yml new file mode 100644 index 00000000..bcc501b7 --- /dev/null +++ b/plugin/platform-bukkit/src/main/resources/messages/help_en.yml @@ -0,0 +1,45 @@ +# Translation config for the AuthMe help, e.g. when /authme help or /authme help register is called + +# ------------------------------------------------------- +# List of texts used in the help section +common: + header: '==========[ AuthMeReloaded HELP ]==========' + optional: 'Optional' + hasPermission: 'You have permission' + noPermission: 'No permission' + default: 'Default' + result: 'Result' + defaultPermissions: + notAllowed: 'No permission' + opOnly: 'OP''s only' + allowed: 'Everyone allowed' + +# ------------------------------------------------------- +# Titles of the individual help sections +# Set the translation text to empty text to disable the section, e.g. to hide alternatives: +# alternatives: '' +section: + command: 'Command' + description: 'Short description' + detailedDescription: 'Detailed description' + arguments: 'Arguments' + permissions: 'Permissions' + alternatives: 'Alternatives' + children: 'Commands' + +# ------------------------------------------------------- +# You can translate the data for all commands using the below pattern. +# For example to translate /authme reload, create a section "authme.reload", or "login" for /login +# If the command has arguments, you can use arg1 as below to translate the first argument, and so forth +# Translations don't need to be complete; any missing section will be taken from the default silently +# Important: Put main commands like "authme" before their children (e.g. "authme.reload") +commands: + authme.register: + description: 'Register a player' + detailedDescription: 'Register the specified player with the specified password.' + arg1: + label: 'player' + description: 'Player name' + arg2: + label: 'password' + description: 'Password' diff --git a/plugin/platform-bukkit/src/main/resources/messages/help_eo.yml b/plugin/platform-bukkit/src/main/resources/messages/help_eo.yml new file mode 100644 index 00000000..ed9ba3fe --- /dev/null +++ b/plugin/platform-bukkit/src/main/resources/messages/help_eo.yml @@ -0,0 +1,31 @@ +common: + header: '==========[ AuthMeReloaded Helpo ]==========' + optional: 'Laŭvola' + hasPermission: 'Vi havas permeson' + noPermission: 'Neniun permeson' + default: 'Apriora' + result: 'Rezulto' + defaultPermissions: + notAllowed: 'Neniun permeson' + opOnly: 'Nur el OP' + allowed: 'Ĉiu rajtas' + +section: + command: 'Komando' + description: 'Mallonga priskribo' + detailedDescription: 'Detala priskribo' + arguments: 'Argumentoj' + permissions: 'Permesoj' + alternatives: 'Alternativoj' + children: 'Komandoj' + +commands: + authme.register: + description: 'Registri ludanto' + detailedDescription: 'Registri la specifita ludanto kun la specifita pasvorton.' + arg1: + label: 'ludanto' + description: 'Ludanta nomo' + arg2: + label: 'pasvorto' + description: 'Pasvorto' diff --git a/plugin/platform-bukkit/src/main/resources/messages/help_es.yml b/plugin/platform-bukkit/src/main/resources/messages/help_es.yml new file mode 100644 index 00000000..cddf5f70 --- /dev/null +++ b/plugin/platform-bukkit/src/main/resources/messages/help_es.yml @@ -0,0 +1,294 @@ +# Translated by MineSAT + +# ------------------------------------------------------- +# Lista de textos usados en la sección de ayuda. +common: + header: ======[ AYUDA de AuthMeReloaded ]====== + optional: Opcional + hasPermission: Tienes permiso + noPermission: No tienes permiso + default: Por defecto + result: Resultado + defaultPermissions: + notAllowed: No tienes permiso + opOnly: Sólamente OPeradores(Owner's) + allowed: Todo permitido +section: + command: Comando + description: Descripción corta + detailedDescription: Descripción detallada + arguments: Argumentos + alternatives: Alternativas + permissions: Permisos + children: Comandos +commands: + authme: + description: AuthMe comandos de OPerador + detailedDescription: El comando principal de AuthMeReloaded. La raíz de todos los comandos de administración. + authme.register: + description: Registrar un jugador + detailedDescription: Registra un jugador específico con una contraseña específica. + arg1: + label: jugador + description: Nombre del jugador + arg2: + label: contraseña + description: Contraseña del jugador + authme.unregister: + description: Cancelar cuenta de un jugador + detailedDescription: Elimina la cuenta registrada de un jugador. + arg1: + label: jugador + description: Nombre del jugador + authme.forcelogin: + description: Obliga el inicio de sesión + detailedDescription: Obliga a un jugador específico a iniciar sesión. + arg1: + label: jugador + description: Nombre del jugador en línea + authme.password: + description: Cambiar contraseña del jugador + detailedDescription: Cambia la contraseña de un jugador. + arg1: + label: jugador + description: Nombre del jugador + arg2: + label: contraseña + description: Nueva contraseña + authme.lastlogin: + description: Último inicio de sesión + detailedDescription: Ver la fecha del último inicio de sesión de un jugador específico. + arg1: + label: jugador + description: Nombre del jugador + authme.accounts: + description: Mostrar cuentas del jugador + detailedDescription: Muestra todas las cuentas de un jugador por su Nombre de Jugador o IP. + arg1: + label: jugador + description: Nombre del jugador o dirección IP + authme.email: + description: Mostrar correo del jugador + detailedDescription: Muestra el correo electrónico de un jugador específico, si este lo ha configurado en su cuenta. + arg1: + label: jugador + description: Nombre del jugador + authme.setemail: + description: Cambia el correo electrónico del jugador + detailedDescription: Cambia la dirección correo electrónico de un jugador especificado. + arg1: + label: jugador + description: Nombre del jugador + arg2: + label: email + description: Correo electrónico del jugador + authme.getip: + description: Obtiene la IP del jugador + detailedDescription: Obtiene la dirección IP de un jugador específico que esté en línea. + arg1: + label: jugador + description: Nombre del jugador + authme.spawn: + description: Teletransporte al Spawn + detailedDescription: Teletranspórtate al Spawn del servidor. + authme.setspawn: + description: Cambia el Spawn + detailedDescription: Cambia la zona de aparición del jugador por tu posición actual. + authme.firstspawn: + description: Teletransporte al primer Spawn + detailedDescription: Teletranspórtate a la primera zona de aparición del servidor. + authme.setfirstspawn: + description: Cambia el primer Spawn + detailedDescription: Cambia la primera zona de aparición del jugador por tu posición actual. + authme.purge: + description: Purgar(Limpiar) datos antiguos + detailedDescription: Purgar o Limpiar datos de AuthMeReloaded anteriores a el número días especificados en la configuración. + arg1: + label: días + description: Número de días + authme.purgeplayer: + description: Purgar los datos de un jugador + detailedDescription: Purgar los datos de un jugador seleccionado. + arg1: + label: jugador + description: El jugador para purgar o limpiar + arg2: + label: opciones + description: 'Escribe ''force'' al final del comando para forzar sin comprobar que el jugador está registrado' + authme.backup: + description: Realiza una copia de seguridad + detailedDescription: Crea una copia de seguridad de los usuarios registrados. + authme.resetpos: + description: Purgar o limpiar la última posición del jugador + detailedDescription: Purgar o limpiar la última posición conocida de un jugador específico o todos los jugadores. + arg1: + label: jugador / * + description: Nombre del jugador o escribe '*' para seleccionar todos los jugadores + authme.purgebannedplayers: + description: Purgar datos de jugadores baneados + detailedDescription: Purgar todos los datos de AuthMeReloaded de los jugadores baneados. + authme.switchantibot: + description: Cambiar el modo AntiBot + detailedDescription: Cambiar o alternar el modo AntiBot al estado especificado. + arg1: + label: modo + description: ON / OFF + authme.reload: + description: Recarga el plugin + detailedDescription: Recarga el plugin AuthMeReloaded. + authme.version: + description: Información de versión + detailedDescription: Muestra información detallada sobre la versión instalada de AuthMeReloaded, los creadores, contribuyentes, y la licencia. + authme.converter: + description: Comando conversor + detailedDescription: Comando conversor de AuthMeReloaded. + arg1: + label: trabajo + description: 'Trabajo de conversión: xauth / crazylogin / rakamak / royalauth / vauth / sqliteToSql / mysqlToSqlite / loginsecurity' + authme.messages: + description: Añade mensajes faltantes + detailedDescription: Añade los mensajes que faltan al archivo de mensajes actual. + arg1: + label: help + description: Añade 'help' para actualizar to update the help messages file + authme.debug: + description: Funciones de depuración + detailedDescription: Permite varias operaciones de depuración. + arg1: + label: child + description: El hijo(child) a ejecutar + arg2: + label: arg + description: Argumento (depende de la sección de depuración) + arg3: + label: arg + description: Argumento (depende de la sección de depuración) + authme.help: + description: Muestra la ayuda + detailedDescription: Muestra detalladamente la ayuda para los comandos /authme. + arg1: + label: consulta + description: El comando o la consulta para ver su description. + email: + description: Añade el correo electrónico o recupera la contraseña + detailedDescription: La base del comando de correo electrónico de AuthMeReloaded. + email.show: + description: Mostrar correo electrónico + detailedDescription: Muestra you correo electrónico actual. + email.add: + description: Añadir correo electrónico + detailedDescription: Añade un nuevo correo electrónico a tu cuenta. + arg1: + label: email + description: Dirección de correo electrónico + arg2: + label: repiteEmail + description: Verificación de la dirección de correo electrónico + email.change: + description: Cambiar correo electrónico + detailedDescription: Cambia la dirección de correo electrónico de tu cuenta. + arg1: + label: anteriorEmail + description: Anterior dirección de correo electrónico + arg2: + label: nuevoEmail + description: Nueva dirección de correo electrónico + email.recover: + description: Recuperar contraseña usando el correo electrónico + detailedDescription: Recupera tu cuenta usando tu correo electrónico para enviarte una nueva contraseña. + arg1: + label: email + description: Dirección de correo electrónico + email.code: + description: Envía el código para recuperar tu contraseña + detailedDescription: Recupera tu cuenta enviando el código recibido en tu correo electrónico. + arg1: + label: código + description: Código de recuperación + email.setpassword: + description: Establece una nueva contraseña después de recuperar + detailedDescription: Establece una nueva contraseña después de recuperar completamente tu cuenta. + arg1: + label: contraseña + description: Nueva contraseña + email.help: + description: Muestra la ayuda + detailedDescription: Muestra detalladamente la ayuda de los comandos de /email. + arg1: + label: consulta + description: El comando o consulta para ver la ayuda de este. + login: + description: Comando de inicio de sesión + detailedDescription: Comando para iniciar sesión utilizado en AuthMeReloaded. + arg1: + label: contraseña + description: Contraseña de inicio de sesión + login.help: + description: Mostrar la ayuda + detailedDescription: Mostrar la ayuda detallada del comando /login. + arg1: + label: consulta + description: El comando o consulta para ver la ayuda de este. + logout: + description: Comando de cierre de sesión + detailedDescription: Comando de cierre de sesión usado por AuthMeReloaded. + logout.help: + description: Mostrar la ayuda + detailedDescription: Mostrar la ayuda detallada del comando /logout. + arg1: + label: consulta + description: El comando o consulta para ver la ayuda de este. + register: + description: Registrar una cuenta + detailedDescription: Comando para registrar una cuenta usado por AuthMeReloaded. + arg1: + label: contraseña + description: Contraseña + arg2: + label: repiteContraseña + description: Repite la contraseña + register.help: + description: Mostrar la ayuda + detailedDescription: Mostrar la ayuda detallada del comando /register. + arg1: + label: consulta + description: El comando o consulta para ver la ayuda de este. + unregister: + description: Eliminar registro de una cuenta + detailedDescription: Comando para eliminar una cuenta registrada usado por AuthMeReloaded. + arg1: + label: contraseña + description: Contraseña + unregister.help: + description: Mostrar la ayuda + detailedDescription: Mostrar la ayuda detallada del comando /unregister. + arg1: + label: consulta + description: El comando o consulta para ver la ayuda de este. + changepassword: + description: Cambia la contraseña de una cuenta + detailedDescription: Comando para cambiar tu contraseña usado por AuthMeReloaded. + arg1: + label: antiguaContraseña + description: Antigua contraseña + arg2: + label: nuevaContraseña + description: Nueva contraseña + changepassword.help: + description: Mostrar la ayuda + detailedDescription: Mostrar la ayuda detallada del comando /changepassword. + arg1: + label: consulta + description: El comando o consulta para ver la ayuda de este. + captcha: + description: Comando del Captcha + detailedDescription: Comando del Captcha para AuthMeReloaded. + arg1: + label: captcha + description: El Captcha + captcha.help: + description: Mostrar la ayuda + detailedDescription: Mostrar la ayuda detallada del comando /captcha. + arg1: + label: consulta + description: El comando o consulta para ver la ayuda de este. diff --git a/plugin/platform-bukkit/src/main/resources/messages/help_et.yml b/plugin/platform-bukkit/src/main/resources/messages/help_et.yml new file mode 100644 index 00000000..03a70213 --- /dev/null +++ b/plugin/platform-bukkit/src/main/resources/messages/help_et.yml @@ -0,0 +1,45 @@ +# Translation config for the AuthMe help, e.g. when /authme help or /authme help register is called + +# ------------------------------------------------------- +# List of texts used in the help section +common: + header: '==========[ AuthMeReloaded ABI ]==========' + optional: 'Valikuline' + hasPermission: 'Sul on luba' + noPermission: 'Pole luba' + default: 'Vaikimisi' + result: 'Tulemus' + defaultPermissions: + notAllowed: 'Pole luba' + opOnly: 'Ainult operaatorid' + allowed: 'Luba kõigil' + +# ------------------------------------------------------- +# Titles of the individual help sections +# Set the translation text to empty text to disable the section, e.g. to hide alternatives: +# alternatives: '' +section: + command: 'Käsklus' + description: 'Lühike kirjeldus' + detailedDescription: 'Detailne kirjeldus' + arguments: 'Argumendid' + permissions: 'Load' + alternatives: 'Alternatiivid' + children: 'Käsklused' + +# ------------------------------------------------------- +# You can translate the data for all commands using the below pattern. +# For example to translate /authme reload, create a section "authme.reload", or "login" for /login +# If the command has arguments, you can use arg1 as below to translate the first argument, and so forth +# Translations don't need to be complete; any missing section will be taken from the default silently +# Important: Put main commands like "authme" before their children (e.g. "authme.reload") +commands: + authme.register: + description: 'Registreeri mängija' + detailedDescription: 'Registreeri valitud mängija valitud parooliga.' + arg1: + label: 'mängija' + description: 'Mängija nimi' + arg2: + label: 'parool' + description: 'Parool' diff --git a/plugin/platform-bukkit/src/main/resources/messages/help_eu.yml b/plugin/platform-bukkit/src/main/resources/messages/help_eu.yml new file mode 100644 index 00000000..52fad802 --- /dev/null +++ b/plugin/platform-bukkit/src/main/resources/messages/help_eu.yml @@ -0,0 +1,23 @@ +# Translation config for the AuthMe help, e.g. when /authme help or /authme help register is called + +# ------------------------------------------------------- +# List of texts used in the help section +common: + header: '==========[ AuthMeReloaded LAGUNTZA ]==========' + optional: 'Hautazkoa' + hasPermission: 'Baimena daukazu' + noPermission: 'Ez daukazu baimenik' + default: 'Lehenetsia' + result: 'Emaitza' + defaultPermissions: + notAllowed: 'Baimenik ez' + opOnly: 'OP bakarrik' + allowed: 'Edonor baimenduta' +section: + command: 'Komandoa' + description: 'Deskribapen laburra' + detailedDescription: 'Deskribapen xehakatua' + arguments: 'Argumentuak' + permissions: 'Baimenak' + alternatives: 'Alternatibak' + children: 'Komandoak' diff --git a/plugin/platform-bukkit/src/main/resources/messages/help_fr.yml b/plugin/platform-bukkit/src/main/resources/messages/help_fr.yml new file mode 100644 index 00000000..dce54f16 --- /dev/null +++ b/plugin/platform-bukkit/src/main/resources/messages/help_fr.yml @@ -0,0 +1,45 @@ +# Traduction des messages d'aide d'AuthMe (par exemple, pour les messages de "/authme help ()" ou de "/register help") + +# ------------------------------------------------------- +# Liste de texte dans les sections d'aide +common: + header: '==========[ AuthMe - AIDE & INFOS ]==========' + optional: 'Optionnel' + hasPermission: 'Vous avez la permission' + noPermission: 'Pas de permission' + default: 'Par défaut' + result: 'Résultat' + defaultPermissions: + notAllowed: 'Non permis' + opOnly: 'Seulement pour OP' + allowed: 'Tout le monde est permis' + +# ------------------------------------------------------- +# Nom individuel des sections d'aide +# Vous pouvez vider la zone de texte d'une section afin de la cacher, ex. pour cacher la section des alternatives: +# alternative(s): '' +section: + command: 'Commande' + description: 'Description' + detailedDescription: 'Description détaillée' + arguments: 'Argument(s)' + permissions: 'Permission' + alternatives: 'Alternative(s)' + children: 'Sous-commande(s)' + +# ------------------------------------------------------- +# Vous pouvez traduire tous les textes d'aide en utilisant la syntaxe ci-dessous. +# Par exemple, pour traduire l'aide du "/authme reload" créez une section nommée "authme.reload", ou "login" pour "/login". +# Si la commande a des arguments, vous pouvez utiliser "arg1" pour traduire le premier argument, "arg2" pour le 2ème, ainsi de suite. +# Les sections non traduites auront leur texte par défaut (en anglais), il n'est donc pas obligatoire de tout traduire. +# Important: Il faut mettre la commande principale (authme) avant sa sous-commande (ex. "authme.register" pour "/authme register") +commands: + authme.register: + description: 'Inscrire un pseudo' + detailedDescription: 'Inscrire un pseudo avec le mot de passe de votre choix' + arg1: + label: 'pseudo' + description: 'Pseudo du joueur à inscrire' + arg2: + label: 'mdp' + description: 'Mot de passe de connexion' diff --git a/plugin/platform-bukkit/src/main/resources/messages/help_hu.yml b/plugin/platform-bukkit/src/main/resources/messages/help_hu.yml new file mode 100644 index 00000000..93bfabd8 --- /dev/null +++ b/plugin/platform-bukkit/src/main/resources/messages/help_hu.yml @@ -0,0 +1,351 @@ +# Translation config for the AuthMe help, e.g. when /authme help or /authme help register is called + +# ------------------------------------------------------- +# List of texts used in the help section +common: + header: ======[ AuthMeReloaded Segítség ]====== + optional: Opcionális + hasPermission: Van ehhez jogod + noPermission: Nincs jogod ehhez + default: Alapértelmezett + result: Eredmény + defaultPermissions: + notAllowed: Nincs jogod ehhez + opOnly: Op jog szükséges + allowed: Mindenkinek elérhető + +# ------------------------------------------------------- +# Titles of the individual help sections +# Set the translation text to empty text to disable the section, e.g. to hide alternatives: +# alternatives: '' +section: + command: Parancs + description: Rövid leírás + detailedDescription: Hosszabb leírás + arguments: Argumentum (változó) + permissions: Jogok + alternatives: Alternatívák + children: Parancsok + +# ------------------------------------------------------- +# You can translate the data for all commands using the below pattern. +# For example to translate /authme reload, create a section "authme.reload", or "login" for /login +# If the command has arguments, you can use arg1 as below to translate the first argument, and so forth +# Translations don't need to be complete; any missing section will be taken from the default silently +# Important: Put main commands like "authme" before their children (e.g. "authme.reload") +commands: + authme: + description: AuthMe operátor parancsok + detailedDescription: Az AuthMeReloaded fő parancsa. Az összes adminisztrációs parancs gyökere. + authme.register: + description: Játékos regisztrálása + detailedDescription: Megadott játékos regisztrálása egy megadott jelszóval. + arg1: + label: játékos + description: Játékos név + arg2: + label: jelszó + description: Jelszó + authme.unregister: + description: A játékos fiókjának visszavonása + detailedDescription: Kiveszi a játékos regisztrált fiókját. + arg1: + label: játékos + description: Játékos név + authme.forcelogin: + description: Kötelező bejelentkezés + detailedDescription: Ez arra kényszerít egy adott játékost, hogy jelentkezzen be. + arg1: + label: játékos + description: A játékos neve, ha online + authme.password: + description: A játékos jelszavának módosítása + detailedDescription: A játékos jelszavának módosítása. + arg1: + label: játékos + description: Játékos név + arg2: + label: jelszó + description: Új jelszó + authme.lastlogin: + description: Utolsó bejelentkezés + detailedDescription: Megtekintheti egy adott játékos utolsó bejelentkezésének dátumát. + arg1: + label: játékos + description: Játékos név + authme.accounts: + description: Játékosok megjelenítése + detailedDescription: Megmutassa a játékos összes fiókját a játékos nevével vagy IP-jével. + arg1: + label: játékos + description: Játékos neve vagy IP címe + authme.email: + description: Játékos levelek megjelenítése + detailedDescription: Egy adott játékos e-mailjét mutatja, ha a fiókjában beállította. + arg1: + label: játékos + description: Játékos név + authme.setemail: + description: Módosítsa a játékos e-mailjét + detailedDescription: Egy adott játékos e-mail címének módosítása. + arg1: + label: játékos + description: Játékos név + arg2: + label: email + description: A játékos e-mailje + authme.getip: + description: A játékos IP cím mutatása + detailedDescription: Kap egy adott online játékos IP-címét. + arg1: + label: játékos + description: Játékos név + authme.spawn: + description: Teleportálás a spawnra + detailedDescription: Teleportál a spawnra. + authme.setspawn: + description: Spawn beállítása + detailedDescription: Beállítja a spawn kezdőhelyet a játékos jelenlegi pozicíójában. + authme.firstspawn: + description: Teleportálás az első spawnhoz + detailedDescription: Teleportál az első spawn kezdőhelyre. + authme.setfirstspawn: + description: Első spawn beállítása + detailedDescription: Beállítja az első spawn kezdőhelyet a játékos jelenlegi pozicíójában. + authme.purge: + description: Törli a régi adatokat + detailedDescription: Törli az AuthMeReloaded játékos adatokat a megadott nap szerint. + arg1: + label: nap + description: Napok száma + authme.purgeplayer: + description: Játékos adat törlés + detailedDescription: Törli a kiválasztott játékos adatait. + arg1: + label: játékos + description: A játékos törlése az adatbázisból + arg2: + label: opciók + description: 'Írd be a parancs végén a ''force'' parancsot, hogy ellenőrizze a játékos regisztrálva van-e' + authme.backup: + description: Biztonsági másolat készítése + detailedDescription: Létrehoz egy biztonsági másolatot a regisztrált felhasználókról. + authme.resetpos: + description: Visszaállítja a játékos utolsó helyzetét + detailedDescription: Visszaállítja az adott játékos vagy az összes játékos utolsó ismert pozícióját. + arg1: + label: játékos + description: Játékos neve vagy írd be a '*' gomb az összes játékos kiválasztásához + authme.purgebannedplayers: + description: Törli az adatokat a kitiltott játékosoktól + detailedDescription: Törli az összes AuthMeReloaded adatokat a kitiltott játékosoktól. + authme.switchantibot: + description: Módosítja az AntiBot módot + detailedDescription: Az AntiBot mód megváltoztatása vagy átkapcsolása a megadott állapotra. + arg1: + label: mód + description: ON / OFF + authme.reload: + description: Plugin újratöltés + detailedDescription: Újratölti az AuthMeReloaded plugint. + authme.version: + description: Plugin verzió + detailedDescription: Részletes információ az AuthMeReloaded telepített verziójáról, az alkotókról, a közreműködőkről és az engedélyekről. + authme.converter: + description: Parancs konvertáló + detailedDescription: AuthMeReloaded konvertáló parancs. + arg1: + label: munka + description: 'Konverziós munka: xauth / crazylogin / rakamak / royalauth / vauth / sqliteToSql / mysqlToSqlite / loginsecurity' + authme.messages: + description: Add hiányzó üzeneteket + detailedDescription: Hozzáadja a hiányzó üzeneteket az aktuális üzenetfájlhoz. + arg1: + label: help + description: A 'help' frissítéséhez, hogy frissítse a segítséget + authme.recent: + description: Utolsó bejelentkezett játékosok megjelenítése + detailedDescription: Megjeleníti az utolsó bejelentkezett játékosokat. + authme.debug: + description: Hibakeresés funkciói + detailedDescription: Lehetővé tesz több hibakeresési műveletet. + arg1: + label: gyermek + description: A végrehajtandó gyermek (gyermek) + arg2: + label: argumentum + description: Argumentum (a debug-szekciótól függ) + arg3: + label: argumentum + description: Argumentum (a debug-szekciótól függ) + authme.help: + description: Segítség megjelenítése + detailedDescription: Részletesen megmutatja az authme parancsok segítséget. + arg1: + label: konzultáció + description: A parancs vagy lekérdezés leírása. + email: + description: Hozzáadja az e-mailt vagy megjeleníti a játékos email-jét + detailedDescription: Az AuthMeReloaded e-mail parancs alapja. + email.show: + description: E-mail megjelenítése + detailedDescription: Jelenlegi e-mail megjelenítése. + email.add: + description: E-mail hozzáadása + detailedDescription: Új e-mail üzenet hozzáadása. + arg1: + label: email + description: E-mail cím + arg2: + label: emailMegerősítés + description: Az e-mail cím ellenőrzése + email.change: + description: E-mail megváltoztatása + detailedDescription: Megváltoztatja a fiók e-mail címét. + arg1: + label: RégiEmail + description: Korábbi e-mail cím + arg2: + label: újEmail + description: Új e-mail cím + email.recover: + description: Jelszó lekérése e-mail alapján + detailedDescription: Visszaszerezheted a fiókod e-mailben, hogy új jelszót küldjön. + arg1: + label: email + description: E-mail cím + email.code: + description: Kód elküldése a jelszó visszaállításához + detailedDescription: Visszaszerezheted a fiókod a kapott kód e-mailben történő elküldésével. + arg1: + label: kód + description: Helyreállítási kód + email.setpassword: + description: Jelszó beállítása + detailedDescription: Új jelszó beállítása a fiók teljes körű visszaállítása után. + arg1: + label: jelszó + description: Új jelszó + email.help: + description: E-mail segítség + detailedDescription: Részletes segítség az /email parancshoz. + arg1: + label: konzultáció + description: A parancs vagy a lekérdezés, hogy lásd a segítséget. + login: + description: Bejelentkezés parancs + detailedDescription: Az AuthMeReloadedban használt bejelentkezési parancs. + arg1: + label: jelszó + description: Bejelentkezés jelszó + login.help: + description: Bejelentkezési segítség + detailedDescription: Részletes segítség a /login parancsról. + arg1: + label: konzultáció + description: A parancs vagy a lekérdezés, hogy lásd a segítséget. + logout: + description: Kijelentkezés parancs + detailedDescription: Az AuthMeReloaded által használt kijelentkezés parancs. + logout.help: + description: Kijelentkezési segítség + detailedDescription: Részletes segítség a /logout parancsról. + arg1: + label: konzultáció + description: A parancs vagy a lekérdezés, hogy lásd a segítséget. + register: + description: Fiók regisztrálása + detailedDescription: Parancs az AuthMeReloaded által használt fiók regisztrálásához. + arg1: + label: jelszó + description: Jelszó + arg2: + label: jelszóMegerősítő + description: Jelszó megerősítése + register.help: + description: Regisztrálási segítség + detailedDescription: Részletes segítség a /register parancsról. + arg1: + label: konzultáció + description: A parancs vagy a lekérdezés, hogy lássuk a segítséget. + unregister: + description: Fiók törlése + detailedDescription: Parancs az AuthMeReloaded által használt regisztrált fiók törléséhez. + arg1: + label: jelszó + description: Jelszó + unregister.help: + description: Fiók törlési segítség + detailedDescription: Részletes segítség az /unregister parancsról. + arg1: + label: konzultáció + description: A parancs vagy a lekérdezés, hogy lássuk a segítséget. + changepassword: + description: Fiók jelszó módosítása + detailedDescription: Parancs az AuthMeReloaded által használt jelszó megváltoztatásához. + arg1: + label: régiJelszó + description: Régi jelszó + arg2: + label: újJelszó + description: Új jelszó + changepassword.help: + description: Jelszó változtatási segítség + detailedDescription: Részletes segítség a /changepassword parancsról. + arg1: + label: konzultáció + description: A parancs vagy a lekérdezés, hogy lássuk a segítséget. + totp: + description: Kétfaktoros hitelesítéssel kapcsolatos műveleteket végez + detailedDescription: Elvégzi a kétfaktoros hitelesítéssel kapcsolatos műveleteket. + totp.code: + description: Feldolgozza a kétfaktorú hitelesítési kódot bejelentkezés során + detailedDescription: A bejelentkezés során feldolgozza a kétfaktorú hitelesítési kódot. + arg1: + label: kód + description: Kód + totp.add: + description: Fiók kétütemű hitelesítése + detailedDescription: Engedélyezi a fiók kétütemű hitelesítését. + totp.confirm: + description: TOTP titkokat visszaigazolás után ment + detailedDescription: A létrehozott TOTP titkokat a visszaigazolás után elmenti. + arg1: + label: kód + description: Kód + totp.remove: + description: Fiók kétütemű hitelesítés letiltása + detailedDescription: Letiltja a fiók kétütemű hitelesítését. + arg1: + label: kód + description: Kód + totp.help: + description: /totp parancsok részletes segítsége + detailedDescription: Megtekinti a /totp parancsok részletes segítségét. + arg1: + label: kérdés + description: Kérdés + captcha: + description: Captcha parancs + detailedDescription: Captcha parancs az AuthMeReloaded számára. + arg1: + label: captcha + description: A captcha + captcha.help: + description: Segítség megjelenítése + detailedDescription: Megmutassa a /captcha parancs részletes segítségét. + arg1: + label: konzultáció + description: A parancs vagy a lekérdezés, hogy lásd a segítséget. + verification: + description: AuthMeReloaded hitelesítési folyamatának befejezése + detailedDescription: Parancs az AuthMeReloaded hitelesítési folyamatának befejezéséhez. + arg1: + label: kód + description: Kód + verification.help: + description: Részletes segítség a /verification parancshoz + detailedDescription: Részletes segítséget mutat a /verification parancshoz. + arg1: + label: kérdés + description: Kérdés diff --git a/plugin/platform-bukkit/src/main/resources/messages/help_it.yml b/plugin/platform-bukkit/src/main/resources/messages/help_it.yml new file mode 100644 index 00000000..efa6d65b --- /dev/null +++ b/plugin/platform-bukkit/src/main/resources/messages/help_it.yml @@ -0,0 +1,151 @@ +# Lingua Italiana creata da Maxetto e sgdc3. +# Translation config for the AuthMe help, e.g. when /authme help or /authme help register is called + +# ------------------------------------------------------- +# List of texts used in the help section +common: + header: '==========[ Assistenza AuthMeReloaded ]==========' + optional: 'Opzionale' + hasPermission: 'Hai il permesso' + noPermission: 'Non hai il permesso' + default: 'Configurazione base' + result: 'Risultato' + defaultPermissions: + notAllowed: 'Nessuno autorizzato' + opOnly: 'Solo per OP' + allowed: 'Tutti autorizzati' + +# ------------------------------------------------------- +# Titles of the individual help sections +# Set the translation text to empty text to disable the section, e.g. to hide alternatives: +# alternatives: '' +section: + command: 'Comando' + description: 'Descrizione breve' + detailedDescription: 'Descrizione dettagliata' + arguments: 'Parametri' + permissions: 'Permessi' + alternatives: 'Alternative' + children: 'Comandi' + +# ------------------------------------------------------- +# You can translate the data for all commands using the below pattern. +# For example to translate /authme reload, create a section "authme.reload", or "login" for /login +# If the command has arguments, you can use arg1 as below to translate the first argument, and so forth +# Translations don't need to be complete; any missing section will be taken from the default silently +# Important: Put main commands like "authme" before their children (e.g. "authme.reload") +commands: + authme.register: + description: 'Registra un giocatore' + detailedDescription: 'Registra il giocatore indicato con la password inserita.' + arg1: + label: 'giocatore' + description: 'Nome del giocatore' + arg2: + label: 'password' + description: 'Password' + authme.unregister: + description: 'Rimuovi un giocatore' + detailedDescription: 'Rimuovi il giocatore indicato dal Database.' + arg1: + label: 'giocatore' + description: 'Nome del giocatore' + authme.forcelogin: + description: 'Forza l''autenticazione ad un giocatore' + detailedDescription: 'Autentica il giocatore indicato.' + arg1: + label: 'giocatore' + description: 'Nome del giocatore connesso' + authme.password: + description: 'Cambia la password di un giocatore' + detailedDescription: 'Cambia la password del giocatore indicato.' + arg1: + label: 'giocatore' + description: 'Nome del giocatore' + arg2: + label: 'password' + description: 'Nuova Password' + authme.lastlogin: + description: 'Ultima autenticazione di un giocatore' + detailedDescription: 'Visualizza l''ultima data di autenticazione del giocatore indicato.' + arg1: + label: 'giocatore' + description: 'Nome del giocatore' + authme.accounts: + description: 'Mostra i profili di un giocatore' + detailedDescription: 'Mostra tutti i profili di un giocatore attraverso il nome o l''indirizzo IP.' + arg1: + label: 'giocatore' + description: 'Nome o indirizzo IP del giocatore' + authme.email: + description: 'Mostra l''indirizzo email di un giocatore' + detailedDescription: 'Mostra l''indirizzo email del giocatore indicato se impostato.' + arg1: + label: 'giocatore' + description: 'Nome del giocatore' + authme.setemail: + description: 'Cambia l''indirizzo email di un giocatore' + detailedDescription: 'Cambia l''indirizzo email del giocatore indicato.' + arg1: + label: 'giocatore' + description: 'Nome del giocatore' + arg2: + label: 'email' + description: 'Indirizzo email del giocatore' + authme.getip: + description: 'Mostra l''indirizzo IP di un giocatore' + detailedDescription: 'Mostra l''indirizzo IP del giocatore indicato.' + arg1: + label: 'giocatore' + description: 'Nome del giocatore connesso' + authme.spawn: + description: 'Teletrasportati al punto di rigenerazione' + detailedDescription: 'Teletrasportati al punto di rigenerazione.' + authme.setspawn: + description: 'Cambia il punto di rigenerazione' + detailedDescription: 'Cambia il punto di rigenerazione dei giocatori alla tua posizione.' + authme.firstspawn: + description: 'Teletrasportati al punto di rigenerazione iniziale' + detailedDescription: 'Teletrasportati al punto di rigenerazione iniziale.' + authme.setfirstspawn: + description: 'Cambia il punto di rigenerazione iniziale' + detailedDescription: 'Cambia il punto di rigenerazione iniziale dei giocatori alla tua posizione.' + authme.purge: + description: 'Elimina i vecchi dati' + detailedDescription: 'Elimina i dati di AuthMeReloaded più vecchi dei giorni indicati.' + arg1: + label: 'giorni' + description: 'Numero di giorni' + authme.resetpos: + description: 'Elimina l''ultima posizione di un giocatore' + detailedDescription: 'Elimina l''ultima posizione conosciuta del giocatore indicato o di tutti i giocatori.' + arg1: + label: 'giocatore/*' + description: 'Nome del giocatore o ''*'' per tutti i giocatori' + authme.purgebannedplayers: + description: 'Elimina i dati dei giocatori banditi' + detailedDescription: 'Elimina tutti i dati di AuthMeReloaded dei giocatori banditi.' + authme.switchantibot: + description: 'Cambia lo stato del servizio di AntiBot' + detailedDescription: 'Cambia lo stato del servizio di AntiBot allo stato indicato.' + arg1: + label: 'stato' + description: 'ON / OFF' + authme.reload: + description: 'Ricarica il plugin' + detailedDescription: 'Ricarica il plugin AuthMeReloaded.' + authme.version: + description: 'Informazioni sulla versione' + detailedDescription: 'Mostra informazioni dettagliate riguardo la versione di AuthMeReloaded in uso, gli sviluppatori, i collaboratori e la licenza.' + authme.converter: + description: 'Comando per il convertitore' + detailedDescription: 'Comando per il convertitore di AuthMeReloaded.' + arg1: + label: 'incarico' + description: 'Incarico di conversione: xauth / crazylogin / rakamak / royalauth / vauth / sqliteToSql / mysqlToSqlite' + authme.help: + description: 'Visualizza l''assistenza' + detailedDescription: 'Visualizza informazioni dettagliate per i comandi ''/authme''.' + arg1: + label: 'comando' + description: 'Il comando di cui vuoi ricevere assistenza' diff --git a/plugin/platform-bukkit/src/main/resources/messages/help_ja.yml b/plugin/platform-bukkit/src/main/resources/messages/help_ja.yml new file mode 100644 index 00000000..830275e4 --- /dev/null +++ b/plugin/platform-bukkit/src/main/resources/messages/help_ja.yml @@ -0,0 +1,45 @@ +# Translation config for the AuthMe help, e.g. when /authme help or /authme help register is called + +# ------------------------------------------------------- +# List of texts used in the help section +common: + header: '==========[ AuthMeReloaded ヘルプ ]==========' + optional: 'オプション' + hasPermission: '権限を持っています' + noPermission: '権限がありません' + default: 'デフォルト' + result: '結果' + defaultPermissions: + notAllowed: '権限がありません' + opOnly: 'OPのみ' + allowed: '全員に許可' + +# ------------------------------------------------------- +# Titles of the individual help sections +# Set the translation text to empty text to disable the section, e.g. to hide alternatives: +# alternatives: '' +section: + command: 'コマンド' + description: '簡単な説明' + detailedDescription: '詳細な説明' + arguments: '引数' + permissions: '権限' + alternatives: '代替' + children: 'コマンド' + +# ------------------------------------------------------- +# You can translate the data for all commands using the below pattern. +# For example to translate /authme reload, create a section "authme.reload", or "login" for /login +# If the command has arguments, you can use arg1 as below to translate the first argument, and so forth +# Translations don't need to be complete; any missing section will be taken from the default silently +# Important: Put main commands like "authme" before their children (e.g. "authme.reload") +commands: + authme.register: + description: 'プレイヤーを登録します' + detailedDescription: '指定されたプレイヤーを指定されたパスワードで登録します。' + arg1: + label: 'プレイヤー名' + description: 'プレイヤー名' + arg2: + label: 'パスワード' + description: 'パスワード' diff --git a/plugin/platform-bukkit/src/main/resources/messages/help_ko.yml b/plugin/platform-bukkit/src/main/resources/messages/help_ko.yml new file mode 100644 index 00000000..561dc715 --- /dev/null +++ b/plugin/platform-bukkit/src/main/resources/messages/help_ko.yml @@ -0,0 +1,45 @@ +# Translation config for the AuthMe help, e.g. when /authme help or /authme help register is called + +# ------------------------------------------------------- +# List of texts used in the help section +common: + header: '==========[ AuthMeReloaded 도움말 ]==========' + optional: '선택' + hasPermission: '권한이 있습니다' + noPermission: '권한이 없습니다' + default: 'Default' + result: 'Result' + defaultPermissions: + notAllowed: '권한이 없습니다' + opOnly: 'OP만 가능' + allowed: '모두에게 허용됨' + +# ------------------------------------------------------- +# Titles of the individual help sections +# Set the translation text to empty text to disable the section, e.g. to hide alternatives: +# alternatives: '' +section: + command: '명령어' + description: '짧은 설명' + detailedDescription: '상세 설명' + arguments: '인수' + permissions: '권한' + alternatives: '대안' + children: '명령어' + +# ------------------------------------------------------- +# You can translate the data for all commands using the below pattern. +# For example to translate /authme reload, create a section "authme.reload", or "login" for /login +# If the command has arguments, you can use arg1 as below to translate the first argument, and so forth +# Translations don't need to be complete; any missing section will be taken from the default silently +# Important: Put main commands like "authme" before their children (e.g. "authme.reload") +commands: + authme.register: + description: '플레이어 회원 가입' + detailedDescription: '지정한 플레이어를 지정한 비밀번호로 등록합니다.' + arg1: + label: '플레이어' + description: '플레이어 닉네임' + arg2: + label: '비밀번호' + description: '비밀번호' diff --git a/plugin/platform-bukkit/src/main/resources/messages/help_lt.yml b/plugin/platform-bukkit/src/main/resources/messages/help_lt.yml new file mode 100644 index 00000000..ee141497 --- /dev/null +++ b/plugin/platform-bukkit/src/main/resources/messages/help_lt.yml @@ -0,0 +1,45 @@ +# Translation config for the AuthMe help, e.g. when /authme help or /authme help register is called + +# ------------------------------------------------------- +# List of texts used in the help section +common: + header: '==========[ AuthMeReloaded PAGALBA ]==========' + optional: 'Neprivaloma' + hasPermission: 'Jūs turite leidimą' + noPermission: 'Jūs neturite leidimo' + default: 'Numatytas' + result: 'Rezultatas' + defaultPermissions: + notAllowed: 'Nėra leidimo' + opOnly: 'Tik OP' + allowed: 'Visiems leistina' + +# ------------------------------------------------------- +# Titles of the individual help sections +# Set the translation text to empty text to disable the section, e.g. to hide alternatives: +# alternatives: '' +section: + command: 'Komanda' + description: 'Trumpas aprašas' + detailedDescription: 'Detalus aprašas' + arguments: 'Argumentai' + permissions: 'Leidimai' + alternatives: 'Alternatyvos' + children: 'Komandos' + +# ------------------------------------------------------- +# You can translate the data for all commands using the below pattern. +# For example to translate /authme reload, create a section "authme.reload", or "login" for /login +# If the command has arguments, you can use arg1 as below to translate the first argument, and so forth +# Translations don't need to be complete; any missing section will be taken from the default silently +# Important: Put main commands like "authme" before their children (e.g. "authme.reload") +commands: + authme.register: + description: 'Užregistruoti žaidėją' + detailedDescription: 'Užregistruokite nurodytą žaidėją su nurodytu slaptažodžiu.' + arg1: + label: 'žaidėjas' + description: 'Žaidėjo vardas' + arg2: + label: 'slaptažodis' + description: 'Slaptažodis' diff --git a/plugin/platform-bukkit/src/main/resources/messages/help_nl.yml b/plugin/platform-bukkit/src/main/resources/messages/help_nl.yml new file mode 100644 index 00000000..bf3cf3c9 --- /dev/null +++ b/plugin/platform-bukkit/src/main/resources/messages/help_nl.yml @@ -0,0 +1,45 @@ +# Vertaling configuratie voor de help commando's in AuthMe, bijvoorbeeld bij het gebruik van /authme help of /authme help register + +# ------------------------------------------------------- +# Lijst van berichten zichtbaar in het help scherm +common: + header: '==========[ AuthMeReloaded HELP ]==========' + optional: 'Optioneel' + hasPermission: 'Je hebt rechten' + noPermission: 'Je hebt geen rechten' + default: 'Standaard' + result: 'Resultaat' + defaultPermissions: + notAllowed: 'Geen rechten' + opOnly: 'Alleen OP''s' + allowed: 'Iedereen is toegestaan' + +# ------------------------------------------------------- +# Titels van individuele help secties +# Stel de vertaling in als lege tekst om alternatieve commando's te verbergen: +# alternatives: '' +section: + command: 'Commando' + description: 'Korte beschrijving' + detailedDescription: 'Gedetailleerde beschrijving' + arguments: 'Argumenten' + permissions: 'Rechten' + alternatives: 'Alternatieven' + children: 'Commando''s' + +# ------------------------------------------------------- +# Je kunt de tekst voor alle commando's hieronder vertalen met het volgende patroon. +# Bijvoorbeeld: om /authme reload te vertalen, maak een sectie "authme.reload" aan, of "login" voor /login +# Als het commando argumenten/parameters bevat, kun je arg1 zoals hieronder vertalen, enzovoorts. +# Vertalingen hoeven niet compleet te zijn; missende secties zullen automatisch vanuit de standaard vertaling gehaald worden. +# Belangrijk: Plaats basis commando's zoals "authme" vóór hun sub-commando's (zoals "authme.reload") +commands: + authme.register: + description: 'Registreer een speler' + detailedDescription: 'Registreer de gespecificeerde speler met het opgegeven wachtwoord.' + arg1: + label: 'speler' + description: 'Naam speler' + arg2: + label: 'wachtwoord' + description: 'Wachtwoord' diff --git a/plugin/platform-bukkit/src/main/resources/messages/help_pl.yml b/plugin/platform-bukkit/src/main/resources/messages/help_pl.yml new file mode 100644 index 00000000..a25326e8 --- /dev/null +++ b/plugin/platform-bukkit/src/main/resources/messages/help_pl.yml @@ -0,0 +1,45 @@ +# Tlumaczenie configu dla AuthMe pomoc, kiedy wpiszesz /authme help lub /authme help register podana wiadomosc zostanie wyslana + +# ------------------------------------------------------- +# Lista tekstu uzyta w pomocy. +common: + header: '==========[ AuthMeReloaded - Pomoc ]==========' + optional: 'Opcjonalnie' + hasPermission: 'Posiadasz uprawnienia' + noPermission: 'Brak uprawnień' + default: 'Domyślnie' + result: 'Wynik' + defaultPermissions: + notAllowed: 'Nie posiadasz uprawnień' + opOnly: 'Tylko dla OP' + allowed: 'Dozwolone dla wszystkich' + +# ------------------------------------------------------- +# Tytuly z inwidualnych stref w pomoc. +# Zostaw tlumaczenie puste aby wylaczyc dana komende. Np.: +# alternatives: '' +section: + command: 'Komenda' + description: 'Opis' + detailedDescription: 'Szczegółowy opis' + arguments: 'Argumenty' + permissions: 'Uprawnienia' + alternatives: 'Aliasy' + children: 'Komendy' + +# ------------------------------------------------------- +# Mozesz przetlumaczyc wszystkie komendy uzywajac tego wzoru. +# Na przyklad jesli chcesz przetlumaczyc /authme reload, utworz selekcje "authme.reload", lub "login" dla /login +# Jesli komenda posiada argumenty, mozesz uzyc arg1 aby przetlumaczyc pierwszy argument, i nastepne +# Tlumaczenia nie musza byc kompletne; kazde braki beda uzupelniane domyslnymi wiadomosciami z pluginu. +# Uwaga: Postaw glowna klase (np. "authme") przed ich dziecmi (np. "authme.reload") +commands: + authme.register: + description: 'Rejestracja gracza' + detailedDescription: 'Rejestracja gracza z określonym hasłem' + arg1: + label: 'gracz' + description: 'Nazwa gracza' + arg2: + label: 'hasło' + description: 'Hasło gracza' diff --git a/plugin/platform-bukkit/src/main/resources/messages/help_pt.yml b/plugin/platform-bukkit/src/main/resources/messages/help_pt.yml new file mode 100644 index 00000000..2b2ead6c --- /dev/null +++ b/plugin/platform-bukkit/src/main/resources/messages/help_pt.yml @@ -0,0 +1,45 @@ +# Translation config for the AuthMe help, e.g. when /authme help or /authme help register is called + +# ------------------------------------------------------- +# List of texts used in the help section +common: + header: '==========[ AJUDA DO AuthMeReloaded ]==========' + optional: 'Opcional' + hasPermission: 'Tu tens permissão' + noPermission: 'Não tens permissão' + default: 'Padrão' + result: 'Resultado' + defaultPermissions: + notAllowed: 'Sem permissão' + opOnly: 'Só OP' + allowed: 'Toda gente é permitida' + +# ------------------------------------------------------- +# Titles of the individual help sections +# Set the translation text to empty text to disable the section, e.g. to hide alternatives: +# alternatives: '' +section: + command: 'Comando' + description: 'Breve descrição' + detailedDescription: 'Descrição detalhada' + arguments: 'Argumentos' + permissions: 'Permissões' + alternatives: 'Alternativas' + children: 'Comandos' + +# ------------------------------------------------------- +# You can translate the data for all commands using the below pattern. +# For example to translate /authme reload, create a section "authme.reload", or "login" for /login +# If the command has arguments, you can use arg1 as below to translate the first argument, and so forth +# Translations don't need to be complete; any missing section will be taken from the default silently +# Important: Put main commands like "authme" before their children (e.g. "authme.reload") +commands: + authme.register: + description: 'Registar um jogador' + detailedDescription: 'Registar um jogador com uma senha especifica.' + arg1: + label: 'jogador' + description: 'Nome de jogador' + arg2: + label: 'senha' + description: 'Senha' diff --git a/plugin/platform-bukkit/src/main/resources/messages/help_ro.yml b/plugin/platform-bukkit/src/main/resources/messages/help_ro.yml new file mode 100644 index 00000000..74e92c1f --- /dev/null +++ b/plugin/platform-bukkit/src/main/resources/messages/help_ro.yml @@ -0,0 +1,45 @@ +# Translation config for the AuthMe help, e.g. when /authme help or /authme help register is called + +# ------------------------------------------------------- +# List of texts used in the help section +common: + header: '==========[ AuthMeReloaded HELP ]==========' + optional: 'Optional' + hasPermission: 'Tu ai permisiunea' + noPermission: 'Fara permisiune' + default: 'Default' + result: 'Rezultat' + defaultPermissions: + notAllowed: 'Nu ai permisiune' + opOnly: 'Doar pentru operatori' + allowed: 'Toata lumea are acces' + +# ------------------------------------------------------- +# Titles of the individual help sections +# Set the translation text to empty text to disable the section, e.g. to hide alternatives: +# alternatives: '' +section: + command: 'Comanda' + description: 'Descriere scurta' + detailedDescription: 'Descriere detaliata' + arguments: 'Argumente' + permissions: 'Permisiuni' + alternatives: 'Alternative' + children: 'Comenzi' + +# ------------------------------------------------------- +# You can translate the data for all commands using the below pattern. +# For example to translate /authme reload, create a section "authme.reload", or "login" for /login +# If the command has arguments, you can use arg1 as below to translate the first argument, and so forth +# Translations don't need to be complete; any missing section will be taken from the default silently +# Important: Put main commands like "authme" before their children (e.g. "authme.reload") +commands: + authme.register: + description: 'Inregistreaza un jucator' + detailedDescription: 'Inregistreaza un jucator cu parola specificata.' + arg1: + label: 'jucator' + description: 'Numele jucatorului' + arg2: + label: 'parola' + description: 'Parola' diff --git a/plugin/platform-bukkit/src/main/resources/messages/help_ru.yml b/plugin/platform-bukkit/src/main/resources/messages/help_ru.yml new file mode 100644 index 00000000..fff95815 --- /dev/null +++ b/plugin/platform-bukkit/src/main/resources/messages/help_ru.yml @@ -0,0 +1,153 @@ +# Translation config for the AuthMe help, e.g. when /authme help or /authme help register is called + +# ------------------------------------------------------- +# List of texts used in the help section +common: + header: '==========[ Помощь по AuthMeReloaded ]==========' + optional: 'необязательно' + hasPermission: 'есть' + noPermission: 'нет' + default: 'По умолчанию' + result: 'Право на использование' + defaultPermissions: + notAllowed: 'нет прав' + opOnly: 'только для операторов' + allowed: 'разрешено всем' + +# ------------------------------------------------------- +# Titles of the individual help sections +# Set the translation text to empty text to disable the section, e.g. to hide alternatives: +# alternatives: '' +section: + command: 'Команда' + description: 'Краткое описание' + detailedDescription: 'Описание' + arguments: 'Аргументы' + permissions: 'Права' + alternatives: 'Альтернативы' + children: 'Команды' + +# ------------------------------------------------------- +# You can translate the data for all commands using the below pattern. +# For example to translate /authme reload, create a section "authme.reload", or "login" for /login +# If the command has arguments, you can use arg1 as below to translate the first argument, and so forth +# Translations don't need to be complete; any missing section will be taken from the default silently +# Important: Put main commands like "authme" before their children (e.g. "authme.reload") +commands: + authme.register: + description: 'Регистрация игрока' + detailedDescription: 'Регистрирует игрока с указанным именем и паролем.' + arg1: + label: 'игрок' + description: 'имя игрока' + arg2: + label: 'пароль' + description: 'пароль' + authme.unregister: + description: 'Удаление регистрации игрока' + detailedDescription: 'Убирает регистрацию указанного игрока из базы данных.' + arg1: + label: 'игрок' + description: 'имя игрока' + authme.forcelogin: + description: 'Авторизация игрока' + detailedDescription: 'Авторизация указанного игрока.' + arg1: + label: 'игрок' + description: 'имя игрока в сети' + authme.password: + description: 'Изменение пароля игрока' + detailedDescription: 'Изменяет пароль указанного игрока.' + arg1: + label: 'игрок' + description: 'имя игрока' + arg2: + label: 'пароль' + description: 'новый пароль' + authme.lastlogin: + description: 'Последний вход игрока' + detailedDescription: 'Показывает дату и время последнего входа указанного игрока.' + arg1: + label: 'игрок' + description: 'имя игрока' + authme.accounts: + description: 'Учётные записи игрока' + detailedDescription: 'Отображает все учётные записи указанного игрока по его имени и IP-адресу.' + arg1: + label: 'игрок' + description: 'имя игрока или IP-адрес' + authme.email: + description: 'Электронная почта игрока' + detailedDescription: 'Отображает адрес электронной почты указанного игрока.' + arg1: + label: 'игрок' + description: 'имя игрока' + authme.setemail: + description: 'Смена эл. почты игрока' + detailedDescription: 'Изменяет адрес электронной почты указанного игрока.' + arg1: + label: 'игрок' + description: 'имя игрока' + arg2: + label: 'эл. почта' + description: 'электронная почта игрока' + authme.getip: + description: 'IP-адрес игрока' + detailedDescription: 'Показывает IP-адрес указанного игрока в сети.' + arg1: + label: 'игрок' + description: 'имя игрока' + authme.spawn: + description: 'Перемещение на точку возрождения' + detailedDescription: 'Перемещает на точку возрождения.' + authme.setspawn: + description: 'Перемещение точки возрождения' + detailedDescription: 'Перемещает точку возрождения игроков на ваше текущее местоположение.' + authme.firstspawn: + description: 'Перемещение на начальную точку появления' + detailedDescription: 'Перемещает на начальную точку появления.' + authme.setfirstspawn: + description: 'Перемещение начальной точки появления' + detailedDescription: 'Перемещает начальную точку появления игроков на ваше текущее местоположение.' + authme.purge: + description: 'Удаление старых данных' + detailedDescription: 'Удаляет старые данные старше указанного количества дней.' + arg1: + label: 'дни' + description: 'количество дней' + authme.resetpos: + description: 'Удаление последнего местоположения игрока' + detailedDescription: 'Удаляет последнее известное местоположение указанного игрока или всех игроков.' + arg1: + label: 'игрок|*' + description: 'имя игрока/все игроки' + authme.purgebannedplayers: + description: 'Удаление данных о заблокированных' + detailedDescription: 'Удаляет все данные о заблокированных игроках.' + authme.switchantibot: + description: 'Изменение AntiBot-режима' + detailedDescription: 'Изменяет AntiBot-режим на указанный.' + arg1: + label: 'ON|OFF' + description: 'включить/выключить' + authme.reload: + description: 'Перезагрузка плагина' + detailedDescription: 'Перезагружает плагин AuthMeReloaded.' + authme.version: + description: 'Информация о версии' + detailedDescription: 'Показывает подробную информацию об установленной версии AuthMeReloaded, его разработчиках, помощниках, а также о лицензии плагина.' + authme.converter: + description: 'Преобразователь' + detailedDescription: 'Преобразовывает базу данных.' + arg1: + label: 'тип' + description: 'тип преобразовывания: xauth / crazylogin / rakamak / royalauth / vauth / sqliteToSql / mysqlToSqlite' + authme.help: + description: 'Просмотр помощи' + detailedDescription: 'Показывает помощь по командам /authme.' + arg1: + label: 'команда' + description: 'команда, для которой нужна помощь' + authme.backup: + description: 'Создание резервной копии' + detailedDescription: 'Создаёт резервную копию зарегистрированных пользователей.' diff --git a/plugin/platform-bukkit/src/main/resources/messages/help_si.yml b/plugin/platform-bukkit/src/main/resources/messages/help_si.yml new file mode 100644 index 00000000..207ec589 --- /dev/null +++ b/plugin/platform-bukkit/src/main/resources/messages/help_si.yml @@ -0,0 +1,45 @@ +# Translation config for the AuthMe help, e.g. when /authme help or /authme help register is called + +# ------------------------------------------------------- +# List of texts used in the help section +common: + header: '==========[ AuthMeReloaded POMOČ ]==========' + optional: 'Neobvezno' + hasPermission: 'Imate dovoljenje' + noPermission: 'Nimate dovoljenja' + default: 'Prevzeto' + result: 'Rezultat' + defaultPermissions: + notAllowed: 'Nimate dovoljenja' + opOnly: 'Samo za OP' + allowed: 'Dovoljeno za vse' + +# ------------------------------------------------------- +# Titles of the individual help sections +# Set the translation text to empty text to disable the section, e.g. to hide alternatives: +# alternatives: '' +section: + command: 'Ukaz' + description: 'Krati opis' + detailedDescription: 'Natančen opis' + arguments: 'Argumenti' + permissions: 'Dovoljenja' + alternatives: 'Alternative' + children: 'Ukazi' + +# ------------------------------------------------------- +# You can translate the data for all commands using the below pattern. +# For example to translate /authme reload, create a section "authme.reload", or "login" for /login +# If the command has arguments, you can use arg1 as below to translate the first argument, and so forth +# Translations don't need to be complete; any missing section will be taken from the default silently +# Important: Put main commands like "authme" before their children (e.g. "authme.reload") +commands: + authme.register: + description: 'Register a player' + detailedDescription: 'Register the specified player with the specified password.' + arg1: + label: 'player' + description: 'Player name' + arg2: + label: 'password' + description: 'Password' diff --git a/plugin/platform-bukkit/src/main/resources/messages/help_sr.yml b/plugin/platform-bukkit/src/main/resources/messages/help_sr.yml new file mode 100644 index 00000000..144b9d9a --- /dev/null +++ b/plugin/platform-bukkit/src/main/resources/messages/help_sr.yml @@ -0,0 +1,45 @@ +# Konfiguracija za prevođenje pomoći AuthMe, primer kada kucate /authme help ili /authme help register + +# ------------------------------------------------------- +# Lista of teksta u pomoćnoj sekciji +common: + header: '==========[ AuthMeReloaded POMOĆ ]==========' + optional: 'Opcionalno' + hasPermission: 'Imate dozvola' + noPermission: 'Nemate dozvola' + default: 'Početan' + result: 'Rezultat' + defaultPermissions: + notAllowed: 'Nemate dozvola' + opOnly: 'Samo OP-ovi' + allowed: 'Dozvoljeno svima' + +# ------------------------------------------------------- +# Naslovi pojedinih sekcija pomoći +# Izbrišite deo teksta ako želite da iskjučite to, primer da sakrijte alternative: +# alternatives: '' +section: + command: 'Komanda' + description: 'Kratak opis' + detailedDescription: 'Detaljan opis' + arguments: 'Argumenti' + permissions: 'Dozvole' + alternatives: 'Alternative' + children: 'Komande' + +# ------------------------------------------------------- +# Možete prevesti podatke svih komandi koristeći uzorak ispod. +# Na primer da prevedete /authme reload, napravite sekciju "authme.reload", ili "login" za /login +# Ako komanda ima argumente, možete koristiti arg1 kao što je prikazano ispod za prvi argument, itd +# Prevodi ne moraju biti kompletni; sve sekcije koje nedostaju će biti uklonjene iz početnog tiho +# Važno: Postavite glavne komande kao što su "authme" pre njihovih sledbenika (primer "authme.reload") +commands: + authme.register: + description: 'Registrujte igrača' + detailedDescription: 'Registrujte specifičnog igrača specifičnom lozinkom.' + arg1: + label: 'igrač' + description: 'Ime igrača' + arg2: + label: 'lozinka' + description: 'Lozinka' diff --git a/plugin/platform-bukkit/src/main/resources/messages/help_vn.yml b/plugin/platform-bukkit/src/main/resources/messages/help_vn.yml new file mode 100644 index 00000000..f63905ed --- /dev/null +++ b/plugin/platform-bukkit/src/main/resources/messages/help_vn.yml @@ -0,0 +1,45 @@ +# Translation config for the AuthMe help, e.g. when /authme help or /authme help register is called + +# ------------------------------------------------------- +# List of texts used in the help section +common: + header: '==========[ AuthMeReloaded Trợ giúp ]==========' + optional: 'Tùy chọn' + hasPermission: 'Bạn có quyền' + noPermission: 'Không có quyền' + default: 'Mặc định' + result: 'Kết quả' + defaultPermissions: + notAllowed: 'Không cho phép' + opOnly: 'Chỉ dành cho Op' + allowed: 'Dành cho mọi người' + +# ------------------------------------------------------- +# Titles of the individual help sections +# Set the translation text to empty text to disable the section, e.g. to hide alternatives: +# alternatives: '' +section: + command: 'Lệnh' + description: 'Miêu tả ngắn' + detailedDescription: 'Miêu tả cụ thể' + arguments: 'Arguments' + permissions: 'Quyền hạn' + alternatives: 'Các lựa chọn khác' + children: 'Lệnh' + +# ------------------------------------------------------- +# You can translate the data for all commands using the below pattern. +# For example to translate /authme reload, create a section "authme.reload", or "login" for /login +# If the command has arguments, you can use arg1 as below to translate the first argument, and so forth +# Translations don't need to be complete; any missing section will be taken from the default silently +# Important: Put main commands like "authme" before their children (e.g. "authme.reload") +commands: + authme.register: + description: 'Đăng ký người chơi' + detailedDescription: 'Đăng ký người chơi chỉ định với mật khẩu' + arg1: + label: 'player' + description: 'Tên của người chơi' + arg2: + label: 'password' + description: 'Mật khẩu' diff --git a/plugin/platform-bukkit/src/main/resources/messages/help_zhcn.yml b/plugin/platform-bukkit/src/main/resources/messages/help_zhcn.yml new file mode 100644 index 00000000..c08add09 --- /dev/null +++ b/plugin/platform-bukkit/src/main/resources/messages/help_zhcn.yml @@ -0,0 +1,204 @@ +common: + header: '======================================' + optional: '可选' + hasPermission: '您拥有权限去使用这个指令' + noPermission: '您没有权限使用这个指令' + default: '默认' + result: '您的权限' + defaultPermissions: + notAllowed: '任何人不能使用' + opOnly: 'OP拥有此权限' + allowed: '所有人都可以使用' + + +section: + command: '指令' + description: '功能' + detailedDescription: '功能详情' + arguments: '参数' + permissions: '权限' + alternatives: '别名' + children: '子命令' + + +commands: + authme.register: + description: '注册一个玩家' + detailedDescription: '注册一个玩家' + arg1: + label: '玩家' + description: '玩家名称' + arg2: + label: '密码' + description: '密码' + authme.unregister: + description: '注销一个玩家' + detailedDescription: '注销一个玩家' + arg1: + label: '玩家' + description: '玩家' + authme.forcelogin: + description: '强制玩家重新登录' + detailedDescription: '强制使指定玩家重新登录' + arg1: + label: '玩家' + description: '玩家' + authme.password: + description: '改变某个玩家的密码' + detailedDescription: '改变某个玩家的密码' + arg1: + label: '玩家' + description: '玩家' + arg2: + label: '新密码' + description: '新密码' + authme.lastlogin: + description: '查看玩家最后登录时间' + detailedDescription: '查看玩家最后登录时间' + arg1: + label: '玩家' + description: '玩家' + authme.accounts: + description: '查看玩家IP下的账户' + detailedDescription: '查看玩家IP下的账户' + arg1: + label: '玩家或IP' + description: '玩家或IP' + authme.email: + description: '查看玩家的邮箱' + detailedDescription: '查看玩家的邮箱' + arg1: + label: '玩家' + description: '玩家' + authme.setemail: + description: '改变玩家的邮箱' + detailedDescription: '改变玩家的邮箱' + arg1: + label: '玩家' + description: '玩家' + arg2: + label: '邮箱' + description: '邮箱' + authme.getip: + description: '获取玩家IP' + detailedDescription: '获取玩家IP' + arg1: + label: '玩家' + description: '玩家' + authme.spawn: + description: '传送到AuthMe出生点' + detailedDescription: '传送到AuthMe出生点' + authme.setspawn: + description: '改变AuthMe出生点' + detailedDescription: '改变AuthMe出生点' + authme.firstspawn: + description: '传送到第一次进入游戏出生点' + detailedDescription: '传送到第一次进入游戏出生点' + authme.setfirstspawn: + description: '设置第一次进入游戏的出生点' + detailedDescription: '设置第一次进入游戏的出生点' + authme.purge: + description: '删除指定天数之前没登录的玩家登陆数据' + detailedDescription: '删除指定天数之前没登录的玩家登陆数据' + arg1: + label: '天数' + description: '天数' + authme.resetpos: + description: '重置玩家登出位置' + detailedDescription: '重置玩家登出位置' + arg1: + label: '玩家/*' + description: '玩家名称或所有玩家' + authme.purgebannedplayers: + description: '删除已经被封禁的玩家数据' + detailedDescription: '删除已经被封禁的玩家数据' + authme.switchantibot: + description: '改变AntiBot的状态' + detailedDescription: '改变AntiBot的状态' + arg1: + label: '开关' + description: '选项: ON/OFF' + authme.reload: + description: '重载插件' + detailedDescription: '重载插件' + authme.version: + description: '查看版本信息' + detailedDescription: '查看AuthmeReload版本,开发者,贡献者和许可' + authme.converter: + description: '转换数据命令' + detailedDescription: '转换数据命令' + arg1: + label: '类型' + description: '转换类型:xauth/crazylogin/rakamak/royalauth/vauth/sqliteToSql/mysqlToSqlite' + authme.messages: + description: '添加信息' + detailedDescription: '在语言文件夹中添加缺少的信息' + authme.help: + description: '查看帮助' + detailedDescription: '查看帮助' + arg1: + label: '子命令' + description: '查看的指令' + unregister: + description: '注销您的账户' + detailedDescription: '注销您的账户' + arg1: + label: '密码' + description: '密码' + changepassword: + description: '更改您的密码' + detailedDescription: '更改您的密码' + arg1: + label: '旧的密码' + description: '旧的密码' + arg2: + label: '新的密码' + description: '新的密码' + email: + description: '绑定邮箱或更改密码' + detailedDescription: '绑定邮箱或更改密码' + email.show: + description: '查看邮箱' + detailedDescription: '查看您的邮箱地址' + email.add: + description: '绑定邮箱' + detailedDescription: '为您的账户绑定邮箱' + arg1: + label: '邮箱' + description: '邮箱地址' + arg2: + label: '邮箱' + description: '重新输入邮箱地址' + email.change: + description: '改变邮箱地址' + detailedDescription: '更改您账户的邮箱地址' + arg1: + label: '旧邮箱' + description: '旧的邮箱地址' + arg2: + label: '新邮箱' + description: '新的邮箱地址' + email.recover: + description: '通过邮箱改变密码' + detailedDescription: '通过邮箱改变密码' + arg1: + label: '邮箱' + description: '邮箱地址' + email.help: + description: '查看帮助' + detailedDescription: '查看邮箱帮助' + arg1: + label: '子命令' + description: '指令' + captcha: + description: '验证码' + detailedDescription: '验证码' + arg1: + label: '验证码' + description: '验证码' + captcha.help: + description: '查看验证码帮助' + detailedDescription: '查看验证码帮助' + arg1: + label: '子命令' + description: '指令' diff --git a/plugin/platform-bukkit/src/main/resources/messages/help_zhhk.yml b/plugin/platform-bukkit/src/main/resources/messages/help_zhhk.yml new file mode 100644 index 00000000..b3b4650a --- /dev/null +++ b/plugin/platform-bukkit/src/main/resources/messages/help_zhhk.yml @@ -0,0 +1,45 @@ +# Translation config for the AuthMe help, e.g. when /authme help or /authme help register is called + +# ------------------------------------------------------- +# List of texts used in the help section +common: + header: '==========[ 登入系統指南 ]==========' + optional: '選填' + hasPermission: '你擁有此權限節點' + noPermission: '你並沒有此權限節點' + default: '預設權限' + result: '你的權限' + defaultPermissions: + notAllowed: '你並沒有此權限節點' + opOnly: '伺服器操作員限定' + allowed: '開放給所有玩家' + +# ------------------------------------------------------- +# Titles of the individual help sections +# Set the translation text to empty text to disable the section, e.g. to hide alternatives: +# alternatives: '' +section: + command: '指令' + description: '簡述' + detailedDescription: '詳述' + arguments: '參數' + permissions: '權限' + alternatives: '替代指令' + children: '指令集' + +# ------------------------------------------------------- +# You can translate the data for all commands using the below pattern. +# For example to translate /authme reload, create a section "authme.reload", or "login" for /login +# If the command has arguments, you can use arg1 as below to translate the first argument, and so forth +# Translations don't need to be complete; any missing section will be taken from the default silently +# Important: Put main commands like "authme" before their children (e.g. "authme.reload") +commands: + authme.register: + description: '登記一個新的玩家帳戶' + detailedDescription: '透過管理員身份,將一個玩家帳戶以指定的密碼註冊' + arg1: + label: 'player' + description: '玩家名稱' + arg2: + label: 'password' + description: '密碼' diff --git a/plugin/platform-bukkit/src/main/resources/messages/help_zhtw.yml b/plugin/platform-bukkit/src/main/resources/messages/help_zhtw.yml new file mode 100644 index 00000000..77eb6441 --- /dev/null +++ b/plugin/platform-bukkit/src/main/resources/messages/help_zhtw.yml @@ -0,0 +1,349 @@ +# Translation config for the AuthMe help, e.g. when /authme help or /authme help register is called + +# ------------------------------------------------------- +# List of texts used in the help section +common: + header: '==========[ AuthMeReloaded 幫助 ]==========' + optional: '選填' + hasPermission: '您有使用此指令的權限' + noPermission: '您沒有使用此指令的權限' + default: '預設' + result: '結果' + defaultPermissions: + notAllowed: '無權限' + opOnly: '僅限管理員' + allowed: '開放給所有人' + +# ------------------------------------------------------- +# Titles of the individual help sections +# Set the translation text to empty text to disable the section, e.g. to hide alternatives: +# alternatives: '' +section: + command: '指令' + description: '簡述' + detailedDescription: '詳述' + arguments: '參數' + permissions: '權限' + alternatives: '別名' + children: '指令集' + +# ------------------------------------------------------- +# You can translate the data for all commands using the below pattern. +# For example to translate /authme reload, create a section "authme.reload", or "login" for /login +# If the command has arguments, you can use arg1 as below to translate the first argument, and so forth +# Translations don't need to be complete; any missing section will be taken from the default silently +# Important: Put main commands like "authme" before their children (e.g. "authme.reload") +commands: + authme.register: + description: '註冊一位玩家' + detailedDescription: '以指定的密碼註冊一位玩家。' + arg1: + label: '名稱' + description: '玩家名稱' + arg2: + label: '密碼' + description: '密碼' + authme.unregister: + description: '註銷一位玩家。' + detailedDescription: '註銷指定的玩家。' + arg1: + label: '名稱' + description: '玩家名稱' + authme.forcelogin: + description: '強制玩家重新登入。' + detailedDescription: '強制指定的玩家重新登入。' + arg1: + label: '名稱' + description: '玩家名稱' + authme.password: + description: '修改玩家的密碼。' + detailedDescription: '修改指定的玩家的密碼。' + arg1: + label: '名稱' + description: '玩家名稱' + arg2: + label: '密碼' + description: '密碼' + authme.lastlogin: + description: '查看玩家最後登入的日期。' + detailedDescription: '查看指定的玩家最後登入的日期。' + arg1: + label: '名稱' + description: '玩家名稱' + authme.accounts: + description: '透過玩家名稱或 IP 顯示玩家的所有帳號。' + detailedDescription: '透過玩家名稱或 IP 顯示玩家的所有帳號。' + arg1: + label: '名稱' + description: '玩家名稱' + authme.email: + description: '顯示指定的玩家的電子郵件地址。(如果有設置的話)' + detailedDescription: '顯示指定的玩家的電子郵件地址。(如果有設置的話)' + arg1: + label: '名稱' + description: '玩家名稱' + authme.setemail: + description: '修改玩家的電子郵件地址。' + detailedDescription: '修改指定的玩家的電子郵件地址。' + arg1: + label: '名稱' + description: '玩家名稱' + arg2: + label: '電子郵件地址' + description: '電子郵件地址' + authme.getip: + description: '取得線上玩家的 IP 位址。' + detailedDescription: '取得指定的線上玩家的 IP 位址。' + arg1: + label: '名稱' + description: '玩家名稱' + authme.totp: + description: '顯示玩家是否開啟兩步驟驗證。' + detailedDescription: '顯示指定的玩家是否開啟兩步驟驗證。' + arg1: + label: '名稱' + description: '玩家名稱' + authme.disabletotp: + description: '為玩家停用兩步驟驗證。' + detailedDescription: '為指定的玩家停用兩步驟驗證。' + arg1: + label: '名稱' + description: '玩家名稱' + authme.spawn: + description: '傳送至重生點。' + detailedDescription: '傳送至重生點。' + authme.setspawn: + description: '將玩家的重生點設為您現在的位置。' + detailedDescription: '將玩家的重生點設為您現在的位置。' + authme.firstspawn: + description: '傳送至新玩家重生點。' + detailedDescription: '傳送至新玩家重生點。' + authme.setfirstspawn: + description: '將新玩家的重生點設為您現在的位置。' + detailedDescription: '將新玩家的重生點設為您現在的位置。' + authme.purge: + description: '刪除超過指定天數的 AuthMeReloaded 資料。' + detailedDescription: '刪除超過指定天數的 AuthMeReloaded 資料。' + arg1: + label: '天' + description: '天數' + authme.purgeplayer: + description: '刪除玩家資料。' + detailedDescription: '刪除指定的玩家的資料。' + arg1: + label: '名稱' + description: '玩家名稱' + arg2: + label: '選項' + description: '選項' + authme.backup: + description: '備份玩家資料。' + detailedDescription: '備份所有玩家的資料。' + authme.resetpos: + description: '重設玩家的登出前位置。' + detailedDescription: '重設指定/所有玩家的登出前位置。' + arg1: + label: '名稱/*' + description: '指定玩家/所有玩家' + authme.purgebannedplayers: + description: '刪除已被封禁的玩家的資料。' + detailedDescription: '刪除所有已被封禁的玩家的資料。' + authme.switchantibot: + description: '切換 AntiBot 的模式。' + detailedDescription: '切換 AntiBot 的模式。' + arg1: + label: '模式' + description: '模式' + authme.reload: + description: '重新載入 AuthMeReloaded。' + detailedDescription: '重新載入 AuthMeReloaded。' + authme.version: + description: '顯示 AuthMeReloaded 的詳細資訊。' + detailedDescription: '顯示 AuthMeReloaded 的詳細資訊。例如版本、開發者、貢獻者、及授權。' + authme.converter: + description: 'AuthMeReloaded 的轉換器指令。' + detailedDescription: 'AuthMeReloaded 的轉換器指令。' + arg1: + label: '工作' + description: '工作' + authme.messages: + description: '修改目前的幫助檔案。' + detailedDescription: '修改目前的幫助檔案。' + authme.recent: + description: '顯示最近登入的玩家。' + detailedDescription: '顯示最近登入的玩家。' + authme.debug: + description: '除錯。' + detailedDescription: '除錯。' + arg1: + label: '子程序' + description: '子程序' + arg2: + label: '參數' + description: '參數' + arg3: + label: '參數' + description: '參數' + authme.help: + description: '顯示 /authme 指令的詳細介紹。' + detailedDescription: '顯示 /authme 指令的詳細介紹。' + arg1: + label: '要查詢的指令' + description: '要查詢的指令' + email.show: + description: '顯示您目前的電子郵件地址。' + detailedDescription: '顯示您目前的電子郵件地址。' + email.add: + description: '新增電子郵件地址' + detailedDescription: '新增電子郵件地址至您的帳號。' + arg1: + label: '電子郵件地址' + description: '電子郵件地址' + arg2: + label: '確認電子郵件地址' + description: '確認電子郵件地址' + email.change: + description: '修改電子郵件地址' + detailedDescription: '修改您的帳號的電子郵件地址。' + arg1: + label: '舊的電子郵件地址' + description: '舊的電子郵件地址' + arg2: + label: '新的電子郵件地址' + description: '新的電子郵件地址' + email.recover: + description: '使用別的電子郵件地址復原您的帳號。' + detailedDescription: '復原您的帳號,將寄送新的密碼至您提供的電子郵件地址。' + arg1: + label: '電子郵件地址' + description: '電子郵件地址' + email.code: + description: '使用代碼復原您的帳號。' + detailedDescription: '輸入已寄送至您的電子郵件信箱的代碼以復原您的帳號。' + arg1: + label: '電子郵件地址' + description: '電子郵件地址' + email.setpassword: + description: '復原帳號後,設置新密碼。' + detailedDescription: '復原帳號後,設置新密碼。' + arg1: + label: '密碼' + description: '密碼' + email.help: + description: '顯示 /email 指令的詳細介紹。' + detailedDescription: '顯示 /email 指令的詳細介紹。' + arg1: + label: '要查詢的指令' + description: '要查詢的指令' + login: + login.help: + description: '顯示 /login 指令的詳細介紹。' + detailedDescription: '顯示 /login 指令的詳細介紹。' + arg1: + label: '要查詢的指令' + description: '要查詢的指令' + logout: + description: '登出帳號' + detailedDescription: '登出您的帳號。' + logout.help: + description: '顯示 /logout 指令的詳細介紹。' + detailedDescription: '顯示 /logout 指令的詳細介紹。' + arg1: + label: '要查詢的指令' + description: '要查詢的指令' + register: + description: '註冊帳號' + detailedDescription: '註冊您的帳號。' + arg1: + label: '密碼' + description: '密碼' + arg2: + label: '確認密碼' + description: '確認密碼' + register.help: + description: '顯示 /register 指令的詳細介紹。' + detailedDescription: '顯示 /register 指令的詳細介紹。' + arg1: + label: '要查詢的指令' + description: '要查詢的指令' + unregister: + description: '註銷帳號' + detailedDescription: '註銷您的帳號。' + arg1: + label: '密碼' + description: '密碼' + unregister.help: + description: '顯示 /unregister 指令的詳細介紹。' + detailedDescription: '顯示 /unregister 指令的詳細介紹。' + arg1: + label: '要查詢的指令' + description: '要查詢的指令' + changepassword: + description: '修改密碼' + detailedDescription: '修改您的密碼。' + arg1: + label: '舊密碼' + description: '舊密碼' + arg2: + label: '新密碼' + description: '新密碼' + changepassword.help: + description: '顯示 /changepassword 指令的詳細介紹。' + detailedDescription: '顯示 /changepassword 指令的詳細介紹。' + arg1: + label: '要查詢的指令' + description: '要查詢的指令' + totp: + description: '執行兩步驟驗證的相關操作。' + detailedDescription: '執行兩步驟驗證的相關操作。' + totp.code: + description: '登入時,輸入您的兩步驟驗證碼。' + detailedDescription: '登入時,輸入您的兩步驟驗證碼。' + arg1: + label: '驗證碼' + description: '驗證碼' + totp.add: + description: '為您的帳號啟用兩步驟驗證。' + detailedDescription: '為您的帳號啟用兩步驟驗證。' + totp.confirm: + description: '確認後,儲存生成的兩步驟驗證金鑰。' + detailedDescription: '確認後,儲存生成的兩步驟驗證金鑰。' + arg1: + label: '驗證碼' + description: '驗證碼' + totp.remove: + description: '為您的帳號停用兩步驟驗證。' + detailedDescription: '為您的帳號停用兩步驟驗證。' + arg1: + label: '驗證碼' + description: '驗證碼' + totp.help: + description: '顯示 /totp 指令的詳細介紹。' + detailedDescription: '顯示 /totp 指令的詳細介紹。' + arg1: + label: '要查詢的指令' + description: '要查詢的指令' + captcha: + description: 'AuthMeReloaded 的 Captcha 指令。' + detailedDescription: 'AuthMeReloaded 的 Captcha 指令。' + arg1: + label: 'Captcha 碼' + description: 'Captcha 碼' + captcha.help: + description: '顯示 /captcha 指令的詳細介紹。' + detailedDescription: '顯示 /captcha 指令的詳細介紹。' + arg1: + label: '要查詢的指令' + description: '要查詢的指令' + verification: + description: '完成驗證過程的指令。' + detailedDescription: '完成驗證過程的指令。' + arg1: + label: '驗證碼' + description: '驗證碼' + verification.help: + description: '顯示 /verification 指令的詳細介紹。' + detailedDescription: '顯示 /verification 指令的詳細介紹。' + arg1: + label: '要查詢的指令' + description: '要查詢的指令' diff --git a/plugin/platform-bukkit/src/main/resources/messages/messages_bg.yml b/plugin/platform-bukkit/src/main/resources/messages/messages_bg.yml new file mode 100644 index 00000000..4b370bd9 --- /dev/null +++ b/plugin/platform-bukkit/src/main/resources/messages/messages_bg.yml @@ -0,0 +1,173 @@ +# List of global tags: +# %nl% - Goes to new line. +# %username% - Replaces the username of the player receiving the message. +# %displayname% - Replaces the nickname (and colors) of the player receiving the message. + +# Registration +registration: + disabled: '&cРегистрациите са изключени!' + name_taken: '&cПотребителското име е заетo!' + register_request: '&3Моля регистрирайте се с: /register парола парола.' + command_usage: '&cКоманда: /register парола парола' + reg_only: '&4Само регистрирани потребители могат да влизат в сървъра! Моля посетете https://example.com, за да се регистрирате!' + success: '&2Успешна регистрация!' + kicked_admin_registered: 'Вие бяхте регистриран от администратора, моля да влезете отново!' + +# Password errors on registration +password: + match_error: '&cПаролите не съвпадат, провете ги отново!' + name_in_password: '&cНе можете да използвате потребителското си име за парола, моля изберете друга парола.' + unsafe_password: '&cИзбраната парола не е безопасна, моля изберете друга парола.' + forbidden_characters: '&4Паролата съдържа непозволени символи. Позволени символи: %valid_chars' + wrong_length: '&cПаролата е твърде къса или прекалено дълга! Моля опитайте с друга парола.' + pwned_password: '&cИзбраната парола не е сигурна. Тя е използвана %pwned_count пъти вече! Моля, използвайте силна парола...' + +# Login +login: + command_usage: '&cКоманда: /login парола' + wrong_password: '&cГрешна парола!' + success: '&2Успешен вход!' + login_request: '&cМоля влезте с: /login парола !' + timeout_error: '&4Времето за вход изтече, бяхте кикнат от сървъра. Моля опитайте отново!' + +# Errors +error: + denied_command: '&cЗа да използвате тази команда трябва да сте си влезли в акаунта!' + denied_chat: '&cЗа да пишете в чата трябва да сте си влезли в акаунта!' + unregistered_user: '&cПотребителското име не е регистрирано!' + not_logged_in: '&cНе сте влезли!' + no_permission: '&4Нямате нужните права за това действие!' + unexpected_error: '&4Получи се неочаквана грешка, моля свържете се с администратора!' + max_registration: '&cВие сте достигнали максималният брой регистрации (%reg_count/%max_acc %reg_names)!' + logged_in: '&cВече сте влезли!' + kick_for_vip: '&3VIP потребител влезе докато сървъра беше пълен, вие бяхте изгонен!' + kick_unresolved_hostname: '&cВъзникна грешка: неразрешено име на играч!' + tempban_max_logins: '&cВие бяхте баннат временно, понеже сте си сгрешили паролата прекалено много пъти.' + +# AntiBot +antibot: + kick_antibot: 'Защитата от ботове е включена! Трябва да изчакате няколко минути преди да влезете в сървъра.' + auto_enabled: '&4Защитата за ботове е включена заради потенциална атака!' + auto_disabled: '&2Защитата за ботове ще се изключи след %m минута/и!' + +# Unregister +unregister: + success: '&cРегистрацията е премахната успешно!' + command_usage: '&cКоманда: /unregister парола' + +# Other messages +misc: + account_not_activated: '&cВашият акаунт все още не е актириван, моля провете своят email адрес!' + not_activated: '&cАкаунтът не е активиран, моля регистрирайте се и го активирайте преди да опитате отново.' + password_changed: '&2Паротала е променена успешно!' + logout: '&2Излязохте успешно!' + reload: '&2Конфигурацията и база данните бяха презаредени правилно!' + usage_change_password: '&cКоманда: /changepassword Стара-Парола Нова-Парола' + accounts_owned_self: 'Притежавате %count акаунт/а:' + accounts_owned_other: 'Потребителят %name има %count акаунт/а:' + +# Session messages +session: + valid_session: '&2Сесията е продължена.' + invalid_session: '&cТвоят IP се е променил и сесията беше прекратена.' + +# Error messages when joining +on_join_validation: + same_ip_online: 'Вече има потребител със същото IP в сървъра!' + same_nick_online: '&4Вече има потребител, който играе в сървъра със същото потребителско име!' + name_length: '&4Потребителското име е прекалено късо или дълго. Моля опитайте с друго потребителско име!' + characters_in_name: '&4Потребителското име съдържа забранени знаци. Позволени знаци: %valid_chars' + kick_full_server: '&4Сървъра е пълен, моля опитайте отново!' + country_banned: '&4Твоята държава е забранена в този сървър!' + not_owner_error: 'Ти не си собственика на този акаунт. Моля избери друго потребителско име!' + invalid_name_case: 'Трябва да влезеш с %valid, а не с %invalid!' + quick_command: 'Използвате команди твърде бързо! Моля, влезте отново в сървъра и изчакайте малко преди да използвате команди.' + +# Email +email: + add_email_request: '&3Моля добавете имейл адрес към своят акаунт: /email add имейл имейл' + usage_email_add: '&cКоманда: /email add имейл имейл' + usage_email_change: '&cКоманда: /email change Стар-Имейл Нов-Имейл' + new_email_invalid: '&cНовият имейл е грешен, опитайте отново!' + old_email_invalid: '&cСтарият имейл е грешен, опитайте отново!' + invalid: '&cИмейла е невалиден, опитайте с друг!' + added: '&2Имейл адреса е добавен!' + add_not_allowed: '&cДобавянето на имейл не е разрешено!' + request_confirmation: '&cМоля потвърдете своя имейл адрес!' + changed: '&2Имейл адреса е сменен!' + change_not_allowed: '&cСмяната на имейл адреса не е разрешена!' + email_show: '&2Вашият имейл адрес е: &f%email' + no_email_for_account: '&2Няма добавен имейл адрес към акаунта.' + already_used: '&4Имейл адреса вече се използва, опитайте с друг.' + incomplete_settings: 'Грешка: Не всички настройки са написани за изпращане на имейл адрес. Моля свържете се с администратора!' + send_failure: 'Съобщението не беше изпратено. Моля свържете се с администратора.' + change_password_expired: 'Вече не е възможно да смените паролата си с тази команда.' + email_cooldown_error: '&cВече е бил изпратен имейл адрес. Трябва а изчакате %time преди да изпратите нов.' + +# Password recovery by email +recovery: + forgot_password_hint: '&3Забравена парола? Използвайте: /email recovery имейл' + command_usage: '&cКоманда: /email recovery имейл' + email_sent: '&2Възстановяващият имейл е изпратен успешно. Моля провете пощата си!' + code: + code_sent: 'Възстановяващият код беше изпратен на вашият email адрес.' + incorrect: 'Възстановяващият код е неправилен! Използвайте: /email recovery имейл, за да генерирате нов.' + tries_exceeded: 'Вие надвишихте максималният брой опити да въведете кода за възстановяване. Използвайте "/email recovery имейл" за да генерирате нов.' + correct: 'Кода за възстановяване е успешно въведен!' + change_password: 'Моля използвайте командата /email setpassword нова-парола за да смените Вашата парола веднага.' + +# Captcha +captcha: + usage_captcha: '&3Моля въведе цифрите/буквите от кода за сигурност: /captcha %captcha_code' + wrong_captcha: '&cКода е грешен, използвайте: "/captcha %captcha_code" в чата!' + valid_captcha: '&2Кода е валиден!' + captcha_for_registration: 'За да се регистрирате, трябва да въведете цифрите / буквите от кода за сигурност, използвайте командата: /captcha %captcha_code' + register_captcha_valid: '&2Кода е валиден! Вие вече може да се регистрирате с команда /register' + +# Verification code +verification: + code_required: '&3Тази команда е чувствителна и изисква потвърждение по имейл! Моля проверете си имейла и следвайте инструкциите в него.' + command_usage: '&cКоманда: /verification код' + incorrect_code: '&cНеправилен код, моля използвайте "/verification код" като използвате кода, който сте получили на Вашият имейл.' + success: '&2Вашата самоличност е потвърдена! Вие може да използвате всички команди в текущата Ви сесия!' + already_verified: '&2Вие вече можете да изпълнявате чувствителни команди в текущата Ви сесия!' + code_expired: '&3Вашият код е изтекъл. Изпълнете друга чувствителна команда за да генерирате нов код!' + email_needed: '&3За да потвърдите Вашата самоличност е необходимо да добавите имейл адрес към Вашият акаунт!!' + +# Time units +time: + second: 'секунда' + seconds: 'секунди' + minute: 'минута' + minutes: 'минути' + hour: 'час' + hours: 'часа' + day: 'ден' + days: 'дни' + +# Two-factor authentication +two_factor: + code_created: '&2Кода е %code. Можете да го проверите от тук: %url' + confirmation_required: 'Моля потвърдете Вашият код с команда /2fa confirm <код>' + code_required: 'Моля изпратете Вашият секретен код с команда /2fa code <код>' + already_enabled: 'Използването на секретен код е вече включено за Вашият акаунт!' + enable_error_no_code: 'Нямате добавен секретен код или кода е изтекъл. Моля изпълнете команда /2fa add' + enable_success: 'Успешно активирахте защита със секретен код за Вашият акаунт.' + enable_error_wrong_code: 'Грешен секретен код или е изтекъл. Моля изпълнете команда /2fa add' + not_enabled_error: 'Защитата със секретен код не е включена за Вашият акаунт. Моля изпълнете команда /2fa add' + removed_success: 'Успешно изключихте защитата със секретен код от Вашият акаунт!' + invalid_code: 'Невалиден код!' + +# 3rd party features: Bedrock Auto Login +bedrock_auto_login: + success: '&aУспешно автоматично влизане за Bedrock!' + +# 3rd party features: Login Location Fix +login_location_fix: + fix_portal: '&aЗаседнали сте в портал по време на влизане.' + fix_underground: '&aЗаседнали сте под земята по време на влизане.' + cannot_fix_underground: '&aЗаседнали сте под земята по време на влизане, но не можем да го поправим.' + +# 3rd party features: Double Login Fix +double_login_fix: + fix_message: '&cБяхте изключени поради двойно влизане.' diff --git a/plugin/platform-bukkit/src/main/resources/messages/messages_br.yml b/plugin/platform-bukkit/src/main/resources/messages/messages_br.yml new file mode 100644 index 00000000..396008bc --- /dev/null +++ b/plugin/platform-bukkit/src/main/resources/messages/messages_br.yml @@ -0,0 +1,176 @@ +# AuthMe Reloaded | Tradução pt-br +# Por Eufranio, GabrielDev (DeathRush) e RenanYudi +# +# Lista de tags globais: +# %nl% - Pula uma linha +# %username% - Substitui pelo nome do jogador que está recebendo a mensagem +# %displayname% - Substitui pelo nickname (e cores) do jogador que está recebendo a mensagem + +# Registro +registration: + disabled: '&cRegistrar-se está desativado neste servidor!' + name_taken: '&cVocê já registrou este nome de usuário!' + register_request: '&3Por favor, registre-se com o comando "/register "' + command_usage: '&cUse: /register ' + reg_only: '&4Somente usuários registrados podem entrar no servidor! Por favor visite www.seusite.com para se registrar!' + success: '&2Registrado com sucesso!' + kicked_admin_registered: 'Um administrador registrou você. Por favor, faça login novamente' + +# Erros de senha ao registrar-se +password: + match_error: '&cAs senhas não coincidem, tente novamente!' + name_in_password: '&Você não pode usar o seu nome como senha. Por favor, escolha outra senha...' + unsafe_password: '&cA senha escolhida não é segura. Por favor, escolha outra senha...' + forbidden_characters: '&Sua senha contém caracteres inválidos. Caracteres permitidos: %valid_chars' + wrong_length: '&cSua senha é muito curta ou muito longa! Por favor, escolha outra senha...' + pwned_password: '&cSua senha escolhida não é segura. Ela foi usada %pwned_count vezes já! Por favor, use uma senha forte...' + +# Login +login: + command_usage: '&cUse: /login ' + wrong_password: '&cSenha incorreta!' + success: '&2Login realizado com sucesso!' + login_request: '&cPor favor, faça login com o comando "/login "' + timeout_error: '&4Tempo limite excedido.' + +# Erros +error: + denied_command: '&cPara utilizar este comando é necessário estar logado!' + denied_chat: '&cPara utilizar o chat é necessário estar logado!' + unregistered_user: '&cEste usuário não está registrado!' + not_logged_in: '&cVocê não está logado!' + no_permission: '&4Você não tem permissão para executar esta ação!' + unexpected_error: '&4Ocorreu um erro inesperado. Por favor contate um administrador!' + max_registration: '&cVocê excedeu o número máximo de registros (%reg_count/%max_acc %reg_names) para o seu IP!' + logged_in: '&cVocê já está logado!' + kick_for_vip: '&3Um jogador VIP juntou-se ao servidor enquanto ele estava cheio!' + kick_unresolved_hostname: '&cErro: hostname do jogador não resolvido!' + tempban_max_logins: '&cVocê foi temporariamente banido por tentar fazer login muitas vezes.' + +# AntiBot +antibot: + kick_antibot: 'O modo de proteção AntiBot está ativo, espere alguns minutos antes de entrar no servidor!' + auto_enabled: '&4O AntiBot foi ativado devido ao grande número de conexões!' + auto_disabled: '&2AntiBot desativado após %m minutos!' + +# Deletar conta +unregister: + success: '&cConta deletada!' + command_usage: '&cUse: /unregister ' + +# Outras mensagens +misc: + account_not_activated: '&cA sua conta ainda não está ativada. Por favor, verifique seus e-mails!' + not_activated: '&cConta não ativada, por favor registre e ative antes de tentar novamente.' + password_changed: '&2Senha alterada com sucesso!' + logout: '&2Desconectado com sucesso!' + reload: '&2A configuração e o banco de dados foram recarregados corretamente!' + usage_change_password: '&cUse: /changepassword ' + accounts_owned_self: 'Você tem %count contas:' + accounts_owned_other: 'O jogador %name tem %count contas:' + +# Mensagens de sessão +session: + valid_session: '&2Você deslogou recentemente, então sua sessão foi retomada!' + invalid_session: '&fO seu IP foi alterado e sua sessão expirou!' + +# Mensagens de erro ao entrar +on_join_validation: + same_ip_online: 'Um jogador com o mesmo IP já está no servidor!' + same_nick_online: '&4Alguém com o mesmo nome de usuário já está jogando no servidor!' + name_length: '&4Seu nome de usuário ou é muito curto ou é muito longo!' + characters_in_name: '&4Seu nome de usuário contém caracteres inválidos. Caracteres permitidos: %valid_chars' + kick_full_server: '&4O servidor está cheio, tente novamente mais tarde!' + country_banned: '&4O seu país está banido deste servidor!' + not_owner_error: 'Você não é o proprietário da conta. Por favor, escolha outro nome!' + invalid_name_case: 'Você deve entrar usando o nome de usuário %valid, não %invalid.' + quick_command: 'Você usou o comando muito rápido! Por favor, entre no servidor e aguarde antes de usar um comando novamente.' + +# E-mail +email: + add_email_request: '&3Por favor, adicione seu e-mail para a sua conta com o comando "/email add "' + usage_email_add: '&cUse: /email add ' + usage_email_change: '&cUse: /email change ' + new_email_invalid: '&cE-mail novo inválido, tente novamente!' + old_email_invalid: '&cE-mail antigo inválido, tente novamente!' + invalid: '&E-mail inválido, tente novamente!' + added: '&2E-mail adicionado com sucesso!' + add_not_allowed: '&cAdicionar um e-mail não é permitido.' + request_confirmation: '&cPor favor, confirme o seu endereço de e-mail!' + changed: '&2E-mail alterado com sucesso!' + change_not_allowed: '&cAlterar o e-mail não é permitido.' + email_show: '&2O seu endereço de e-mail atual é: &f%email' + no_email_for_account: '&2Você atualmente não têm endereço de e-mail associado a esta conta.' + already_used: '&4O endereço de e-mail já está sendo usado' + incomplete_settings: 'Erro: Nem todas as configurações necessárias estão definidas para o envio de e-mails. Entre em contato com um administrador.' + send_failure: '&cO e-mail não pôde ser enviado, reporte isso a um administrador!' + change_password_expired: 'Você não pode mais usar esse comando de recuperação de senha!' + email_cooldown_error: '&cUm e-mail já foi enviado, espere %time antes de enviar novamente!' + +# Recuperação de senha por e-mail +recovery: + forgot_password_hint: '&3Esqueceu a sua senha? Use o comando "/email recovery "' + command_usage: '&cUse: /email recovery ' + email_sent: '&2E-mail de recuperação enviado! Por favor, verifique sua caixa de entrada!' + code: + code_sent: 'Um código de recuperação para a redefinição de senha foi enviado ao seu e-mail.' + incorrect: 'Código de recuperação inválido! Você tem %count tentativas restantes.' + tries_exceeded: 'Você excedeu o limite de tentativas de usar o código de recuperação! Use "/email recovery [email]" para gerar um novo.' + correct: 'Código de recuperação aceito!' + change_password: 'Por favor, use o comando /email setpassword para alterar sua senha!' + +# Captcha +captcha: + usage_captcha: '&3Para iniciar sessão você tem que resolver um captcha, utilize o comando "/captcha %captcha_code"' + wrong_captcha: '&cCaptcha incorreto. Por favor, escreva "/captcha %captcha_code" no chat!' + valid_captcha: '&2Captcha correto!' + captcha_for_registration: 'Para se registrar você tem que resolver um código captcha, utilize o comando "/captcha %captcha_code"' + register_captcha_valid: '&2Captcha correto! Agora você pode se registrar usando /register !' + +# Código de verificação +verification: + code_required: '&3Esse comando é sensível e precisa de uma verificação via e-mail! Verifique sua caixa de entrada e siga as instruções do e-mail.' + command_usage: '&cUse: /verification ' + incorrect_code: '&cCódigo incorreto, utilize "/verification " com o código que você recebeu por e-mail!' + success: '&2Sua identidade foi verificada, agora você pode usar todos os comandos durante esta sessão.' + already_verified: '&2Você já pode executar comandos sensíveis durante esta sessão!' + code_expired: '&3O seu código expirou! Execute outro comando sensível para gerar um outro código.' + email_needed: '&3Para verificar sua identidade, você precisa vincular um e-mail à sua conta!' + +# Unidades de tempo +time: + second: 'segundo' + seconds: 'segundos' + minute: 'minuto' + minutes: 'minutos' + hour: 'hora' + hours: 'horas' + day: 'dia' + days: 'dias' + +# Verificação em duas etapas +two_factor: + code_created: '&2O seu código secreto é %code. Você pode verificá-lo aqui %url' + confirmation_required: 'Confirme seu código com /2fa confirm ' + code_required: 'Registre o seu código de verificação em duas etapas com /2fa code ' + already_enabled: 'A verificação em duas etapas já está ativada nesta conta!' + enable_error_no_code: 'Nenhuma chave de verificação foi gerada ou ela expirou. Por favor, use /2fa add' + enable_success: 'Verificação em duas etapas ativada com sucesso para esta conta!' + enable_error_wrong_code: 'Código incorreto ou expirado! Por favor, use /2fa add' + not_enabled_error: 'A verificação em duas etapas não está ativada nesta conta. Use /2fa add' + removed_success: 'Verificação em duas etapas desativada com sucesso!' + invalid_code: 'Código inválido!' + +# 3rd party features: Bedrock Auto Login +bedrock_auto_login: + success: '&aLogin automático Bedrock bem-sucedido!' + +# 3rd party features: Login Location Fix +login_location_fix: + fix_portal: '&aVocê está preso no portal durante o login.' + fix_underground: '&aVocê está preso no subsolo durante o login.' + cannot_fix_underground: '&aVocê está preso no subsolo durante o login, mas não podemos corrigir.' + +# 3rd party features: Double Login Fix +double_login_fix: + fix_message: '&cVocê foi desconectado devido a login duplo.' diff --git a/plugin/platform-bukkit/src/main/resources/messages/messages_cz.yml b/plugin/platform-bukkit/src/main/resources/messages/messages_cz.yml new file mode 100644 index 00000000..4954457e --- /dev/null +++ b/plugin/platform-bukkit/src/main/resources/messages/messages_cz.yml @@ -0,0 +1,173 @@ +# List of global tags: +# %nl% - Goes to new line. +# %username% - Replaces the username of the player receiving the message. +# %displayname% - Replaces the nickname (and colors) of the player receiving the message. + +# Registration +registration: + disabled: '&cRegistrace je zakázána!' + name_taken: '&cUživatelské jméno je již zaregistrováno.' + register_request: '&cProsím, zaregistruj se pomocí "/register "' + command_usage: '&cPoužij: "/register heslo ZnovuHeslo".' + reg_only: '&cServer je pouze pro registrované! Navštiv naši webovou stránku https://priklad.cz.' + success: '&cRegistrace úspěšná!' + kicked_admin_registered: 'Admin vás právě zaregistroval; Přihlašte se prosím znovu.' + +# Password errors on registration +password: + match_error: '&cHesla se neshodují!' + name_in_password: '&cNemůžeš použít své jméno jako heslo, prosím, zvol si jiné heslo...' + unsafe_password: '&cToto heslo není bezpečné, prosím, zvol si jiné heslo...' + forbidden_characters: '&4Tvoje heslo obsahuje nepovolené znaky. Přípustné znaky jsou: %valid_chars' + wrong_length: '&cTvoje heslo nedosahuje minimální délky.' + pwned_password: '&cZvolené heslo není bezpečné. Bylo použito již %pwned_count krát! Prosím, použijte silné heslo...' + +# Login +login: + command_usage: '&cPoužij: "/login TvojeHeslo".' + wrong_password: '&cŠpatné heslo.' + success: '&cÚspěšně přihlášen!' + login_request: '&cProsím přihlaš se pomocí "/login TvojeHeslo".' + timeout_error: '&cČas pro přihlášení vypršel!' + +# Errors +error: + denied_command: '&cPokud chceš použít tento příkaz musíš být přihlášen/registrován!' + denied_chat: '&cNemůžeš psát do chatu dokuď se nepřihlásíš/nezaregistruješ!' + unregistered_user: '&cUživatelské jméno není zaregistrováno.' + not_logged_in: '&cNejsi přihlášený!' + no_permission: '&cNa tento příkaz nemáš dostatečné pravomoce.' + unexpected_error: '&cVyskytla se chyba - kontaktujte prosím administrátora ...' + max_registration: '&cPřekročil(a) jsi limit pro počet účtů (%reg_count/%max_acc %reg_names) z jedné IP adresy.' + logged_in: '&cJiž jsi přihlášen!' + kick_for_vip: '&cOmlouváme se, ale VIP hráč se připojil na plný server!' + kick_unresolved_hostname: '&cChyba: unresolved player hostname!' + tempban_max_logins: '&cByl jsi dočasně zabanován za příliš mnoho neúspěšných pokusů o přihlášení.' + +# AntiBot +antibot: + kick_antibot: 'Bezpečnostní mód AntiBot je zapnut! Musíš počkat několik minut než se budeš moct připojit znovu na server.' + auto_enabled: '[AuthMe] AntiBotMod automaticky spuštěn z důvodu mnoha souběžných připojení!' + auto_disabled: '[AuthMe] AntiBotMod automaticky ukončen po %m minutách, doufejme v konec invaze' + +# Unregister +unregister: + success: '&cÚspěšně odregistrován!' + command_usage: '&cPoužij: "/unregister TvojeHeslo".' + +# Other messages +misc: + account_not_activated: '&cTvůj účet ještě není aktivovaný, zkontroluj svůj E-mail.' + not_activated: '&cÚčet není aktivován, prosím zaregistrujte se a aktivujte ho před dalším pokusem.' + password_changed: '&cHeslo změněno!' + logout: '&cÚspěšně jsi se odhlásil.' + reload: '&cZnovu načtení nastavení AuthMe proběhlo úspěšně.' + usage_change_password: '&cPoužij: "/changepassword StaréHeslo NovéHeslo".' + accounts_owned_self: 'Vlastníš tyto účty (%count):' + accounts_owned_other: 'Hráč %name vlastní tyto účty (%count):' + +# Session messages +session: + valid_session: '&cAutomatické znovupřihlášení.' + invalid_session: '&cChyba: Počkej než vyprší tvoje relace.' + +# Error messages when joining +on_join_validation: + same_ip_online: 'Hráč se stejnou IP adresou nyní hraje na serveru!' + same_nick_online: '&cUživatel s tímto jménem již hraje na serveru.' + name_length: '&cTvůj nick je přílíš krátký, nebo přílíš dlouhý' + characters_in_name: '&cTvůj nick obsahuje nepovolené znaky. Přípustné znaky jsou: %valid_chars' + kick_full_server: '&cServer je momentálně plný, zkus to prosím později!' + country_banned: 'Připojení z tvé země je na tomto serveru zakázáno.' + not_owner_error: 'Nejsi majitelem tohoto účtu, prosím zvol si jiné jméno!' + invalid_name_case: 'Měl bys použít jméno %valid, ne jméno %invalid.' + quick_command: 'Použil jsi příkaz příliš rychle. Prosím, připoj se znovu a chvíli čekej před použitím dalšího příkazu.' + +# Email +email: + add_email_request: '&cPřidej prosím svůj email pomocí : /email add TvůjEmail TvůjEmail' + usage_email_add: '&fPoužij: /email add ' + usage_email_change: '&fPoužij: /email change ' + new_email_invalid: '[AuthMe] Nový email je chybně zadán!' + old_email_invalid: '[AuthMe] Starý email je chybně zadán!' + invalid: '[AuthMe] Nesprávný email' + added: '[AuthMe] Email přidán!' + add_not_allowed: '&cPřidávání emailu není povoleno!' + request_confirmation: '[AuthMe] Potvrď prosím svůj email!' + changed: '[AuthMe] Email změněn!' + change_not_allowed: '&cZměna emailu není povolena!' + email_show: '&2Tvůj aktuální email je: &f%email' + no_email_for_account: '&2K tomuto účtu nemáš přidanou žádnou emailovou adresu.' + already_used: '&4Tato emailová adresa je již používána' + incomplete_settings: 'Chyba: Chybí některé důležité informace pro odeslání emailu. Kontaktuj prosím admina.' + send_failure: 'Email nemohl být odeslán. Kontaktuj prosím admina.' + change_password_expired: 'Nemůžeš si změnit heslo pomocí toho příkazu.' + email_cooldown_error: '&cEmail už byl nedávno odeslán. Musíš čekat ještě %time před odesláním nového.' + +# Password recovery by email +recovery: + forgot_password_hint: '&cZapomněl jsi heslo? Napiš: /email recovery ' + command_usage: '&fPoužij: /email recovery ' + email_sent: '[AuthMe] Email pro obnovení hesla odeslán!' + code: + code_sent: 'Kód pro obnovení hesla byl odeslán na váš email.' + incorrect: 'Obnovovací kód není správný! Počet zbývajících pokusů: %count.' + tries_exceeded: 'Překročil(a) jsi počet pokusů pro vložení kódu. Použij "/email recovery [email]" pro vygenerování nového' + correct: 'Obnovovací kód zadán správně!' + change_password: 'Prosím, použij příkaz /email setpassword pro změnu hesla okamžitě.' + +# Captcha +captcha: + usage_captcha: '&cPoužij: /captcha %captcha_code' + wrong_captcha: '&cŠpatné opsána Captcha, použij prosím: /captcha %captcha_code' + valid_captcha: '&cCaptcha je zadána v pořádku!' + captcha_for_registration: 'Před registrací je nutné správně opsat captchu. Prosím použij příkaz: /captcha %captcha_code' + register_captcha_valid: '&2Captcha je v pořádku! Nyní se můžeš zaregistrovat příkazem /register' + +# Verification code +verification: + code_required: '&3Tento příkaz vyžaduje ověření emailu! Zkontroluj svou mailovou schránku a postupuj dle instrukcí' + command_usage: '&cPoužití: /verification ' + incorrect_code: '&cNesprávný kód, prosím napiš "/verification " do chatu, místo použij kód co ti přišel emailem' + success: '&2Tvoje identita ověřena! V rámci své relace můžeš nyní používat všechny příkazy.' + already_verified: '&2Již můžeš používat příkazy ve své relaci bez omezení.' + code_expired: '&3Tvůj kód vypršel. Použij další citlivý příkaz pro získání nového kódu.' + email_needed: '&3Pro ověření tvé identity potřebujeme, abys ke svému účtu přidal svůj email.' + +# Time units +time: + second: 'sekundy' + seconds: 'sekund' + minute: 'minuty' + minutes: 'minut' + hour: 'hodiny' + hours: 'hodin' + day: 'dny' + days: 'dnu' + +# Two-factor authentication +two_factor: + code_created: '&2Tvůj tajný kód je %code. Můžeš ho oskenovat zde %url' + confirmation_required: 'Prosím, potvrď svůj kód příkazem /2fa confirm ' + code_required: 'Prosím, zadej kód dvoufaktorového ověření příkazem /2fa code ' + already_enabled: 'Dvoufaktorové ověření je již zapnuté pro tomto účtu.' + enable_error_no_code: 'Pro tvůj účet nebyl vygenerován žádný 2FA klíč, a nebo již vypršel. Prosím vygeneruj si ho pomocí /2fa add' + enable_success: 'Dvoufaktorové ověření bylo úspěšně nastaveno pro tvůj účet' + enable_error_wrong_code: 'Nesprávný kód, nebo kód vypršel. Prosím spusť příkaz /2fa add' + not_enabled_error: 'Dvoufaktorové ověření není zapnuté na tvém účtu. Můžeš ho zapnout použitím příkazu /2fa add' + removed_success: 'Dvoufaktorovka byla úspěšně odebrána z tvého účtu' + invalid_code: 'Nesprávný kód!' + +# 3rd party features: Bedrock Auto Login +bedrock_auto_login: + success: '&aAutomatické přihlášení Bedrock úspěšné!' + +# 3rd party features: Login Location Fix +login_location_fix: + fix_portal: '&aPři přihlášení jste uvízli v portálu.' + fix_underground: '&aPři přihlášení jste uvízli pod zemí.' + cannot_fix_underground: '&aPři přihlášení jste uvízli pod zemí, ale nemůžeme to opravit.' + +# 3rd party features: Double Login Fix +double_login_fix: + fix_message: '&cByli jste odpojeni kvůli dvojímu přihlášení.' diff --git a/plugin/platform-bukkit/src/main/resources/messages/messages_de.yml b/plugin/platform-bukkit/src/main/resources/messages/messages_de.yml new file mode 100644 index 00000000..433e42a7 --- /dev/null +++ b/plugin/platform-bukkit/src/main/resources/messages/messages_de.yml @@ -0,0 +1,173 @@ +# List of global tags: +# %nl% - Goes to new line. +# %username% - Replaces the username of the player receiving the message. +# %displayname% - Replaces the nickname (and colors) of the player receiving the message. + +# Registration +registration: + disabled: '&cRegistrierungen sind deaktiviert' + name_taken: '&cDieser Benutzername ist schon vergeben' + register_request: '&3Bitte registriere dich mit "/register "' + command_usage: '&cBenutze: /register ' + reg_only: '&4Nur für registrierte Spieler! Bitte besuche https://example.com um dich zu registrieren.' + success: '&2Erfolgreich registriert!' + kicked_admin_registered: 'Ein Administrator hat dich bereits registriert; bitte logge dich erneut ein.' + +# Password errors on registration +password: + match_error: '&cPasswörter stimmen nicht überein!' + name_in_password: '&cDu kannst deinen Namen nicht als Passwort verwenden!' + unsafe_password: '&cPasswort unsicher! Bitte wähle ein anderes.' + forbidden_characters: '&4Dein Passwort enthält unerlaubte Zeichen. Zulässige Zeichen: %valid_chars' + wrong_length: '&cDein Passwort ist zu kurz oder zu lang!' + pwned_password: '&cIhr gewähltes Passwort ist nicht sicher. Es wurde bereits %pwned_count Mal verwendet! Bitte verwenden Sie ein starkes Passwort...' + +# Login +login: + command_usage: '&cBenutze: /login ' + wrong_password: '&cFalsches Passwort' + success: '&2Successful login!' + login_request: '&cBitte logge dich ein mit "/login "' + timeout_error: '&4Zeitüberschreitung beim Login' + +# Errors +error: + denied_command: '&cUm diesen Befehl zu nutzen musst du authentifiziert sein!' + denied_chat: '&cDu musst eingeloggt sein, um chatten zu können!' + unregistered_user: '&cBenutzername nicht registriert!' + not_logged_in: '&cNicht eingeloggt!' + no_permission: '&4Du hast keine Rechte, um diese Aktion auszuführen!' + unexpected_error: '&4Ein Fehler ist aufgetreten. Bitte kontaktiere einen Administrator.' + max_registration: '&cDu hast die maximale Anzahl an Accounts erreicht (%reg_count/%max_acc %reg_names).' + logged_in: '&cBereits eingeloggt!' + kick_for_vip: '&3Ein VIP-Spieler hat den vollen Server betreten!' + kick_unresolved_hostname: '&cEin Fehler ist aufgetreten: nicht auflösbarer Spieler-Hostname!' + tempban_max_logins: '&cDu bist wegen zu vielen fehlgeschlagenen Login-Versuchen temporär gebannt!' + +# AntiBot +antibot: + kick_antibot: 'AntiBotMod ist aktiviert! Bitte warte einige Minuten, bevor du dich mit dem Server verbindest.' + auto_enabled: '&4[AntiBotService] AntiBotMod wurde aufgrund hoher Netzauslastung automatisch aktiviert!' + auto_disabled: '&2[AntiBotService] AntiBotMod wurde nach %m Minuten deaktiviert, hoffentlich ist die Invasion vorbei.' + +# Unregister +unregister: + success: '&cBenutzerkonto erfolgreich gelöscht!' + command_usage: '&cBenutze: /unregister ' + +# Other messages +misc: + account_not_activated: '&cDein Account wurde noch nicht aktiviert. Bitte prüfe deine E-Mails!' + not_activated: '&cKonto nicht aktiviert, bitte registrieren und aktivieren Sie es, bevor Sie es erneut versuchen.' + password_changed: '&2Passwort geändert!' + logout: '&2Erfolgreich ausgeloggt' + reload: '&2Konfiguration und Datenbank wurden erfolgreich neu geladen.' + usage_change_password: '&cBenutze: /changepassword ' + accounts_owned_self: 'Du besitzt %count Accounts:' + accounts_owned_other: 'Der Spieler %name hat %count Accounts:' + +# Session messages +session: + valid_session: '&2Erfolgreich eingeloggt!' + invalid_session: '&cUngültige Session. Bitte starte das Spiel neu oder warte, bis die Session abgelaufen ist.' + +# Error messages when joining +on_join_validation: + same_ip_online: 'Ein Spieler mit derselben IP ist bereits online!' + same_nick_online: '&4Jemand mit diesem Namen spielt bereits auf dem Server!' + name_length: '&4Dein Nickname ist zu kurz oder zu lang.' + characters_in_name: '&4Dein Nickname enthält unerlaubte Zeichen. Zulässige Zeichen: %valid_chars' + kick_full_server: '&4Der Server ist momentan voll, Sorry!' + country_banned: '&4Dein Land ist gesperrt!' + not_owner_error: 'Du bist nicht der Besitzer dieses Accounts. Bitte wähle einen anderen Namen!' + invalid_name_case: 'Dein registrierter Benutzername ist &2%valid&f - nicht &4%invalid&f.' + quick_command: 'Du hast einen Befehl zu schnell benutzt! Bitte trete dem Server erneut bei und warte, bevor du irgendeinen Befehl nutzt.' + +# Email +email: + add_email_request: '&3Bitte hinterlege deine E-Mail-Adresse: /email add ' + usage_email_add: '&cBenutze: /email add ' + usage_email_change: '&cBenutze: /email change ' + new_email_invalid: '&cDie neue E-Mail ist ungültig!' + old_email_invalid: '&cDie alte E-Mail ist ungültig!' + invalid: '&cUngültige E-Mail!' + added: '&2E-Mail hinzugefügt!' + add_not_allowed: '&cHinzufügen einer E-Mail nicht gestattet!' + request_confirmation: '&cBitte bestätige deine E-Mail!' + changed: '&2E-Mail aktualisiert!' + change_not_allowed: '&cBearbeiten einer E-Mail nicht gestattet!' + email_show: '&2Deine aktuelle E-Mail-Adresse ist: &f%email' + no_email_for_account: '&2Du hast zur Zeit keine E-Mail-Adresse für deinen Account hinterlegt.' + already_used: '&4Diese E-Mail-Adresse wird bereits genutzt.' + incomplete_settings: 'Fehler: Es wurden nicht alle notwendigen Einstellungen vorgenommen, um E-Mails zu senden. Bitte kontaktiere einen Administrator.' + send_failure: 'Die E-Mail konnte nicht gesendet werden. Bitte kontaktiere einen Administrator.' + change_password_expired: 'Mit diesem Befehl kannst du dein Passwort nicht mehr ändern.' + email_cooldown_error: '&cEine E-Mail wurde erst kürzlich versendet. Du musst %time warten, bevor du eine neue anfordern kannst.' + +# Password recovery by email +recovery: + forgot_password_hint: '&3Passwort vergessen? Nutze "/email recovery " für ein neues Passwort' + command_usage: '&cBenutze: /email recovery ' + email_sent: '&2Wiederherstellungs-E-Mail wurde gesendet!' + code: + code_sent: 'Ein Wiederherstellungscode zum Zurücksetzen deines Passworts wurde an deine E-Mail-Adresse geschickt.' + incorrect: 'Der Wiederherstellungscode stimmt nicht! Du hast noch %count Versuche. Nutze /email recovery [email] um einen neuen zu generieren.' + tries_exceeded: 'Du hast die maximale Anzahl an Versuchen zur Eingabe des Wiederherstellungscodes überschritten. Benutze "/email recovery [email]" um einen neuen zu generieren.' + correct: 'Der eingegebene Wiederherstellungscode ist richtig!' + change_password: 'Benutze bitte den Befehl /email setpassword um dein Passwort umgehend zu ändern.' + +# Captcha +captcha: + usage_captcha: '&3Um dich einzuloggen, tippe dieses Captcha so ein: /captcha %captcha_code' + wrong_captcha: '&cFalsches Captcha, bitte nutze: /captcha %captcha_code' + valid_captcha: '&2Das Captcha ist korrekt!' + captcha_for_registration: 'Um dich zu registrieren, musst du erst ein Captcha lösen, bitte nutze den Befehl: /captcha %captcha_code' + register_captcha_valid: '&2Captcha richtig! Du kannst dich jetzt registrieren mit: /register' + +# Verification code +verification: + code_required: '&3Dieser Befehl ist sensibel und erfordert eine E-Mail-Verifizierung! Überprüfe deinen Posteingang und folge den Anweisungen der E-Mail.' + command_usage: '&cBenutze: /verification ' + incorrect_code: '&Falscher Code, bitte gib "/verification " in den Chat ein, und verwende den Code, den Du per E-Mail erhalten hast.' + success: '&2Deine Identität wurde verifiziert! Du kannst nun alle Befehle innerhalb der aktuellen Sitzung ausführen!' + already_verified: '&2Du kannst bereits jeden sensiblen Befehl innerhalb der aktuellen Sitzung ausführen!' + code_expired: '&3Dein Code ist abgelaufen! Führe einen weiteren sensiblen Befehl aus, um einen neuen Code zu erhalten!' + email_needed: '&3Um deine Identität zu überprüfen, musst Du eine E-Mail-Adresse mit deinem Konto verknüpfen!' + +# Time units +time: + second: 'Sekunde' + seconds: 'Sekunden' + minute: 'Minute' + minutes: 'Minuten' + hour: 'Stunde' + hours: 'Stunden' + day: 'Tag' + days: 'Tage' + +# Two-factor authentication +two_factor: + code_created: '&2Dein geheimer Code ist %code. Du kannst ihn hier abfragen: %url' + confirmation_required: 'Bitte bestätige deinen Code mit /2fa confirm ' + code_required: 'Bitte übermittle deinen Zwei-Faktor-Authentifizierungscode mit /2fa code ' + already_enabled: 'Die Zwei-Faktor-Authentifizierung ist für dein Konto bereits aktiviert!' + enable_error_no_code: 'Es wurde kein 2FA-Schlüssel für dich generiert oder er ist abgelaufen. Bitte nutze /2fa add' + enable_success: 'Zwei-Faktor-Authentifizierung für dein Konto erfolgreich aktiviert' + enable_error_wrong_code: 'Code falsch oder abgelaufen. Bitte nutze /2fa add' + not_enabled_error: 'Die Zwei-Faktor-Authentifizierung ist für dein Konto nicht aktiviert. Benutze /2fa add' + removed_success: 'Die Zwei-Faktor-Authentifizierung wurde erfolgreich von deinem Konto entfernt' + invalid_code: 'Ungültiger Code!' + +# 3rd party features: Bedrock Auto Login +bedrock_auto_login: + success: '&aBedrock Auto-Login erfolgreich!' + +# 3rd party features: Login Location Fix +login_location_fix: + fix_portal: '&aSie stecken während des Logins im Portal fest.' + fix_underground: '&aSie stecken während des Logins unter der Erde fest.' + cannot_fix_underground: '&aSie stecken während des Logins unter der Erde fest, aber wir können es nicht beheben.' + +# 3rd party features: Double Login Fix +double_login_fix: + fix_message: '&cSie wurden wegen doppeltem Login getrennt.' diff --git a/plugin/platform-bukkit/src/main/resources/messages/messages_en.yml b/plugin/platform-bukkit/src/main/resources/messages/messages_en.yml new file mode 100644 index 00000000..40ed034b --- /dev/null +++ b/plugin/platform-bukkit/src/main/resources/messages/messages_en.yml @@ -0,0 +1,173 @@ +# List of global tags: +# %nl% - Goes to new line. +# %username% - Replaces the username of the player receiving the message. +# %displayname% - Replaces the nickname (and colors) of the player receiving the message. + +# Registration +registration: + disabled: '&cIn-game registration is disabled!' + name_taken: '&cYou already have registered this username!' + register_request: '&3Please, register to the server with the command: /register ' + command_usage: '&cUsage: /register ' + reg_only: '&4Only registered users can join the server! Please visit https://example.com to register yourself!' + success: '&2Successfully registered!' + kicked_admin_registered: 'An admin just registered you; please log in again.' + +# Password errors on registration +password: + match_error: '&cPasswords didn''t match, check them again!' + name_in_password: '&cYou can''t use your name as password, please choose another one...' + unsafe_password: '&cThe chosen password isn''t safe, please choose another one...' + forbidden_characters: '&4Your password contains illegal characters. Allowed chars: %valid_chars' + wrong_length: '&cYour password is too short or too long! Please try with another one!' + pwned_password: '&cYour chosen password is not secure. It was used %pwned_count times already! Please use a strong password...' + +# Login +login: + command_usage: '&cUsage: /login ' + wrong_password: '&cWrong password!' + success: '&2Successful login!' + login_request: '&cPlease, login with the command: /login ' + timeout_error: '&4Login timeout exceeded, you have been kicked from the server, please try again!' + +# Errors +error: + denied_command: '&cIn order to use this command you must be authenticated!' + denied_chat: '&cIn order to chat you must be authenticated!' + unregistered_user: '&cThis user isn''t registered!' + not_logged_in: '&cYou''re not logged in!' + no_permission: '&4You don''t have the permission to perform this action!' + unexpected_error: '&4An unexpected error occurred, please contact an administrator!' + max_registration: '&cYou have exceeded the maximum number of registrations (%reg_count/%max_acc %reg_names) for your connection!' + logged_in: '&cYou''re already logged in!' + kick_for_vip: '&3A VIP player has joined the server when it was full!' + kick_unresolved_hostname: '&cAn error occurred: unresolved player hostname!' + tempban_max_logins: '&cYou have been temporarily banned for failing to log in too many times.' + +# AntiBot +antibot: + kick_antibot: 'AntiBot protection mode is enabled! You have to wait some minutes before joining the server.' + auto_enabled: '&4[AntiBotService] AntiBot enabled due to the huge number of connections!' + auto_disabled: '&2[AntiBotService] AntiBot disabled after %m minutes!' + +# Unregister +unregister: + success: '&cSuccessfully unregistered!' + command_usage: '&cUsage: /unregister ' + +# Other messages +misc: + account_not_activated: '&cYour account isn''t activated yet, please check your emails!' + not_activated: '&cAccount not activated, please register and activate it before trying again.' + password_changed: '&2Password changed successfully!' + logout: '&2Logged out successfully!' + reload: '&2Configuration and database have been reloaded correctly!' + usage_change_password: '&cUsage: /changepassword ' + accounts_owned_self: 'You own %count accounts:' + accounts_owned_other: 'The player %name has %count accounts:' + +# Session messages +session: + invalid_session: '&cYour IP has been changed and your session data has expired!' + valid_session: '&2Logged-in due to Session Reconnection.' + +# Error messages when joining +on_join_validation: + same_ip_online: 'A player with the same IP is already in game!' + same_nick_online: '&4The same username is already playing on the server!' + name_length: '&4Your username is either too short or too long!' + characters_in_name: '&4Your username contains illegal characters. Allowed chars: %valid_chars' + kick_full_server: '&4The server is full, try again later!' + country_banned: '&4Your country is banned from this server!' + 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.' + quick_command: 'You used a command too fast! Please, join the server again and wait more before using any command.' + +# Email +email: + add_email_request: '&3Please add your email to your account with the command: /email add ' + usage_email_add: '&cUsage: /email add ' + usage_email_change: '&cUsage: /email change ' + new_email_invalid: '&cInvalid new email, try again!' + old_email_invalid: '&cInvalid old email, try again!' + invalid: '&cInvalid email address, try again!' + added: '&2Email address successfully added to your account!' + add_not_allowed: '&cAdding email was not allowed.' + request_confirmation: '&cPlease confirm your email address!' + changed: '&2Email address changed correctly!' + change_not_allowed: '&cChanging email was not allowed.' + email_show: '&2Your current email address is: &f%email' + no_email_for_account: '&2You currently don''t have email address associated with this account.' + already_used: '&4The email address is already being used' + incomplete_settings: 'Error: not all required settings are set for sending emails. Please contact an admin.' + send_failure: 'The email could not be sent. Please contact an administrator.' + change_password_expired: 'You cannot change your password using this command anymore.' + email_cooldown_error: '&cAn email was already sent recently. You must wait %time before you can send a new one.' + +# Password recovery by email +recovery: + forgot_password_hint: '&3Forgot your password? Please use the command: /email recovery ' + command_usage: '&cUsage: /email recovery ' + email_sent: '&2Recovery email sent successfully! Please check your email inbox!' + code: + code_sent: 'A recovery code to reset your password has been sent to your email.' + incorrect: 'The recovery code is not correct! You have %count tries remaining.' + tries_exceeded: 'You have exceeded the maximum number attempts to enter the recovery code. Use "/email recovery [email]" to generate a new one.' + correct: 'Recovery code entered correctly!' + change_password: 'Please use the command /email setpassword to change your password immediately.' + +# Captcha +captcha: + usage_captcha: '&3To log in you have to solve a captcha code, please use the command: /captcha %captcha_code' + wrong_captcha: '&cWrong captcha, please type "/captcha %captcha_code" into the chat!' + valid_captcha: '&2Captcha code solved correctly!' + captcha_for_registration: 'To register you have to solve a captcha first, please use the command: /captcha %captcha_code' + register_captcha_valid: '&2Valid captcha! You may now register with /register' + +# Verification code +verification: + code_required: '&3This command is sensitive and requires an email verification! Check your inbox and follow the email''s instructions.' + command_usage: '&cUsage: /verification ' + incorrect_code: '&cIncorrect code, please type "/verification " into the chat, using the code you received by email' + success: '&2Your identity has been verified! You can now execute all commands within the current session!' + already_verified: '&2You can already execute every sensitive command within the current session!' + code_expired: '&3Your code has expired! Execute another sensitive command to get a new code!' + email_needed: '&3To verify your identity you need to link an email address with your account!!' + +# Time units +time: + second: 'second' + seconds: 'seconds' + minute: 'minute' + minutes: 'minutes' + hour: 'hour' + hours: 'hours' + day: 'day' + days: 'days' + +# Two-factor authentication +two_factor: + code_created: '&2Your secret code is %code. You can scan it from here %url' + confirmation_required: 'Please confirm your code with /2fa confirm ' + code_required: 'Please submit your two-factor authentication code with /2fa code ' + already_enabled: 'Two-factor authentication is already enabled for your account!' + enable_error_no_code: 'No 2fa key has been generated for you or it has expired. Please run /2fa add' + enable_success: 'Successfully enabled two-factor authentication for your account' + enable_error_wrong_code: 'Wrong code or code has expired. Please run /2fa add' + not_enabled_error: 'Two-factor authentication is not enabled for your account. Run /2fa add' + removed_success: 'Successfully removed two-factor auth from your account' + invalid_code: 'Invalid code!' + +# 3rd party features: Bedrock Auto Login +bedrock_auto_login: + success: '&aBedrock auto login success!' + +# 3rd party features: Login Location Fix +login_location_fix: + fix_portal: '&aYou are stuck in portal during login.' + fix_underground: '&aYou are stuck underground during login.' + cannot_fix_underground: '&aYou are stuck underground during login, but we cant fix it.' + +# 3rd party features: Double Login Fix +double_login_fix: + fix_message: '&cYou have been disconnected due to doubled login.' diff --git a/plugin/platform-bukkit/src/main/resources/messages/messages_eo.yml b/plugin/platform-bukkit/src/main/resources/messages/messages_eo.yml new file mode 100644 index 00000000..c8623f35 --- /dev/null +++ b/plugin/platform-bukkit/src/main/resources/messages/messages_eo.yml @@ -0,0 +1,173 @@ +# List of global tags: +# %nl% - Goes to new line. +# %username% - Replaces the username of the player receiving the message. +# %displayname% - Replaces the nickname (and colors) of the player receiving the message. + +# Registration +registration: + disabled: '&cEn-ludo registriĝo estas malebligita!' + name_taken: '&cVi jam registris tiun uzantnomon!' + register_request: '&3Bonvolu registri al la servilo per la komando: /register ' + command_usage: '&cUzado: /register ' + reg_only: '&4Nur registritaj uzantoj povas aliĝi la servilon! Bonvolu viziti https://example.com registri vin mem!' + success: '&2Sukcese registris!' + kicked_admin_registered: 'Administranto ĵus registrita vin; bonvolu ensaluti denove' + +# Password errors on registration +password: + match_error: '&cLa pasvortoj ne kongruas, kontrolu ilin!' + name_in_password: '&cVi ne povas uzi vian nomon kiel pasvorto, bonvolu elekti alian...' + unsafe_password: '&cLa elektita pasvorto estas danĝere, bonvolu elekti alian...' + forbidden_characters: '&4Via pasvorto enhavas kontraŭleĝan karakteroj. Permesita signoj: %valid_chars' + wrong_length: '&cVia pasvorto estas tro mallonga aŭ tro longa! Bonvolu provi alian pasvorton!' + pwned_password: '&cVia elektita pasvorto ne estas sekura. Ĝi estis uzita %pwned_count fojojn jam! Bonvolu uzi fortan pasvorton...' + +# Login +login: + command_usage: '&cUzado: /login ' + wrong_password: '&cErara pasvorto!' + success: '&2Sukcesa ensaluto!' + login_request: '&cBonvolu ensaluti per la komando: /login ' + timeout_error: '&4Salutnomo tempolimo superis, vi estis piedbatita el la servilo, bonvolu provi denove!' + +# Errors +error: + denied_command: '&cPor uzi ĉi komando vi devas esti legalizita!' + denied_chat: '&cPor babili vi devas esti legalizita!' + unregistered_user: '&cTiu uzanto ne estas registrita!' + not_logged_in: '&cVi ne estas ensalutita!' + no_permission: '&4Vi ne havas la permeson por fari ĉi tiun funkcion!' + unexpected_error: '&4Neatendita eraro, bonvolu kontakti administranto!' + max_registration: 'Vi superis la maksimuman nombron de enregistroj (%reg_count/%max_acc %reg_names) pro via ligo!' + logged_in: '&cVi jam estas ensalutinta!' + kick_for_vip: '&3VIP ludanto aliĝis al la servilo kiam ĝi pleniĝis!' + kick_unresolved_hostname: '&cEraro okazis: neatingebla ludanta gastiga nomo!' + tempban_max_logins: '&cVi estis portempe malpermesita por ne ensaluti tro multajn fojojn.' + +# AntiBot +antibot: + kick_antibot: 'KontraŭRoboto protekto modon estas ŝaltita! Vi devas atendi kelkajn minutojn antaŭ kunigi al la servilo.' + auto_enabled: '&4[KontraŭRobotoServo] KontraŭRoboto ŝaltita pro la grandega nombro de konektoj!' + auto_disabled: '&2[KontraŭRobotoServo] KontraŭRoboto malebligita post %m minutoj!' + +# Unregister +unregister: + success: '&cSukcese neregistritajn!' + command_usage: '&cUzado: /unregister ' + +# Other messages +misc: + account_not_activated: '&cVia konto ne aktivigis tamen, bonvolu kontroli viajn retpoŝtojn!' + not_activated: '&cKonto ne aktivigita, bonvolu registriĝi kaj aktivigi ĝin antaŭ ol provi denove.' + password_changed: '&2Pasvorto sukcese ŝanĝita!' + logout: '&2Elsalutita sukcese!' + reload: '&2Agordo kaj datumbazo estis larditaj korekte!' + usage_change_password: '&cUzado: /changepassword ' + accounts_owned_self: 'Vi posedas %count kontoj:' + accounts_owned_other: 'La ludanto %name havas %count kontojn::' + +# Session messages +session: + valid_session: '&2Ensalutantojn pro Sesio Rekonektas.' + invalid_session: '&cVia IP estis ŝanĝita kaj via seanco datumoj finiĝis!' + +# Error messages when joining +on_join_validation: + same_ip_online: 'Ludanto kun la sama IP jam estas en ludo!' + same_nick_online: '&4La sama uzantnomo estas jam ludas sur la servilo!' + name_length: '&4Via uzantnomo estas aŭ tro mallonga aŭ tro longa!' + characters_in_name: '&4Via uzantnomo enhavas kontraŭleĝan karakteroj. Permesita signoj: %valid_chars' + kick_full_server: '&4La servilo estas plena, reprovi poste!' + country_banned: '&4Via lando estas malpermesitaj de tiu servilo!' + not_owner_error: 'Vi ne estas la posedanto de tiu konto. Bonvolu elekti alian nomon!' + invalid_name_case: 'Vi devus aliĝi uzante uzantnomon %valid, ne %invalid.' + quick_command: 'Vi uzis komandon tro rapide! Bonvolu, re-aliĝi al la servilo kaj atendi pli longe antaŭ ol uzi iun ajn komandon.' + +# Email +email: + add_email_request: '&3Bonvolu aldoni vian retpoŝtan vian konton kun la komando: /email add ' + usage_email_add: '&cUzado: /email add ' + usage_email_change: '&cUzado: /email change ' + new_email_invalid: '&cNevalida nova retpoŝta, provu denove!' + old_email_invalid: '&cNevalida malnovaj retpoŝto, provu denove!' + invalid: '&cNevalida retadreso, provu denove!' + added: '&2Retpoŝtadreso sukcese aldonitaj al via konto!' + add_not_allowed: '&cAldoni retpoŝton ne estis permesita.' + request_confirmation: '&cBonvolu konfirmi vian retadreson!' + changed: '&2Retpoŝtadreso ŝanĝis ĝuste!' + change_not_allowed: '&cŜanĝi retpoŝton ne estis permesita.' + email_show: '&2Via nuna retadreso estas: &f%email' + no_email_for_account: '&2Vi aktuale ne havas retadreson asociita kun ĉi tiu konto.' + already_used: '&4La retpoŝto jam estas uzata' + incomplete_settings: 'Eraro: ne ĉiuj postulata agordoj estas metita por sendi retpoŝtojn. Bonvolu kontakti administranto.' + send_failure: 'La retpoŝto ne estis sendita. Bonvolu kontakti administranto.' + change_password_expired: 'Vi ne povas ŝanĝi vian pasvorton per tiu ĉi komando plu.' + email_cooldown_error: '&cRetmesaĝon jam sendita lastatempe. Vi devas atendi %time antaŭ vi povas sendi novan.' + +# Password recovery by email +recovery: + forgot_password_hint: '&3Ĉu vi forgesis vian pasvorton? Bonvolu uzi la komando: /email recovery ' + command_usage: '&cUzado: /email recovery ' + email_sent: '&2Reakiro retpoŝto sendita sukcese! Bonvolu kontroli vian retpoŝton enirkesto!' + code: + code_sent: 'Rekorda kodo restarigi vian pasvorton sendis al via retpoŝto.' + incorrect: 'La reakiro kodo estas ne korekti! Vi havas %count provoj restas.' + tries_exceeded: 'Vi superis la maksimuman nombron provas eniri la reakiro kodo. Uzo "/email recovery [retpoŝto]" por generi novan.' + correct: 'Reakiro kodo eniris ĝuste!' + change_password: 'Bonvolu uzi la komando /email setpassword ŝanĝi vian pasvorton tuj.' + +# Captcha +captcha: + usage_captcha: '&3Ensaluti vi devas solvi captcha kodo, bonvolu uzi la komando: /captcha %captcha_code' + wrong_captcha: '&cMalĝusta captcha, bonvolu tajpi "/captcha %captcha_code" en la babilejo!' + valid_captcha: '&2Captcha kodo solvita ĝuste!' + captcha_for_registration: 'Por registri vi devas unue solvi kapĉon, bonvolu uzi la komandon: /captcha %captcha_code' + register_captcha_valid: '&2Valida kapĉo! Vi nun povas registri per /register' + +# Verification code +verification: + code_required: '&3Tiu ĉi komando estas sentema kaj postulas retpoŝtan kontrolon! Kontrolu vian leterkeston kaj sekvu la instrukciojn en la retpoŝto.' + command_usage: '&cUzado: /verification ' + incorrect_code: '&cMalĝusta kodo, bonvolu tajpi "/verification " en la babilejo, uzante la kodon, kiun vi ricevis per retpoŝto' + success: '&2Via identeco estis kontrolita! Vi nun povas ekzekuti ĉiujn komandojn dum la aktuala sesio!' + already_verified: '&2Vi jam povas ekzekuti ĉiujn sentemajn komandojn dum la aktuala sesio!' + code_expired: '&3Via kodo eksvalidiĝis! Ekzekutu alian senteman komandon por ricevi novan kodon!' + email_needed: '&3Por kontroli vian identecon vi devas ligi retpoŝtan adreson kun via konto!' + +# Time units +time: + second: 'sekundo' + seconds: 'sekundoj' + minute: 'minuto' + minutes: 'minutoj' + hour: 'horo' + hours: 'horoj' + day: 'tago' + days: 'tagoj' + +# Two-factor authentication +two_factor: + code_created: '&2Via sekreta kodo estas %code. Vi povas skani ĝin de tie %url' + confirmation_required: 'Bonvolu konfirmi vian kodon per /2fa confirm ' + code_required: 'Bonvolu sendi vian du-faktoran aŭtentikigan kodon per /2fa code ' + already_enabled: 'Du-faktora aŭtentikigo jam estas ebligita por via konto!' + enable_error_no_code: 'Neniu 2fa ŝlosilo estis generita por vi aŭ ĝi eksvalidiĝis. Bonvolu kuri /2fa add' + enable_success: 'Sukcese ebligis du-faktoran aŭtentikigon por via konto' + enable_error_wrong_code: 'Malĝusta kodo aŭ kodo eksvalidiĝis. Bonvolu kuri /2fa add' + not_enabled_error: 'Du-faktora aŭtentikigo ne estas ebligita por via konto. Kuru /2fa add' + removed_success: 'Sukcese forigis du-faktoran aŭtentikigon de via konto' + invalid_code: 'Nevalida kodo!' + +# 3rd party features: Bedrock Auto Login +bedrock_auto_login: + success: '&aSukcesa Bedrock-aŭtomata ensaluto!' + +# 3rd party features: Login Location Fix +login_location_fix: + fix_portal: '&aVi estas blokita en portalo dum ensaluto.' + fix_underground: '&aVi estas blokita subtere dum ensaluto.' + cannot_fix_underground: '&aVi estas blokita subtere dum ensaluto, sed ni ne povas ripari ĝin.' + +# 3rd party features: Double Login Fix +double_login_fix: + fix_message: '&cVi estis malkonektita pro duobla ensaluto.' diff --git a/plugin/platform-bukkit/src/main/resources/messages/messages_es.yml b/plugin/platform-bukkit/src/main/resources/messages/messages_es.yml new file mode 100644 index 00000000..fbded288 --- /dev/null +++ b/plugin/platform-bukkit/src/main/resources/messages/messages_es.yml @@ -0,0 +1,174 @@ +# This file must be in ANSI if win, or UTF-8 if linux. +# List of global tags: +# %nl% - Goes to new line. +# %username% - Replaces the username of the player receiving the message. +# %displayname% - Replaces the nickname (and colors) of the player receiving the message. + +# Registration +registration: + disabled: '&cEl registro está desactivado' + name_taken: '&cUsuario ya registrado' + register_request: '&cPor favor, regístrate con "/register ' + command_usage: '&cUso: /register Contraseña ConfirmarContraseña' + reg_only: '&f¡Sólo para jugadores registrados! Por favor visita https://www.example.com/ para registrarte' + success: '&c¡Registrado correctamente!' + kicked_admin_registered: 'Un administrador te acaba de registrar; entra en la cuenta de nuevo' + +# Password errors on registration +password: + match_error: '&fLas contraseñas no son iguales' + name_in_password: '&cNo puedes utilizar tu nombre como contraseña. Por favor, elija otra...' + unsafe_password: '&cLa contraseña elegida no es segura, por favor elija otra...' + forbidden_characters: '&cTu contraseña tiene carácteres no admitidos, los cuales son: %valid_chars' + wrong_length: '&fTu contraseña es muy larga o muy corta' + pwned_password: '&cLa contraseña elegida no es segura. ¡Se ha usado %pwned_count veces ya! Por favor, use una contraseña fuerte...' + +# Login +login: + command_usage: '&cUso: /login contraseña' + wrong_password: '&cContraseña incorrecta' + success: '&c¡Sesión iniciada!' + login_request: '&cInicia sesión con "/login contraseña"' + timeout_error: '&fTiempo de espera para inicio de sesión excedido' + +# Errors +error: + denied_command: '&c¡Para utilizar este comando debes iniciar sesión!' + denied_chat: '&c¡Para poder hablar en el chat debes iniciar sesión!' + unregistered_user: '&cUsuario no registrado' + not_logged_in: '&c¡No has iniciado sesión!' + no_permission: '&cNo tienes permiso' + unexpected_error: '&fHa ocurrido un error. Por favor contacta al administrador.' + max_registration: '&fHas excedido la cantidad máxima de registros para tu cuenta' + logged_in: '&c¡Ya has iniciado sesión!' + kick_for_vip: '&c¡Un jugador VIP ha ingresado al servidor lleno!' + kick_unresolved_hostname: '&cSe produjo un error: nombre de host del jugador no resuelto!' + tempban_max_logins: '&cHas sido expulsado temporalmente por intentar iniciar sesión demasiadas veces.' + +# AntiBot +antibot: + kick_antibot: '¡El modo de protección AntiBot está habilitado! Tienes que esperar varios minutos antes de entrar en el servidor.' + auto_enabled: '[AuthMe] AntiBotMod activado automáticamente debido a conexiones masivas!' + auto_disabled: '[AuthMe] AntiBotMod desactivado automáticamente después de %m minutos. Esperamos que haya terminado' + +# Unregister +unregister: + success: '&c¡Cuenta eliminada del registro!' + command_usage: '&cUso: /unregister contraseña' + +# Other messages +misc: + account_not_activated: '&fTu cuenta no está activada aún, ¡revisa tu correo!' + not_activated: '&cCuenta no activada, por favor regístrese y actívela antes de intentarlo de nuevo.' + password_changed: '&c¡Contraseña cambiada!' + logout: '&cDesconectado correctamente.' + reload: '&fLa configuración y la base de datos han sido recargados' + usage_change_password: '&fUso: /changepw contraseñaActual contraseñaNueva' + accounts_owned_self: 'Eres propietario de %count cuentas:' + accounts_owned_other: 'El jugador %name tiene %count cuentas:' + +# Session messages +session: + valid_session: '&cInicio de sesión' + invalid_session: '&fLos datos de sesión no corresponden. Por favor espera a terminar la sesión.' + +# Error messages when joining +on_join_validation: + same_ip_online: '¡Un jugador con la misma IP ya está en el juego!' + same_nick_online: '&fYa hay un usuario con ese nick conectado (posible error)' + name_length: '&cTu nombre de usuario es muy largo o muy corto.' + characters_in_name: '&cTu usuario tiene carácteres no admitidos, los cuales son: %valid_chars' + kick_full_server: '&c¡El servidor está lleno, lo sentimos!' + country_banned: '¡Tu país ha sido baneado de este servidor!' + not_owner_error: 'No eres el propietario de esta cuenta. ¡Por favor, elije otro nombre!' + invalid_name_case: 'Solo puedes unirte mediante el nombre de usuario %valid, no %invalid.' + quick_command: 'Has usado el comando demasiado rápido! Porfavor, entra al servidor de nuevo y espera un poco antes de usar cualquier comando.' + +# Email +email: + add_email_request: '&cPor favor agrega tu e-mail con: /email add tuEmail confirmarEmail' + usage_email_add: '&fUso: /email add ' + usage_email_change: '&fUso: /email change ' + new_email_invalid: '[AuthMe] Nuevo email inválido!' + old_email_invalid: '[AuthMe] Email anterior inválido!' + invalid: '[AuthMe] Email inválido' + added: '[AuthMe] Email agregado !' + add_not_allowed: '&cNo se permite añadir un Email' + request_confirmation: '[AuthMe] Confirma tu Email !' + changed: '[AuthMe] Email cambiado !' + change_not_allowed: '&cNo se permite el cambio de Email' + email_show: '&2Tu dirección de E-Mail actual es: &f%email' + no_email_for_account: '&2No tienes ningun E-Mail asociado en esta cuenta.' + already_used: '&4La dirección Email ya está siendo usada' + incomplete_settings: 'Error: no todos los ajustes necesarios se han configurado para poder enviar correos. Por favor, contacta con un administrador.' + send_failure: 'No se ha podido enviar el correo electrónico. Por favor, contacta con un administrador.' + change_password_expired: 'No puedes cambiar la contraseña utilizando este comando.' + email_cooldown_error: '&cEl correo ha sido enviado recientemente. Debes esperar %time antes de volver a enviar uno nuevo.' + +# Password recovery by email +recovery: + forgot_password_hint: '&c¿Olvidaste tu contraseña? Por favor usa /email recovery ' + command_usage: '&fUso: /email recovery ' + email_sent: '[AuthMe] Correo de recuperación enviado !' + code: + code_sent: 'El código de recuperación para recuperar tu contraseña se ha enviado a tu correo.' + incorrect: '¡El código de recuperación no es correcto! Usa "/email recovery [email]" para generar uno nuevo' + tries_exceeded: 'Has excedido el número máximo de intentos para introducir el código de recuperación. Escribe "/email recovery [tuEmail]" para generar uno nuevo.' + correct: '¡Código de recuperación introducido correctamente!' + change_password: 'Por favor usa el comando "/email setpassword " para cambiar tu contraseña inmediatamente.' + +# Captcha +captcha: + usage_captcha: '&cUso: /captcha %captcha_code' + wrong_captcha: '&cCaptcha incorrecto, por favor usa: /captcha %captcha_code' + valid_captcha: '&c¡Captcha ingresado correctamente!' + captcha_for_registration: 'Para registrarse primero debes resolver el captcha, por favor usa el comando: /captcha %captcha_code' + register_captcha_valid: '&2¡Captcha validado! Ahora puedes registrarte usando /register' + +# Verification code +verification: + code_required: '&3¡Este comando es privado y requiere una verificación por correo electrónico! Revisa tu bandeja de entrada y sigue las instrucciones del correo electrónico.' + command_usage: '&cUso: /verification ' + incorrect_code: '&cCódigo incorrecto, por favor escribe "/verification " en el chat, utilizando el código que has recibido por correo electrónico.' + success: '&2Tu identidad ha sido verificada! Ahora puedes ejecutar todos los comandos dentro de la sesión actual.' + already_verified: '&2¡Ya puedes ejecutar todos los comandos de seguridad dentro de la sesión actual!' + code_expired: '&3Tu código ha expirado! Ejecuta otra vez el comando de seguridad para obtener un nuevo código!' + email_needed: '&3¡Para verificar tu identidad necesitas añadir tu correo electrónico a tu cuenta!' + +# Time units +time: + second: 'segundo' + seconds: 'segundos' + minute: 'minuto' + minutes: 'minutos' + hour: 'hora' + hours: 'horas' + day: 'día' + days: 'días' + +# Two-factor authentication +two_factor: + code_created: '&2Tu código secreto es %code. Lo puedes escanear desde aquí %url' + confirmation_required: 'Por favor, confirma tu código con /2fa confirm ' + code_required: 'Por favor, envía tu código de atenticación de dos factores con /2fa code ' + already_enabled: '¡La autenticación de dos factores ha sido habilitada para tu cuenta!' + enable_error_no_code: 'No se ha generado ninguna clave o código 2fa o ha expirado. Por favor usa /2fa add' + enable_success: 'Se ha habilitado correctamente la autenticación de dos factores para tu cuenta' + enable_error_wrong_code: 'El código es incorrecto o ha expirado. Por favor usa /2fa add' + not_enabled_error: 'La autenticación de dos factores no está habilitada para tu cuenta. Por favor usa /2fa add' + removed_success: 'Se ha eliminado correctamente la autenticación de dos factores de tu cuenta' + invalid_code: '¡Código incorrecto!' + +# 3rd party features: Bedrock Auto Login +bedrock_auto_login: + success: '&a¡Inicio de sesión automático de Bedrock exitoso!' + +# 3rd party features: Login Location Fix +login_location_fix: + fix_portal: '&aEstás atascado en el portal durante el inicio de sesión.' + fix_underground: '&aEstás atascado bajo tierra durante el inicio de sesión.' + cannot_fix_underground: '&aEstás atascado bajo tierra durante el inicio de sesión, pero no podemos solucionarlo.' + +# 3rd party features: Double Login Fix +double_login_fix: + fix_message: '&cHas sido desconectado debido a un inicio de sesión doble.' diff --git a/plugin/platform-bukkit/src/main/resources/messages/messages_et.yml b/plugin/platform-bukkit/src/main/resources/messages/messages_et.yml new file mode 100644 index 00000000..8f940ed5 --- /dev/null +++ b/plugin/platform-bukkit/src/main/resources/messages/messages_et.yml @@ -0,0 +1,173 @@ +# List of global tags: +# %nl% - Goes to new line. +# %username% - Replaces the username of the player receiving the message. +# %displayname% - Replaces the nickname (and colors) of the player receiving the message. + +# Registration +registration: + disabled: '&cMängusisene registreerimine on välja lülitatud!' + name_taken: '&cSee kasutaja on juba registreeritud!' + register_request: '&3Palun registreeri käsklusega: /register ' + command_usage: '&cKasutus: /register ' + reg_only: '&4Vaid registreeritud mängijad saavad serveriga liituda! Enda kasutaja registreerimiseks külasta https://example.com!' + success: '&2Edukalt registreeritud!' + kicked_admin_registered: 'Administraator registreeris su kasutaja, palun logi uuesti sisse.' + +# Password errors on registration +password: + match_error: '&cParoolid ei kattu, palun proovi uuesti!' + name_in_password: '&cSa ei saa oma kasutajanime paroolina kasutada, palun vali mõni teine parool.' + unsafe_password: '&cSee parool ei ole turvaline, palun vali mõni teine parool.' + forbidden_characters: '&4Sinu parool sisaldab keelatud tähemärke. Lubatud tähemärgid: %valid_chars' + wrong_length: '&cSinu parool on liiga pikk või lühike, palun vali mõni teine parool.' + pwned_password: '&cTeie valitud parool ei ole turvaline. Seda on kasutatud juba %pwned_count korda! Palun kasutage tugevat parooli...' + +# Login +login: + command_usage: '&cKasutus: /login ' + wrong_password: '&cVale parool!' + success: '&2Edukalt sisselogitud!' + login_request: '&cPalun logi sisse kasutades käsklust: /login ' + timeout_error: '&4Sisselogimiseks antud aeg on läbi ning sind on serverist välja visatud, palun proovi uuesti!' + +# Errors +error: + denied_command: '&cSelle käskluse kasutamiseks pead olema sisselogitud!' + denied_chat: '&cVestlemiseks pead olema sisselogitud!' + unregistered_user: '&cSee kasutaja ei ole registreeritud!' + not_logged_in: '&cSa ei ole sisselogitud!' + no_permission: '&4Sul puudub selle käskluse kasutamiseks luba.' + unexpected_error: '&4Esines ootamatu tõrge, palun teavita administraatorit!' + max_registration: '&cSinu IP-aadressile on registreeritud liiga palju kasutajaid! (%reg_count/%max_acc %reg_names)' + logged_in: '&cSa oled juba sisselogitud!' + kick_for_vip: '&3VIP-mängija liitus serveriga ajal, mil see oli täis!' + kick_unresolved_hostname: '&cEsines tõrge: mängija hostinimi on lahendamata!' + tempban_max_logins: '&cSind on ajutiselt serverist blokeeritud, kuna sisestasid mitu korda vale parooli.' + +# AntiBot +antibot: + kick_antibot: 'AntiBot-kaitse sisse lülitatud! Pead ootama mõne minuti enne kui serveriga liituda saad.' + auto_enabled: '&4[AntiBotTeenus] AntiBot sisselülitatud!' + auto_disabled: '&2[AntiBotTeenus] AntiBot välja lülitatud peale %m minutit!' + +# Unregister +unregister: + success: '&cKasutaja edukalt kustutatud!' + command_usage: '&cKasutus: /unregister ' + +# Other messages +misc: + account_not_activated: '&cSinu konto ei ole veel aktiveeritud, kontrolli oma meili!' + not_activated: '&cKonto ei ole aktiveeritud, palun registreerige ja aktiveerige see enne uuesti proovimist.' + password_changed: '&2Parool edukalt vahetatud!' + logout: '&2Edukalt välja logitud!' + reload: '&2Seadistused ning andmebaas on edukalt taaslaaditud!' + usage_change_password: '&cKasutus: /changepassword ' + accounts_owned_self: 'Sa omad %count kontot:' + accounts_owned_other: 'Mängijal %name on %count kontot:' + +# Session messages +session: + valid_session: '&2Sisse logitud sessiooni jätkumise tõttu.' + invalid_session: '&cSinu IP-aadress muutus, seega sinu sessioon aegus!' + +# Error messages when joining +on_join_validation: + same_ip_online: 'Sama IP-aadressiga mängija juba mängib!' + same_nick_online: '&4Sama kasutaja on juba serveriga ühendatud!' + name_length: '&4Sinu kasutajanimi on liiga pikk või liiga lühike!' + characters_in_name: '&4Sinu kasutajanimi sisaldab keelatud tähemärke. Lubatud tähemärgid: %valid_chars' + country_banned: '&4Sinu riigist ei ole võimalik sellesse serverisse ühenduda!' + not_owner_error: 'Sa ei ole selle konto omanik. Vali teine nimi!' + kick_full_server: '&4Server on täis, proovi hiljem uuesti!' + invalid_name_case: 'Sa peaksid liituma nimega %valid, mitte nimega %invalid.' + quick_command: 'Sa kasutasid käsklust liiga kiiresti! Palun liitu serveriga uuesti ning oota enne mõne käskluse kasutamist kauem.' + +# Email +email: + add_email_request: '&3Palun seo oma kasutajaga meiliaadress kasutades käsklust: /email add ' + usage_email_add: '&cKasutus: /email add ' + usage_email_change: '&cKasutus: /email change ' + new_email_invalid: '&cUus meiliaadress on sobimatu, proovi uuesti!' + old_email_invalid: '&cVana meiliaadress on sobimatu, proovi uuesti!' + invalid: '&cSobimatu meiliaadress, proovi uuesti!' + added: '&2Meiliaadress edukalt lisatud!' + add_not_allowed: '&cMeiliaadressi lisamine ei ole lubatud.' + request_confirmation: '&cPalun kinnita oma meiliaadress!' + changed: '&2Meiliaadress edukalt muudetud!' + change_not_allowed: '&cMeiliaadressi muutmine ei ole lubatud.' + email_show: '&2Sinu praegune meiliaadress on: &f%email' + no_email_for_account: '&2Selle kasutajaga ei ole seotud ühtegi meiliaadressi.' + already_used: '&4See meiliaadress on juba kasutuses!' + incomplete_settings: 'Viga: meili saatmiseks pole kõik vajalikud seaded seadistatud. Teata sellest administraatorit.' + send_failure: 'Meili ei õnnestunud saata. Teata sellest administraatorit.' + change_password_expired: 'Selle käsklusega ei saa sa enam parooli muuta.' + email_cooldown_error: '&cMeil on juba saadetud. Sa pead ootama %time enne kui saad küsida uue saatmist.' + +# Password recovery by email +recovery: + forgot_password_hint: '&3Unustasid oma parooli? Kasuta käsklust: /email recovery ' + command_usage: '&cKasutus: /email recovery ' + email_sent: '&2Konto taastamiseks vajalik meil saadetud! Vaata oma postkasti.' + code: + code_sent: 'Konto taastamise kood on saadetud!' + incorrect: 'Kood on vale! Sul on %count katset jäänud.' + tries_exceeded: 'Sul on katsed otsas. Kasuta käsklust "/email recovery [email]" uue koodi saamiseks.' + correct: 'Kood õigesti sisestatud!' + change_password: 'Palun kasuta parooli muutmiseks käsklust /email setpassword .' + +# Captcha +captcha: + usage_captcha: '&3Sisselogimiseks lahenda robotilõks käsklusega: /captcha %captcha_code' + wrong_captcha: '&cVale robotilõks, kasuta käsklust "/captcha %captcha_code"!' + valid_captcha: '&2Robotilõks lahendatud!' + captcha_for_registration: 'Registreerimiseks lahenda robotilõks kasutades käsklust: /captcha %captcha_code' + register_captcha_valid: '&2Robotilõks lahendatud! Võid nüüd registreerida kasutades käsklust /register' + +# Verification code +verification: + code_required: '&3See käsklus on ohtlik mistõttu saatsime sulle meili. Kontrolli oma meili ja järgi saadetud meili juhiseid.' + command_usage: '&cKasutus: /verification ' + incorrect_code: '&cVale kood, palun kasuta käsklust "/verification " koodiga, mille saatsime sulle meilile.' + success: '&2Sinu identiteet on kinnitatud! Sa saad nüüd praeguse sessiooni jal kasutada kõiki käsklusi.' + already_verified: '&2Sa juba saad kasutada kõiki ohtlikke käsklusi!' + code_expired: '&3Kood on aegunud! Kasuta mõnda ohtlikku käsklust, et saada uus kood!' + email_needed: '&3Konto kinnitamiseks pead siduma oma kontoga enda meiliaadressi!' + +# Time units +time: + second: 'sekund' + seconds: 'sekundit' + minute: 'minut' + minutes: 'minutit' + hour: 'tund' + hours: 'tundi' + day: 'päev' + days: 'päeva' + +# Two-factor authentication +two_factor: + code_created: '&2Sinu privaatne kood on %code. Sa saad selle skännida aadressil %url' + confirmation_required: 'Palun kinnita oma kaheastmeline autentimise kood käsklusega /2fa confirm ' + code_required: 'Palun sisesta kaheastmeline autentimise kood kasutades /2fa code ' + already_enabled: 'Kaheastmeline autentimine on juba sisselülitatud!' + enable_error_no_code: '2FA võtit ei ole genereeritud või on see aegunud. Kasuta käsklust /2fa add' + enable_success: 'Kaheastmeline autentimine edukalt sisselülitatud!' + enable_error_wrong_code: 'Vale kood või kood on aegunud. Kasuta käsklust /2fa add' + not_enabled_error: 'Kaheastmeline autentimine ei ole su kontol sisse lülitatud. Kasuta käsklust /2fa add' + removed_success: 'Sinu kontolt on edukalt eemaldatud kaheastmeline autentimine.' + invalid_code: 'Vale kood!' + +# 3rd party features: Bedrock Auto Login +bedrock_auto_login: + success: '&aBedrocki automaatne sisselogimine õnnestus!' + +# 3rd party features: Login Location Fix +login_location_fix: + fix_portal: '&aOlete sisselogimise ajal portaalis kinni.' + fix_underground: '&aOlete sisselogimise ajal maa all kinni.' + cannot_fix_underground: '&aOlete sisselogimise ajal maa all kinni, kuid me ei saa seda parandada.' + +# 3rd party features: Double Login Fix +double_login_fix: + fix_message: '&cTeid on ühendatud lahti kahekordse sisselogimise tõttu.' diff --git a/plugin/platform-bukkit/src/main/resources/messages/messages_eu.yml b/plugin/platform-bukkit/src/main/resources/messages/messages_eu.yml new file mode 100644 index 00000000..8d581be6 --- /dev/null +++ b/plugin/platform-bukkit/src/main/resources/messages/messages_eu.yml @@ -0,0 +1,173 @@ +# List of global tags: +# %nl% - Goes to new line. +# %username% - Replaces the username of the player receiving the message. +# %displayname% - Replaces the nickname (and colors) of the player receiving the message. + +# Registration +registration: + disabled: '&cIzen ematea itxita dago!' + name_taken: '&cDagoeneko badago erabiltzaile izen hori duen norbait.' + register_request: '&cMesedez erabili "/register " izena emateko' + command_usage: '&cErabilera: /register ' + reg_only: '&fIzena eman duten jokalariak bakarrik sartu daitezke! Mesedez, eman izena https://example.com webgunean' + success: '&cEskerrik asko izena emateagatik!' + kicked_admin_registered: 'Administratzaile batek izena eman dizu, mesedez irten eta sartu zerbitzaritik.' + +# Password errors on registration +password: + match_error: '&fBi pasahitzak ez datoz bat' + name_in_password: '&cPasahitza eta erabiltzaile izena ezin dira berdinak izan. Mesedez, aukeratu beste pasahitz bat...' + unsafe_password: '&cAukeratutako pasahitza ez da segurua.Mesedez, aukeratu beste bat...' + forbidden_characters: '&4Pasahitzak ondorengo karaktereak bakarrik izan ditzake: %valid_chars' + wrong_length: '&fZure pasahitza motzegia edo luzeegia da' + pwned_password: '&cAukeratutako pasahitza ez da segurua. %pwned_count aldiz erabili da dagoeneko! Mesedez, erabili pasahitz sendo bat...' + +# Login +login: + command_usage: '&cErabilera: /login pasahitza' + wrong_password: '&cPasahitz okerra' + success: '&cSaioa hasi duzu!' + login_request: '&cMesedez erabili "/login pasahitza" saioa hasteko' + timeout_error: '&fDenbora gehiegi egon zara saioa hasi gabe.' + +# Errors +error: + denied_command: '&cHori egiteko ezinbestekoa da saioa hastea!' + denied_chat: '&cTxatean hitz egiteko ezinbestekoa da saioa hastea!' + unregistered_user: '&cErabiltzaileak ez du izena eman!' + not_logged_in: '&cEz duzu saioa hasi!' + no_permission: '&cEz daukazu baimenik' + unexpected_error: '&fUstekabeko errore bat gertatu da. Mesedez jarri harremanetan administratzaile batekin.' + max_registration: '&fKonexioko gehienezko erabiltzaile kopurua gainditu duzu (%reg_count/%max_acc %reg_names)' + logged_in: '&cDagoeneko saioa hasita duzu!' + kick_for_vip: '&cVIP erabiltzaile bati lekua egiteko kanporatua izan zara!' + kick_unresolved_hostname: '&cErrore bat geratu da: ezin izan da erabiltzailearen ostalari izena lortu!' + tempban_max_logins: '&cDenbora baterako kanporatua izan zara, pasahitza behin baino gehiagotan gaizki sartzeagatik.' + +# AntiBot +antibot: + kick_antibot: 'AntiBot babesa aktibatuta dago! Minutu batzuk itxaron beharko dituzu berriro sartu aurretik.' + auto_enabled: '&4[AntiBotZerbitzua] AntiBot babesa aktibatu da konexio kopurua handia delako!' + auto_disabled: '&2[AntiBotZerbitzua] AntiBot babesa desaktibatu da %m minutuz martxan egon ondoren!' + +# Unregister +unregister: + success: '&cZure kontua ezabatu duzu!' + command_usage: '&cErabilera: /unregister ' + +# Other messages +misc: + account_not_activated: '&fZure kontua aktibatu gabe dago. Mesedez, berretsi zure posta elektronikoa!' + not_activated: '&cKontua ez dago aktibatuta, mesedez erregistratu eta aktibatu berriro saiatu aurretik.' + password_changed: '&cPasahitza ondo aldatu duzu!' + logout: '&cSaioa itxi duzu' + reload: '&fEzarpenak eta datu-basea berrabiarazi dira' + usage_change_password: '&fErabilera: /changepassword ' + accounts_owned_self: '%count kontu dituzu:' + accounts_owned_other: '%name erabiltzaileak %count kontu ditu:' + +# Session messages +session: + valid_session: '&cSesioa mantendu eta beraz ez daukazu saioa hasi beharrik.' + invalid_session: '&cZure IP helbidea aldatu da, eta horregatik zure sesioa iraungi da!' + +# Error messages when joining +on_join_validation: + same_ip_online: 'IP helbide bera duen beste erabiltzaile bat dago zerbitzarian!' + same_nick_online: '&fErabiltzaile izen bera duen erabailtzaile bat dago zerbitzarian' + name_length: '&cZure erabiltzaile izena motzegia edo luzeegia da' + characters_in_name: '&cZure erabiltzaileak debekatutako karaktereak ditu. Ondorengoak bakarrik onartzen ditugu: %valid_chars' + kick_full_server: '&cZerbitzaria beteta dago. Sentitzen dugu!' + country_banned: 'Zure herrialdetik ezin zara zerbitzari honetan sartu' + not_owner_error: 'Kontu hau ez da zurea! Erabili beste bat!' + invalid_name_case: '%valid erabiltzailea erabili beharko duzu, ez %invalid.' + quick_command: 'Komandoa azkarregi exekutatu duzu! Mesdez, sartu zerbitzarian berriro eta itxaron pixka bat ezer idatzi aurretik.' + +# Email +email: + add_email_request: '&cMesedez, sartu zure posta elektroniko helbidea : /email add ' + usage_email_add: '&fErabilera: /email add ' + usage_email_change: '&fErabilera: /email change ' + new_email_invalid: 'Helbide berria okerra da!' + old_email_invalid: 'Lehengo helbidea ez dator bat!' + invalid: 'Helbide okerra' + added: 'Helbidea ondo sartu duzu' + add_not_allowed: '&cEzin duzu posta helbiderik sartu' + request_confirmation: 'Berretsi zure posta helbidea!' + changed: 'Posta helbidea ondo aldatu duzu' + change_not_allowed: '&cEzin duzu posta helbidea aldatu' + email_show: '&2Zure posta helbidea ondorengoa da: &f%email' + no_email_for_account: '&2Ez daukazu posta helbiderik kontu honetara lotuta.' + already_used: '&4Posta helbide hori dagoeneko badarabil norbaitek' + incomplete_settings: 'Errorea: posta elektronikoak bidaltzeko ezparpen guztiak ez daude konfiguratu. Mesedez, jarri harremanetan administratzaile batekin.' + send_failure: 'Ezin izan da mezua bidali. Mesedez, jarri harremanetan administratzaile batekin.' + change_password_expired: 'Pasahitza aldatzeko denbora iraungi da.' + email_cooldown_error: '&cMezu bat bidali dizugu duela denbora gutxik. %time itxaron behar duzu berriro bidaltzeko eskatu aurretik.' + +# Password recovery by email +recovery: + forgot_password_hint: '&cPasahitza ahaztu duzu? Berreskuratzeko erabili /email recovery ' + command_usage: '&fErabili: /email recovery ' + email_sent: 'Berreskuratze mezua bidali dizugu. Mesedez begiratu sarrera ontzia!' + code: + code_sent: 'Pasahitza berreskuratzeko kode bat bidali dizugu zure posta elektronikora.' + incorrect: 'Berreskuratze kodea ez da zuzena! %count saiakera dituzu.' + tries_exceeded: 'Berreskuratze kodea sartzeko gehienezko saiakera kopurua gainditu duzu. Erabili "/email recovery [helbidea]" berri bat jasotzeko.' + correct: 'Kode zuzena!' + change_password: 'Mesedez, erabili "/email setpassword pasahitza aldatzeko.' + +# Captcha +captcha: + usage_captcha: '&3Sartzeko captcha asmatu beharko duzu. Mesedez erabaili /captcha %captcha_code' + wrong_captcha: '&cCaptacha okerra, mesedez, idatzi "/captcha %captcha_code" txatean!' + valid_captcha: '&2Captcha asmatu duzu!' + captcha_for_registration: 'Izena emateko captcha asmatu beharko duzu, mesedez erabili ondorengo komandoa: /captcha %captcha_code' + register_captcha_valid: '&2Captcha zuzena! Orain izena eman dezakezu /register erabilita' + +# Verification code +verification: + code_required: '&3Komando hori babestuta dago, eta horregatik eposta berrespena beharrazkoa da! Begiratu zure sarrera ontzia, eta jarraitu bertan adierazitako pausoak.' + command_usage: '&cErabilera: /verification ' + incorrect_code: '&cKode okerra, mesedez idatzi "/verification " txatean, postan jaso duzun kodea erabilita' + success: '&2Zure identitatea berretsi duzu! Orain komando babestuak erabiltzeko baimena daukazu sesioak irauten duen bitartean!' + already_verified: '&2Dagoeneko badaukazu komando babestuak erabiltzeko baimena sesio honek irauten duen bitartean!' + code_expired: '&3Kodea iraungi egin da! Exekutatu beste komando babestu bat kode berri bat lortzeko!' + email_needed: '&3Zure identitatea berresteko beharrezkoa da zure kontuan posta elektronikoa konfiguratuta izatea!!' + +# Time units +time: + second: 'segundu' + seconds: 'segundu' + minute: 'minutu' + minutes: 'minutu' + hour: 'ordu' + hours: 'ordu' + day: 'egun' + days: 'egun' + +# Two-factor authentication +two_factor: + code_created: '&2Zure kode sekretua %code da. Hemen eskaneatu dezakezu: %url' + confirmation_required: 'Mesedez, berretsi kodea ondorengoa erabilita: /2fa confirm ' + code_required: 'Mesedez, bidali zure bi faktoreko autentikazio kodea ondorengoa erabilita: /2fa code ' + already_enabled: 'Dagoeneko badaukazu bi faktoreko autentikazioa aktibatuta!' + enable_error_no_code: 'Ez duzu 2fa koderik sortu, edo iraungita dago. Mesedez, erabili /2fa add' + enable_success: '2 faktoreko autentikazioa aktibatu duzu' + enable_error_wrong_code: 'Kode okerra, edo iraungita dago. Mesedez erabili /2fa add' + not_enabled_error: 'Ez duzu 2 faktore autentikazioa konfiguratu. Erabaili /2fa add' + removed_success: '2 faktoreko autentikazioa desaktibatu duzu' + invalid_code: 'Kode okerra!' + +# 3rd party features: Bedrock Auto Login +bedrock_auto_login: + success: '&aBedrock automatikoki saioa hasi da!' + +# 3rd party features: Login Location Fix +login_location_fix: + fix_portal: '&aPortal batean trabatuta zaude saioa hasten ari zarela.' + fix_underground: '&aLur azpian trabatuta zaude saioa hasten ari zarela.' + cannot_fix_underground: '&aLur azpian trabatuta zaude saioa hasten ari zarela, baina ezin dugu konpondu.' + +# 3rd party features: Double Login Fix +double_login_fix: + fix_message: '&cSistematik kanporatu zaitugu saio bikoitza egiteagatik.' diff --git a/plugin/platform-bukkit/src/main/resources/messages/messages_fi.yml b/plugin/platform-bukkit/src/main/resources/messages/messages_fi.yml new file mode 100644 index 00000000..8d349f77 --- /dev/null +++ b/plugin/platform-bukkit/src/main/resources/messages/messages_fi.yml @@ -0,0 +1,173 @@ +# List of global tags: +# %nl% - Goes to new line. +# %username% - Replaces the username of the player receiving the message. +# %displayname% - Replaces the nickname (and colors) of the player receiving the message. + +# Registration +registration: + disabled: '&cRekisteröinti on suljettu!' + name_taken: '&cPelaaja on jo rekisteröity' + register_request: '&cRekisteröidy palvelimellemme komennolla "/register "' + command_usage: '&cKäyttötapa: /register ' + reg_only: '&fMene sivustolle: https://example.com rekisteröityäksesi!' + success: '&cRekisteröidyit onnistuneesti!' + kicked_admin_registered: 'Ylläpitäjä rekisteröi sinut juuri; kirjaudu sisään uudelleen.' + +# Password errors on registration +password: + match_error: '&fSalasanat ei täsmää' + name_in_password: '&cEt voi käyttää nimeäsi salasanana, valitse toinen...' + unsafe_password: '&cValitsemasi salasana ei ole turvallinen, valitse toinen...' + forbidden_characters: '&4Salasanassasi on kiellettyjä merkkejä. Sallitut merkit: %valid_chars' + wrong_length: '&fSalasanasi on liian pitkä tai lyhyt.' + pwned_password: '&cValitsemasi salasana ei ole turvallinen. Sitä on käytetty %pwned_count kertaa jo! Käytä vahvaa salasanaa...' + +# Login +login: + command_usage: '&cKäyttötapa: /login salasana' + wrong_password: '&cVäärä salasana' + success: '&cKirjauduit onnistuneesti' + login_request: '&cKirjaudu palvelimmelle komennolla "/login salasana"' + timeout_error: '&fKirjautumisaika meni umpeen.' + +# Errors +error: + denied_command: '&cSinun on oltava todennettu käyttääksesi tätä komentoa!' + denied_chat: '&cSinun on oltava todennettu voidaksesi chattailla!' + unregistered_user: '&cSalasanat eivät täsmää' + not_logged_in: '&cEt ole kirjautunut sisään!' + no_permission: '&cEi oikeuksia' + unexpected_error: '&fVirhe: Ota yhteys palveluntarjoojaan!' + max_registration: '&fSinulla ei ole oikeuksia tehdä enempää pelaajatilejä!' + logged_in: '&cOlet jo kirjautunut!' + kick_for_vip: '&cVIP pelaaja liittyi täyteen palvelimeen!' + kick_unresolved_hostname: '&cTapahtui virhe: pelaajan verkkonimiä ei voitu ratkaista!' + tempban_max_logins: '&cSinut on tilapäisesti kielletty liian monen kirjautumisen epäonnistumisen vuoksi.' + +# AntiBot +antibot: + kick_antibot: 'Antibot-suojatila on käytössä! Sinun on odotettava muutama minuutti ennen kuin voit liittyä palvelimelle.' + auto_enabled: '&4[AntiBotService] Antibot otettu käyttöön suuren yhteyksien määrän vuoksi!' + auto_disabled: '&2[AntiBotService] Antibot poistettu käytöstä %m minuutin kuluttua!' + +# Unregister +unregister: + success: '&cPelaajatili poistettu onnistuneesti!' + command_usage: '&cKäyttötapa: /unregister password' + +# Other messages +misc: + account_not_activated: '&fKäyttäjäsi ei ole vahvistettu!' + not_activated: '&cTiliä ei ole aktivoitu, rekisteröidy ja aktivoi se ennen kuin yrität uudelleen.' + password_changed: '&cSalasana vaihdettu!!' + logout: '&cKirjauduit ulos palvelimelta.' + reload: '&fAsetukset uudelleenladattu' + usage_change_password: '&fKäyttötapa: /changepassword vanhaSalasana uusiSalasana' + accounts_owned_self: 'Omistat %count tiliä:' + accounts_owned_other: 'Pelaajalla %name on %count tiliä:' + +# Session messages +session: + valid_session: '&cIstunto jatkettu!' + invalid_session: '&fIstunto ei täsmää! Ole hyvä ja odota istunnon loppuun' + +# Error messages when joining +on_join_validation: + same_ip_online: 'Sama IP-osoitteella oleva pelaaja on jo pelissä!' + same_nick_online: '&COlet jo palvelimella! &COdota käyttäjän aikakatkaisua tai ota yhteyttä palveluntarjoojaan.' + name_length: '&cPelaajanimesi on liian lyhyt tai pitkä' + characters_in_name: '&cPelaajanimesi sisältää luvattomia merkkejä. Hyväksytyt merkit: %valid_chars' + kick_full_server: '&cPalvelin on täynnä, Yritä pian uudelleen!' + country_banned: '&4Maasi on estetty tästä palvelimesta!' + not_owner_error: 'Et ole tämän tilin omistaja. Valitse toinen nimi!' + invalid_name_case: 'Sinun pitäisi liittyä käyttäen käyttäjänimeä %valid, ei %invalid.' + quick_command: 'Käytit komentoa liian nopeasti! Kirjaudu sisään uudelleen ja odota enemmän ennen minkään komennon käyttöä.' + +# Email +email: + add_email_request: '&cLisää sähköpostisi: /email add sähköpostisi sähköpostisiUudelleen' + usage_email_add: '&fKäyttötapa: /email add ' + usage_email_change: '&fKäyttötapa: /email change ' + new_email_invalid: '[AuthMe] Uusi sähköposti on väärä!' + old_email_invalid: '[AuthMe] Vanha sähköposti on väärä!' + invalid: '[AuthMe] Väärä sähköposti' + added: '[AuthMe] Sähköposti lisätty!' + add_not_allowed: '&cSähköpostin lisääminen ei ollut sallittua.' + request_confirmation: '[AuthMe] Vahvistuta sähköposti!' + changed: '[AuthMe] Sähköposti vaihdettu!' + change_not_allowed: '&cSähköpostin muuttaminen ei ollut sallittua.' + email_show: '&2Nykyinen sähköpostiosoitteesi on: &f%email' + no_email_for_account: '&2Sinulla ei tällä hetkellä ole liitettyä sähköpostiosoitetta tähän tiliin.' + already_used: '&4Sähköpostiosoite on jo käytössä' + incomplete_settings: 'Virhe: kaikki tarvittavat asetukset eivät ole asetettu sähköpostien lähettämistä varten. Ota yhteyttä ylläpitäjään.' + send_failure: 'Sähköpostia ei voitu lähettää. Ota yhteyttä järjestelmänvalvojaan.' + change_password_expired: 'Et voi enää vaihtaa salasanaa tällä komennolla.' + email_cooldown_error: '&cSähköpostia on jo lähetetty äskettäin. Sinun on odotettava %time ennen kuin voit lähettää uuden.' + +# Password recovery by email +recovery: + forgot_password_hint: '&cUnohtuiko salasana? Käytä komentoa: /email recovery ' + command_usage: '&fKäyttötapa: /email recovery ' + email_sent: '[AuthMe] Palautus sähköposti lähetetty!' + code: + code_sent: 'Salasanasi palauttamiseksi lähetetty palautuskoodi on lähetetty sähköpostiisi.' + incorrect: 'Palautuskoodi ei ole oikea! Sinulla on %count yritystä jäljellä.' + tries_exceeded: 'Olet ylittänyt enimmäisyritysten määrän palautuskoodin syöttämisessä. Käytä "/email recovery [email]" luodaksesi uuden.' + correct: 'Palautuskoodi syötetty oikein!' + change_password: 'Käytä komentoa /email setpassword vaihtaaksesi salasanasi välittömästi.' + +# Captcha +captcha: + usage_captcha: '&cKäyttötapa: /captcha %captcha_code' + wrong_captcha: '&cVäärä varmistus, käytä : /captcha %captcha_code' + valid_captcha: '&cSinun varmistus onnistui.!' + captcha_for_registration: 'Rekisteröityäksesi sinun on ensin ratkaistava captcha, käytä komentoa: /captcha %captcha_code' + register_captcha_valid: '&2Validi captcha! Voit nyt rekisteröityä käyttäen komentoa /register' + +# Verification code +verification: + code_required: '&3Tämä komento on herkkä ja vaatii sähköpostivahvistuksen! Tarkista sähköpostisi ja seuraa sähköpostin ohjeita.' + command_usage: '&cKäyttö: /verification ' + incorrect_code: '&cVirheellinen koodi, kirjoita "/verification " chatiin, käyttäen koodia, jonka sait sähköpostitse' + success: '&2Henkilöllisyytesi on varmennettu! Voit nyt suorittaa kaikki komennot tämän istunnon aikana!' + already_verified: '&2Voit jo suorittaa jokaisen herkän komennon tämän istunnon aikana!' + code_expired: '&3Koodisi on vanhentunut! Suorita toinen herkkä komento saadaksesi uuden koodin!' + email_needed: '&3Vahvistaaksesi henkilöllisyytesi sinun on liitettävä sähköpostiosoite tilillesi!!' + +# Time units +time: + second: 'sekunti' + seconds: 'sekuntia' + minute: 'minuutti' + minutes: 'minuuttia' + hour: 'tunti' + hours: 'tuntia' + day: 'päivä' + days: 'päivää' + +# Two-factor authentication +two_factor: + code_created: '&2Salainen koodisi on %code. Voit skannata sen täältä %url' + confirmation_required: 'Vahvista koodisi komennolla /2fa confirm ' + code_required: 'Lähetä kaksivaiheisen todennuksen koodisi komennolla /2fa code ' + already_enabled: 'Kaksivaiheinen todennus on jo käytössä tililläsi!' + enable_error_no_code: 'Sinulle ei ole luotu kaksivaiheisen todennuksen avainta tai se on vanhentunut. Suorita komento /2fa add' + enable_success: 'Kaksivaiheinen todennus onnistuneesti käytössä tililläsi' + enable_error_wrong_code: 'Väärä koodi tai koodi on vanhentunut. Suorita komento /2fa add' + not_enabled_error: 'Kaksivaiheista todennusta ei ole käytössä tililläsi. Suorita komento /2fa add' + removed_success: 'Kaksivaiheinen todennus poistettu tililtäsi onnistuneesti' + invalid_code: 'Virheellinen koodi!' + +# 3rd party features: Bedrock Auto Login +bedrock_auto_login: + success: '&aBedrock-automaattinen sisäänkirjautuminen onnistui!' + +# 3rd party features: Login Location Fix +login_location_fix: + fix_portal: '&aOlet jumissa portaalissa sisäänkirjautumisen aikana.' + fix_underground: '&aOlet jumissa maan alla sisäänkirjautumisen aikana.' + cannot_fix_underground: '&aOlet jumissa maan alla sisäänkirjautumisen aikana, mutta emme voi korjata sitä.' + +# 3rd party features: Double Login Fix +double_login_fix: + fix_message: '&cSinut on katkaistu kaksoiskirjautumisen vuoksi.' diff --git a/plugin/platform-bukkit/src/main/resources/messages/messages_fr.yml b/plugin/platform-bukkit/src/main/resources/messages/messages_fr.yml new file mode 100644 index 00000000..7fbf2e0a --- /dev/null +++ b/plugin/platform-bukkit/src/main/resources/messages/messages_fr.yml @@ -0,0 +1,176 @@ +# Traduction par: André & Twonox + +# Pour afficher une apostrophe, vous devez en mettre deux consécutivement (ex: «J''ai» au lieu de «J'ai») +# Liste des tags globaux: +# %nl% - Permet de passer à la ligne. +# %username% - Affiche le pseudo du joueur recevant le message. +# %displayname% - Affiche le pseudo, avec couleurs, du joueur recevant le message. + +# Enregistrement +registration: + disabled: '&cL''inscription est désactivée.' + name_taken: '&cUtilisateur déjà inscrit.' + register_request: '&cPour vous inscrire, utilisez "/register "' + command_usage: '&cUsage: /register ' + reg_only: 'Seul les joueurs enregistrés sont admis!%nl%Veuillez vous rendre sur https://www.example.com pour plus d''infos.' + success: '&aInscription effectuée !' + kicked_admin_registered: 'Un admin vient de vous inscrire, veuillez vous reconnecter.' + +# Erreur de mot de passe lors de l'enregistrement +password: + match_error: '&cLe mot de passe de confirmation ne correspond pas.' + name_in_password: '&cVous ne pouvez pas utiliser votre pseudo comme mot de passe.' + unsafe_password: '&cCe mot de passe n''est pas accepté, choisissez-en un autre.' + forbidden_characters: '&cVotre mot de passe contient des caractères non autorisés. Caractères permis : %valid_chars' + wrong_length: '&cVotre mot de passe est trop court ou trop long !' + pwned_password: '&cLe mot de passe choisi n''est pas sécurisé. Il a déjà été utilisé %pwned_count fois ! Veuillez utiliser un mot de passe fort...' + +# Identification +login: + command_usage: '&cUsage: /login ' + wrong_password: '&cMauvais mot de passe !' + success: '&aIdentification effectuée !' + login_request: '&cPour vous identifier, utilisez "/login "' + timeout_error: 'Vous avez été expulsé car vous êtes trop lent pour vous enregistrer/identifier !' + +# Erreurs +error: + denied_command: '&cVous devez être connecté pour pouvoir utiliser cette commande.' + denied_chat: '&cVous devez être connecté pour pouvoir écrire dans le chat.' + unregistered_user: '&cUtilisateur non-inscrit.' + not_logged_in: '&cUtilisateur non connecté !' + no_permission: '&cVous n''êtes pas autorisé à utiliser cette commande.' + unexpected_error: '&cUne erreur est apparue, veuillez contacter un administrateur.' + max_registration: 'Vous avez atteint la limite d''inscription !%nl%&cVous avez %reg_count sur %max_acc : %reg_names' + logged_in: '&aVous êtes déjà connecté.' + kick_for_vip: 'Un joueur VIP a rejoint le serveur à votre place (serveur plein).' + kick_unresolved_hostname: '&cUne erreur est apparue : nom d''hôte non identifié !' + tempban_max_logins: '&cVous êtes temporairement banni suite à plusieurs échecs de connexions !' + +# AntiBot +antibot: + kick_antibot: 'L''AntiBot est activé, veuillez attendre %m minutes avant de joindre le serveur.' + auto_enabled: 'L''AntiBot a été activé automatiquement à cause de nombreuses connexions !' + auto_disabled: 'L''AntiBot a été désactivé automatiquement après %m minutes, espérons que l''invasion se soit arrêtée !' + +# Dé-enregistrement +unregister: + success: '&aCompte supprimé !' + command_usage: '&cPour supprimer votre compte, utilisez "/unregister "' + +# Autres messages +misc: + account_not_activated: '&fCe compte n''est pas actif, consultez vos mails !' + not_activated: '&cCompte non activé, veuillez vous inscrire et l''activer avant de réessayer.' + password_changed: '&aMot de passe changé avec succès !' + logout: '&cVous avez été déconnecté !' + reload: '&aAuthMe a été relancé avec succès.' + usage_change_password: '&cPour changer de mot de passe, utilisez "/changepassword "' + accounts_owned_self: 'Vous avez %count comptes:' + accounts_owned_other: 'Le joueur %name a %count comptes:' + +# Session +session: + valid_session: '&aVous avez été automatiquement connecté !' + invalid_session: 'Session expirée suite à un changement d''IP.' + +# Erreurs lors d'une tentative connexion +on_join_validation: + same_ip_online: 'Un joueur avec la même adresse IP joue déjà !' + same_nick_online: 'Un joueur ayant le même pseudo est déjà connecté.' + name_length: 'Votre pseudo est trop long ou trop court.' + characters_in_name: 'Caractères de pseudo autorisés: %valid_chars' + kick_full_server: '&cLe serveur est actuellement plein, désolé !' + country_banned: 'Votre pays est banni de ce serveur.' + not_owner_error: 'Vous n''êtes pas le propriétaire de ce compte. Veuillez utiliser un autre pseudo !' + invalid_name_case: 'Veuillez vous connecter avec "%valid" et non pas avec "%invalid".' + quick_command: '&cUtilisation trop rapide de commande! Veuillez vous reconnecter et attendre un peu avant d''exécuter une commande.' + +# Email +email: + add_email_request: '&cLiez votre email à votre compte en faisant "/email add "' + usage_email_add: '&fUsage: /email add ' + usage_email_change: '&fUsage: /email change ' + new_email_invalid: '&cNouvel email invalide !' + old_email_invalid: '&cAncien email invalide !' + invalid: '&cL''email inscrit est invalide !' + added: '&aEmail enregistré. En cas de perte de MDP, faites "/email recover "' + add_not_allowed: '&cVous n''êtes pas autorisé à ajouter une adresse mail.' + request_confirmation: '&cLa confirmation de l''email est manquante ou éronnée.' + changed: '&aVotre email a été mis à jour.' + change_not_allowed: '&cVous n''êtes pas autorisé à modifier l''adresse mail.' + email_show: '&fL''email enregistré pour votre compte est: %email' + no_email_for_account: '&c&oVous n''avez aucun email enregistré sur votre compte.' + already_used: '&cCet email est déjà utilisé !' + incomplete_settings: '&cErreur : Tous les paramètres requis ne sont pas présent pour l''envoi de mail, veuillez contacter un admin.' + send_failure: '&cLe mail n''a pas pu être envoyé. Veuillez contacter un admin.' + change_password_expired: 'Vous ne pouvez pas changer votre mot de passe avec cette commande.' + email_cooldown_error: '&cUn mail de récupération a déjà été envoyé récemment. Veuillez attendre %time pour le demander de nouveau.' + +# Récupération de mot de passe par mail +recovery: + forgot_password_hint: '&cVous avez oublié votre Mot de Passe? Utilisez "/email recovery "' + command_usage: '&fUsage: /email recovery ' + email_sent: '&aMail de récupération envoyé !' + code: + code_sent: 'Un code de récupération a été envoyé à votre email afin de réinitialiser votre mot de passe.' + incorrect: '&cLe code de réinitialisation est incorrect! Il vous reste %count% essai(s).' + tries_exceeded: 'Vous avez atteint le nombre maximum d''essais pour rentrer le code.%nl%Refaites "/email recovery " pour en régénérer à nouveau.' + correct: '&aCode de réinitialisation correct !' + change_password: 'Veuillez faire "/email setpassword " pour changer votre mot de passe directement.' + +# Captcha +captcha: + usage_captcha: '&cTrop de tentatives de connexion ont échoué, faites: /captcha %captcha_code' + wrong_captcha: '&cCaptcha incorrect, écrivez de nouveau: /captcha %captcha_code' + valid_captcha: '&aCaptcha validé! Vous pouvez maintenant réessayer de vous connecter.' + captcha_for_registration: 'Avant de vous inscrire, veuillez rentrer un captcha en faisant "/captcha %captcha_code"' + register_captcha_valid: '&aCaptcha validé! Vous pouvez maintenant vous inscrire.' + +# Code de vérification +verification: + code_required: '&cCette commande est sensible, elle nécessite donc une confirmation par email.%nl%&cVeuillez suivre les instructions qui viennent de vous être envoyées par email.' + command_usage: '&cUsage: /verification ' + incorrect_code: '&cCode incorrect !%nl%&cVeuillez taper "/verification " dans le chat en utilisant le code reçu par mail.' + success: '&aVotre identité a bien été vérifiée !%nl%&aVous pouvez désormais utiliser la commande souhaitée durant toute la session.' + already_verified: '&aVous êtes déjà autorisé à utiliser les commandes sensibles durant votre session actuelle.' + code_expired: '&cVotre code d''identification a expiré !%nl%&cVeuillez re-exécuter une commande sensible pour recevoir un nouveau code.' + email_needed: '&cAfin de vérifier votre identité, vous devez avoir un email lié à votre compte.%nl%&cPour cela, faites "/email add "' + +# Unités de temps +time: + second: 'seconde' + seconds: 'secondes' + minute: 'minute' + minutes: 'minutes' + hour: 'heure' + hours: 'heures' + day: 'jour' + days: 'jours' + +# Identification à deux facteurs +two_factor: + code_created: '&aVotre code secret est &2%code&a. Vous pouvez le scanner depuis &2%url' + confirmation_required: 'Veuillez confirmer votre code secret en écrivant "/2fa confirm "' + code_required: 'Veuillez indiquer votre code secret en écrivant "/2fa code "' + already_enabled: '&aL''authentification à double facteur est déjà active !' + enable_error_no_code: '&cAucun code secret n''a été généré ou bien celui-ci a expiré. Veuillez écrire "/2fa add"' + enable_success: '&aL''authentification à double facteur a été activé pour votre compte !' + enable_error_wrong_code: '&cLe code secret est faux ou bien celui-ci a expiré. Veuillez écrire "/2fa add"' + not_enabled_error: '&cL''authentification à double facteur n''est pas active sur votre compte. Faites "/2fa add" pour l''activer.' + removed_success: '&cL''authentification à double facteur a été désactivé pour votre compte !' + invalid_code: '&cCode secret invalide !' + +# 3rd party features: Bedrock Auto Login +bedrock_auto_login: + success: '&aConnexion automatique Bedrock réussie !' + +# 3rd party features: Login Location Fix +login_location_fix: + fix_portal: '&aVous êtes bloqué dans un portail lors de la connexion.' + fix_underground: '&aVous êtes bloqué sous terre lors de la connexion.' + cannot_fix_underground: '&aVous êtes bloqué sous terre lors de la connexion, mais nous ne pouvons pas le corriger.' + +# 3rd party features: Double Login Fix +double_login_fix: + fix_message: '&cVous avez été déconnecté en raison d''une double connexion.' diff --git a/plugin/platform-bukkit/src/main/resources/messages/messages_gl.yml b/plugin/platform-bukkit/src/main/resources/messages/messages_gl.yml new file mode 100644 index 00000000..8a7a7f30 --- /dev/null +++ b/plugin/platform-bukkit/src/main/resources/messages/messages_gl.yml @@ -0,0 +1,172 @@ +# List of global tags: +# %nl% - Goes to new line. +# %username% - Replaces the username of the player receiving the message. +# %displayname% - Replaces the nickname (and colors) of the player receiving the message. + +# Registration +registration: + disabled: '&cO rexistro está deshabilitado' + name_taken: '&cEse nome de usuario xa está rexistrado' + register_request: '&cPor favor, rexístrate con "/register "' + command_usage: '&cUso: /register contrasinal confirmarContrasinal' + reg_only: '&fSó xogadores rexistrados! Por favor, visita https://example.com para rexistrarte' + success: '&cRexistrado con éxito!' + kicked_admin_registered: 'Un administrador acaba de rexistrarte; por favor, volve a iniciar sesión.' + +# Password errors on registration +password: + match_error: '&fO contrasinal non coincide' + name_in_password: '&cNon podes usar o teu nome como contrasinal, por favor, escolle outro...' + unsafe_password: '&cO contrasinal escollido non é seguro, por favor, escolle outro...' + forbidden_characters: '&4O teu contrasinal contén caracteres non permitidos. Caracteres permitidos: %valid_chars' + wrong_length: '&fO teu contrasinal non alcanza a lonxitude mínima ou excede a lonxitude máxima' + pwned_password: '&cO contrasinal elixido non é seguro. Foi usado %pwned_count veces xa! Por favor, use un contrasinal forte...' + +# Login +login: + command_usage: '&cUso: /login ' + wrong_password: '&cContrasinal equivocado' + success: '&cIdentificación con éxito!' + login_request: '&cPor favor, identifícate con "/login "' + timeout_error: '&fRematou o tempo da autentificación' + +# Errors +error: + denied_command: '&cPara usar este comando debes estar autenticado!' + denied_chat: '&cPara chatear debes estar autenticado!' + unregistered_user: '&cEse nome de usuario non está rexistrado' + not_logged_in: '&cNon te identificaches!' + no_permission: '&cNon tes o permiso' + unexpected_error: '&fOcurriu un erro; contacta cun administrador' + max_registration: '&fExcediches o máximo de rexistros para a túa Conta' + logged_in: '&cXa estás identificado!' + kick_for_vip: '&cUn xogador VIP uniuse ao servidor cheo!' + kick_unresolved_hostname: '&cProduciuse un erro: nome do xogador non resolto!' + tempban_max_logins: '&cEstás temporalmente expulsado por fallar ao acceder en demasiadas ocasións.' + +# AntiBot +antibot: + kick_antibot: 'O modo de protección AntiBot está activado! Tes que agardar uns minutos antes de unirte ao servidor.' + auto_enabled: '[AuthMe] AntiBotMod conectouse automáticamente debido a conexións masivas!' + auto_disabled: '[AuthMe] AntiBotMod desactivouse automáticamente despois de %m minutos, esperemos que a invasión se detivera' + +# Unregister +unregister: + success: '&cFeito! Xa non estás rexistrado!' + command_usage: '&cUso: /unregister ' + +# Other messages +misc: + account_not_activated: '&fA túa conta aínda non está activada, comproba a túa bandexa de correo!!' + not_activated: '&cConta non activada, por favor rexístrese e actívea antes de tentalo de novo.' + password_changed: '&cCambiouse o contrasinal!' + logout: '&cSesión pechada con éxito' + reload: '&fRecargáronse a configuración e a base de datos' + usage_change_password: '&fUso: /changepassword ' + accounts_owned_self: 'Tes %count contas:' + accounts_owned_other: 'O xogador %name ten %count contas:' + +# Session messages +session: + valid_session: '&cIdentificado mediante a sesión' + invalid_session: '&fOs datos de sesión non corresponden, por favor, espere a que remate a sesión' + +# Error messages when joining +on_join_validation: + same_ip_online: 'Un xogador coa mesma IP xa está en xogo!' + same_nick_online: '&fXa está xogando alguén co mesmo nome' + name_length: '&cO teu nome é demasiado curto ou demasiado longo' + characters_in_name: '&cO teu nome contén caracteres ilegais. Caracteres permitidos: %valid_chars' + kick_full_server: '&cO servidor está actualmente cheo, sentímolo!' + country_banned: '&4O teu país está prohibido neste servidor!' + not_owner_error: 'Non es o dono desta conta. Por favor, escolle outro nome!' + invalid_name_case: 'Deberías unirte usando o nome de usuario %valid, non %invalid.' + quick_command: 'Usaches un comando moi rápido! Por favor, úneste ao servidor de novo e agarda máis antes de usar calquera comando.' + +# Email +email: + add_email_request: '&cPor favor, engade o teu correo electrónico con: /email add ' + usage_email_add: '&fUso: /email add ' + usage_email_change: '&fUso: /email change ' + new_email_invalid: '[AuthMe] O novo correo non é válido!' + old_email_invalid: '[AuthMe] O correo vello non é válido!' + invalid: '[AuthMe] Correo non válido' + added: '[AuthMe] Correo engadido!' + add_not_allowed: '&cEngadir o correo electrónico non estaba permitido.' + request_confirmation: '[AuthMe] Confirma o teu correo!' + changed: '[AuthMe] Cambiouse o correo!' + email_show: '&2O teu enderezo de correo electrónico actual é: &f%email' + no_email_for_account: '&2Actualmente non tes ningún enderezo de correo electrónico asociado con esta conta.' + already_used: '&4O enderezo de correo electrónico xa está a ser usado' + incomplete_settings: 'Erro: non todos os axustes necesarios están configurados para enviar correos electrónicos. Por favor, contacta cun administrador.' + send_failure: 'O correo electrónico non puido ser enviado. Por favor, contacta cun administrador.' + change_password_expired: 'Non podes cambiar o teu contrasinal usando este comando máis.' + email_cooldown_error: '&cUn correo electrónico xa foi enviado recentemente. Debes agardar %time antes de poder enviar un novo.' + +# Password recovery by email +recovery: + forgot_password_hint: '&cOlvidaches o contrasinal? Por favor, usa /email recovery ' + command_usage: '&fUso: /email recovery ' + email_sent: '[AuthMe] Enviouse o correo de confirmación!' + code: + code_sent: 'Enviouse un código de recuperación para restablecer o teu contrasinal ao teu correo electrónico.' + incorrect: 'O código de recuperación non é correcto! Tes %count intentos restantes.' + tries_exceeded: 'Excedeches o número máximo de intentos para ingresar o código de recuperación. Usa "/email recovery [email]" para xerar un novo.' + correct: 'O código de recuperación foi ingresado correctamente!' + change_password: 'Usa o comando /email setpassword para cambiar o teu contrasinal inmediatamente.' + +# Captcha +captcha: + usage_captcha: '&cNecesitas escribir un captcha, por favor escribe: /captcha %captcha_code' + wrong_captcha: '&cCaptcha equivocado, por favor usa: /captcha %captcha_code' + valid_captcha: '&cO teu captcha é válido !' + captcha_for_registration: 'Para rexistrarte debes resolver un captcha primeiro, por favor, usa o comando: /captcha %captcha_code' + register_captcha_valid: '&2Captcha válido! Agora podes rexistrarte con /register' + +# Verification code +verification: + code_required: '&3Este comando é sensible e require unha verificación por correo electrónico! Comproba a túa bandeixa de entrada e segue as instrucións do correo electrónico.' + command_usage: '&cUso: /verification ' + incorrect_code: '&cCódigo incorrecto, por favor, escribe "/verification " no chat, usando o código que recibiches por correo electrónico' + success: '&2A túa identidade foi verificada! Agora podes executar todos os comandos dentro da sesión actual!' + already_verified: '&2Xa podes executar todos os comandos sensibles dentro da sesión actual!' + code_expired: '&3O teu código caducou! Executa outro comando sensible para obter un novo código!' + email_needed: '&3Para verificar a túa identidade debes vincular un enderezo de correo electrónico á túa conta!!' + +# Time units +time: + second: 'segundo' + seconds: 'segundos' + minute: 'minuto' + minutes: 'minutos' + hour: 'hora' + hours: 'horas' + day: 'día' + days: 'días' + +# Two-factor authentication +two_factor: + code_created: '&2O teu código secreto é %code. Podes escanealo desde aquí %url' + confirmation_required: 'Por favor, confirma o teu código con /2fa confirm ' + code_required: 'Por favor, envía o teu código de autenticación en dous pasos con /2fa code ' + already_enabled: 'A autenticación en dous pasos xa está activada para a túa conta!' + enable_error_no_code: 'Non se xerou ningún clave 2fa para ti ou caducou. Por favor, executa /2fa add' + enable_success: 'Autenticación en dous pasos activada con éxito para a túa conta' + enable_error_wrong_code: 'Código incorrecto ou caducado. Por favor, executa /2fa add' + not_enabled_error: 'A autenticación en dous pasos non está activada para a túa conta. Executa /2fa add' + removed_success: 'Autenticación en dous pasos eliminada con éxito da túa conta' + invalid_code: 'Código incorrecto!' + +# 3rd party features: Bedrock Auto Login +bedrock_auto_login: + success: '&aInicio de sesión automático de Bedrock exitoso!' + +# 3rd party features: Login Location Fix +login_location_fix: + fix_portal: '&aEstá atrapado no portal durante o inicio de sesión.' + fix_underground: '&aEstá atrapado baixo terra durante o inicio de sesión.' + cannot_fix_underground: '&aEstá atrapado baixo terra durante o inicio de sesión, pero non podemos arranxalo.' + +# 3rd party features: Double Login Fix +double_login_fix: + fix_message: '&cFoi desconectado debido a un inicio de sesión dobre.' diff --git a/plugin/platform-bukkit/src/main/resources/messages/messages_hu.yml b/plugin/platform-bukkit/src/main/resources/messages/messages_hu.yml new file mode 100644 index 00000000..215a0660 --- /dev/null +++ b/plugin/platform-bukkit/src/main/resources/messages/messages_hu.yml @@ -0,0 +1,173 @@ +# List of global tags: +# %nl% - Goes to new line. +# %username% - Replaces the username of the player receiving the message. +# %displayname% - Replaces the nickname (and colors) of the player receiving the message. + +# Registration +registration: + disabled: '&cA regisztráció letiltva!' + name_taken: '&cEz a játékosnév már regisztrálva van!' + register_request: '&cKérlek, regisztrálj be a következő paranccsal: "&7/register &c".' + command_usage: '&cHasználat: "&7/register &c".' + reg_only: '&4Csak a regisztrált játékosok tudnak csatlakozni a szerverhez!' + success: '&aA regisztráció sikeres!' + kicked_admin_registered: 'Adminisztrátor által regisztrálva lettél. Kérlek, lépj be újra a szerverbe!' + +# Password errors on registration +password: + match_error: '&cA két jelszó nem egyezik!' + name_in_password: '&cNem használhatod a felhasználóneved jelszónak, kérlek, válassz másikat...' + unsafe_password: '&cA választott jelszó nem biztonságos, kérlek, válassz másikat...' + forbidden_characters: '&4A választott jelszó nem engedélyezett karaktereket tartalmaz. Engedélyezett karakterek: %valid_chars' + wrong_length: 'A jelszavad nem éri el a minimális hosszúságot!' + pwned_password: '&cA választott jelszavad nem biztonságos. Már %pwned_count alkalommal használták! Kérlek használj erős jelszót...' + +# Login +login: + command_usage: '&cBejelentkezés: "&7/login &c".' + wrong_password: '&4A jelszó helytelen!' + success: '&aSikeresen beléptél!' + login_request: '&cKérlek, jelentkezz be: "&7/login &c"!' + timeout_error: 'Bejelentkezési időtúllépés!' + +# Errors +error: + denied_command: '&cAmíg nem vagy bejelentkezve, nem használhatod ezt a parancsot!' + denied_chat: '&cAmíg nem vagy bejelentkezve, nem használhatod a csevegőt!' + unregistered_user: '&cEz a felhasználó nincs regisztrálva!' + not_logged_in: '&cNem vagy bejelentkezve!' + no_permission: '&cNincs jogosultságod a használatára!' + unexpected_error: '&cHiba lépett fel! Lépj kapcsolatba a szerver tulajával!' + max_registration: '&cElérted a maximálisan beregisztrálható karakterek számát. (%reg_count/%max_acc %reg_names)' + logged_in: '&cMár be vagy jelentkezve!' + kick_for_vip: '&3VIP játékos csatlakozott a szerverhez!' + kick_unresolved_hostname: '&cHiba történt: feloldatlan játékos hosztnév!' + tempban_max_logins: '&cIdeiglenesen ki lettél tiltva, mert túl sok alkalommal rontottad el a jelszavad!' + +# AntiBot +antibot: + kick_antibot: 'Az AntiBot védelem bekapcsolva! Kérlek, várj pár percet mielőtt csatlakozol.' + auto_enabled: '&4[AntiBot] Az AntiBot védelem bekapcsolt, mert a megszabott időn belül több felhasználó csatlakozott!' + auto_disabled: '&2[AntiBot] Az AntiBot kikapcsol %m perc múlva!' + +# Unregister +unregister: + success: '&cA regisztráció sikeresen törölve!' + command_usage: '&cHasználat: "&7/unregister &c"' + +# Other messages +misc: + account_not_activated: '&cA felhasználód aktiválása még nem történt meg, ellenőrizd a megadott emailed!' + not_activated: '&cA fiók nincs aktiválva, kérlek regisztrálj és aktiváld azt mielőtt újra megpróbálkoznál.' + password_changed: '&cA jelszó sikeresen megváltoztatva!' + logout: '&cSikeresen kijelentkeztél!' + reload: 'Beállítások és az adatbázis újratöltve!' + usage_change_password: 'Használat: "/changepassword <új jelszó>".' + accounts_owned_self: '%count db regisztrációd van:' + accounts_owned_other: 'A %name nevű játékosnak, %count db regisztrációja van:' + +# Session messages +session: + valid_session: '&2A megadott időkereten belül csatlakoztál vissza, így a rendszer automatikusan beléptetett.' + invalid_session: '&cAz IP címed megváltozott, ezért a visszacsatlakozási időkereted lejárt.' + +# Error messages when joining +on_join_validation: + same_ip_online: 'Már valaki csatlakozott a szerverhez ezzel az IP címmel!' + same_nick_online: 'Ezzel a játékosnévvel már játszanak a szerveren.' + name_length: '&4A felhasználóneved túl hosszú, vagy túl rövid! Kérlek, válassz másikat!' + characters_in_name: '&4A felhasználóneved nem engedélyezett karaktereket tartalmaz. Engedélyezett karakterek: %valid_chars' + kick_full_server: '&4A szerver megtelt, próbálj csatlakozni később!' + country_banned: '&4Az országod a tiltólistán van ezen a szerveren!' + 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.' + quick_command: 'Túl gyorsan használtad a parancsot! Kérjük, csatlakoz a szerverhez, és várj, amíg bármilyen parancsot használsz.' + +# Email +email: + add_email_request: '&3Kérlek, rendeld hozzá a felhasználódhoz az email címedet "&7/email add &3".' + usage_email_add: '&cHasználat: "&7/email add &c".' + usage_email_change: '&cHasználat: "&7/email change <új email>&c".' + 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!' + invalid: '&cHibás az email cím, próbáld újra!' + added: '&2Az email címed rögzítése sikeresen megtörtént!' + add_not_allowed: '&cAz email hozzáadása nem engedélyezett' + request_confirmation: '&cKérlek, ellenőrízd az email címedet!' + changed: '&2Az email cím cseréje sikeresen megtörtént!' + change_not_allowed: '&cAz emailek módosítása nem engedélyezett' + email_show: '&2A jelenlegi emailed a következő: &f%email' + no_email_for_account: '&2Ehhez a felhasználóhoz jelenleg még nincs email hozzárendelve.' + already_used: '&4Ez az email cím már használatban van!' + incomplete_settings: 'Hiba: nem lett beállítva az összes szükséges beállítás az email küldéshez. Vedd fel a kapcsolatot egy adminnal.' + send_failure: 'Nem sikerült elküldeni az emailt. Lépj kapcsolatba egy adminnal.' + change_password_expired: 'Ezzel a paranccsal már nem módosíthatja jelszavát.' + email_cooldown_error: '&cEgy emailt már kiküldtünk. Következő email küldése előtt várnod kell: %time.' + +# Password recovery by email +recovery: + forgot_password_hint: '&3Ha elfelejtetted a jelszavad, használd az "&7/email recovery &3".' + command_usage: '&cHasználat: "&7/email recovery &c".' + email_sent: '&2A jelszó visszaállításhoz szükséges emailt elküldtük! Ellenőrizd a leveleidet!' + code: + code_sent: 'A jelszavad visszaállításához szükséges kódot sikeresen elküldtük az email címedre!' + incorrect: 'A visszaállító kód helytelen volt! Még %count lehetőséged maradt.' + tries_exceeded: 'Elérted a próbálkozások maximális számát. Használd a következő parancsot: "/email recovery [email címed]" egy új generálásához.' + correct: 'A visszaállító kód helyes!' + change_password: 'Használd a következő parancsot: "/email setpassword <új jelszó>", hogy azonnal megváltoztasd a jelszavad.' + +# Captcha +captcha: + usage_captcha: '&3A bejelentkezéshez CAPTCHA szükséges, kérlek, használd a következő parancsot: "&7/captcha %captcha_code&3".' + wrong_captcha: '&cHibás CAPTCHA, kérlek, írd be a következő parancsot: "&7/captcha %captcha_code&c"!' + valid_captcha: '&2A CAPTCHA sikeresen feloldva!' + captcha_for_registration: 'A regisztrációhoz meg kell oldanod a captcha-t, kérjük, használd a parancsot: /captcha %captcha_code' + register_captcha_valid: '&2Érvényes captcha! Most regisztrálhatsz a /register paranccsal.' + +# Verification code +verification: + code_required: '&3Ez a parancs érzékeny, és emailes igazolást igényel! Ellenőrizze a bejövő postafiókot, és kövesse az email utasításait.' + command_usage: '&cHasználat: /verification ' + incorrect_code: '&cHelytelen kód, írd be "/verification " be a chatbe, az emailben kapott kód használatával' + success: '&2Az Ön személyazonosságát ellenőrizték! Mostantól végrehajthatja az összes parancsot az aktuális munkamenetben!' + already_verified: '&2Már minden érzékeny parancsot végrehajthat az aktuális munkameneten belül!' + code_expired: '&3A kód lejárt! Végezzen el egy másik érzékeny parancsot, hogy új kódot kapjon!' + email_needed: '&3A személyazonosságának igazolásához email címet kell csatolnia fiókjához!' + +# Time units +time: + second: 'másodperc' + seconds: 'másodperc' + minute: 'perc' + minutes: 'perc' + hour: 'óra' + hours: 'óra' + day: 'nap' + days: 'nap' + +# Two-factor authentication +two_factor: + code_created: '&2A titkos kódod a következő: %code. Vagy skenneld be a következő oldalról: %url' + confirmation_required: 'Kérjük, erősítsd meg a kódot /2fa confirm ' + code_required: 'Kérjük, küldd el a kétütemű hitelesítési kódot /2fa code ' + already_enabled: 'Kétszámjegyű hitelesítés már engedélyezve van a fiókodban!' + enable_error_no_code: 'Nem hoztad létre a 2fa kulcsot számodra, vagy lejárt. Kérlek, futtasd a /2fa add' + enable_success: 'Sikeresen engedélyezted a fiók kétütemű hitelesítését' + enable_error_wrong_code: 'Hibás kód vagy a kód lejárt. Futtasd a /2fa add' + not_enabled_error: 'Kétszámjegyű hitelesítés nincs engedélyezve a fiókodban. Futtasd a /2fa add' + removed_success: 'Sikeresen eltávolítottad a fiók két számjegyű hitelesítőjét' + invalid_code: 'Érvénytelen kód!' + +# 3rd party features: Bedrock Auto Login +bedrock_auto_login: + success: '&aBedrock automatikus bejelentkezés sikeres!' + +# 3rd party features: Login Location Fix +login_location_fix: + fix_portal: '&aBeragadtál a portálban a bejelentkezés közben.' + fix_underground: '&aBeragadtál a föld alatt a bejelentkezés közben.' + cannot_fix_underground: '&aBeragadtál a föld alatt a bejelentkezés közben, de nem tudjuk megjavítani.' + +# 3rd party features: Double Login Fix +double_login_fix: + fix_message: '&cKét bejelentkezés miatt lecsatlakoztattak.' diff --git a/plugin/platform-bukkit/src/main/resources/messages/messages_id.yml b/plugin/platform-bukkit/src/main/resources/messages/messages_id.yml new file mode 100644 index 00000000..bf46218f --- /dev/null +++ b/plugin/platform-bukkit/src/main/resources/messages/messages_id.yml @@ -0,0 +1,172 @@ +# List of global tags: +# %nl% - Goes to new line. +# %username% - Replaces the username of the player receiving the message. +# %displayname% - Replaces the nickname (and colors) of the player receiving the message. + +# Registration +registration: + disabled: '&cRegister dalam game tidak diaktifkan!' + name_taken: '&cKamu telah mendaftarkan username ini!' + register_request: '&3Silahkan mendaftar ke server menggunakan perintah "/register "' + command_usage: '&cPenggunaan: /register ' + reg_only: '&4Hanya pengguna terdaftar yang bisa bergabung! Silahkan kunjungi https://example.com untuk mendaftar!' + success: '&2Register berhasil!' + kicked_admin_registered: 'Administrator sudah meregistrasi kamu; dimohon untuk login kembali' + +# Password errors on registration +password: + match_error: '&cPassword tidak cocok, silahkan periksa dan ulangi kembali!' + name_in_password: '&cKamu tidak bisa menggunakan namamu sebagai password, silahkan coba yang lain...' + unsafe_password: '&cPassword yang kamu pilih tidak aman, silahkan coba yang lain...' + forbidden_characters: '&4Password kamu memiliki karakter illegal. Karakter yang diperbolehkan: %valid_chars' + wrong_length: '&cPassword kamu terlalu panjang/pendek! Silahkan pilih yang lain!' + +# Login +login: + command_usage: '&cPenggunaan: /login ' + wrong_password: '&cPassword salah!' + success: '&2Login berhasil!' + login_request: '&cSilahkan login menggunakan perintah "/login "' + timeout_error: '&4Jangka waktu login telah habis, kamu dikeluarkan dari server. Silahkan coba lagi!' + +# Errors +error: + denied_command: '&cUntuk menggunakan perintah ini, kamu harus terautentikasi!' + denied_chat: '&cUntuk mengobrol, kamu harus terautentikasi!' + unregistered_user: '&cUser ini belum terdaftar!' + not_logged_in: '&cKamu belum login!' + no_permission: '&4Kamu tidak mempunyai izin melakukan ini!' + unexpected_error: '&4Terjadi kesalahan tak dikenal, silahkan hubungi Administrator!' + max_registration: '&Kamu telah mencapai batas maksimum pendaftaran di server ini!' + logged_in: '&cKamu telah login!' + kick_for_vip: '&3Player VIP mencoba masuk pada saat server sedang penuh!' + kick_unresolved_hostname: '&cTerjadi kesalahan: nama host pemain tidak dapat dipecahkan!' + tempban_max_logins: '&cKamu untuk sementara diblokir karena terlalu sering salah saat login.' + +# AntiBot +antibot: + kick_antibot: 'Proteksi AntiBot diaktifkan! Kamu harus menunggu beberapa menit sebelum masuk server.' + auto_enabled: '&4[AntiBotService] AntiBot diaktifkan dikarenakan banyak koneksi yg diterima!' + auto_disabled: '&2[AntiBotService] AntiBot dimatikan setelah %m menit!' + +# Unregister +unregister: + success: '&cUnregister berhasil!' + command_usage: '&cPenggunaan: /unregister ' + +# Other messages +misc: + account_not_activated: '&cAkunmu belum diaktifkan, silahkan periksa email kamu!' + not_activated: '&cAkun belum diaktifkan, silakan daftar dan aktivasi sebelum mencoba lagi.' + password_changed: '&2Berhasil mengubah password!' + logout: '&2Berhasil logout!' + reload: '&2Konfigurasi dan database telah dimuat ulang!' + usage_change_password: '&cPenggunaan: /changepassword ' + accounts_owned_self: 'Kamu memiliki %count akun:' + accounts_owned_other: 'Pengguna akun %name memiliki %count akun:' + +# Session messages +session: + valid_session: '&2Otomatis login, karena sesi masih terhubung.' + invalid_session: '&cIP kamu telah berubah, dan sesi kamu telah berakhir!' + +# Error messages when joining +on_join_validation: + same_ip_online: 'Seorang pemain dengan IP yang sama sudah didalam permainan!' + same_nick_online: '&4Username yg sama telah bermain di server ini!' + name_length: '&4Username kamu terlalu panjang atau terlalu pendek!' + characters_in_name: '&4Username kamu mengandung karakter illegal. Karakter yg diizinkan: %valid_chars' + kick_full_server: '&4Server sedang penuh, silahkan coba lagi nanti!' + country_banned: '&4Negara kamu diblokir dari server ini!' + not_owner_error: 'Kamu bukan pemilik dari akun ini. Tolong pilih nama lain!' + invalid_name_case: 'Kamu seharusnya masuk menggunakan username %valid, bukan %invalid.' + quick_command: 'Kamu menggunakan perintah terlalu cepat! Tolong masuk kembali ke server dan tunggu sebentar sebelum menggunakan perintah lagi.' + +# Email +email: + add_email_request: '&3Silahkan tambahkan email ke akunmu menggunakan perintah "/email add "' + usage_email_add: '&cPenggunaan: /email add ' + usage_email_change: '&cPenggunaan: /email change ' + new_email_invalid: '&cEmail baru tidak valid, coba lagi!' + old_email_invalid: '&cEmail lama tidak valid, coba lagi!' + invalid: '&cAlamat email tidak valid, coba lagi!' + added: '&2Berhasil menambahkan alamat email ke akunmu!' + add_not_allowed: '&cMenambah email tidak diperbolehkan' + request_confirmation: '&cSilahkan konfirmasi alamat email kamu!' + changed: '&2Alamat email telah diubah dengan benar!' + change_not_allowed: '&cMengubah email tidak diperbolehkan' + email_show: '&2Emailmu saat ini: &f%email' + no_email_for_account: '&2Kamu tidak memiliki alamat email terkait dengan akun ini.' + already_used: '&4Alamat email sudah terpakai' + incomplete_settings: 'Error: tidak semua syarat setelan disetel untuk mengirim email. Tolong kontak Administrator.' + send_failure: 'Email tidak dapat dikirim. Tolong kontak Administrator.' + change_password_expired: 'Kamu tidak dapat mengubah password menggunakan perintah ini lagi.' + email_cooldown_error: '&cSebuah email telah dikirim beberapa waktu lalu. Kamu harus menunggu %time sebelum kamu mengirim yang baru.' + +# Password recovery by email +recovery: + forgot_password_hint: '&3Lupa password? silahkan gunakan command "/email recovery "' + command_usage: '&cPenggunaan: /email recovery ' + email_sent: '&2Email pemulihan akun telah dikirim! Silahkan periksa kotak masuk emailmu!' + code: + code_sent: 'Sebuah kode pemulihan untuk mengatur ulang passwordmu telah terkirim ke emailmu.' + incorrect: 'Kode pemulihan salah! Kamu memiliki %count kali kesempatan tersisa.' + tries_exceeded: 'Kamu telah mencapai batas maksimal untuk memasukkan kode pemulihan. Gunakan "/email recovery [email]" membuat yang baru.' + correct: 'Kode pemulihan berhasil dimasukkan!' + change_password: 'Tolong gunakan perintah /email setpassword untuk mengubah passwordmu segera.' + +# Captcha +captcha: + usage_captcha: '&3Kamu harus menyelesaikan kode captcha untuk login, silahkan gunakan perintah "/captcha %captcha_code"' + wrong_captcha: '&cCaptcha salah, gunakan perintah "/captcha %captcha_code" pada komentar!' + valid_captcha: '&2Kode captcha terselesaikan!' + captcha_for_registration: 'Untuk meregistrasi, kamu harus menyelesaikan captcha terlebih dahulu, tolong gunakan perintah: /captcha %captcha_code' + register_captcha_valid: '&2Captcha terselesaikan! Kini kamu dapat mendaftar dengan /register' + +# Verification code +verification: + code_required: '&3Perintah ini sensitif dan memerlukan verifikasi email! Periksa kotak masuk Anda dan ikuti petunjuk email.' + command_usage: '&cPenggunaan: /verification ' + incorrect_code: '&cKode salah, ketik "/verification " di chat, menggunakan kode yang Anda terima melalui email' + success: '&2Identitas Anda telah diverifikasi! Anda sekarang dapat menjalankan semua perintah dalam sesi saat ini!' + already_verified: '&2Anda sudah dapat menjalankan setiap perintah sensitif dalam sesi saat ini!' + code_expired: '&3Kode Anda telah kedaluwarsa! Jalankan perintah sensitif lain untuk mendapatkan kode baru!' + email_needed: '&3Untuk memverifikasi identitas Anda, Anda perlu menyambungkan alamat email dengan akun Anda!!' + +# Time units +time: + second: 'detik' + seconds: 'detik' + minute: 'menit' + minutes: 'menit' + hour: 'jam' + hours: 'jam' + day: 'hari' + days: 'hari' + +# Two-factor authentication +two_factor: + code_created: '&2Kode rahasiamu adalah %code. Kamu dapat memindainya di %url' + confirmation_required: 'Tolong konfirmasi kodemu dengan /2fa confirm ' + code_required: 'Mohon kirimkan kode dari autentikasi dua langkah dengan /2fa code ' + already_enabled: 'Autentikasi dua langkah sudah diaktifkan untuk akunmu!' + enable_error_no_code: 'Tidak ada kunci autentikasi dua langkah atau sudah kadaluarsa. Tolong jalankan /2fa add' + enable_success: 'Sukses mengaktifkan autentikasi dua langkah untuk akunmu' + enable_error_wrong_code: 'Kode salah atau kode sudah kadaluarsa. Tolong jalankan /2fa add' + not_enabled_error: 'Autentikasi dua langkah tidak diaktifkan untuk akunmu. Jalankan /2fa add' + removed_success: 'Sukses menghapus autentikasi dua langkah dari akunmu' + invalid_code: 'Kode tidak valid!' + +# 3rd party features: Bedrock Auto Login +bedrock_auto_login: + success: '&aLogin otomatis Bedrock berhasil!' + +# 3rd party features: Login Location Fix +login_location_fix: + fix_portal: '&aAnda terjebak di portal selama proses login.' + fix_underground: '&aAnda terjebak di bawah tanah selama proses login.' + cannot_fix_underground: '&aAnda terjebak di bawah tanah selama proses login, tetapi kami tidak dapat memperbaikinya.' + +# 3rd party features: Double Login Fix +double_login_fix: + fix_message: '&cAnda telah terputus karena login ganda.' diff --git a/plugin/platform-bukkit/src/main/resources/messages/messages_it.yml b/plugin/platform-bukkit/src/main/resources/messages/messages_it.yml new file mode 100644 index 00000000..53e8afa5 --- /dev/null +++ b/plugin/platform-bukkit/src/main/resources/messages/messages_it.yml @@ -0,0 +1,174 @@ +# Lingua Italiana creata da Maxetto. +# Tag globali disponibili: +# %nl% - Vai a capo. +# %username% - Sostituisce il nome del giocatore che riceve il messaggio. +# %displayname% - Sostituisce il nickname (e i colori) del giocatore che riceve il messaggio. + +# Registration +registration: + disabled: '&cLa registrazione tramite i comandi di gioco è disabilitata.' + name_taken: '&cHai già eseguito la registrazione!' + register_request: '&3Per favore, esegui la registrazione con il comando: /register ' + command_usage: '&cUtilizzo: /register ' + reg_only: '&4Puoi giocare in questo server solo dopo aver eseguito la registrazione attraverso il sito web! Per favore, vai su https://esempio.it per procedere!' + success: '&2Registrato correttamente!' + kicked_admin_registered: 'Un amministratore ti ha appena registrato, per favore rientra nel server' + +# Password errors on registration +password: + match_error: '&cLe password non corrispondono!' + name_in_password: '&cNon puoi usare il tuo nome utente come password, per favore scegline un''altra...' + unsafe_password: '&cLa password che hai inserito non è sicura, per favore scegline un''altra...' + forbidden_characters: '&4La tua password contiene caratteri non consentiti. I caratteri consentiti sono: %valid_chars' + wrong_length: '&cLa password che hai inserito è troppo corta o troppo lunga, per favore scegline un''altra...' + pwned_password: '&cLa password scelta non è sicura. È stata già utilizzata %pwned_count volte! Si prega di utilizzare una password sicura...' + +# Login +login: + command_usage: '&cUtilizzo: /login ' + wrong_password: '&cPassword non corretta!' + success: '&2Autenticazione eseguita correttamente!' + login_request: '&cPer favore, esegui l''autenticazione con il comando: /login ' + timeout_error: '&4Tempo scaduto per eseguire l''autenticazione, sei stato espulso dal server, per favore riprova!' + +# Errors +error: + denied_command: '&cPer poter usare questo comando devi essere autenticato!' + denied_chat: '&cPer poter scrivere messaggi in chat devi essere autenticato!' + unregistered_user: '&cL''utente non ha ancora eseguito la registrazione.' + not_logged_in: '&cNon hai ancora eseguito l''autenticazione!' + no_permission: '&4Non hai il permesso di eseguire questa operazione.' + unexpected_error: '&4Qualcosa è andato storto, riporta questo errore ad un amministratore!' + max_registration: '&cHai raggiunto il numero massimo di registrazioni (%reg_count/%max_acc %reg_names) per questo indirizzo IP!' + logged_in: '&cHai già eseguito l''autenticazione!' + kick_for_vip: '&3Un giocatore VIP è entrato mentre il server era pieno e ha preso il tuo posto!' + kick_unresolved_hostname: '&cQualcosa è andato storto: hostname del giocatore irrisolvibile!' + tempban_max_logins: '&cSei stato temporaneamente bandito per aver fallito l''autenticazione troppe volte.' + +# AntiBot +antibot: + kick_antibot: 'Il servizio di AntiBot è attualmente attivo! Devi aspettare qualche minuto prima di poter entrare nel server.' + auto_enabled: '&4Il servizio di AntiBot è stato automaticamente abilitato a seguito delle numerose connessioni!' + auto_disabled: '&2Il servizio di AntiBot è stato automaticamente disabilitato dopo %m minuti!' + +# Unregister +unregister: + success: '&2Sei stato correttamente rimosso dal database!' + command_usage: '&cUtilizzo: /unregister ' + +# Other messages +misc: + account_not_activated: '&cIl tuo account non è stato ancora verificato, controlla fra le tue email per scoprire come attivarlo!' + not_activated: '&cAccount non attivato, si prega di registrarsi e attivarlo prima di riprovare.' + password_changed: '&2Password cambiata correttamente!' + logout: '&2Disconnessione avvenuta correttamente!' + reload: '&2La configurazione e il database sono stati ricaricati correttamente!' + usage_change_password: '&cUtilizzo: /changepassword ' + accounts_owned_self: 'Possiedi %count account:' + accounts_owned_other: 'Il giocatore %name possiede %count account:' + +# Session messages +session: + valid_session: '&2Autenticato automaticamente attraverso la precedente sessione!' + invalid_session: '&cIl tuo indirizzo IP è cambiato e la tua sessione è stata terminata!' + +# Error messages when joining +on_join_validation: + same_ip_online: 'Un giocatore con il tuo stesso IP è già connesso sul server!' + same_nick_online: '&4Un giocatore con il tuo stesso nome utente è già connesso sul server!' + name_length: '&4Il tuo nome utente è troppo corto o troppo lungo!' + characters_in_name: '&4Il tuo nome utente contiene caratteri non consentiti. I caratteri consentiti sono: %valid_chars' + kick_full_server: '&4Il server è attualmente pieno, riprova più tardi!' + country_banned: '&4Il tuo paese è bandito da questo server!' + 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".' + quick_command: 'Hai usato un comando troppo velocemente dal tuo accesso! Per favore, rientra nel server e aspetta un po'' di più prima di usare un qualsiasi comando.' + +# Email +email: + add_email_request: '&3Per poter recuperare la password in futuro, aggiungi un indirizzo email al tuo account con il comando: /email add ' + usage_email_add: '&cUtilizzo: /email add ' + usage_email_change: '&cUtilizzo: /email change ' + new_email_invalid: '&cIl nuovo indirizzo email inserito non è valido, riprova!' + old_email_invalid: '&cIl vecchio indirizzo email inserito non è valido, riprova!' + invalid: '&cL''indirizzo email inserito non è valido, riprova!' + added: '&2Indirizzo email aggiunto correttamente al tuo account!' + add_not_allowed: '&cNon hai il permesso di aggiungere un indirizzo email' + request_confirmation: '&cPer favore, conferma il tuo indirizzo email!' + changed: '&2Indirizzo email cambiato correttamente!' + change_not_allowed: '&cNon hai il permesso di cambiare l''indirizzo email' + email_show: '&2Il tuo indirizzo email al momento è: &f%email' + no_email_for_account: '&2Al momento non hai nessun indirizzo email associato al tuo account.' + already_used: '&4L''indirizzo email inserito è già in uso' + incomplete_settings: 'Errore: non tutte le impostazioni richieste per inviare le email sono state impostate. Per favore contatta un amministratore.' + send_failure: 'Non è stato possibile inviare l''email di recupero. Per favore contatta un amministratore.' + change_password_expired: 'Non puoi più cambiare la tua password con questo comando.' + email_cooldown_error: '&cUna email di recupero ti è già stata inviata recentemente. Devi attendere %time prima di poterne richiedere una nuova.' + +# Password recovery by email +recovery: + forgot_password_hint: '&3Hai dimenticato la tua password? Puoi recuperarla usando il comando: /email recovery ' + command_usage: '&cUtilizzo: /email recovery ' + email_sent: '&2Una email di recupero è stata appena inviata al tuo indirizzo email!' + code: + code_sent: 'Una email contenente il codice di recupero per reimpostare la tua password è stata appena inviata al tuo indirizzo email.' + incorrect: 'Il codice di recupero inserito non è corretto! Hai altri %count tentativi rimanenti.' + tries_exceeded: 'Hai superato il numero massimo di tentativi per inserire il codice di recupero. Scrivi "/email recovery [email]" per generarne uno nuovo.' + correct: 'Codice di recupero inserito correttamente!' + change_password: 'Per favore usa il comando "/email setpassword " per cambiare immediatamente la tua password.' + +# Captcha +captcha: + usage_captcha: '&3Per poterti autenticare devi risolvere un captcha, per favore scrivi: /captcha %captcha_code' + wrong_captcha: '&cCaptcha sbagliato, per favore riprova scrivendo "/captcha %captcha_code" in chat!' + valid_captcha: '&2Il captcha inserito è valido!' + captcha_for_registration: 'Per poterti registrare devi prima risolvere un captcha, per favore scrivi: /captcha %captcha_code' + register_captcha_valid: '&2Il captcha inserito è valido! Ora puoi eseguire la registrazione con: /register ' + +# Verification code +verification: + code_required: '&3Questo comando va a modificare dati sensibili e richiede una verifica tramite email! Controlla la tua posta in arrivo e segui le istruzioni nell''email.' + command_usage: '&cUtilizzo: /verification ' + incorrect_code: '&cCodice sbagliato, per favore riprova scrivendo "/verification " in chat, usando il codice che hai ricevuto tramite email' + success: '&2La tua identità è stata verificata! Ora puoi eseguire tutti i comandi che modificano dati sensibili per questa sessione!' + already_verified: '&2Puoi già eseguire tutti i comandi che modificano dati sensibili per questa sessione!' + code_expired: '&3Il tuo codice è scaduto! Esegui nuovamente un comando che modifica dati sensibili per ricevere uno nuovo codice!' + email_needed: '&3Per verificare la tua identità devi collegare un indirizzo email al tuo account!' + +# Time units +time: + second: 'secondo' + seconds: 'secondi' + minute: 'minuto' + minutes: 'minuti' + hour: 'ora' + hours: 'ore' + day: 'giorno' + days: 'giorni' + +# Two-factor authentication +two_factor: + code_created: '&2Il tuo codice segreto è: &f%code%%nl%&2Puoi anche scannerizzare il codice QR da qui: &f%url' + confirmation_required: 'Per favore conferma il tuo codice con: /2fa confirm ' + code_required: 'Per favore inserisci il tuo codice per l''autenticazione a 2 fattori con: /2fa code ' + already_enabled: 'Hai già abilitato l''autenticazione a 2 fattori!' + enable_error_no_code: 'Non hai ancora generato un codice per l''autenticazione a 2 fattori oppure il tuo codice è scaduto. Per favore scrivi: /2fa add' + enable_success: 'Autenticazione a 2 fattori abilitata correttamente' + enable_error_wrong_code: 'Hai inserito un codice sbagliato o scaduto. Per favore scrivi: /2fa add' + not_enabled_error: 'L''autenticazione a 2 fattori non è ancora abilitata per il tuo account. Scrivi: /2fa add' + removed_success: 'Autenticazione a 2 fattori rimossa correttamente' + invalid_code: 'Il codice inserito non è valido, riprova!' + +# 3rd party features: Bedrock Auto Login +bedrock_auto_login: + success: '&aAccesso automatico Bedrock riuscito!' + +# 3rd party features: Login Location Fix +login_location_fix: + fix_portal: '&aSei bloccato nel portale durante il login.' + fix_underground: '&aSei bloccato sottoterra durante il login.' + cannot_fix_underground: '&aSei bloccato sottoterra durante il login, ma non possiamo risolverlo.' + +# 3rd party features: Double Login Fix +double_login_fix: + fix_message: '&cSei stato disconnesso a causa di un login doppio.' diff --git a/plugin/platform-bukkit/src/main/resources/messages/messages_ja.yml b/plugin/platform-bukkit/src/main/resources/messages/messages_ja.yml new file mode 100644 index 00000000..ac846a25 --- /dev/null +++ b/plugin/platform-bukkit/src/main/resources/messages/messages_ja.yml @@ -0,0 +1,173 @@ +# List of global tags: +# %nl% - Goes to new line. +# %username% - Replaces the username of the player receiving the message. +# %displayname% - Replaces the nickname (and colors) of the player receiving the message. + +# Registration +registration: + disabled: '&cゲーム内での登録は無効になっています!' + name_taken: '&cこのユーザー名はすでに登録されています!' + register_request: '&3サーバーに登録するには、次のコマンドを使用してください: /register <パスワード> <パスワードの確認>' + command_usage: '&c使用方法: /register <パスワード> <パスワードの確認>' + reg_only: '&4登録済みのユーザーのみサーバーに参加できます! 自分自身を登録するには、https://example.com にアクセスしてください!' + success: '&2登録が完了しました!' + kicked_admin_registered: '管理者があなたを登録しました。再度ログインしてください。' + +# Password errors on registration +password: + match_error: '&cパスワードが一致しません。もう一度確認してください!' + name_in_password: '&cパスワードには自分の名前を使用することはできません。別のパスワードを選択してください...' + unsafe_password: '&c選択したパスワードは安全ではありません。別のパスワードを選択してください...' + forbidden_characters: '&4パスワードに不正な文字が含まれています。許可されている文字: %valid_chars' + wrong_length: '&cパスワードが短すぎるか長すぎます!別のパスワードを試してください!' + pwned_password: '&c選択されたパスワードは安全ではありません。すでに %pwned_count 回使用されています!強力なパスワードを使用してください...' + +# Login +login: + command_usage: '&c使用方法: /login <パスワード>' + wrong_password: '&cパスワードが間違っています!' + success: '&2ログインが成功しました!' + login_request: '&c次のコマンドを使用してログインしてください: /login <パスワード>' + timeout_error: '&4ログインのタイムアウトが発生しました。サーバーからキックされました。もう一度試してください!' + +# Errors +error: + denied_command: '&cこのコマンドを使用するには認証が必要です!' + denied_chat: '&cチャットするには認証が必要です!' + unregistered_user: '&cこのユーザーは登録されていません!' + not_logged_in: '&cログインしていません!' + no_permission: '&4この操作を実行する権限がありません!' + unexpected_error: '&4予期しないエラーが発生しました。管理者に連絡してください!' + max_registration: '&c接続ごとの登録数が最大値を超えています(%reg_count/%max_acc %reg_names)!' + logged_in: '&cすでにログイン済みです!' + kick_for_vip: '&3VIPプレイヤーがサーバーが満員の状態で参加しました!' + kick_unresolved_hostname: '&cエラーが発生しました:解決できないプレイヤーのホスト名!' + tempban_max_logins: '&cログインに失敗した回数が多すぎるため、一時的にアクセスが制限されています。' + +# AntiBot +antibot: + kick_antibot: 'AntiBot保護モードが有効です!サーバーに参加するまでにしばらくお待ちください。' + auto_enabled: '&4[AntiBotService] 接続数が非常に多いため、AntiBotが有効になりました!' + auto_disabled: '&2[AntiBotService] %m 分後にAntiBotが無効になりました!' + +# Unregister +unregister: + success: '&c登録が正常に解除されました!' + command_usage: '&c使用方法: /unregister <パスワード>' + +# Other messages +misc: + account_not_activated: '&cアカウントはまだ有効化されていません。メールを確認してください!' + not_activated: '&cアカウントは有効化されていません。再試行する前に登録してアクティブ化してください。' + password_changed: '&2パスワードが正常に変更されました!' + logout: '&2正常にログアウトしました!' + reload: '&2設定とデータベースが正常に再読み込みされました!' + usage_change_password: '&c使用方法: /changepassword <旧パスワード> <新パスワード>' + accounts_owned_self: '所持しているアカウント数:%count 個' + accounts_owned_other: 'プレイヤー %name のアカウント数:%count 個' + +# Session messages +session: + valid_session: '&2セッションの再接続によるログインです。' + invalid_session: '&cIPアドレスが変更され、セッションのデータが期限切れです!' + +# Error messages when joining +on_join_validation: + same_ip_online: '同じIPアドレスを持つプレイヤーが既にゲーム内にいます!' + same_nick_online: '&4同じユーザー名のプレイヤーが既にサーバーでプレイしています!' + name_length: '&4ユーザー名が短すぎるか長すぎます!' + characters_in_name: '&4ユーザー名に無効な文字が含まれています。許可される文字:%valid_chars' + kick_full_server: '&4サーバーが満員です。後でもう一度お試しください!' + country_banned: '&4このサーバーへのアクセスは、お使いの国から制限されています!' + not_owner_error: 'このアカウントの所有者ではありません。別の名前を選択してください!' + invalid_name_case: '正しいユーザー名は %valid です。%invalid ではなく、このユーザー名で参加してください。' + quick_command: 'コマンドを速すぎる速度で使用しました!もう一度サーバーに参加してから、コマンドを使用する前にしばらくお待ちください。' + +# Email +email: + add_email_request: '&3コマンド「/email add <あなたのメールアドレス> <確認用メールアドレス>」を使用して、アカウントにメールアドレスを追加してください。' + usage_email_add: '&c使用方法:/email add <メールアドレス> <メールアドレスの確認>' + usage_email_change: '&c使用方法:/email change <古いメールアドレス> <新しいメールアドレス>' + new_email_invalid: '&c無効な新しいメールアドレスです。もう一度やり直してください!' + old_email_invalid: '&c無効な古いメールアドレスです。もう一度やり直してください!' + invalid: '&c無効なメールアドレスです。もう一度やり直してください!' + added: '&2メールアドレスがアカウントに正常に追加されました!' + add_not_allowed: '&cメールアドレスの追加は許可されていません。' + request_confirmation: '&cメールアドレスを確認してください!' + changed: '&2メールアドレスが正しく変更されました!' + change_not_allowed: '&cメールアドレスの変更は許可されていません。' + email_show: '&2現在のメールアドレスは:%email' + no_email_for_account: '&2現在、このアカウントに関連付けられたメールアドレスはありません。' + already_used: '&4そのメールアドレスは既に使用されています' + incomplete_settings: 'エラー:メールの送信に必要なすべての設定が設定されていません。管理者に連絡してください。' + send_failure: 'メールを送信できませんでした。管理者に連絡してください。' + change_password_expired: 'このコマンドを使用してパスワードを変更することはできません。' + email_cooldown_error: '&c最近すでにメールが送信されています。新しいメールを送信する前に、%time 待つ必要があります。' + +# Password recovery by email +recovery: + forgot_password_hint: '&3パスワードを忘れましたか?次のコマンドを使用してください:/email recovery <あなたのメールアドレス>' + command_usage: '&c使用方法:/email recovery <メールアドレス>' + email_sent: '&2パスワードの回復メールが正常に送信されました!メールの受信トレイを確認してください!' + code: + code_sent: 'パスワードをリセットするための回復コードがメールで送信されました。' + incorrect: '回復コードが正しくありません!残りの試行回数:%count' + tries_exceeded: '回復コードの入力試行回数が上限を超えました。新しいコードを生成するには、"/email recovery [メールアドレス]" を実行してください。' + correct: '回復コードが正しく入力されました!' + change_password: '即座にパスワードを変更するには、コマンド「/email setpassword <新しいパスワード>」を使用してください。' + +# Captcha +captcha: + usage_captcha: '&3ログインするには、Captchaコードを解決する必要があります。次のコマンドを使用してください:/captcha %captcha_code' + wrong_captcha: '&cCaptchaコードが間違っています。チャットに「/captcha %captcha_code」と入力してください!' + valid_captcha: '&2Captchaコードが正しく解決されました!' + captcha_for_registration: '登録するには、まずCaptchaを解決する必要があります。次のコマンドを使用してください:/captcha %captcha_code' + register_captcha_valid: '&2有効なCaptchaです!/register を使用して登録できます' + +# Verification code +verification: + code_required: '&3このコマンドはセンシティブな操作であり、メールの認証が必要です!受信トレイを確認し、メールの指示に従ってください。' + command_usage: '&c使用方法:/verification <コード>' + incorrect_code: '&cコードが正しくありません。メールで受け取ったコードを使用して、「/verification <コード>」とチャットに入力してください。' + success: '&2身元が確認されました!現在のセッション内ですべてのコマンドを実行できます!' + already_verified: '&2現在のセッションでは、既にすべてのセンシティブなコマンドを実行できます!' + code_expired: '&3コードの有効期限が切れています!新しいコードを取得するには、別のセンシティブなコマンドを実行してください!' + email_needed: '&3アカウントにはメールアドレスのリンクが必要です。身元を確認するためにはメールアドレスを関連付けてください!' + +# Time units +time: + second: '秒' + seconds: '秒' + minute: '分' + minutes: '分' + hour: '時間' + hours: '時間' + day: '日' + days: '日' + +# Two-factor authentication +two_factor: + code_created: '&2秘密コードは %code です。こちらからスキャンできます:%url' + confirmation_required: 'コードを確認するには、/2fa confirm <コード> を使用してください' + code_required: '二要素認証コードを提出するには、/2fa code <コード> を使用してください' + already_enabled: 'アカウントで既に二要素認証が有効になっています!' + enable_error_no_code: '2要素認証キーが生成されていないか、期限が切れています。/2fa add を実行してください' + enable_success: 'アカウントでの二要素認証が正常に有効になりました' + enable_error_wrong_code: 'コードが間違っているか、期限が切れています。/2fa add を実行してください' + not_enabled_error: 'アカウントでは二要素認証が有効になっていません。/2fa add を実行してください' + removed_success: 'アカウントから二要素認証が正常に削除されました' + invalid_code: '無効なコードです!' + +# 3rd party features: Bedrock Auto Login +bedrock_auto_login: + success: '&aBedrock 自動ログイン成功!' + +# 3rd party features: Login Location Fix +login_location_fix: + fix_portal: '&aログイン中にポータルに閉じ込められました。' + fix_underground: '&aログイン中に地下に閉じ込められました。' + cannot_fix_underground: '&aログイン中に地下に閉じ込められましたが、修正できません。' + +# 3rd party features: Double Login Fix +double_login_fix: + fix_message: '&c二重ログインのため、切断されました。' diff --git a/plugin/platform-bukkit/src/main/resources/messages/messages_ko.yml b/plugin/platform-bukkit/src/main/resources/messages/messages_ko.yml new file mode 100644 index 00000000..6354fdf4 --- /dev/null +++ b/plugin/platform-bukkit/src/main/resources/messages/messages_ko.yml @@ -0,0 +1,175 @@ +#Translated by Kirito (kds123321@naver.com), Spectre (me@ptr.kr), Adeuran(adeuran@tistory.com) +#14.05.2017 Thanks for use +# List of global tags: +# %nl% - Goes to new line. +# %username% - Replaces the username of the player receiving the message. +# %displayname% - Replaces the nickname (and colors) of the player receiving the message. + +# Registration +registration: + disabled: '&c현재 회원 가입이 비활성화 되어 있습니다!' + name_taken: '&c이미 이 닉네임으로 회원 가입이 되어 있습니다!' + register_request: '&3다음 명령어로 서버에 가입해주세요: /register <비밀번호> <비밀번호 확인>' + command_usage: '&c사용법: /register <비밀번호> <비밀번호 확인>' + reg_only: '&4등록된 유저만 서버에 접속할 수 있습니다! 서버 홈페이지에 방문하여 가입해 주세요!' + success: '&2회원 가입이 완료되었습니다!' + kicked_admin_registered: '관리자가 방금 이 닉네임을 등록했습니다. 다시 로그인 해주세요' + +# Password errors on registration +password: + match_error: '&c비밀번호가 일치하지 않습니다, 다시 확인해주세요!' + name_in_password: '&c자신의 닉네임을 비밀번호로 사용할 수 없습니다, 다른 비밀번호를 사용하세요...' + unsafe_password: '&c이 비밀번호는 안전하지 않습니다, 다른 비밀번호를 사용하세요...' + forbidden_characters: '&4비밀번호에 잘못된 문자가 있습니다. 허용된 문자: %valid_chars' + wrong_length: '&c비밀번호가 너무 짧거나 너무 깁니다!' + pwned_password: '&c선택한 비밀번호가 안전하지 않습니다. 이미 %pwned_count 번 사용되었습니다! 강력한 비밀번호를 사용하세요...' + +# Login +login: + command_usage: '&c사용법: /login <비밀번호>' + wrong_password: '&c비밀번호가 잘못되었습니다!' + success: '&2로그인 되었습니다!' + login_request: '&c다음 명령어로 로그인 해주세요: /login <비밀번호>' + timeout_error: '&4로그인 시간이 초과 되어 서버에서 추방당했습니다. 다시 시도하세요!' + +# Errors +error: + denied_command: '&c이 명령어를 사용하려면 로그인해야 합니다!' + denied_chat: '&c채팅을 하려면 로그인해야 합니다!' + unregistered_user: '&c이 유저는 등록되지 않았습니다!' + not_logged_in: '&c로그인이 되어있지 않습니다!' + no_permission: '&4이 작업을 수행할 수 있는 권한이 없습니다!' + unexpected_error: '&4예기치 않은 오류가 발생했습니다, 관리자에게 알려주세요!' + max_registration: '&c당신은 가입할 수 있는 계정 한도를 초과했습니다 (%reg_count/%max_acc %reg_names)!' + logged_in: '&c이미 로그인되어 있습니다!' + kick_for_vip: '&3서버가 꽉 차있을 때는 VIP 플레이어만 접속이 가능합니다!' + kick_unresolved_hostname: '&c오류 발생: 확인되지 않은 플레이어 호스트 이름!' + tempban_max_logins: '&c너무 많이 로그인에 실패하여 잠시 서버에서 차단되었습니다.' + +# AntiBot +antibot: + kick_antibot: 'AntiBot 보호 모드가 활성화 되었습니다! 서버에 접속하려면 몇 분 기다려야 합니다.' + auto_enabled: '&4[AntiBotService] 엄청나게 많은 연결이 감지 되어 AntiBot이 활성화 되었습니다!' + auto_disabled: '&2[AntiBotService] %m 분 후에 AntiBot이 비활성화 됩니다!' + +# Unregister +unregister: + success: '&c회원 탈퇴가 완료되었습니다!' + command_usage: '&c사용법: /unregister <비밀번호>' + +# Other messages +misc: + account_not_activated: '&c계정이 아직 활성화되지 않았습니다. 이메일을 확인해보세요!' + not_activated: '&c계정이 활성화되지 않았습니다. 다시 시도하기 전에 등록하고 활성화하세요.' + password_changed: '&2비밀번호가 변경되었습니다!' + logout: '&2로그아웃 되었습니다!' + reload: '&2설정과 데이터 베이스가 새로고침 되었습니다!' + usage_change_password: '&c사용법: /changepassword <예전 비밀번호> <새 비밀번호>' + accounts_owned_self: '%count 개의 계정을 소유하고 있습니다.' + accounts_owned_other: '플레이어 %name 는 %count 개의 계정을 소유하고 있습니다:' + +# Session messages +session: + valid_session: '&2세션 재 연결로 인해 로그인 되었습니다.' + invalid_session: '&cIP가 변경되어 세션이 만료되었습니다!' + +# Error messages when joining +on_join_validation: + same_ip_online: '똑같은 IP가 이미 이 서버에 접속해 있습니다!' + same_nick_online: '&4똑같은 닉네임이 이미 이 서버에 접속해 있습니다!' + name_length: '&4닉네임이 너무 짧거나 너무 깁니다!' + characters_in_name: '&4닉네임에 잘못된 문자가 있습니다. 허용된 문자: %valid_chars' + kick_full_server: '&4서버가 꽉 찼습니다. 나중에 다시 접속해보세요!' + country_banned: '&4당신의 국가에서는 이 서버를 이용하실 수 없습니다!' + not_owner_error: '이 계정의 소유자가 아닙니다. 다른 닉네임을 선택하세요!' + invalid_name_case: '%invalid가 아닌, %valid 사용하여 접속해야 합니다.' + quick_command: '명령어를 너무 빠르게 입력하고 있습니다! 서버에 다시 접속한 후 조금만 기다려주시기 바랍니다.' + +# Email +email: + add_email_request: '&3다음 명령어로 계정에 이메일 주소를 추가하세요: /email add <이메일 주소> <이메일 주소 확인>' + usage_email_add: '&c사용법: /email add <이메일 주소> <이메일 주소 확인>' + usage_email_change: '&c사용법: /email change <예전 이메일 주소> <새 이메일 주소>' + new_email_invalid: '&c새 이메일 주소가 잘못되었습니다. 다시 시도해보세요!' + old_email_invalid: '&c예전 이메일 주소가 잘못되었습니다. 다시 시도해보세요!' + invalid: '&c이메일 주소가 잘못되었습니다. 다시 시도해보세요!' + added: '&2계정에 이메일 주소를 추가했습니다!' + add_not_allowed: '&c이메일 주소의 추가가 제한된 상태입니다.' + request_confirmation: '&c이메일 주소를 확인해주세요!' + changed: '&2이메일 주소가 변경되었습니다!' + change_not_allowed: '&c이메일 주소의 변경이 제한된 상태입니다.' + email_show: '&2현재 이메일 주소: &f%email' + no_email_for_account: '&2현재 이 계정과 연결된 이메일 주소가 없습니다.' + already_used: '&4이메일 주소가 이미 사용 중입니다.' + incomplete_settings: '오류: 메일을 보내기 위해 필요한 설정이 되어 있지 않습니다. 관리자에게 알려주세요.' + send_failure: '이메일을 보낼 수 없습니다. 관리자에게 알려주세요.' + change_password_expired: '더 이상 이 명령어를 통해 비밀번호를 변경할 수 없습니다.' + email_cooldown_error: '&c이메일을 이미 발송했습니다. %time 후에 다시 발송할 수 있습니다.' + +# Password recovery by email +recovery: + forgot_password_hint: '&3비밀번호를 잊으셨나요? 이 명령어를 사용해보세요: /email recovery <이메일 주소>' + command_usage: '&c사용법: /email recovery <이메일 주소>' + email_sent: '&2복구 이메일을 보냈습니다! 메일함을 확인해보세요!' + code: + code_sent: '비밀번호 재설정을 위한 복구 코드가 이메일로 전송되었습니다.' + incorrect: '복구 코드가 올바르지 않습니다! "/email recovery [이메일 주소]"를 이용하여 새로 생성하세요.' + tries_exceeded: '잘못된 복구 코드를 너무 많이 입력했습니다. "/email recovery [이메일 주소]"를 통해 새로 생성하세요.' + correct: '복구 코드가 정상적으로 입력되었습니다!' + change_password: '비밀번호를 변경하려면 /email setpassword <새 비밀번호>를 입력해주세요.' + +# Captcha +captcha: + usage_captcha: '&3로그인 하려면 CAPTCHA 코드를 입력해야 합니다. 이 명령어를 사용하세요: /captcha %captcha_code' + wrong_captcha: '&c잘못된 CAPTCHA 코드 입니다. "/captcha %captcha_code" 형태로 입력해주세요!' + valid_captcha: '&2CAPTCHA 코드가 확인되었습니다!' + captcha_for_registration: '회원 가입을 하기 위해서는 먼저 CAPTCHA 코드를 입력해야합니다. 이 명령어를 이 명령어를 사용하세요: /captcha %captcha_code' + register_captcha_valid: '&2올바른 CAPTCHA 코드입니다! 이제 /register 명령어를 이용하여 회원 가입할 수 있습니다.' + +# Verification code +verification: + code_required: '&3이 명령어는 매우 민감하게 작동되며, 이메일 인증을 필요로 합니다. 이메일을 확인하고 지시에 따르십시오.' + command_usage: '&c사용법: /verification <인증 코드>' + incorrect_code: '&c코드가 올바르지 않습니다. "/verification <인증 코드>" 형태로 입력해주세요.' + success: '&2신원이 확인되었습니다. 귀하는 이 세션이 만료되기 전까지 모든 명령어를 사용할 수 있습니다.' + already_verified: '&2이 인증 코드는 이미 사용되었습니다!' + code_expired: '&3이 인증 코드는 만료되었습니다! 새 인증 코드를 발급받아주세요.' + email_needed: '&3귀하의 신원을 확인하기 위해서는 귀하의 계정과 이메일 주소가 연결되어 있어야 합니다.' + +# Time units +time: + second: '초' + seconds: '초' + minute: '분' + minutes: '분' + hour: '시간' + hours: '시간' + day: '일' + days: '일' + +# Two-factor authentication +two_factor: + code_created: '&2당신의 2단계 인증 코드는 %code 입니다. %url 에서 스캔할 수 있습니다' + confirmation_required: '/2fa confirm 명령어를 통해 귀하의 코드를 확인해 주시기 바랍니다.' + code_required: '/2fa code 명령어를 통해 2단계 인증을 진행해주세요.' + already_enabled: '2단계 인증 기능을 이미 활성화하셨습니다!' + enable_error_no_code: '2단계 인증 키가 생성되지 않았거나 만료되었습니다. /2fa add 명령어를 통해 새로 생성해주세요.' + enable_success: '2단계 인증 기능을 성공적으로 활성화하였습니다.' + enable_error_wrong_code: '잘못되었거나 만료된 코드입니다. /2fa add 명령어를 통해 새로 생성해주세요.' + not_enabled_error: '2단계 인증 기능을 활성화하시지 않았습니다. /2fa add 명령어를 통해 활성화해주세요.' + removed_success: '2단계 인증 기능을 성공적으로 비활성화하였습니다.' + invalid_code: '올바르지 않은 인증 코드입니다!' + +# 3rd party features: Bedrock Auto Login +bedrock_auto_login: + success: '&aBedrock 자동 로그인 성공!' + +# 3rd party features: Login Location Fix +login_location_fix: + fix_portal: '&a로그인 중에 포탈에 갇혔습니다.' + fix_underground: '&a로그인 중에 지하에 갇혔습니다.' + cannot_fix_underground: '&a로그인 중에 지하에 갇혔지만, 수정할 수 없습니다.' + +# 3rd party features: Double Login Fix +double_login_fix: + fix_message: '&c중복 로그인으로 인해 연결이 종료되었습니다.' diff --git a/plugin/platform-bukkit/src/main/resources/messages/messages_lt.yml b/plugin/platform-bukkit/src/main/resources/messages/messages_lt.yml new file mode 100644 index 00000000..b09bbb64 --- /dev/null +++ b/plugin/platform-bukkit/src/main/resources/messages/messages_lt.yml @@ -0,0 +1,173 @@ +# List of global tags: +# %nl% - Goes to new line. +# %username% - Replaces the username of the player receiving the message. +# %displayname% - Replaces the nickname (and colors) of the player receiving the message. + +# Registration +registration: + disabled: '&6Registracija yra išjungta' + name_taken: '&cVartotojo vardas jau užregistruotas' + register_request: '&ePrašome prisiregistruoti: /register slaptažodis pakartotiSlaptažodį' + command_usage: '&eNaudojimas: /register slaptažodis pakartotiSlaptažodį' + reg_only: '&cTik prisiregistravusiems žaidėjams: apsilankykite: https://example.com tam, kad užsiregistruoti.' + success: '&aSėkmingai prisiregistravote.' + kicked_admin_registered: 'Administatorius Jus užregistravo. Prisijunkite iš naujo' + +# Password errors on registration +password: + match_error: '&cSlaptažodžiai nesutampa' + name_in_password: '&cJūs negalite naudoti savo vardo slaptažodyje' + unsafe_password: '&cŠį Slaptažodį lengva nulaužti, pasirinkite kitą slaptažodį' + forbidden_characters: '&4Jūsų slaptažodis turi netinkamų simbolių. Leidžiami simboliai: %valid_chars' + wrong_length: '&cJūsų pasirinktas slaptažodis per ilgas arba per trumpas.' + pwned_password: '&cJūsų pasirinktas slaptažodis nėra saugus. Jis buvo naudotas %pwned_count kartų! Prašome naudoti stiprų slaptažodį...' + +# Login +login: + command_usage: '&eKomandos panaudojimas: /login slaptažodis' + wrong_password: '&cNeteisingas Slaptažosdis' + success: '&aSėkmingai prisijungėte' + login_request: '&ePrašome prisijungti: /login slaptažodis' + timeout_error: '&cNespėjote prisijungti' + +# Errors +error: + denied_command: '&cKad galetumėte naudoti šią komandą turite būti prisijungę!' + denied_chat: '&cKad galetumėte kalbėti Jūs turite būti prisijungę!' + unregistered_user: '&cVartotojas neprisiregistravęs' + not_logged_in: '&cJūs neprisijungę!' + no_permission: '&cNėra leidimo' + unexpected_error: '&cAtsirado klaida, praneškite adminstratoriui.' + max_registration: '&cJūs pasiekėte maksimalų registracijų skaičių.' + logged_in: '&cTu jau prisijungęs!' + kick_for_vip: '&cRėmėjas prisijungė į pilną serverį!' + kick_unresolved_hostname: '&cĮvyko klaida su žaidejo adresu!' + tempban_max_logins: '&cJūs laikinai užblokuotas, nes kelis kartus neteisingai suvedėte slaptažodį.' + +# AntiBot +antibot: + kick_antibot: 'AntiBot prevencija įjungta! Palaukite prieš prisijungiant.' + auto_enabled: '&4[AntiBotService] AntiBot prevencija pajungta dėl didelio kiekio prisijungimų!' + auto_disabled: '&2[AntiBotService] AntiBot bus išjungtas po %m minučių!' + +# Unregister +unregister: + success: '&aSėkmingai išsiregistravote!' + command_usage: '&ePanaikinti registraciją: "/unregister slaptažodis"' + +# Other messages +misc: + account_not_activated: '&aJūsų vartotojas nėra patvirtintas, pasitikrinkite el.paštą.' + not_activated: '&cPaskyra neaktyvuota, prašome užsiregistruoti ir aktyvuoti prieš bandant dar kartą.' + password_changed: '&aSlaptažodis pakeistas' + logout: '&aSėkmingai atsijungėte' + reload: '&aNustatymai ir duomenų bazė buvo perkrauta.' + usage_change_password: '&ePanaudojimas: /changepassword senasSlaptažodis naujasSlaptažodis' + accounts_owned_self: 'Jūs turite %count paskyrą(-s):' + accounts_owned_other: 'Žaidejas %name turi %count paskyrą(-s):' + +# Session messages +session: + valid_session: '&aAutomatinis sesijos prisijungimas' + invalid_session: '&cSesijos laikai nesutampa. Prašome palaukti kol sesija baigsis.' + +# Error messages when joining +on_join_validation: + same_ip_online: 'Žaidejas su tuo pačiu IP adresu jau yra žaidime!' + same_nick_online: '&cKažkas šituo vardu jau žaidžia.' + name_length: '&cJūsų vardas yra per ilgas arba per trumpas.' + characters_in_name: '&cJūsų varde yra neledziamų simbolių. Leidžiami: %valid_chars' + kick_full_server: '&cServeris yra pilnas, atsiprašome.' + country_banned: '&4Jūsų šalis yra užblokuota šiame serveryje!' + not_owner_error: 'J0s nesate šios paskyros savininkas, pasirinkite kitą vardą!' + invalid_name_case: 'Turėtumėte prisijungti su vardu %valid, o ne su: %invalid.' + quick_command: 'Jūs panaudojote komandą per greitai! Prisijunkite iš naujo ir šiek tiek palaukite prieš naudojant komandas.' + +# Email +email: + add_email_request: '&ePrašome jūsų pridėti savo el.paštą : /email add ' + usage_email_add: '&cNaudojimas: /email add ' + usage_email_change: '&cNaudojimas: /email change ' + new_email_invalid: '&cNeteisingas el.paštas, bandykite iš naujo!' + old_email_invalid: '&cNeteisingas senas el.paštas, bandykite iš naujo!' + invalid: '&cNeteisingas el.paštas, bandykite iš naujo!' + added: '&2El.paštas sėkmingai pridėtas!' + add_not_allowed: '&cNaujo el.pašto pridejimas nėra galimas' + request_confirmation: '&cPatvirtinkite savo el.paštą!' + changed: '&2El.paštą pakeistas sėkmingai!' + change_not_allowed: '&cEl.pašto keitimas nėra galimas' + email_show: '&2Jūsų dabartinis el.pašto adresas: &f%email' + no_email_for_account: '&2Šiuo metu Jūs neturite pridėję jokio el.pašto adreso.' + already_used: '&4Jis el.pašto adresas jau yra naudojamas' + incomplete_settings: 'Klaida: Ne visi nustatymai yra nustatyti laiško siuntimui. Susikietite su administratoriumi.' + send_failure: 'El.pašto laiškas nebuvo išsiųstas. Susikietite su administratoriumi.' + change_password_expired: 'Jūs nebegalite pakeisti savo slaptažodzio naudojant šią komandą.' + email_cooldown_error: '&cEl.pašto laiškas jau buvo išsiųstas. Palaukite %time prieš šiunčiant naują.' + +# Password recovery by email +recovery: + forgot_password_hint: '&cPamiršote savo slaptažodį? Rašykite: /email recovery el.paštas' + command_usage: '&cNaudojimas: /email recovery el.paštas' + email_sent: '&2Laiškas į Jūsų el.pašto adresą buvo išsiųstas!' + code: + code_sent: 'Kodas slaptažodžio atstatymui buvo išsiųstas į Jūsų el.paštą.' + incorrect: 'Kodas neteisingas! Jums liko %count bandymai(-as).' + tries_exceeded: 'Jūs išnaudojote visus bandymus. Naudokite "/email recovery el.paštas", kad gauti naują kodą.' + correct: 'Kodas įvestas sėkmingai!' + change_password: 'Naudokite /email setpassword , kad pasikeistumėte slaptažodį.' + +# Captcha +captcha: + usage_captcha: '&cPanaudojimas: /captcha %captcha_code' + wrong_captcha: '&cNeteisinga captcha, naudokite : /captcha %captcha_code' + valid_captcha: '&cJūsų captcha teisinga!' + captcha_for_registration: 'Kad prisiregistruotumėte turite įvygdyti captchą. Rašykite: /captcha %captcha_code' + register_captcha_valid: '&2Captcha teisinga! Galite naudoti /register' + +# Verification code +verification: + code_required: '&3Kad galėtumėte naudoti šią komandą turite patvirtinti savo el.pašto adresą! Sekite instrukcijas savo el.pašte.' + command_usage: '&cNaudojimas: /verification ' + incorrect_code: '&cNeteisingas kodas, naudokite "/verification " įvesdami kodą gautą savo el.pašte' + success: '&2Jūsų paskyra patvirtinta! Jūs galite naudoti visas komandas!' + already_verified: '&2Jūs jau esate patvirtinę savo paskyrą ir galite naudoti visas komandas!' + code_expired: '&3Jūsų kodo galiojimas baigėsi! Panaudokite komandą iš naujo, kad gautumėte naują kodą!' + email_needed: '&3Kad patvirtinti savo paskyra turite pridėti el.pašto adresą!' + +# Time units +time: + second: 'sekundę' + seconds: 'sekundes' + minute: 'minutę' + minutes: 'minutes' + hour: 'valandą' + hours: 'valandas' + day: 'dieną' + days: 'dienas' + +# Two-factor authentication +two_factor: + code_created: '&2Jūsų slaptas kodas yra %code. Jį galite nuskenuoti čia: %url' + confirmation_required: 'Patvirtinkite savo kodą su: /2fa confirm ' + code_required: 'Patvirkinkite savo kodą su: /2fa code ' + already_enabled: 'Jūs jau turite dviejų faktorių autentifikaciją!' + enable_error_no_code: 'Jūs neturite dviejų faktorių autentifikacijos arba ji pasibaigė. Rašykite /2fa add' + enable_success: 'Dviejų faktorių autentifikacija sėkmingai įjungta' + enable_error_wrong_code: 'Neteisingas arba pasibaigęs kodas. Rašykite /2fa add' + not_enabled_error: 'Dviejų faktorių autentifikavimas nėra įjungtas ant jūsų paskyros. Rašykite /2fa add' + removed_success: 'Dviejų faktorių autentifikavimas sėkmingai pašalintas iš jūsų paskyros.' + invalid_code: 'Neteisingas kodas!' + +# 3rd party features: Bedrock Auto Login +bedrock_auto_login: + success: '&aBedrock automatinis prisijungimas sėkmingas!' + +# 3rd party features: Login Location Fix +login_location_fix: + fix_portal: '&aJūs užstrigote portale prisijungimo metu.' + fix_underground: '&aJūs užstrigote po žeme prisijungimo metu.' + cannot_fix_underground: '&aJūs užstrigote po žeme prisijungimo metu, bet mes negalime to pataisyti.' + +# 3rd party features: Double Login Fix +double_login_fix: + fix_message: '&cJūs buvote atjungtas dėl dvigubo prisijungimo.' diff --git a/plugin/platform-bukkit/src/main/resources/messages/messages_nl.yml b/plugin/platform-bukkit/src/main/resources/messages/messages_nl.yml new file mode 100644 index 00000000..42c3207c --- /dev/null +++ b/plugin/platform-bukkit/src/main/resources/messages/messages_nl.yml @@ -0,0 +1,173 @@ +# List of global tags: +# %nl% - Goes to new line. +# %username% - Replaces the username of the player receiving the message. +# %displayname% - Replaces the nickname (and colors) of the player receiving the message. + +# Registration +registration: + disabled: '&cRegistratie is uitgeschakeld!' + name_taken: '&cJe hebt deze gebruikersnaam al geregistreerd!' + register_request: '&cRegistreer met het commando: /register ' + command_usage: '&cGebruik: /register ' + reg_only: 'Alleen voor geregistreerde spelers! Bezoek https://example.com om te registreren!' + success: '&cSuccesvol geregistreerd!' + kicked_admin_registered: 'Een administrator heeft je net geregistreerd; log alsjeblieft opnieuw in.' + +# Password errors on registration +password: + match_error: 'Jouw wachtwoorden komen niet overeen, controleer ze opnieuw!' + name_in_password: '&fJe kunt je gebruikersnaam niet als wachtwoord gebruiken, kies een ander wachtwoord...' + unsafe_password: '&fDit wachtwoord is onveilig, kies een ander wachtwoord...' + forbidden_characters: '&cJouw wachtwoord bevat ongeldige tekens. Toegestane karakters zijn: %valid_chars' + wrong_length: 'Jouw gekozen wachtwoord voldoet niet aan de minimum of maximum lengte, kies een ander wachtwoord...' + pwned_password: '&cHet gekozen wachtwoord is niet veilig. Het is al %pwned_count keer gebruikt! Gebruik alstublieft een sterk wachtwoord...' + +# Login +login: + command_usage: '&cGebruik: /login ' + wrong_password: '&cFout wachtwoord' + success: '&cSuccesvol ingelogd!' + login_request: '&cLog in met: /login ' + timeout_error: 'Login time-out: het duurde te lang tot je inlogde.' + +# Errors +error: + denied_command: '&cJe moet ingelogd zijn om dit commando te gebruiken!' + denied_chat: '&cJe moet ingelogd zijn om te kunnen chatten!' + unregistered_user: '&cDeze gebruikersnaam is niet geregistreerd!' + not_logged_in: '&cNiet ingelogd!' + no_permission: '&cJe hebt geen rechten om deze actie uit te voeren!' + unexpected_error: 'Er is een onverwachte fout opgetreden, neem contact op met een administrator!' + max_registration: 'Je hebt het maximum aantal registraties overschreden (%reg_count/%max_acc: %reg_names).' + logged_in: '&cJe bent al ingelogd!' + kick_for_vip: '&cEen VIP-gebruiker heeft ingelogd toen de server vol was!' + kick_unresolved_hostname: '&cEr heeft een fout plaatsgevonden: hostname van de speler kon niet gevonden worden!' + tempban_max_logins: '&cJe bent tijdelijk gebanned omdat het inloggen te vaak mislukt is.' + +# AntiBot +antibot: + kick_antibot: 'AntiBot is aangezet! Wacht alsjeblieft enkele minuten voor je je met de server verbindt.' + auto_enabled: '[AuthMe] AntiBotMod automatisch aangezet vanwege te veel verbindingen!' + auto_disabled: '[AuthMe] AntiBotMod automatisch uitgezet na %m minuten!' + +# Unregister +unregister: + success: '&cSuccesvol afgemeld!' + command_usage: '&cGebruik: /unregister [wachtwoord]' + +# Other messages +misc: + account_not_activated: 'Je account is nog niet geactiveerd, controleer je mailbox!' + not_activated: '&cAccount niet geactiveerd, registreer en activeer het alstublieft voordat u het opnieuw probeert.' + password_changed: '&cWachtwoord succesvol aangepast!' + logout: '&2Je bent succesvol uitgelogd!' + reload: '&2De configuratie en database zijn succesvol herladen!' + usage_change_password: '&cGebruik: /changepassword ' + accounts_owned_self: 'Je bezit %count accounts:' + accounts_owned_other: 'De speler %name heeft %count accounts:' + +# Session messages +session: + valid_session: '&2Ingelogd wegens sessie-herverbinding.' + invalid_session: '&cJouw IP-adres is veranderd en je sessie is verlopen!' + +# Error messages when joining +on_join_validation: + same_ip_online: 'Een speler met hetzelfde IP-adres is al online!' + same_nick_online: '&4Er is al iemand met jouw gebruikersnaam online.' + name_length: '&4Jouw gebruikersnaam is te kort of te lang!' + characters_in_name: '&4Jouw gebruikersnaam bevat ongeldige tekens. Toegestane karakters zijn: %valid_chars' + kick_full_server: '&4De server is vol, probeer het later opnieuw!' + country_banned: '&4Jouw land is gebanned op deze server!' + not_owner_error: 'Jij bent niet de eigenaar van dit account. Kies alsjeblieft een andere naam!' + invalid_name_case: 'Verbind je alsjeblieft als %valid, niet als %invalid.' + quick_command: 'Je voerde te snel een commando uit! Verbind alsjeblieft opnieuw met de server en wacht langer voor het gebruiken van een commando.' + +# Email +email: + add_email_request: '&3Voeg jouw e-mailadres alsjeblieft toe met: /email add ' + usage_email_add: '&cGebruik: /email add ' + usage_email_change: '&cGebruik: /email change ' + new_email_invalid: '&cOngeldig nieuw e-mailadres, probeer het opnieuw!' + old_email_invalid: '&cOngeldig oud e-mailadres, probeer het opnieuw!' + invalid: '&cOngeldig E-mailadres, probeer het opnieuw!' + added: '&2Het e-mailadres is succesvol toegevoegd aan je account!' + add_not_allowed: '&cEen e-mailadres toevoegen is niet toegestaan' + request_confirmation: '&cVerifiëer je e-mailadres alsjeblieft!' + changed: '&2Het e-mailadres is succesvol veranderd!' + change_not_allowed: '&cEen e-mailadres wijzigen is niet toegestaan' + email_show: '&2Jouw huidige e-mailadres is: %email' + no_email_for_account: '&2Je hebt nog geen e-mailadres toegevoegd aan dit account.' + already_used: '&4Dit e-mailadres wordt al gebruikt' + incomplete_settings: 'Fout; er moeten nog enkele opties ingevuld worden om mails te kunnen sturen. Neem contact op met een administrator.' + send_failure: 'De e-mail kon niet verzonden worden. Neem contact op met een administrator.' + change_password_expired: 'Je kunt je wachtwoord niet meer veranderen met dit commando.' + email_cooldown_error: '&cEr is recent al een e-mail verzonden. Je moet %time wachten voordat je een nieuw bericht kunt versturen.' + +# Password recovery by email +recovery: + forgot_password_hint: '&3Wachtwoord vergeten? Gebruik alsjeblieft het commando: /email recovery ' + command_usage: '&cGebruik: /email recovery ' + email_sent: '&2Een herstel e-mail is verzonden! Check alsjeblieft je mailbox!' + code: + code_sent: 'Een herstelcode voor je wachtwoord is naar je mailbox gestuurd.' + incorrect: 'De herstelcode is niet correct! Je kunt het nog %count keer proberen.' + tries_exceeded: 'Je hebt het maximum aantal pogingen om een herstel code in te vullen bereikt. Gebruik "/email recovery [e-mail]" om een nieuwe code te krijgen' + correct: 'De herstelcode is correct!' + change_password: 'Gebruik alsjeblieft het commando /email setpassword om je wachtwoord direct te veranderen.' + +# Captcha +captcha: + usage_captcha: '&3Om in te loggen moet je een captcha-code oplossen, gebruik het commando: /captcha %captcha_code' + wrong_captcha: '&cVerkeerde captcha-code, typ alsjeblieft "/captcha %captcha_code" in de chat!' + valid_captcha: '&2De captcha-code is geldig!' + captcha_for_registration: 'Om je te registreren moet je eerst een captcha oplossen. Gebruik alsjeblieft het volgende commando: /captcha %captcha_code' + register_captcha_valid: 'Valide puzzel! Je mag je nu registreren met /register' + +# Verification code +verification: + code_required: '&3Dit commando is gevoelig en vereist een e-mail verificatie! Bekijk je inbox en volg de opgegeven instructies.' + command_usage: '&cGebruik: /verification ' + incorrect_code: '&cIncorrecte code, type alsjeblieft "/verification " in de chat met de code die je hebt ontvangen in je email' + success: '&2Je identiteit is vastgesteld! Je kan nu alle commandos uitvoeren tijdens deze sessie!' + already_verified: '&2Je kan momenteel alle gevoelige commandos al uitvoeren!' + code_expired: '&3Je code is verlopen! Voer een nieuw gevoelig commando uit om een nieuwe code te ontvangen!' + email_needed: '&3Om je identiteit vast te stellen moet je een e-mailadres verbinden aan je account!!' + +# Time units +time: + second: 'seconde' + seconds: 'seconden' + minute: 'minuut' + minutes: 'minuten' + hour: 'uur' + hours: 'uren' + day: 'dag' + days: 'dagen' + +# Two-factor authentication +two_factor: + code_created: '&2Je geheime code is %code. Je kunt hem scannen op %url' + confirmation_required: 'Verifieer alsjeblieft je code met /2fa confirm ' + code_required: 'Stuur alsjeblieft je twee-factor authenticatie code met /2fa code ' + already_enabled: 'Twee-factor authenticatie is al aangezet voor jouw account!' + enable_error_no_code: 'Geen 2fa-sleutel is voor jou gegenereerd. Of hij is verlopen. Voer alsjeblieft "/2fa add" uit' + enable_success: 'Je hebt successvol twee-factor authenticatie ingeschakelt voor jou account' + enable_error_wrong_code: 'Foutieve, of verlopen code. Voer alsjeblieft "/2fa add" uit.' + not_enabled_error: 'Twee-factor authenticatie is uitgeschakeld voor jou account. Voer "/2fa add" uit' + removed_success: 'Twee-factor authenticatie is met succes van jouw account verwijderd.' + invalid_code: 'Ongeldige code!' + +# 3rd party features: Bedrock Auto Login +bedrock_auto_login: + success: '&aBedrock automatisch inloggen gelukt!' + +# 3rd party features: Login Location Fix +login_location_fix: + fix_portal: '&aU zit vast in een portal tijdens het inloggen.' + fix_underground: '&aU zit vast onder de grond tijdens het inloggen.' + cannot_fix_underground: '&aU zit vast onder de grond tijdens het inloggen, maar we kunnen het niet repareren.' + +# 3rd party features: Double Login Fix +double_login_fix: + fix_message: '&cU bent losgekoppeld vanwege een dubbele inlog.' diff --git a/plugin/platform-bukkit/src/main/resources/messages/messages_pl.yml b/plugin/platform-bukkit/src/main/resources/messages/messages_pl.yml new file mode 100644 index 00000000..e263df56 --- /dev/null +++ b/plugin/platform-bukkit/src/main/resources/messages/messages_pl.yml @@ -0,0 +1,173 @@ +# List of global tags: +# %nl% - Goes to new line. +# %username% - Replaces the username of the player receiving the message. +# %displayname% - Replaces the nickname (and colors) of the player receiving the message. + +# Registration +registration: + disabled: '&4Rejestracja jest wyłączona.' + name_taken: '&4Gracz już jest zarejestrowany.' + register_request: '&2Proszę się zarejestrować przy użyciu &6/register ' + command_usage: '&4Użycie: /register ' + reg_only: '&fTylko zarejestrowani użytkownicy mają do tego dostęp!' + success: '&aPomyślnie zarejestrowany!' + kicked_admin_registered: 'Administrator zarejestrował Ciebie, możesz się zalogować.' + +# Password errors on registration +password: + match_error: '&fHasło niepoprawne!' + name_in_password: '&cNie możesz używać nicku jako hasła, wybierz inne...' + unsafe_password: '&cTwoje hasło nie jest bezpieczne, wybierz inne...' + forbidden_characters: '&4W twoim haśle występują niedozwolone znaki, dozwolone znaki to: %valid_chars' + wrong_length: '&fTwoje hasło jest za krótkie lub za długie! Spróbuj ponownie...' + pwned_password: '&cTwoje wybrane hasło nie jest bezpieczne. Zostało już użyte %pwned_count razy! Proszę użyj silnego hasła...' + +# Login +login: + command_usage: '&cUżycie: /login hasło' + wrong_password: '&cNiepoprawne hasło.' + success: '&aHasło zaakceptowane!' + login_request: '&2Proszę się zalogować przy użyciu &6/login ' + timeout_error: '&cUpłynął limit czasu zalogowania' + +# Errors +error: + denied_command: '&cAby użyć tej komendy musisz się zalogować!' + denied_chat: '&cAby pisać na chacie musisz się zalogować!' + unregistered_user: '&fGracz nie jest zarejestrowany.' + not_logged_in: '&4Nie jesteś zalogowany!' + no_permission: '&4Nie posiadasz wymaganych uprawnień.' + unexpected_error: '&fWystąpił błąd, prosimy skontaktować się z administracją serwera.' + max_registration: '&cPrzekroczyłeś limit zarejestrowanych kont na serwerze &8(&e%reg_count/%max_acc %reg_names&8) &cdla twojego połączenia.' + logged_in: '&fJesteś już zalogowany!' + kick_for_vip: '&cGracz VIP dołączył do gry!' + kick_unresolved_hostname: '&cWystąpił błąd: nierozwiązana nazwa hosta gracza!' + tempban_max_logins: '&cZostałeś tymczasowo zbanowany za dużą liczbę nieudanych logowań!' + +# AntiBot +antibot: + kick_antibot: '&cAntyBot został włączony, musisz poczekać minutę przed dołączeniem do serwera.' + auto_enabled: '&4[AntiBot] &aAntyBot włączony z powodu dużej liczby połączeń!' + auto_disabled: '&2[AntiBot] &aAntyBot zostanie wyłączony za &7%m &aminut!' + +# Unregister +unregister: + success: '&4Pomyślnie wyrejestrowany!' + command_usage: '&cUżycie: /unregister hasło' + +# Other messages +misc: + account_not_activated: '&fTwoje konto nie zostało aktywowane! Sprawdź maila.' + not_activated: '&cKonto nieaktywowane, proszę zarejestruj się i aktywuj konto przed ponowną próbą.' + password_changed: '&fHasło zostało zmienione!' + logout: '&cPomyślnie wylogowany' + reload: '&fKonfiguracja bazy danych została przeładowana.' + usage_change_password: '&fUżycie: /changepassword ' + accounts_owned_self: '&7Posiadasz %count kont:' + accounts_owned_other: '&7Gracz %name posiada %count kont:' + +# Session messages +session: + valid_session: '&aZalogowano automatycznie z powodu sesji logowania.' + invalid_session: '&fSesja logowania zakończona!' + +# Error messages when joining +on_join_validation: + same_ip_online: '&cGracz z takim samym adresem ip jest aktualnie w grze!' + same_nick_online: '&fTen nick już gra na serwerze!' + name_length: '&cTwoje konto ma za długa bądź za krotką nazwę.' + characters_in_name: '&cTwoje konto ma w nazwie niedozwolone znaki. Dozwolone znaki: %valid_chars' + kick_full_server: '&cSerwer jest teraz zapełniony, przepraszamy!' + country_banned: '&4Ten kraj jest zbanowany na tym serwerze' + not_owner_error: '&cNie jesteś właścicielem tego konta, wybierz inny nick!' + invalid_name_case: '&cPowinieneś dołączyć do serwera z nicku %valid, a nie %invalid.' + quick_command: '&cUżyłeś komendy zbyt szybko! Ponownie dołącz do serwera i poczekaj chwilę, zanim użyjesz dowolnej komendy.' + +# Email +email: + add_email_request: '&cProszę dodać swój e-mail: /email add ' + usage_email_add: '&fWpisz: /email add ' + usage_email_change: '&fWpisz: /email change ' + new_email_invalid: '[AuthMe] Nowy e-mail niepoprawny!' + old_email_invalid: '[AuthMe] Stary e-mail niepoprawny!' + invalid: '[AuthMe] Nieprawidłowy adres e-mail.' + added: '[AuthMe] E-mail został dodany do Twojego konta!' + add_not_allowed: '&cMożliwość dodania adresu e-mail jest wyłączona.' + request_confirmation: '[AuthMe] Potwierdź swój adres e-mail!' + changed: '[AuthMe] E-mail został zmieniony!' + change_not_allowed: '&cMożliwość zmiany adresu e-mail jest wyłączona.' + email_show: '&2Twój aktualny adres e-mail to: &f%email' + no_email_for_account: '&2Nie posiadasz adresu e-mail przypisanego do tego konta.' + already_used: '&4Ten adres e-mail jest aktualnie używany!' + incomplete_settings: 'Błąd: Nie wszystkie opcje odpowiedzialne za wysyłanie e-maili zostały skonfigurowane. Skontaktuj się z administracją.' + send_failure: 'Nie można wysłać e-maila. Skontaktuj się z administracją.' + change_password_expired: 'Nie zmienisz już hasła przy użyciu tej komendy.' + email_cooldown_error: '&cE-mail został wysłany, musisz poczekać %time przed wysłaniem następnego.' + +# Password recovery by email +recovery: + forgot_password_hint: '&cZapomniałeś hasła? Proszę użyj komendy /email recovery ' + command_usage: '&fWpisz: /email recovery ' + email_sent: '[AuthMe] E-mail z odzyskaniem wysłany!' + code: + code_sent: 'Kod odzyskiwania hasła został wysłany na adres e-mail przypisany do konta.' + incorrect: '&cKod odzyskiwania hasła jest błędny! &4Pozostałe próby: %count.' + tries_exceeded: 'Osiągnięto limit prób wpisania kodu odzyskiwania. Wpisz /email recovery [e-mail] aby wygenerować nowy kod.' + correct: '&aKod odzyskiwania został wpisany pomyślnie!' + change_password: 'Wpisz komendę /email setpassword aby zmienić hasło do konta.' + +# Captcha +captcha: + usage_captcha: '&cWpisz: /captcha %captcha_code' + wrong_captcha: '&cZły kod, proszę wpisać: /captcha %captcha_code' + valid_captcha: '&cTwój kod jest nieprawidłowy!' + captcha_for_registration: '&cAby się zarejestrować, musisz przepisać kod captacha. Użyj komendy: /captcha %captcha_code' + register_captcha_valid: '&2Prawidłowy kod captacha! Teraz możesz się zarejestrować komendą /register' + +# Verification code +verification: + code_required: '&3Ta komenda wymaga weryfikacji przez e-mail! Sprawdź swoją skrzynkę odbiorczą i postępuj zgodnie z instrukcjami w wiadomości.' + command_usage: '&cUżycie: /verification ' + incorrect_code: '&cNieprawidłowy kod z e-maila, wpisz komende "/verification ". W miejscu wpisz swój kod z emaila.' + success: '&2Twoja tożsamość została zweryfikowana! Możesz teraz wykonywać wszystkie komendy w bieżącej sesji!' + already_verified: '&2Możesz już wykonać każdą komende w bieżącej sesji!' + code_expired: '&3Twój kod wygasł! Wykonaj ponownie komende, aby uzyskać nowy kod!' + email_needed: '&3Aby zweryfikować swoją tożsamość, musisz połączyć adres e-mail z kontem!' + +# Time units +time: + second: 'sekundy' + seconds: 'sekund' + minute: 'minuty' + minutes: 'minut' + hour: 'godziny' + hours: 'godzin' + day: 'dzień' + days: 'dni' + +# Two-factor authentication +two_factor: + code_created: '&2Twój sekretny kod to %code. Możesz zeskanować go tutaj: %url' + confirmation_required: 'Musisz potwierdzić swój kod komendą /2fa confirm ' + code_required: 'Wpisz swój kod weryfikacji dwuetapowej przy pomocy komendy /2fa code ' + already_enabled: '&aWeryfikacja dwuetapowa jest już włączona dla Twojego konta.' + enable_error_no_code: '&cKod weryfikacji dwuetapowej nie został dla Ciebie wygenerowany lub wygasł. Wpisz komende /2fa add' + enable_success: '&aWeryfikacja dwuetapowa została włączona dla Twojego konta.' + enable_error_wrong_code: '&cWpisany kod jest nieprawidłowy lub wygasły. Wpisz ponownie /2fa add' + not_enabled_error: 'Weryfikacja dwuetapowa nie jest włączona dla twojego konta. Wpisz komende /2fa add' + removed_success: '&aPomyślnie usunięto weryfikacje dwuetapową z Twojego konta.' + invalid_code: '&cWpisany kod jest nieprawidłowy, spróbuj jeszcze raz.' + +# 3rd party features: Bedrock Auto Login +bedrock_auto_login: + success: '&aAutomatyczne logowanie na Bedrocku udane!' + +# 3rd party features: Login Location Fix +login_location_fix: + fix_portal: '&aUtknąłeś w portalu podczas logowania.' + fix_underground: '&aUtknąłeś pod ziemią podczas logowania.' + cannot_fix_underground: '&aUtknąłeś pod ziemią podczas logowania, ale nie możemy tego naprawić.' + +# 3rd party features: Double Login Fix +double_login_fix: + fix_message: '&cZostałeś rozłączony z powodu podwójnego logowania.' diff --git a/plugin/platform-bukkit/src/main/resources/messages/messages_pt.yml b/plugin/platform-bukkit/src/main/resources/messages/messages_pt.yml new file mode 100644 index 00000000..d8352e51 --- /dev/null +++ b/plugin/platform-bukkit/src/main/resources/messages/messages_pt.yml @@ -0,0 +1,173 @@ +# List of global tags: +# %nl% - Goes to new line. +# %username% - Replaces the username of the player receiving the message. +# %displayname% - Replaces the nickname (and colors) of the player receiving the message. + +# Registration +registration: + disabled: '&cRegisto de novos utilizadores desactivado' + name_taken: '&cUtilizador já registado' + register_request: '&cPor favor registe-se com "/register "' + command_usage: '&cUse: /register ' + reg_only: '&fApenas jogadores registados podem entrar no servidor! Visite https://example.com para se registar' + success: '&cRegistado com sucesso!' + kicked_admin_registered: 'Um administrador registou-te, por favor entre novamente' + +# Password errors on registration +password: + match_error: '&fAs passwords não coincidem' + name_in_password: '&cNão pode o usar seu nome como senha, por favor, escolha outra ...' + unsafe_password: '&cA senha escolhida não é segura, por favor, escolha outra ...' + forbidden_characters: '&4Sua senha contém caracteres ilegais. Caracteres permitidos: %valid_chars' + wrong_length: '&fPassword demasiado curta ou longa! Por favor escolhe outra outra!' + pwned_password: '&cA senha escolhida não é segura. Ela foi usada %pwned_count vezes! Por favor, use uma senha forte...' + +# Login +login: + command_usage: '&cUse: /login ' + wrong_password: '&cPassword errada!' + success: '&bAutenticado com sucesso!' + login_request: '&cIdentifique-se com "/login "' + timeout_error: '&fExcedeu o tempo para autenticação' + +# Errors +error: + denied_command: '&cPara utilizar este comando é necessário estar logado!' + denied_chat: '&cPara usar o chat deve estar logado!' + unregistered_user: '&cUsername não registado' + not_logged_in: '&cNão autenticado!' + no_permission: '&cSem Permissões' + unexpected_error: '&fOcorreu um erro; Por favor contacte um administrador' + max_registration: '&cAtingiu o numero máximo de %reg_count contas registas, maximo de contas %max_acc' + logged_in: '&cJá se encontra autenticado!' + kick_for_vip: '&cUm jogador VIP entrou no servidor cheio!' + kick_unresolved_hostname: '&cOcorreu um erro: nome do servidor do jogador não resolvido!' + tempban_max_logins: '&cVocê foi temporariamente banido por falhar muitas vezes o login.' + +# AntiBot +antibot: + kick_antibot: 'Modo de protecção anti-Bot está habilitado! Tem que espere alguns minutos antes de entrar no servidor.' + auto_enabled: '[AuthMe] AntiBotMod activado automaticamente devido a um aumento anormal de tentativas de ligação!' + auto_disabled: '[AuthMe] AntiBotMod desactivado automaticamente após %m minutos, esperamos que a invasão tenha parado' + +# Unregister +unregister: + success: '&cRegisto eliminado com sucesso!' + command_usage: '&cUse: /unregister ' + +# Other messages +misc: + account_not_activated: '&fA sua conta não foi ainda activada, verifique o seu email onde irá receber indicações para activação de conta. ' + not_activated: '&cConta não ativada, por favor, registre-se e ative antes de tentar novamente.' + password_changed: '&cPassword alterada!' + logout: '&cSaida com sucesso' + reload: '&fConfiguração e base de dados foram recarregadas' + usage_change_password: '&fUse: /changepassword ' + accounts_owned_self: 'Você possui %count contas:' + accounts_owned_other: 'O jogador %name possui %count contas:' + +# Session messages +session: + valid_session: '&cSessão válida' + invalid_session: '&fDados de sessão não correspondem. Por favor aguarde o fim da sessão' + +# Error messages when joining +on_join_validation: + same_ip_online: 'Um jogador com o mesmo IP já está em jogo!' + same_nick_online: '&fO mesmo nickname já se encontra a jogar no servidor' + name_length: '&cO seu nick é demasiado curto ou muito longo.' + characters_in_name: '&cO seu nickname contém caracteres não permitidos. Permitido: %valid_chars' + kick_full_server: '&cO servidor está actualmente cheio, lamentamos!' + country_banned: 'O seu país está banido deste servidor' + not_owner_error: 'Não é o proprietário da conta. Por favor, escolha outro nome!' + invalid_name_case: 'Deve se juntar usando nome de usuário %valid, não %invalid.' + quick_command: 'Você usou o comando demasiado rapido por favor re-entre no servidor e aguarde antes de digitar qualquer comando.' + +# Email +email: + add_email_request: '&cPor favor adicione o seu email com : /email add ' + usage_email_add: '&fUse: /email add ' + usage_email_change: '&fUse: /email change ' + new_email_invalid: 'Novo email inválido!' + old_email_invalid: 'Email antigo inválido!' + invalid: 'Email inválido!' + added: 'Email adicionado com sucesso!' + add_not_allowed: '&cAdicionar e-mail não é permitido' + request_confirmation: 'Confirme o seu email!' + changed: 'Email alterado com sucesso!' + change_not_allowed: '&cAlterar e-mail não é permitido' + email_show: '&2O seu endereço de email atual é &f%email' + no_email_for_account: '&2Você atualmente não tem um endereço de email associado a essa conta.' + already_used: '&4O endereço de e-mail já está sendo usado' + incomplete_settings: 'Erro: nem todas as definições necessarias para enviar email foram preenchidas. Por favor contate um administrador.' + send_failure: 'Não foi possivel enviar o email. Por favor contate um administrador.' + change_password_expired: 'Você não pode mais alterar a sua password usando este comando.' + email_cooldown_error: '&cUm email já foi enviado recentemente.Por favor, espere %time antes de enviar novamente' + +# Password recovery by email +recovery: + forgot_password_hint: '&cPerdeu a sua password? Para a recuperar escreva /email recovery ' + command_usage: '&fUse: /email recovery ' + email_sent: 'Nova palavra-passe enviada para o seu email!' + code: + code_sent: 'O codigo para redefinir a senha foi enviado para o seu e-mail.' + incorrect: 'O codigo de recuperação está incorreto! Use "/email recovery [email]" para gerar um novo' + tries_exceeded: 'Você excedeu o numero maximo de tentativas para introduzir o codigo de recuperação. Use "/email recovery [email]" para gerar um novo.' + correct: 'O codigo de recuperação foi introduzido corretamente!' + change_password: 'Por favor use o comando /email setpassword para mudar a password imediatamente.' + +# Captcha +captcha: + usage_captcha: '&cPrecisa digitar um captcha, escreva: /captcha %captcha_code' + wrong_captcha: '&cCaptcha errado, por favor escreva: /captcha %captcha_code' + valid_captcha: '&cO seu captcha é válido!' + captcha_for_registration: 'Para se registar tem de resolver o captcha primeiro, por favor use: /captcha %captcha_code' + register_captcha_valid: '&2Captcha Valido! Agora você pode te registar com /register' + +# Verification code +verification: + code_required: '&3Este codigo é sensivel e requer uma verificação por e-mail! Verifique na sua caixa de entrada e siga as instruções do e-mail.' + command_usage: '&cUso: /verification ' + incorrect_code: '&cCodigo incorreto, por favor digite "/verification " no chat, utilizando o codigo que recebeu no e-mail.' + success: '&2Sua identidade foi verificada! Agora você pode executar todos os comandos nesta sessão!' + already_verified: '&2Você já pode digitar todos os comandos sensiveis nesta sessão!' + code_expired: '&3Seu codigo expirou! Execute outro comando sensivel para obter um novo codigo!' + email_needed: '&3Para confirmar a sua identidade necessita de associar um endereço de e-mail!!' + +# Time units +time: + second: 'segundo' + seconds: 'segundos' + minute: 'minuto' + minutes: 'minutos' + hour: 'hora' + hours: 'horas' + day: 'dia' + days: 'dias' + +# Two-factor authentication +two_factor: + code_created: '&2O seu código secreto é o %code. Você pode verificá-lo a partir daqui %url' + confirmation_required: 'Por favor confirme seu codigo de 2 etapas com /2fa confirm ' + code_required: 'Por favor submita seu codigo de duas etapas com /2fa code ' + already_enabled: 'Autenticação de duas etapas já se encontra habilitada na sua conta!' + enable_error_no_code: 'Nenhuma chave 2fa foi gerada por você ou expirou. Por favor digite /2fa add' + enable_success: 'Autenticação de duas etapas habilitada com sucesso na sua conta' + enable_error_wrong_code: 'Codigo errado ou o codigo expirou. Por favor digite /2fa add' + not_enabled_error: 'Autenticação de duas etapas não está habilitada na sua conta. Digite /2fa add' + removed_success: 'Autenticação de duas etapas removida com sucesso da sua conta' + invalid_code: 'Codigo invalido!' + +# 3rd party features: Bedrock Auto Login +bedrock_auto_login: + success: '&aLogin automático no Bedrock bem-sucedido!' + +# 3rd party features: Login Location Fix +login_location_fix: + fix_portal: '&aVocê está preso no portal durante o login.' + fix_underground: '&aVocê está preso no subsolo durante o login.' + cannot_fix_underground: '&aVocê está preso no subsolo durante o login, mas não podemos consertar.' + +# 3rd party features: Double Login Fix +double_login_fix: + fix_message: '&cVocê foi desconectado devido a um login duplicado.' diff --git a/plugin/platform-bukkit/src/main/resources/messages/messages_ro.yml b/plugin/platform-bukkit/src/main/resources/messages/messages_ro.yml new file mode 100644 index 00000000..dc8d3b1b --- /dev/null +++ b/plugin/platform-bukkit/src/main/resources/messages/messages_ro.yml @@ -0,0 +1,172 @@ +# List of global tags: +# %nl% - Goes to new line. +# %username% - Replaces the username of the player receiving the message. +# %displayname% - Replaces the nickname (and colors) of the player receiving the message. + +# Registration +registration: + disabled: '&cInregistrarea in joc nu este activata!' + name_taken: '&cCineva este inregistrat cu acest nume!' + register_request: '&3Te rugam sa te inregistrezi folosind comanda "/register "' + command_usage: '&cFoloseste comanda: /register ' + reg_only: '&4Doar jucatori inregistrati pot intra pe server! Te rugam foloseste https://example.com pentru a te inregistra!' + success: '&2Te-ai inregistrat cu succes!' + kicked_admin_registered: 'Un administrator tocmai te-a inregistrat, te rog autentifica-te din nou.' + +# Password errors on registration +password: + match_error: '&cParolele nu corespund, verifica-le din nou!' + name_in_password: '&c&cNu poti folosi numele ca si parola. Te rugam sa folosesti alta parola...' + unsafe_password: '&cParola aleasa nu este sigura. Te rugam sa folosesti alta parola...' + forbidden_characters: '&4Parola ta contine caractere nepermise. Caractere permise: %valid_chars' + wrong_length: '&cParola ta este prea scurta pentru a te inregistra! Te rugam incearca alta parola!' + pwned_password: '&cParola aleasă nu este sigură. A fost folosită de %pwned_count ori! Vă rugăm să utilizați o parolă puternică...' + +# Login +login: + command_usage: '&cFoloseste comanda "/login " pentru a te autentifica.' + wrong_password: '&cParola gresita!' + success: '&2Te-ai autentificat cu succes!' + login_request: '&cTe rugam sa te autentifici folosind comanda: /login ' + timeout_error: '&4A expirat timpul de autentificare si ai fost dat afara de server, te rugam incearca din nou!' + +# Errors +error: + denied_command: '&cPentru a utiliza aceasta comanda trebuie sa fi autentificat!' + denied_chat: '&cPentru a utiliza chat-ul trebuie sa fi autentificat!' + unregistered_user: '&cAcest jucator nu este inregistrat!' + not_logged_in: '&cNu esti autentificat!' + no_permission: '&4Nu ai permisiunea de a efectua aceasta actiune!' + unexpected_error: '&4A aparut o eroare, te rugam contacteaza un administrator!' + max_registration: '&cAi depasit numarul maxim de inregistrari (%reg_count /%max_acc %reg_names) pentru conexiunea dvs.!' + logged_in: '&cEsti deja autentificat!' + kick_for_vip: '&3Un VIP a intrat pe server cand era plin!' + kick_unresolved_hostname: '&cA aparut o eroare: nume gazda nerezolvat!' + tempban_max_logins: '&cAi fost banat temporar deoarece ai esuat sa te autentifici de prea multe ori.' + +# AntiBot +antibot: + kick_antibot: 'Protectia AntiBot este activata! Trebuie sa astepti cateva minute pentru a intra pe server.' + auto_enabled: '&4[Protectie AntiBot] AntiBot-ul a fost activat din cauza numarului mare de conexiuni!' + auto_disabled: '&2[Protectie AntiBot] AntiBot-ul a fost dezactivat dupa %m minute!' + +unregister: + success: '&cTi-ai sters contul cu succes!' + command_usage: '&cFoloseste comanda: /unregister ' + +# Other messages +misc: + account_not_activated: '&cContul tau nu este activat, te rugam verifica-ti email-ul!' + not_activated: '&cContul nu este activat, vă rugăm să vă înregistrați și să îl activați înainte de a încerca din nou.' + password_changed: '&2Parola a fost schimbata cu succes!' + logout: '&2Te-ai deconectat cu succes!' + reload: '&2Configuratiile si baza de date s-au reincarcat corect!' + usage_change_password: '&cFoloseste comanda: /changepassword ' + accounts_owned_self: 'Detii %count conturi:' + accounts_owned_other: 'Jucatorul %name are %count conturi:' + +# Session messages +session: + valid_session: '&2Conectat datorita reconectarii sesiunii.' + invalid_session: '&cIP-ul tau a fost schimbat si sesiunea ta a expirat!' + +# Error messages when joining +on_join_validation: + same_ip_online: 'Un jucator cu acelasi IP este deja in joc!' + same_nick_online: '&4Acelasi nume apartine unui jucator care e deja pe server!' + name_length: '&4Numele tau este fie prea scurt, fie prea lung!' + characters_in_name: '&4Numele tau contine caractere ilegale. Caractere permise: %valid_chars' + kick_full_server: '&4Server-ul este plin, incearca mai tarziu!' + country_banned: '&4Tara ta este interzisa pe acest server!' + not_owner_error: 'Tu nu esti detinatorul acestui cont. Te rugam alege alt nume!' + invalid_name_case: 'Ar trebui sa intri cu numele %valid, nu %invalid' + quick_command: 'Ai folosit o comanda prea repede! Te rugam, intra din nou pe server si astepta mai mult inainte de a utiliza orice comanda.' + +# Email +email: + add_email_request: '&3Te rugam adaugati email-ul la contul tau folosind comanda "/email add "' + usage_email_add: '&cFoloseste comanda: /email add ' + usage_email_change: '&cFoloseste comanda: /email change ' + new_email_invalid: '&cNoul email este nevalid, incearca din nou!' + old_email_invalid: '&cEmail-ul vechi este nevalid, incearca din nou!' + invalid: '&cEmail-ul este nevalid, incearca din nou!' + added: '&2Email-ul a fost adaugat cu succes la contul tau!' + add_not_allowed: '&cAdaugarea email-ului nu a fost permisa.' + request_confirmation: '&cTe rugam sa confirmi adresa ta de email!' + changed: '&2Email-ul a fost schimbat cu succes!' + change_not_allowed: '&cModificarea email-ului nu a fost permisa.' + email_show: '&2Adresa ta curenta de email este: &f%email' + no_email_for_account: '&2Nu ai nici o adresa de email asociata cu acest cont.' + already_used: '&4Email-ul acesta este deja folosit de altcineva' + incomplete_settings: 'Eroare: Nu indeplinesti conditiile trimiterii unui email! Te rugam contacteaza un administrator.' + send_failure: 'Email-ul nu a putut fi trimis. Ta rugam contactatezi un administrator.' + change_password_expired: 'Nu mai iti poti schimba parola folosind aceasta comanda.' + email_cooldown_error: '&cAi primit deja un mail pentru schimbarea parolei. Trebuie sa astepti %time inainte de a trimite unul nou.' + +# Password recovery by email +recovery: + forgot_password_hint: '&3Ti-ai uitat parola? Te rugam foloseste comanda "/email recovery "' + command_usage: '&cFoloseste comanda: /email recovery ' + email_sent: '&2Email-ul de recuperarea a fost trimis cu succes! Te rugam sa verifici casuta de email!' + code: + code_sent: 'Un cod de recuperare a parolei a fost trimis catre email-ul tau.' + incorrect: 'Codul de recuperare nu este corect! Mai ai %count incercari ramase.' + tries_exceeded: 'Ai atins numarul maxim de incercari pentru introducerea codului de recuperare. Foloseste "/email recovery [email]" pentru a genera unul nou.' + correct: 'Cod de recuperare corect!' + change_password: 'Foloseste imediat comanda /email setpassword pentru a-ti schimba parola.' + +# Captcha +captcha: + usage_captcha: '&3Pentru a te autentifica, trebuie sa rezolvi un cod captcha, foloseste comanda: /captcha %captcha_code' + wrong_captcha: '&cCodul de verificare este gresit, te rugam foloseste comanda: /captcha %captcha_code' + valid_captcha: '&2Codul de verificare a fost scris corect!' + captcha_for_registration: 'Pentru a te inregistra, trebuie sa rezolvi mai intai un captcha, foloseste comanda: /captcha %captcha_code' + register_captcha_valid: '&2Captcha valid! Poti sa te inregistrezi acum cu /register' + +# Verification code +verification: + code_required: '&3Aceasta comanda este sensibila si necesita o verificare prin email! Verificati adresa de email si urmati instructiunile de pe email.' + command_usage: '&cFoloseste: /verification ' + incorrect_code: '&cCod incorect, te rugam sa foloseste "/verification " in chat, folosind codul pe care l-ai primit prin email' + success: '&2Identitatea ta a fost verificata! Acum poti executa toate comenzile in sesiunea curenta!' + already_verified: '&2Poti deja sa executi fiecare comanda sensibila in cadrul sesiunii curente!' + code_expired: '&3Codul tau a expirat! Executa o alta comanda sensibila pentru a obtine un cod nou!' + email_needed: '&3Pentru ati verifica identitatea, trebuie sa conectezi o adresa de email cu contul tau!!' + +# Time units +time: + second: 'secunda' + seconds: 'secunde' + minute: 'minut' + minutes: 'minute' + hour: 'ora' + hours: 'ore' + day: 'zi' + days: 'zile' + +# Two-factor authentication +two_factor: + code_created: '&2Codul tau secret este %code. Il poti scana de aici %url' + confirmation_required: 'Te rugam sa confirmi codul tau cu /2fa confirm ' + code_required: 'Te rugam sa trimiti codul tau de autentificare in doi pasi cu /2fa code ' + already_enabled: 'Autentificarea in doi pasi este deja activata pentru contul tau!' + enable_error_no_code: 'Nicio cheie 2fa nu a fost generata pentru tine sau a expirat. te rugam sa folosesti /2fa add' + enable_success: 'Activarea cu succes a autentificarii in doi pasi pentru contul tau' + enable_error_wrong_code: 'Cod gresit sau codul a expirat. Te rugam sa folosesti /2fa add' + not_enabled_error: 'Autentificarea in doi pasi nu este activata pentru contul tau. foloseste /2fa add' + removed_success: 'Eliminare cu succes a autentificarii in doi pasi de pe cont' + invalid_code: 'Cod invalid!' + +# 3rd party features: Bedrock Auto Login +bedrock_auto_login: + success: '&aAutentificare automată Bedrock reușită!' + +# 3rd party features: Login Location Fix +login_location_fix: + fix_portal: '&aSunteți blocat în portal în timpul autentificării.' + fix_underground: '&aSunteți blocat sub pământ în timpul autentificării.' + cannot_fix_underground: '&aSunteți blocat sub pământ în timpul autentificării, dar nu putem remedia.' + +# 3rd party features: Double Login Fix +double_login_fix: + fix_message: '&cAți fost deconectat din cauza unei autentificări duble.' diff --git a/plugin/platform-bukkit/src/main/resources/messages/messages_ru.yml b/plugin/platform-bukkit/src/main/resources/messages/messages_ru.yml new file mode 100644 index 00000000..42ac6320 --- /dev/null +++ b/plugin/platform-bukkit/src/main/resources/messages/messages_ru.yml @@ -0,0 +1,172 @@ +# List of global tags: +# %nl% - Goes to new line. +# %username% - Replaces the username of the player receiving the message. +# %displayname% - Replaces the nickname (and colors) of the player receiving the message. + +# Registration +registration: + disabled: '&cРегистрация отключена.' + name_taken: '&cИгрок с таким никнеймом уже зарегистрирован.' + register_request: '&3Регистрация: /reg <пароль> <повтор пароля>' + command_usage: '&cИспользование: /reg <пароль> <повтор пароля>' + reg_only: '&4Вход только для зарегистрированных! Посетите https://сайт_сервера.ru для регистрации.' + success: '&2Вы успешно зарегистрировались!' + kicked_admin_registered: 'Администратор зарегистрировал вас. Авторизуйтесь снова.' + +# Password errors on registration +password: + match_error: '&cПароли не совпадают.' + name_in_password: '&cНельзя использовать свой никнейм в качестве пароля.' + unsafe_password: '&cТакой пароль небезопасен.' + forbidden_characters: '&4Пароль содержит запрещённые символы. Разрешённые: %valid_chars' + wrong_length: '&cПароль слишком длинный/короткий.' + pwned_password: '&cВыбранный вами пароль небезопасен. Он уже был использован %pwned_count раз! Пожалуйста, используйте надежный пароль...' + +# Login +login: + command_usage: '&cИспользование: /login <пароль>' + wrong_password: '&cНеправильный пароль!' + success: '&2Вы успешно вошли!' + login_request: '&3Авторизация: /login <Пароль>' + timeout_error: '&4Время авторизации истекло.' + +# Errors +error: + denied_command: '&cНеобходимо авторизоваться для использования этой команды!' + denied_chat: '&cНеобходимо авторизоваться, чтобы писать в чат!' + unregistered_user: '&cИгрок с таким именем не зарегистрирован.' + not_logged_in: '&cВы ещё не вошли!' + no_permission: '&4Недостаточно прав.' + unexpected_error: '&cПроизошла ошибка. Свяжитесь с администратором.' + max_registration: '&cПревышено максимальное количество регистраций на сервере! (%reg_count/%max_acc %reg_names)' + logged_in: '&cВы уже авторизированы!' + kick_for_vip: '&3VIP-игрок зашёл на переполненный сервер.' + kick_unresolved_hostname: '&cПроизошла ошибка: неразрешенное имя узла игрока!' + tempban_max_logins: '&cВы временно заблокированы из-за большого количества неудачных попыток авторизоваться.' + +# AntiBot +antibot: + kick_antibot: 'Сработала защита против ботов! Необходимо подождать перед следующим входом на сервер.' + auto_enabled: '&4[AuthMe] AntiBot-режим включился из-за большого количества входов!' + auto_disabled: '&2[AuthMe] AntiBot-режим отключился спустя %m мин.' + +unregister: + success: '&cУчётная запись успешно удалена!' + command_usage: '&cИспользование: /unregister <пароль>' + +# Other messages +misc: + account_not_activated: '&cВаша уч. запись ещё не активирована. Проверьте электронную почту!' + not_activated: '&cАккаунт не активирован, пожалуйста, зарегистрируйтесь и активируйте его перед повторной попыткой.' + password_changed: '&2Ваш пароль изменён!' + logout: '&2Успешно вышли из системы!' + reload: '&6Конфигурация и база данных перезагружены.' + usage_change_password: '&cИспользование: /changepassword <пароль> <новый пароль>' + accounts_owned_self: 'У вас %count уч. записей:' + accounts_owned_other: 'У игрока %name %count уч. записей:' + +# Session messages +session: + valid_session: '&2Вы автоматически авторизовались!' + invalid_session: '&cСессия некорректна. Дождитесь, пока она закончится.' + +# Error messages when joining +on_join_validation: + same_ip_online: 'Игрок с данным IP-адресом уже играет на сервере!' + same_nick_online: '&4Игрок с данным никнеймом уже играет на сервере!' + name_length: '&4Ваш никнейм слишком длинный/короткий.' + characters_in_name: '&4Ваш никнейм содержит запрещённые символы. Разрешённые: %valid_chars' + kick_full_server: '&4Сервер полон. Попробуйте зайти позже!' + country_banned: '&4Вход с IP-адресов вашей страны запрещён на этом сервере.' + not_owner_error: 'Вы не являетесь владельцем данной уч. записи. Выберите себе другой никнейм!' + invalid_name_case: 'Неверный никнейм! Зайдите под никнеймом %valid, а не %invalid.' + quick_command: 'Вы вводили команды слишком часто! Пожалуйста, переподключитесь и вводите команды медленнее.' + +# Email +email: + add_email_request: '&3Добавьте электронную почту: /email add <эл. почта> <повтор эл. почты>' + usage_email_add: '&cИспользование: /email add <эл. почта> <повтор эл. почты>' + usage_email_change: '&cИспользование: /email change <эл. почта> <новая эл. почта>' + new_email_invalid: '&cНедействительная новая электронная почта!' + old_email_invalid: '&cНедействительная старая электронная почта!' + invalid: '&cНедействительный адрес электронной почты!' + added: '&2Электронная почта успешно добавлена!' + add_not_allowed: '&cДобавление электронной почты не было разрешено.' + request_confirmation: '&cПодтвердите свою электронную почту!' + changed: '&2Адрес электронной почты изменён!' + change_not_allowed: '&cИзменение электронной почты не было разрешено.' + email_show: '&2Текущий адрес электронной почты — &f%email' + no_email_for_account: '&2К вашей уч. записи не привязана электронная почта.' + already_used: '&4Эта электронная почта уже используется.' + incomplete_settings: 'Ошибка: не все необходимые параметры установлены для отправки электронной почты. Свяжитесь с администратором.' + send_failure: 'Письмо не может быть отправлено. Свяжитесь в администратором.' + change_password_expired: 'Больше нельзя сменить свой пароль, используя эту команду.' + email_cooldown_error: '&cПисьмо было отправлено недавно. Подождите %time, прежде чем отправить новое.' + +# Password recovery by email +recovery: + forgot_password_hint: '&Забыли пароль? Используйте «/email recovery <эл. почта>».' + command_usage: '&cИспользование: /email recovery <эл. почта>' + email_sent: '&2Письмо с инструкциями для восстановления было отправлено на вашу электронную почту!' + code: + code_sent: 'Код восстановления для сброса пароля был отправлен на электронную почту.' + incorrect: 'Неверный код восстановления! Попыток осталось: %count.' + tries_exceeded: 'Вы слишком много раз неверно ввели код восстановления. Используйте «/email recovery [эл. почта]», чтобы получить новый код.' + correct: 'Код восстановления введён верно!' + change_password: 'Используйте «/email setpassword <новый пароль>», чтобы сменить свой пароль.' + +# Captcha +captcha: + usage_captcha: '&3Необходимо ввести текст с каптчи. Используйте «/captcha %captcha_code»' + wrong_captcha: '&cНеверно! Используйте «/captcha %captcha_code».' + valid_captcha: '&2Вы успешно решили каптчу!' + captcha_for_registration: 'Чтобы зарегистрироваться, решите каптчу используя команду: «/captcha %captcha_code»' + register_captcha_valid: '&2Вы успешно решили каптчу! Теперь вы можете зарегистрироваться командой «/register»' + +# Verification code +verification: + code_required: '&3Эта команда чувствительна и требует подтверждения электронной почты! Проверьте свою почту и следуйте инструкциям в письме.' + command_usage: '&cИспользование: /verification <код>' + incorrect_code: '&cНеверный код, используйте «/verification <код>», подставив код из полученного письма.' + success: '&2Ваша личность подтверждена! Теперь можно выполнять все чувствительные команды в текущем сеансе!' + already_verified: '&2Вы уже можете выполнять все чувствительные команды в текущем сеансе!' + code_expired: '&3Срок действия кода истёк! Выполните чувствительную команду, чтобы получить новый код!' + email_needed: '&3Чтобы подтвердить вашу личность, необходимо привязать электронную почту к учётной записи!!' + +# Time units +time: + second: 'с.' + seconds: 'с.' + minute: 'мин.' + minutes: 'мин.' + hour: 'ч.' + hours: 'ч.' + day: 'дн.' + days: 'дн.' + +# Two-factor authentication +two_factor: + code_created: '&2Ваш секретный код — %code. Просканируйте его здесь: %url' + confirmation_required: 'Пожалуйста, подтвердите ваш код с помощью /2fa confirm <код>' + code_required: 'Пожалуйста, введите ваш код двухфакторной аутентификации используя команду /2fa code <код>' + already_enabled: 'Двухфакторная аутентификация уже активирована для вашего аккаунта!' + enable_error_no_code: 'Код двухфакторной аутентификации не был сгенерирован или истек. Пожалуйста, введите /2fa add' + enable_success: 'Двухфакторная аутентификация для вашего аккаунта успешно подключена' + enable_error_wrong_code: 'Срок действия кода истек или код неверный. Введите /2fa add' + not_enabled_error: 'Двухфакторная аутентификация не включена для вашего аккаунта. Введите /2fa add' + removed_success: 'Двухфакторная аутентификация успешно удалена с вашего аккаунта!' + invalid_code: 'Неверный код!' + +# 3rd party features: Bedrock Auto Login +bedrock_auto_login: + success: '&aВы автоматически авторизовались потому что вы играете с Bedrock!' + +# 3rd party features: Login Location Fix +login_location_fix: + fix_portal: '&aВы застряли в портале во время авторизации.' + fix_underground: '&aВы застряли под землей во время авторизации.' + cannot_fix_underground: '&aВы застряли под землей во время авторизации, но мы не можем это исправить.' + +# 3rd party features: Double Login Fix +double_login_fix: + fix_message: '&cВы были отключены из-за двойного входа.' diff --git a/plugin/platform-bukkit/src/main/resources/messages/messages_si.yml b/plugin/platform-bukkit/src/main/resources/messages/messages_si.yml new file mode 100644 index 00000000..d046e480 --- /dev/null +++ b/plugin/platform-bukkit/src/main/resources/messages/messages_si.yml @@ -0,0 +1,173 @@ +# List of global tags: +# %nl% - Goes to new line. +# %username% - Replaces the username of the player receiving the message. +# %displayname% - Replaces the nickname (and colors) of the player receiving the message. + +# Registration +registration: + disabled: '&cRegistracija v igri je onemogočena!' + name_taken: '&cTo uporabniško ime ste ze registrirali!' + register_request: '&3Registrirajte se z ukazom "/register "' + command_usage: '&cUporaba: /register ' + reg_only: '&4Samo registrirani uporabniki se lahko povezejo! Obiscite https://example.com , da se registrirate!' + success: '&2Uspešno registriran!' + kicked_admin_registered: 'Administrator vas je registriral; prosimo, da se prijavite.' + +# Password errors on registration +password: + match_error: '&cGesli se ne ujemata, ponovno ju preverite!' + name_in_password: '&cSvoje uporabniško ime ne morete uporabiti kot geslo, izberite drugo...' + unsafe_password: '&cIzbrano geslo ni varno, izberite drugo...' + forbidden_characters: '&4Vaše geslo vsebuje nedovoljene znake. Dovoljeni znaki: %valid_chars' + wrong_length: '&cVaše geslo je prekratko ali predolgo! Poskusite z drugim!' + pwned_password: '&cIzbrano geslo ni varno. Uporabljeno je bilo že %pwned_count krat! Prosimo, uporabite močno geslo...' + +# Login +login: + command_usage: '&cUporaba: /login ' + wrong_password: '&cNapačno geslo!' + success: '&2Uspešna prijava!' + login_request: '&cPrijavi se z ukazom "/login "' + timeout_error: '&4Časovna omejitev prijave prekoračena, vrzeni ste bili s strežnika, poskusite ponovno!' + +# Errors +error: + denied_command: '&cZa uporabo ukazov se je potrebno prijaviti!' + denied_chat: '&cZa pogovor se je potrebno prijaviti!' + unregistered_user: '&cNi mogoče najti zahtevanega uporabnika v podatkovni bazi!' + not_logged_in: '&cNiste prijavljeni!' + no_permission: '&4Nimate dovoljenja za izvedbo tega dejanja!' + unexpected_error: '&4Prišlo je do nepričakovane napake, prosim kontaktirajte administratorja!' + max_registration: '&cPresegli ste največjo stevilo registracij (%reg_count/%max_acc %reg_names) za vašo povezavo!' + logged_in: '&cSte že povezani!' + kick_for_vip: '&3VIP igralec se je pridruzil serverju, ko je bil poln!' + kick_unresolved_hostname: '&cPrišlo je do napake: ime gostitelja igralca ni razrešeno!' + tempban_max_logins: '&cBil si začasno izločen zaradi preveč neuspešnih prijav.' + +# AntiBot +antibot: + kick_antibot: 'Počakajte nekaj minut preden se povezete na strežnik.' + auto_enabled: '&4[AntiBotService] AntiBot je bil aktiviran zaradi velikega števila prijav!' + auto_disabled: '&2[AntiBotService] AntiBot je bil deaktiviran po %m minut!' + +# Unregister +unregister: + success: '&cUsprešno deregistriran!' + command_usage: '&cUporaba: /unregister ' + +# Other messages +misc: + account_not_activated: '&cVas račun se ni aktiviran, preverite e-mail!' + not_activated: '&cRačun ni aktiviran, prosimo, registrirajte se in ga aktivirajte, preden poskusite znova.' + password_changed: '&2Geslo uspesno spremenjeno!' + logout: '&2Odjavljeni uspesno!' + reload: '&2Konfiguracija in baza podatkov sta bila uspesno osvezena!' + usage_change_password: '&cUporaba: /changepassword ' + accounts_owned_self: 'You own %count accounts:' + accounts_owned_other: 'The player %name has %count accounts:' + +# Session messages +session: + valid_session: '&2Uspešna povezava.' + invalid_session: '&cVas IP je bil spremenjen in vasa seja je potekla!' + +# Error messages when joining +on_join_validation: + same_ip_online: 'Igralec z enakim IP-jem kot vi je že v igri.' + same_nick_online: '&4Uporabnik z enakim imenom ze igra!' + name_length: '&4Vaše uporabniško ime je prekratko ali predolgo!' + characters_in_name: '&4Vaše uporabniško ime vsebuje nedovoljene znake. Dovoljeni znaki: %valid_chars' + kick_full_server: '&4Streznik je trenutno poln!' + country_banned: '&4Vaša država je prepovedana na strežniku!' + not_owner_error: 'Niste lastnik tega uporabniškega računa. Izberite drugo uporabnisko ime!' + invalid_name_case: 'Povežite se z uporabo uporabniskega imena %valid in ne %invalid.' + quick_command: 'Komando ste uporabili prehitro! Prosimo, da se ponovno povežete in počakate pred ponovno uporabo komand.' + +# Email +email: + add_email_request: '&3Dodajte email vašemu računu z ukazom "/email add "' + usage_email_add: '&cUporaba: /email add ' + usage_email_change: '&cUporaba: /email change ' + new_email_invalid: '&cNeveljaven nov email, poskusite ponovno!' + old_email_invalid: '&cNeveljaven star email, poskusite ponovno!' + invalid: '&cNapačni email!' + added: '&2Email uspešno dodan!' + add_not_allowed: '&cDodajanje emailov ni dovoljeno!' + request_confirmation: '&cPotrdite vaš email naslov.' + changed: '&2Email uspešno spremenjen!' + change_not_allowed: '&cSpreminjanje emailov ni dovoljeno.' + email_show: '&2Vaš trenuten email: &f%email' + no_email_for_account: '&2Trenutno nimate povezanega emaila.' + already_used: '&4Ta email naslov je že v uporabi!' + incomplete_settings: 'Napaka: Administrator ni nastavil vseh nastavitev za pošiljanje emailov. Prosimo, da ga obvestite.' + send_failure: 'Emaila ni bilo mogoče poslati. Prosimo obvestite administratorja.' + change_password_expired: 'Prosim uporabite novi ukaz.' + email_cooldown_error: '&cSporočilo je bilo že poslano. Pocakajte %time preden zahtevate novega!' + +# Password recovery by email +recovery: + forgot_password_hint: '&3Ste pozabili vaše geslo? Uporabite ukaz "/email recovery "' + command_usage: '&cUporaba: /email recovery ' + email_sent: '&2Email za obnovitev uspešno poslan! Preverite prejeta email sporočila! Preverite tudi Spam/Vsiljeno pošto!' + code: + code_sent: 'Koda za ponastavitev gesla je bila poslana na vaš email.' + incorrect: 'Koda je nepravilna! Uporabite /email recovery [Email], da pridobite novo kodo.' + tries_exceeded: 'Prekoračili ste število poskusov! Uporabite /email recovery [Email], da pridobite novo kodo.' + correct: 'Uspešno vnesena koda!' + change_password: 'Prosim uporabite /email setpassword za nastavitev novega gesla.' + +# Captcha +captcha: + usage_captcha: '&3Da se prijavite rešite captcha kodo, uporabite ukaz "/captcha %captcha_code"' + wrong_captcha: '&cNapačna captcha, vpišite "/captcha %captcha_code" v chat!' + valid_captcha: '&2Captcha pravilno rešena!' + captcha_for_registration: 'Za registracijo morate prvo rešiti captcha. Uporabite: /captcha %captcha_code' + register_captcha_valid: '&2Pravilna captcha! Sedaj se lahko registrirate z /register' + +# Verification code +verification: + code_required: '&3Ta ukaz je lahko tvegan in zahteva preverjanje preko emaila. Preverite email za dodatna navodila.' + command_usage: '&cUporabite: /verification ' + incorrect_code: '&cIncorrect code, please type "/verification " into the chat, using the code you received by email' + success: '&2Your identity has been verified! You can now execute all commands within the current session!' + already_verified: '&2You can already execute every sensitive command within the current session!' + code_expired: '&3Your code has expired! Execute another sensitive command to get a new code!' + email_needed: '&3To verify your identity you need to link an email address with your account!' + +# Time units +time: + second: 'sekunda' + seconds: 'sekund' + minute: 'minuta' + minutes: 'minut' + hour: 'ur' + hours: 'ure' + day: 'dan' + days: 'dni' + +# Two-factor authentication +two_factor: + code_created: '&2Vasa skrivna koda je %code. Lahko je skenirate tu %url!' + confirmation_required: 'Prosimo, da potrdite svojo dvo stopično kodo z /2fa confirm ' + code_required: 'Prosimo, da pošljete svojo dvo stopično kodo z /2fa code ' + already_enabled: 'Dvo stopična prijava je že vključena za vaš račun!' + enable_error_no_code: 'Nobeden dvo stopični ključ ni bil generiran za vaš račun ali pa je potekel. Uporabite /2fa add' + enable_success: 'Usprešno ste vključili dvo stopično prijavo za vaš račun.' + enable_error_wrong_code: 'Napačna koda ali pa je potekla. Uporabite /2fa add' + not_enabled_error: 'Dvo stopična prijava ni vključena za vaš račun. Uporabite /2fa add' + removed_success: 'Usprešno ste odstranili dvo stopično prijavo za vaš račun.' + invalid_code: 'Nepravilna koda!' + +# 3rd party features: Bedrock Auto Login +bedrock_auto_login: + success: '&aSamodejno prijavljanje v Bedrocku uspešno!' + +# 3rd party features: Login Location Fix +login_location_fix: + fix_portal: '&aMed prijavo ste obtičali v portalu.' + fix_underground: '&aMed prijavo ste obtičali pod zemljo.' + cannot_fix_underground: '&aMed prijavo ste obtičali pod zemljo, vendar tega ne moremo popraviti.' + +# 3rd party features: Double Login Fix +double_login_fix: + fix_message: '&cBili ste odklopljeni zaradi podvojenega prijave.' diff --git a/plugin/platform-bukkit/src/main/resources/messages/messages_sk.yml b/plugin/platform-bukkit/src/main/resources/messages/messages_sk.yml new file mode 100644 index 00000000..fd40f0de --- /dev/null +++ b/plugin/platform-bukkit/src/main/resources/messages/messages_sk.yml @@ -0,0 +1,179 @@ +# Slovak translate by: Judzi 2.2.2013 # +# Next Translation & Correction by: # +# Thymue 7.8.2017 # +# in future there can be more translators # +# if they are not listed here # +# check Translators on GitHub Wiki. # +# List of global tags: +# %nl% - Goes to new line. +# %username% - Replaces the username of the player receiving the message. +# %displayname% - Replaces the nickname (and colors) of the player receiving the message. + +# Registration +registration: + disabled: '&cRegistrácia nie je povolená.' + name_taken: '&cZadané meno je už zaregistrované.' + register_request: '&cZaregistruj sa príkazom "/register ".' + command_usage: '&cPoužitie: /register ' + reg_only: '&fIba zaregistrovaný hráči sa môžu pripojiť na tento server! Navštív https://example.com pre registráciu.' + success: '&cBol si úspešne zaregistrovaný.' + kicked_admin_registered: 'Admin ťa zaregistroval. Prosím, prihlás sa znovu.' + +# Password errors on registration +password: + match_error: '&fHeslá sa nezhodujú.' + name_in_password: '&cNemôžeš použiť tvoje meno ako heslo. Prosím, zvoľ si iné...' + unsafe_password: '&cTvoje heslo nieje bezpečné. Prosím, zvoľ si iné...' + forbidden_characters: '&4Tvoje heslo obsahuje zakázané znaky. Povolené znaky: %valid_chars' + wrong_length: '&fHeslo je veľmi krátke alebo veľmi dlhé.' + pwned_password: '&cVaše zvolené heslo nie je bezpečné. Bolo použité %pwned_count krát! Prosím, použite silné heslo...' + +# Login +login: + command_usage: '&cPoužitie: /login ' + wrong_password: '&cZadal si zlé heslo.' + success: '&cBol si úspešne prihlásený!' + login_request: '&cPrihlás sa príkazom "/login ".' + timeout_error: '&fVypršal čas na prihlásenie, pripoj sa a skús to znovu.' + +# Errors +error: + denied_command: '&cPre použitie tohto príkazu sa musíš prihlásiť!' + denied_chat: '&cMusíš byť prihlásený ak chceš použiť chat!' + unregistered_user: '&cZadané meno nie je zaregistrované!' + not_logged_in: '&cEšte nie si prihlásený!' + no_permission: '&cNemáš dostatočné práva na vykonanie tejto činnosti.' + unexpected_error: '&fNastala chyba, prosím kontaktuj Administrátora.' + max_registration: '&fPrekročil si maximum registrovaných účtov(%reg_count/%max_acc|%reg_names).' + logged_in: '&cAktuálne si už prihlásený!' + kick_for_vip: '&3Uvoľnil si miesto pre VIP hráča!' + kick_unresolved_hostname: '&cDošlo k chybe: nerozpoznaný hostname hráča!' + tempban_max_logins: '&cBol si dočasne zabanovaný za opakované zadanie zlého hesla.' + +# AntiBot +antibot: + kick_antibot: 'AntiBot je zapnutý! Musíš počkať niekoľko minút pred znovupripojením sa na server.' + auto_enabled: '&4[AntiBotService] AntiBot bol zapnutý kôli masívnym pokusom o pripojenie!' + auto_disabled: '&2[AntiBotService] AntiBot bol vypnutý po %m minútach!' + +# Unregister +unregister: + success: '&cÚčet bol vymazaný!' + command_usage: '&cPoužitie: /unregister heslo' + +# Other messages +misc: + account_not_activated: '&fTvoj účet nie je aktivovaný. Prezri si svoj e-mail!' + not_activated: '&cÚčet nie je aktivovaný, prosím, zaregistrujte sa a aktivujte ho pred pokusom o prihlásenie znova.' + password_changed: '&cHeslo zmenené!' + logout: '&cBol si úspešne odhlásený.' + reload: '&fZnovu načítanie konfigurácie a databázy bolo úspešné.' + usage_change_password: '&fPoužitie: /changepassword ' + accounts_owned_self: 'Vlastníš tieto účty(%count): ' + accounts_owned_other: 'Hráč %name vlastní tieto účty(%count): ' + +# Session messages +session: + valid_session: '&cAutomatické prihlásenie z dôvodu pokračovania relácie.' + invalid_session: '&fTvoja IP sa zmenila a tvoje prihlásenie(relácia) vypršalo(/a).' + +# Error messages when joining +on_join_validation: + same_ip_online: 'Hráč s tvojou IP už hrá na tomto serveri!' + same_nick_online: '&fHráč s týmto nickom už hrá!' + name_length: '&cTvoje meno je veľmi krátke alebo veľmi dlhé.' + characters_in_name: '&cTvoje meno obsahuje zakázané znaky. Povolené znaky: %valid_chars' + kick_full_server: '&4Server je plný, skús to znovu neskôr!' + country_banned: '&4Tvoja krajina je zabanovaná na tomto serveri!' + not_owner_error: 'Nie si majiteľom tohto účtu. Prosím zvoľ si iné meno!' + invalid_name_case: 'Mal by si sa pripojiť s nickom %valid, nie %invalid - pozor na veľké a malé písmená.' + quick_command: 'Príkaz ste použili príliš rýchlo! Prosím, znovu sa pripojte na server a počkajte dlhšie pred použitím akéhokoľvek príkazu.' + +# Email +email: + add_email_request: '&cPridaj svoj e-mail príkazom "/email add ".' + usage_email_add: '&cPoužitie: /email add ' + usage_email_change: '&cPoužitie: /email change ' + new_email_invalid: '&cNeplatný nový email, skús to znovu!' + old_email_invalid: '&cNeplatný starý email, skús to znovu!' + invalid: '&cNeplatná emailová adresa, skús to znovu!' + added: '&2Emailová adresa bola úspešne pridaná k tvojmu účtu!' + add_not_allowed: '&cPridanie emailu nebolo povolené.' + request_confirmation: '&cProsím potvrď svoju emailovú adresu!' + changed: '&2Emailová adresa bola úspešne zmenená!' + change_not_allowed: '&cZmena emailu nebola povolená.' + email_show: '&2Tvoja súčastná emailová adresa je: &f%email' + no_email_for_account: '&2Momentálne nemáš emailovú adresu spojenú s týmto účtom.' + already_used: '&4Túto emailovú adresu už niekto používa.' + incomplete_settings: 'Chyba: nie všetky potrebné nastavenia sú nastavené pre posielanie emailov. Prosím kontaktuj Administrátora.' + send_failure: 'Email nemohol byť poslaný. Prosím kontaktuj Administrátora.' + change_password_expired: 'Už nemôžeš zmeniť svoje heslo týmto príkazom.' + email_cooldown_error: '&cEmail bol nedávno poslaný. Musíš počkať %time predtým ako ti pošleme nový.' + +# Password recovery by email +recovery: + forgot_password_hint: '&cZabudol si heslo? Použi príkaz /email recovery ' + command_usage: '&cPoužitie: /email recovery ' + email_sent: '&2Email na obnovenie bol úspešne odoslaný! Prosím skontroluj si svoju emailovú schránku!' + code: + code_sent: 'Kód na obnovenie účtu bol poslaný na tvoj e-mail.' + incorrect: 'Nesprávny kód! Zostávajúce pokusy: %count.' + tries_exceeded: 'Prekročil si maximálny počet pokusov o zadanie kódu pre obnovenie účtu. Použi "/email recovery [email]" pre vygenerovanie nového kódu.' + correct: 'Správny kód!' + change_password: 'Prosím hneď použi príkaz /email setpassword pre zmenenie tvojeho hesla.' + +# Captcha +captcha: + usage_captcha: '&3Pre prihlásenie musíš vyriešiť captcha kód, prosím použi príkaz: /captcha %captcha_code' + wrong_captcha: '&cNesprávny kód captcha, prosím napíš "/captcha %captcha_code" do chatu!' + valid_captcha: '&2Správne si vyriešil captcha kód!' + captcha_for_registration: 'Ak sa chcete zaregistrovať, musíte najskôr vyriešiť captcha, prosím, použite príkaz: /captcha %captcha_code' + register_captcha_valid: '&2Platná captcha! Teraz sa môžete zaregistrovať pomocou /register' + +# Verification code +verification: + code_required: '&3Tento príkaz je citlivý a vyžaduje overenie emailom! Skontrolujte svoju schránku a postupujte podľa pokynov v emaile.' + command_usage: '&cPoužitie: /verification ' + incorrect_code: '&cNesprávny kód, prosím, zadajte "/verification " do chatu, pomocou kódu, ktorý ste dostali emailom' + success: '&2Vaša identita bola overená! Teraz môžete vykonávať všetky príkazy v rámci aktuálnej relácie!' + already_verified: '&2Už môžete vykonávať každý citlivý príkaz v rámci aktuálnej relácie!' + code_expired: '&3Váš kód vypršal! Vykonajte ďalší citlivý príkaz na získanie nového kódu!' + email_needed: '&3Na overenie vašej identity potrebujete prepojiť emailovú adresu so svojím účtom!!' + +# Time units +time: + second: 'sek.' + seconds: 'sek.' + minute: 'min.' + minutes: 'min.' + hour: 'hod.' + hours: 'hod.' + day: 'd.' + days: 'd.' + +# Two-factor authentication +two_factor: + code_created: '&2Tvoj tajný kód je %code. Môžeš ho oskenovať tu: %url' + confirmation_required: 'Prosím, potvrďte svoj kód pomocou /2fa confirm ' + code_required: 'Prosím, zadajte svoj dvojfaktorový autentifikačný kód pomocou /2fa code ' + already_enabled: 'Dvojfaktorová autentifikácia je už pre váš účet povolená!' + enable_error_no_code: 'Nebolo pre vás vygenerované žiadne 2fa kľúč alebo vypršal jeho platnosť. Prosím, použite príkaz /2fa add' + enable_success: 'Dvojfaktorová autentifikácia bola úspešne povolená pre váš účet' + enable_error_wrong_code: 'Nesprávny kód alebo kód vypršal. Prosím, použite príkaz /2fa add' + not_enabled_error: 'Dvojfaktorová autentifikácia nie je pre váš účet povolená. Použite príkaz /2fa add' + removed_success: 'Dvojfaktorová autentifikácia bola úspešne odstránená z vášho účtu' + invalid_code: 'Neplatný kód!' + +# 3rd party features: Bedrock Auto Login +bedrock_auto_login: + success: '&aAutomatické prihlásenie do Bedrocku úspešné!' + +# 3rd party features: Login Location Fix +login_location_fix: + fix_portal: '&aPočas prihlasovania ste uviazli v portáli.' + fix_underground: '&aPočas prihlasovania ste uviazli pod zemou.' + cannot_fix_underground: '&aPočas prihlasovania ste uviazli pod zemou, ale nemôžeme to opraviť.' + +# 3rd party features: Double Login Fix +double_login_fix: + fix_message: '&cBoli ste odpojení kvôli zdvojenému prihláseniu.' diff --git a/plugin/platform-bukkit/src/main/resources/messages/messages_sr.yml b/plugin/platform-bukkit/src/main/resources/messages/messages_sr.yml new file mode 100644 index 00000000..e143f6b2 --- /dev/null +++ b/plugin/platform-bukkit/src/main/resources/messages/messages_sr.yml @@ -0,0 +1,173 @@ +# Lista globalnih tagova: +# %nl% - Nova linija. +# %username% - Zamenjuje ime igrača kome se šalje poruka. +# %displayname% - Zamenjuje nadimak (i boje) igrača kome se šalje poruka. + +# Registration +registration: + disabled: '&cRegistracija u igri je isključena!' + name_taken: '&cKorisnik je već registrovan!' + register_request: '&3Molimo Vas, registrujte se na server komandom: /register ' + command_usage: '&cUpotreba: /register ' + reg_only: '&4Samo registrovani igrači mogu ući na server! Molimo Vas posetite https://example.com da biste se registrovali!' + success: '&2Uspešno registrovani!' + kicked_admin_registered: 'Admin vas je upravo registrovao; molimo Vas uđite ponovo' + +# Password errors on registration +password: + match_error: '&cLozinke se nisu složile, proverite ih ponovo!' + name_in_password: '&cNe možete koristiti svoje ime za lozinku, molimo vas da izaberete drugu...' + unsafe_password: '&cIzabrana lozinka nije bezbedna, molimo vas da izaberete drugu...' + forbidden_characters: '&4Vaša lozinka sadrži nedozvoljene karaktere. Dozvoljeni karakteri: %valid_chars' + wrong_length: '&cVaša lozinka je prekratka ili predugačka! Molimo Vas da probate drugu!' + pwned_password: '&cVaša izabrana lozinka nije sigurna. Već je korišćena %pwned_count puta! Molimo koristite jaku lozinku...' + +# Login +login: + command_usage: '&cUpotreba: /login ' + wrong_password: '&cPogrešna lozinka!' + success: '&2Uspešno ste se ulogovali!' + login_request: '&cMolimo Vas, ulogujte se komandom: /login ' + timeout_error: '&4Vreme za login isteklo, izbačeni ste sa servera, molimo Vas da pokušate ponovo!' + +# Errors +error: + denied_command: '&cDa biste koristili komande morate se autentifikovati!' + denied_chat: '&cDa biste koristili čet morate se autentifikovati!' + unregistered_user: '&cKorisnik nije registrovan!' + not_logged_in: '&cNiste ulogovani!' + no_permission: '&4Nemate dovoljno dozvola da uradite to!' + unexpected_error: '&4Pojavila se neočekivana greška, molimo Vas kontaktirajte administratora!' + max_registration: '&cDostigli ste maksimalan broj registracija (%reg_count/%max_acc %reg_names) za vaše povezivanje!' + logged_in: '&cVeć ste ulogovani!' + kick_for_vip: '&3VIP igrač je ušao na server dok je bio pun!' + kick_unresolved_hostname: '&cDošlo je do greške: nerešeno ime domaćina igrača!' + tempban_max_logins: '&cPrivremeno ste banovani zbog previše pogrešnih pokušaja ulogovanja.' + +# AntiBot +antibot: + kick_antibot: 'AntiBot mod zaštite je aktiviran! Morate sačekati par minuta pre povezivanja na server.' + auto_enabled: '&4[AntiBotSlužba] AntiBot je aktiviran zbog prevelikog broja povezivanja!' + auto_disabled: '&2[AntiBotSlužba] AntiBot je isključen posle %m minut(a)!' + +# Unregister +unregister: + success: '&cUspešno odregistrovani!' + command_usage: '&cUpotreba: /unregister ' + +# Other messages +misc: + account_not_activated: '&cVaš nalog još uvek nije aktiviran, molimo Vas proverite svoj email!' + not_activated: '&cNalog nije aktiviran, molimo registrujte se i aktivirajte pre nego što pokušate ponovo.' + password_changed: '&2Lozinka uspešno promenjena!' + logout: '&2Uspešno ste se odlogovali!' + reload: '&2Konfiguracija i databaza su uspešno osveženi!' + usage_change_password: '&cUpotreba: /changepassword ' + accounts_owned_self: 'Vi imate %count naloga:' + accounts_owned_other: 'Igrač %name ima %count naloga:' + +# Session messages +session: + valid_session: '&2Ulogovani ste zbog ponovnog povezivanja sesije.' + invalid_session: '&cVaš IP je promenjen i vaši podaci o sesiji su istekli!' + +# Error messages when joining +on_join_validation: + same_ip_online: 'Igrač sa istim IP-em je već u igri!' + same_nick_online: '&4Isto korisničko ime već igra na serveru!' + name_length: '&4Vaše korisničko ime je prekratko ili predugačko!' + characters_in_name: '&4Vaše korisničko ime sadrži nedozvoljene karaktere. Dozvoljeni karakteri: %valid_chars' + kick_full_server: '&4Server je pun, probajte ponovo kasnije!' + country_banned: '&4Vaša država je banovana sa ovog servera!' + not_owner_error: 'Ne posedujete ovaj nalog. Molimo Vas izaberite drugo ime!' + invalid_name_case: 'Morate ući sa korisničkim imenom %valid, umesto %invalid.' + quick_command: 'Iskoristili ste komandu previše brzo! Molimo Vas, uđite opet na server i sačekajte malo pre korišćenja komandi.' + +# Email +email: + add_email_request: '&3Molimo Vas dodajte email adresu na vaš nalog komandom: /email add ' + usage_email_add: '&cUpotreba: /email add ' + usage_email_change: '&cUpotreba: /email change ' + new_email_invalid: '&cNevažeći email, pokušajte ponovo!' + old_email_invalid: '&cNevažeći stari email, pokušajte ponovo!' + invalid: '&cNevažeći email, pokušajte ponovo!' + added: '&2Email adresa je uspešno dodata na vaš nalog!' + add_not_allowed: '&cDodavanje emaila nije dozvoljeno' + request_confirmation: '&cMolimo Vas podvrdite svoju email adresu!' + changed: '&2Email adresa uspešno promenjena!' + change_not_allowed: '&cPromena emaila nije dozvoljena' + email_show: '&2Vaša email adresa je: &f%email' + no_email_for_account: '&2Trenutno nemate email adresu povezanu sa svojim nalogom.' + already_used: '&4Email adresa je već u upotrebi' + incomplete_settings: 'Greška: nisu postavljena potrebna podešavanja za slanje emaila. Molimo Vas da kontaktirate admina.' + send_failure: 'Email se nije mogao poslati. Molimo Vas da kontaktirate administratora.' + change_password_expired: 'Ne možete više promeniti svoju lozinku koristeći ovu komandu.' + email_cooldown_error: '&cEmail je već poslat. Morate sačekati %time pre nego što možete poslati drugi.' + +# Password recovery by email +recovery: + forgot_password_hint: '&3Zaboravili ste lozinku? Molimo Vas ukucajte komandu: /email recovery ' + command_usage: '&cUpotreba: /email recovery ' + email_sent: '&2Email za povratak je poslat uspešno! Molimo Vas proverite inbox svog emaila!' + code: + code_sent: 'Povratni kod za resetovanje vaše lozinke je poslat na vaš email.' + incorrect: 'Povratni kod nije tačan! Imate još %count pokušaja.' + tries_exceeded: 'Dostigli ste maksimalni broj pokušaja povratka sa kodom. Koristite "/email recovery [email]" da generišite nov.' + correct: 'Povratni kod uspešno unesen!' + change_password: 'Molimo Vas iskoristite komandu /email setpassword da odmah promenite svoju lozinku.' + +# Captcha +captcha: + usage_captcha: '&3Da biste se ulogovali morate rešiti captcha kod, molimo Vas iskoristite komandu: /captcha %captcha_code' + wrong_captcha: '&cPogrešna captcha, molimo Vas kucajte "/captcha %captcha_code" u četu!' + valid_captcha: '&2Captcha kod uspešno unesen!' + captcha_for_registration: 'Da biste se registrovali morate rešiti captcha kod, molimo Vas iskoristite komandu: /captcha %captcha_code' + register_captcha_valid: '&2Ispravna captcha! Sada se možete registrovati koristeći /register' + +# Verification code +verification: + code_required: '&3Ova komanda je osetljiva i zahteva verifikaciju emaila! Molimo Vas proverite svoj inbox i pratite instrukcije u emailu.' + command_usage: '&cUpotreba: /verification ' + incorrect_code: '&cNeispravan kod, molimo Vas unesite "/verification " u čet, koristeći kod koji ste dobili u emailu' + success: '&2Vaš identitet je verifikovan! Sada možete koristiti sve komande u trenutnoj sesiji!' + already_verified: '&2Već možete koristiti sve osetljive komande u trenutnoj sesiji!' + code_expired: '&3Vaš kod je istekao! Iskoristite drugu osetljivu komandu da dobijete kod!' + email_needed: '&3Da biste verifikovali svoj identitet morate da povežete svoju email adresu sa svojim nalogom!!' + +# Time units +time: + second: 'sekund' + seconds: 'sekundi' + minute: 'minut' + minutes: 'minuta' + hour: 'čas' + hours: 'časova' + day: 'dan' + days: 'dana' + +# Two-factor authentication +two_factor: + code_created: '&2Vaš tajni kod je %code. Možete ga skenirati sa %url' + confirmation_required: 'Molimo Vas potvrdite svoj kod koristeći /2fa confirm ' + code_required: 'Molimo Vas unesite svoj autentifikacioni kod dva-faktora koristeći /2fa code ' + already_enabled: 'Autentifikacioni dupli-faktor je već aktiviran za vaš nalog!' + enable_error_no_code: 'Nijedan 2fa ključ nije generisan za vas ili je istekao. Molimo Vas kucajte /2fa add' + enable_success: 'Uspešno ste aktivirali dvo-faktornu autentifikaciju za vaš nalog' + enable_error_wrong_code: 'Pogrešan kod ili je kod istekao. Molimo Vas kucajte /2fa add' + not_enabled_error: 'Dvo-faktorna autentifikacija nije aktivirana za vaš nalog. Kucajte /2fa add' + removed_success: 'Uspešno ste uklonili dvo-faktornu autentifikaciju sa vašeg naloga' + invalid_code: 'Nevažeći kod!' + +# 3rd party features: Bedrock Auto Login +bedrock_auto_login: + success: '&aBedrock automatsko prijavljivanje uspešno!' + +# 3rd party features: Login Location Fix +login_location_fix: + fix_portal: '&aZaglavljeni ste u portalu tokom prijavljivanja.' + fix_underground: '&aZaglavljeni ste pod zemljom tokom prijavljivanja.' + cannot_fix_underground: '&aZaglavljeni ste pod zemljom tokom prijavljivanja, ali ne možemo to popraviti.' + +# 3rd party features: Double Login Fix +double_login_fix: + fix_message: '&cPrekinuti ste zbog duplog prijavljivanja.' diff --git a/plugin/platform-bukkit/src/main/resources/messages/messages_tr.yml b/plugin/platform-bukkit/src/main/resources/messages/messages_tr.yml new file mode 100644 index 00000000..b9d12bf9 --- /dev/null +++ b/plugin/platform-bukkit/src/main/resources/messages/messages_tr.yml @@ -0,0 +1,173 @@ +# List of global tags: +# %nl% - Goes to new line. +# %username% - Replaces the username of the player receiving the message. +# %displayname% - Replaces the nickname (and colors) of the player receiving the message. + +# Registration +registration: + disabled: '&cOyun icin kayit olma kapatildi!' + name_taken: '&cSenin adinda daha once birisi kaydolmus!' + register_request: '&3Lutfen kayit komutunu kullanin "/register "' + command_usage: '&cKullanim: /register ' + reg_only: '&4Sunucuya kayit sadece internet uzerinden yapilmakta! Lutfen https://ornek.com sitesini kayit icin ziyaret edin!' + success: '&2Basariyla kaydoldun!' + kicked_admin_registered: 'Bir yetkili seni kayit etti; tekrardan giris yap' + +# Password errors on registration +password: + match_error: '&cSifre eslesmiyor, tekrar deneyin!' + name_in_password: '&cSifrenize adinizi koyamazsiniz, lutfen farkli bir sifre secin...' + unsafe_password: '&cSectiginiz sifre guvenli degil, lutfen farkli bir sifre secin...' + forbidden_characters: '&4Sifrenizde izin verilmeyen karakterler bulunmakta. Izin verilen karakterler: %valid_chars' + wrong_length: '&cSenin sifren ya cok kisa yada cok uzun! Lutfen farkli birsey dene!' + pwned_password: '&cSeçtiğiniz şifre güvenli değil. Zaten %pwned_count kez kullanılmış! Lütfen güçlü bir şifre kullanın...' + +# Login +login: + command_usage: '&cKullanim: /login ' + wrong_password: '&cYanlis sifre!' + success: '&2Giris basarili!' + login_request: '&cLutfen giris komutunu kullanin "/login "' + timeout_error: '&4Giris izni icin verilen zaman suresini astigin icin sunucudan atildin, tekrar deneyin!' + +# Errors +error: + denied_command: '&cSuanda bu komutu kullanamazsin!' + denied_chat: '&cSuanda sohbeti kullanamazsin!' + unregistered_user: '&cBu oyuncu kayitli degil!' + not_logged_in: '&cGiris yapmadin!' + no_permission: '&4Bunu yapmak icin iznin yok!' + unexpected_error: '&4Beklenmedik bir hata olustu, yetkili ile iletisime gecin!' + max_registration: '&cSen maksimum kayit sinirini astin (%reg_count/%max_acc %reg_names)!' + logged_in: '&cZaten giris yaptin!' + kick_for_vip: '&3Bir VIP oyuna giris yaptigi icin atildin!' + kick_unresolved_hostname: '&cBir hata olustu: cozumlenemeyen oyuncu bilgisayar adi!' + tempban_max_logins: '&cBir cok kez yanlis giris yaptiginiz icin gecici olarak banlandiniz.' + +# AntiBot +antibot: + kick_antibot: 'AntiBot koruma modu aktif! Birkac dakika sonra tekrar girmeyi deneyin.' + auto_enabled: '&4[AntiBotServis] Saldiri oldugu icin AntiBot aktif edildi!' + auto_disabled: '&2[AntiBotServis] AntiBot, %m dakika sonra deaktif edilecek!' + +# Unregister +unregister: + success: '&cKayit basariyla kaldirildi!' + command_usage: '&cKullanim: /unregister ' + +# Other messages +misc: + account_not_activated: '&cHeabiniz henuz aktif edilmemis, e-postanizi kontrol edin!' + not_activated: '&cHesap aktif değil, lütfen tekrar denemeden önce kaydolun ve aktif hale getirin.' + password_changed: '&2Sifre basariyla degistirildi!' + logout: '&2Basariyla cikis yaptin!' + reload: '&2Ayarlar ve veritabani yenilendi!' + usage_change_password: '&cKullanim: /changepassword ' + accounts_owned_self: 'Sen %count hesaba sahipsin:' + accounts_owned_other: 'Oyuncu %name %count hesaba sahip:' + +# Session messages +session: + valid_session: '&2Oturuma girisiniz otomatikmen yapilmistir.' + invalid_session: '&cIP adresin degistirildi ve oturum suren doldu!' + +# Error messages when joining +on_join_validation: + same_ip_online: 'Oyunda sizin ipnizden giren biri bulunmakta!' + same_nick_online: '&4Senin isminde bir oyuncu suncuda bulunmakta!' + name_length: '&4Senin ismin ya cok kisa yada cok uzun!' + characters_in_name: '&4Senin isminde uygunsuz karakterler bulunmakta. Izin verilen karakterler: %valid_chars' + kick_full_server: '&4Sunucu suanda dolu, daha sonra tekrar deneyin!' + country_banned: '&4Senin bolgen sunucudan yasaklandi!' + not_owner_error: 'Bu hesabin sahibi degilsin. Lutfen farkli bir isim sec!' + invalid_name_case: 'Oyuna %valid isminde katilmalisin. %invalid ismini kullanarak katilamazsin.' + quick_command: 'Bu komutu cok hizli kullandin! Lutfen, sunucuya tekrar gir ve herhangi bir komut kullanmadan once biraz bekle.' + +# Email +email: + add_email_request: '&3Lutfen hesabinize eposta adresinizi komut ile ekleyin "/email add "' + usage_email_add: '&cKullanim: /email add ' + usage_email_change: '&cKullanim: /email change ' + new_email_invalid: '&cGecersiz yeni eposta, tekrar deneyin!' + old_email_invalid: '&cGecersiz eski eposta, tekrar deneyin!' + invalid: '&cGecersiz eposta, tekrar deneyin!' + added: '&2Eposta basariyla kullaniciniza eklendi!' + add_not_allowed: '&cE-posta eklenmesine izin verilmedi!' + request_confirmation: '&cLutfen tekrar epostanizi giriniz!' + changed: '&2Epostaniz basariyla degistirildi!' + change_not_allowed: '&cEposta degistirilmesine izin verilmedi!' + email_show: '&2Suanki eposta adresin: &f%email' + no_email_for_account: '&2Bu hesapla iliskili bir eposta bulunmuyor.' + already_used: '&4Eposta adresi zaten kullaniliyor.' + incomplete_settings: 'Hata: Gonderilen epostada bazi ayarlar tamamlanmis degil. Yetkili ile iletisime gec.' + send_failure: 'Eposta gonderilemedi. Yetkili ile iletisime gec.' + change_password_expired: 'Artik bu komutu kullanarak sifrenizi degistiremezsiniz.' + email_cooldown_error: '&cKisa bir sure once eposta gonderildi. Yeni bir eposta almak icin %time beklemelisin.' + +# Password recovery by email +recovery: + forgot_password_hint: '&3Sifreni mi unuttun ? Komut kullanarak ogrenebilirsin "/email recovery "' + command_usage: '&cKullanim: /email recovery ' + email_sent: '&2Sifreniz epostaniza gonderildi! Lutfen eposta kutunuzu kontrol edin!' + code: + code_sent: 'Sifre sifirlama kodu eposta adresinize gonderildi.' + incorrect: 'Kod dogru degil! Kullanim "/email recovery [eposta]" ile yeni bir kod olustur' + tries_exceeded: 'Kurtarma kodu icin girilen maksimum sayisi astiniz. "/email recovery [email]" komutunu kullanarak yeni bir tane olusturabilirsiniz.' + correct: 'Kurtarma kodu dogru sekilde girildi!' + change_password: 'Lutfen sifrenizi degistirmek icin /email setpassword komutunu kullanin.' + +# Captcha +captcha: + usage_captcha: '&3Giris yapmak icin guvenlik kodunu komut yazarak girin "/captcha %captcha_code"' + wrong_captcha: '&cYanlis guvenlik kodu, kullanim sekli "/captcha %captcha_code" sohbete yazin!' + valid_captcha: '&2Guvenlik kodu dogrulandi!' + captcha_for_registration: 'Kayit olmak icin once bir captcha cozmeniz gerekiyor, lutfen bu komutu kullan: /captcha %captcha_code' + register_captcha_valid: '&2Gecerli captcha! /register ile kayit olabilirsiniz' + +# Verification code +verification: + code_required: '&3Bu komut hassas ve eposta dogrulamasi gerektiriyor! Gelen kutunuzu kontrol edin ve epostalarin talimatlarina uyun.' + command_usage: '&Kullanim: /verification ' + incorrect_code: '&cYanlis kod, lutfen email ile aldiginiz kodu kullanarak, chate "/verification " yazin' + success: '&2Kimliginiz dogrulandi! Simdi mevcut oturumdaki butun komutlari calistirabilirsiniz!' + already_verified: '&2Mevcut oturumda her hassas komutu zaten calistirabilirsiniz!' + code_expired: '&3Kodunuzun suresi doldu! Diger hassas komutlari calistirmak icin yeni bir kod alin!' + email_needed: '&3Kimliginizi dogrulamak icin hesabiniza bir email adresi baglamis olmaniz gerekir!!' + +# Time units +time: + second: 'saniye' + seconds: 'saniye' + minute: 'dakika' + minutes: 'dakika' + hour: 'saat' + hours: 'saat' + day: 'gun' + days: 'gun' + +# Two-factor authentication +two_factor: + code_created: '&2Gizli kodunuz %code. Buradan test edebilirsin, %url' + confirmation_required: 'Lutfen kodunuzu /2fa confirm komutu ile dogrulayin' + code_required: 'Lutfen iki-faktorlu dogrulama kodunuzu /2fa code komutu ile gonderin' + already_enabled: 'Iki-faktorlu dogrulama zaten hesabinizda aktif durumda!' + enable_error_no_code: 'Sizin icin 2fa anahtari olusturulmamis ya da suresi dolmus. Lutfen /2fa add komutunu calistirin' + enable_success: 'Iki-faktorlu kimlik dogrulama hesabiniz icin basariyla aktif edildi' + enable_error_wrong_code: 'Yanlis kod veya kodun suresi dolmus. Lutfen /2fa add komutunu calistirin' + not_enabled_error: 'Iki-faktorlu kimlik dogrulama kodu hesabiniz icin aktif edilmemis. /2fa add komutunu calistirin' + removed_success: 'Iki-faktorlu dogrulama hesabinizdan basariyla kaldirilmistir' + invalid_code: 'Gecersiz kod!' + +# 3rd party features: Bedrock Auto Login +bedrock_auto_login: + success: '&aBedrock otomatik giriş başarılı!' + +# 3rd party features: Login Location Fix +login_location_fix: + fix_portal: '&aGiriş sırasında portalda sıkıştınız.' + fix_underground: '&aGiriş sırasında yeraltında sıkıştınız.' + cannot_fix_underground: '&aGiriş sırasında yeraltında sıkıştınız, ancak bunu düzeltemiyoruz.' + +# 3rd party features: Double Login Fix +double_login_fix: + fix_message: '&cÇifte giriş nedeniyle bağlantınız kesildi.' diff --git a/plugin/platform-bukkit/src/main/resources/messages/messages_uk.yml b/plugin/platform-bukkit/src/main/resources/messages/messages_uk.yml new file mode 100644 index 00000000..b16bbb98 --- /dev/null +++ b/plugin/platform-bukkit/src/main/resources/messages/messages_uk.yml @@ -0,0 +1,173 @@ +# List of global tags: +# %nl% - Goes to new line. +# %username% - Replaces the username of the player receiving the message. +# %displayname% - Replaces the nickname (and colors) of the player receiving the message. + +# Registration +registration: + disabled: '&cВнутрішньоігрову реєстрацію зараз вимкнено.' + name_taken: '&cТакий нікнейм вже зареєстровано.' + register_request: '&3Перш ніж почати гру, вам потрібно зареєструвати свій нікнейм!%nl%&3Для цього просто введіть команду "/register <пароль> <повторПароля>"' + command_usage: '&cСинтаксис: /register <пароль> <повторПароля>' + reg_only: '&4Лише зареєстровані гравці можуть підключатись до сервера!%nl%&4Будь ласка, відвідайте https://example.com для реєстрації.' + success: '&2Реєстрація пройшла успішно!' + kicked_admin_registered: 'Адміністратор вас зареєстрував; Будь ласка, авторизуйтесь знову!' + +# Password errors on registration +password: + match_error: '&cПаролі не співпадають!' + name_in_password: '&cНе можна використовувати свій нікнейм у якості пароля! Будь ласка, виберіть щось інакше...' + unsafe_password: '&cЦей пароль надто простий! Будь ласка, придумайте інакший...' + forbidden_characters: '&4Ваш пароль містить недопустимі символи. Підберіть інакший...%nl%&4(Reg-ex: %valid_chars)' + wrong_length: '&cВаш пароль надто короткий або надто довгий! Спробуйте інакший...' + pwned_password: '&cВаш вибраний пароль небезпечний. Він вже використовувався %pwned_count разів! Будь ласка, використовуйте надійний пароль...' + +# Login +login: + command_usage: '&cСинтаксис: /login <пароль>' + wrong_password: '&cНевірний пароль!' + success: '&2Успішна авторизація!' + login_request: '&cДля авторизації, введіть команду "/login <пароль>"' + timeout_error: '&4Час для авторизації сплинув. Будь ласка, спробуйте ще раз!' + +# Errors +error: + denied_command: '&cДля використання цієї команди потрібна авторизація.' + denied_chat: '&cДля доступу до чату потрібна авторизація.' + unregistered_user: '&cЦей гравець не є зареєстрованим.' + not_logged_in: '&cВи не авторизовані!' + no_permission: '&4У вас недостатньо прав, щоб застосувати цю команду!' + unexpected_error: '&4[AuthMe] Помилка. Будь ласка, повідомте адміністратора!' + max_registration: '&cВичерпано ліміт реєстрацій (%reg_count/%max_acc %reg_names) для вашого підключення!' + logged_in: '&cВи вже авторизовані!' + kick_for_vip: '&3Вас кікнуто, внаслідок того, що VIP гравець зайшов на сервер коли небуло вільних місць.' + kick_unresolved_hostname: '&cЗнайдена помилка: невирішене ім''я вузла гравця!' + tempban_max_logins: '&cВаш IP тимчасово заблоковано, із‒за багатократного введення хибного пароля.' + +# AntiBot +antibot: + kick_antibot: 'На сервер здійснено DDoS атаку. Будь ласка, зачекайте декілька хвилин доки активність спаде.' + auto_enabled: '&4[AntiBotService] Ненормативне число з’єднань. Активовано антибот систему!' + auto_disabled: '&2[AntiBotService] Антибот систему деактивовано після %m хв. активності.' + +# Unregister +unregister: + success: '&cДані про реєстрацію успішно видалено' + command_usage: '&cСинтаксис: /unregister <пароль>' + +# Other messages +misc: + account_not_activated: '&cВаш акаунт ще не активовано. Будь ласка, провірте свою електронну пошту!' + not_activated: '&cАкаунт не активовано, будь ласка, зареєструйтесь і активуйте його перед повторною спробою.' + password_changed: '&2Пароль успішно змінено!' + logout: '&2Ви вийшли зі свого акаунта!' + reload: '&2Конфігурації та базу даних було успішно перезавантажено!' + usage_change_password: '&cСинтаксис: /changepassword <старийПароль> <новийПароль>' + accounts_owned_self: 'Кількість ваших твінк‒акаунтів: %count:' + accounts_owned_other: 'Кількість твінк‒акаунтів гравця %name: %count' + +# Session messages +session: + valid_session: '&2Сесію відновлено.' + invalid_session: '&cСесію було розірвано внаслідок зміни IP.' + +# Error messages when joining +on_join_validation: + same_ip_online: 'Перевищено ліміт на авторизацію з одного IP.' + same_nick_online: '&4Хтось з таким ніком зараз вже сидить на сервері!' + name_length: '&4Ваш нікнейм надто короткий або надто довгий!' + characters_in_name: '&4Ваш нікнейм містить недопустимі символи! Reg-ex: %valid_chars' + kick_full_server: '&4Сервер переповнено! Доведеться зачекати доки хтось вийде.' + country_banned: '&4Ваша країна заборонена на цьому сервері!' + not_owner_error: 'Цей акаунт вам не належить! Будь ласка, оберіть інакший нікнейм!' + invalid_name_case: 'Регістр у вашому нікнеймі відрізняється від регістру при реєстрації.%nl%Поточний регістр: &c%invalid&f. Валідний регістр: &a%valid&f.%nl%Будь ласка, перезайдіть з валідним регістром!' + quick_command: 'Ви занадто швидко використовували команду! Будь ласка, приєднайтесь знову до сервера і почекайте, перш ніж використовувати будь-яку команду.' + +# Email +email: + add_email_request: '&3Не забудьте прив’язати електронну пошту до свого акаунта, за допомогою команди "/email add "' + usage_email_add: '&cСинтаксис: /email add ' + usage_email_change: '&cСинтаксис: /email change <старий e-mail> <новий e-mail>' + new_email_invalid: '&cНекоректний формат.' + old_email_invalid: '&cСтарий e-mail, що прив’язано до вашого акаунта, відрізняється від введеного вами.' + invalid: '&cФормат вказаного e-mail’у є некоректним, або його домен внесено до блеклисту.' + added: '&2Електронну пошту успішно прив’язано до вашого акаунта.' + add_not_allowed: '&cДобавлення електронної пошти заборонено' + request_confirmation: '&cАдреси не співпадають.' + changed: '&2E-mail успішно змінено.' + change_not_allowed: '&cЗмiнення пошти заборонено' + email_show: '&2Ваша нинiшня адреса електронної пошти: &f%email' + no_email_for_account: '&2Наразі у вас немає адреси електронної пошти, пов’язаної з цим акаунтом.' + already_used: '&4До цієї електронної пошти прив’язано забагато акаунтів!' + incomplete_settings: '&4Не всі необхідні налаштування є встановленими, щоб надсилати електронну пошту. Будь ласка, повідомте адміністратора!' + send_failure: 'Не вдалося надіслати лист. Зверніться до адміністратора.' + change_password_expired: 'Ви більше не можете змінювати свій пароль, використовуючи цю команду.' + email_cooldown_error: '&cНещодавно вже було надіслано електронний лист. Заждиiть ще %time перш нiж вiдправляти ще листа.' + +# Password recovery by email +recovery: + forgot_password_hint: 'Забули пароль? Можете скористатись командою &9/email recovery &f<&9ваш e-mail&f>' + command_usage: '&cСинтаксис: /email recovery ' + email_sent: '&2Лист для відновлення доступу надіслано. Будь ласка, провірте свою пошту!' + code: + code_sent: 'На вашу адресу електронної пошти надіслано код відновлення для скидання пароля.' + incorrect: 'Код відновлення невірний! У вас залишилось ще %count спроб.' + tries_exceeded: 'Ви перевищили максимальну кількість спроб, щоб ввести код відновлення. Використовуйте "/email recovery [email]" для створення нового запросу.' + correct: 'Код відновлення введено правильно!' + change_password: 'Будь ласка, використовуйте команду /email setpassword <НовийПароль> щоб негайно змінити свій пароль.' + +# Captcha +captcha: + usage_captcha: '&3Для продовження доведеться ввести капчу — "/captcha %captcha_code"' + wrong_captcha: '&cНевірно введена капча! Спробуйте ще раз — "/captcha %captcha_code"' + valid_captcha: '&2Капчу прийнято.' + captcha_for_registration: 'Для того, щоб зареєструвати, ви повинні спочатку вирішити капчу, скористайтеся командою: /captcha %captcha_code' + register_captcha_valid: '&2Ви можете зареєструватися використовуя: /register' + +# Verification code +verification: + code_required: '&3Ця команда вимагає підтвердження електронною поштою! Перевірте свою пошту та дотримуйтесь вказівок у електронному листi.' + command_usage: '&cВикористання: /verification ' + incorrect_code: '&cНеправильний код, введіть "/verification <код>" у чаті, використовуючи код, отриманий електронною поштою' + success: '&2Ваша особа підтверджена! Тепер ви можете виконати всі команди протягом нинiшнього сеансу!' + already_verified: '&2Ви вже можете виконати кожну команду протягом нинiшнього сеансу!' + code_expired: '&3Ваш код закінчився! Виконайте ще одну команду, щоб отримати новий код!' + email_needed: '&3Щоб підтвердити свою особу, вам потрібно зв’язати електронну адресу зі своїм акаунтом!!' + +# Time units +time: + second: 'секунда' + seconds: 'секунд' + minute: 'хвилина' + minutes: 'хвилин' + hour: 'година' + hours: 'годин' + day: 'день' + days: 'днiв' + +# Two-factor authentication +two_factor: + code_created: '&2Ваш секретний код — %code %nl%&2Можете зкопіювати його за цим посиланням — %url' + confirmation_required: 'Підтвердьте свій код за допомогою / 2fa confirm <код>' + code_required: 'Будь ласка, надішліть свій двофакторний код аутентифікації використовуючи: /2fa code ' + already_enabled: 'Для вашого акаунту вже включена двофакторна аутентифікація!' + enable_error_no_code: 'Для вас не створено жодного ключа двухфакторної аутентифiкацii або термін його дії закінчився. Будь ласка, біжи /2fa add' + enable_success: 'Двофакторна автентифікація для вашого акаунта успішно включена' + enable_error_wrong_code: 'Неправильний код або код минув. Будь ласка, використовуйте: /2fa add' + not_enabled_error: 'Двофакторна аутентифікація не включена для вашого акаунту. Використовуйте: /2fa add' + removed_success: 'Двофакторну авторизацію успішно видалено з вашого акаунту' + invalid_code: 'Невірний код!' + +# 3rd party features: Bedrock Auto Login +bedrock_auto_login: + success: '&aАвтоматичний вхід Bedrock успішний!' + +# 3rd party features: Login Location Fix +login_location_fix: + fix_portal: '&aВи застрягли в порталі під час входу.' + fix_underground: '&aВи застрягли під землею під час входу.' + cannot_fix_underground: '&aВи застрягли під землею під час входу, але ми не можемо це виправити.' + +# 3rd party features: Double Login Fix +double_login_fix: + fix_message: '&cВас відключено через подвійний вхід.' diff --git a/plugin/platform-bukkit/src/main/resources/messages/messages_vn.yml b/plugin/platform-bukkit/src/main/resources/messages/messages_vn.yml new file mode 100644 index 00000000..788e5eec --- /dev/null +++ b/plugin/platform-bukkit/src/main/resources/messages/messages_vn.yml @@ -0,0 +1,173 @@ +# List of global tags: +# %nl% - Goes to new line. +# %username% - Replaces the username of the player receiving the message. +# %displayname% - Replaces the nickname (and colors) of the player receiving the message. + +# Registration +registration: + disabled: '&cKhông cho phép đăng ký tài khoản trong máy chủ!' + name_taken: '&cTài khoản này đã được đăng ký!' + register_request: '&2Xin vui lòng đăng ký tài khoản với lệnh "/register "' + command_usage: '&cSử dụng: /register ' + 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 https://example.com để đăng ký thành viên!' + success: '&2Đăng ký thành công!' + kicked_admin_registered: 'Một quản trị viên đã đăng ký cho bạn; vui lòng đăng nhập lại' + +# Password errors on registration +password: + match_error: '&cMật khẩu không đúng, vui lòng kiểm tra lại!' + name_in_password: '&cBạn không thể đặt mật khẩu bằng tên của mình, vui lòng đặt lại...' + unsafe_password: '&cMật khẩu của bạn vừa đặt không an toàn, vui lòng đặt lại...' + forbidden_characters: '&4Mật khẩu của bạn chứa ký tự không hợp lệ. Các ký tự cho phép: %valid_chars' + wrong_length: '&cMật khẩu của bạn đặt quá dài hoặc quá ngắn, vui lòng đặt lại!' + pwned_password: '&cMật khẩu bạn chọn không an toàn. Nó đã được sử dụng %pwned_count lần! Vui lòng sử dụng mật khẩu mạnh...' + +# Login +login: + command_usage: '&cSử dụng: /login ' + wrong_password: '&cSai mật khẩu!' + success: '&2Đăng nhập thành công!' + login_request: '&cXin vui lòng đăng nhập bằng lệnh "/login "' + timeout_error: '&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!' + +# Errors +error: + denied_command: '&cBạn phải đăng nhập trước rồi mới có thể dùng lệnh này!' + denied_chat: '&cBạn phải đăng nhập trước rồi mới có thể chat!' + unregistered_user: '&cNgười dùng này chưa được đăng ký!' + not_logged_in: '&cBạn chưa đăng nhập!' + no_permission: '&4Bạn không có quyền dùng cập lệnh này!' + unexpected_error: '&4Lỗi! Vui lòng liên hệ quản trị viên.' + max_registration: '&cBạn đã vượt quá giới hạn tối đa đăng ký tài khoản (%reg_count/%max_acc %reg_names) trên đường truyền của bạn!' + logged_in: '&cBạn đã đăng nhập rồi!' + kick_for_vip: '&eChỉ có thành viên VIP mới được tham gia khi máy chủ đầy!' + kick_unresolved_hostname: '&cLỗi đã xảy ra: Không thể phân giải hostname của người chơi!' + tempban_max_logins: '&cBạn đã bị chặn tạm thời do đăng nhập sai quá nhiều lần.' + +# AntiBot +antibot: + 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ủ.' + auto_enabled: '&4[AntiBotService] AntiBot đã được kích hoạt do số lượng lớn kết nối đến máy chủ!' + auto_disabled: '&2[AntiBotService] AntiBot đã được tắt sau %m phút!' + +# Unregister +unregister: + success: '&cHủy đăng ký thành công!' + command_usage: '&cSử dụng: /unregister ' + +# Other messages +misc: + account_not_activated: '&cTài khoản của bạn chưa được kích hoạt, vui lòng kiểm tra email!' + not_activated: '&cTài khoản chưa được kích hoạt, vui lòng đăng ký và kích hoạt trước khi thử lại.' + password_changed: '&2Thay đổi mật khẩu thành công!' + logout: '&2Bạn đã đăng xuất!' + reload: '&2Cấu hình và cơ sở dử liệu đã được tải lại thành công!' + usage_change_password: '&cSử dụng: /changepassword ' + accounts_owned_self: 'Bạn sở hữu %count tài khoản:' + accounts_owned_other: 'Người chơi %name có %count tài khoản:' + +# Session messages +session: + valid_session: '&2Phiên đăng nhập đã được kết nối trở 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!' + +# Error messages when joining +on_join_validation: + same_ip_online: 'Một người chơi với cùng địa chỉ IP đã kết nối vào máy chủ!' + same_nick_online: '&4Tài khoản đang được sử dụng trên máy chủ!' + name_length: '&4Tên đăng nhập của bạn quá ngắn hoặc quá dài!' + characters_in_name: '&4Tên nhân vật có chứa ký tự không hợp lệ. Các ký tự được cho phép: %valid_chars' + kick_full_server: '&4Máy chủ quá tải, vui lòng thử lại sau!' + country_banned: '&4Quốc gia của bạn bị cấm tham gia máy chủ này!' + not_owner_error: 'Bạn không phải là chủ sở hữu tài khoản 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.' + quick_command: 'Bạn đang xài lệnh quá nhanh. Hãy thoát máy chủ và chờ một lúc trước khi sử dụng lệnh.' + +# Email +email: + add_email_request: '&eVui lòng thêm email của bạn với lệnh "/email add "' + usage_email_add: '&cSử dụng: /email add ' + usage_email_change: '&cSử dụng: /email change ' + 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!' + invalid: '&cĐại chỉ email không hợp lệ, vui lòng thử lại!' + added: '&2Địa chỉ email đã được thêm vào tài khoản của bạn.' + add_not_allowed: '&cKhông được phép thêm địa chỉ email!' + request_confirmation: '&cVui lòng xác nhận địa chỉ email của bạn!' + changed: '&2Địa chỉ email đã thay đổi!' + change_not_allowed: '&cKhông được phép thay đổi địa chỉ email!' + email_show: '&2Địa chỉ email hiện tại của bạn là: &f%email' + no_email_for_account: '&2Hiện tại bạn chưa liên kết bất kỳ email nào với tài khoản này.' + already_used: '&4Địa chỉ email đã được sử dụng!' + incomplete_settings: 'Lỗi: các thiết lập để gửi thư không được cài đặt đúng cách. Vui lòng liên hệ với quản trị viên để báo lỗi.' + send_failure: 'Không thể gửi thư. Vui lòng liên hệ với ban quản trị.' + change_password_expired: '&cBạn không thể thay đổi mật khẩu bằng lệnh này nữa.' + email_cooldown_error: '&cMột email đã được gửi gần đây. Bạn phải chờ %time trước khi có thể gửi một email mới.' + +# Password recovery by email +recovery: + forgot_password_hint: '&aBạn quên mật khẩu? Vui lòng gõ lệnh "/email recovery "' + command_usage: '&cSử dụng: /email recovery ' + email_sent: '&2Email khôi phục đã được gửi thành công! Vui lòng kiểm tra hộp thư đến trong email của bạn.' + code: + code_sent: 'Một mã khôi phục mật khẩu đã được gửi đến địa chỉ email của bạn.' + incorrect: 'Mã khôi phục không đúng! Dùng lệnh /email recovery [email] để tạo một mã mới' + tries_exceeded: 'Bạn đã vượt quá số lần tối đa cho phép nhập mã khôi phục. Hãy sử dụng lệnh "/email recovery [email]" để tạo một mã mới.' + correct: 'Mã khôi phục đã được nhập chính xác!' + change_password: 'Hãy sử dụng lệnh "/email setpassword " để đổi mật khẩu của bạn ngay lập tức.' + +# Captcha +captcha: + usage_captcha: '&eĐể đăng nhập, hãy nhập mã Captcha, Vui lòng gõ lệnh "/captcha %captcha_code"' + wrong_captcha: '&cSai mã captcha, Vui lòng gõ lệnh "/captcha %captcha_code"!' + valid_captcha: '&2Đã xác minh captcha!' + captcha_for_registration: 'Để đăng ký, hãy nhập mã Captcha trước. Vui lòng gõ lệnh "/captcha %captcha_code"' + register_captcha_valid: '&2Đã xác minh captcha! Bây giờ bạn có thể đăng ký với lệnh "/register"' + +# Verification code +verification: + code_required: '&3Lệnh này nhạy cảm và yêu cầu xác minh email! Vui lòng iểm tra hộp thư đến của bạn và làm theo hướng dẫn trong email.' + command_usage: '&cLệnh: /verification ' + incorrect_code: '&cMã xác minh không chính xác, vui lòng nhập "/verify " với mã bạn nhận được qua email.' + success: '&2Xác minh thành công! Bạn đã có thể sử dụng các lệnh trong phiên đăng nhập này.' + already_verified: '&2Bạn đã có thể thực hiện mọi lệnh nhạy cảm trong phiên hiện tại!' + code_expired: '&3Mã xác minh đã hết han! Hãy sử dụng một lệnh nhạy cảm để lấy mã mới.' + email_needed: '&3Để xác định danh tính của bạn, bạn cần kết nối tài khoản này với 1 email!' + +# Time units +time: + second: 'giây' + seconds: 'giây' + minute: 'phút' + minutes: 'phút' + hour: 'giờ' + hours: 'giờ' + day: 'ngày' + days: 'ngày' + +# Two-factor authentication +two_factor: + code_created: '&2Mã bí mật của bạn là %code. Bạn có thể quét nó tại đây %url' + confirmation_required: 'Hãy xác thực mã của bạn bằng lệnh /2fa confirm ' + code_required: 'Hãy nhập mã xác thực 2 lớp của bạn bằng lệnh /2fa code ' + already_enabled: 'Xác thực 2 lớp đã được kích hoạt trên tài khoản của bạn!' + enable_error_no_code: 'Không có mã xác thực nào đã được tạo cho tài khoản của bạn hoặc nó đã hết hạn. Hãy sử dụng lệnh /2fa add' + enable_success: 'Đã thành công kích hoạt xác thực 2 lớp cho tài khoản của bạn!' + enable_error_wrong_code: 'Mã xác thực bị sai hoặc đã bị hết hạn. Hãy sử dụng lệnh /2fa add' + not_enabled_error: 'Xác thực 2 lớp không được kích hoạt trên tài khoản của bạn. Hãy sử dụng lệnh /2fa add' + removed_success: 'Đã thành công tắt xác thực 2 lớp khỏi tài khoản của bạn' + invalid_code: 'Mã xác thực không hợp lệ!' + +# 3rd party features: Bedrock Auto Login +bedrock_auto_login: + success: '&aĐăng nhập tự động Bedrock thành công!' + +# 3rd party features: Login Location Fix +login_location_fix: + fix_portal: '&aBạn bị kẹt trong cổng khi đăng nhập.' + fix_underground: '&aBạn bị kẹt dưới lòng đất khi đăng nhập.' + cannot_fix_underground: '&aBạn bị kẹt dưới lòng đất khi đăng nhập, nhưng chúng tôi không thể khắc phục điều này.' + +# 3rd party features: Double Login Fix +double_login_fix: + fix_message: '&cBạn đã bị ngắt kết nối do đăng nhập đôi.' diff --git a/plugin/platform-bukkit/src/main/resources/messages/messages_zhcn.yml b/plugin/platform-bukkit/src/main/resources/messages/messages_zhcn.yml new file mode 100644 index 00000000..53ad8493 --- /dev/null +++ b/plugin/platform-bukkit/src/main/resources/messages/messages_zhcn.yml @@ -0,0 +1,181 @@ +# List of global tags: +# %nl% - Goes to new line. +# %username% - Replaces the username of the player receiving the message. +# %displayname% - Replaces the nickname (and colors) of the player receiving the message. + +# Registration +registration: + disabled: '&c注册已被禁止' + name_taken: '&c此用户已经在此服务器注册过' + register_request: '&b请输入“&a/reg <密码> <重复密码>&b”以注册' + command_usage: '&c正确用法:“/reg <密码> <重复密码>”' + reg_only: '&c只允许注册过的玩家进服!' + success: '&a*** 已成功注册 ***' + kicked_admin_registered: '&a*** 管理员刚刚注册了您; 请重新登录 ***' + +# Password errors on registration +password: + match_error: '&c密码不相同' + name_in_password: '&c您不能使用您的名字作为密码。 ' + unsafe_password: '&c您不能使用安全性过低的密码。 ' + forbidden_characters: '&4您的密码包含了非法字符.可使用的字符: %valid_chars' + wrong_length: '&4您的密码没有达到要求!' + pwned_password: '&c你使用的密码并不安全。它已经被使用了 %pwned_count 次! 请使用一个更强大的密码...' + +# Login +login: + command_usage: '&c正确用法:“/l <密码>”' + wrong_password: '&c*** 密码错误 ***' + success: '&a*** 已成功登录 ***' + login_request: '&c请输入“/l <密码>”以登录' + timeout_error: '给您登录的时间已经过了' + +# Errors +error: + denied_command: '&7您需要先通过验证才能使用该命令!' + denied_chat: '&7您需要先通过验证才能聊天!' + unregistered_user: '&c此用户名还未注册过' + not_logged_in: '&c您还未登录!' + no_permission: '&c没有权限' + unexpected_error: '&4发现错误,请联系管理员' + max_registration: '&c该地址已无法注册,请联系管理员进行注册.' + logged_in: '&c您已经登陆过了!' + kick_for_vip: '&c一个VIP玩家加入了已满的服务器!' + kick_unresolved_hostname: '&c发生了一个错误: 无法解析玩家的主机名' + tempban_max_logins: '&c由于您登录失败次数过多,已被暂时禁止登录。' + +# AntiBot +antibot: + kick_antibot: '连接异常,请稍后加入' + auto_enabled: '&c由于发生大量异常连接,本服将禁止连接.' + auto_disabled: '&a异常连接减少,本服将在 &a%m &a分钟后自动开放连接.' + +# Unregister +unregister: + success: '&a*** 已成功注销 ***' + command_usage: '&c正确用法:“/unregister <密码>”' + +# Other messages +misc: + account_not_activated: |- + &a一封包含密码的邮件已发送至您的收件箱. + &a请在十分钟内完成登录,否则将注销此账户. + not_activated: '&c账户未激活,请注册激活后再次尝试.' + password_changed: '&a*** 密码已修改 ***' + logout: '&a*** 已成功登出 ***' + reload: '&a配置以及数据已经重新加载完毕' + usage_change_password: '&a正确用法:“/changepassword 旧密码 新密码”' + accounts_owned_self: '您拥有 %count 个账户:' + accounts_owned_other: '玩家 %name 拥有 %count 个账户:' + +# Session messages +session: + valid_session: '' + invalid_session: '&c*** 请重新登录 ***' + +# Error messages when joining +on_join_validation: + same_ip_online: '已有一个同IP玩家在游戏中了!' + same_nick_online: '&a同样的用户名现在在线且已经登录了!' + name_length: '&c您的用户名太短或者太长了' + characters_in_name: '&c您的用户名不符合标准.' + kick_full_server: '&c抱歉,服务器已满!' + country_banned: |- + &6[&b&lAccount Security System&6] + &c为保证您的游玩体验,请使用中国境内网络连接. + &cTo ensure your play experience,please use the Internet connection within China. + not_owner_error: |- + &6[&b&lAccount Security System&6] + &c请勿尝试登陆系统账户,系统账户受安全系统保护. + &c如果您并不知情,请更换您的用户名重新加入该服务器. + invalid_name_case: '&c您应该使用 %valid 登录服务器,当前名字: %invalid .' + quick_command: '&c您发送命令的速度太快了,请重新加入服务器等待一会后再使用命令' + +# Email +email: + add_email_request: '' + usage_email_add: '&a用法: /email add <邮箱> <确认邮箱> ' + usage_email_change: '&a用法: /email change <旧邮箱> <新邮箱> ' + new_email_invalid: '&c新邮箱无效!' + old_email_invalid: '&c旧邮箱无效!' + invalid: '&c无效的邮箱' + added: '&a*** 邮箱已添加 ***' + add_not_allowed: '&c服务器不允许添加电子邮箱' + request_confirmation: '&c确认您的邮箱' + changed: '&a*** 邮箱已修改 ***' + change_not_allowed: '&c服务器不允许修改邮箱地址' + email_show: '&a该账户使用的电子邮箱为: &a%email' + no_email_for_account: '&c当前并没有任何邮箱与该账号绑定' + already_used: '&c邮箱已被使用' + incomplete_settings: '&c错误: 必要设置未设定完成,请联系管理员' + send_failure: '&c邮件已被作废,请检查您的邮箱是否正常工作.' + change_password_expired: '&c您不能使用此命令更改密码' + email_cooldown_error: '&c您需要等待 %time 后才能再次请求发送' + +# Password recovery by email +recovery: + forgot_password_hint: '&c忘了您的密码?请输入:“/email recovery <您的邮箱>”' + command_usage: '&a用法: /email recovery <邮箱>' + email_sent: '&a找回密码邮件已发送!' + code: + code_sent: '&a*** 已发送验证邮件 ***' + incorrect: '&a验证码不正确! 使用 /email recovery [邮箱] 以生成新的验证码' + tries_exceeded: '&a您已经达到输入验证码次数的最大允许次数.' + correct: '&a*** 验证通过 ***' + change_password: '&c请使用 /email setpassword <新密码> 立即设置新的密码' + +# Captcha +captcha: + usage_captcha: '&7请输入 /captcha %captcha_code 来验证操作.' + wrong_captcha: '&c错误的验证码.' + valid_captcha: '&a*** 验证通过 ***' + captcha_for_registration: '&7请输入 /captcha %captcha_code 来验证操作.' + register_captcha_valid: '&a验证通过, 现在可以使用 /register 注册啦!' + +# Verification code +verification: + code_required: '&a*** 已发送验证邮件 ***' + command_usage: '&c使用方法:/verification <验证码>' + incorrect_code: '&c错误的验证码.' + success: '&a*** 验证通过 ***' + already_verified: '&a您已经通过验证' + code_expired: '&c验证码已失效' + email_needed: '&c邮箱未绑定' + +# Time units +time: + second: '秒' + seconds: '秒' + minute: '分' + minutes: '分' + hour: '时' + hours: '时' + day: '天' + days: '天' + +# Two-factor authentication +two_factor: + code_created: '&7您正在激活双重验证, 请打开 &a%url &7扫描二维码' + confirmation_required: '&7请输入“/totp confirm <验证码>”来确认激活双重验证' + code_required: '&c请输入“/totp code <验证码>”来提交验证码' + already_enabled: '&a双重验证已启用' + enable_error_no_code: '&c验证码丢失' + enable_success: '&a已成功启用双重验证' + enable_error_wrong_code: '&c验证码错误或者已经过期,请重新执行“/totp add”' + not_enabled_error: '&c双重验证未在您的账号上启用,请使用“/totp add”来启用' + removed_success: '&c双重验证已从您的账号上禁用' + invalid_code: '&c无效的验证码' + +# 3rd party features: Bedrock Auto Login +bedrock_auto_login: + success: "&a基岩版自动登录完成" + +# 3rd party features: Login Location Fix +login_location_fix: + fix_portal: '&a你在登录时卡在了地狱门, 现已修正' + fix_underground: '&a你被埋住了, 坐标已修正, 下次下线之前请小心!' + cannot_fix_underground: '&a你被埋住了, 坐标无法修正, 只好送你去了最高点, 自求多福吧少年~' + +# 3rd party features: Double Login Fix +double_login_fix: + fix_message: '&a已修复幽灵玩家, 请重新进入' diff --git a/plugin/platform-bukkit/src/main/resources/messages/messages_zhhk.yml b/plugin/platform-bukkit/src/main/resources/messages/messages_zhhk.yml new file mode 100644 index 00000000..42a96a88 --- /dev/null +++ b/plugin/platform-bukkit/src/main/resources/messages/messages_zhhk.yml @@ -0,0 +1,176 @@ +# Translator: lifehome # +# Last modif: 1541690611 UTC # +# -------------------------------------------- # +# List of global tags: +# %nl% - Goes to new line. +# %username% - Replaces the username of the player receiving the message. +# %displayname% - Replaces the nickname (and colors) of the player receiving the message. + +# Registration +registration: + disabled: '&8[&6用戶系統&8] &c本伺服器已停止新玩家註冊。' + name_taken: '&8[&6用戶系統&8] &c此用戶名已經註冊過了。' + register_request: '&8[&6用戶系統&8] &c請使用這個指令來註冊:《 &f/register <密碼> <重覆密碼>&c 》' + command_usage: '&8[&6用戶系統&8] &f用法:《 /register <密碼> <重覆密碼> 》' + reg_only: '&8[&6用戶系統&8] &f限已註冊會員,請先到本服網站進行註冊。' + success: '&8[&6用戶系統&8] &b你成功註冊了。' + kicked_admin_registered: '&8[&6用戶系統&8] &b管理員已替你完成註冊,請重新登入。' + +# Password errors on registration +password: + match_error: '&8[&6用戶系統&8] &f密碼不符合。' + name_in_password: '&8[&6用戶系統&8] &c這個密碼太不安全了 !' + unsafe_password: '&8[&6用戶系統&8] &c這個密碼太不安全了 !' + forbidden_characters: '&8[&6用戶系統&8] &4密碼字符只能含有這些喔:%valid_chars' + wrong_length: '&8[&6用戶系統&8] &f嗯... 你的密碼並不符合規定長度。' + pwned_password: '&8[&6用戶系統&8] &c你使用的密碼並不安全。它已經被使用了 %pwned_count 次! 請使用一個更強大的密碼...' + +# Login +login: + command_usage: '&8[&6用戶系統&8] &f用法:《 /login <密碼> 》' + wrong_password: '&8[&6用戶系統&8] &c你輸入了錯誤的密碼。' + success: '&8[&6用戶系統&8] &a你成功登入了。' + login_request: '&8[&6用戶系統&8] &c請使用這個指令來登入:《 &f/login <密碼>&c 》' + timeout_error: '&8[&6用戶系統&8] &f登入逾時。' + +# Errors +error: + denied_command: '&8[&6用戶系統&8] &c請先登入以便使用此指令。' + denied_chat: '&8[&6用戶系統&8] &c請先登入以便與其他玩家聊天。' + unregistered_user: '&8[&6用戶系統&8] &c此用戶名沒有已登記資料。' + not_logged_in: '&8[&6用戶系統&8] &c你還沒有登入 !' + no_permission: '&8[&6用戶系統&8] &b嗯~你想幹甚麼?' + unexpected_error: '&8[&6用戶系統&8] &f發生錯誤,請與管理員聯絡。' + max_registration: '&8[&6用戶系統&8] &f你的IP地址已達到註冊數上限。 &7(info: %reg_count/%max_acc %reg_names)' + logged_in: '&8[&6用戶系統&8] &c你已經登入過了。' + kick_for_vip: '&8[&6用戶系統&8] &c喔 !因為有VIP玩家登入了伺服器。' + kick_unresolved_hostname: '&8[&6用戶系統&8] &c發生了一個錯誤: 無法解析玩家的主機名' + tempban_max_logins: '&8[&6用戶系統&8] &c因為多次登入失敗,你已被暫時封禁。' + +# AntiBot +antibot: + kick_antibot: '&8[&6用戶系統&8] &c伺服器錯誤 !請稍候再嘗試登入吧。 &7(err: kick_due2_bot)' + auto_enabled: '&8[&6用戶系統&8] &3防止機械人程序已因應現時大量不尋常連線而啟用。' + auto_disabled: '&8[&6用戶系統&8] &3不正常連接數已減少,防止機械人程序將於 %m 分鐘後停止。' + +# Unregister +unregister: + success: '&8[&6用戶系統&8] &c已成功刪除會員註冊記錄。' + command_usage: '&8[&6用戶系統&8] &f用法:《 /unregister <密碼> 》' + +# Other messages +misc: + account_not_activated: '&8[&6用戶系統&8] &f你的帳戶還沒有經過電郵驗證 !' + not_activated: '&8[&6用戶系統&8] &c賬戶未激活,請註冊激活後再次嘗試.' + password_changed: '&8[&6用戶系統&8] &c你成功更換了你的密碼 !' + logout: '&8[&6用戶系統&8] &b你成功登出了。' + reload: '&8[&6用戶系統&8] &b登入系統設定及資料庫重新載入完畢。' + usage_change_password: '&8[&6用戶系統&8] &f用法:《 /changepassword <舊密碼> <新密碼> 》' + accounts_owned_self: '你擁有 %count 個帳戶:' + accounts_owned_other: '玩家《%name》擁有 %count 個帳戶:' + +# Session messages +session: + valid_session: '&8[&6用戶系統&8] &b嗨 ! 歡迎回來喔~' + invalid_session: '&8[&6用戶系統&8] &f登入階段資料已損壞,請等待登入階段結束。' + +# Error messages when joining +on_join_validation: + same_ip_online: '&8[&6用戶系統&8] &c你正使用的 IP 位址已被其他玩家佔用。' + same_nick_online: '&8[&6用戶系統&8] &f同名玩家已在遊玩。' + name_length: '&8[&6用戶系統&8] &c你的用戶名不符合規定長度。' + characters_in_name: '&8[&6用戶系統&8] &c用戶名稱錯誤 ! 登入系統只接受以下字符:%valid_chars' + kick_full_server: '&c抱歉 ! 因為伺服器滿人了,所以你目前未能登入伺服器。' + country_banned: '&8[&6用戶系統&8] &c抱歉 !&4本伺服器已停止對你的國家提供遊戲服務。' + not_owner_error: '&8[&6用戶系統&8] &4警告 !&c你並不是此帳戶持有人,請立即登出。' + invalid_name_case: '&8[&6用戶系統&8] &4警告 !&c你應該使用「%valid」而並非「%invalid」登入遊戲。' + quick_command: '&8[&6用戶系統&8] &4警告 !&c你使用指令的速度太快了,請重新登入並稍等一會才再次使用指令。' + +# Email +email: + add_email_request: '&8[&6用戶系統&8] &b請為你的帳戶立即添加電郵地址:《 /email add <電郵地址> <重覆電郵地址> 》' + usage_email_add: '&8[&6用戶系統&8] &f用法:《 /email add <電郵> <重覆電郵> 》' + usage_email_change: '&8[&6用戶系統&8] &f用法:《 /email change <舊電郵> <新電郵> 》' + new_email_invalid: '&8[&6用戶系統&8] &c你所填寫的新電郵地址並不正確。' + old_email_invalid: '&8[&6用戶系統&8] &c你所填寫的舊電郵地址並不正確。' + invalid: '&8[&6用戶系統&8] &c你所填寫的電郵地址並不正確。' + added: '&8[&6用戶系統&8] &a已新增你的電郵地址。' + add_not_allowed: '&8[&6用戶系統&8] &c你不能新增電郵地址到你的帳戶。' + request_confirmation: '&8[&6用戶系統&8] &5請重覆輸入你的電郵地址。' + changed: '&8[&6用戶系統&8] &a你的電郵地址已更改。' + change_not_allowed: '&8[&6用戶系統&8] &c你不能更改你的電郵地址。' + email_show: '&8[&6用戶系統&8] &2你所使用的電郵地址為: &f%email' + no_email_for_account: '&8[&6用戶系統&8] &2你並未有綁定電郵地址到此帳戶。' + already_used: '&8[&6用戶系統&8] &4這個電郵地址已被使用。' + incomplete_settings: '&8[&6用戶系統&8] &c電郵系統錯誤,請聯絡伺服器管理員。 &7(err: mailconfig_incomplete)' + send_failure: '&8[&6用戶系統&8] &c電郵系統錯誤,請聯絡伺服器管理員。 &7(err: smtperr)' + change_password_expired: '&8[&6用戶系統&8] &c此指令已過期,請重新辦理。' + email_cooldown_error: '&8[&6用戶系統&8] &c你最近已經辦理過重寄郵件,請等待 %time 後再嘗試吧。' + +# Password recovery by email +recovery: + forgot_password_hint: '&8[&6用戶系統&8] &b忘記密碼?請使用 /email recovery <電郵地址> 來更新密碼。' + command_usage: '&8[&6用戶系統&8] &f用法:《 /email recovery <電郵> 》' + email_sent: '&8[&6用戶系統&8] &a忘記密碼信件已寄出,請查收。' + code: + code_sent: '&8[&6用戶系統&8] &b帳戶驗證碼已發送到你的郵箱,請查收。' + incorrect: '&8[&6用戶系統&8] &c帳戶驗證碼無效。 &7(你尚餘 %count 次嘗試機會)' + tries_exceeded: '&8[&6用戶系統&8] &c此驗證碼已因多次嘗試後失效,請使用《 &f/email recovery <電郵地址>&c 》重新辦理。' + correct: '&8[&6用戶系統&8] &a帳戶驗證碼輸入正確。' + change_password: '&8[&6用戶系統&8] &c請立即使用《 &f/email setpassword <新密碼>&c 》指令,以重設你的帳戶密碼。' + +# Captcha +captcha: + usage_captcha: '&8[&6用戶系統&8] &f用法:《 /captcha %captcha_code 》' + wrong_captcha: '&8[&6用戶系統&8] &c你所輸入的驗證碼無效,請使用 《 &f/captcha %captcha_code&c 》 再次輸入。' + valid_captcha: '&8[&6用戶系統&8] &c你所輸入的驗證碼無效 !' + captcha_for_registration: '&8[&6用戶系統&8] &2要完成玩家註冊程序,請先執行驗証指令:《 &f/captcha %captcha_code&2 》' + register_captcha_valid: '&8[&6用戶系統&8] &2驗證成功 !你現在可以使用指令《 &f/register&2 》進行註冊了~' + +# Verification code +verification: + code_required: '&8[&6用戶系統&8] &e這個高級魔法指令需要再一次進行電郵驗證方可使用,請查收電郵並按信件內容進行驗證。' + command_usage: '&8[&6用戶系統&8] &f用法:《 /verification <驗證碼> 》' + incorrect_code: '&8[&6用戶系統&8] &c錯誤的驗證碼,請使用指令《 &f/verification <驗證碼>&c 》再次嘗試驗證。' + success: '&8[&6用戶系統&8] &2身份驗證成功 !你現在可以在目前的登入階段執行所有等級的指令了 !' + already_verified: '&8[&6用戶系統&8] &2你已經爲目前的登入階段成功通過電郵身份驗證了,毋須再次驗證。' + code_expired: '&8[&6用戶系統&8] &c你的驗證碼已經過期,請執行另一個高等魔法指令以觸發新的身份驗證挑戰。' + email_needed: '&8[&6用戶系統&8] &c嗯... 你的帳戶沒有已經連結的電郵地址,不能使用電郵驗證功能。' + +# Time units +time: + second: '秒' + seconds: '秒' + minute: '分' + minutes: '分' + hour: '小時' + hours: '小時' + day: '日' + days: '日' + +# Two-factor authentication +two_factor: + code_created: '&8[&6用戶系統&8] &b你的登入金鑰為&9〔 &c%code&9 〕&b,掃描連結為:%nl%&c %url' + confirmation_required: '&8[&6用戶系統&8] &b請使用指令《 &f/2fa confirm <驗證碼>&b 》以確認你的身份。' + code_required: '&8[&6用戶系統&8] &b請使用指令《 &f/2fa code <驗證碼>&b 》以驗證你的帳戶。' + already_enabled: '&8[&6用戶系統&8] &2嗯...這個帳戶已經啓用了兩步驗證功能,毋須再次啓用。' + enable_error_no_code: '&8[&6用戶系統&8] &c無效的登入金鑰 ! &f你可以使用指令《 /2fa add 》再次嘗試啓用此功能。' + enable_success: '&8[&6用戶系統&8] &2你已成功啓用兩步驗證功能 !' + enable_error_wrong_code: '&8[&6用戶系統&8] &c無效的兩步驗證碼 ! &f你可以使用指令《 /2fa add 》再次嘗試啓用此功能。' + not_enabled_error: '&8[&6用戶系統&8] &c你的帳戶尚未啓用兩步驗證功能 ! &f你可以使用指令《 /2fa add 》以啓用此功能。' + removed_success: '&8[&6用戶系統&8] &2你已成功關閉兩步驗證功能;&c為保障帳戶安全,請儘快於可行的情況下重新啟用本功能。' + invalid_code: '&8[&6用戶系統&8] &c無效的兩步驗證碼 !' + +# 3rd party features: Bedrock Auto Login +bedrock_auto_login: + success: "&8[&6用戶系統&8] &a基岩版自動登錄完成" + +# 3rd party features: Login Location Fix +login_location_fix: + fix_portal: '&8[&6用戶系統&8] &a你在登錄時卡在了地獄門, 現已修正' + fix_underground: '&8[&6用戶系統&8] &a你被埋住了, 坐標已修正, 下次下線之前請小心!' + cannot_fix_underground: '&8[&6用戶系統&8] &a你被埋住了, 坐標無法修正, 只好送你去了最高點, 自求多福吧少年~' + +# 3rd party features: Double Login Fix +double_login_fix: + fix_message: '&8[&6用戶系統&8] &a已修復幽靈玩家, 請重新進入' diff --git a/plugin/platform-bukkit/src/main/resources/messages/messages_zhmc.yml b/plugin/platform-bukkit/src/main/resources/messages/messages_zhmc.yml new file mode 100644 index 00000000..a0613cb5 --- /dev/null +++ b/plugin/platform-bukkit/src/main/resources/messages/messages_zhmc.yml @@ -0,0 +1,173 @@ +# List of global tags: +# %nl% - Goes to new line. +# %username% - Replaces the username of the player receiving the message. +# %displayname% - Replaces the nickname (and colors) of the player receiving the message. + +# Registration +registration: + disabled: '&c遊戲內註冊已停用!' + name_taken: '&c您已經註冊此用戶名!' + register_request: '&3[請先注冊] 請按T , 然後輸入 "/register [你的密碼] [重覆確認你的密碼]" 來注冊。' + command_usage: '&c使用方法: 輸入"/register [你的密碼] [重覆確認你的密碼]"' + reg_only: '&4只有註冊用戶才能加入服務器! 請訪問https://example.com註冊!' + success: '&2已成功註冊!' + kicked_admin_registered: '管理員剛剛註冊了您; 請重新登錄。' + +# Password errors on registration +password: + match_error: '&密碼錯誤!' + name_in_password: '&c你不能使用你的用戶名作密碼,請 choose another one...' + unsafe_password: '&c所選的你的密碼並不安全的,請選擇另一個...' + forbidden_characters: '&4您的密碼含有非法字符。允許的字符:%valid_chars' + wrong_length: '&c你的密碼太短或太長!請嘗試另一個!' + pwned_password: '&c你使用的密碼並不安全。它已經被使用了 %pwned_count 次! 請使用一個更強大的密碼...' + +# Login +login: + command_usage: '&b使用方法: 輸入"/login [你的密碼]" 來登入' + wrong_password: '&c密碼錯誤!' + success: '&2你已成功登入!' + login_request: '&c [請先登入] 請按T , 然後輸入 "/login [你的密碼]" 。' + timeout_error: '&4超過登錄超時,您已從伺服器中踢出,請重試!' + +# Errors +error: + denied_command: '&c你必須得到權限來使用此指令!' + denied_chat: '&c你必須得到權限來使用聊天功能!' + unregistered_user: '&c此用戶尚未注冊!' + not_logged_in: '&c你尚未登入!' + no_permission: '&4您沒有執行此操作的權限!' + unexpected_error: '&4發生錯誤!請聯繫伺服器管理員!' + max_registration: '&c您已超過註冊的最大數量(%reg_count/%max_acc %reg_names)!' + logged_in: '&c您已經登錄!' + kick_for_vip: '&3一名VIP玩家在服務器已滿時已加入伺服器!' + kick_unresolved_hostname: '&c發生了一個錯誤: 無法解析玩家的主機名' + tempban_max_logins: '&c由於登錄失敗次數過多,您已被暫時禁止。' + +# AntiBot +antibot: + kick_antibot: '伺服器正在啟用AntiBot保護模式! 您必須等待幾分鐘才能加入服務器。' + auto_enabled: '&4[AntiBotService] 伺服器由於連接數量龐大而啟用AntiBot!' + auto_disabled: '&2[AntiBotService] AntiBot將在%m分鐘後禁用!' + +# Unregister +unregister: + success: '&c帳戶已刪除!' + command_usage: '&c使用方法: "/unregister <你的密碼>"' + +# Other messages +misc: + account_not_activated: '&c你的帳戶未激活,請確認電郵!' + not_activated: '&c賬戶未激活,請註冊激活後再次嘗試.' + password_changed: '&2密碼已更變!' + logout: '&2已成功註銷!' + reload: '&2伺服器已正確地被重新加載配置和數據庫!' + usage_change_password: '&c使用方法: "/changepassword [舊密碼] [新密碼]"' + accounts_owned_self: '您擁有 %count 個帳戶:' + accounts_owned_other: '玩家 %name 擁有 %count 個帳戶:' + +# Session messages +session: + valid_session: '&2由於會話重新連接而登錄.' + invalid_session: '&c您的IP已更改,並且您的會話數據已過期!' + +# Error messages when joining +on_join_validation: + same_ip_online: '一位同一IP的玩家已在遊戲中!' + same_nick_online: '&4伺服器上已經在使用相同的用戶名!' + name_length: '&4你的暱稱太短或太長!' + characters_in_name: '&4您的暱稱含有非法字符。允許的字符:%valid_chars' + kick_full_server: '&4服務器已滿,請稍後再試!' + country_banned: '&4您的國家/地區已被禁止使用此伺服器!' + not_owner_error: '您不是此帳戶的所有者。 請選擇其他名稱!' + invalid_name_case: '您應該使用用戶名 %valid 而不是 %invalid 來加入。' + quick_command: '&c您發送命令的速度太快了,請重新加入伺服器等待一會後再使用命令' + +# Email +email: + add_email_request: '&3請使用命令: /email add [你的電郵地址] [重覆確認你的電郵地址] 將您的電子郵件添加到您的帳戶"' + usage_email_add: '&c使用方法: "/email add [電郵地址] [confirmEmail]"' + usage_email_change: '&c使用方法: "/email change [舊電郵地址] [新電郵地址]"' + new_email_invalid: '&c新電子郵件地址無效,請重試!' + old_email_invalid: '&c舊電子郵件地址無效,請重試!' + invalid: '&c電子郵件地址無效,請重試!' + added: '&2電子郵件地址已成功添加到您的帳戶!' + add_not_allowed: '&c伺服器不允許添加電子郵箱' + request_confirmation: '&c請確認你的電郵地址!' + changed: '&2已正確地更改電子郵件地址!' + change_not_allowed: '&c伺服器不允許修改郵箱地址' + email_show: '&a該賬戶使用的電子郵箱為: &a%email' + no_email_for_account: '&c當前並沒有任何郵箱與該賬戶綁定' + already_used: '&4此電子郵件地址已被使用' + incomplete_settings: '缺少必要的配置來為發送電子郵件。請聯繫管理員。' + send_failure: '&c郵件已被作廢,請檢查您的郵箱是否正常工作.' + change_password_expired: '&c您不能使用此命令更改密碼' + email_cooldown_error: '&c您需要等待 %time 後才能再次請求發送' + +# Password recovery by email +recovery: + forgot_password_hint: '&3忘記密碼了嗎? 請使用命令: "/email recovery [你的電郵地址]"' + command_usage: '&c使用方法: "/email recovery [電郵地址]"' + email_sent: '&2帳戶恢復電子郵件已成功發送! 請檢查您的電子郵件收件箱!' + code: + code_sent: '已將重設密碼的恢復代碼發送到您的電子郵件。' + incorrect: '恢復代碼錯誤!使用指令: "/email recovery [電郵地址]" 生成新的一個恢復代碼。' + tries_exceeded: '&a您已經達到輸入驗證碼次數的最大允許次數.' + correct: '&a*** 驗證通過 ***' + change_password: '&c請使用 /email setpassword <新密碼> 立即設置新的密碼' + +# Captcha +captcha: + usage_captcha: '&3T要登錄您必須使用captcha驗證碼,請使用命令: "/captcha %captcha_code"' + wrong_captcha: '&c驗證碼錯誤!請按T在聊天中輸入 "/captcha %captcha_code"' + valid_captcha: '&2驗證碼正確!' + captcha_for_registration: '&7請輸入 /captcha %captcha_code 來驗證操作.' + register_captcha_valid: '&a驗證通過, 現在可以使用 /register 註冊啦!' + +# Verification code +verification: + code_required: '&a*** 已發送驗證郵件 ***' + command_usage: '&c使用方法:/verification <驗證碼>' + incorrect_code: '&c錯誤的驗證碼.' + success: '&a*** 驗證通過 ***' + already_verified: '&a您已經通過驗證' + code_expired: '&c驗證碼已失效' + email_needed: '&c郵箱未綁定' + +# Time units +time: + second: '秒' + seconds: '秒' + minute: '分' + minutes: '分' + hour: '時' + hours: '時' + day: '天' + days: '天' + +# Two-factor authentication +two_factor: + code_created: '&2您的密碼是 %code。您可以從這裡掃描 %url' + confirmation_required: '&7請輸入“/totp confirm <驗證碼>”來確認啟動雙重驗證' + code_required: '&c請輸入“/totp code <驗證碼>”來提交驗證碼' + already_enabled: '&a雙重驗證已啟用' + enable_error_no_code: '&c驗證碼丟失' + enable_success: '&a已成功啟用雙重驗證' + enable_error_wrong_code: '&c驗證碼錯誤或者已經過期,請重新執行“/totp add”' + not_enabled_error: '&c雙重驗證未在您的賬號上啟用,請使用“/totp add”來啟用' + removed_success: '&c雙重驗證已從您的賬號上禁用' + invalid_code: '&c無效的驗證碼' + +# 3rd party features: Bedrock Auto Login +bedrock_auto_login: + success: "&a基岩版自動登錄完成" + +# 3rd party features: Login Location Fix +login_location_fix: + fix_portal: '&a你在登錄時卡在了地獄門, 現已修正' + fix_underground: '&a你被埋住了, 坐標已修正, 下次下線之前請小心!' + cannot_fix_underground: '&a你被埋住了, 坐標無法修正, 只好送你去了最高點, 自求多福吧少年~' + +# 3rd party features: Double Login Fix +double_login_fix: + fix_message: '&a已修復幽靈玩家, 請重新進入' diff --git a/plugin/platform-bukkit/src/main/resources/messages/messages_zhtw.yml b/plugin/platform-bukkit/src/main/resources/messages/messages_zhtw.yml new file mode 100644 index 00000000..a1eb1e07 --- /dev/null +++ b/plugin/platform-bukkit/src/main/resources/messages/messages_zhtw.yml @@ -0,0 +1,175 @@ +# Translators: MineWolf50, lifehome, haer0248 # +# -------------------------------------------- # +# List of global tags: +# %nl% - Goes to new line. +# %username% - Replaces the username of the player receiving the message. +# %displayname% - Replaces the nickname (and colors) of the player receiving the message. + +# Registration +registration: + disabled: '&b【AuthMe】&6已關閉註冊功能。' + name_taken: '&b【AuthMe】&6這個帳號已經被註冊過了!' + register_request: '&b【AuthMe】&6請使用 "&c/register <密碼> <確認密碼>" 來註冊。' + command_usage: '&b【AuthMe】&6用法: &c"/register <密碼> <確認密碼>"' + reg_only: '&b【AuthMe】&6請到下列網站:「 https://example.tw 」進行註冊。' + success: '&b【AuthMe】&6您已成功註冊!' + kicked_admin_registered: '&b【AuthMe】&6管理員已協助您註冊,請重新登入。' + +# Password errors on registration +password: + match_error: '&b【AuthMe】&6兩次輸入的密碼不一致!' + name_in_password: '&b【AuthMe】&6您不可以用您的 ID (遊戲名稱) 來當作密碼!' + unsafe_password: '&b【AuthMe】&6您不可以使用這個不安全的密碼!' + forbidden_characters: '&b【AuthMe】&c密碼包含非法字符,可使用:%valid_chars' + wrong_length: '&b【AuthMe】&6您的密碼 超過最大字數 / 小於最小字數' + pwned_password: '&b【AuthMe】&c你使用的密碼並不安全。它已經被使用了 %pwned_count 次! 請使用一個更強大的密碼...' + +# Login +login: + command_usage: '&b【AuthMe】&6用法: &c"/login <密碼>"' + wrong_password: '&b【AuthMe】&6密碼錯誤!' + success: '&b【AuthMe】&6密碼正確,您已成功登入!' + login_request: '&b【AuthMe】&6請使用 &c"/login <密碼>" &6來登入。' + timeout_error: '&b【AuthMe】&6超過登入時間,請稍後再試一次。' + +# Errors +error: + denied_command: '&b【AuthMe】&c使用指令之前必須通過驗證!' + denied_chat: '&b【AuthMe】&c說話之前必須通過驗證!' + unregistered_user: '&b【AuthMe】&6這個帳號還沒有註冊過。' + not_logged_in: '&b【AuthMe】&6您還沒有登入!' + no_permission: '&b【AuthMe】&6您沒有使用該指令的權限。' + unexpected_error: '&b【AuthMe】&6發生錯誤,請聯繫管理員' + max_registration: '&b【AuthMe】&6您的 IP 位置所註冊的帳號數量已經達到最大限制。' + logged_in: '&b【AuthMe】&6您已經登入了!' + kick_for_vip: '&b【AuthMe】&6您已經被請出。&c原因:有 VIP 玩家登入伺服器' + kick_unresolved_hostname: '&b【AuthMe】&6無法解析玩家主機名稱。' + tempban_max_logins: '&b【AuthMe】&c您已被暫時封鎖IP位置,因為您登入失敗太多次。' + +# AntiBot +antibot: + kick_antibot: '&b【AuthMe】&cAntiBotMod 正在啟用中,請稍後再嘗試登入吧!' + auto_enabled: '&b【AuthMe】&6AntiBotMod 已自動啟用!' + auto_disabled: '&b【AuthMe】&6AntiBotMod 將於 &c%m &6分鐘後自動關閉' + +# Unregister +unregister: + success: '&b【AuthMe】&6您已經成功註銷。' + command_usage: '&b【AuthMe】&6用法:&c"/unregister <密碼>"' + +# Other messages +misc: + account_not_activated: '&b【AuthMe】&6您的帳號還沒有經過驗證!檢查看看您的電子郵件 (Email) 吧!' + not_activated: '&b【AuthMe】&c賬戶未激活,請註冊激活後再次嘗試.' + password_changed: '&b【AuthMe】&6密碼變更成功!' + logout: '&b【AuthMe】&6您已成功登出。' + reload: '&b【AuthMe】&6已重新讀取設定檔及資料庫。' + usage_change_password: '&b【AuthMe】&6用法:&c"/changepassword <舊密碼> <新密碼>"' + accounts_owned_self: '&b【AuthMe】&6您擁有 %count 個帳號:' + accounts_owned_other: '&b【AuthMe】&6玩家 %name 擁有 %count 個帳號:' + +# Session messages +session: + valid_session: '&b【AuthMe】&6您已經成功登入!' + invalid_session: '&b【AuthMe】&6Session驗證不相符!' + +# Error messages when joining +on_join_validation: + same_ip_online: '&b【AuthMe】&6相同IP玩家在線上!' + same_nick_online: '&b【AuthMe】&6有同樣帳號的玩家在線上!' + name_length: '&b【AuthMe】&6您的暱稱 太長 / 太短 了!' + characters_in_name: '&b【AuthMe】&6暱稱裡能使用的字符為: %valid_chars' + kick_full_server: '&b【AuthMe】&6伺服器已經滿了,請等等再試一次。' + country_banned: '&b【AuthMe】&6您所在的地區無法進入此伺服器。' + not_owner_error: '&b【AuthMe】&4警告!&c您並不是此帳戶持有人,請立即登出。' + invalid_name_case: '&b【AuthMe】&4警告!&c您應該使用「%valid」而並非「%invalid」登入遊戲。' + quick_command: '&b【AuthMe】&4指令使用過快,請加入伺服器後稍等一下再使用指令。' + +# Email +email: + add_email_request: '&b【AuthMe】&6請使用 &c"/email add <電子郵件> <再次輸入電子郵件>" &6來新增電子郵件' + usage_email_add: '&b【AuthMe】&6用法: &c"/email add <電子郵件> <再次輸入電子郵件>"' + usage_email_change: '&b【AuthMe】&6用法: &c"/email change <舊的電子郵件> <新的電子郵件>"' + new_email_invalid: '&b【AuthMe】&6新的電子郵件無效!' + old_email_invalid: '&b【AuthMe】&6舊的電子郵件無效!' + invalid: '&b【AuthMe】&6無效的電子郵件!' + added: '&b【AuthMe】&6已新增電子郵件!' + add_not_allowed: '&b【AuthMe】&c不允許新增電子郵件' + request_confirmation: '&b【AuthMe】&6請驗證您的電子郵件!' + changed: '&b【AuthMe】&6電子郵件已變更!' + change_not_allowed: '&b【AuthMe】&c不允許變更電子郵件' + email_show: '&b【AuthMe】&2目前的電子郵件:&f%email' + no_email_for_account: '&b【AuthMe】&2您目前沒有設定電子郵件。' + already_used: '&b【AuthMe】&4這個電子郵件已被使用。' + incomplete_settings: '&b【AuthMe】&4因為電子郵件設定不完整導致無法傳送,請聯絡管理員。' + send_failure: '&b【AuthMe】&4無法傳送電子郵件,請聯絡管理員。' + change_password_expired: '&b【AuthMe】&6您現在不能使用這個指令變更密碼了。' + email_cooldown_error: '&b【AuthMe】&c電子郵件已經寄出了,您只能在 %time 後才能傳送。' + +# Password recovery by email +recovery: + forgot_password_hint: '&b【AuthMe】&6忘記密碼了嗎?使用 &c"/email recovery <電子郵件>"' + command_usage: '&b【AuthMe】&6用法: &c"/email recovery <電子郵件>"' + email_sent: '&b【AuthMe】&6已經送出重設密碼要求至您的電子郵件,請查收。' + code: + code_sent: '&b【AuthMe】&6忘記密碼的恢復密碼電子郵件已傳送至您的信箱中。' + incorrect: '&b【AuthMe】&6恢復密碼錯誤!您剩餘 %count 次嘗試機會。' + tries_exceeded: '&b【AuthMe】&6恢復密碼過多次數錯誤。使用 "/email recovery [電子郵件]" 取得新的恢復密碼。' + correct: '&b【AuthMe】&6恢復密碼正確!' + change_password: '&b【AuthMe】&6請使用 "/email setpassword <新密碼>" 變更您的密碼。' + +# Captcha +captcha: + usage_captcha: '&b【AuthMe】&6請用 &c"/captcha %captcha_code" &6來輸入您的驗證碼。' + wrong_captcha: '&b【AuthMe】&6錯誤的驗證碼,請使用 "/captcha %captcha_code" 再試一次。' + valid_captcha: '&b【AuthMe】&6驗證碼無效。' + captcha_for_registration: '&b【AuthMe】&6註冊前必須先提供驗證碼,使用 /captcha %captcha_code 來驗證。' + register_captcha_valid: '&b【AuthMe】&2驗證已通過,現在可以使用 /register 來進行註冊了。' + +# Verification code +verification: + code_required: '&b【AuthMe】&3敏感指令,需要電子郵件驗證後才能執行,請檢查電子郵件。' + command_usage: '&b【AuthMe】&c用法:/verification <驗證碼>' + incorrect_code: '&b【AuthMe】&c驗證碼錯誤,請在聊天室使用 "/verification <驗證碼>" 電子郵件收到的驗證碼' + success: '&b【AuthMe】&2身分已驗證,您現在可以使用所有指令!' + already_verified: '&b【AuthMe】&2您已經可以使用所有指令!' + code_expired: '&b【AuthMe】&3驗證碼已過期,請使用其他敏感指令來取得新的驗證碼!' + email_needed: '&b【AuthMe】&3若需要身分驗證,請先新增電子郵件!' + +# Time units +time: + second: '秒' + seconds: '秒' + minute: '分' + minutes: '分' + hour: '時' + hours: '時' + day: '天' + days: '天' + +# Two-factor authentication +two_factor: + code_created: '&b【AuthMe】&b您的登入金鑰為&9「%c%code&9」&b,掃描連結為:&c %url' + confirmation_required: '&b【AuthMe】&6請使用 /2fa confirm <驗證碼> 來確認雙重驗證。' + code_required: '&b【AuthMe】&c請使用 /2fa code <驗證碼> 來完成驗證。' + already_enabled: '&b【AuthMe】&c雙重驗證已經開啟。' + enable_error_no_code: '&b【AuthMe】&6雙重驗證代碼不存在或失效,使用 /2fa add 來新增。' + enable_success: '&b【AuthMe】&6雙重驗證已開啟!' + enable_error_wrong_code: '&b【AuthMe】&6雙重驗證代碼錯誤或失效,使用 /2fa add 來新增。' + not_enabled_error: '&b【AuthMe】&6雙重驗證尚未開啟,使用 /2fa add 來開啟。' + removed_success: '&b【AuthMe】&6雙重驗證已成功移除!' + invalid_code: '&b【AuthMe】&c驗證碼錯誤。' + +# 3rd party features: Bedrock Auto Login +bedrock_auto_login: + success: "&b【AuthMe】&a基岩版自動登錄完成" + +# 3rd party features: Login Location Fix +login_location_fix: + fix_portal: '&b【AuthMe】&a你在登錄時卡在了地獄門, 現已修正' + fix_underground: '&b【AuthMe】&a你被埋住了, 坐標已修正, 下次下線之前請小心!' + cannot_fix_underground: '&b【AuthMe】&a你被埋住了, 坐標無法修正, 只好送你去了最高點, 自求多福吧少年~' + +# 3rd party features: Double Login Fix +double_login_fix: + fix_message: '&b【AuthMe】&a已修復幽靈玩家, 請重新進入' diff --git a/plugin/platform-bukkit/src/main/resources/new_email.html b/plugin/platform-bukkit/src/main/resources/new_email.html new file mode 100644 index 00000000..74a1a087 --- /dev/null +++ b/plugin/platform-bukkit/src/main/resources/new_email.html @@ -0,0 +1,127 @@ +
+ +
+ + + + + + +
+ + + + + + + + + + + + +
+

账户激活邮件

+
+ + + + + + +
+

Minecraft ·

+
+ + + + + + + + + + + + + + + + + + +
+ 您的账户初始密码为 +
+
+
已将地址()进行记录. +
+
请妥善保存,在新地址上进行登录时,需提供该密码. +
若非必要,请勿更换密码,否则将对您的账户安全构成威胁. +
账户所绑定的邮箱地址已被永久存储,需要更换请联系管理员. +
+
更换密码: /changepassword [新密码] +
+
+
账户将在激活后生效 +
欢迎您的加入~! +
+ +
+
+
+
+ + + + + + +
+

© 2024 HomoCraft. All rights reserved.

+ wdsj.in +
+
+
+
+
diff --git a/plugin/platform-bukkit/src/main/resources/otheraccounts.yml b/plugin/platform-bukkit/src/main/resources/otheraccounts.yml new file mode 100644 index 00000000..e69de29b diff --git a/plugin/platform-bukkit/src/main/resources/players.yml b/plugin/platform-bukkit/src/main/resources/players.yml new file mode 100644 index 00000000..cf746f52 --- /dev/null +++ b/plugin/platform-bukkit/src/main/resources/players.yml @@ -0,0 +1 @@ +players: [] \ No newline at end of file diff --git a/plugin/platform-bukkit/src/main/resources/plugin.yml b/plugin/platform-bukkit/src/main/resources/plugin.yml new file mode 100644 index 00000000..6414af78 --- /dev/null +++ b/plugin/platform-bukkit/src/main/resources/plugin.yml @@ -0,0 +1,316 @@ +name: AuthMe +authors: [sgdc3, games647, Hex3l, krusic22, DGun Otto] +website: https://github.com/HaHaWTH/AuthMeReReloaded/ +description: A fork of AuthMeReloaded that contains bug fixes +main: fr.xephi.authme.AuthMe +folia-supported: true +version: 5.6.0-FORK-b50 +api-version: 1.13 +softdepend: + - Vault + - LuckPerms + - PermissionsEx + - bPermissions + - zPermissions + - Multiverse-Core + - Essentials + - EssentialsSpawn + - ProtocolLib + - floodgate +commands: + authme: + description: AuthMe op commands + usage: /authme register|unregister|forcelogin|password|lastlogin|accounts|email|setemail|getip|totp|disabletotp|spawn|setspawn|firstspawn|setfirstspawn|purge|purgeplayer|backup|resetpos|purgebannedplayers|switchantibot|reload|version|converter|messages|recent|debug + email: + description: Add email or recover password + usage: /email show|add|change|recover|code|setpassword + login: + description: Login command + usage: /login + aliases: + - l + logout: + description: Logout command + usage: /logout + register: + description: Register an account + usage: /register [password] [verifyPassword] + aliases: + - reg + unregister: + description: Unregister an account + usage: /unregister + aliases: + - unreg + changepassword: + description: Change password of an account + usage: /changepassword + aliases: + - changepass + - cp + totp: + description: TOTP commands + usage: /totp code|add|confirm|remove + aliases: + - 2fa + captcha: + description: Captcha command + usage: /captcha + verification: + description: Verification command + usage: /verification +permissions: + authme.admin.*: + description: Gives access to all admin commands + children: + authme.admin.accounts: true + authme.admin.antibotmessages: true + authme.admin.backup: true + authme.admin.changemail: true + authme.admin.changepassword: true + authme.admin.converter: true + authme.admin.firstspawn: true + authme.admin.forcelogin: true + authme.admin.getemail: true + authme.admin.getip: true + authme.admin.lastlogin: true + authme.admin.purge: true + authme.admin.purgebannedplayers: true + authme.admin.purgelastpos: true + authme.admin.purgeplayer: true + authme.admin.register: true + authme.admin.reload: true + authme.admin.seeotheraccounts: true + authme.admin.seerecent: true + authme.admin.setfirstspawn: true + authme.admin.setspawn: true + authme.admin.spawn: true + authme.admin.switchantibot: true + authme.admin.totpdisable: true + authme.admin.totpviewstatus: true + authme.admin.unregister: true + authme.admin.updatemessages: true + authme.admin.accounts: + description: Administrator command to see all accounts associated with a user. + default: op + authme.admin.antibotmessages: + description: Permission to see Antibot messages. + default: op + authme.admin.backup: + description: Allows to use the backup command. + default: op + authme.admin.changemail: + description: Administrator command to set or change the email address of a user. + default: op + authme.admin.changepassword: + description: Administrator command to change the password of a user. + default: op + authme.admin.converter: + description: Administrator command to convert old or other data to AuthMe data. + default: op + authme.admin.firstspawn: + description: Administrator command to teleport to the first AuthMe spawn. + default: op + authme.admin.forcelogin: + description: Administrator command to force-login an existing user. + default: op + authme.admin.getemail: + description: Administrator command to get the email address of a user, if set. + default: op + authme.admin.getip: + description: Administrator command to get the last known IP of a user. + default: op + authme.admin.lastlogin: + description: Administrator command to see the last login date and time of a user. + default: op + authme.admin.purge: + description: Administrator command to purge old user data. + default: op + authme.admin.purgebannedplayers: + description: Administrator command to purge all data associated with banned players. + default: op + authme.admin.purgelastpos: + description: Administrator command to purge the last position of a user. + default: op + authme.admin.purgeplayer: + description: Administrator command to purge a given player. + default: op + authme.admin.register: + description: Administrator command to register a new user. + default: op + authme.admin.reload: + description: Administrator command to reload the plugin configuration. + default: op + authme.admin.seeotheraccounts: + description: Permission to see the other accounts of the players that log in. + default: op + authme.admin.seerecent: + description: Administrator command to see the last recently logged in players. + default: op + authme.admin.setfirstspawn: + description: Administrator command to set the first AuthMe spawn. + default: op + authme.admin.setspawn: + description: Administrator command to set the AuthMe spawn. + default: op + authme.admin.spawn: + description: Administrator command to teleport to the AuthMe spawn. + default: op + authme.admin.switchantibot: + description: Administrator command to toggle the AntiBot protection status. + default: op + authme.admin.totpdisable: + description: Administrator command to disable the two-factor auth of a user. + default: op + authme.admin.totpviewstatus: + description: Administrator command to see whether a player has enabled two-factor + authentication. + default: op + authme.admin.unregister: + description: Administrator command to unregister an existing user. + default: op + authme.admin.updatemessages: + description: Permission to use the update messages command. + default: op + authme.allowchatbeforelogin: + description: Permission to send chat messages before being logged in. + default: false + authme.allowmultipleaccounts: + description: Permission to be able to register multiple accounts. + default: op + authme.bypassbungeesend: + description: Permission node to bypass BungeeCord server teleportation. + default: false + authme.bypassantibot: + description: Permission node to bypass AntiBot protection. + default: op + authme.bypasscountrycheck: + description: Permission to bypass the GeoIp country code check. + default: false + authme.bypassforcesurvival: + description: Permission for users to bypass force-survival mode. + default: op + authme.bypasspurge: + description: Permission to bypass the purging process. + default: false + authme.debug: + description: Gives access to /authme debug and all its sections + children: + authme.debug.command: true + authme.debug.country: true + authme.debug.db: true + authme.debug.group: true + authme.debug.limbo: true + authme.debug.mail: true + authme.debug.mysqldef: true + authme.debug.perm: true + authme.debug.spawn: true + authme.debug.stats: true + authme.debug.valid: true + authme.debug.command: + description: General permission to use the /authme debug command. + default: op + authme.debug.country: + description: Permission to use the country lookup section. + default: op + authme.debug.db: + description: Permission to view data from the database. + default: op + authme.debug.group: + description: Permission to view permission groups. + default: op + authme.debug.limbo: + description: Permission to use the limbo data viewer. + default: op + authme.debug.mail: + description: Permission to use the test email sender. + default: op + authme.debug.mysqldef: + description: Permission to change nullable status of MySQL columns. + default: op + authme.debug.perm: + description: Permission to use the permission checker. + default: op + authme.debug.spawn: + description: Permission to view spawn information. + default: op + authme.debug.stats: + description: Permission to use the stats section. + default: op + authme.debug.valid: + description: Permission to use sample validation. + default: op + authme.player.*: + description: Gives access to all player commands + children: + authme.player.canbeforced: true + authme.player.captcha: true + authme.player.changepassword: true + authme.player.email.add: true + authme.player.email.recover: true + authme.player.email.see: true + authme.player.login: true + authme.player.logout: true + authme.player.protection.quickcommandsprotection: true + authme.player.register: true + authme.player.security.verificationcode: true + authme.player.totpadd: true + authme.player.totpremove: true + authme.player.canbeforced: + description: Permission for users a login can be forced to. + default: true + authme.player.captcha: + description: Command permission to use captcha. + default: true + authme.player.changepassword: + description: Command permission to change the password. + default: true + authme.player.email: + description: Gives access to all email commands + children: + authme.player.email.add: true + authme.player.email.recover: true + authme.player.email.see: true + authme.player.email.add: + description: Command permission to add an email address. + default: true + authme.player.email.change: + description: Command permission to change the email address. + default: op + authme.player.email.recover: + description: Command permission to recover an account using its email address. + default: true + authme.player.email.see: + description: Command permission to see the own email address. + default: true + authme.player.login: + description: Command permission to login. + default: true + authme.player.logout: + description: Command permission to logout. + default: true + authme.player.protection.quickcommandsprotection: + description: Permission that enables on join quick commands checks for the player. + default: true + authme.player.register: + description: Command permission to register. + default: true + authme.player.security.verificationcode: + description: Permission to use the email verification codes feature. + default: true + authme.player.seeownaccounts: + description: Permission to use to see own other accounts. + default: true + authme.player.totpadd: + description: Permission to enable two-factor authentication. + default: true + authme.player.totpremove: + description: Permission to disable two-factor authentication. + default: true + authme.player.unregister: + description: Command permission to unregister. + default: op + authme.vip: + description: When the server is full and someone with this permission joins the + server, someone will be kicked. + default: false diff --git a/plugin/platform-bukkit/src/main/resources/recovery_code_email.html b/plugin/platform-bukkit/src/main/resources/recovery_code_email.html new file mode 100644 index 00000000..c41c00c9 --- /dev/null +++ b/plugin/platform-bukkit/src/main/resources/recovery_code_email.html @@ -0,0 +1,120 @@ +
+ +
+ + + + + + +
+ + + + + + + + + + + + +
+

代码验证邮件

+
+ + + + + + +
+

Minecraft ·

+
+ + + + + + + + + + + + + + + + + + +
+ 您正在申请的验证码为 +
+
+
使用指令: /email code 来完成验证过程. +
+
+
验证码将在小时后失效 +
+ +
+
+
+
+ + + + + + +
+

© 2024 HomoCraft. All rights reserved.

+ wdsj.in +
+
+
+
+
diff --git a/plugin/platform-bukkit/src/main/resources/shutdown.html b/plugin/platform-bukkit/src/main/resources/shutdown.html new file mode 100644 index 00000000..2819ebfc --- /dev/null +++ b/plugin/platform-bukkit/src/main/resources/shutdown.html @@ -0,0 +1,118 @@ +
+ +
+ + + + + + +
+ + + + + + + + + + + + +
+

服务器关闭通知

+
+ + + + + + +
+

Minecraft ·

+
+ + + + + + + + + + + + + + + +
+
紧急通知
+
服务器当前已被关闭 +
+
请及时检查服务器运行状态. +
+
+
+
IrisCraft Team +
+ +
+
+
+
+ + + + + + +
+

© 2024 HomoCraft. All rights reserved.

+ wdsj.in +
+
+
+
+
diff --git a/plugin/platform-bukkit/src/main/resources/spawn.yml b/plugin/platform-bukkit/src/main/resources/spawn.yml new file mode 100644 index 00000000..5191803c --- /dev/null +++ b/plugin/platform-bukkit/src/main/resources/spawn.yml @@ -0,0 +1,14 @@ +spawn: + world: '' + x: '' + y: '' + z: '' + yaw: '' + pitch: '' +firstspawn: + world: '' + x: '' + y: '' + z: '' + yaw: '' + pitch: '' \ No newline at end of file diff --git a/plugin/platform-bukkit/src/main/resources/verification_code_email.html b/plugin/platform-bukkit/src/main/resources/verification_code_email.html new file mode 100644 index 00000000..0e8d1ddd --- /dev/null +++ b/plugin/platform-bukkit/src/main/resources/verification_code_email.html @@ -0,0 +1,120 @@ +
+ +
+ + + + + + +
+ + + + + + + + + + + + +
+

代码验证邮件

+
+ + + + + + +
+

Minecraft ·

+
+ + + + + + + + + + + + + + + + + + +
+ 您正在申请的验证码为 +
+
+
使用指令: /verification 来完成验证过程. +
+
+
验证码将在30分钟后失效 +
+ +
+
+
+
+ + + + + + +
+

© 2024 HomoCraft. All rights reserved.

+ wdsj.in +
+
+
+
+
diff --git a/project/build.gradle.kts b/project/build.gradle.kts index 2a6624a7..95b02754 100644 --- a/project/build.gradle.kts +++ b/project/build.gradle.kts @@ -1,8 +1,4 @@ - -description = "Fork of the first authentication plugin for the Bukkit API!" - repositories { - mavenCentral() } dependencies { diff --git a/project/module-configuration/build.gradle.kts b/project/module-configuration/build.gradle.kts new file mode 100644 index 00000000..368454ff --- /dev/null +++ b/project/module-configuration/build.gradle.kts @@ -0,0 +1,22 @@ +/* + 代码来自 Taboolib:module-configuration + 链接 https://github.com/TabooLib/taboolib/blob/master/module/module-configuration + */ + +dependencies { + compileOnly(project(":project:module-util")) + implementation("org.yaml:snakeyaml:2.2") + implementation("com.electronwill.night-config:core:3.6.7") + implementation("com.electronwill.night-config:yaml:3.6.7") { + exclude("org.yaml", "snakeyaml") + } +} + +tasks { + shadowJar { + // NightConfig + relocate("com.electronwill.nightconfig", "com.electronwill.nightconfig_3_6_7") + // Snakeyaml + relocate("org.yaml.snakeyaml", "org.yaml.snakeyaml_2_2") + } +} \ No newline at end of file diff --git a/project/module-configuration/src/main/kotlin/fr/xephi/authme/configruation/Comments.kt b/project/module-configuration/src/main/kotlin/fr/xephi/authme/configruation/Comments.kt new file mode 100644 index 00000000..35056c19 --- /dev/null +++ b/project/module-configuration/src/main/kotlin/fr/xephi/authme/configruation/Comments.kt @@ -0,0 +1,12 @@ +package fr.xephi.authme.configruation + +/** + * Comments + * + * @author Taboolib + * @since 2024/7/10 20:04 + */ + +class Commented(val value: Any?, val comment: String) + +class CommentedList(val value: Any?, val comment: List) \ No newline at end of file diff --git a/project/module-configuration/src/main/kotlin/fr/xephi/authme/configruation/ConfigFile.kt b/project/module-configuration/src/main/kotlin/fr/xephi/authme/configruation/ConfigFile.kt new file mode 100644 index 00000000..40735393 --- /dev/null +++ b/project/module-configuration/src/main/kotlin/fr/xephi/authme/configruation/ConfigFile.kt @@ -0,0 +1,103 @@ +package fr.xephi.authme.configruation + +/** + * ConfigFile + * + * @author Taboolib + * @since 2024/7/10 19:57 + */ +import com.electronwill.nightconfig.core.Config +import com.electronwill.nightconfig.core.file.FileNotFoundAction +import com.electronwill.nightconfig.core.io.ConfigParser +import com.electronwill.nightconfig.core.io.ParsingMode +import java.io.File +import java.io.InputStream +import java.io.Reader +import java.text.SimpleDateFormat + +open class ConfigFile(root: Config) : ConfigSection(root), Configuration { + + override var file: File? = null + + private val reloadCallback = ArrayList() + + override fun onReload(runnable: Runnable) { + reloadCallback.add(runnable) + } + + override fun saveToString(): String { + return toString() + } + + override fun saveToFile(file: File?) { + (file ?: this.file)?.writeText(saveToString()) ?: error("File not specified") + } + + override fun loadFromFile(file: File) { + this.file = file + try { + clear() + parser().parse(file, root, ParsingMode.REPLACE, FileNotFoundAction.THROW_ERROR) + } catch (ex: Exception) { + if (file.extension != "bak") { + file.copyTo( + File( + file.parent, + file.name + "_" + SimpleDateFormat("yyyyMMddHHmmss").format(System.currentTimeMillis()) + ".bak" + ) + ) + } + println("[warning] File: $file") + throw ex + } + reloadCallback.forEach { it.run() } + } + + override fun loadFromString(contents: String) { + try { + clear() + parser().parse(contents, root, ParsingMode.REPLACE) + } catch (t: Exception) { + println("[warning] Source: \n$contents") + throw t + } + reloadCallback.forEach { it.run() } + } + + override fun loadFromReader(reader: Reader) { + clear() + parser().parse(reader, root, ParsingMode.REPLACE) + reloadCallback.forEach { it.run() } + } + + override fun loadFromInputStream(inputStream: InputStream) { + clear() + parser().parse(inputStream, root, ParsingMode.REPLACE) + reloadCallback.forEach { it.run() } + } + + override fun reload() { + loadFromFile(file ?: return) + } + + override fun changeType(type: Type) { + val format = type.newFormat() + fun process(value: Any) { + when (value) { + is Map<*, *> -> value.forEach { process(it.value ?: return@forEach) } + is List<*> -> value.forEach { process(it ?: return@forEach) } + is Config -> { + val field = value::class.java.getField("configFormat") + field.isAccessible = true + field.set(value, format) + value.valueMap().forEach { process(it.value ?: return) } + } + } + } + process(root) + } + + private fun parser(): ConfigParser { + return root.configFormat().createParser() + } +} \ No newline at end of file diff --git a/project/module-configuration/src/main/kotlin/fr/xephi/authme/configruation/ConfigSection.kt b/project/module-configuration/src/main/kotlin/fr/xephi/authme/configruation/ConfigSection.kt new file mode 100644 index 00000000..c84a63da --- /dev/null +++ b/project/module-configuration/src/main/kotlin/fr/xephi/authme/configruation/ConfigSection.kt @@ -0,0 +1,332 @@ +package fr.xephi.authme.configruation + +import com.electronwill.nightconfig.core.CommentedConfig +import com.electronwill.nightconfig.core.Config +import com.electronwill.nightconfig.core.EnumGetMethod +import org.tabooproject.reflex.Reflex.Companion.setProperty +import taboolib.common.util.decodeUnicode +import taboolib.common5.Coerce +import taboolib.library.configuration.ConfigurationSection +import taboolib.module.configuration.util.Commented +import taboolib.module.configuration.util.CommentedList + +/** + * ConfigSection + * + * @author Taboolib + * @since 2024/7/10 19:33 + */ +open class ConfigSection(var root: Config, override val name: String = "", override val parent: ConfigurationSection? = null) : ConfigurationSection { + + private val configType = Type.getType(root.configFormat()) + + override val primitiveConfig: Any + get() = root + + override val type: Type + get() = configType + + override fun getKeys(deep: Boolean): Set { + val keys = LinkedHashSet() + fun process(map: Map, parent: String = "") { + map.forEach { (k, v) -> + if (v is Config) { + if (deep) { + process(v.valueMap(), "$parent$k.") + } else { + keys += "$parent$k" + } + } else { + keys += "$parent$k" + } + } + } + process(root.valueMap()) + return keys + } + + override fun contains(path: String): Boolean { + return root.contains(path) + } + + override fun get(path: String): Any? { + return get(path, null) + } + + override fun get(path: String, def: Any?): Any? { + // 不知道为什么大家都喜欢用 getConfigurationSection("") + // 感觉都是一个师傅教的 + if (path.isEmpty()) { + return this + } + var name = path + var parent: ConfigurationSection? = null + if (path.contains('.')) { + name = path.substringAfterLast('.') + parent = getConfigurationSection(path.substringBeforeLast('.').substringAfterLast('.')) + } + return when (val value = root.getOrElse(path, def)) { + is Config -> ConfigSection(value, name, parent) + // 理论是无法获取到 Map 类型 + // 因为在 set 方法中 Map 会被转换为 Config 类型 + is Map<*, *> -> { + val subConfig = root.createSubConfig() + subConfig.setProperty("map", value) + ConfigSection(subConfig, name, parent) + } + else -> unwrap(value) + } + } + + override fun set(path: String, value: Any?) { + when { + value == null -> root.remove(path) + value is List<*> -> root.set(path, unwrap(value, this)) + value is Collection<*> && value !is List<*> -> set(path, value.toList()) + value is ConfigurationSection -> set(path, value.getConfig()) + value is Map<*, *> -> set(path, value.toConfig(this)) + value is Commented -> { + set(path, value.value) + setComment(path, value.comment) + } + value is CommentedList -> { + set(path, value.value) + setComments(path, value.comment) + } + else -> root.set(path, value) + } + } + + override fun getString(path: String): String? { + val value = get(path) ?: return null + return if (value is List<*>) value.joinToString("\n") else value.toString() + } + + override fun getString(path: String, def: String?): String? { + return getString(path) ?: def + } + + override fun isString(path: String): Boolean { + return get(path) is String + } + + override fun getInt(path: String): Int { + return Coerce.toInteger(get(path)) + } + + override fun getInt(path: String, def: Int): Int { + return Coerce.toInteger(get(path) ?: def) + } + + override fun isInt(path: String): Boolean { + val value = get(path) + return value is Long || value is Int + } + + override fun getBoolean(path: String): Boolean { + return Coerce.toBoolean(get(path)) + } + + override fun getBoolean(path: String, def: Boolean): Boolean { + return Coerce.toBoolean(get(path) ?: def) + } + + override fun isBoolean(path: String): Boolean { + return get(path) is Double + } + + override fun getDouble(path: String): Double { + return Coerce.toDouble(get(path)) + } + + override fun getDouble(path: String, def: Double): Double { + return Coerce.toDouble(get(path) ?: def) + } + + override fun isDouble(path: String): Boolean { + return get(path) is Double + } + + override fun getLong(path: String): Long { + return Coerce.toLong(get(path)) + } + + override fun getLong(path: String, def: Long): Long { + return Coerce.toLong(get(path) ?: def) + } + + override fun isLong(path: String): Boolean { + return get(path) is Long + } + + override fun getList(path: String): List<*>? { + return (get(path) as? List<*>)?.map { unwrap(it) }?.toList() + } + + override fun getList(path: String, def: List<*>?): List<*>? { + return get(path) as? List<*> ?: def + } + + override fun isList(path: String): Boolean { + return get(path) is List<*> + } + + override fun getStringList(path: String): List { + return getList(path)?.map { it.toString() }?.toList() ?: ArrayList() + } + + override fun getIntegerList(path: String): List { + return getList(path)?.map { Coerce.toInteger(it) }?.toList() ?: ArrayList() + } + + override fun getBooleanList(path: String): List { + return getList(path)?.map { Coerce.toBoolean(it) }?.toList() ?: ArrayList() + } + + override fun getDoubleList(path: String): List { + return getList(path)?.map { Coerce.toDouble(it) }?.toList() ?: ArrayList() + } + + override fun getFloatList(path: String): List { + return getList(path)?.map { Coerce.toFloat(it) }?.toList() ?: ArrayList() + } + + override fun getLongList(path: String): List { + return getList(path)?.map { Coerce.toLong(it) }?.toList() ?: ArrayList() + } + + override fun getByteList(path: String): List { + return getList(path)?.map { Coerce.toByte(it) }?.toList() ?: ArrayList() + } + + override fun getCharacterList(path: String): List { + return getList(path)?.map { Coerce.toChar(it) }?.toList() ?: ArrayList() + } + + override fun getShortList(path: String): List { + return getList(path)?.map { Coerce.toShort(it) }?.toList() ?: ArrayList() + } + + override fun getMapList(path: String): List> { + return getList(path)?.filterIsInstance>()?.toList() ?: ArrayList() + } + + override fun getConfigurationSection(path: String): ConfigurationSection? { + return get(path) as? ConfigurationSection + } + + override fun isConfigurationSection(path: String): Boolean { + return get(path) is ConfigurationSection + } + + override fun > getEnum(path: String, type: Class): T? { + return root.getEnum(path, type) + } + + override fun > getEnumList(path: String, type: Class): List { + return getStringList(path).mapNotNull { EnumGetMethod.NAME_IGNORECASE.get(it, type) } + } + + override fun createSection(path: String): ConfigurationSection { + val subConfig = root.createSubConfig() + set(path, subConfig) + var name = path + var parent: ConfigurationSection? = null + if (path.contains('.')) { + name = path.substringAfterLast('.') + parent = getConfigurationSection(path.substringBeforeLast('.').substringAfterLast('.')) + } + return ConfigSection(subConfig, name, parent) + } + + override fun toMap(): Map { + fun process(map: Map): Map { + val newMap = LinkedHashMap() + map.forEach { (k, v) -> newMap[k] = unwrap(v) } + return newMap + } + return process(root.valueMap()) + } + + override fun getComment(path: String): String? { + return (root as? CommentedConfig)?.getComment(path) + } + + override fun getComments(path: String): List { + return getComment(path)?.lines() ?: emptyList() + } + + override fun setComment(path: String, comment: String?) { + (root as? CommentedConfig)?.setComment(path, if (comment?.isBlank() == true) null else comment) + } + + override fun setComments(path: String, comments: List) { + return setComment(path, comments.joinToString("\n")) + } + + override fun addComments(path: String, comments: List) { + getComments(path).toMutableList().apply { + addAll(comments) + setComments(path, this) + } + } + + override fun getValues(deep: Boolean): Map { + return getKeys(deep).associateWith { get(it) } + } + + override fun toString(): String { + return root.configFormat().createWriter().writeToString(root) + } + + override fun clear() { + root.clear() + } + + companion object { + + private fun ConfigurationSection.getConfig(): Config { + return if (this is ConfigSection) root else error("Not supported") + } + + private fun Map<*, *>.toConfig(parent: ConfigSection): Config { + val section = ConfigSection(parent.root.createSubConfig()) + forEach { (k, v) -> section[k.toString()] = v } + return section.root + } + + /** + * 解包 + * 要么变为原始类型,要么变成 Map + */ + fun unwrap(v: Any?): Any? { + return when (v) { + "~", "null" -> null + "''", "\"\"" -> "" + else -> when (v) { + is ConfigSection -> v.toMap() + is ConfigurationSection -> unwrap(v.getConfig()) + is Config -> unwrap(v.valueMap()) + is Collection<*> -> v.map { unwrap(it) }.toList() + is Map<*, *> -> v.map { it.key to unwrap(it.value) }.toMap() + is String -> StringUtil.decodeUnicode(v) + else -> v + } + } + } + + fun unwrap(list: List<*>, parent: ConfigSection): List<*> { + fun process(value: Any?): Any? { + return when { + value is List<*> -> unwrap(value, parent) + value is Collection<*> && value !is List<*> -> value.toList() + value is ConfigurationSection -> value.getConfig() + value is Map<*, *> -> value.toConfig(parent) + else -> value + } + } + return list.map { process(it) } + } + } + +} \ No newline at end of file diff --git a/project/module-configuration/src/main/kotlin/fr/xephi/authme/configruation/Configuration.kt b/project/module-configuration/src/main/kotlin/fr/xephi/authme/configruation/Configuration.kt new file mode 100644 index 00000000..11e69556 --- /dev/null +++ b/project/module-configuration/src/main/kotlin/fr/xephi/authme/configruation/Configuration.kt @@ -0,0 +1,217 @@ +package fr.xephi.authme.configruation + +import java.io.File +import java.io.InputStream +import java.io.Reader + +/** + * Configuration + * + * @author Taboolib + * @since 2024/7/10 19:30 + */ +interface Configuration : ConfigurationSection { + + /** + * 文件 + */ + var file: File? + + /** + * 保存为字符串 + */ + fun saveToString(): String + + /** + * 保存到文件 + * + * @param file 文件 + */ + fun saveToFile(file: File? = null) + + /** + * 从文件加载 + * + * @param file 文件 + */ + fun loadFromFile(file: File) + + /** + * 从字符串加载 + * + * @param contents 字符串 + */ + fun loadFromString(contents: String) + + /** + * 从 [Reader] 加载 + * + * @param reader 输入流 + */ + fun loadFromReader(reader: Reader) + + /** + * 从 [InputStream] 加载 + * + * @param inputStream 输入流 + */ + fun loadFromInputStream(inputStream: InputStream) + + /** + * 重载 + */ + fun reload() + + /** + * 注册重载回调 + * + * @param runnable 回调 + */ + fun onReload(runnable: Runnable) + + /** + * 变更类型 + * + * @param type 类型 + */ + fun changeType(type: Type) + + companion object { + + /** + * 识别可能的 [ConfigurationSection] 类型 + */ + fun parse(any: Any, type: Type = Type.YAML, concurrent: Boolean = true): ConfigurationSection { + val unwrapped = ConfigSection.unwrap(any) + if (unwrapped is Map<*, *>) { + return fromMap(unwrapped, type, concurrent) + } + return empty(type, concurrent) + } + + /** + * 创建空配置 + * + * @param type 类型 + * @param concurrent 是否支持并发 + * @return [Configuration] + */ + fun empty(type: Type = Type.YAML, concurrent: Boolean = true): Configuration { + return ConfigFile( + if (concurrent) type.newFormat().createConcurrentConfig() else type.newFormat() + .createConfig { LinkedHashMap() }) + } + + /** + * 从文件加载 + * + * @param file 文件 + * @param type 类型 + * @param concurrent 是否支持并发 + * @return [Configuration] + */ + fun loadFromFile(file: File, type: Type? = null, concurrent: Boolean = true): Configuration { + val format = (type ?: getTypeFromFile(file)).newFormat() + val configFile = + ConfigFile(if (concurrent) format.createConcurrentConfig() else format.createConfig { LinkedHashMap() }) + configFile.loadFromFile(file) + return configFile + } + + /** + * 从 [Reader] 加载 + * + * @param reader Reader + * @param type 类型 + * @param concurrent 是否支持并发 + * @return [Configuration] + */ + fun loadFromReader(reader: Reader, type: Type = Type.YAML, concurrent: Boolean = true): Configuration { + val format = type.newFormat() + val configFile = + ConfigFile(if (concurrent) format.createConcurrentConfig() else format.createConfig { LinkedHashMap() }) + configFile.loadFromReader(reader) + return configFile + } + + /** + * 从字符串加载 + * + * @param contents 字符串 + * @param type 类型 + * @param concurrent 是否支持并发 + * @return [Configuration] + */ + fun loadFromString(contents: String, type: Type = Type.YAML, concurrent: Boolean = true): Configuration { + val format = type.newFormat() + val configFile = + ConfigFile(if (concurrent) format.createConcurrentConfig() else format.createConfig { LinkedHashMap() }) + configFile.loadFromString(contents) + return configFile + } + + /** + * 从 [InputStream] 加载 + * + * @param inputStream 输入流 + * @param type 类型 + * @param concurrent 是否支持并发 + * @return [Configuration] + */ + fun loadFromInputStream( + inputStream: InputStream, + type: Type = Type.YAML, + concurrent: Boolean = true + ): Configuration { + val format = type.newFormat() + val configFile = + ConfigFile(if (concurrent) format.createConcurrentConfig() else format.createConfig { LinkedHashMap() }) + configFile.loadFromInputStream(inputStream) + return configFile + } + + /** + * 从 Map 加载 [ConfigurationSection] + * + * @param map [Map] + * @param type 类型 + * @param concurrent 是否支持并发 + * @return [ConfigurationSection] + */ + fun fromMap(map: Map<*, *>, type: Type = Type.YAML, concurrent: Boolean = true): ConfigurationSection { + val empty = empty(type, concurrent) + map.forEach { (k, v) -> empty[k.toString()] = v } + return empty + } + + /** + * 从文件获取类型 + * + * @param file 文件 + * @param def 默认类型 + * @return [Type] + */ + fun getTypeFromFile(file: File, def: Type = Type.YAML): Type { + return getTypeFromExtension(file.extension, def) + } + + /** + * 从文件扩展名获取类型 + * + * @param extension 扩展名 + * @param def 默认类型 + * @return [Type] + */ + fun getTypeFromExtension(extension: String, def: Type = Type.YAML): Type { + return when (extension) { + "yaml", "yml" -> Type.YAML +// "toml", "tml" -> Type.TOML +// "json" -> Type.JSON +// "conf" -> Type.HOCON + else -> def + } + } + + } + +} \ No newline at end of file diff --git a/project/module-configuration/src/main/kotlin/fr/xephi/authme/configruation/ConfigurationSection.kt b/project/module-configuration/src/main/kotlin/fr/xephi/authme/configruation/ConfigurationSection.kt new file mode 100644 index 00000000..7184291b --- /dev/null +++ b/project/module-configuration/src/main/kotlin/fr/xephi/authme/configruation/ConfigurationSection.kt @@ -0,0 +1,538 @@ +package fr.xephi.authme.configruation + +/** + * ConfigurationSection + * + * @author Bukkit, 坏黑 + * @since 2024/7/10 19:29 + */ +interface ConfigurationSection { + + /** 原始类型 */ + val primitiveConfig: Any + + /** 父节点 */ + val parent: ConfigurationSection? + + /** 节点名 */ + val name: String + + /** 节点类型 */ + val type: Type + + /** + * Gets a set containing all keys in this section. + * + * + * If deep is set to true, then this will contain all the keys within any + * child [ConfigurationSection]s (and their children, etc). These + * will be in a valid path notation for you to use. + * + * + * If deep is set to false, then this will contain only the keys of any + * direct children, and not their own children. + * + * @param deep Whether or not to get a deep list, as opposed to a shallow + * list. + * @return Set of keys contained within this ConfigurationSection. + */ + fun getKeys(deep: Boolean): Set + + /** + * Checks if this [ConfigurationSection] contains the given path. + * + * + * If the value for the requested path does not exist but a default value + * has been specified, this will return true. + * + * @param path Path to check for existence. + * @return True if this section contains the requested path, either via + * default or being set. + * @throws IllegalArgumentException Thrown when path is null. + */ + operator fun contains(path: String): Boolean + + /** + * Gets the requested Object by path. + * + * + * If the Object does not exist but a default value has been specified, + * this will return the default value. If the Object does not exist and no + * default value was specified, this will return null. + * + * @param path Path of the Object to get. + * @return Requested Object. + */ + operator fun get(path: String): Any? + + /** + * Gets the requested Object by path, returning a default value if not + * found. + * + * @param path Path of the Object to get. + * @param def The default value to return if the path is not found. + * @return Requested Object. + */ + operator fun get(path: String, def: Any?): Any? + + /** + * Sets the specified path to the given value. + * + * + * If value is null, the entry will be removed. Any existing entry will be + * replaced, regardless of what the new value is. + * + * @param path Path of the object to set. + * @param value New value to set the path to. + */ + operator fun set(path: String, value: Any?) + + /** + * Gets the requested String by path. + * + * + * If the String does not exist but a default value has been specified, + * this will return the default value. If the String does not exist and no + * default value was specified, this will return null. + * + * @param path Path of the String to get. + * @return Requested String. + */ + fun getString(path: String): String? + + /** + * Gets the requested String by path, returning a default value if not + * found. + * + * @param path Path of the String to get. + * @param def The default value to return if the path is not found or is + * not a String. + * @return Requested String. + */ + fun getString(path: String, def: String?): String? + + /** + * Checks if the specified path is a String. + * + * + * If the path exists but is not a String, this will return false. If the + * path does not exist, this will return false. If the path does not exist + * but a default value has been specified, this will check if that default + * value is a String and return appropriately. + * + * @param path Path of the String to check. + * @return Whether or not the specified path is a String. + */ + fun isString(path: String): Boolean + + /** + * Gets the requested int by path. + * + * + * If the int does not exist but a default value has been specified, this + * will return the default value. If the int does not exist and no default + * value was specified, this will return 0. + * + * @param path Path of the int to get. + * @return Requested int. + */ + fun getInt(path: String): Int + + /** + * Gets the requested int by path, returning a default value if not found. + * + * @param path Path of the int to get. + * @param def The default value to return if the path is not found or is + * not an int. + * @return Requested int. + */ + fun getInt(path: String, def: Int): Int + + /** + * Checks if the specified path is an int. + * + * + * If the path exists but is not a int, this will return false. If the + * path does not exist, this will return false. If the path does not exist + * but a default value has been specified, this will check if that default + * value is a int and return appropriately. + * + * @param path Path of the int to check. + * @return Whether or not the specified path is an int. + */ + fun isInt(path: String): Boolean + + /** + * Gets the requested boolean by path. + * + * + * If the boolean does not exist but a default value has been specified, + * this will return the default value. If the boolean does not exist and + * no default value was specified, this will return false. + * + * @param path Path of the boolean to get. + * @return Requested boolean. + */ + fun getBoolean(path: String): Boolean + + /** + * Gets the requested boolean by path, returning a default value if not + * found. + * + * @param path Path of the boolean to get. + * @param def The default value to return if the path is not found or is + * not a boolean. + * @return Requested boolean. + */ + fun getBoolean(path: String, def: Boolean): Boolean + + /** + * Checks if the specified path is a boolean. + * + * + * If the path exists but is not a boolean, this will return false. If the + * path does not exist, this will return false. If the path does not exist + * but a default value has been specified, this will check if that default + * value is a boolean and return appropriately. + * + * @param path Path of the boolean to check. + * @return Whether or not the specified path is a boolean. + */ + fun isBoolean(path: String): Boolean + + /** + * Gets the requested double by path. + * + * + * If the double does not exist but a default value has been specified, + * this will return the default value. If the double does not exist and no + * default value was specified, this will return 0. + * + * @param path Path of the double to get. + * @return Requested double. + */ + fun getDouble(path: String): Double + + /** + * Gets the requested double by path, returning a default value if not + * found. + * + * @param path Path of the double to get. + * @param def The default value to return if the path is not found or is + * not a double. + * @return Requested double. + */ + fun getDouble(path: String, def: Double): Double + + /** + * Checks if the specified path is a double. + * + * + * If the path exists but is not a double, this will return false. If the + * path does not exist, this will return false. If the path does not exist + * but a default value has been specified, this will check if that default + * value is a double and return appropriately. + * + * @param path Path of the double to check. + * @return Whether or not the specified path is a double. + */ + fun isDouble(path: String): Boolean + + /** + * Gets the requested long by path. + * + * + * If the long does not exist but a default value has been specified, this + * will return the default value. If the long does not exist and no + * default value was specified, this will return 0. + * + * @param path Path of the long to get. + * @return Requested long. + */ + fun getLong(path: String): Long + + /** + * Gets the requested long by path, returning a default value if not + * found. + * + * @param path Path of the long to get. + * @param def The default value to return if the path is not found or is + * not a long. + * @return Requested long. + */ + fun getLong(path: String, def: Long): Long + + /** + * Checks if the specified path is a long. + * + * + * If the path exists but is not a long, this will return false. If the + * path does not exist, this will return false. If the path does not exist + * but a default value has been specified, this will check if that default + * value is a long and return appropriately. + * + * @param path Path of the long to check. + * @return Whether or not the specified path is a long. + */ + fun isLong(path: String): Boolean + + /** + * Gets the requested List by path. + * + * + * If the List does not exist but a default value has been specified, this + * will return the default value. If the List does not exist and no + * default value was specified, this will return null. + * + * @param path Path of the List to get. + * @return Requested List. + */ + fun getList(path: String): List<*>? + + /** + * Gets the requested List by path, returning a default value if not + * found. + * + * @param path Path of the List to get. + * @param def The default value to return if the path is not found or is + * not a List. + * @return Requested List. + */ + fun getList(path: String, def: List<*>?): List<*>? + + /** + * Checks if the specified path is a List. + * + * + * If the path exists but is not a List, this will return false. If the + * path does not exist, this will return false. If the path does not exist + * but a default value has been specified, this will check if that default + * value is a List and return appropriately. + * + * @param path Path of the List to check. + * @return Whether or not the specified path is a List. + */ + fun isList(path: String): Boolean + + /** + * Gets the requested List of String by path. + * + * + * If the List does not exist but a default value has been specified, this + * will return the default value. If the List does not exist and no + * default value was specified, this will return an empty List. + * + * + * This method will attempt to cast any values into a String if possible, + * but may miss any values out if they are not compatible. + * + * @param path Path of the List to get. + * @return Requested List of String. + */ + fun getStringList(path: String): List + + /** + * Gets the requested List of Integer by path. + * + * + * If the List does not exist but a default value has been specified, this + * will return the default value. If the List does not exist and no + * default value was specified, this will return an empty List. + * + * + * This method will attempt to cast any values into a Integer if possible, + * but may miss any values out if they are not compatible. + * + * @param path Path of the List to get. + * @return Requested List of Integer. + */ + fun getIntegerList(path: String): List + + /** + * Gets the requested List of Boolean by path. + * + * + * If the List does not exist but a default value has been specified, this + * will return the default value. If the List does not exist and no + * default value was specified, this will return an empty List. + * + * + * This method will attempt to cast any values into a Boolean if possible, + * but may miss any values out if they are not compatible. + * + * @param path Path of the List to get. + * @return Requested List of Boolean. + */ + fun getBooleanList(path: String): List + + /** + * Gets the requested List of Double by path. + * + * + * If the List does not exist but a default value has been specified, this + * will return the default value. If the List does not exist and no + * default value was specified, this will return an empty List. + * + * + * This method will attempt to cast any values into a Double if possible, + * but may miss any values out if they are not compatible. + * + * @param path Path of the List to get. + * @return Requested List of Double. + */ + fun getDoubleList(path: String): List + + /** + * Gets the requested List of Float by path. + * + * + * If the List does not exist but a default value has been specified, this + * will return the default value. If the List does not exist and no + * default value was specified, this will return an empty List. + * + * + * This method will attempt to cast any values into a Float if possible, + * but may miss any values out if they are not compatible. + * + * @param path Path of the List to get. + * @return Requested List of Float. + */ + fun getFloatList(path: String): List + + /** + * Gets the requested List of Long by path. + * + * + * If the List does not exist but a default value has been specified, this + * will return the default value. If the List does not exist and no + * default value was specified, this will return an empty List. + * + * + * This method will attempt to cast any values into a Long if possible, + * but may miss any values out if they are not compatible. + * + * @param path Path of the List to get. + * @return Requested List of Long. + */ + fun getLongList(path: String): List + + /** + * Gets the requested List of Byte by path. + * + * + * If the List does not exist but a default value has been specified, this + * will return the default value. If the List does not exist and no + * default value was specified, this will return an empty List. + * + * + * This method will attempt to cast any values into a Byte if possible, + * but may miss any values out if they are not compatible. + * + * @param path Path of the List to get. + * @return Requested List of Byte. + */ + fun getByteList(path: String): List + + /** + * Gets the requested List of Character by path. + * + * + * If the List does not exist but a default value has been specified, this + * will return the default value. If the List does not exist and no + * default value was specified, this will return an empty List. + * + * + * This method will attempt to cast any values into a Character if + * possible, but may miss any values out if they are not compatible. + * + * @param path Path of the List to get. + * @return Requested List of Character. + */ + fun getCharacterList(path: String): List + + /** + * Gets the requested List of Short by path. + * + * + * If the List does not exist but a default value has been specified, this + * will return the default value. If the List does not exist and no + * default value was specified, this will return an empty List. + * + * + * This method will attempt to cast any values into a Short if possible, + * but may miss any values out if they are not compatible. + * + * @param path Path of the List to get. + * @return Requested List of Short. + */ + fun getShortList(path: String): List + + /** + * Gets the requested List of Maps by path. + * + * + * If the List does not exist but a default value has been specified, this + * will return the default value. If the List does not exist and no + * default value was specified, this will return an empty List. + * + * + * This method will attempt to cast any values into a Map if possible, but + * may miss any values out if they are not compatible. + * + * @param path Path of the List to get. + * @return Requested List of Maps. + */ + fun getMapList(path: String): List> + + /** + * Gets the requested ConfigurationSection by path. + * + * + * If the ConfigurationSection does not exist but a default value has been + * specified, this will return the default value. If the + * ConfigurationSection does not exist and no default value was specified, + * this will return null. + * + * @param path Path of the ConfigurationSection to get. + * @return Requested ConfigurationSection. + */ + fun getConfigurationSection(path: String): ConfigurationSection? + + /** + * Checks if the specified path is a ConfigurationSection. + * + * + * If the path exists but is not a ConfigurationSection, this will return + * false. If the path does not exist, this will return false. If the path + * does not exist but a default value has been specified, this will check + * if that default value is a ConfigurationSection and return + * appropriately. + * + * @param path Path of the ConfigurationSection to check. + * @return Whether or not the specified path is a ConfigurationSection. + */ + fun isConfigurationSection(path: String): Boolean + + fun > getEnum(path: String, type: Class): T? + + fun > getEnumList(path: String, type: Class): List + + fun createSection(path: String): ConfigurationSection + + fun toMap(): Map + + fun getComment(path: String): String? + + fun getComments(path: String): List + + fun setComment(path: String, comment: String?) + + fun setComments(path: String, comments: List) + + fun addComments(path: String, comments: List) + + fun getValues(deep: Boolean): Map + + fun clear() + +} \ No newline at end of file diff --git a/project/module-configuration/src/main/kotlin/fr/xephi/authme/configruation/Type.kt b/project/module-configuration/src/main/kotlin/fr/xephi/authme/configruation/Type.kt new file mode 100644 index 00000000..eca5d1f7 --- /dev/null +++ b/project/module-configuration/src/main/kotlin/fr/xephi/authme/configruation/Type.kt @@ -0,0 +1,31 @@ +package fr.xephi.authme.configruation + +/** + * Type + * + * @author Taboolib + * @since 2024/7/10 19:31 + */ +enum class Type(private val format: () -> ConfigFormat) { + + YAML({ YamlFormat.INSTANCE }); + +// TOML({ TomlFormat.instance() }), +// +// JSON({ JsonFormat.emptyTolerantInstance() }), +// +// FAST_JSON({ JsonFormat.minimalEmptyTolerantInstance() }), +// +// HOCON({ HoconFormat.instance() }); + + fun newFormat(): ConfigFormat { + return format() + } + + companion object { + + fun getType(format: ConfigFormat<*>): Type { + return values().first { it.newFormat().javaClass == format.javaClass } + } + } +} \ No newline at end of file diff --git a/project/module-util/build.gradle.kts b/project/module-util/build.gradle.kts new file mode 100644 index 00000000..7ab8afb6 --- /dev/null +++ b/project/module-util/build.gradle.kts @@ -0,0 +1,3 @@ +dependencies { + compileOnly("com.google.guava:guava:33.2.1-jre") +} \ No newline at end of file diff --git a/project/module-util/src/main/java/fr/xephi/authme/util/Coerce.java b/project/module-util/src/main/java/fr/xephi/authme/util/Coerce.java new file mode 100644 index 00000000..529a32a2 --- /dev/null +++ b/project/module-util/src/main/java/fr/xephi/authme/util/Coerce.java @@ -0,0 +1,303 @@ +/* + * This file is part of Sponge, licensed under the MIT License (MIT). + * + * Copyright (c) SpongePowered + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.spongepowered.common.data.builder; + +import com.google.common.primitives.Doubles; +import com.google.common.primitives.Ints; +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Utility class for coercing unknown values to specific target types. + */ +public final class Coerce { + + private static final Pattern listPattern = Pattern.compile("^([\\(\\[\\{]?)(.+?)([\\)\\]\\}]?)$"); + + private static final String[] listPairings = {"([{", ")]}"}; + + /** + * No subclasses for you. + */ + private Coerce() {} + + /** + * Gets the given object as a {@link String}. + * + * @param obj The object to translate + * @return The string value, if available + */ + public static Optional asString(@Nullable Object obj) { + if (obj instanceof String) { + return Optional.of((String) obj); + } else if (obj == null) { + return Optional.empty(); + } else { + return Optional.of(obj.toString()); + } + } + + /** + * Gets the given object as a {@link Boolean}. + * + * @param obj The object to translate + * @return The boolean, if available + */ + public static Optional asBoolean(@Nullable Object obj) { + if (obj instanceof Boolean) { + return Optional.of((Boolean) obj); + } else if (obj instanceof Byte) { + return Optional.of((Byte) obj != 0); + } + return Optional.empty(); + } + + /** + * Gets the given object as a {@link Integer}. + * + *

Note that this does not translate numbers spelled out as strings.

+ * + * @param obj The object to translate + * @return The integer value, if available + */ + public static Optional asInteger(@Nullable Object obj) { + if (obj == null) { + // fail fast + return Optional.empty(); + } + if (obj instanceof Number) { + return Optional.of(((Number) obj).intValue()); + } + + try { + return Optional.ofNullable(Integer.valueOf(obj.toString())); + } catch (NumberFormatException | NullPointerException e) { + // do nothing + } + + final String strObj = Coerce.sanitiseNumber(obj); + final Integer iParsed = Ints.tryParse(strObj); + if (iParsed == null) { + final Double dParsed = Doubles.tryParse(strObj); + // try parsing as double now + return dParsed == null ? Optional.empty() : Optional.of(dParsed.intValue()); + } + return Optional.of(iParsed); + } + + /** + * Gets the given object as a {@link Double}. + * + *

Note that this does not translate numbers spelled out as strings.

+ * + * @param obj The object to translate + * @return The double value, if available + */ + public static Optional asDouble(@Nullable Object obj) { + if (obj == null) { + // fail fast + return Optional.empty(); + } + if (obj instanceof Number) { + return Optional.of(((Number) obj).doubleValue()); + } + + try { + return Optional.ofNullable(Double.valueOf(obj.toString())); + } catch (NumberFormatException | NullPointerException e) { + // do nothing + } + + final String strObj = Coerce.sanitiseNumber(obj); + final Double dParsed = Doubles.tryParse(strObj); + // try parsing as double now + return dParsed == null ? Optional.empty() : Optional.of(dParsed); + } + + /** + * Gets the given object as a {@link Float}. + * + *

Note that this does not translate numbers spelled out as strings.

+ * + * @param obj The object to translate + * @return The float value, if available + */ + public static Optional asFloat(@Nullable Object obj) { + if (obj == null) { + // fail fast + return Optional.empty(); + } + if (obj instanceof Number) { + return Optional.of(((Number) obj).floatValue()); + } + + try { + return Optional.ofNullable(Float.valueOf(obj.toString())); + } catch (NumberFormatException | NullPointerException e) { + // do nothing + } + + final String strObj = Coerce.sanitiseNumber(obj); + final Double dParsed = Doubles.tryParse(strObj); + return dParsed == null ? Optional.empty() : Optional.of(dParsed.floatValue()); + } + + /** + * Gets the given object as a {@link Short}. + * + *

Note that this does not translate numbers spelled out as strings.

+ * + * @param obj The object to translate + * @return The short value, if available + */ + public static Optional asShort(@Nullable Object obj) { + if (obj == null) { + // fail fast + return Optional.empty(); + } + if (obj instanceof Number) { + return Optional.of(((Number) obj).shortValue()); + } + + try { + return Optional.ofNullable(Short.parseShort(Coerce.sanitiseNumber(obj))); + } catch (NumberFormatException | NullPointerException e) { + // do nothing + } + return Optional.empty(); + } + + /** + * Gets the given object as a {@link Byte}. + * + *

Note that this does not translate numbers spelled out as strings.

+ * + * @param obj The object to translate + * @return The byte value, if available + */ + public static Optional asByte(@Nullable Object obj) { + if (obj == null) { + // fail fast + return Optional.empty(); + } + if (obj instanceof Number) { + return Optional.of(((Number) obj).byteValue()); + } + + try { + return Optional.ofNullable(Byte.parseByte(Coerce.sanitiseNumber(obj))); + } catch (NumberFormatException | NullPointerException e) { + // do nothing + } + return Optional.empty(); + } + + /** + * Gets the given object as a {@link Long}. + * + *

Note that this does not translate numbers spelled out as strings.

+ * + * @param obj The object to translate + * @return The long value, if available + */ + public static Optional asLong(@Nullable Object obj) { + if (obj == null) { + // fail fast + return Optional.empty(); + } + if (obj instanceof Number) { + return Optional.of(((Number) obj).longValue()); + } + + try { + return Optional.ofNullable(Long.parseLong(Coerce.sanitiseNumber(obj))); + } catch (NumberFormatException | NullPointerException e) { + // do nothing + } + return Optional.empty(); + } + + /** + * Gets the given object as a {@link Character}. + * + * @param obj The object to translate + * @return The character, if available + */ + public static Optional asChar(@Nullable Object obj) { + if (obj == null) { + return Optional.empty(); + } + if (obj instanceof Character) { + return Optional.of((Character) obj); + } + try { + return Optional.of(obj.toString().charAt(0)); + } catch (Exception e) { + // do nothing + } + return Optional.empty(); + } + + /** + * Sanitise a string containing a common representation of a number to make + * it parsable. Strips thousand-separating commas and trims later members + * of a comma-separated list. For example the string "(9.5, 10.6, 33.2)" + * will be sanitised to "9.5". + * + * @param obj Object to sanitise + * @return Sanitised number-format string to parse + */ + private static String sanitiseNumber(Object obj) { + String string = obj.toString().trim(); + if (string.length() < 1) { + return "0"; + } + + final Matcher candidate = Coerce.listPattern.matcher(string); + if (Coerce.listBracketsMatch(candidate)) { + string = candidate.group(2).trim(); + } + + final int decimal = string.indexOf('.'); + final int comma = string.indexOf(',', decimal); + if (decimal > -1 && comma > -1) { + return Coerce.sanitiseNumber(string.substring(0, comma)); + } + + if (string.indexOf('-', 1) != -1) { + return "0"; + } + + return string.replace(",", "").split(" ", 0)[0]; + } + + private static boolean listBracketsMatch(Matcher candidate) { + return candidate.matches() && Coerce.listPairings[0].indexOf(candidate.group(1)) == Coerce.listPairings[1].indexOf(candidate.group(3)); + } + +} \ No newline at end of file diff --git a/project/module-util/src/main/java/fr/xephi/authme/util/StringUtil.java b/project/module-util/src/main/java/fr/xephi/authme/util/StringUtil.java new file mode 100644 index 00000000..1b65a904 --- /dev/null +++ b/project/module-util/src/main/java/fr/xephi/authme/util/StringUtil.java @@ -0,0 +1,27 @@ +package fr.xephi.authme.util; + +/** + * StringUtil + * + * @author TheFloodDragon + * @since 2024/7/10 19:51 + */ +public final class StringUtil { + + private StringUtil() { + } + + /** + * 解码 Unicode + */ + public static String decodeUnicode(String str) { + String r = str; + int i = r.indexOf("\\u"); + if (i != -1) { + r = r.substring(0, i) + (char) Integer.parseInt(r.substring(i + 2, i + 6), 16) + r.substring(i + 6); + decodeUnicode(r); + } + return r; + } + +} \ No newline at end of file