From fafd3aba6afa2b262c5cd760c8d491e0d7405dac Mon Sep 17 00:00:00 2001 From: sk89q Date: Sat, 2 Aug 2014 21:03:12 -0700 Subject: [PATCH] Implement UUID migration. --- .../bukkit/ConfigurationManager.java | 15 +- .../bukkit/RegionPermissionModel.java | 6 +- .../worldguard/bukkit/WorldGuardPlugin.java | 78 ++++++-- .../bukkit/commands/RegionCommands.java | 60 ++++++ .../worldguard/domains/DefaultDomain.java | 42 ++++- .../protection/GlobalRegionManager.java | 20 +- .../migrator/AbstractDatabaseMigrator.java | 8 +- .../databases/migrator/UUIDMigrator.java | 176 ++++++++++++++++++ 8 files changed, 377 insertions(+), 28 deletions(-) create mode 100644 src/main/java/com/sk89q/worldguard/protection/databases/migrator/UUIDMigrator.java diff --git a/src/main/java/com/sk89q/worldguard/bukkit/ConfigurationManager.java b/src/main/java/com/sk89q/worldguard/bukkit/ConfigurationManager.java index eadde464..29e4dfcc 100644 --- a/src/main/java/com/sk89q/worldguard/bukkit/ConfigurationManager.java +++ b/src/main/java/com/sk89q/worldguard/bukkit/ConfigurationManager.java @@ -102,6 +102,8 @@ public class ConfigurationManager { public boolean usePlayerTeleports; public boolean deopOnJoin; public boolean blockInGameOp; + public boolean migrateRegionsToUuid; + public boolean keepUnresolvedNames; public Map hostKeys = new HashMap(); /** @@ -142,6 +144,8 @@ public void load() { config.removeProperty("suppress-tick-sync-warnings"); useRegionsScheduler = config.getBoolean("regions.use-scheduler", true); + migrateRegionsToUuid = config.getBoolean("regions.uuid-migration.perform-on-next-start", true); + keepUnresolvedNames = config.getBoolean("regions.uuid-migration.keep-names-that-lack-uuids", true); useRegionsCreatureSpawnEvent = config.getBoolean("regions.use-creature-spawn-event", true); autoGodMode = config.getBoolean("auto-invincible", config.getBoolean("auto-invincible-permission", false)); config.removeProperty("auto-invincible-permission"); @@ -177,10 +181,6 @@ public void load() { } config.setHeader(CONFIG_HEADER); - - if (!config.save()) { - plugin.getLogger().severe("Error saving configuration!"); - } } /** @@ -190,6 +190,13 @@ public void unload() { worlds.clear(); } + public void disableUuidMigration() { + config.setProperty("regions.uuid-migration.perform-on-next-start", false); + if (!config.save()) { + plugin.getLogger().severe("Error saving configuration!"); + } + } + /** * Get the configuration for a world. * diff --git a/src/main/java/com/sk89q/worldguard/bukkit/RegionPermissionModel.java b/src/main/java/com/sk89q/worldguard/bukkit/RegionPermissionModel.java index 44b07c0d..3e0ba0fe 100644 --- a/src/main/java/com/sk89q/worldguard/bukkit/RegionPermissionModel.java +++ b/src/main/java/com/sk89q/worldguard/bukkit/RegionPermissionModel.java @@ -42,10 +42,14 @@ public boolean mayForceLoadRegions() { public boolean mayForceSaveRegions() { return hasPluginPermission("region.save"); } - + public boolean mayMigrateRegionStore() { return hasPluginPermission("region.migratedb"); } + + public boolean mayMigrateRegionNames() { + return hasPluginPermission("region.migrateuuid"); + } public boolean mayDefine() { return hasPluginPermission("region.define"); diff --git a/src/main/java/com/sk89q/worldguard/bukkit/WorldGuardPlugin.java b/src/main/java/com/sk89q/worldguard/bukkit/WorldGuardPlugin.java index a7213061..6889fd5d 100644 --- a/src/main/java/com/sk89q/worldguard/bukkit/WorldGuardPlugin.java +++ b/src/main/java/com/sk89q/worldguard/bukkit/WorldGuardPlugin.java @@ -52,6 +52,9 @@ import com.sk89q.worldguard.internal.listener.ChestProtectionListener; import com.sk89q.worldguard.internal.listener.RegionProtectionListener; import com.sk89q.worldguard.protection.GlobalRegionManager; +import com.sk89q.worldguard.protection.databases.ProtectionDatabaseException; +import com.sk89q.worldguard.protection.databases.migrator.MigrationException; +import com.sk89q.worldguard.protection.databases.migrator.UUIDMigrator; import com.sk89q.worldguard.protection.databases.util.UnresolvedNamesException; import com.sk89q.worldguard.protection.managers.RegionManager; import com.sk89q.worldguard.util.FatalConfigurationLoadingException; @@ -154,6 +157,21 @@ public void run() { // Need to create the plugins/WorldGuard folder getDataFolder().mkdirs(); + File cacheDir = new File(getDataFolder(), "cache"); + cacheDir.mkdirs(); + try { + profileCache = new SQLiteCache(new File(cacheDir, "profiles.sqlite")); + } catch (IOException e) { + getLogger().log(Level.WARNING, "Failed to initialize SQLite profile cache"); + profileCache = new HashMapCache(); + } + + profileService = new CacheForwardingService( + new CombinedProfileService( + BukkitPlayerService.getInstance(), + HttpRepositoryService.forMinecraft()), + profileCache); + PermissionsResolverManager.initialize(this); // This must be done before configuration is loaded @@ -162,10 +180,24 @@ public void run() { try { // Load the configuration configuration.load(); + + getLogger().info("Loading region data..."); globalRegionManager.preload(); + + migrateRegionUniqueIds(); // Migrate to UUIDs } catch (FatalConfigurationLoadingException e) { - e.printStackTrace(); + getLogger().log(Level.WARNING, "Encountered fatal error while loading configuration", e); getServer().shutdown(); + getLogger().log(Level.WARNING, "\n" + + "******************************************************\n" + + "* Failed to load WorldGuard configuration!\n" + + "* \n" + + "* Shutting down the server just in case...\n" + + "* \n" + + "* The error should be printed above this message. If you can't\n" + + "* figure out the problem, ask us on our forums:\n" + + "* http://forum.enginehub.org\n" + + "******************************************************\n"); } // Migrate regions after the regions were loaded because @@ -207,21 +239,6 @@ public void run() { } worldListener.registerEvents(); - File cacheDir = new File(getDataFolder(), "cache"); - cacheDir.mkdirs(); - try { - profileCache = new SQLiteCache(new File(cacheDir, "profiles.sqlite")); - } catch (IOException e) { - getLogger().log(Level.WARNING, "Failed to initialize SQLite profile cache"); - profileCache = new HashMapCache(); - } - - profileService = new CacheForwardingService( - new CombinedProfileService( - BukkitPlayerService.getInstance(), - HttpRepositoryService.forMinecraft()), - profileCache); - if (!configuration.hasCommandBookGodMode()) { // Check god mode for existing players, if any for (Player player : getServer().getOnlinePlayers()) { @@ -260,6 +277,35 @@ public void onDisable() { this.getServer().getScheduler().cancelTasks(this); } + private void migrateRegionUniqueIds() { + try { + if (configuration.migrateRegionsToUuid) { + UUIDMigrator migrator = new UUIDMigrator(profileService, getLogger()); + migrator.readConfiguration(configuration); + List managers = globalRegionManager.getLoaded(); + + // Try migration + if (migrator.migrate(managers)) { + getLogger().info("Now saving regions... this may take a while."); + + for (RegionManager manager : managers) { + manager.save(); + } + + getLogger().info( + "Regions saved after UUID migration! This won't happen again unless " + + "you change the relevant configuration option in WorldGuard's config."); + + configuration.disableUuidMigration(); + } + } + } catch (MigrationException e) { + getLogger().log(Level.WARNING, "Failed to migrate regions to use UUIDs instead of player names", e); + } catch (ProtectionDatabaseException e) { + getLogger().log(Level.WARNING, "Failed to save regions after UUID conversion", e); + } + } + @Override public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) { try { diff --git a/src/main/java/com/sk89q/worldguard/bukkit/commands/RegionCommands.java b/src/main/java/com/sk89q/worldguard/bukkit/commands/RegionCommands.java index 4bf89246..84f8f5bf 100644 --- a/src/main/java/com/sk89q/worldguard/bukkit/commands/RegionCommands.java +++ b/src/main/java/com/sk89q/worldguard/bukkit/commands/RegionCommands.java @@ -34,14 +34,17 @@ import com.sk89q.worldedit.bukkit.selections.Polygonal2DSelection; import com.sk89q.worldedit.bukkit.selections.Selection; import com.sk89q.worldguard.LocalPlayer; +import com.sk89q.worldguard.bukkit.LoggerToChatHandler; import com.sk89q.worldguard.bukkit.RegionPermissionModel; import com.sk89q.worldguard.bukkit.WorldConfiguration; import com.sk89q.worldguard.bukkit.WorldGuardPlugin; import com.sk89q.worldguard.protection.ApplicableRegionSet; +import com.sk89q.worldguard.protection.databases.ProtectionDatabaseException; import com.sk89q.worldguard.protection.databases.RegionDBUtil; import com.sk89q.worldguard.protection.databases.migrator.AbstractDatabaseMigrator; import com.sk89q.worldguard.protection.databases.migrator.MigrationException; import com.sk89q.worldguard.protection.databases.migrator.MigratorKey; +import com.sk89q.worldguard.protection.databases.migrator.UUIDMigrator; import com.sk89q.worldguard.protection.flags.DefaultFlag; import com.sk89q.worldguard.protection.flags.Flag; import com.sk89q.worldguard.protection.flags.InvalidFlagFormat; @@ -65,6 +68,8 @@ import java.util.Date; import java.util.List; import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; /** * Implements the /region commands for WorldGuard. @@ -1199,6 +1204,61 @@ public void migrateDB(CommandContext args, CommandSender sender) throws CommandE "If you wish to use the destination format as your new backend, please update your config and reload WorldGuard."); } + /** + * Migrate the region databases to use UUIDs rather than name. + * + * @param args the arguments + * @param sender the sender + * @throws CommandException any error + */ + @Command(aliases = {"migrateuuid"}, + desc = "Migrate loaded databases to use UUIDs", max = 0) + public void migrateUuid(CommandContext args, CommandSender sender) throws CommandException { + // Check permissions + if (!getPermissionModel(sender).mayMigrateRegionNames()) { + throw new CommandPermissionsException(); + } + + LoggerToChatHandler handler = null; + Logger minecraftLogger = null; + + if (sender instanceof Player) { + handler = new LoggerToChatHandler(sender); + handler.setLevel(Level.ALL); + minecraftLogger = Logger.getLogger("Minecraft"); + minecraftLogger.addHandler(handler); + } + + try { + UUIDMigrator migrator = new UUIDMigrator(plugin.getProfileService(), plugin.getLogger()); + migrator.readConfiguration(plugin.getGlobalStateManager()); + List managers = plugin.getGlobalRegionManager().getLoaded(); + + // Try migration + if (migrator.migrate(managers)) { + sender.sendMessage(ChatColor.YELLOW + "Now saving regions... this may take a while."); + + for (RegionManager manager : managers) { + manager.save(); + } + + sender.sendMessage(ChatColor.YELLOW + "Migration complete!"); + } else { + sender.sendMessage(ChatColor.YELLOW + "There were no names to migrate."); + } + } catch (ProtectionDatabaseException e) { + plugin.getLogger().log(Level.WARNING, "Failed to save", e); + throw new CommandException("Error encountered while saving: " + e.getMessage()); + } catch (MigrationException e) { + plugin.getLogger().log(Level.WARNING, "Failed to migrate", e); + throw new CommandException("Error encountered while migrating: " + e.getMessage()); + } finally { + if (minecraftLogger != null) { + minecraftLogger.removeHandler(handler); + } + } + } + /** * Teleport to a region * diff --git a/src/main/java/com/sk89q/worldguard/domains/DefaultDomain.java b/src/main/java/com/sk89q/worldguard/domains/DefaultDomain.java index 05c77830..abe347b9 100644 --- a/src/main/java/com/sk89q/worldguard/domains/DefaultDomain.java +++ b/src/main/java/com/sk89q/worldguard/domains/DefaultDomain.java @@ -39,8 +39,46 @@ */ public class DefaultDomain implements Domain { - private final PlayerDomain playerDomain = new PlayerDomain(); - private final GroupDomain groupDomain = new GroupDomain(); + private PlayerDomain playerDomain = new PlayerDomain(); + private GroupDomain groupDomain = new GroupDomain(); + + /** + * Get the domain that holds the players. + * + * @return a domain + */ + public PlayerDomain getPlayerDomain() { + return playerDomain; + } + + /** + * Set a new player domain. + * + * @param playerDomain a domain + */ + public void setPlayerDomain(PlayerDomain playerDomain) { + checkNotNull(playerDomain); + this.playerDomain = playerDomain; + } + + /** + * Set the domain that holds the groups. + * + * @return a domain + */ + public GroupDomain getGroupDomain() { + return groupDomain; + } + + /** + * Set a new group domain. + * + * @param groupDomain a domain + */ + public void setGroupDomain(GroupDomain groupDomain) { + checkNotNull(groupDomain); + this.groupDomain = groupDomain; + } /** * Add the given player to the domain, identified by the player's name. diff --git a/src/main/java/com/sk89q/worldguard/protection/GlobalRegionManager.java b/src/main/java/com/sk89q/worldguard/protection/GlobalRegionManager.java index cf3c4859..5624e8ee 100644 --- a/src/main/java/com/sk89q/worldguard/protection/GlobalRegionManager.java +++ b/src/main/java/com/sk89q/worldguard/protection/GlobalRegionManager.java @@ -25,9 +25,9 @@ import com.sk89q.worldguard.bukkit.WorldConfiguration; import com.sk89q.worldguard.bukkit.WorldGuardPlugin; import com.sk89q.worldguard.protection.databases.MySQLDatabase; -import com.sk89q.worldguard.protection.databases.YAMLDatabase; import com.sk89q.worldguard.protection.databases.ProtectionDatabase; import com.sk89q.worldguard.protection.databases.ProtectionDatabaseException; +import com.sk89q.worldguard.protection.databases.YAMLDatabase; import com.sk89q.worldguard.protection.flags.StateFlag; import com.sk89q.worldguard.protection.managers.PRTreeRegionManager; import com.sk89q.worldguard.protection.managers.RegionManager; @@ -38,7 +38,10 @@ import java.io.File; import java.io.FileNotFoundException; +import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Level; @@ -247,6 +250,21 @@ public RegionManager get(World world) { return manager; } + /** + * Get a list of loaded region managers. + * + * @return an unmodifiable list + */ + public List getLoaded() { + List list = new ArrayList(); + for (RegionManager manager : managers.values()) { + if (manager != null) { + list.add(manager); + } + } + return Collections.unmodifiableList(list); + } + /** * Returns whether the player can bypass. * diff --git a/src/main/java/com/sk89q/worldguard/protection/databases/migrator/AbstractDatabaseMigrator.java b/src/main/java/com/sk89q/worldguard/protection/databases/migrator/AbstractDatabaseMigrator.java index abfd57a6..1d12bf29 100644 --- a/src/main/java/com/sk89q/worldguard/protection/databases/migrator/AbstractDatabaseMigrator.java +++ b/src/main/java/com/sk89q/worldguard/protection/databases/migrator/AbstractDatabaseMigrator.java @@ -19,14 +19,14 @@ package com.sk89q.worldguard.protection.databases.migrator; -import java.util.HashMap; -import java.util.Map; -import java.util.Set; - import com.sk89q.worldguard.protection.databases.ProtectionDatabase; import com.sk89q.worldguard.protection.databases.ProtectionDatabaseException; import com.sk89q.worldguard.protection.regions.ProtectedRegion; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + public abstract class AbstractDatabaseMigrator implements DatabaseMigrator { private static HashMap> migrators = diff --git a/src/main/java/com/sk89q/worldguard/protection/databases/migrator/UUIDMigrator.java b/src/main/java/com/sk89q/worldguard/protection/databases/migrator/UUIDMigrator.java new file mode 100644 index 00000000..1788c4cd --- /dev/null +++ b/src/main/java/com/sk89q/worldguard/protection/databases/migrator/UUIDMigrator.java @@ -0,0 +1,176 @@ +/* + * WorldGuard, a suite of tools for Minecraft + * Copyright (C) sk89q + * Copyright (C) WorldGuard team and contributors + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by the + * Free Software Foundation, either version 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 Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldguard.protection.databases.migrator; + +import com.google.common.base.Predicate; +import com.sk89q.squirrelid.Profile; +import com.sk89q.squirrelid.resolver.ProfileService; +import com.sk89q.worldguard.bukkit.ConfigurationManager; +import com.sk89q.worldguard.domains.DefaultDomain; +import com.sk89q.worldguard.domains.PlayerDomain; +import com.sk89q.worldguard.protection.managers.RegionManager; +import com.sk89q.worldguard.protection.regions.ProtectedRegion; + +import java.io.IOException; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; +import java.util.Timer; +import java.util.TimerTask; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class UUIDMigrator { + + private static final int LOG_DELAY = 5000; + + private final Timer timer = new Timer(); + private final ProfileService profileService; + private final Logger logger; + private final ConcurrentMap resolvedNames = new ConcurrentHashMap(); + private final Set unresolvedNames = new HashSet(); + private boolean keepUnresolvedNames = true; + + public UUIDMigrator(ProfileService profileService, Logger logger) { + checkNotNull(profileService); + checkNotNull(logger); + + this.profileService = profileService; + this.logger = logger; + } + + public boolean migrate(Collection managers) throws MigrationException { + Set names = new HashSet(); + + // Name scan pass + logger.log(Level.INFO, "UUID migrator: Gathering names to convert..."); + for (RegionManager manager : managers) { + scanForNames(manager, names); + } + + if (names.isEmpty()) { + logger.log(Level.INFO, "UUID migrator: No names to convert!"); + return false; + } + + TimerTask resolvedTask = new ResolvedNamesTimerTask(); + try { + timer.schedule(resolvedTask, LOG_DELAY, LOG_DELAY); + logger.log(Level.INFO, "UUID migrator: Resolving " + names.size() + " name(s) into UUIDs... this may take a while."); + profileService.findAllByName(names, new Predicate() { + @Override + public boolean apply(Profile profile) { + resolvedNames.put(profile.getName().toLowerCase(), profile.getUniqueId()); + return true; + } + }); + } catch (IOException e) { + throw new MigrationException("The name -> UUID service failed", e); + } catch (InterruptedException e) { + throw new MigrationException("The migration was interrupted"); + } finally { + resolvedTask.cancel(); + } + + logger.log(Level.INFO, "UUID migrator: UUIDs resolved... now migrating all regions to UUIDs where possible..."); + for (RegionManager manager : managers) { + convertToUniqueIds(manager); + } + + if (!unresolvedNames.isEmpty()) { + if (keepUnresolvedNames) { + logger.log(Level.WARNING, + "UUID migrator: Some member and owner names do not seem to exist or own Minecraft so they " + + "could not be converted into UUIDs. They have been left as names, but the conversion can " + + "be re-run with 'keep-names-that-lack-uuids' set to false in the configuration in " + + "order to remove these names. Leaving the names means that someone can register with one of " + + "these names in the future and become that player."); + } else { + logger.log(Level.WARNING, + "UUID migrator: Some member and owner names do not seem to exist or own Minecraft so they " + + "could not be converted into UUIDs. These names have been removed."); + } + } + + logger.log(Level.INFO, "UUID migrator: Migration finished!"); + + return true; + } + + private void scanForNames(RegionManager manager, Set target) { + for (ProtectedRegion region : manager.getRegions().values()) { + target.addAll(region.getOwners().getPlayers()); + target.addAll(region.getMembers().getPlayers()); + } + } + + private void convertToUniqueIds(RegionManager manager) { + for (ProtectedRegion region : manager.getRegions().values()) { + convertToUniqueIds(region.getOwners()); + convertToUniqueIds(region.getMembers()); + } + } + + private void convertToUniqueIds(DefaultDomain domain) { + PlayerDomain playerDomain = new PlayerDomain(); + for (UUID uuid : domain.getUniqueIds()) { + playerDomain.addPlayer(uuid); + } + + for (String name : domain.getPlayers()) { + UUID uuid = resolvedNames.get(name.toLowerCase()); + if (uuid != null) { + playerDomain.addPlayer(uuid); + } else { + if (keepUnresolvedNames) { + playerDomain.addPlayer(name); + } + unresolvedNames.add(name); + } + } + + domain.setPlayerDomain(playerDomain); + } + + public boolean isKeepUnresolvedNames() { + return keepUnresolvedNames; + } + + public void setKeepUnresolvedNames(boolean keepUnresolvedNames) { + this.keepUnresolvedNames = keepUnresolvedNames; + } + + public void readConfiguration(ConfigurationManager config) { + setKeepUnresolvedNames(config.keepUnresolvedNames); + } + + private class ResolvedNamesTimerTask extends TimerTask { + @Override + public void run() { + logger.info("UUID migrator: UUIDs have been found for " + resolvedNames.size() + " name(s)..."); + } + } + +}