Inject a fake map into the super PermissionAttachment instance to fix compat with plugins using reflection to change attachment data (#528)

This commit is contained in:
Luck 2017-11-07 17:05:16 +00:00
parent f1a50f433f
commit a2801bff7c
No known key found for this signature in database
GPG Key ID: EFA9B3EC5FD90F8B

View File

@ -28,17 +28,25 @@ package me.lucko.luckperms.bukkit.model;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import com.google.common.base.Preconditions;
import me.lucko.luckperms.api.Node;
import me.lucko.luckperms.common.config.ConfigKeys; import me.lucko.luckperms.common.config.ConfigKeys;
import me.lucko.luckperms.common.model.User;
import me.lucko.luckperms.common.node.ImmutableTransientNode; import me.lucko.luckperms.common.node.ImmutableTransientNode;
import me.lucko.luckperms.common.node.NodeFactory; import me.lucko.luckperms.common.node.NodeFactory;
import org.bukkit.permissions.PermissibleBase;
import org.bukkit.permissions.PermissionAttachment; import org.bukkit.permissions.PermissionAttachment;
import org.bukkit.permissions.PermissionRemovedExecutor; import org.bukkit.permissions.PermissionRemovedExecutor;
import org.bukkit.plugin.Plugin; import org.bukkit.plugin.Plugin;
import java.lang.reflect.Field;
import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Set;
/** /**
* PermissionAttachment for LuckPerms. * PermissionAttachment for LuckPerms.
@ -47,6 +55,23 @@ import java.util.Map;
*/ */
public class LPPermissionAttachment extends PermissionAttachment { public class LPPermissionAttachment extends PermissionAttachment {
/**
* The field in PermissionAttachment where the attachments applied permissions
* are *usually* held.
*/
private static final Field PERMISSION_ATTACHMENT_PERMISSIONS_FIELD;
static {
Field permissionAttachmentPermissionsField;
try {
permissionAttachmentPermissionsField = PermissibleBase.class.getDeclaredField("permissions");
permissionAttachmentPermissionsField.setAccessible(true);
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
PERMISSION_ATTACHMENT_PERMISSIONS_FIELD = permissionAttachmentPermissionsField;
}
/** /**
* The parent LPPermissible * The parent LPPermissible
*/ */
@ -79,6 +104,8 @@ public class LPPermissionAttachment extends PermissionAttachment {
super(DummyPlugin.INSTANCE, null); super(DummyPlugin.INSTANCE, null);
this.permissible = permissible; this.permissible = permissible;
this.owner = owner; this.owner = owner;
injectFakeMap();
} }
public LPPermissionAttachment(LPPermissible permissible, PermissionAttachment bukkit) { public LPPermissionAttachment(LPPermissible permissible, PermissionAttachment bukkit) {
@ -88,12 +115,41 @@ public class LPPermissionAttachment extends PermissionAttachment {
// copy // copy
perms.putAll(bukkit.getPermissions()); perms.putAll(bukkit.getPermissions());
injectFakeMap();
} }
/**
* Injects a fake 'permissions' map into the superclass, for (clever/dumb??) plugins
* which attempt to modify attachment permissions using reflection to get around the slow bukkit
* behaviour in the base PermissionAttachment implementation.
*
* The fake map proxies calls back to the methods on this attachment
*/
private void injectFakeMap() {
// inner class - this proxies calls back to us
FakeBackingMap fakeMap = new FakeBackingMap();
try {
// what's this doing, ay?
// the field we need to modify is in the superclass - it's set to private
// so we have to use reflection to modify it.
PERMISSION_ATTACHMENT_PERMISSIONS_FIELD.set(this, fakeMap);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* Hooks this attachment with the parent {@link User} instance.
*/
public void hook() { public void hook() {
hooked = true; hooked = true;
permissible.attachments.add(this); permissible.attachments.add(this);
for (Map.Entry<String, Boolean> entry : perms.entrySet()) { for (Map.Entry<String, Boolean> entry : perms.entrySet()) {
if (entry.getKey() == null || entry.getKey().isEmpty()) {
continue;
}
setPermissionInternal(entry.getKey(), entry.getValue()); setPermissionInternal(entry.getKey(), entry.getValue());
} }
} }
@ -103,9 +159,20 @@ public class LPPermissionAttachment extends PermissionAttachment {
return; return;
} }
ImmutableTransientNode node = ImmutableTransientNode.of(NodeFactory.make(name, value), this); // construct a node for the permission being set
if (permissible.getUser().setTransientPermission(node).asBoolean()) { // we use the servers static context to *try* to ensure that the node will apply
permissible.getUser().getRefreshBuffer().request(); Node node = NodeFactory.newBuilder(name)
.setValue(value)
.withExtraContext(permissible.getPlugin().getContextManager().getStaticContext())
.build();
// convert the constructed node to a transient node instance to refer back to this attachment
ImmutableTransientNode transientNode = ImmutableTransientNode.of(node, this);
// set the transient node
User user = permissible.getUser();
if (user.setTransientPermission(transientNode).asBoolean()) {
user.getRefreshBuffer().request();
} }
} }
@ -114,8 +181,18 @@ public class LPPermissionAttachment extends PermissionAttachment {
return; return;
} }
if (permissible.getUser().removeIfTransient(n -> n instanceof ImmutableTransientNode && ((ImmutableTransientNode) n).getOwner() == this && n.getPermission().equals(name))) { // remove transient permissions from the holder which were added by this attachment & equal the permission
permissible.getUser().getRefreshBuffer().request(); User user = permissible.getUser();
if (user.removeIfTransient(n -> n instanceof ImmutableTransientNode && ((ImmutableTransientNode) n).getOwner() == this && n.getPermission().equals(name))) {
user.getRefreshBuffer().request();
}
}
private void clearInternal() {
// remove all transient permissions added by this attachment
User user = permissible.getUser();
if (user.removeIfTransient(n -> n instanceof ImmutableTransientNode && ((ImmutableTransientNode) n).getOwner() == this)) {
user.getRefreshBuffer().request();
} }
} }
@ -125,14 +202,15 @@ public class LPPermissionAttachment extends PermissionAttachment {
return false; return false;
} }
if (permissible.getUser().removeIfTransient(n -> n instanceof ImmutableTransientNode && ((ImmutableTransientNode) n).getOwner() == this)) { // clear the internal permissions
permissible.getUser().getRefreshBuffer().request(); clearInternal();
}
// run the callback
if (removalCallback != null) { if (removalCallback != null) {
removalCallback.attachmentRemoved(this); removalCallback.attachmentRemoved(this);
} }
// unhook from the permissible
hooked = false; hooked = false;
permissible.attachments.remove(this); permissible.attachments.remove(this);
return true; return true;
@ -140,34 +218,48 @@ public class LPPermissionAttachment extends PermissionAttachment {
@Override @Override
public void setPermission(String name, boolean value) { public void setPermission(String name, boolean value) {
Boolean previous = perms.put(name, value); Preconditions.checkNotNull(name, "name is null");
Preconditions.checkArgument(!name.isEmpty(), "name is empty");
String permission = name.toLowerCase();
Boolean previous = perms.put(permission, value);
if (previous != null && previous == value) { if (previous != null && previous == value) {
return; return;
} }
// if we're not hooked, thn don't actually apply the change
// it will get applied on hook - if that ever happens
if (!hooked) { if (!hooked) {
return; return;
} }
if (previous != null) { if (previous != null) {
unsetPermissionInternal(name); unsetPermissionInternal(permission);
} }
setPermissionInternal(name, value); setPermissionInternal(permission, value);
} }
@Override @Override
public void unsetPermission(String name) { public void unsetPermission(String name) {
Boolean previous = perms.remove(name); Preconditions.checkNotNull(name, "name is null");
Preconditions.checkArgument(!name.isEmpty(), "name is empty");
String permission = name.toLowerCase();
Boolean previous = perms.remove(permission);
if (previous == null) { if (previous == null) {
return; return;
} }
// if we're not hooked, thn don't actually apply the change
// it will get applied on hook - if that ever happens
if (!hooked) { if (!hooked) {
return; return;
} }
unsetPermissionInternal(name); unsetPermissionInternal(permission);
} }
@Override @Override
@ -189,4 +281,116 @@ public class LPPermissionAttachment extends PermissionAttachment {
public int hashCode() { public int hashCode() {
return System.identityHashCode(this); return System.identityHashCode(this);
} }
/**
* A fake map to be injected into the superclass. This implementation simply
* proxies calls back to this attachment instance.
*
* Some (clever/dumb??) plugins attempt to modify attachment permissions using reflection
* to get around the slow bukkit behaviour in the base PermissionAttachment implementation.
*
* An instance of this map is injected into the super instance so these plugins continue
* to work with LuckPerms.
*/
private final class FakeBackingMap implements Map<String, Boolean> {
@Override
public Boolean put(String key, Boolean value) {
// grab the previous result, so we can still satisfy the method signature of Map
Boolean previous = perms.get(key);
// proxy the call back through the PermissionAttachment instance
setPermission(key, value);
// return the previous value
return previous;
}
@Override
public Boolean remove(Object key) {
// we only accept string keys
if (!(key instanceof String)) {
return null;
}
String permission = ((String) key);
// grab the previous result, so we can still satisfy the method signature of Map
Boolean previous = perms.get(permission);
// proxy the call back through the PermissionAttachment instance
unsetPermission(permission);
// return the previous value
return previous;
}
@Override
public void putAll(Map<? extends String, ? extends Boolean> m) {
for (Map.Entry<? extends String, ? extends Boolean> entry : m.entrySet()) {
put(entry.getKey(), entry.getValue());
}
}
@Override
public void clear() {
// remove the permissions which have already been applied
if (hooked) {
clearInternal();
}
// clear the backing map
perms.clear();
}
@Override
public int size() {
// return the size of the permissions map - probably the most accurate value we have
return perms.size();
}
@Override
public boolean isEmpty() {
// return if the permissions map is empty - again probably the most accurate thing
// we can return
return perms.isEmpty();
}
@Override
public boolean containsKey(Object key) {
// just proxy
return perms.containsKey(key);
}
@Override
public boolean containsValue(Object value) {
// just proxy
return perms.containsValue(value);
}
@Override
public Boolean get(Object key) {
// just proxy
return perms.get(key);
}
@Override
public Set<String> keySet() {
// just proxy
return perms.keySet();
}
@Override
public Collection<Boolean> values() {
// just proxy
return perms.values();
}
@Override
public Set<Entry<String, Boolean>> entrySet() {
// just proxy
return perms.entrySet();
}
}
} }