package com.platymuus.bukkit.permissions;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.InvalidConfigurationException;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player;
import org.bukkit.permissions.PermissionAttachment;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.util.FileUtil;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.lang.reflect.Field;
import java.util.*;
import java.util.logging.Level;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Main class for PermissionsBukkit.
*/
public final class PermissionsPlugin extends JavaPlugin {
private final PlayerListener playerListener = new PlayerListener(this);
private final PermissionsCommand commandExecutor = new PermissionsCommand(this);
private final PermissionsTabComplete tabCompleter = new PermissionsTabComplete(this);
private final PermissionsMetrics metrics = new PermissionsMetrics(this);
private final HashMap<UUID, PermissionAttachment> permissions = new HashMap<UUID, PermissionAttachment>();
private File configFile;
private YamlConfiguration config;
public boolean configLoadError = false;
// -- Basic stuff
@Override
public void onEnable() {
// Take care of configuration
configFile = new File(getDataFolder(), "config.yml");
saveDefaultConfig();
reloadConfig();
// Register stuff
getCommand("permissions").setExecutor(commandExecutor);
getCommand("permissions").setTabCompleter(tabCompleter);
getServer().getPluginManager().registerEvents(playerListener, this);
// Register everyone online right now
for (Player p : getServer().getOnlinePlayers()) {
registerPlayer(p);
}
// Metrics are fun!
try {
metrics.start();
} catch (IOException ex) {
getLogger().warning("Failed to connect to plugin metrics: " + ex.getMessage());
}
// How are you gentlemen
int count = getServer().getOnlinePlayers().length;
if (count > 0) {
getLogger().info("Enabled successfully, " + count + " online players registered");
} else {
// "0 players registered" sounds too much like an error
getLogger().info("Enabled successfully");
}
}
@Override
public FileConfiguration getConfig() {
return config;
}
@Override
public void reloadConfig() {
config = new YamlConfiguration();
config.options().pathSeparator('/');
try {
config.load(configFile);
} catch (InvalidConfigurationException ex) {
configLoadError = true;
// extract line numbers from the exception if we can
ArrayList<String> lines = new ArrayList<String>();
Pattern pattern = Pattern.compile("line (\\d+), column");
Matcher matcher = pattern.matcher(ex.getMessage());
while (matcher.find()) {
String lineNo = matcher.group(1);
if (!lines.contains(lineNo)) {
lines.add(lineNo);
}
}
// make a nice message
String msg = "Your configuration is invalid! ";
if (lines.size() == 0) {
msg += "Unable to find any line numbers.";
} else {
msg += "Take a look at line(s): " + lines.get(0);
for (String lineNo : lines.subList(1, lines.size())) {
msg += ", " + lineNo;
}
}
getLogger().severe(msg);
// save the whole error to config_error.txt
try {
File outFile = new File(getDataFolder(), "config_error.txt");
PrintStream out = new PrintStream(new FileOutputStream(outFile));
out.println("Use the following website to help you find and fix configuration errors:");
out.println("https://yaml-online-parser.appspot.com/");
out.println();
out.println(ex.toString());
out.close();
getLogger().info("Saved the full error message to " + outFile);
} catch (IOException ex2) {
getLogger().severe("Failed to save the full error message!");
}
// save a backup
File backupFile = new File(getDataFolder(), "config_backup.yml");
File sourceFile = new File(getDataFolder(), "config.yml");
if (FileUtil.copy(sourceFile, backupFile)) {
getLogger().info("Saved a backup of your configuration to " + backupFile);
} else {
getLogger().severe("Failed to save a configuration backup!");
}
} catch (Exception ex) {
getLogger().log(Level.SEVERE, "Failed to load configuration", ex);
}
}
@Override
public void saveConfig() {
// If there's no keys (such as in the event of a load failure) don't save
if (config.getKeys(true).size() > 0) {
try {
config.save(configFile);
} catch (IOException ex) {
getLogger().log(Level.SEVERE, "Failed to save configuration", ex);
}
}
}
@Override
public void onDisable() {
// Unregister everyone
for (Player p : getServer().getOnlinePlayers()) {
unregisterPlayer(p);
}
// Good day to you! I said good day!
int count = getServer().getOnlinePlayers().length;
if (count > 0) {
getLogger().info("Disabled successfully, " + count + " online players unregistered");
}
}
// -- External API
/**
* Get the group with the given name.
*
* @param groupName The name of the group.
* @return A Group if it exists or null otherwise.
*/
public Group getGroup(String groupName) {
metrics.apiUsed();
if (getNode("groups") != null) {
for (String key : getNode("groups").getKeys(false)) {
if (key.equalsIgnoreCase(groupName)) {
return new Group(this, key);
}
}
}
return null;
}
/**
* Returns a list of groups a player is in.
*
* @param playerName The name of the player.
* @return The groups this player is in. May be empty.
* @deprecated Use UUIDs instead.
*/
@Deprecated
public List<Group> getGroups(String playerName) {
metrics.apiUsed();
ArrayList<Group> result = new ArrayList<Group>();
ConfigurationSection node = getUsernameNode(playerName);
if (node != null) {
for (String key : node.getStringList("groups")) {
result.add(new Group(this, key));
}
} else {
result.add(new Group(this, "default"));
}
return result;
}
/**
* Returns a list of groups a player is in.
*
* @param player The uuid of the player.
* @return The groups this player is in. May be empty.
*/
public List<Group> getGroups(UUID player) {
metrics.apiUsed();
ArrayList<Group> result = new ArrayList<Group>();
if (getNode("users/" + player) != null) {
for (String key : getNode("users/" + player).getStringList("groups")) {
result.add(new Group(this, key));
}
} else {
result.add(new Group(this, "default"));
}
return result;
}
/**
* Returns permission info on the given player.
*
* @param playerName The name of the player.
* @return A PermissionsInfo about this player.
* @deprecated Use UUIDs instead.
*/
@Deprecated
public PermissionInfo getPlayerInfo(String playerName) {
metrics.apiUsed();
ConfigurationSection node = getUsernameNode(playerName);
if (node == null) {
return null;
} else {
return new PermissionInfo(this, node, "groups");
}
}
/**
* Returns permission info on the given player.
*
* @param player The uuid of the player.
* @return A PermissionsInfo about this player.
*/
public PermissionInfo getPlayerInfo(UUID player) {
metrics.apiUsed();
if (getNode("users/" + player) == null) {
return null;
} else {
return new PermissionInfo(this, getNode("users/" + player), "groups");
}
}
/**
* Returns a list of all defined groups.
*
* @return The list of groups.
*/
public List<Group> getAllGroups() {
metrics.apiUsed();
ArrayList<Group> result = new ArrayList<Group>();
if (getNode("groups") != null) {
for (String key : getNode("groups").getKeys(false)) {
result.add(new Group(this, key));
}
}
return result;
}
// -- Plugin stuff
protected PermissionsMetrics getMetrics() {
return metrics;
}
protected void registerPlayer(Player player) {
if (permissions.containsKey(player.getUniqueId())) {
debug("Registering " + player.getName() + ": was already registered");
unregisterPlayer(player);
}
PermissionAttachment attachment = player.addAttachment(this);
permissions.put(player.getUniqueId(), attachment);
calculateAttachment(player);
}
protected void unregisterPlayer(Player player) {
if (permissions.containsKey(player.getUniqueId())) {
try {
player.removeAttachment(permissions.get(player.getUniqueId()));
} catch (IllegalArgumentException ex) {
debug("Unregistering " + player.getName() + ": player did not have attachment");
}
permissions.remove(player.getUniqueId());
} else {
debug("Unregistering " + player.getName() + ": was not registered");
}
}
protected void refreshForPlayer(UUID player) {
saveConfig();
debug("Refreshing for player " + player);
Player onlinePlayer = getServer().getPlayer(player);
if (onlinePlayer != null) {
calculateAttachment(onlinePlayer);
}
}
private void fillChildGroups(HashSet<String> childGroups, String group) {
if (childGroups.contains(group)) return;
childGroups.add(group);
for (String key : getNode("groups").getKeys(false)) {
for (String parent : getNode("groups/" + key).getStringList("inheritance")) {
if (parent.equalsIgnoreCase(group)) {
fillChildGroups(childGroups, key);
}
}
}
}
protected void refreshForGroup(String group) {
saveConfig();
// build the set of groups which are children of "group"
// e.g. if Bob is only a member of "expert" which inherits "user", he
// must be updated if the permissions of "user" change
HashSet<String> childGroups = new HashSet<String>();
fillChildGroups(childGroups, group);
debug("Refreshing for group " + group + " (total " + childGroups.size() + " subgroups)");
for (UUID uuid : permissions.keySet()) {
Player player = getServer().getPlayer(uuid);
ConfigurationSection node = getUserNode(player);
// if the player isn't in the config, act like they're in default
List<String> groupList = (node != null) ? node.getStringList("groups") : Arrays.asList("default");
for (String userGroup : groupList) {
if (childGroups.contains(userGroup)) {
calculateAttachment(player);
break;
}
}
}
}
protected void refreshPermissions() {
debug("Refreshing all permissions (for " + permissions.size() + " players)");
for (UUID player : permissions.keySet()) {
calculateAttachment(getServer().getPlayer(player));
}
}
protected ConfigurationSection getNode(String node) {
for (String entry : getConfig().getKeys(true)) {
if (node.equalsIgnoreCase(entry) && getConfig().isConfigurationSection(entry)) {
return getConfig().getConfigurationSection(entry);
}
}
return null;
}
protected ConfigurationSection getUserNode(Player player) {
ConfigurationSection sec = getNode("users/" + player.getUniqueId());
if (sec == null) {
sec = getNode("users/" + player.getName());
if (sec != null) {
getConfig().set(sec.getCurrentPath(), null);
getConfig().set("users/" + player.getUniqueId(), sec);
sec.set("name", player.getName());
debug("Migrated " + player.getName() + " to UUID " + player.getUniqueId());
saveConfig();
}
}
// make sure name field matches
if (sec != null) {
if (!player.getName().equals(sec.getString("name"))) {
debug("Updating name of " + player.getUniqueId() + " to: " + player.getName());
sec.set("name", player.getName());
saveConfig();
}
}
return sec;
}
protected ConfigurationSection getUsernameNode(String name) {
// try to look up node based on username rather than UUID
ConfigurationSection sec = getNode("users");
if (sec != null) {
for (String child : sec.getKeys(false)) {
ConfigurationSection node = sec.getConfigurationSection(child);
if (node != null && (name.equals(node.getString("name")) || name.equals("child"))) {
// either the "name" field matches or the key matches
return node;
}
}
}
return null;
}
protected ConfigurationSection createNode(String node) {
ConfigurationSection sec = getConfig();
for (String piece : node.split("/")) {
ConfigurationSection sec2 = getNode(sec == getConfig() ? piece : sec.getCurrentPath() + "/" + piece);
if (sec2 == null) {
sec2 = sec.createSection(piece);
}
sec = sec2;
}
return sec;
}
protected HashMap<String, Boolean> getAllPerms(String desc, String path) {
ConfigurationSection node = getNode(path);
int failures = 0;
String firstFailure = "";
// Make an attempt to autofix incorrect nesting
boolean fixed = false, fixedNow = true;
while (fixedNow) {
fixedNow = false;
for (String key : node.getKeys(true)) {
if (node.isBoolean(key) && key.contains("/")) {
node.set(key.replace("/", "."), node.getBoolean(key));
node.set(key, null);
fixed = fixedNow = true;
} else if (node.isConfigurationSection(key) && node.getConfigurationSection(key).getKeys(true).size() == 0) {
node.set(key, null);
fixed = fixedNow = true;
}
}
}
if (fixed) {
getLogger().info("Fixed broken nesting in " + desc + ".");
saveConfig();
}
LinkedHashMap<String, Boolean> result = new LinkedHashMap<String, Boolean>();
// Do the actual getting of permissions
for (String key : node.getKeys(false)) {
if (node.isBoolean(key)) {
result.put(key, node.getBoolean(key));
} else {
++failures;
if (firstFailure.length() == 0) {
firstFailure = key;
}
}
}
if (failures == 1) {
getLogger().warning("In " + desc + ": " + firstFailure + " is non-boolean.");
} else if (failures > 1) {
getLogger().warning("In " + desc + ": " + firstFailure + " is non-boolean (+" + (failures - 1) + " more).");
}
return result;
}
protected void debug(String message) {
if (getConfig().getBoolean("debug", false)) {
getLogger().info("Debug: " + message);
}
}
protected void calculateAttachment(Player player) {
if (player == null) {
return;
}
PermissionAttachment attachment = permissions.get(player.getUniqueId());
if (attachment == null) {
debug("Calculating permissions on " + player.getName() + ": attachment was null");
return;
}
Map<String, Boolean> values = calculatePlayerPermissions(player, player.getWorld().getName());
// Fill the attachment reflectively so we don't recalculate for each permission
// it turns out there's a lot of permissions!
Map<String, Boolean> dest = reflectMap(attachment);
dest.clear();
dest.putAll(values);
debug("Calculated permissions on " + player.getName() + ": " + dest.size() + " values");
player.recalculatePermissions();
}
// -- Private stuff
private Field pField;
@SuppressWarnings("unchecked")
private Map<String, Boolean> reflectMap(PermissionAttachment attachment) {
try {
if (pField == null) {
pField = PermissionAttachment.class.getDeclaredField("permissions");
pField.setAccessible(true);
}
return (Map<String, Boolean>) pField.get(attachment);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
// normally, LinkedHashMap.put (and thus putAll) will not reorder the list
// if that key is already in the map, which we don't want - later puts should
// always be bumped to the end of the list
private <K, V> void put(Map<K, V> dest, K key, V value) {
dest.remove(key);
dest.put(key, value);
}
private <K, V> void putAll(Map<K, V> dest, Map<K, V> src) {
for (Map.Entry<K, V> entry : src.entrySet()) {
put(dest, entry.getKey(), entry.getValue());
}
}
private Map<String, Boolean> calculatePlayerPermissions(Player player, String world) {
ConfigurationSection node = getUserNode(player);
// if the player isn't in the config, act like they're in default
if (node == null) {
return calculateGroupPermissions("default", world);
}
String nodePath = node.getCurrentPath();
Map<String, Boolean> perms = new LinkedHashMap<String, Boolean>();
// first, apply the player's groups (getStringList returns an empty list if not found)
// later groups override earlier groups
for (String group : node.getStringList("groups")) {
putAll(perms, calculateGroupPermissions(group, world));
}
// now apply user-specific permissions
if (getNode(nodePath + "/permissions") != null) {
putAll(perms, getAllPerms("user " + player, nodePath + "/permissions"));
}
// now apply world- and user-specific permissions
if (getNode(nodePath + "/worlds/" + world) != null) {
putAll(perms, getAllPerms("user " + player + " world " + world, nodePath + "/worlds/" + world));
}
return perms;
}
private Map<String, Boolean> calculateGroupPermissions(String group, String world) {
return calculateGroupPermissions0(new HashSet<String>(), group, world);
}
private Map<String, Boolean> calculateGroupPermissions0(Set<String> recursionBuffer, String group, String world) {
String groupNode = "groups/" + group;
// if the group's not in the config, nothing
if (getNode(groupNode) == null) {
return new LinkedHashMap<String, Boolean>();
}
recursionBuffer.add(group);
Map<String, Boolean> perms = new LinkedHashMap<String, Boolean>();
// first apply any parent groups (see calculatePlayerPermissions for more)
for (String parent : getNode(groupNode).getStringList("inheritance")) {
if (recursionBuffer.contains(parent)) {
getLogger().warning("In group " + group + ": recursive inheritance from " + parent);
continue;
}
putAll(perms, calculateGroupPermissions0(recursionBuffer, parent, world));
}
// now apply the group's permissions
if (getNode(groupNode + "/permissions") != null) {
putAll(perms, getAllPerms("group " + group, groupNode + "/permissions"));
}
// now apply world-specific permissions
if (getNode(groupNode + "/worlds/" + world) != null) {
putAll(perms, getAllPerms("group " + group + " world " + world, groupNode + "/worlds/" + world));
}
return perms;
}
}