Refactor command filter into a unit tested class.

Also adds support for regex \s as the subcommand delimiter.
This commit is contained in:
sk89q 2014-07-21 19:36:25 -07:00
parent abeab91be4
commit cd221ea19c
4 changed files with 409 additions and 82 deletions

View File

@ -25,7 +25,9 @@
<allow pkg="java"/>
<allow pkg="javax"/>
<allow pkg="org.junit"/>
<allow pkg="junit"/>
<allow pkg="org.mockito"/>
<allow pkg="org.hamcrest"/>
<allow pkg="com.sk89q"/>
<allow pkg="org.enginehub"/>
<allow pkg="org.bukkit"/>

View File

@ -35,6 +35,7 @@
import com.sk89q.worldguard.protection.flags.DefaultFlag;
import com.sk89q.worldguard.protection.managers.RegionManager;
import com.sk89q.worldguard.protection.regions.ProtectedRegion;
import com.sk89q.worldguard.util.command.CommandFilter;
import org.bukkit.ChatColor;
import org.bukkit.GameMode;
import org.bukkit.Location;
@ -1443,91 +1444,12 @@ public void onPlayerCommandPreprocess(PlayerCommandPreprocessEvent event) {
RegionManager mgr = plugin.getGlobalRegionManager().get(world);
ApplicableRegionSet set = mgr.getApplicableRegions(pt);
String usedCommand = event.getMessage().toLowerCase();
Set<String> allowedCommands = set.getFlag(DefaultFlag.ALLOWED_CMDS, localPlayer);
Set<String> blockedCommands = set.getFlag(DefaultFlag.BLOCKED_CMDS, localPlayer);
CommandFilter test = new CommandFilter(allowedCommands, blockedCommands);
/*
* blocked used allow?
* x x no
* x x y no
* x y x yes
* x y x y no
*
* allowed used allow?
* x x yes
* x x y yes
* x y x no
* x y x y yes
*/
String result = "";
String[] usedParts = usedCommand.split(" ");
if (blockedCommands != null) {
blocked:
for (String blockedCommand : blockedCommands) {
String[] blockedParts = blockedCommand.split(" ");
for (int i = 0; i < blockedParts.length && i < usedParts.length; i++) {
if (blockedParts[i].equalsIgnoreCase(usedParts[i])) {
// first part matches - check if it's the whole thing
if (i + 1 == blockedParts.length) {
// consumed all blocked parts, block entire command
result = blockedCommand;
break blocked;
} else {
// more blocked parts to check, also check used length
if (i + 1 == usedParts.length) {
// all that was used, but there is more in blocked
// allow this, check next command in flag
continue blocked;
} else {
// more in both blocked and used, continue checking
continue;
}
}
} else {
// found non-matching part, stop checking this command
continue blocked;
}
}
}
}
if (allowedCommands != null) {
allowed:
for (String allowedCommand : allowedCommands) {
String[] allowedParts = allowedCommand.split(" ");
for (int i = 0; i < allowedParts.length && i < usedParts.length; i++) {
if (allowedParts[i].equalsIgnoreCase(usedParts[i])) {
// this part matches - check if it's the whole thing
if (i + 1 == allowedParts.length) {
// consumed all allowed parts before reaching used length
// this command is definitely allowed
result = "";
break allowed;
} else {
// more allowed parts to check
if (i + 1 == usedParts.length) {
// all that was used, but there is more in allowed
// block for now, check next part of flag
result = usedCommand;
continue allowed;
} else {
// more in both allowed and used, continue checking for match
continue;
}
}
} else {
// doesn't match at all, block it, check next flag string
result = usedCommand;
continue allowed;
}
}
}
}
if (!result.isEmpty()) {
player.sendMessage(ChatColor.RED + result + " is not allowed in this area.");
if (!test.apply(event.getMessage())) {
player.sendMessage(ChatColor.RED + event.getMessage() + " is not allowed in this area.");
event.setCancelled(true);
return;
}

View File

@ -0,0 +1,207 @@
/*
* WorldGuard, a suite of tools for Minecraft
* Copyright (C) sk89q <http://www.sk89q.com>
* 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 <http://www.gnu.org/licenses/>.
*/
package com.sk89q.worldguard.util.command;
import com.google.common.base.Predicate;
import javax.annotation.Nullable;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* Checks whether a command is permitted with support for subcommands
* split by {@code \s} (regular expressions).
*
* <p>{@code permitted} always overrides {@code denied} (unlike other parts of
* WorldGuard). Either can be null. If both are null, then every command is
* permitted. If only {@code permitted} is null, then all commands but
* those in the list of denied are permitted. If only {@code denied} is null,
* then only commands in the list of permitted are permitted. If neither are
* null, only permitted commands are permitted and the list of denied commands
* is not used.</p>
*
* <p>The test is case in-sensitive.</p>
*/
public class CommandFilter implements Predicate<String> {
@Nullable
private final Collection<String> permitted;
@Nullable
private final Collection<String> denied;
/**
* Create a new instance.
*
* @param permitted a list of rules for permitted commands
* @param denied a list of rules for denied commands
*/
public CommandFilter(@Nullable Collection<String> permitted, @Nullable Collection<String> denied) {
this.permitted = permitted;
this.denied = denied;
}
@SuppressWarnings("StatementWithEmptyBody")
@Override
public boolean apply(String command) {
command = command.toLowerCase();
/*
* denied used allow?
* x x no
* x x y no
* x y x yes
* x y x y no
*
* permitted used allow?
* x x yes
* x x y yes
* x y x no
* x y x y yes
*/
String result = "";
String[] usedParts = command.split("\\s+");
if (denied != null) {
denied:
for (String deniedCommand : denied) {
String[] deniedParts = deniedCommand.split("\\s+");
for (int i = 0; i < deniedParts.length && i < usedParts.length; i++) {
if (deniedParts[i].equalsIgnoreCase(usedParts[i])) {
// first part matches - check if it's the whole thing
if (i + 1 == deniedParts.length) {
// consumed all denied parts, block entire command
result = deniedCommand;
break denied;
} else {
// more denied parts to check, also check used length
if (i + 1 == usedParts.length) {
// all that was used, but there is more in denied
// allow this, check next command in flag
continue denied;
} else {
// more in both denied and used, continue checking
}
}
} else {
// found non-matching part, stop checking this command
continue denied;
}
}
}
}
if (permitted != null) {
permitted:
for (String permittedCommand : permitted) {
String[] permittedParts = permittedCommand.split("\\s+");
for (int i = 0; i < permittedParts.length && i < usedParts.length; i++) {
if (permittedParts[i].equalsIgnoreCase(usedParts[i])) {
// this part matches - check if it's the whole thing
if (i + 1 == permittedParts.length) {
// consumed all permitted parts before reaching used length
// this command is definitely permitted
result = "";
break permitted;
} else {
// more permitted parts to check
if (i + 1 == usedParts.length) {
// all that was used, but there is more in permitted
// block for now, check next part of flag
result = command;
continue permitted;
} else {
// more in both permitted and used, continue checking for match
}
}
} else {
// doesn't match at all, block it, check next flag string
result = command;
continue permitted;
}
}
}
}
return result.isEmpty();
}
/**
* Builder class for {@code CommandFilter}.
*
* <p>If {@link #permit(String...)} is never called, then the
* permitted rule list will be {@code null}. Likewise if
* {@link #deny(String...)} is never called.</p>
*/
public static class Builder {
private Set<String> permitted;
private Set<String> denied;
/**
* Create a new instance.
*/
public Builder() {
}
/**
* Permit the given list of commands.
*
* @param rules list of commands
* @return the builder object
*/
public Builder permit(String ... rules) {
checkNotNull(rules);
if (permitted == null) {
permitted = new HashSet<String>();
}
permitted.addAll(Arrays.asList(rules));
return this;
}
/**
* Deny the given list of commands.
*
* @param rules list of commands
* @return the builder object
*/
public Builder deny(String ... rules) {
checkNotNull(rules);
if (denied == null) {
denied = new HashSet<String>();
}
denied.addAll(Arrays.asList(rules));
return this;
}
/**
* Create a command filter.
*
* @return a new command filter
*/
public CommandFilter build() {
return new CommandFilter(
permitted != null ? new HashSet<String>(permitted) : null,
denied != null ? new HashSet<String>(denied) : null);
}
}
}

View File

@ -0,0 +1,196 @@
/*
* WorldGuard, a suite of tools for Minecraft
* Copyright (C) sk89q <http://www.sk89q.com>
* 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 <http://www.gnu.org/licenses/>.
*/
package com.sk89q.worldguard.util;
import com.sk89q.worldguard.util.command.CommandFilter;
import com.sk89q.worldguard.util.command.CommandFilter.Builder;
import junit.framework.TestCase;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
public class CommandFilterTest extends TestCase {
private static final String[] COMMAND_SEPARATORS = new String[] {" ", " ", "\t", " \t", "\n", "\r\n"};
public void testApply() throws Exception {
CommandFilter filter;
// ====================================================================
// No rules
// ====================================================================
filter = new Builder().build();
assertSubcommands(filter, "/permit1", true);
assertSubcommands(filter, "/deny1", true);
assertSubcommands(filter, "/other", true);
// ====================================================================
// Root PERMIT
// ====================================================================
filter = new Builder()
.permit("/permit1", "/permit2")
.build();
assertSubcommands(filter, "/permit1", true);
assertSubcommands(filter, "/permit2", true);
assertSubcommands(filter, "/other", false);
// ====================================================================
// Root DENY
// ====================================================================
filter = new Builder()
.deny("/deny1", "/deny2")
.build();
assertSubcommands(filter, "/deny1", false);
assertSubcommands(filter, "/deny2", false);
assertSubcommands(filter, "/other", true);
// ====================================================================
// Root PERMIT + DENY no overlap
// ====================================================================
filter = new Builder()
.permit("/permit1", "/permit2")
.deny("/deny1", "/deny2")
.build();
assertSubcommands(filter, "/permit1", true);
assertSubcommands(filter, "/permit1", true);
assertSubcommands(filter, "/deny1", false);
assertSubcommands(filter, "/deny2", false);
assertSubcommands(filter, "/other", false);
// ====================================================================
// Root PERMIT + DENY WITH overlap
// ====================================================================
filter = new Builder()
.permit("/permit1", "/permit2", "/strange")
.deny("/deny1", "/deny2", "/strange")
.build();
assertSubcommands(filter, "/permit1", true);
assertSubcommands(filter, "/permit1", true);
assertSubcommands(filter, "/deny1", false);
assertSubcommands(filter, "/deny2", false);
assertSubcommands(filter, "/strange", true);
assertSubcommands(filter, "/other", false);
// ====================================================================
// Subcommand PERMIT
// ====================================================================
filter = new Builder()
.permit("/permit1", "/parent permit1", "/parent between subpermit1")
.build();
assertSubcommands(filter, "/permit1", true);
assertSubcommands(filter, "/parent", false);
assertSubcommands(filter, "/parent permit1", true);
assertSubcommands(filter, "/parent other", false);
assertSubcommands(filter, "/parent between", false);
assertSubcommands(filter, "/parent between subpermit1", true);
assertSubcommands(filter, "/parent between other", false);
assertSubcommands(filter, "/parent between other subpermit1", false);
assertSubcommands(filter, "/other", false);
assertSubcommands(filter, "/other permit1", false);
assertSubcommands(filter, "/other between", false);
assertSubcommands(filter, "/other between subpermit1", false);
// ====================================================================
// Mixed DENY
// ====================================================================
filter = new Builder()
.deny("/deny1", "/parent deny1", "/parent between subdeny1")
.build();
assertSubcommands(filter, "/deny1", false);
assertSubcommands(filter, "/parent", true);
assertSubcommands(filter, "/parent deny1", false);
assertSubcommands(filter, "/parent between", true);
assertSubcommands(filter, "/parent between subdeny1", false);
assertSubcommands(filter, "/parent between else", true);
assertSubcommands(filter, "/parent else", true);
assertSubcommands(filter, "/other", true);
assertSubcommands(filter, "/other deny1", true);
assertSubcommands(filter, "/other between", true);
assertSubcommands(filter, "/other between subdeny1", true);
assertSubcommands(filter, "/other between else", true);
// ====================================================================
// Mixed PERMIT + DENY no overlap
// ====================================================================
filter = new Builder()
.deny("/deny1", "/denyparent deny1", "/denyparent between subdeny1")
.permit("/permit1", "/permitparent permit1", "/permitparent between subpermit1")
.build();
assertSubcommands(filter, "/deny1", false);
assertSubcommands(filter, "/denyparent", false);
assertSubcommands(filter, "/denyparent deny1", false);
assertSubcommands(filter, "/denyparent else", false);
assertSubcommands(filter, "/denyparent between", false);
assertSubcommands(filter, "/denyparent between subdeny1", false);
assertSubcommands(filter, "/permit1", true);
assertSubcommands(filter, "/permitparent", false);
assertSubcommands(filter, "/permitparent permit1", true);
assertSubcommands(filter, "/permitparent else", false);
assertSubcommands(filter, "/permitparent between", false);
assertSubcommands(filter, "/permitparent between subpermit1", true);
assertSubcommands(filter, "/other", false);
assertSubcommands(filter, "/other permit1", false);
assertSubcommands(filter, "/other between", false);
assertSubcommands(filter, "/other between subpermit1", false);
// ====================================================================
// Mixed PERMIT + DENY overlap
// ====================================================================
filter = new Builder()
.deny("/deny1", "/parent deny1", "/parent between subdeny1", "/parent between", "/parent between strange", "/parent between strange sub")
.permit("/permit1", "/parent permit1", "/parent between sub", "/parent between strange")
.build();
assertSubcommands(filter, "/deny1", false);
assertSubcommands(filter, "/parent", false);
assertSubcommands(filter, "/parent deny1", false);
assertSubcommands(filter, "/parent else", false);
assertSubcommands(filter, "/parent permit1", true);
assertSubcommands(filter, "/parent between", false);
assertSubcommands(filter, "/parent between sub", true);
assertSubcommands(filter, "/parent between other", false);
assertSubcommands(filter, "/parent between strange", true);
assertSubcommands(filter, "/parent between strange sub", true);
assertSubcommands(filter, "/parent between strange other", true);
assertSubcommands(filter, "/permit1", true);
assertSubcommands(filter, "/permit1 deny1", true);
assertSubcommands(filter, "/other", false);
assertSubcommands(filter, "/other permit1", false);
assertSubcommands(filter, "/other between", false);
assertSubcommands(filter, "/other between subpermit1", false);
}
private void assertSubcommands(CommandFilter filter, final String root, boolean expected) {
for (String separator : COMMAND_SEPARATORS) {
assertThat(filter.apply(root.replaceAll(" ", separator)), is(expected));
assertThat(filter.apply((root + " _subcmd").replaceAll(" ", separator)), is(expected));
assertThat(filter.apply((root + " _subcmd _another").replaceAll(" ", separator)), is(expected));
}
}
}