forked from D3Public/oxtotp
add test for totp login controller
This commit is contained in:
parent
d20322867d
commit
ff9f1722af
@ -20,6 +20,7 @@ use D3\Totp\Application\Model\d3totp;
|
|||||||
use OxidEsales\Eshop\Application\Controller\FrontendController;
|
use OxidEsales\Eshop\Application\Controller\FrontendController;
|
||||||
use OxidEsales\Eshop\Core\Exception\DatabaseConnectionException;
|
use OxidEsales\Eshop\Core\Exception\DatabaseConnectionException;
|
||||||
use OxidEsales\Eshop\Core\Registry;
|
use OxidEsales\Eshop\Core\Registry;
|
||||||
|
use OxidEsales\Eshop\Core\Utils;
|
||||||
|
|
||||||
class d3totplogin extends FrontendController
|
class d3totplogin extends FrontendController
|
||||||
{
|
{
|
||||||
@ -30,8 +31,10 @@ class d3totplogin extends FrontendController
|
|||||||
if (Registry::getSession()->hasVariable(d3totp::TOTP_SESSION_VARNAME) ||
|
if (Registry::getSession()->hasVariable(d3totp::TOTP_SESSION_VARNAME) ||
|
||||||
false == Registry::getSession()->hasVariable(d3totp::TOTP_SESSION_CURRENTUSER)
|
false == Registry::getSession()->hasVariable(d3totp::TOTP_SESSION_CURRENTUSER)
|
||||||
) {
|
) {
|
||||||
Registry::getUtils()->redirect('index.php?cl=start', true, 302);
|
$this->getUtils()->redirect('index.php?cl=start', true, 302);
|
||||||
exit;
|
if (false == defined('OXID_PHP_UNIT')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->addTplParam('navFormParams', Registry::getSession()->getVariable(d3totp::TOTP_SESSION_NAVFORMPARAMS));
|
$this->addTplParam('navFormParams', Registry::getSession()->getVariable(d3totp::TOTP_SESSION_NAVFORMPARAMS));
|
||||||
@ -39,13 +42,21 @@ class d3totplogin extends FrontendController
|
|||||||
return parent::render();
|
return parent::render();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Utils
|
||||||
|
*/
|
||||||
|
public function getUtils()
|
||||||
|
{
|
||||||
|
return Registry::getUtils();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return string|void
|
* @return string|void
|
||||||
* @throws DatabaseConnectionException
|
* @throws DatabaseConnectionException
|
||||||
*/
|
*/
|
||||||
public function getBackupCodeCountMessage()
|
public function getBackupCodeCountMessage()
|
||||||
{
|
{
|
||||||
$oBackupCodeList = oxNew(d3backupcodelist::class);
|
$oBackupCodeList = $this->getBackupCodeListObject();
|
||||||
$iCount = $oBackupCodeList->getAvailableCodeCount(Registry::getSession()->getVariable(d3totp::TOTP_SESSION_CURRENTUSER));
|
$iCount = $oBackupCodeList->getAvailableCodeCount(Registry::getSession()->getVariable(d3totp::TOTP_SESSION_CURRENTUSER));
|
||||||
|
|
||||||
if ($iCount < 4) {
|
if ($iCount < 4) {
|
||||||
@ -58,6 +69,14 @@ class d3totplogin extends FrontendController
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return d3backupcodelist
|
||||||
|
*/
|
||||||
|
public function getBackupCodeListObject()
|
||||||
|
{
|
||||||
|
return oxNew(d3backupcodelist::class);
|
||||||
|
}
|
||||||
|
|
||||||
public function getPreviousClass()
|
public function getPreviousClass()
|
||||||
{
|
{
|
||||||
return Registry::getSession()->getVariable(d3totp::TOTP_SESSION_CURRENTCLASS);
|
return Registry::getSession()->getVariable(d3totp::TOTP_SESSION_CURRENTCLASS);
|
||||||
|
@ -44,7 +44,7 @@ $aLang = [
|
|||||||
|
|
||||||
'D3_TOTP_BACKUPCODES' => 'Backupcodes',
|
'D3_TOTP_BACKUPCODES' => 'Backupcodes',
|
||||||
'D3_TOTP_BACKUPCODES_DESC' => 'Mit diesen Backupcodes können Sie sich anmelden, wenn die Generierung des Einmalpasswortes nicht möglich ist (z.B. Gerät verloren oder neu installiert). Sie können dann die Einstellungen zur Verwendung der 2-Faktor-Authentisierung ändern oder einen neuen Zugang erstellen. Speichern Sie sich diese Codes bitte in diesem Moment sicher ab. Nach Verlassen dieser Seite können diese Codes nicht erneut angezeigt werden.',
|
'D3_TOTP_BACKUPCODES_DESC' => 'Mit diesen Backupcodes können Sie sich anmelden, wenn die Generierung des Einmalpasswortes nicht möglich ist (z.B. Gerät verloren oder neu installiert). Sie können dann die Einstellungen zur Verwendung der 2-Faktor-Authentisierung ändern oder einen neuen Zugang erstellen. Speichern Sie sich diese Codes bitte in diesem Moment sicher ab. Nach Verlassen dieser Seite können diese Codes nicht erneut angezeigt werden.',
|
||||||
'D3_TOTP_AVAILBACKUPCODECOUNT' => 'noch %1$s Backupcodes verfügbar',
|
'D3_TOTP_AVAILBACKUPCODECOUNT' => 'noch %1$s Backupcode(s) verfügbar',
|
||||||
'D3_TOTP_AVAILBACKUPCODECOUNT_DESC' => 'Um neue Backupcodes zu erstellen, löschen Sie die bestehende Registrierung und legen diese bitte neu an.',
|
'D3_TOTP_AVAILBACKUPCODECOUNT_DESC' => 'Um neue Backupcodes zu erstellen, löschen Sie die bestehende Registrierung und legen diese bitte neu an.',
|
||||||
|
|
||||||
'D3_TOTP_ACCOUNT_SAVE' => 'Einstellungen übernehmen',
|
'D3_TOTP_ACCOUNT_SAVE' => 'Einstellungen übernehmen',
|
||||||
|
@ -44,7 +44,7 @@ $aLang = [
|
|||||||
|
|
||||||
'D3_TOTP_BACKUPCODES' => 'backup codes',
|
'D3_TOTP_BACKUPCODES' => 'backup codes',
|
||||||
'D3_TOTP_BACKUPCODES_DESC' => 'You can use these backup codes to log on if it is not possible to generate the one-time password (e.g. device lost or newly installed). You can then change the settings to use 2-factor authentication or create a new account. Please save these codes securely at this moment. After leaving this page, these codes cannot be displayed again.',
|
'D3_TOTP_BACKUPCODES_DESC' => 'You can use these backup codes to log on if it is not possible to generate the one-time password (e.g. device lost or newly installed). You can then change the settings to use 2-factor authentication or create a new account. Please save these codes securely at this moment. After leaving this page, these codes cannot be displayed again.',
|
||||||
'D3_TOTP_AVAILBACKUPCODECOUNT' => 'still %1$s backup codes available',
|
'D3_TOTP_AVAILBACKUPCODECOUNT' => 'still %1$s backup code(s) available',
|
||||||
'D3_TOTP_AVAILBACKUPCODECOUNT_DESC' => 'To create new backup codes, delete the existing registry and create a new one.',
|
'D3_TOTP_AVAILBACKUPCODECOUNT_DESC' => 'To create new backup codes, delete the existing registry and create a new one.',
|
||||||
|
|
||||||
'D3_TOTP_ACCOUNT_SAVE' => 'Confirm settings',
|
'D3_TOTP_ACCOUNT_SAVE' => 'Confirm settings',
|
||||||
|
@ -42,7 +42,7 @@ $aLang = [
|
|||||||
|
|
||||||
'D3_TOTP_BACKUPCODES' => 'Backupcodes',
|
'D3_TOTP_BACKUPCODES' => 'Backupcodes',
|
||||||
'D3_TOTP_BACKUPCODES_DESC' => 'Mit diesen Backupcodes können Sie sich anmelden, wenn die Generierung des Einmalpasswortes nicht möglich ist (z.B. Gerät verloren oder neu installiert). Sie können dann die Einstellungen zur Verwendung der 2-Faktor-Authentisierung ändern oder einen neuen Zugang erstellen. Speichern Sie sich diese Codes bitte in diesem Moment sicher ab. Nach Verlassen dieser Seite können diese Codes nicht erneut angezeigt werden.',
|
'D3_TOTP_BACKUPCODES_DESC' => 'Mit diesen Backupcodes können Sie sich anmelden, wenn die Generierung des Einmalpasswortes nicht möglich ist (z.B. Gerät verloren oder neu installiert). Sie können dann die Einstellungen zur Verwendung der 2-Faktor-Authentisierung ändern oder einen neuen Zugang erstellen. Speichern Sie sich diese Codes bitte in diesem Moment sicher ab. Nach Verlassen dieser Seite können diese Codes nicht erneut angezeigt werden.',
|
||||||
'D3_TOTP_AVAILBACKUPCODECOUNT' => 'noch %1$s Backupcodes verfügbar',
|
'D3_TOTP_AVAILBACKUPCODECOUNT' => 'noch %1$s Backupcode(s) verfügbar',
|
||||||
'D3_TOTP_AVAILBACKUPCODECOUNT_DESC' => 'Um neue Backupcodes zu erstellen, löschen Sie die bestehende Registrierung und legen diese bitte neu an.',
|
'D3_TOTP_AVAILBACKUPCODECOUNT_DESC' => 'Um neue Backupcodes zu erstellen, löschen Sie die bestehende Registrierung und legen diese bitte neu an.',
|
||||||
|
|
||||||
'D3_TOTP_SAVE' => 'Speichern',
|
'D3_TOTP_SAVE' => 'Speichern',
|
||||||
|
@ -42,7 +42,7 @@ $aLang = [
|
|||||||
|
|
||||||
'D3_TOTP_BACKUPCODES' => 'backup codes',
|
'D3_TOTP_BACKUPCODES' => 'backup codes',
|
||||||
'D3_TOTP_BACKUPCODES_DESC' => 'You can use these backup codes to log on if it is not possible to generate the one-time password (e.g. device lost or newly installed). You can then change the settings to use 2-factor authentication or create a new 2FA login. Please save these codes safely at this moment. After leaving this page, these codes cannot be displayed again.',
|
'D3_TOTP_BACKUPCODES_DESC' => 'You can use these backup codes to log on if it is not possible to generate the one-time password (e.g. device lost or newly installed). You can then change the settings to use 2-factor authentication or create a new 2FA login. Please save these codes safely at this moment. After leaving this page, these codes cannot be displayed again.',
|
||||||
'D3_TOTP_AVAILBACKUPCODECOUNT' => '%1$s backup codes still available',
|
'D3_TOTP_AVAILBACKUPCODECOUNT' => '%1$s backup code(s) still available',
|
||||||
'D3_TOTP_AVAILBACKUPCODECOUNT_DESC' => 'To create new backup codes, delete the existing registry and create a new one.',
|
'D3_TOTP_AVAILBACKUPCODECOUNT_DESC' => 'To create new backup codes, delete the existing registry and create a new one.',
|
||||||
|
|
||||||
'D3_TOTP_SAVE' => 'Save',
|
'D3_TOTP_SAVE' => 'Save',
|
||||||
|
281
src/tests/unit/Application/Controller/d3totploginTest.php
Normal file
281
src/tests/unit/Application/Controller/d3totploginTest.php
Normal file
@ -0,0 +1,281 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This Software is the property of Data Development and is protected
|
||||||
|
* by copyright law - it is NOT Freeware.
|
||||||
|
*
|
||||||
|
* Any unauthorized use of this software without a valid license
|
||||||
|
* is a violation of the license agreement and will be prosecuted by
|
||||||
|
* civil and criminal law.
|
||||||
|
*
|
||||||
|
* http://www.shopmodule.com
|
||||||
|
*
|
||||||
|
* @copyright (C) D3 Data Development (Inh. Thomas Dartsch)
|
||||||
|
* @author D3 Data Development - Daniel Seifert <support@shopmodule.com>
|
||||||
|
* @link http://www.oxidmodule.com
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace D3\Totp\tests\unit\Application\Controller;
|
||||||
|
|
||||||
|
use D3\Totp\Application\Controller\d3totplogin;
|
||||||
|
use D3\Totp\Application\Model\d3backupcodelist;
|
||||||
|
use D3\Totp\Application\Model\d3totp;
|
||||||
|
use D3\Totp\tests\unit\d3TotpUnitTestCase;
|
||||||
|
use Doctrine\DBAL\DBALException;
|
||||||
|
use OxidEsales\Eshop\Core\Exception\DatabaseConnectionException;
|
||||||
|
use OxidEsales\Eshop\Core\Exception\DatabaseErrorException;
|
||||||
|
use OxidEsales\Eshop\Core\Registry;
|
||||||
|
use OxidEsales\Eshop\Core\Utils;
|
||||||
|
use PHPUnit_Framework_MockObject_MockObject;
|
||||||
|
use ReflectionException;
|
||||||
|
|
||||||
|
class d3totploginTest extends d3TotpUnitTestCase
|
||||||
|
{
|
||||||
|
/** @var d3totplogin */
|
||||||
|
protected $_oController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* setup basic requirements
|
||||||
|
* @throws DBALException
|
||||||
|
* @throws DatabaseConnectionException
|
||||||
|
* @throws DatabaseErrorException
|
||||||
|
*/
|
||||||
|
public function setUp()
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
$this->_oController = oxNew(d3totplogin::class);
|
||||||
|
|
||||||
|
Registry::getSession()->deleteVariable(d3totp::TOTP_SESSION_CURRENTUSER);
|
||||||
|
Registry::getSession()->deleteVariable(d3totp::TOTP_SESSION_CURRENTCLASS);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function tearDown()
|
||||||
|
{
|
||||||
|
parent::tearDown();
|
||||||
|
|
||||||
|
unset($this->_oController);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
* @throws ReflectionException
|
||||||
|
*/
|
||||||
|
public function renderRedirectIfNoTotp()
|
||||||
|
{
|
||||||
|
/** @var Utils|PHPUnit_Framework_MockObject_MockObject $oUtilsMock */
|
||||||
|
$oUtilsMock = $this->getMock(Utils::class, array(
|
||||||
|
'redirect'
|
||||||
|
));
|
||||||
|
$oUtilsMock->expects($this->once())->method('redirect')->willReturn(true);
|
||||||
|
|
||||||
|
/** @var d3totplogin|PHPUnit_Framework_MockObject_MockObject $oControllerMock */
|
||||||
|
$oControllerMock = $this->getMock(d3totplogin::class, array(
|
||||||
|
'getUtils'
|
||||||
|
));
|
||||||
|
$oControllerMock->method('getUtils')->willReturn($oUtilsMock);
|
||||||
|
|
||||||
|
$this->_oController = $oControllerMock;
|
||||||
|
|
||||||
|
$this->callMethod($this->_oController, 'render');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
* @throws ReflectionException
|
||||||
|
*/
|
||||||
|
public function renderDontRedirect()
|
||||||
|
{
|
||||||
|
Registry::getSession()->setVariable(d3totp::TOTP_SESSION_CURRENTUSER, 'foo');
|
||||||
|
|
||||||
|
/** @var Utils|PHPUnit_Framework_MockObject_MockObject $oUtilsMock */
|
||||||
|
$oUtilsMock = $this->getMock(Utils::class, array(
|
||||||
|
'redirect'
|
||||||
|
));
|
||||||
|
$oUtilsMock->expects($this->never())->method('redirect')->willReturn(true);
|
||||||
|
|
||||||
|
/** @var d3totplogin|PHPUnit_Framework_MockObject_MockObject $oControllerMock */
|
||||||
|
$oControllerMock = $this->getMock(d3totplogin::class, array(
|
||||||
|
'getUtils'
|
||||||
|
));
|
||||||
|
$oControllerMock->method('getUtils')->willReturn($oUtilsMock);
|
||||||
|
|
||||||
|
$this->_oController = $oControllerMock;
|
||||||
|
|
||||||
|
$this->assertSame(
|
||||||
|
'd3totplogin.tpl',
|
||||||
|
$this->callMethod($this->_oController, 'render')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
* @throws ReflectionException
|
||||||
|
*/
|
||||||
|
public function getUtilsReturnsRightInstance()
|
||||||
|
{
|
||||||
|
$this->assertInstanceOf(
|
||||||
|
Utils::class,
|
||||||
|
$this->callMethod($this->_oController, 'getUtils')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
* @throws ReflectionException
|
||||||
|
*/
|
||||||
|
public function getBackupCodeCountMessageReturnMessage()
|
||||||
|
{
|
||||||
|
/** @var d3backupcodelist|PHPUnit_Framework_MockObject_MockObject $oBackupCodesListMock */
|
||||||
|
$oBackupCodesListMock = $this->getMock(d3backupcodelist::class, array(
|
||||||
|
'getAvailableCodeCount'
|
||||||
|
));
|
||||||
|
$oBackupCodesListMock->method('getAvailableCodeCount')->willReturn(1);
|
||||||
|
|
||||||
|
/** @var d3totplogin|PHPUnit_Framework_MockObject_MockObject $oControllerMock */
|
||||||
|
$oControllerMock = $this->getMock(d3totplogin::class, array(
|
||||||
|
'getBackupCodeListObject'
|
||||||
|
));
|
||||||
|
$oControllerMock->method('getBackupCodeListObject')->willReturn($oBackupCodesListMock);
|
||||||
|
|
||||||
|
$this->_oController = $oControllerMock;
|
||||||
|
|
||||||
|
$this->assertGreaterThan(
|
||||||
|
0,
|
||||||
|
strpos(
|
||||||
|
$this->callMethod($this->_oController, 'getBackupCodeCountMessage'),
|
||||||
|
' 1 '
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
* @throws ReflectionException
|
||||||
|
*/
|
||||||
|
public function getBackupCodeCountMessageReturnNoMessage()
|
||||||
|
{
|
||||||
|
/** @var d3backupcodelist|PHPUnit_Framework_MockObject_MockObject $oBackupCodesListMock */
|
||||||
|
$oBackupCodesListMock = $this->getMock(d3backupcodelist::class, array(
|
||||||
|
'getAvailableCodeCount'
|
||||||
|
));
|
||||||
|
$oBackupCodesListMock->method('getAvailableCodeCount')->willReturn(1234);
|
||||||
|
|
||||||
|
/** @var d3totplogin|PHPUnit_Framework_MockObject_MockObject $oControllerMock */
|
||||||
|
$oControllerMock = $this->getMock(d3totplogin::class, array(
|
||||||
|
'getBackupCodeListObject'
|
||||||
|
));
|
||||||
|
$oControllerMock->method('getBackupCodeListObject')->willReturn($oBackupCodesListMock);
|
||||||
|
|
||||||
|
$this->_oController = $oControllerMock;
|
||||||
|
|
||||||
|
$this->assertEmpty(
|
||||||
|
$this->callMethod($this->_oController, 'getBackupCodeCountMessage')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
* @throws ReflectionException
|
||||||
|
*/
|
||||||
|
public function getBackupCodeListObjectReturnsRightInstance()
|
||||||
|
{
|
||||||
|
$this->assertInstanceOf(
|
||||||
|
d3backupcodelist::class,
|
||||||
|
$this->callMethod($this->_oController, 'getBackupCodeListObject')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
* @throws ReflectionException
|
||||||
|
*/
|
||||||
|
public function canGetPreviousClass()
|
||||||
|
{
|
||||||
|
$className = "testClass";
|
||||||
|
Registry::getSession()->setVariable(d3totp::TOTP_SESSION_CURRENTCLASS, $className);
|
||||||
|
|
||||||
|
$this->assertSame(
|
||||||
|
$className,
|
||||||
|
$this->callMethod($this->_oController, 'getPreviousClass')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
* @throws ReflectionException
|
||||||
|
*/
|
||||||
|
public function orderClassIsOrderStep()
|
||||||
|
{
|
||||||
|
Registry::getSession()->setVariable(d3totp::TOTP_SESSION_CURRENTCLASS, 'order');
|
||||||
|
|
||||||
|
$this->assertTrue(
|
||||||
|
$this->callMethod(
|
||||||
|
$this->_oController,
|
||||||
|
'previousClassIsOrderStep'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
* @throws ReflectionException
|
||||||
|
*/
|
||||||
|
public function startClassIsNoOrderStep()
|
||||||
|
{
|
||||||
|
Registry::getSession()->setVariable(d3totp::TOTP_SESSION_CURRENTCLASS, 'start');
|
||||||
|
|
||||||
|
$this->assertFalse(
|
||||||
|
$this->callMethod(
|
||||||
|
$this->_oController,
|
||||||
|
'previousClassIsOrderStep'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
* @throws ReflectionException
|
||||||
|
*/
|
||||||
|
public function getIsOrderStepIsSameLikeOrderClass()
|
||||||
|
{
|
||||||
|
Registry::getSession()->setVariable(d3totp::TOTP_SESSION_CURRENTCLASS, 'order');
|
||||||
|
|
||||||
|
$this->assertTrue(
|
||||||
|
$this->callMethod(
|
||||||
|
$this->_oController,
|
||||||
|
'getIsOrderStep'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
* @throws ReflectionException
|
||||||
|
*/
|
||||||
|
public function getIsOrderStepIsSameLikeStartClass()
|
||||||
|
{
|
||||||
|
Registry::getSession()->setVariable(d3totp::TOTP_SESSION_CURRENTCLASS, 'start');
|
||||||
|
|
||||||
|
$this->assertFalse(
|
||||||
|
$this->callMethod(
|
||||||
|
$this->_oController,
|
||||||
|
'getIsOrderStep'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
* @throws ReflectionException
|
||||||
|
*/
|
||||||
|
public function canGetBreadCrumb()
|
||||||
|
{
|
||||||
|
$aBreadCrumb = $this->callMethod($this->_oController, 'getBreadCrumb');
|
||||||
|
|
||||||
|
$this->assertInternalType('string', $aBreadCrumb[0]['title']);
|
||||||
|
$this->assertTrue(strlen($aBreadCrumb[0]['title']) > 1);
|
||||||
|
$this->assertInternalType('string', $aBreadCrumb[0]['link']);
|
||||||
|
$this->assertTrue(strlen($aBreadCrumb[0]['link']) > 1);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user