Merge branch '3.4' into 4.2
* 3.4: Show more accurate message in profiler when missing stopwatch CS Fixes: Not double split with one array argument Remove redundant animation prefixes Remove redundant `box-sizing` prefixes Rework firewall access denied rule fixed CS Fix missing $extraDirs when open_basedir returns
This commit is contained in:
commit
b13a23fe45
@ -126,14 +126,12 @@ class UserPasswordEncoderCommandTest extends WebTestCase
|
|||||||
|
|
||||||
public function testEncodePasswordEmptySaltOutput()
|
public function testEncodePasswordEmptySaltOutput()
|
||||||
{
|
{
|
||||||
$this->passwordEncoderCommandTester->execute(
|
$this->passwordEncoderCommandTester->execute([
|
||||||
[
|
|
||||||
'command' => 'security:encode-password',
|
'command' => 'security:encode-password',
|
||||||
'password' => 'p@ssw0rd',
|
'password' => 'p@ssw0rd',
|
||||||
'user-class' => 'Symfony\Component\Security\Core\User\User',
|
'user-class' => 'Symfony\Component\Security\Core\User\User',
|
||||||
'--empty-salt' => true,
|
'--empty-salt' => true,
|
||||||
]
|
]);
|
||||||
);
|
|
||||||
|
|
||||||
$this->assertContains('Password encoding succeeded', $this->passwordEncoderCommandTester->getDisplay());
|
$this->assertContains('Password encoding succeeded', $this->passwordEncoderCommandTester->getDisplay());
|
||||||
$this->assertContains(' Encoded password p@ssw0rd', $this->passwordEncoderCommandTester->getDisplay());
|
$this->assertContains(' Encoded password p@ssw0rd', $this->passwordEncoderCommandTester->getDisplay());
|
||||||
|
@ -97,7 +97,11 @@
|
|||||||
|
|
||||||
<h2>Execution timeline</h2>
|
<h2>Execution timeline</h2>
|
||||||
|
|
||||||
{% if collector.events is empty %}
|
{% if not collector.isStopwatchInstalled() %}
|
||||||
|
<div class="empty">
|
||||||
|
<p>The Stopwatch component is not installed. If you want to see timing events, run: <code>composer require symfony/stopwatch</code>.</p>
|
||||||
|
</div>
|
||||||
|
{% elseif collector.events is empty %}
|
||||||
<div class="empty">
|
<div class="empty">
|
||||||
<p>No timing events have been recorded. Check that symfony/stopwatch is installed and debugging enabled in the kernel.</p>
|
<p>No timing events have been recorded. Check that symfony/stopwatch is installed and debugging enabled in the kernel.</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -5,8 +5,6 @@
|
|||||||
background-color: #222;
|
background-color: #222;
|
||||||
border-top-left-radius: 4px;
|
border-top-left-radius: 4px;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
-webkit-box-sizing: border-box;
|
|
||||||
-moz-box-sizing: border-box;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
display: none;
|
display: none;
|
||||||
height: 36px;
|
height: 36px;
|
||||||
@ -36,8 +34,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sf-toolbarreset * {
|
.sf-toolbarreset * {
|
||||||
-webkit-box-sizing: content-box;
|
|
||||||
-moz-box-sizing: content-box;
|
|
||||||
box-sizing: content-box;
|
box-sizing: content-box;
|
||||||
vertical-align: baseline;
|
vertical-align: baseline;
|
||||||
letter-spacing: normal;
|
letter-spacing: normal;
|
||||||
@ -371,21 +367,8 @@ div.sf-toolbar .sf-toolbar-block a:hover {
|
|||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
.sf-ajax-request-loading {
|
.sf-ajax-request-loading {
|
||||||
-webkit-animation: sf-blink .5s ease-in-out infinite;
|
|
||||||
-o-animation: sf-blink .5s ease-in-out infinite;
|
|
||||||
-moz-animation: sf-blink .5s ease-in-out infinite;
|
|
||||||
animation: sf-blink .5s ease-in-out infinite;
|
animation: sf-blink .5s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
@-webkit-keyframes sf-blink {
|
|
||||||
0% { background: #222; }
|
|
||||||
50% { background: #444; }
|
|
||||||
100% { background: #222; }
|
|
||||||
}
|
|
||||||
@-moz-keyframes sf-blink {
|
|
||||||
0% { background: #222; }
|
|
||||||
50% { background: #444; }
|
|
||||||
100% { background: #222; }
|
|
||||||
}
|
|
||||||
@keyframes sf-blink {
|
@keyframes sf-blink {
|
||||||
0% { background: #222; }
|
0% { background: #222; }
|
||||||
50% { background: #444; }
|
50% { background: #444; }
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
"require": {
|
"require": {
|
||||||
"php": "^7.1.3",
|
"php": "^7.1.3",
|
||||||
"symfony/config": "^4.2",
|
"symfony/config": "^4.2",
|
||||||
"symfony/http-kernel": "~4.2",
|
"symfony/http-kernel": "^4.2.6",
|
||||||
"symfony/routing": "~3.4|~4.0",
|
"symfony/routing": "~3.4|~4.0",
|
||||||
"symfony/twig-bundle": "~4.2",
|
"symfony/twig-bundle": "~4.2",
|
||||||
"symfony/var-dumper": "~3.4|~4.0",
|
"symfony/var-dumper": "~3.4|~4.0",
|
||||||
|
@ -205,8 +205,7 @@ class DateIntervalType extends AbstractType
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
$resolver->setDefaults(
|
$resolver->setDefaults([
|
||||||
[
|
|
||||||
'with_years' => true,
|
'with_years' => true,
|
||||||
'with_months' => true,
|
'with_months' => true,
|
||||||
'with_days' => true,
|
'with_days' => true,
|
||||||
@ -235,8 +234,7 @@ class DateIntervalType extends AbstractType
|
|||||||
'compound' => $compound,
|
'compound' => $compound,
|
||||||
'empty_data' => $emptyData,
|
'empty_data' => $emptyData,
|
||||||
'labels' => [],
|
'labels' => [],
|
||||||
]
|
]);
|
||||||
);
|
|
||||||
$resolver->setNormalizer('placeholder', $placeholderNormalizer);
|
$resolver->setNormalizer('placeholder', $placeholderNormalizer);
|
||||||
$resolver->setNormalizer('labels', $labelsNormalizer);
|
$resolver->setNormalizer('labels', $labelsNormalizer);
|
||||||
|
|
||||||
|
@ -144,12 +144,10 @@ class DefaultChoiceListFactoryTest extends TestCase
|
|||||||
|
|
||||||
public function testCreateFromChoicesGrouped()
|
public function testCreateFromChoicesGrouped()
|
||||||
{
|
{
|
||||||
$list = $this->factory->createListFromChoices(
|
$list = $this->factory->createListFromChoices([
|
||||||
[
|
|
||||||
'Group 1' => ['A' => $this->obj1, 'B' => $this->obj2],
|
'Group 1' => ['A' => $this->obj1, 'B' => $this->obj2],
|
||||||
'Group 2' => ['C' => $this->obj3, 'D' => $this->obj4],
|
'Group 2' => ['C' => $this->obj3, 'D' => $this->obj4],
|
||||||
]
|
]);
|
||||||
);
|
|
||||||
|
|
||||||
$this->assertObjectListWithGeneratedValues($list);
|
$this->assertObjectListWithGeneratedValues($list);
|
||||||
}
|
}
|
||||||
|
@ -47,6 +47,7 @@ class TimeDataCollector extends DataCollector implements LateDataCollectorInterf
|
|||||||
'token' => $response->headers->get('X-Debug-Token'),
|
'token' => $response->headers->get('X-Debug-Token'),
|
||||||
'start_time' => $startTime * 1000,
|
'start_time' => $startTime * 1000,
|
||||||
'events' => [],
|
'events' => [],
|
||||||
|
'stopwatch_installed' => \class_exists(Stopwatch::class, false),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,6 +140,14 @@ class TimeDataCollector extends DataCollector implements LateDataCollectorInterf
|
|||||||
return $this->data['start_time'];
|
return $this->data['start_time'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return bool whether or not the stopwatch component is installed
|
||||||
|
*/
|
||||||
|
public function isStopwatchInstalled()
|
||||||
|
{
|
||||||
|
return $this->data['stopwatch_installed'];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritdoc}
|
* {@inheritdoc}
|
||||||
*/
|
*/
|
||||||
|
@ -15,6 +15,7 @@ use PHPUnit\Framework\TestCase;
|
|||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\HttpKernel\DataCollector\TimeDataCollector;
|
use Symfony\Component\HttpKernel\DataCollector\TimeDataCollector;
|
||||||
|
use Symfony\Component\Stopwatch\Stopwatch;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @group time-sensitive
|
* @group time-sensitive
|
||||||
@ -51,5 +52,6 @@ class TimeDataCollectorTest extends TestCase
|
|||||||
|
|
||||||
$c->collect($request, new Response());
|
$c->collect($request, new Response());
|
||||||
$this->assertEquals(123456000, $c->getStartTime());
|
$this->assertEquals(123456000, $c->getStartTime());
|
||||||
|
$this->assertSame(\class_exists(Stopwatch::class, false), $c->isStopwatchInstalled());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -851,13 +851,11 @@ class OptionsResolverTest extends TestCase
|
|||||||
$this->resolver->setDefined('foo');
|
$this->resolver->setDefined('foo');
|
||||||
$this->resolver->setAllowedTypes('foo', 'int[][]');
|
$this->resolver->setAllowedTypes('foo', 'int[][]');
|
||||||
|
|
||||||
$this->resolver->resolve(
|
$this->resolver->resolve([
|
||||||
[
|
|
||||||
'foo' => [
|
'foo' => [
|
||||||
[1.2],
|
[1.2],
|
||||||
],
|
],
|
||||||
]
|
]);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1918,13 +1916,11 @@ class OptionsResolverTest extends TestCase
|
|||||||
1, 2,
|
1, 2,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
], $this->resolver->resolve(
|
], $this->resolver->resolve([
|
||||||
[
|
|
||||||
'foo' => [
|
'foo' => [
|
||||||
[1, 2],
|
[1, 2],
|
||||||
],
|
],
|
||||||
]
|
]));
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testNested2Arrays()
|
public function testNested2Arrays()
|
||||||
@ -1964,8 +1960,7 @@ class OptionsResolverTest extends TestCase
|
|||||||
$this->resolver->setDefined('foo');
|
$this->resolver->setDefined('foo');
|
||||||
$this->resolver->setAllowedTypes('foo', 'float[][][][]');
|
$this->resolver->setAllowedTypes('foo', 'float[][][][]');
|
||||||
|
|
||||||
$this->resolver->resolve(
|
$this->resolver->resolve([
|
||||||
[
|
|
||||||
'foo' => [
|
'foo' => [
|
||||||
[
|
[
|
||||||
[
|
[
|
||||||
@ -1973,8 +1968,7 @@ class OptionsResolverTest extends TestCase
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
]
|
]);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -51,7 +51,7 @@ class ExecutableFinder
|
|||||||
public function find($name, $default = null, array $extraDirs = [])
|
public function find($name, $default = null, array $extraDirs = [])
|
||||||
{
|
{
|
||||||
if (ini_get('open_basedir')) {
|
if (ini_get('open_basedir')) {
|
||||||
$searchPath = explode(PATH_SEPARATOR, ini_get('open_basedir'));
|
$searchPath = array_merge(explode(PATH_SEPARATOR, ini_get('open_basedir')), $extraDirs);
|
||||||
$dirs = [];
|
$dirs = [];
|
||||||
foreach ($searchPath as $path) {
|
foreach ($searchPath as $path) {
|
||||||
// Silencing against https://bugs.php.net/69240
|
// Silencing against https://bugs.php.net/69240
|
||||||
|
@ -131,8 +131,6 @@ class ExceptionListener
|
|||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
$event->setException($e);
|
$event->setException($e);
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (null !== $this->logger) {
|
if (null !== $this->logger) {
|
||||||
@ -150,7 +148,7 @@ class ExceptionListener
|
|||||||
$subRequest = $this->httpUtils->createRequest($event->getRequest(), $this->errorPage);
|
$subRequest = $this->httpUtils->createRequest($event->getRequest(), $this->errorPage);
|
||||||
$subRequest->attributes->set(Security::ACCESS_DENIED_ERROR, $exception);
|
$subRequest->attributes->set(Security::ACCESS_DENIED_ERROR, $exception);
|
||||||
|
|
||||||
$event->setResponse($event->getKernel()->handle($subRequest, HttpKernelInterface::SUB_REQUEST, true));
|
$event->setResponse($event->getKernel()->handle($subRequest, HttpKernelInterface::SUB_REQUEST));
|
||||||
$event->allowCustomResponseCode();
|
$event->allowCustomResponseCode();
|
||||||
}
|
}
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
|
@ -130,10 +130,8 @@ class ExceptionListenerTest extends TestCase
|
|||||||
{
|
{
|
||||||
$event = $this->createEvent($exception);
|
$event = $this->createEvent($exception);
|
||||||
|
|
||||||
$accessDeniedHandler = $this->getMockBuilder('Symfony\Component\Security\Http\Authorization\AccessDeniedHandlerInterface')->getMock();
|
$listener = $this->createExceptionListener(null, $this->createTrustResolver(true), null, null, null, $this->createCustomAccessDeniedHandler(new Response('error')));
|
||||||
$accessDeniedHandler->expects($this->once())->method('handle')->will($this->returnValue(new Response('error')));
|
|
||||||
|
|
||||||
$listener = $this->createExceptionListener(null, $this->createTrustResolver(true), null, null, null, $accessDeniedHandler);
|
|
||||||
$listener->onKernelException($event);
|
$listener->onKernelException($event);
|
||||||
|
|
||||||
$this->assertEquals('error', $event->getResponse()->getContent());
|
$this->assertEquals('error', $event->getResponse()->getContent());
|
||||||
@ -147,16 +145,51 @@ class ExceptionListenerTest extends TestCase
|
|||||||
{
|
{
|
||||||
$event = $this->createEvent($exception);
|
$event = $this->createEvent($exception);
|
||||||
|
|
||||||
$tokenStorage = $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface')->getMock();
|
$listener = $this->createExceptionListener($this->createTokenStorage(), $this->createTrustResolver(false), null, $this->createEntryPoint());
|
||||||
$tokenStorage->expects($this->once())->method('getToken')->will($this->returnValue($this->getMockBuilder('Symfony\Component\Security\Core\Authentication\Token\TokenInterface')->getMock()));
|
|
||||||
|
|
||||||
$listener = $this->createExceptionListener($tokenStorage, $this->createTrustResolver(false), null, $this->createEntryPoint());
|
|
||||||
$listener->onKernelException($event);
|
$listener->onKernelException($event);
|
||||||
|
|
||||||
$this->assertEquals('OK', $event->getResponse()->getContent());
|
$this->assertEquals('OK', $event->getResponse()->getContent());
|
||||||
$this->assertSame(null === $eventException ? $exception : $eventException, $event->getException()->getPrevious());
|
$this->assertSame(null === $eventException ? $exception : $eventException, $event->getException()->getPrevious());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider getAccessDeniedExceptionProvider
|
||||||
|
*/
|
||||||
|
public function testAccessDeniedExceptionNotFullFledgedAndWithAccessDeniedHandlerAndWithoutErrorPage(\Exception $exception, \Exception $eventException = null)
|
||||||
|
{
|
||||||
|
$event = $this->createEvent($exception);
|
||||||
|
|
||||||
|
$listener = $this->createExceptionListener($this->createTokenStorage(), $this->createTrustResolver(false), null, $this->createEntryPoint(), null, $this->createCustomAccessDeniedHandler(new Response('denied', 403)));
|
||||||
|
$listener->onKernelException($event);
|
||||||
|
|
||||||
|
$this->assertEquals('denied', $event->getResponse()->getContent());
|
||||||
|
$this->assertEquals(403, $event->getResponse()->getStatusCode());
|
||||||
|
$this->assertSame(null === $eventException ? $exception : $eventException, $event->getException()->getPrevious());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider getAccessDeniedExceptionProvider
|
||||||
|
*/
|
||||||
|
public function testAccessDeniedExceptionNotFullFledgedAndWithoutAccessDeniedHandlerAndWithErrorPage(\Exception $exception, \Exception $eventException = null)
|
||||||
|
{
|
||||||
|
$kernel = $this->getMockBuilder('Symfony\Component\HttpKernel\HttpKernelInterface')->getMock();
|
||||||
|
$kernel->expects($this->once())->method('handle')->will($this->returnValue(new Response('Unauthorized', 401)));
|
||||||
|
|
||||||
|
$event = $this->createEvent($exception, $kernel);
|
||||||
|
|
||||||
|
$httpUtils = $this->getMockBuilder('Symfony\Component\Security\Http\HttpUtils')->getMock();
|
||||||
|
$httpUtils->expects($this->once())->method('createRequest')->will($this->returnValue(Request::create('/error')));
|
||||||
|
|
||||||
|
$listener = $this->createExceptionListener($this->createTokenStorage(), $this->createTrustResolver(true), $httpUtils, null, '/error');
|
||||||
|
$listener->onKernelException($event);
|
||||||
|
|
||||||
|
$this->assertTrue($event->isAllowingCustomResponseCode());
|
||||||
|
|
||||||
|
$this->assertEquals('Unauthorized', $event->getResponse()->getContent());
|
||||||
|
$this->assertEquals(401, $event->getResponse()->getStatusCode());
|
||||||
|
$this->assertSame(null === $eventException ? $exception : $eventException, $event->getException()->getPrevious());
|
||||||
|
}
|
||||||
|
|
||||||
public function getAccessDeniedExceptionProvider()
|
public function getAccessDeniedExceptionProvider()
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
@ -168,6 +201,22 @@ class ExceptionListenerTest extends TestCase
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function createTokenStorage()
|
||||||
|
{
|
||||||
|
$tokenStorage = $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface')->getMock();
|
||||||
|
$tokenStorage->expects($this->once())->method('getToken')->will($this->returnValue($this->getMockBuilder('Symfony\Component\Security\Core\Authentication\Token\TokenInterface')->getMock()));
|
||||||
|
|
||||||
|
return $tokenStorage;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createCustomAccessDeniedHandler(Response $response)
|
||||||
|
{
|
||||||
|
$accessDeniedHandler = $this->getMockBuilder('Symfony\Component\Security\Http\Authorization\AccessDeniedHandlerInterface')->getMock();
|
||||||
|
$accessDeniedHandler->expects($this->once())->method('handle')->will($this->returnValue($response));
|
||||||
|
|
||||||
|
return $accessDeniedHandler;
|
||||||
|
}
|
||||||
|
|
||||||
private function createEntryPoint(Response $response = null)
|
private function createEntryPoint(Response $response = null)
|
||||||
{
|
{
|
||||||
$entryPoint = $this->getMockBuilder('Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface')->getMock();
|
$entryPoint = $this->getMockBuilder('Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface')->getMock();
|
||||||
|
@ -20,8 +20,7 @@ class YamlFileDumperTest extends TestCase
|
|||||||
public function testTreeFormatCatalogue()
|
public function testTreeFormatCatalogue()
|
||||||
{
|
{
|
||||||
$catalogue = new MessageCatalogue('en');
|
$catalogue = new MessageCatalogue('en');
|
||||||
$catalogue->add(
|
$catalogue->add([
|
||||||
[
|
|
||||||
'foo.bar1' => 'value1',
|
'foo.bar1' => 'value1',
|
||||||
'foo.bar2' => 'value2',
|
'foo.bar2' => 'value2',
|
||||||
]);
|
]);
|
||||||
@ -34,8 +33,7 @@ class YamlFileDumperTest extends TestCase
|
|||||||
public function testLinearFormatCatalogue()
|
public function testLinearFormatCatalogue()
|
||||||
{
|
{
|
||||||
$catalogue = new MessageCatalogue('en');
|
$catalogue = new MessageCatalogue('en');
|
||||||
$catalogue->add(
|
$catalogue->add([
|
||||||
[
|
|
||||||
'foo.bar1' => 'value1',
|
'foo.bar1' => 'value1',
|
||||||
'foo.bar2' => 'value2',
|
'foo.bar2' => 'value2',
|
||||||
]);
|
]);
|
||||||
|
@ -54,11 +54,9 @@ class ChoiceValidatorTest extends ConstraintValidatorTestCase
|
|||||||
{
|
{
|
||||||
$this->validator->validate(
|
$this->validator->validate(
|
||||||
null,
|
null,
|
||||||
new Choice(
|
new Choice([
|
||||||
[
|
|
||||||
'choices' => ['foo', 'bar'],
|
'choices' => ['foo', 'bar'],
|
||||||
]
|
])
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->assertNoViolation();
|
$this->assertNoViolation();
|
||||||
@ -100,6 +98,7 @@ class ChoiceValidatorTest extends ConstraintValidatorTestCase
|
|||||||
|
|
||||||
public function testValidChoiceCallbackClosure()
|
public function testValidChoiceCallbackClosure()
|
||||||
{
|
{
|
||||||
|
<<<<<<< HEAD
|
||||||
$constraint = new Choice(
|
$constraint = new Choice(
|
||||||
[
|
[
|
||||||
'callback' => function () {
|
'callback' => function () {
|
||||||
@ -107,6 +106,14 @@ class ChoiceValidatorTest extends ConstraintValidatorTestCase
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
=======
|
||||||
|
$constraint = new Choice([
|
||||||
|
'strict' => true,
|
||||||
|
'callback' => function () {
|
||||||
|
return ['foo', 'bar'];
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
>>>>>>> 3.4
|
||||||
|
|
||||||
$this->validator->validate('bar', $constraint);
|
$this->validator->validate('bar', $constraint);
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user