mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
Merge pull request #10457 from creative-commoners/pulls/5/rescue-master-extensions-expose-public
API Rescue Master Branch PR: Only expose public extension methods
This commit is contained in:
commit
250a75b233
@ -4,18 +4,20 @@ namespace SilverStripe\Core;
|
|||||||
|
|
||||||
use BadMethodCallException;
|
use BadMethodCallException;
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
use SilverStripe\Dev\Deprecation;
|
use ReflectionClass;
|
||||||
|
use ReflectionMethod;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allows an object to declare a set of custom methods
|
* Allows an object to declare a set of custom methods
|
||||||
*/
|
*/
|
||||||
trait CustomMethods
|
trait CustomMethods
|
||||||
{
|
{
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom method sources
|
* Custom method sources
|
||||||
*
|
*
|
||||||
* @var array
|
* @var array Array of class names (lowercase) to list of methods.
|
||||||
|
* The list of methods will have lowercase keys. Each value in this array
|
||||||
|
* can be a callable, array, or string callback
|
||||||
*/
|
*/
|
||||||
protected static $extra_methods = [];
|
protected static $extra_methods = [];
|
||||||
|
|
||||||
@ -27,9 +29,10 @@ trait CustomMethods
|
|||||||
protected $extra_method_registers = [];
|
protected $extra_method_registers = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Non-custom methods
|
* Non-custom public methods.
|
||||||
*
|
*
|
||||||
* @var array
|
* @var array Array of class names (lowercase) to list of methods.
|
||||||
|
* The list of methods will have lowercase keys and correct-case values.
|
||||||
*/
|
*/
|
||||||
protected static $built_in_methods = [];
|
protected static $built_in_methods = [];
|
||||||
|
|
||||||
@ -74,19 +77,18 @@ trait CustomMethods
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call without setOwner
|
// Call on object
|
||||||
if (empty($config['callSetOwnerFirst'])) {
|
|
||||||
return $obj->$method(...$arguments);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @var Extension $obj */
|
|
||||||
try {
|
try {
|
||||||
|
if ($obj instanceof Extension) {
|
||||||
$obj->setOwner($this);
|
$obj->setOwner($this);
|
||||||
|
}
|
||||||
return $obj->$method(...$arguments);
|
return $obj->$method(...$arguments);
|
||||||
} finally {
|
} finally {
|
||||||
|
if ($obj instanceof Extension) {
|
||||||
$obj->clearOwner();
|
$obj->clearOwner();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
case isset($config['wrap']): {
|
case isset($config['wrap']): {
|
||||||
array_unshift($arguments, $config['method']);
|
array_unshift($arguments, $config['method']);
|
||||||
$wrapped = $config['wrap'];
|
$wrapped = $config['wrap'];
|
||||||
@ -160,66 +162,87 @@ trait CustomMethods
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
// Lazy define methods
|
// Lazy define methods
|
||||||
if (!isset(self::$extra_methods[static::class])) {
|
$lowerClass = strtolower(static::class);
|
||||||
|
if (!isset(self::$extra_methods[$lowerClass])) {
|
||||||
$this->defineMethods();
|
$this->defineMethods();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset(self::$extra_methods[static::class][strtolower($method)])) {
|
return self::$extra_methods[$lowerClass][strtolower($method)] ?? null;
|
||||||
return self::$extra_methods[static::class][strtolower($method)];
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the names of all the methods available on this object
|
* Return the names of all the methods available on this object
|
||||||
*
|
*
|
||||||
* @param bool $custom include methods added dynamically at runtime
|
* @param bool $custom include methods added dynamically at runtime
|
||||||
* @return array
|
* @return array Map of method names with lowercase keys
|
||||||
*/
|
*/
|
||||||
public function allMethodNames($custom = false)
|
public function allMethodNames($custom = false)
|
||||||
{
|
{
|
||||||
$class = static::class;
|
$methods = static::findBuiltInMethods();
|
||||||
if (!isset(self::$built_in_methods[$class])) {
|
|
||||||
self::$built_in_methods[$class] = array_map('strtolower', get_class_methods($this ?? ''));
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($custom && isset(self::$extra_methods[$class])) {
|
// Query extra methods
|
||||||
return array_merge(self::$built_in_methods[$class], array_keys(self::$extra_methods[$class] ?? []));
|
$lowerClass = strtolower(static::class);
|
||||||
} else {
|
if ($custom && isset(self::$extra_methods[$lowerClass])) {
|
||||||
return self::$built_in_methods[$class];
|
$methods = array_merge(self::$extra_methods[$lowerClass], $methods);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param object $extension
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
protected function findMethodsFromExtension($extension)
|
|
||||||
{
|
|
||||||
if (method_exists($extension, 'allMethodNames')) {
|
|
||||||
if ($extension instanceof Extension) {
|
|
||||||
try {
|
|
||||||
$extension->setOwner($this);
|
|
||||||
$methods = $extension->allMethodNames(true);
|
|
||||||
} finally {
|
|
||||||
$extension->clearOwner();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$methods = $extension->allMethodNames(true);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$class = get_class($extension);
|
|
||||||
if (!isset(self::$built_in_methods[$class])) {
|
|
||||||
self::$built_in_methods[$class] = array_map('strtolower', get_class_methods($extension ?? ''));
|
|
||||||
}
|
|
||||||
$methods = self::$built_in_methods[$class];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $methods;
|
return $methods;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add all the methods from an object property (which is an {@link Extension}) to this object.
|
* Get all public built in methods for this class
|
||||||
|
*
|
||||||
|
* @param string|object $class Class or instance to query methods from (defaults to static::class)
|
||||||
|
* @return array Map of methods with lowercase key name
|
||||||
|
*/
|
||||||
|
protected static function findBuiltInMethods($class = null)
|
||||||
|
{
|
||||||
|
$class = is_object($class) ? get_class($class) : ($class ?: static::class);
|
||||||
|
$lowerClass = strtolower($class);
|
||||||
|
if (isset(self::$built_in_methods[$lowerClass])) {
|
||||||
|
return self::$built_in_methods[$lowerClass];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build new list
|
||||||
|
$reflection = new ReflectionClass($class);
|
||||||
|
$methods = $reflection->getMethods(ReflectionMethod::IS_PUBLIC);
|
||||||
|
self::$built_in_methods[$lowerClass] = [];
|
||||||
|
foreach ($methods as $method) {
|
||||||
|
$name = $method->getName();
|
||||||
|
self::$built_in_methods[$lowerClass][strtolower($name)] = $name;
|
||||||
|
}
|
||||||
|
return self::$built_in_methods[$lowerClass];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find all methods on the given object.
|
||||||
|
*
|
||||||
|
* @param object $object
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
protected function findMethodsFrom($object)
|
||||||
|
{
|
||||||
|
// Respect "allMethodNames"
|
||||||
|
if (method_exists($object, 'allMethodNames')) {
|
||||||
|
if ($object instanceof Extension) {
|
||||||
|
try {
|
||||||
|
$object->setOwner($this);
|
||||||
|
$methods = $object->allMethodNames(true);
|
||||||
|
} finally {
|
||||||
|
$object->clearOwner();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$methods = $object->allMethodNames(true);
|
||||||
|
}
|
||||||
|
return $methods;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get methods
|
||||||
|
return static::findBuiltInMethods($object);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add all the methods from an object property.
|
||||||
*
|
*
|
||||||
* @param string $property the property name
|
* @param string $property the property name
|
||||||
* @param string|int $index an index to use if the property is an array
|
* @param string|int $index an index to use if the property is an array
|
||||||
@ -228,37 +251,31 @@ trait CustomMethods
|
|||||||
protected function addMethodsFrom($property, $index = null)
|
protected function addMethodsFrom($property, $index = null)
|
||||||
{
|
{
|
||||||
$class = static::class;
|
$class = static::class;
|
||||||
$extension = ($index !== null) ? $this->{$property}[$index] : $this->$property;
|
$object = ($index !== null) ? $this->{$property}[$index] : $this->$property;
|
||||||
|
|
||||||
if (!$extension) {
|
if (!$object) {
|
||||||
throw new InvalidArgumentException(
|
throw new InvalidArgumentException(
|
||||||
"Object->addMethodsFrom(): could not add methods from {$class}->{$property}[$index]"
|
"Object->addMethodsFrom(): could not add methods from {$class}->{$property}[$index]"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$methods = $this->findMethodsFromExtension($extension);
|
$methods = $this->findMethodsFrom($object);
|
||||||
if ($methods) {
|
if (!$methods) {
|
||||||
if ($extension instanceof Extension) {
|
return;
|
||||||
Deprecation::notice(
|
|
||||||
'5.0',
|
|
||||||
'Register custom methods from extensions with addCallbackMethod.'
|
|
||||||
. ' callSetOwnerFirst will be removed in 5.0'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
$methodInfo = [
|
$methodInfo = [
|
||||||
'property' => $property,
|
'property' => $property,
|
||||||
'index' => $index,
|
'index' => $index,
|
||||||
'callSetOwnerFirst' => $extension instanceof Extension,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
$newMethods = array_fill_keys($methods ?? [], $methodInfo);
|
$newMethods = array_fill_keys(array_keys($methods), $methodInfo);
|
||||||
|
|
||||||
if (isset(self::$extra_methods[$class])) {
|
// Merge with extra_methods
|
||||||
self::$extra_methods[$class] =
|
$lowerClass = strtolower($class);
|
||||||
array_merge(self::$extra_methods[$class], $newMethods);
|
if (isset(self::$extra_methods[$lowerClass])) {
|
||||||
|
self::$extra_methods[$lowerClass] = array_merge(self::$extra_methods[$lowerClass], $newMethods);
|
||||||
} else {
|
} else {
|
||||||
self::$extra_methods[$class] = $newMethods;
|
self::$extra_methods[$lowerClass] = $newMethods;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -279,26 +296,18 @@ trait CustomMethods
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$methods = $this->findMethodsFromExtension($extension);
|
$lowerClass = strtolower($class);
|
||||||
if ($methods) {
|
if (!isset(self::$extra_methods[$lowerClass])) {
|
||||||
foreach ($methods as $method) {
|
return;
|
||||||
if (!isset(self::$extra_methods[$class][$method])) {
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
$methods = $this->findMethodsFrom($extension);
|
||||||
|
|
||||||
$methodInfo = self::$extra_methods[$class][$method];
|
// Unset by key
|
||||||
|
self::$extra_methods[$lowerClass] = array_diff_key(self::$extra_methods[$lowerClass], $methods);
|
||||||
|
|
||||||
// always check for property, AND
|
// Clear empty list
|
||||||
// check for index only if provided
|
if (empty(self::$extra_methods[$lowerClass])) {
|
||||||
if ((isset($methodInfo['property']) && $methodInfo['property'] === $property) &&
|
unset(self::$extra_methods[$lowerClass]);
|
||||||
(!$index || ($index && isset($methodInfo['index']) && $methodInfo['index'] === $index))) {
|
|
||||||
unset(self::$extra_methods[$class][$method]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (empty(self::$extra_methods[$class])) {
|
|
||||||
unset(self::$extra_methods[$class]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -311,7 +320,7 @@ trait CustomMethods
|
|||||||
*/
|
*/
|
||||||
protected function addWrapperMethod($method, $wrap)
|
protected function addWrapperMethod($method, $wrap)
|
||||||
{
|
{
|
||||||
self::$extra_methods[static::class][strtolower($method)] = [
|
self::$extra_methods[strtolower(static::class)][strtolower($method)] = [
|
||||||
'wrap' => $wrap,
|
'wrap' => $wrap,
|
||||||
'method' => $method
|
'method' => $method
|
||||||
];
|
];
|
||||||
@ -326,7 +335,7 @@ trait CustomMethods
|
|||||||
*/
|
*/
|
||||||
protected function addCallbackMethod($method, $callback)
|
protected function addCallbackMethod($method, $callback)
|
||||||
{
|
{
|
||||||
self::$extra_methods[static::class][strtolower($method)] = [
|
self::$extra_methods[strtolower(static::class)][strtolower($method)] = [
|
||||||
'callback' => $callback,
|
'callback' => $callback,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -104,14 +104,6 @@ trait Extensible
|
|||||||
$this->afterExtendCallbacks[$method][] = $callback;
|
$this->afterExtendCallbacks[$method][] = $callback;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated 4.0.0:5.0.0 Extensions and methods are now lazy-loaded
|
|
||||||
*/
|
|
||||||
protected function constructExtensions()
|
|
||||||
{
|
|
||||||
Deprecation::notice('5.0', 'constructExtensions does not need to be invoked and will be removed in 5.0');
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function defineMethods()
|
protected function defineMethods()
|
||||||
{
|
{
|
||||||
$this->defineMethodsCustom();
|
$this->defineMethodsCustom();
|
||||||
@ -131,7 +123,7 @@ trait Extensible
|
|||||||
{
|
{
|
||||||
$extensions = $this->getExtensionInstances();
|
$extensions = $this->getExtensionInstances();
|
||||||
foreach ($extensions as $extensionClass => $extensionInstance) {
|
foreach ($extensions as $extensionClass => $extensionInstance) {
|
||||||
foreach ($this->findMethodsFromExtension($extensionInstance) as $method) {
|
foreach ($this->findMethodsFrom($extensionInstance) as $method) {
|
||||||
$this->addCallbackMethod($method, function ($inst, $args) use ($method, $extensionClass) {
|
$this->addCallbackMethod($method, function ($inst, $args) use ($method, $extensionClass) {
|
||||||
/** @var Extensible $inst */
|
/** @var Extensible $inst */
|
||||||
$extension = $inst->getExtensionInstance($extensionClass);
|
$extension = $inst->getExtensionInstance($extensionClass);
|
||||||
@ -199,11 +191,8 @@ trait Extensible
|
|||||||
// unset some caches
|
// unset some caches
|
||||||
$subclasses = ClassInfo::subclassesFor($class);
|
$subclasses = ClassInfo::subclassesFor($class);
|
||||||
$subclasses[] = $class;
|
$subclasses[] = $class;
|
||||||
|
|
||||||
if ($subclasses) {
|
|
||||||
foreach ($subclasses as $subclass) {
|
foreach ($subclasses as $subclass) {
|
||||||
unset(self::$extra_methods[$subclass]);
|
unset(self::$extra_methods[strtolower($subclass)]);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Config::modify()
|
Config::modify()
|
||||||
@ -261,10 +250,8 @@ trait Extensible
|
|||||||
// unset some caches
|
// unset some caches
|
||||||
$subclasses = ClassInfo::subclassesFor($class);
|
$subclasses = ClassInfo::subclassesFor($class);
|
||||||
$subclasses[] = $class;
|
$subclasses[] = $class;
|
||||||
if ($subclasses) {
|
|
||||||
foreach ($subclasses as $subclass) {
|
foreach ($subclasses as $subclass) {
|
||||||
unset(self::$extra_methods[$subclass]);
|
unset(self::$extra_methods[strtolower($subclass)]);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -403,25 +390,19 @@ trait Extensible
|
|||||||
* all results into an array
|
* all results into an array
|
||||||
*
|
*
|
||||||
* @param string $method the method name to call
|
* @param string $method the method name to call
|
||||||
* @param mixed $a1
|
* @param mixed ...$arguments List of arguments
|
||||||
* @param mixed $a2
|
|
||||||
* @param mixed $a3
|
|
||||||
* @param mixed $a4
|
|
||||||
* @param mixed $a5
|
|
||||||
* @param mixed $a6
|
|
||||||
* @param mixed $a7
|
|
||||||
* @return array List of results with nulls filtered out
|
* @return array List of results with nulls filtered out
|
||||||
*/
|
*/
|
||||||
public function invokeWithExtensions($method, &$a1 = null, &$a2 = null, &$a3 = null, &$a4 = null, &$a5 = null, &$a6 = null, &$a7 = null)
|
public function invokeWithExtensions($method, &...$arguments)
|
||||||
{
|
{
|
||||||
$result = [];
|
$result = [];
|
||||||
if (method_exists($this, $method ?? '')) {
|
if (method_exists($this, $method ?? '')) {
|
||||||
$thisResult = $this->$method($a1, $a2, $a3, $a4, $a5, $a6, $a7);
|
$thisResult = $this->$method(...$arguments);
|
||||||
if ($thisResult !== null) {
|
if ($thisResult !== null) {
|
||||||
$result[] = $thisResult;
|
$result[] = $thisResult;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$extras = $this->extend($method, $a1, $a2, $a3, $a4, $a5, $a6, $a7);
|
$extras = $this->extend($method, ...$arguments);
|
||||||
|
|
||||||
return $extras ? array_merge($result, $extras) : $result;
|
return $extras ? array_merge($result, $extras) : $result;
|
||||||
}
|
}
|
||||||
@ -438,22 +419,16 @@ trait Extensible
|
|||||||
* The extension methods are defined during {@link __construct()} in {@link defineMethods()}.
|
* The extension methods are defined during {@link __construct()} in {@link defineMethods()}.
|
||||||
*
|
*
|
||||||
* @param string $method the name of the method to call on each extension
|
* @param string $method the name of the method to call on each extension
|
||||||
* @param mixed $a1
|
* @param mixed &...$arguments
|
||||||
* @param mixed $a2
|
|
||||||
* @param mixed $a3
|
|
||||||
* @param mixed $a4
|
|
||||||
* @param mixed $a5
|
|
||||||
* @param mixed $a6
|
|
||||||
* @param mixed $a7
|
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
public function extend($method, &$a1 = null, &$a2 = null, &$a3 = null, &$a4 = null, &$a5 = null, &$a6 = null, &$a7 = null)
|
public function extend($method, &...$arguments)
|
||||||
{
|
{
|
||||||
$values = [];
|
$values = [];
|
||||||
|
|
||||||
if (!empty($this->beforeExtendCallbacks[$method])) {
|
if (!empty($this->beforeExtendCallbacks[$method])) {
|
||||||
foreach (array_reverse($this->beforeExtendCallbacks[$method] ?? []) as $callback) {
|
foreach (array_reverse($this->beforeExtendCallbacks[$method ?? '']) as $callback) {
|
||||||
$value = call_user_func_array($callback, [&$a1, &$a2, &$a3, &$a4, &$a5, &$a6, &$a7]);
|
$value = call_user_func_array($callback, $arguments);
|
||||||
if ($value !== null) {
|
if ($value !== null) {
|
||||||
$values[] = $value;
|
$values[] = $value;
|
||||||
}
|
}
|
||||||
@ -462,22 +437,16 @@ trait Extensible
|
|||||||
}
|
}
|
||||||
|
|
||||||
foreach ($this->getExtensionInstances() as $instance) {
|
foreach ($this->getExtensionInstances() as $instance) {
|
||||||
if (method_exists($instance, $method ?? '')) {
|
// Prefer `extend` prefixed methods
|
||||||
try {
|
$value = $instance->invokeExtension($this, $method, ...$arguments);
|
||||||
$instance->setOwner($this);
|
|
||||||
$value = $instance->$method($a1, $a2, $a3, $a4, $a5, $a6, $a7);
|
|
||||||
} finally {
|
|
||||||
$instance->clearOwner();
|
|
||||||
}
|
|
||||||
if ($value !== null) {
|
if ($value !== null) {
|
||||||
$values[] = $value;
|
$values[] = $value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (!empty($this->afterExtendCallbacks[$method])) {
|
if (!empty($this->afterExtendCallbacks[$method])) {
|
||||||
foreach (array_reverse($this->afterExtendCallbacks[$method] ?? []) as $callback) {
|
foreach (array_reverse($this->afterExtendCallbacks[$method ?? '']) as $callback) {
|
||||||
$value = call_user_func_array($callback, [&$a1, &$a2, &$a3, &$a4, &$a5, &$a6, &$a7]);
|
$value = call_user_func_array($callback, $arguments);
|
||||||
if ($value !== null) {
|
if ($value !== null) {
|
||||||
$values[] = $value;
|
$values[] = $value;
|
||||||
}
|
}
|
||||||
|
@ -115,4 +115,31 @@ abstract class Extension
|
|||||||
// Split out both args and service name
|
// Split out both args and service name
|
||||||
return strtok(strtok($extensionStr ?? '', '(') ?? '', '.');
|
return strtok(strtok($extensionStr ?? '', '(') ?? '', '.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoke extension point. This will prefer explicit `extend` prefixed
|
||||||
|
* methods.
|
||||||
|
*
|
||||||
|
* @param object $owner
|
||||||
|
* @param string $method
|
||||||
|
* @param array &...$arguments
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function invokeExtension($owner, $method, &...$arguments)
|
||||||
|
{
|
||||||
|
// Prefer `extend` prefixed methods
|
||||||
|
$instanceMethod = method_exists($this, "extend{$method}")
|
||||||
|
? "extend{$method}"
|
||||||
|
: (method_exists($this, $method) ? $method : null);
|
||||||
|
if (!$instanceMethod) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->setOwner($owner);
|
||||||
|
return $this->$instanceMethod(...$arguments);
|
||||||
|
} finally {
|
||||||
|
$this->clearOwner();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user