Merge branch 'master' of https://github.com/AuthMe/AuthMeReloaded into 1141-optional-additional-2fa-auth
# Conflicts: # src/main/java/fr/xephi/authme/datasource/MySQL.java
This commit is contained in:
@@ -2,7 +2,9 @@ package fr.xephi.authme;
|
||||
|
||||
import ch.jalu.injector.Injector;
|
||||
import ch.jalu.injector.InjectorBuilder;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
|
||||
import fr.xephi.authme.api.NewAPI;
|
||||
import fr.xephi.authme.command.CommandHandler;
|
||||
import fr.xephi.authme.datasource.DataSource;
|
||||
@@ -33,6 +35,9 @@ import fr.xephi.authme.settings.properties.SecuritySettings;
|
||||
import fr.xephi.authme.task.CleanupTask;
|
||||
import fr.xephi.authme.task.purge.PurgeService;
|
||||
import fr.xephi.authme.util.ExceptionUtils;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import org.apache.commons.lang.SystemUtils;
|
||||
import org.bukkit.Server;
|
||||
import org.bukkit.command.Command;
|
||||
@@ -43,8 +48,6 @@ import org.bukkit.plugin.java.JavaPlugin;
|
||||
import org.bukkit.plugin.java.JavaPluginLoader;
|
||||
import org.bukkit.scheduler.BukkitScheduler;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import static fr.xephi.authme.service.BukkitService.TICKS_PER_MINUTE;
|
||||
import static fr.xephi.authme.util.Utils.isClassLoaded;
|
||||
|
||||
@@ -156,9 +159,9 @@ public class AuthMe extends JavaPlugin {
|
||||
OnStartupTasks.sendMetrics(this, settings);
|
||||
|
||||
// Sponsor messages
|
||||
ConsoleLogger.info("Development builds are available on our jenkins, thanks to f14stelt.");
|
||||
ConsoleLogger.info("Do you want a good game server? Look at our sponsor GameHosting.it leader "
|
||||
+ "in Italy as Game Server Provider!");
|
||||
ConsoleLogger.info("Development builds are available on our jenkins, thanks to FastVM.io");
|
||||
ConsoleLogger.info("Do you want a good vps for your game server? Look at our sponsor FastVM.io leader "
|
||||
+ "as virtual server provider!");
|
||||
|
||||
// Successful message
|
||||
ConsoleLogger.info("AuthMe " + getPluginVersion() + " build n." + getPluginBuildNumber()
|
||||
|
||||
@@ -74,7 +74,7 @@ public class RegisterAdminCommand implements ExecutableCommand {
|
||||
final Player player = bukkitService.getPlayerExact(playerName);
|
||||
if (player != null) {
|
||||
bukkitService.scheduleSyncTaskFromOptionallyAsyncTask(() ->
|
||||
player.kickPlayer(commonService.retrieveSingleMessage(MessageKey.KICK_FOR_ADMIN_REGISTER)));
|
||||
player.kickPlayer(commonService.retrieveSingleMessage(player, MessageKey.KICK_FOR_ADMIN_REGISTER)));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -172,6 +172,11 @@ class MySqlDefaultChanger implements DebugSection {
|
||||
+ 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);
|
||||
|
||||
@@ -97,7 +97,7 @@ public class TempbanManager implements SettingsDependent, HasCleanup {
|
||||
if (isEnabled) {
|
||||
final String name = player.getName();
|
||||
final String ip = PlayerUtils.getPlayerIp(player);
|
||||
final String reason = messages.retrieveSingle(MessageKey.TEMPBAN_MAX_LOGINS);
|
||||
final String reason = messages.retrieveSingle(player, MessageKey.TEMPBAN_MAX_LOGINS);
|
||||
|
||||
final Date expires = new Date();
|
||||
long newTime = expires.getTime() + (length * MILLIS_PER_MINUTE);
|
||||
|
||||
@@ -49,9 +49,9 @@ class LimboPlayerTaskManager {
|
||||
*/
|
||||
void registerMessageTask(Player player, LimboPlayer limbo, boolean isRegistered) {
|
||||
int interval = settings.getProperty(RegistrationSettings.MESSAGE_INTERVAL);
|
||||
MessageResult messageResult = getMessageKey(player.getName(), isRegistered);
|
||||
MessageResult result = getMessageKey(player.getName(), isRegistered);
|
||||
if (interval > 0) {
|
||||
String[] joinMessage = messages.retrieveSingle(messageResult.messageKey, messageResult.args).split("\n");
|
||||
String[] joinMessage = messages.retrieveSingle(player, result.messageKey, result.args).split("\n");
|
||||
MessageTask messageTask = new MessageTask(player, joinMessage);
|
||||
bukkitService.runTaskTimer(messageTask, 2 * TICKS_PER_SECOND, interval * TICKS_PER_SECOND);
|
||||
limbo.setMessageTask(messageTask);
|
||||
@@ -67,7 +67,7 @@ class LimboPlayerTaskManager {
|
||||
void registerTimeoutTask(Player player, LimboPlayer limbo) {
|
||||
final int timeout = settings.getProperty(RestrictionSettings.TIMEOUT) * TICKS_PER_SECOND;
|
||||
if (timeout > 0) {
|
||||
String message = messages.retrieveSingle(MessageKey.LOGIN_TIMEOUT_ERROR);
|
||||
String message = messages.retrieveSingle(player, MessageKey.LOGIN_TIMEOUT_ERROR);
|
||||
BukkitTask task = bukkitService.runTaskLater(new TimeoutTask(player, message, playerCache), timeout);
|
||||
limbo.setTimeoutTask(task);
|
||||
}
|
||||
|
||||
@@ -28,6 +28,10 @@ import java.util.Set;
|
||||
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 implements DataSource {
|
||||
|
||||
private boolean useSsl;
|
||||
@@ -747,6 +751,13 @@ public class MySQL implements DataSource {
|
||||
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);
|
||||
|
||||
@@ -28,6 +28,7 @@ import static fr.xephi.authme.datasource.SqlDataSourceUtils.logSqlException;
|
||||
/**
|
||||
* SQLite data source.
|
||||
*/
|
||||
@SuppressWarnings({"checkstyle:AbbreviationAsWordInName"}) // Justification: Class name cannot be changed anymore
|
||||
public class SQLite implements DataSource {
|
||||
|
||||
private final Settings settings;
|
||||
|
||||
@@ -123,7 +123,7 @@ public class OnJoinVerifier implements Reloadable {
|
||||
return false;
|
||||
} else if (!permissionsManager.hasPermission(player, PlayerStatePermission.IS_VIP)) {
|
||||
// Server is full and player is NOT VIP; set kick message and proceed with kick
|
||||
event.setKickMessage(messages.retrieveSingle(MessageKey.KICK_FULL_SERVER));
|
||||
event.setKickMessage(messages.retrieveSingle(player, MessageKey.KICK_FULL_SERVER));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -135,12 +135,12 @@ public class OnJoinVerifier implements Reloadable {
|
||||
}
|
||||
Player nonVipPlayer = generateKickPlayer(onlinePlayers);
|
||||
if (nonVipPlayer != null) {
|
||||
nonVipPlayer.kickPlayer(messages.retrieveSingle(MessageKey.KICK_FOR_VIP));
|
||||
nonVipPlayer.kickPlayer(messages.retrieveSingle(player, MessageKey.KICK_FOR_VIP));
|
||||
event.allow();
|
||||
return false;
|
||||
} else {
|
||||
ConsoleLogger.info("VIP player " + player.getName() + " tried to join, but the server was full");
|
||||
event.setKickMessage(messages.retrieveSingle(MessageKey.KICK_FULL_SERVER));
|
||||
event.setKickMessage(messages.retrieveSingle(player, MessageKey.KICK_FULL_SERVER));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,7 +257,7 @@ public class PlayerListener implements Listener {
|
||||
try {
|
||||
runOnJoinChecks(JoiningPlayer.fromName(name), event.getAddress().getHostAddress());
|
||||
} catch (FailedVerificationException e) {
|
||||
event.setKickMessage(m.retrieveSingle(e.getReason(), e.getArgs()));
|
||||
event.setKickMessage(m.retrieveSingle(name, e.getReason(), e.getArgs()));
|
||||
event.setLoginResult(AsyncPlayerPreLoginEvent.Result.KICK_OTHER);
|
||||
}
|
||||
}
|
||||
@@ -285,7 +285,7 @@ public class PlayerListener implements Listener {
|
||||
try {
|
||||
runOnJoinChecks(JoiningPlayer.fromPlayerObject(player), event.getAddress().getHostAddress());
|
||||
} catch (FailedVerificationException e) {
|
||||
event.setKickMessage(m.retrieveSingle(e.getReason(), e.getArgs()));
|
||||
event.setKickMessage(m.retrieveSingle(player, e.getReason(), e.getArgs()));
|
||||
event.setResult(PlayerLoginEvent.Result.KICK_OTHER);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,10 +12,11 @@ import javax.inject.Inject;
|
||||
|
||||
public class PlayerListener19Spigot implements Listener {
|
||||
|
||||
private static boolean isPlayerSpawnLocationEventCalled = false;
|
||||
|
||||
@Inject
|
||||
private TeleportationService teleportationService;
|
||||
|
||||
private static boolean isPlayerSpawnLocationEventCalled = false;
|
||||
|
||||
public static boolean isPlayerSpawnLocationEventCalled() {
|
||||
return isPlayerSpawnLocationEventCalled;
|
||||
|
||||
@@ -5,9 +5,9 @@ import fr.xephi.authme.AuthMe;
|
||||
import fr.xephi.authme.ConsoleLogger;
|
||||
import fr.xephi.authme.data.auth.PlayerCache;
|
||||
import fr.xephi.authme.initialization.SettingsDependent;
|
||||
import fr.xephi.authme.service.BukkitService;
|
||||
import fr.xephi.authme.settings.Settings;
|
||||
import fr.xephi.authme.settings.properties.RestrictionSettings;
|
||||
import fr.xephi.authme.service.BukkitService;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import javax.inject.Inject;
|
||||
@@ -46,7 +46,7 @@ public class ProtocolLibService implements SettingsDependent {
|
||||
if (protectInvBeforeLogin) {
|
||||
ConsoleLogger.warning("WARNING! The protectInventory feature requires ProtocolLib! Disabling it...");
|
||||
}
|
||||
|
||||
|
||||
if (denyTabCompleteBeforeLogin) {
|
||||
ConsoleLogger.warning("WARNING! The denyTabComplete feature requires ProtocolLib! Disabling it...");
|
||||
}
|
||||
@@ -56,16 +56,21 @@ public class ProtocolLibService implements SettingsDependent {
|
||||
}
|
||||
|
||||
// Set up packet adapters
|
||||
if (protectInvBeforeLogin && inventoryPacketAdapter == null) {
|
||||
inventoryPacketAdapter = new InventoryPacketAdapter(plugin, playerCache);
|
||||
inventoryPacketAdapter.register();
|
||||
if (protectInvBeforeLogin) {
|
||||
if (inventoryPacketAdapter == null) {
|
||||
inventoryPacketAdapter = new InventoryPacketAdapter(plugin, playerCache);
|
||||
inventoryPacketAdapter.register();
|
||||
}
|
||||
} else if (inventoryPacketAdapter != null) {
|
||||
inventoryPacketAdapter.unregister();
|
||||
inventoryPacketAdapter = null;
|
||||
}
|
||||
if (denyTabCompleteBeforeLogin && tabCompletePacketAdapter == null) {
|
||||
tabCompletePacketAdapter = new TabCompletePacketAdapter(plugin, playerCache);
|
||||
tabCompletePacketAdapter.register();
|
||||
|
||||
if (denyTabCompleteBeforeLogin) {
|
||||
if (tabCompletePacketAdapter == null) {
|
||||
tabCompletePacketAdapter = new TabCompletePacketAdapter(plugin, playerCache);
|
||||
tabCompletePacketAdapter.register();
|
||||
}
|
||||
} else if (tabCompletePacketAdapter != null) {
|
||||
tabCompletePacketAdapter.unregister();
|
||||
tabCompletePacketAdapter = null;
|
||||
@@ -106,7 +111,7 @@ public class ProtocolLibService implements SettingsDependent {
|
||||
this.denyTabCompleteBeforeLogin = settings.getProperty(RestrictionSettings.DENY_TABCOMPLETE_BEFORE_LOGIN);
|
||||
|
||||
//it was true and will be deactivated now, so we need to restore the inventory for every player
|
||||
if (oldProtectInventory && !protectInvBeforeLogin) {
|
||||
if (oldProtectInventory && !protectInvBeforeLogin && inventoryPacketAdapter != null) {
|
||||
inventoryPacketAdapter.unregister();
|
||||
for (Player onlinePlayer : bukkitService.getOnlinePlayers()) {
|
||||
if (!playerCache.isAuthenticated(onlinePlayer.getName())) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import fr.xephi.authme.ConsoleLogger;
|
||||
import fr.xephi.authme.util.expiring.Duration;
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.util.Map;
|
||||
@@ -18,7 +19,9 @@ public class Messages {
|
||||
// Custom Authme tag replaced to new line
|
||||
private static final String NEWLINE_TAG = "%nl%";
|
||||
|
||||
private static final String PLAYER_TAG = "%username%";
|
||||
// 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<TimeUnit, MessageKey> TIME_UNIT_SINGULARS = ImmutableMap.<TimeUnit, MessageKey>builder()
|
||||
@@ -51,9 +54,9 @@ public class Messages {
|
||||
* @param key The key of the message to send
|
||||
*/
|
||||
public void send(CommandSender sender, MessageKey key) {
|
||||
String[] lines = retrieve(key);
|
||||
String[] lines = retrieve(key, sender);
|
||||
for (String line : lines) {
|
||||
sender.sendMessage(line.replaceAll(PLAYER_TAG, sender.getName()));
|
||||
sender.sendMessage(line);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +70,7 @@ public class Messages {
|
||||
* @param replacements The replacements to apply for the tags
|
||||
*/
|
||||
public void send(CommandSender sender, MessageKey key, String... replacements) {
|
||||
String message = retrieveSingle(key, replacements).replaceAll(PLAYER_TAG, sender.getName());
|
||||
String message = retrieveSingle(sender, key, replacements);
|
||||
for (String line : message.split("\n")) {
|
||||
sender.sendMessage(line);
|
||||
}
|
||||
@@ -77,10 +80,11 @@ public class Messages {
|
||||
* 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) {
|
||||
String message = retrieveMessage(key);
|
||||
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];
|
||||
@@ -101,18 +105,43 @@ public class Messages {
|
||||
? TIME_UNIT_SINGULARS.get(duration.getTimeUnit())
|
||||
: TIME_UNIT_PLURALS.get(duration.getTimeUnit());
|
||||
|
||||
return value + " " + retrieveMessage(timeUnitKey);
|
||||
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) {
|
||||
return formatMessage(
|
||||
messagesFileHandler.getMessage(key.getKey()));
|
||||
private String retrieveMessage(MessageKey key, CommandSender sender) {
|
||||
String message = messagesFileHandler.getMessage(key.getKey());
|
||||
String displayName = sender.getName();
|
||||
if (sender instanceof Player) {
|
||||
displayName = ((Player) sender).getDisplayName();
|
||||
}
|
||||
|
||||
return ChatColor.translateAlternateColorCodes('&', 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('&', message)
|
||||
.replace(NEWLINE_TAG, "\n")
|
||||
.replace(USERNAME_TAG, name)
|
||||
.replace(DISPLAYNAME_TAG, name);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -120,12 +149,13 @@ public class Messages {
|
||||
* 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(MessageKey key, String... replacements) {
|
||||
String message = retrieveMessage(key);
|
||||
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) {
|
||||
@@ -137,9 +167,26 @@ public class Messages {
|
||||
return message;
|
||||
}
|
||||
|
||||
private static String formatMessage(String message) {
|
||||
return ChatColor.translateAlternateColorCodes('&', message)
|
||||
.replace(NEWLINE_TAG, "\n");
|
||||
/**
|
||||
* 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 {
|
||||
ConsoleLogger.warning("Invalid number of replacements for message key '" + key + "'");
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -62,7 +62,8 @@ public class LuckPermsHandler implements PermissionHandler {
|
||||
return false;
|
||||
}
|
||||
|
||||
DataMutateResult result = user.setPermissionUnchecked(luckPermsApi.getNodeFactory().makeGroupNode(newGroup).build());
|
||||
DataMutateResult result = user.setPermissionUnchecked(
|
||||
luckPermsApi.getNodeFactory().makeGroupNode(newGroup).build());
|
||||
if (result == DataMutateResult.FAIL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -138,7 +138,7 @@ public class AsynchronousJoin implements AsynchronousProcess {
|
||||
|
||||
private void handlePlayerWithUnmetNameRestriction(Player player, String ip) {
|
||||
bukkitService.scheduleSyncTaskFromOptionallyAsyncTask(() -> {
|
||||
player.kickPlayer(service.retrieveSingleMessage(MessageKey.NOT_OWNER_ERROR));
|
||||
player.kickPlayer(service.retrieveSingleMessage(player, MessageKey.NOT_OWNER_ERROR));
|
||||
if (service.getProperty(RestrictionSettings.BAN_UNKNOWN_IP)) {
|
||||
server.banIP(ip);
|
||||
}
|
||||
@@ -188,7 +188,7 @@ public class AsynchronousJoin implements AsynchronousProcess {
|
||||
&& countOnlinePlayersByIp(ip) > service.getProperty(RestrictionSettings.MAX_JOIN_PER_IP)) {
|
||||
|
||||
bukkitService.scheduleSyncTaskFromOptionallyAsyncTask(
|
||||
() -> player.kickPlayer(service.retrieveSingleMessage(MessageKey.SAME_IP_ONLINE)));
|
||||
() -> player.kickPlayer(service.retrieveSingleMessage(player, MessageKey.SAME_IP_ONLINE)));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -214,7 +214,7 @@ public class AsynchronousLogin implements AsynchronousProcess {
|
||||
tempbanManager.tempbanPlayer(player);
|
||||
} else if (service.getProperty(RestrictionSettings.KICK_ON_WRONG_PASSWORD)) {
|
||||
bukkitService.scheduleSyncTaskFromOptionallyAsyncTask(
|
||||
() -> player.kickPlayer(service.retrieveSingleMessage(MessageKey.WRONG_PASSWORD)));
|
||||
() -> player.kickPlayer(service.retrieveSingleMessage(player, MessageKey.WRONG_PASSWORD)));
|
||||
} else {
|
||||
service.send(player, MessageKey.WRONG_PASSWORD);
|
||||
|
||||
|
||||
@@ -10,7 +10,9 @@ 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 {
|
||||
|
||||
@Inject
|
||||
@@ -22,6 +24,11 @@ public class ProcessSyncEmailRegister implements SynchronousProcess {
|
||||
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);
|
||||
|
||||
@@ -15,6 +15,7 @@ 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 {
|
||||
|
||||
@@ -46,6 +47,11 @@ public class ProcessSyncPasswordRegister implements SynchronousProcess {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
@@ -58,7 +64,7 @@ public class ProcessSyncPasswordRegister implements SynchronousProcess {
|
||||
|
||||
// Kick Player after Registration is enabled, kick the player
|
||||
if (service.getProperty(RegistrationSettings.FORCE_KICK_AFTER_REGISTER)) {
|
||||
player.kickPlayer(service.retrieveSingleMessage(MessageKey.REGISTER_SUCCESS));
|
||||
player.kickPlayer(service.retrieveSingleMessage(player, MessageKey.REGISTER_SUCCESS));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -63,11 +63,12 @@ public class CommonService {
|
||||
/**
|
||||
* 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(MessageKey key) {
|
||||
return messages.retrieveSingle(key);
|
||||
public String retrieveSingleMessage(CommandSender sender, MessageKey key) {
|
||||
return messages.retrieveSingle(sender, key);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,46 +1,81 @@
|
||||
package fr.xephi.authme.service;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.maxmind.geoip.LookupService;
|
||||
import com.google.common.hash.HashCode;
|
||||
import com.google.common.hash.HashFunction;
|
||||
import com.google.common.hash.Hashing;
|
||||
import com.google.common.io.Resources;
|
||||
import com.ice.tar.TarEntry;
|
||||
import com.ice.tar.TarInputStream;
|
||||
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.util.FileUtils;
|
||||
import fr.xephi.authme.util.InternetProtocolUtils;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.InetAddress;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.net.UnknownHostException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.nio.file.attribute.FileTime;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
|
||||
import static com.maxmind.geoip.LookupService.GEOIP_MEMORY_CACHE;
|
||||
import javax.inject.Inject;
|
||||
|
||||
public class GeoIpService {
|
||||
private static final String LICENSE =
|
||||
"[LICENSE] This product uses data from the GeoLite API created by MaxMind, available at http://www.maxmind.com";
|
||||
private static final String GEOIP_URL =
|
||||
"http://geolite.maxmind.com/download/geoip/database/GeoLiteCountry/GeoIP.dat.gz";
|
||||
private LookupService lookupService;
|
||||
private Thread downloadTask;
|
||||
|
||||
private final File dataFile;
|
||||
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_EXT = ".mmdb";
|
||||
private static final String DATABASE_FILE = DATABASE_NAME + DATABASE_EXT;
|
||||
|
||||
private static final String ARCHIVE_FILE = DATABASE_NAME + ".tar.gz";
|
||||
|
||||
private static final String ARCHIVE_URL = "https://geolite.maxmind.com/download/geoip/database/" + ARCHIVE_FILE;
|
||||
private static final String CHECKSUM_URL = ARCHIVE_URL + ".md5";
|
||||
|
||||
private static final int UPDATE_INTERVAL_DAYS = 30;
|
||||
|
||||
private final Path dataFile;
|
||||
private final BukkitService bukkitService;
|
||||
|
||||
private GeoIp2Provider databaseReader;
|
||||
private volatile boolean downloading;
|
||||
|
||||
@Inject
|
||||
GeoIpService(@DataFolder File dataFolder) {
|
||||
this.dataFile = new File(dataFolder, "GeoIP.dat");
|
||||
GeoIpService(@DataFolder File dataFolder, BukkitService bukkitService) {
|
||||
this.bukkitService = bukkitService;
|
||||
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, LookupService lookupService) {
|
||||
this.dataFile = dataFolder;
|
||||
this.lookupService = lookupService;
|
||||
GeoIpService(@DataFolder File dataFolder, BukkitService bukkitService, GeoIp2Provider reader) {
|
||||
this.bukkitService = bukkitService;
|
||||
this.dataFile = dataFolder.toPath().resolve(DATABASE_FILE);
|
||||
|
||||
this.databaseReader = reader;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -49,94 +84,186 @@ public class GeoIpService {
|
||||
* @return True if the data is available, false otherwise.
|
||||
*/
|
||||
private synchronized boolean isDataAvailable() {
|
||||
if (downloadTask != null && downloadTask.isAlive()) {
|
||||
if (downloading) {
|
||||
// we are currently downloading the database
|
||||
return false;
|
||||
}
|
||||
if (lookupService != null) {
|
||||
|
||||
if (databaseReader != null) {
|
||||
// everything is initialized
|
||||
return true;
|
||||
}
|
||||
|
||||
if (dataFile.exists()) {
|
||||
boolean dataIsOld = (System.currentTimeMillis() - dataFile.lastModified()) > TimeUnit.DAYS.toMillis(30);
|
||||
if (!dataIsOld) {
|
||||
try {
|
||||
lookupService = new LookupService(dataFile, GEOIP_MEMORY_CACHE);
|
||||
if (Files.exists(dataFile)) {
|
||||
try {
|
||||
FileTime lastModifiedTime = Files.getLastModifiedTime(dataFile);
|
||||
if (Duration.between(lastModifiedTime.toInstant(), Instant.now()).toDays() <= UPDATE_INTERVAL_DAYS) {
|
||||
databaseReader = new Reader(dataFile.toFile(), FileMode.MEMORY, new CHMCache());
|
||||
ConsoleLogger.info(LICENSE);
|
||||
|
||||
// don't fire the update task - we are up to date
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
ConsoleLogger.logException("Failed to load GeoLiteAPI database", e);
|
||||
return false;
|
||||
} else {
|
||||
ConsoleLogger.debug("GEO Ip database is older than " + UPDATE_INTERVAL_DAYS + " Days");
|
||||
}
|
||||
} else {
|
||||
FileUtils.delete(dataFile);
|
||||
} catch (IOException ioEx) {
|
||||
ConsoleLogger.logException("Failed to load GeoLiteAPI database", ioEx);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Ok, let's try to download the data file!
|
||||
downloadTask = createDownloadTask();
|
||||
downloadTask.start();
|
||||
|
||||
// File is outdated or doesn't exist - let's try to download the data file!
|
||||
startDownloadTask();
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a thread which will attempt to download new data from the GeoLite website.
|
||||
*
|
||||
* @return the generated download thread
|
||||
*/
|
||||
private Thread createDownloadTask() {
|
||||
return new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
URL downloadUrl = new URL(GEOIP_URL);
|
||||
URLConnection conn = downloadUrl.openConnection();
|
||||
conn.setConnectTimeout(10000);
|
||||
conn.connect();
|
||||
InputStream input = conn.getInputStream();
|
||||
if (conn.getURL().toString().endsWith(".gz")) {
|
||||
input = new GZIPInputStream(input);
|
||||
}
|
||||
OutputStream output = new FileOutputStream(dataFile);
|
||||
byte[] buffer = new byte[2048];
|
||||
int length = input.read(buffer);
|
||||
while (length >= 0) {
|
||||
output.write(buffer, 0, length);
|
||||
length = input.read(buffer);
|
||||
}
|
||||
output.close();
|
||||
input.close();
|
||||
} catch (IOException e) {
|
||||
ConsoleLogger.logException("Could not download GeoLiteAPI database", e);
|
||||
private void startDownloadTask() {
|
||||
downloading = true;
|
||||
|
||||
// use bukkit's cached threads
|
||||
bukkitService.runTaskAsynchronously(() -> {
|
||||
ConsoleLogger.info("Downloading GEO IP database, because the old database is outdated or doesn't exist");
|
||||
|
||||
Path tempFile = null;
|
||||
try {
|
||||
// download database to temporarily location
|
||||
tempFile = Files.createTempFile(ARCHIVE_FILE, null);
|
||||
try (OutputStream out = Files.newOutputStream(tempFile)) {
|
||||
Resources.copy(new URL(ARCHIVE_URL), out);
|
||||
}
|
||||
|
||||
// MD5 checksum verification
|
||||
String targetChecksum = Resources.toString(new URL(CHECKSUM_URL), StandardCharsets.UTF_8);
|
||||
if (!verifyChecksum(Hashing.md5(), tempFile, targetChecksum)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// tar extract database and copy to target destination
|
||||
if (!extractDatabase(tempFile, dataFile)) {
|
||||
ConsoleLogger.warning("Cannot find database inside downloaded GEO IP file at " + tempFile);
|
||||
return;
|
||||
}
|
||||
|
||||
ConsoleLogger.info("Successfully downloaded new GEO IP database to " + dataFile);
|
||||
|
||||
//only set this value to false on success otherwise errors could lead to endless download triggers
|
||||
downloading = false;
|
||||
} catch (IOException ioEx) {
|
||||
ConsoleLogger.logException("Could not download GeoLiteAPI database", ioEx);
|
||||
} finally {
|
||||
// clean up
|
||||
if (tempFile != null) {
|
||||
FileUtils.delete(tempFile.toFile());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @return true if equal, false otherwise
|
||||
* @throws IOException on I/O error reading the file
|
||||
*/
|
||||
private boolean verifyChecksum(HashFunction function, Path file, String expectedChecksum) throws IOException {
|
||||
HashCode actualHash = function.hashBytes(Files.readAllBytes(file));
|
||||
HashCode expectedHash = HashCode.fromString(expectedChecksum);
|
||||
if (Objects.equals(actualHash, expectedHash)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
ConsoleLogger.warning("GEO IP checksum verification failed");
|
||||
ConsoleLogger.warning("Expected: " + expectedHash + " Actual: " + actualHash);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the database from the tar archive. Existing outputFile will be replaced if it already exists.
|
||||
*
|
||||
* @param tarInputFile gzipped tar input file where the database is
|
||||
* @param outputFile destination file for the database
|
||||
* @return true if the database was found, false otherwise
|
||||
* @throws IOException on I/O error reading the tar archive or writing the output
|
||||
*/
|
||||
private boolean extractDatabase(Path tarInputFile, Path outputFile) throws IOException {
|
||||
// .gz -> gzipped file
|
||||
try (BufferedInputStream in = new BufferedInputStream(Files.newInputStream(tarInputFile));
|
||||
TarInputStream tarIn = new TarInputStream(new GZIPInputStream(in))) {
|
||||
TarEntry entry;
|
||||
while ((entry = tarIn.getNextEntry()) != null) {
|
||||
if (!entry.isDirectory()) {
|
||||
// filename including folders (absolute path inside the archive)
|
||||
String filename = entry.getName();
|
||||
if (filename.endsWith(DATABASE_EXT)) {
|
||||
// found the database file
|
||||
Files.copy(tarIn, outputFile, StandardCopyOption.REPLACE_EXISTING);
|
||||
|
||||
// update the last modification date to be same as in the archive
|
||||
Files.setLastModifiedTime(outputFile, FileTime.from(entry.getModTime().toInstant()));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
if (!InternetProtocolUtils.isLocalAddress(ip) && isDataAvailable()) {
|
||||
return lookupService.getCountry(ip).getCode();
|
||||
}
|
||||
return "--";
|
||||
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.
|
||||
*/
|
||||
public String getCountryName(String ip) {
|
||||
if (!InternetProtocolUtils.isLocalAddress(ip) && isDataAvailable()) {
|
||||
return lookupService.getCountry(ip).getName();
|
||||
}
|
||||
return "N/A";
|
||||
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
|
||||
* <ul>
|
||||
* <li>Database reader isn't initialized</li>
|
||||
* <li>MaxMind has no record about this IP address</li>
|
||||
* <li>IP address is local</li>
|
||||
* <li>Textual representation is not a valid IP address</li>
|
||||
* </ul>
|
||||
*/
|
||||
private Optional<Country> getCountry(String ip) {
|
||||
if (ip == null || ip.isEmpty() || InternetProtocolUtils.isLocalAddress(ip) || !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) {
|
||||
ConsoleLogger.logException("Cannot lookup country for " + ip + " at GEO IP database", ioEx);
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,11 +45,13 @@ public class SessionService implements Reloadable {
|
||||
database.setUnlogged(name);
|
||||
database.revokeSession(name);
|
||||
PlayerAuth auth = database.getAuth(name);
|
||||
if (hasValidSessionData(auth, player)) {
|
||||
|
||||
SessionState state = fetchSessionStatus(auth, player);
|
||||
if (state.equals(SessionState.VALID)) {
|
||||
RestoreSessionEvent event = bukkitService.createAndCallEvent(
|
||||
isAsync -> new RestoreSessionEvent(player, isAsync));
|
||||
return !event.isCancelled();
|
||||
} else {
|
||||
} else if (state.equals(SessionState.IP_CHANGED)) {
|
||||
service.send(player, MessageKey.SESSION_EXPIRED);
|
||||
}
|
||||
}
|
||||
@@ -62,19 +64,26 @@ public class SessionService implements Reloadable {
|
||||
*
|
||||
* @param auth the player auth
|
||||
* @param player the associated player
|
||||
* @return true if the player may resume his login session, false otherwise
|
||||
* @return SessionState based on the state of the session (VALID, NOT_VALID, OUTDATED, IP_CHANGED)
|
||||
*/
|
||||
private boolean hasValidSessionData(PlayerAuth auth, Player player) {
|
||||
private SessionState fetchSessionStatus(PlayerAuth auth, Player player) {
|
||||
if (auth == null) {
|
||||
ConsoleLogger.warning("No PlayerAuth in database for '" + player.getName() + "' during session check");
|
||||
return false;
|
||||
return SessionState.NOT_VALID;
|
||||
} else if (auth.getLastLogin() == null) {
|
||||
return false;
|
||||
return SessionState.NOT_VALID;
|
||||
}
|
||||
long timeSinceLastLogin = System.currentTimeMillis() - auth.getLastLogin();
|
||||
return PlayerUtils.getPlayerIp(player).equals(auth.getLastIp())
|
||||
&& timeSinceLastLogin > 0
|
||||
&& timeSinceLastLogin < service.getProperty(PluginSettings.SESSIONS_TIMEOUT) * MILLIS_PER_MINUTE;
|
||||
|
||||
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) {
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package fr.xephi.authme.service;
|
||||
|
||||
public enum SessionState {
|
||||
|
||||
VALID,
|
||||
|
||||
NOT_VALID,
|
||||
|
||||
OUTDATED,
|
||||
|
||||
IP_CHANGED
|
||||
|
||||
}
|
||||
@@ -65,7 +65,7 @@ public class BungeeSender implements SettingsDependent {
|
||||
public void connectPlayerOnLogin(Player player) {
|
||||
if (isEnabled && !destinationServerOnLogin.isEmpty()) {
|
||||
bukkitService.scheduleSyncDelayedTask(() ->
|
||||
sendBungeecordMessage("ConnectOther", player.getName(), destinationServerOnLogin), 20L);
|
||||
sendBungeecordMessage("ConnectOther", player.getName(), destinationServerOnLogin), 5L);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -56,9 +56,9 @@ public class SettingsWarner {
|
||||
// Warn if spigot.yml has settings.bungeecord set to true but config.yml has Hooks.bungeecord set to false
|
||||
if (Utils.isSpigot() && Bukkit.spigot().getConfig().getBoolean("settings.bungeecord")
|
||||
&& !settings.getProperty(HooksSettings.BUNGEECORD)) {
|
||||
ConsoleLogger.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!");
|
||||
ConsoleLogger.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!");
|
||||
}
|
||||
|
||||
// Check if argon2 library is present and can be loaded
|
||||
|
||||
@@ -22,7 +22,7 @@ public final class ProtectionSettings implements SettingsHolder {
|
||||
|
||||
@Comment({
|
||||
"Countries allowed to join the server and register. For country codes, see",
|
||||
"http://dev.maxmind.com/geoip/legacy/codes/iso3166/",
|
||||
"https://dev.maxmind.com/geoip/legacy/codes/iso3166/",
|
||||
"PLEASE USE QUOTES!"})
|
||||
public static final Property<List<String>> COUNTRIES_WHITELIST =
|
||||
newListProperty("Protection.countries", "US", "GB");
|
||||
|
||||
@@ -61,6 +61,11 @@ public class PurgeExecutor {
|
||||
purgePermissions(players);
|
||||
}
|
||||
|
||||
/**
|
||||
* Purges data from the AntiXray plugin.
|
||||
*
|
||||
* @param cleared the players whose data should be cleared
|
||||
*/
|
||||
synchronized void purgeAntiXray(Collection<String> cleared) {
|
||||
if (!settings.getProperty(PurgeSettings.REMOVE_ANTI_XRAY_FILE)) {
|
||||
return;
|
||||
@@ -95,6 +100,11 @@ public class PurgeExecutor {
|
||||
ConsoleLogger.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<String> cleared) {
|
||||
if (!settings.getProperty(PurgeSettings.REMOVE_LIMITED_CREATIVE_INVENTORIES)) {
|
||||
return;
|
||||
@@ -191,6 +201,11 @@ public class PurgeExecutor {
|
||||
ConsoleLogger.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<OfflinePlayer> cleared) {
|
||||
if (!settings.getProperty(PurgeSettings.REMOVE_PERMISSIONS)) {
|
||||
return;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package fr.xephi.authme.util;
|
||||
|
||||
import fr.xephi.authme.ConsoleLogger;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.command.ConsoleCommandSender;
|
||||
@@ -28,7 +29,12 @@ public final class Utils {
|
||||
* @return true if the running server instance is spigot-based.
|
||||
*/
|
||||
public static boolean isSpigot() {
|
||||
return isClassLoaded("org.spigotmc.SpigotConfig");
|
||||
try {
|
||||
Bukkit.spigot();
|
||||
return true;
|
||||
} catch (NoSuchMethodError e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user