/**
* PermissionsEx
* Copyright (C) zml and PermissionsEx contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ninja.leaping.permissionsex.bukkit;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import net.milkbowl.vault.chat.Chat;
import net.milkbowl.vault.permission.Permission;
import ninja.leaping.configurate.ConfigurationNode;
import ninja.leaping.configurate.loader.ConfigurationLoader;
import ninja.leaping.configurate.yaml.YAMLConfigurationLoader;
import ninja.leaping.permissionsex.ImplementationInterface;
import ninja.leaping.permissionsex.PermissionsEx;
import ninja.leaping.permissionsex.config.FilePermissionsExConfiguration;
import ninja.leaping.permissionsex.logging.TranslatableLogger;
import ninja.leaping.permissionsex.subject.SubjectType;
import ninja.leaping.permissionsex.util.command.CommandSpec;
import org.bukkit.command.PluginCommand;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.AsyncPlayerPreLoginEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.permissions.Permissible;
import org.bukkit.plugin.ServicePriority;
import org.bukkit.plugin.java.JavaPlugin;
import org.slf4j.Logger;
import org.slf4j.impl.JDK14LoggerAdapter;
import org.yaml.snakeyaml.DumperOptions;
import javax.sql.DataSource;
import java.io.File;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.function.BiFunction;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static ninja.leaping.permissionsex.bukkit.CraftBukkitInterface.getCBClassName;
import static ninja.leaping.permissionsex.bukkit.BukkitTranslations.t;
/**
* PermissionsEx plugin
*/
public class PermissionsExPlugin extends JavaPlugin implements Listener {
private static final PermissibleInjector[] INJECTORS = new PermissibleInjector[] {
new PermissibleInjector.ClassPresencePermissibleInjector("net.glowstone.entity.GlowHumanEntity", "permissions", true),
new PermissibleInjector.ClassPresencePermissibleInjector("org.getspout.server.entity.SpoutHumanEntity", "permissions", true),
new PermissibleInjector.ClassNameRegexPermissibleInjector("org.getspout.spout.player.SpoutCraftPlayer", "perm", false, "org\\.getspout\\.spout\\.player\\.SpoutCraftPlayer"),
new PermissibleInjector.ClassPresencePermissibleInjector(getCBClassName("entity.CraftHumanEntity"), "perm", true),
};
public static final String SERVER_TAG_CONTEXT = "server-tag";
private static final Pattern JDBC_URL_REGEX = Pattern.compile("(?:jdbc:)?([^:]+):(//)?(?:([^:]+)(?::([^@]+))?@)?(.*)");
static final Map<String, BiFunction<PermissionsExPlugin, String, String>> PATH_CANONICALIZERS;
static final Map<String, Properties> PROTOCOL_SPECIFIC_PROPS;
static {
ImmutableMap.Builder<String, Properties> build = ImmutableMap.builder();
final Properties mySqlProps = new Properties();
mySqlProps.setProperty("useConfigs",
"maxPerformance"); // Config options based on http://assets.en.oreilly
// .com/1/event/21/Connector_J%20Performance%20Gems%20Presentation.pdf
build.put("com.mysql.jdbc.Driver", mySqlProps);
build.put("org.mariadb.jdbc.Driver", mySqlProps);
PROTOCOL_SPECIFIC_PROPS = build.build();
PATH_CANONICALIZERS = ImmutableMap.of("h2", (plugin, orig) -> {
// Bleh if only h2 had a better way of supplying a base directory... oh well...
org.h2.engine.ConnectionInfo h2Info = new org.h2.engine.ConnectionInfo(orig);
if (!h2Info.isPersistent() || h2Info.isRemote()) {
return orig;
}
if (orig.startsWith("file:")) {
orig = orig.substring("file:".length());
}
Path origPath = Paths.get(orig);
if (origPath.isAbsolute()) {
return origPath.toString();
} else {
return plugin.getDataFolder().toPath().toAbsolutePath().resolve(origPath).toString();
}
});
}
private PermissionsEx manager;
private TranslatableLogger logger;
// Injections into superperms
private PermissionList permsList;
// Permissions subscriptions handling
private PEXPermissionSubscriptionMap subscriptionHandler;
private volatile boolean enabled;
private Path dataPath;
/**
* Because of Bukkit's special logging fun, we have to get an slf4j wrapper using specifically the logger that Bukkit provides us...
* @return
*/
private TranslatableLogger createLogger() {
try {
Constructor<JDK14LoggerAdapter> adapter = JDK14LoggerAdapter.class.getDeclaredConstructor(java.util.logging.Logger.class);
adapter.setAccessible(true);
return TranslatableLogger.forLogger(adapter.newInstance(getLogger()));
} catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
throw new ExceptionInInitializerError(e);
}
}
@Override
public void onEnable() {
this.dataPath = getDataFolder().toPath();
logger = createLogger();
ConfigurationLoader<ConfigurationNode> configLoader = YAMLConfigurationLoader.builder()
.setFile(new File(getDataFolder(), "config.yml"))
.setFlowStyle(DumperOptions.FlowStyle.BLOCK)
.build();
try {
getDataFolder().mkdirs();
this.manager = new PermissionsEx(FilePermissionsExConfiguration.fromLoader(configLoader), new BukkitImplementationInterface());
/*} catch (PEBKACException e) {
logger.warn(e.getTranslatableMessage());
getServer().getPluginManager().disablePlugin(this);
return;*/
} catch (Exception e) {
logger.error(t("Error occurred while enabling %s", getDescription().getName()), e);
getServer().getPluginManager().disablePlugin(this);
return;
}
manager.getSubjects(PermissionsEx.SUBJECTS_USER).setTypeInfo(new UserSubjectTypeDescription(PermissionsEx.SUBJECTS_USER, this));
getServer().getPluginManager().registerEvents(this, this);
subscriptionHandler = PEXPermissionSubscriptionMap.inject(this, this.getServer().getPluginManager());
permsList = PermissionList.inject(this);
injectAllPermissibles();
if (getServer().getPluginManager().isPluginEnabled("Vault")) {
final PEXVault vault = new PEXVault(this);
getServer().getServicesManager().register(Permission.class, vault, this, ServicePriority.High); // Hook into vault
getServer().getServicesManager().register(Chat.class, new PEXVaultChat(vault), this, ServicePriority.High);
logger.info(t("Hooked into Vault for Permission and Chat interfaces"));
}
enabled = true;
}
@Override
public void onDisable() {
if (manager != null) {
manager.close();
manager = null;
}
if (subscriptionHandler != null) {
subscriptionHandler.uninject();
subscriptionHandler = null;
}
if (permsList != null) {
permsList.uninject();
}
uninjectAllPermissibles();
}
@EventHandler
public void onPlayerPreLogin(final AsyncPlayerPreLoginEvent event) {
getUserSubjects().get(event.getUniqueId().toString()).exceptionally(e -> {
logger.warn(t("Error while loading data for user %s/%s during prelogin: %s", event.getName(), event.getUniqueId().toString(), e.getMessage()), e);
return null;
});
}
@EventHandler
public void onPlayerJoin(final PlayerJoinEvent event) {
final String identifier = event.getPlayer().getUniqueId().toString();
getUserSubjects().isRegistered(identifier).thenAccept(registered -> {
if (registered) {
getUserSubjects().persistentData().update(identifier, input -> {
if (!event.getPlayer().getName().equals(input.getOptions(PermissionsEx.GLOBAL_CONTEXT).get("name"))) {
return input.setOption(PermissionsEx.GLOBAL_CONTEXT, "name", event.getPlayer().getName());
} else {
return input;
}
});
}
});
injectPermissible(event.getPlayer());
}
@EventHandler(priority = EventPriority.MONITOR) // Happen last
public void onPlayerQuit(PlayerQuitEvent event) {
uninjectPermissible(event.getPlayer());
getUserSubjects().uncache(event.getPlayer().getUniqueId().toString());
}
public PermissionList getPermissionList() {
return permsList;
}
public PermissionsEx getManager() {
return this.manager;
}
public SubjectType getUserSubjects() {
return getManager().getSubjects(PermissionsEx.SUBJECTS_USER);
}
public SubjectType getGroupSubjects() {
return getManager().getSubjects(PermissionsEx.SUBJECTS_GROUP);
}
public void injectPermissible(Player player) {
try {
PEXPermissible permissible = new PEXPermissible(player, this);
boolean success = false, found = false;
for (PermissibleInjector injector : INJECTORS) {
if (injector.isApplicable(player)) {
found = true;
Permissible oldPerm = injector.inject(player, permissible);
if (oldPerm != null) {
permissible.setPreviousPermissible(oldPerm);
success = true;
break;
}
}
}
if (!found) {
logger.warn(t("No Permissible injector found for your server implementation!"));
} else if (!success) {
logger.warn(t("Unable to inject PEX's permissible for %s", player.getName()));
}
permissible.recalculatePermissions();
if (success && getManager().hasDebugMode()) {
logger.info(t("Permissions handler for %s successfully injected", player.getName()));
}
} catch (Throwable e) {
logger.error(t("Unable to inject permissible for %s", player.getName()), e);
}
}
private void injectAllPermissibles() {
getServer().getOnlinePlayers().forEach(this::injectPermissible);
}
private void uninjectPermissible(Player player) {
try {
boolean success = false;
for (PermissibleInjector injector : INJECTORS) {
if (injector.isApplicable(player)) {
Permissible pexPerm = injector.getPermissible(player);
if (pexPerm instanceof PEXPermissible) {
if (injector.inject(player, ((PEXPermissible) pexPerm).getPreviousPermissible()) != null) {
success = true;
break;
}
} else {
success = true;
break;
}
}
}
if (!success) {
logger.warn(t("No Permissible injector found for your server implementation (while uninjecting for %s)!", player.getName()));
} else if (getManager() != null && getManager().hasDebugMode()) {
logger.info(t("Permissions handler for %s successfully uninjected", player.getName()));
}
} catch (Throwable e) {
e.printStackTrace();
}
}
private void uninjectAllPermissibles() {
getServer().getOnlinePlayers().forEach(this::uninjectPermissible);
}
private class BukkitImplementationInterface implements ImplementationInterface {
private final Executor bukkitExecutor = runnable -> {
if (enabled) {
getServer().getScheduler()
.runTaskAsynchronously(PermissionsExPlugin.this, runnable);
} else {
runnable.run();
}
};
@Override
public Path getBaseDirectory() {
return dataPath;
}
@Override
public Logger getLogger() {
return logger;
}
@Override
public DataSource getDataSourceForURL(String url) throws SQLException {
// Based on Sponge`s code, but without alias handling and caching
Matcher match = JDBC_URL_REGEX.matcher(url);
if (!match.matches()) {
throw new IllegalArgumentException("URL " + url + " is not a valid JDBC URL");
}
final String protocol = match.group(1);
final boolean hasSlashes = match.group(2) != null;
final String user = match.group(3);
final String pass = match.group(4);
String serverDatabaseSpecifier = match.group(5);
BiFunction<PermissionsExPlugin, String, String> derelativizer = PATH_CANONICALIZERS.get(protocol);
if (derelativizer != null) {
serverDatabaseSpecifier = derelativizer.apply(PermissionsExPlugin.this, serverDatabaseSpecifier);
}
final String unauthedUrl = "jdbc:" + protocol + (hasSlashes ? "://" : ":") + serverDatabaseSpecifier;
final String driverClass = DriverManager.getDriver(unauthedUrl).getClass().getCanonicalName();
HikariConfig config = new HikariConfig();
config.setUsername(user);
config.setPassword(pass);
config.setDriverClassName(driverClass);
// https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing for info on pool sizing
config.setMaximumPoolSize((Runtime.getRuntime().availableProcessors() * 2) + 1);
Properties driverSpecificProperties = PROTOCOL_SPECIFIC_PROPS.get(driverClass);
final Properties dsProps = driverSpecificProperties == null ? new Properties() : new Properties(driverSpecificProperties);
dsProps.setProperty("baseDir", getBaseDirectory().toAbsolutePath().toString());
config.setDataSourceProperties(dsProps);
config.setJdbcUrl(unauthedUrl);
return new HikariDataSource(config);
}
/**
* Get an executor to run tasks asynchronously on.
*
* @return The async executor
*/
@Override
public Executor getAsyncExecutor() {
return bukkitExecutor;
}
@Override
public void registerCommand(CommandSpec command) {
PluginCommand cmd = getCommand(command.getAliases().get(0));
if (cmd != null) {
PEXBukkitCommand bukkitCommand = new PEXBukkitCommand(command, PermissionsExPlugin.this);
cmd.setExecutor(bukkitCommand);
cmd.setTabCompleter(bukkitCommand);
}
}
@Override
public Set<CommandSpec> getImplementationCommands() {
return ImmutableSet.of();
}
@Override
public String getVersion() {
return getDescription().getVersion();
}
}
}