package me.desht.scrollingmenusign.parser;
import com.google.common.base.Joiner;
import me.desht.dhutils.Debugger;
import me.desht.dhutils.LogUtils;
import me.desht.dhutils.MiscUtil;
import me.desht.dhutils.PermissionUtils;
import me.desht.dhutils.cost.Cost;
import me.desht.dhutils.cost.ItemCost;
import me.desht.scrollingmenusign.SMSException;
import me.desht.scrollingmenusign.SMSMacro;
import me.desht.scrollingmenusign.ScrollingMenuSign;
import me.desht.scrollingmenusign.commandlets.CommandletManager;
import me.desht.scrollingmenusign.enums.ReturnStatus;
import me.desht.scrollingmenusign.expector.ExpectCommandSubstitution;
import me.desht.scrollingmenusign.spout.SpoutUtils;
import me.desht.scrollingmenusign.util.Substitutions;
import me.desht.scrollingmenusign.views.CommandTrigger;
import me.desht.scrollingmenusign.views.SMSView;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import java.io.File;
import java.io.IOException;
import java.util.HashSet;
import java.util.List;
import java.util.Scanner;
import java.util.Set;
import java.util.logging.FileHandler;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class CommandParser {
private static final Pattern promptPat = Pattern.compile("<\\$:(.+?)>");
private static final Pattern passwordPat = Pattern.compile("<\\$p:(.+?)>");
private enum RunMode {CHECK_PERMS, EXECUTE}
private static Logger cmdLogger = null;
private final Set<String> macroHistory;
public CommandParser() {
if (cmdLogger == null) {
cmdLogger = Logger.getLogger(CommandParser.class.getName());
setLogFile(ScrollingMenuSign.getInstance().getConfig().getString("sms.command_log_file"));
}
this.macroHistory = new HashSet<String>();
}
public static void setLogFile(String logFileName) {
for (Handler h : cmdLogger.getHandlers()) {
h.close();
cmdLogger.removeHandler(h);
}
if (logFileName != null && !logFileName.isEmpty()) {
try {
File logFile = new File(ScrollingMenuSign.getInstance().getDataFolder(), logFileName);
FileHandler fh = new FileHandler(logFile.getPath());
CommandLogFormatter formatter = new CommandLogFormatter();
fh.setFormatter(formatter);
cmdLogger.addHandler(fh);
cmdLogger.setUseParentHandlers(false);
} catch (SecurityException e) {
LogUtils.warning("Can't log to " + logFileName + ": " + e.getMessage());
e.printStackTrace();
} catch (IOException e) {
LogUtils.warning("Can't log to " + logFileName + ": " + e.getMessage());
e.printStackTrace();
}
} else {
// no explicit log file - just use the parent handler, which should log to the Bukkit console
cmdLogger.setUseParentHandlers(true);
}
}
/**
* Parse and run a command string via the SMS command engine
*
* @param sender Player who is running the command
* @param command The command to be run
* @param trigger The command trigger
* @return The parsed command object, which gives access to details on how the command ran
* @throws SMSException if there was any problem running the command
*/
public ParsedCommand executeCommand(CommandSender sender, String command, CommandTrigger trigger) {
return handleCommandString(sender, trigger, command, RunMode.EXECUTE);
}
/**
* Parse and run a command string via the SMS command engine
*
* @param sender Player who is running the command
* @param command The command to be run
* @return The parsed command object, which gives access to details on how the command ran
* @throws SMSException if there was any problem running the command
*/
public ParsedCommand executeCommand(CommandSender sender, String command) {
return handleCommandString(sender, null, command, RunMode.EXECUTE);
}
/**
* Check that the given player has permission to create a menu entry with the given command.
*
* @param player Player who is creating the menu item
* @param command The command to be run
* @return true if the player is allowed to create this item, false otherwise
* @throws SMSException
*/
public boolean verifyCreationPerms(Player player, String command) throws SMSException {
ParsedCommand cmd = handleCommandString(player, null, command, RunMode.CHECK_PERMS);
return cmd == null || cmd.getStatus() == ReturnStatus.CMD_OK;
}
/**
* Handle one command string, which may contain multiple commands (chained with && or $$)
*
* @param sender the command sender
* @param trigger the command trigger
* @param command the command string
* @param mode the run mode
* @return a ParsedCommand object
* @throws SMSException
*/
private ParsedCommand handleCommandString(CommandSender sender, CommandTrigger trigger, String command, RunMode mode) throws SMSException {
if (sender instanceof Player) {
Player player = (Player) sender;
// see if any interactive substitution is needed
if (mode == RunMode.EXECUTE) {
Matcher m = promptPat.matcher(command);
if (m.find() && m.groupCount() > 0) {
ScrollingMenuSign.getInstance().responseHandler.expect(player, new ExpectCommandSubstitution(command, trigger));
return new ParsedCommand(ReturnStatus.SUBSTITUTION_NEEDED, m.group(1));
} else {
m = passwordPat.matcher(command);
if (m.find() && m.groupCount() > 0 && ScrollingMenuSign.getInstance().isSpoutEnabled()) {
SpoutUtils.setupPasswordPrompt(player, command, trigger);
return new ParsedCommand(ReturnStatus.SUBSTITUTION_NEEDED, m.group(1));
}
}
}
// make any user-defined substitutions
Set<String> missing = new HashSet<String>();
command = Substitutions.userVariableSubs(player, command, missing);
if (!missing.isEmpty() && mode == RunMode.EXECUTE) {
return new ParsedCommand(ReturnStatus.BAD_VARIABLE, "Command has uninitialised variables: " + missing.toString());
}
}
// make any view-specific substitutions
if (trigger instanceof SMSView) {
command = Substitutions.viewVariableSubs((SMSView) trigger, command);
} else {
command = Substitutions.viewVariableSubs(null, command);
}
Scanner scanner = new Scanner(command);
ParsedCommand cmd = null;
while (scanner.hasNext()) {
if (cmd != null && cmd.isCommandStopped()) {
// Not the first command in the sequence, and the outcome of the previous command
// means we need to stop here, i.e. the last command ended with "&&" but didn't run, or
// ended with "$$" but ran OK.
break;
}
cmd = new ParsedCommand(sender, trigger, scanner);
switch (mode) {
case EXECUTE:
execute(sender, trigger, cmd);
logCommandUsage(sender, cmd);
break;
case CHECK_PERMS:
cmd.setStatus(ReturnStatus.CMD_OK);
if ((cmd.isElevated() || cmd.isConsole()) && !PermissionUtils.isAllowedTo(sender, "scrollingmenusign.create.elevated")) {
cmd.setStatus(ReturnStatus.NO_PERMS);
return cmd;
} else if (!cmd.getCosts().isEmpty() && !PermissionUtils.isAllowedTo(sender, "scrollingmenusign.create.cost")) {
cmd.setStatus(ReturnStatus.NO_PERMS);
return cmd;
}
break;
default:
throw new IllegalArgumentException("unexpected run mode for parseCommandString()");
}
}
Debugger.getInstance().debug("final command: " + cmd);
return cmd;
}
private void logCommandUsage(CommandSender sender, ParsedCommand cmd) {
if (ScrollingMenuSign.getInstance().getConfig().getBoolean("sms.log_commands")) {
cmdLogger.log(Level.INFO, sender.getName() + " ran [" + cmd.getRawCommand() + "], outcome = " + cmd.getStatus() + " (" + cmd.getLastError() + ")");
}
}
private void execute(CommandSender sender, CommandTrigger trigger, ParsedCommand cmd) throws SMSException {
if (cmd.isRestricted()) {
// restriction checks can stop a command from running, but it's not an error condition
cmd.setLastError("Restriction checks prevented command from running");
cmd.setStatus(ReturnStatus.RESTRICTED);
return;
}
if (!cmd.isAffordable()) {
// failure to meet costs is an error condition that we report to the player
cmd.setLastError("You can't afford to run this command.");
cmd.setStatus(ReturnStatus.CANT_AFFORD);
return;
}
if (!cmd.isApplicable()) {
// an inapplicable cost is an error condition that we report to the player
cmd.setLastError("Doing this would not make sense...");
cmd.setStatus(ReturnStatus.INAPPLICABLE);
return;
}
// apply any costs associated with this command
if (sender instanceof Player) {
for (Cost cost : cmd.getCosts()) {
cost.apply((Player) sender);
if (cost instanceof ItemCost && ((ItemCost) cost).isItemsDropped()) {
MiscUtil.statusMessage(sender, "&6Your inventory is full. Some items dropped.");
}
}
}
if (cmd.getCommand() == null || cmd.getCommand().isEmpty()) {
// this allows for "commands" which only apply a cost and don't have an actual command
cmd.setStatus(ReturnStatus.CMD_OK);
return;
}
String command = cmd.getCommand() + " " + Joiner.on(' ').join(cmd.getArgs());
if (cmd.isMacro()) {
// run a macro
runMacro(sender, trigger, cmd);
} else if (cmd.isCommandlet()) {
runCommandlet(sender, trigger, cmd);
} else if (cmd.isWhisper()) {
// private message to the player
MiscUtil.alertMessage(sender, command);
cmd.setStatus(ReturnStatus.CMD_OK);
} else if (cmd.isConsole()) {
// run this as a console command
// only works for commands that may be run via the console, but should always work
if (!PermissionUtils.isAllowedTo(sender, "scrollingmenusign.execute.elevated")) {
cmd.setStatus(ReturnStatus.NO_PERMS);
cmd.setLastError("You don't have permission to run this command.");
return;
}
Debugger.getInstance().debug("Execute (console): " + command);
executeLowLevelCommand(Bukkit.getServer().getConsoleSender(), cmd, command);
} else if (cmd.isElevated()) {
// this is a /@ command, to be run as the real player, but with temporary permissions
// (this now also handles the /* fake-player style, which is no longer directly supported)
if (!PermissionUtils.isAllowedTo(sender, "scrollingmenusign.execute.elevated") || ScrollingMenuSign.permission == null) {
cmd.setStatus(ReturnStatus.NO_PERMS);
cmd.setLastError(ScrollingMenuSign.permission == null ?
"Permission elevation is not supported." :
"You don't have permission to run this command.");
return;
}
if (sender instanceof Player) {
executeElevated((Player) sender, cmd, command);
} else {
executeLowLevelCommand(sender, cmd, command);
}
} else {
// just an ordinary command (possibly chat), no special privilege elevation
Debugger.getInstance().debug("Execute (normal): " + command);
executeLowLevelCommand(sender, cmd, command);
}
}
private void executeElevated(Player player, ParsedCommand cmd, String command) {
List<String> nodes = ScrollingMenuSign.getInstance().getConfig().getStringList("sms.elevation.nodes");
boolean tempOp = false;
try {
for (String node : nodes) {
if (!node.isEmpty()) {
ScrollingMenuSign.permission.playerAddTransient(player, node);
Debugger.getInstance().debug("Added temporary permission node '" + node + "' to " + player.getDisplayName());
}
}
if (ScrollingMenuSign.getInstance().getConfig().getBoolean("sms.elevation.grant_op", false) && !player.isOp()) {
tempOp = true;
player.setOp(true);
Debugger.getInstance().debug("Granted temporary op to " + player.getDisplayName());
}
Debugger.getInstance().debug("Execute (elevated): " + command);
executeLowLevelCommand(player, cmd, command);
} finally {
// revoke all temporary permissions granted to the user
for (String node : nodes) {
if (!node.isEmpty()) {
ScrollingMenuSign.permission.playerRemoveTransient(player, node);
Debugger.getInstance().debug("Removed temporary permission node '" + node + "' from " + player.getDisplayName());
}
}
if (tempOp) {
player.setOp(false);
Debugger.getInstance().debug("Removed temporary op from " + player.getDisplayName());
}
}
}
private void runMacro(CommandSender sender, CommandTrigger trigger, ParsedCommand cmd) throws SMSException {
String macroName = cmd.getCommand();
if (macroHistory.contains(macroName)) {
LogUtils.warning("Recursion detected and stopped in macro " + macroName);
cmd.setStatus(ReturnStatus.WOULD_RECURSE);
cmd.setLastError("Recursion detected and stopped in macro " + macroName);
} else if (SMSMacro.hasMacro(macroName)) {
macroHistory.add(macroName);
ParsedCommand subCommand = null;
String allArgs = Joiner.on(" ").join(cmd.getArgs());
for (String c : SMSMacro.getCommands(macroName)) {
for (int i = 0; i < cmd.getQuotedArgs().length; i++) {
c = c.replace("<" + i + ">", cmd.getQuotedArgs()[i]);
}
c = c.replace("<*>", allArgs);
subCommand = handleCommandString(sender, trigger, c, RunMode.EXECUTE);
if (subCommand.isMacroStopped())
break;
}
// return status of a macro is the return status of the last command that was run
if (subCommand == null) {
cmd.setStatus(ReturnStatus.BAD_MACRO);
cmd.setLastError("Empty macro?");
} else {
cmd.setStatus(subCommand.getStatus());
cmd.setLastError(subCommand.getLastError());
}
} else {
cmd.setStatus(ReturnStatus.BAD_MACRO);
cmd.setLastError("Unknown macro " + macroName + ".");
}
}
private void runCommandlet(CommandSender sender, CommandTrigger trigger, ParsedCommand cmd) {
CommandletManager cmdlets = ScrollingMenuSign.getInstance().getCommandletManager();
boolean res = cmdlets.getCommandlet(cmd.getCommand()).execute(cmdlets.getPlugin(), sender, trigger, cmd.getCommand(), cmd.getQuotedArgs());
if (!res) {
// a commandlet returning false indicates the command should be treated as restricted
cmd.setStatus(ReturnStatus.RESTRICTED);
cmd.setRestricted(true);
} else {
cmd.setStatus(ReturnStatus.CMD_OK);
}
}
private void executeLowLevelCommand(CommandSender sender, ParsedCommand cmd, String command) {
cmd.setStatus(ReturnStatus.CMD_OK);
if (command.startsWith("/") && !cmd.isChat()) {
if (!Bukkit.getServer().dispatchCommand(sender, command.substring(1))) {
// It's possible the command is OK, but some plugins insist on implementing commands by hooking
// chat events, and dispatchCommand() does not work for those. So we'll try running the command
// via player.chat(). Sadly, player.chat() doesn't tell us if the command was found or not.
cmd.setStatus(ReturnStatus.UNKNOWN);
((Player) sender).chat(command);
}
} else if (sender instanceof Player) {
((Player) sender).chat(MiscUtil.parseColourSpec(command));
} else {
LogUtils.info("Chat: " + command);
}
}
}