Experimental fix for issue #7 on Github.
Attempt to detect the correct "get" method in a IntHashMap.
Dieser Commit ist enthalten in:
Ursprung
3923e05178
Commit
be9bbc924e
@ -21,7 +21,6 @@ import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
@ -35,8 +34,8 @@ import org.bukkit.entity.Player;
|
||||
import com.comphenix.protocol.reflect.FieldAccessException;
|
||||
import com.comphenix.protocol.reflect.FieldUtils;
|
||||
import com.comphenix.protocol.reflect.FuzzyReflection;
|
||||
import com.comphenix.protocol.reflect.fuzzy.FuzzyMethodContract;
|
||||
import com.comphenix.protocol.utility.MinecraftReflection;
|
||||
import com.comphenix.protocol.wrappers.WrappedIntHashMap;
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
/**
|
||||
@ -51,7 +50,6 @@ class EntityUtilities {
|
||||
private static Field trackedPlayersField;
|
||||
private static Field trackerField;
|
||||
|
||||
private static Method hashGetMethod;
|
||||
private static Method scanPlayersMethod;
|
||||
|
||||
/*
|
||||
@ -180,7 +178,7 @@ class EntityUtilities {
|
||||
|
||||
if (trackedEntitiesField == null) {
|
||||
@SuppressWarnings("rawtypes")
|
||||
Set<Class> ignoredTypes = new HashSet<Class>();
|
||||
Set<Class> ignoredTypes = new HashSet<Class>();
|
||||
|
||||
// Well, this is more difficult. But we're looking for a Minecraft object that is not
|
||||
// created by the constructor(s).
|
||||
@ -197,42 +195,14 @@ class EntityUtilities {
|
||||
|
||||
// Read the entity hashmap
|
||||
Object trackedEntities = null;
|
||||
|
||||
|
||||
try {
|
||||
trackedEntities = FieldUtils.readField(trackedEntitiesField, tracker, true);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new FieldAccessException("Cannot access 'trackedEntities' field due to security limitations.", e);
|
||||
}
|
||||
|
||||
// Getting the "get" method is pretty hard, but first - try to just get it by name
|
||||
if (hashGetMethod == null) {
|
||||
|
||||
Class<?> type = trackedEntities.getClass();
|
||||
|
||||
try {
|
||||
hashGetMethod = type.getMethod("get", int.class);
|
||||
} catch (NoSuchMethodException e) {
|
||||
// Then it's probably the lowest named method that takes an int-parameter and returns a object
|
||||
hashGetMethod = FuzzyReflection.fromClass(type).getMethod(
|
||||
FuzzyMethodContract.newBuilder().banModifier(Modifier.STATIC).
|
||||
parameterCount(1).
|
||||
parameterExactType(int.class).
|
||||
returnTypeExact(Object.class).
|
||||
build()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Wrap exceptions
|
||||
try {
|
||||
return hashGetMethod.invoke(trackedEntities, entityID);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw e;
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new FieldAccessException("Security limitation prevents access to 'get' method in IntHashMap", e);
|
||||
} catch (InvocationTargetException e) {
|
||||
throw new RuntimeException("Exception occurred in Minecraft.", e);
|
||||
}
|
||||
return WrappedIntHashMap.fromHandle(trackedEntities).get(entityID);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -20,7 +20,6 @@ package com.comphenix.protocol.injector.packet;
|
||||
import java.io.DataInput;
|
||||
import java.io.DataInputStream;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
@ -48,6 +47,7 @@ import com.comphenix.protocol.reflect.FuzzyReflection;
|
||||
import com.comphenix.protocol.reflect.MethodInfo;
|
||||
import com.comphenix.protocol.reflect.fuzzy.FuzzyMethodContract;
|
||||
import com.comphenix.protocol.utility.MinecraftReflection;
|
||||
import com.comphenix.protocol.wrappers.WrappedIntHashMap;
|
||||
|
||||
/**
|
||||
* This class is responsible for adding or removing proxy objects that intercepts recieved packets.
|
||||
@ -67,9 +67,7 @@ class ProxyPacketInjector implements PacketInjector {
|
||||
}
|
||||
|
||||
private static class IntHashMapLookup implements PacketClassLookup {
|
||||
// The "put" method that associates a packet ID with a packet class
|
||||
private Method putMethod;
|
||||
private Object intHashMap;
|
||||
private WrappedIntHashMap intHashMap;
|
||||
|
||||
public IntHashMapLookup() throws IllegalAccessException {
|
||||
initialize();
|
||||
@ -77,32 +75,21 @@ class ProxyPacketInjector implements PacketInjector {
|
||||
|
||||
@Override
|
||||
public void setLookup(int packetID, Class<?> clazz) {
|
||||
try {
|
||||
putMethod.invoke(intHashMap, packetID, clazz);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new RuntimeException("Illegal argument.", e);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException("Cannot access method.", e);
|
||||
} catch (InvocationTargetException e) {
|
||||
throw new RuntimeException("Exception occured in IntHashMap.put.", e);
|
||||
}
|
||||
intHashMap.put(packetID, clazz);
|
||||
}
|
||||
|
||||
private void initialize() throws IllegalAccessException {
|
||||
if (intHashMap == null) {
|
||||
// We're looking for the first static field with a Minecraft-object. This should be a IntHashMap.
|
||||
Field intHashMapField = FuzzyReflection.fromClass(MinecraftReflection.getPacketClass(), true).
|
||||
getFieldByType(MinecraftReflection.getMinecraftObjectRegex());
|
||||
getFieldByType("packetIdMap", MinecraftReflection.getIntHashMapClass());
|
||||
|
||||
try {
|
||||
intHashMap = FieldUtils.readField(intHashMapField, (Object) null, true);
|
||||
intHashMap = WrappedIntHashMap.fromHandle(
|
||||
FieldUtils.readField(intHashMapField, (Object) null, true));
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new RuntimeException("Minecraft is incompatible.", e);
|
||||
}
|
||||
|
||||
// Now, get the "put" method.
|
||||
putMethod = FuzzyReflection.fromObject(intHashMap).
|
||||
getMethodByParameters("put", int.class, Object.class);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package com.comphenix.protocol.reflect.fuzzy;
|
||||
|
||||
import java.lang.reflect.Member;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@ -46,6 +47,14 @@ public abstract class AbstractFuzzyMember<T extends Member> extends AbstractFuzz
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Require that every matching member is public.
|
||||
* @return This builder, for chaining.
|
||||
*/
|
||||
public Builder<T> requirePublic() {
|
||||
return requireModifier(Modifier.PUBLIC);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a given bit-field of modifers that will skip or ignore members.
|
||||
* @param modifier - bit-field of modifiers to skip or ignore.
|
||||
|
@ -34,6 +34,12 @@ public class FuzzyFieldContract extends AbstractFuzzyMember<Field> {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder requirePublic() {
|
||||
super.requirePublic();
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder nameRegex(String regex) {
|
||||
super.nameRegex(regex);
|
||||
|
@ -4,6 +4,9 @@ import java.lang.reflect.Member;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.collect.Sets;
|
||||
|
||||
/**
|
||||
@ -12,14 +15,56 @@ import com.google.common.collect.Sets;
|
||||
* @author Kristian
|
||||
*/
|
||||
public class FuzzyMatchers {
|
||||
// Constant matchers
|
||||
private static AbstractFuzzyMatcher<Class<?>> MATCH_ALL = new AbstractFuzzyMatcher<Class<?>>() {
|
||||
@Override
|
||||
public boolean isMatch(Class<?> value, Object parent) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int calculateRoundNumber() {
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
private FuzzyMatchers() {
|
||||
// Don't make this constructable
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a class matcher that matches an array with a given component matcher.
|
||||
* @param componentMatcher - the component matcher.
|
||||
* @return A new array matcher.
|
||||
*/
|
||||
public static AbstractFuzzyMatcher<Class<?>> matchArray(@Nonnull final AbstractFuzzyMatcher<Class<?>> componentMatcher) {
|
||||
Preconditions.checkNotNull(componentMatcher, "componentMatcher cannot be NULL.");
|
||||
return new AbstractFuzzyMatcher<Class<?>>() {
|
||||
@Override
|
||||
public boolean isMatch(Class<?> value, Object parent) {
|
||||
return value.isArray() && componentMatcher.isMatch(value.getComponentType(), parent);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int calculateRoundNumber() {
|
||||
// We're just above object
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a fuzzy matcher that will match any class.
|
||||
* @return A class matcher.
|
||||
*/
|
||||
public static AbstractFuzzyMatcher<Class<?>> matchAll() {
|
||||
return MATCH_ALL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a class matcher that matches types exactly.
|
||||
* @param matcher - the matching class.
|
||||
* @return A new class mathcher.
|
||||
* @return A new class matcher.
|
||||
*/
|
||||
public static AbstractFuzzyMatcher<Class<?>> matchExact(Class<?> matcher) {
|
||||
return new ClassExactMatcher(matcher, ClassExactMatcher.Options.MATCH_EXACT);
|
||||
@ -28,7 +73,7 @@ public class FuzzyMatchers {
|
||||
/**
|
||||
* Construct a class matcher that matches any of the given classes exactly.
|
||||
* @param classes - list of classes to match.
|
||||
* @return A new class mathcher.
|
||||
* @return A new class matcher.
|
||||
*/
|
||||
public static AbstractFuzzyMatcher<Class<?>> matchAnyOf(Class<?>... classes) {
|
||||
return matchAnyOf(Sets.newHashSet(classes));
|
||||
@ -37,7 +82,7 @@ public class FuzzyMatchers {
|
||||
/**
|
||||
* Construct a class matcher that matches any of the given classes exactly.
|
||||
* @param classes - set of classes to match.
|
||||
* @return A new class mathcher.
|
||||
* @return A new class matcher.
|
||||
*/
|
||||
public static AbstractFuzzyMatcher<Class<?>> matchAnyOf(Set<Class<?>> classes) {
|
||||
return new ClassSetMatcher(classes);
|
||||
@ -46,7 +91,7 @@ public class FuzzyMatchers {
|
||||
/**
|
||||
* Construct a class matcher that matches super types of the given class.
|
||||
* @param matcher - the matching type must be a super class of this type.
|
||||
* @return A new class mathcher.
|
||||
* @return A new class matcher.
|
||||
*/
|
||||
public static AbstractFuzzyMatcher<Class<?>> matchSuper(Class<?> matcher) {
|
||||
return new ClassExactMatcher(matcher, ClassExactMatcher.Options.MATCH_SUPER);
|
||||
@ -55,7 +100,7 @@ public class FuzzyMatchers {
|
||||
/**
|
||||
* Construct a class matcher that matches derived types of the given class.
|
||||
* @param matcher - the matching type must be a derived class of this type.
|
||||
* @return A new class mathcher.
|
||||
* @return A new class matcher.
|
||||
*/
|
||||
public static AbstractFuzzyMatcher<Class<?>> matchDerived(Class<?> matcher) {
|
||||
return new ClassExactMatcher(matcher, ClassExactMatcher.Options.MATCH_DERIVED);
|
||||
|
@ -98,6 +98,12 @@ public class FuzzyMethodContract extends AbstractFuzzyMember<MethodInfo> {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder requirePublic() {
|
||||
super.requirePublic();
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder banModifier(int modifier) {
|
||||
super.banModifier(modifier);
|
||||
|
@ -419,6 +419,15 @@ public class MinecraftReflection {
|
||||
return getDataWatcherClass().isAssignableFrom(obj.getClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the given object is an IntHashMap object.
|
||||
* @param obj - the given object.
|
||||
* @return TRUE if it is, FALSE otherwise.
|
||||
*/
|
||||
public static boolean isIntHashMap(Object obj) {
|
||||
return getIntHashMapClass().isAssignableFrom(obj.getClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the given object is a CraftItemStack instancey.
|
||||
* @param obj - the given object.
|
||||
@ -1010,6 +1019,45 @@ public class MinecraftReflection {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the IntHashMap class.
|
||||
* @return IntHashMap class.
|
||||
*/
|
||||
public static Class<?> getIntHashMapClass() {
|
||||
try {
|
||||
return getMinecraftClass("IntHashMap");
|
||||
} catch (RuntimeException e) {
|
||||
final Class<?> parent = getEntityTrackerClass();
|
||||
|
||||
// Expected structure of a IntHashMap
|
||||
final FuzzyClassContract intHashContract = FuzzyClassContract.newBuilder().
|
||||
// add(int key, Object value)
|
||||
method(FuzzyMethodContract.newBuilder().
|
||||
parameterCount(2).
|
||||
parameterExactType(int.class, 0).
|
||||
parameterExactType(Object.class, 1).requirePublic()
|
||||
).
|
||||
// Object get(int key)
|
||||
method(FuzzyMethodContract.newBuilder().
|
||||
parameterCount(1).
|
||||
parameterExactType(int.class).
|
||||
returnTypeExact(Object.class).requirePublic()
|
||||
).
|
||||
// Finally, there should be an array of some kind
|
||||
field(FuzzyFieldContract.newBuilder().
|
||||
typeMatches(FuzzyMatchers.matchArray(FuzzyMatchers.matchAll()))
|
||||
).
|
||||
build();
|
||||
|
||||
final AbstractFuzzyMatcher<Field> intHashField = FuzzyFieldContract.newBuilder().
|
||||
typeMatches(getMinecraftObjectMatcher().and(intHashContract)).
|
||||
build();
|
||||
|
||||
// Use the type of the first field that matches
|
||||
return setMinecraftClass("IntHashMap", FuzzyReflection.fromClass(parent).getField(intHashField).getType());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the attribute modifier class.
|
||||
* @return Attribute modifier class.
|
||||
@ -1295,6 +1343,4 @@ public class MinecraftReflection {
|
||||
public static String getNetLoginHandlerName() {
|
||||
return getNetLoginHandlerClass().getSimpleName();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,176 @@
|
||||
package com.comphenix.protocol.wrappers;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
import com.comphenix.protocol.reflect.FuzzyReflection;
|
||||
import com.comphenix.protocol.reflect.fuzzy.FuzzyMethodContract;
|
||||
import com.comphenix.protocol.utility.MinecraftReflection;
|
||||
import com.google.common.base.Preconditions;
|
||||
|
||||
/**
|
||||
* Represents a wrapper for the internal IntHashMap in Minecraft.
|
||||
* @author Kristian
|
||||
*/
|
||||
public class WrappedIntHashMap {
|
||||
private static final Class<?> INT_HASH_MAP = MinecraftReflection.getIntHashMapClass();
|
||||
|
||||
private static Method PUT_METHOD;
|
||||
private static Method GET_METHOD;
|
||||
|
||||
private Object handle;
|
||||
|
||||
/**
|
||||
* Construct an IntHashMap wrapper around an instance.
|
||||
* @param handle - the NMS instance.
|
||||
*/
|
||||
private WrappedIntHashMap(Object handle) {
|
||||
this.handle = handle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a new IntHashMap.
|
||||
* @return A new IntHashMap.
|
||||
*/
|
||||
public static WrappedIntHashMap newMap() {
|
||||
try {
|
||||
return new WrappedIntHashMap(INT_HASH_MAP.newInstance());
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Unable to construct IntHashMap.", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a wrapper around a given NMS IntHashMap.
|
||||
* @param handle - the NMS IntHashMap.
|
||||
* @return The created wrapped.
|
||||
* @throws IllegalArgumentException If the handle is not an IntHasMap.
|
||||
*/
|
||||
public static WrappedIntHashMap fromHandle(@Nonnull Object handle) {
|
||||
Preconditions.checkNotNull(handle, "handle cannot be NULL");
|
||||
Preconditions.checkState(MinecraftReflection.isIntHashMap(handle),
|
||||
"handle is a " + handle.getClass() + ", not an IntHashMap.");
|
||||
|
||||
return new WrappedIntHashMap(handle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Associates a specified key with the given value in the integer map.
|
||||
* <p>
|
||||
* If the key has already been associated with a value, then it will be replaced by the new value.
|
||||
* @param key - the key to insert.
|
||||
* @param value - the value to insert. Cannot be NULL.
|
||||
* @throws RuntimeException If the reflection machinery failed.
|
||||
*/
|
||||
public void put(int key, Object value) {
|
||||
Preconditions.checkNotNull(value, "value cannot be NULL.");
|
||||
|
||||
initializePutMethod();
|
||||
putInternal(key, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when a value must be inserted into the underlying map, regardless of preconditions.
|
||||
* @param key - the key.
|
||||
* @param value - the value to insert.
|
||||
*/
|
||||
private void putInternal(int key, Object value) {
|
||||
try {
|
||||
PUT_METHOD.invoke(handle, key, value);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new RuntimeException("Illegal argument.", e);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException("Cannot access method.", e);
|
||||
} catch (InvocationTargetException e) {
|
||||
throw new RuntimeException("Exception occured in " + PUT_METHOD + " for" + handle, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the value associated with a specific key, or NULL if not found.
|
||||
* @param key - the integer key.
|
||||
* @return The associated value, or NULL.
|
||||
*/
|
||||
public Object get(int key) {
|
||||
initializeGetMethod();
|
||||
|
||||
try {
|
||||
return GET_METHOD.invoke(handle, key);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new RuntimeException("Illegal argument.", e);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException("Cannot access method.", e);
|
||||
} catch (InvocationTargetException e) {
|
||||
throw new RuntimeException("Unable to invoke " + GET_METHOD + " on " + handle, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a mapping of a key to a value if it is present.
|
||||
* @param key - the key of the mapping to remove.
|
||||
* @return TRUE if a key-value pair was removed, FALSE otherwise.
|
||||
*/
|
||||
public boolean remove(int key) {
|
||||
Object prev = get(key);
|
||||
|
||||
if (prev != null) {
|
||||
putInternal(key, null);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void initializePutMethod() {
|
||||
if (PUT_METHOD == null) {
|
||||
// Fairly straight forward
|
||||
PUT_METHOD = FuzzyReflection.fromClass(INT_HASH_MAP).getMethod(
|
||||
FuzzyMethodContract.newBuilder().
|
||||
banModifier(Modifier.STATIC).
|
||||
parameterCount(2).
|
||||
parameterExactType(int.class).
|
||||
parameterExactType(Object.class).
|
||||
build());
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeGetMethod() {
|
||||
if (GET_METHOD == null) {
|
||||
WrappedIntHashMap temp = WrappedIntHashMap.newMap();
|
||||
String expected = "hello";
|
||||
|
||||
// Initialize a value
|
||||
temp.put(1, expected);
|
||||
|
||||
// Determine which method to trust
|
||||
for (Method method : FuzzyReflection.fromClass(INT_HASH_MAP).
|
||||
getMethodListByParameters(Object.class, new Class<?>[] { int.class })) {
|
||||
|
||||
// Skip static methods
|
||||
if (Modifier.isStatic(method.getModifiers()))
|
||||
continue;
|
||||
|
||||
try {
|
||||
// See if we found the method we are looking for
|
||||
if (expected.equals(method.invoke(temp.getHandle(), 1))) {
|
||||
GET_METHOD = method;
|
||||
return;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Suppress
|
||||
}
|
||||
}
|
||||
throw new IllegalStateException("Unable to find appropriate GET_METHOD for IntHashMap.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the underlying IntHashMap object.
|
||||
* @return The underlying object.
|
||||
*/
|
||||
public Object getHandle() {
|
||||
return handle;
|
||||
}
|
||||
}
|
@ -8,6 +8,8 @@ import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
import net.minecraft.server.v1_6_R2.IntHashMap;
|
||||
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.craftbukkit.v1_6_R2.inventory.CraftItemFactory;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
@ -28,6 +30,11 @@ public class StreamSerializerTest {
|
||||
BukkitInitialization.initializeItemMeta();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMinecraftReflection() {
|
||||
assertEquals(IntHashMap.class, MinecraftReflection.getIntHashMapClass());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSerializer() throws IOException {
|
||||
ItemStack before = new ItemStack(Material.GOLD_AXE);
|
||||
|
In neuem Issue referenzieren
Einen Benutzer sperren