From 0f2e4e7119aa0bc2b00bc054d63751f71c37374d Mon Sep 17 00:00:00 2001 From: nuxsmin Date: Sun, 22 Jul 2018 19:31:00 +0200 Subject: [PATCH] * [ADD] Unit testing. Work in progress * [MOD] Improved Public Links key generation * [MOD] Code refactoring --- .../web/Controllers/AccountController.php | 2 +- app/modules/web/Forms/PublicLinkForm.php | 3 - .../Repositories/Plugin/PluginRepository.php | 2 +- .../Services/AuthToken/AuthTokenService.php | 1 + lib/SP/Services/Plugin/PluginService.php | 58 ++- lib/SP/Services/PublicLink/PublicLinkKey.php | 87 ++++ .../Services/PublicLink/PublicLinkService.php | 102 ++-- lib/SP/Services/Upgrade/UpgradePublicLink.php | 2 - test/Repositories/PluginRepositoryTest.php | 12 +- .../Repositories/PublicLinkRepositoryTest.php | 2 +- test/Services/Plugin/PluginServiceTest.php | 384 +++++++++++++++ .../PublicLink/PublicLinkServiceTest.php | 459 ++++++++++++++++++ test/res/datasets/syspass_publicLink.xml | 39 ++ 13 files changed, 1078 insertions(+), 75 deletions(-) create mode 100644 lib/SP/Services/PublicLink/PublicLinkKey.php create mode 100644 test/Services/Plugin/PluginServiceTest.php create mode 100644 test/Services/PublicLink/PublicLinkServiceTest.php create mode 100644 test/res/datasets/syspass_publicLink.xml diff --git a/app/modules/web/Controllers/AccountController.php b/app/modules/web/Controllers/AccountController.php index f3b81b0c..b2feba2c 100644 --- a/app/modules/web/Controllers/AccountController.php +++ b/app/modules/web/Controllers/AccountController.php @@ -195,7 +195,7 @@ class AccountController extends ControllerBase implements CrudControllerInterfac $vault = unserialize($publicLinkData->getData()); /** @var AccountExtData $accountData */ - $accountData = Util::unserialize(AccountExtData::class, $vault->getData(PublicLinkService::getKeyForHash($this->config->getConfigData()->getPasswordSalt(), $publicLinkData))); + $accountData = Util::unserialize(AccountExtData::class, $vault->getData($publicLinkService->getPublicLinkKey($publicLinkData->getHash())->getKey())); $this->view->assign('title', [ diff --git a/app/modules/web/Forms/PublicLinkForm.php b/app/modules/web/Forms/PublicLinkForm.php index 01644ec9..c529f45a 100644 --- a/app/modules/web/Forms/PublicLinkForm.php +++ b/app/modules/web/Forms/PublicLinkForm.php @@ -29,7 +29,6 @@ use SP\Core\Exceptions\ValidationException; use SP\DataModel\PublicLinkData; use SP\Mgmt\PublicLinks\PublicLink; use SP\Services\PublicLink\PublicLinkService; -use SP\Util\Util; /** * Class PublicLinkForm @@ -68,7 +67,6 @@ class PublicLinkForm extends FormBase implements FormInterface * Analizar los datos de la petición HTTP * * @return void - * @throws \Defuse\Crypto\Exception\EnvironmentIsBrokenException */ protected function analyzeRequestData() { @@ -77,7 +75,6 @@ class PublicLinkForm extends FormBase implements FormInterface $this->publicLinkData->setTypeId(PublicLinkService::TYPE_ACCOUNT); $this->publicLinkData->setItemId($this->request->analyzeInt('accountId')); $this->publicLinkData->setNotify($this->request->analyzeBool('notify', false)); - $this->publicLinkData->setHash(Util::generateRandomBytes()); } /** diff --git a/lib/SP/Repositories/Plugin/PluginRepository.php b/lib/SP/Repositories/Plugin/PluginRepository.php index 7abbd95c..38b9dde4 100644 --- a/lib/SP/Repositories/Plugin/PluginRepository.php +++ b/lib/SP/Repositories/Plugin/PluginRepository.php @@ -209,7 +209,7 @@ class PluginRepository extends Repository implements RepositoryItemInterface $queryData = new QueryData(); $queryData->setQuery('DELETE FROM Plugin WHERE id IN (' . $this->getParamsFromArray($ids) . ')'); $queryData->setParams($ids); - $queryData->setOnErrorMessage(__u('Error al eliminar el plugin')); + $queryData->setOnErrorMessage(__u('Error al eliminar los plugins')); return $this->db->doQuery($queryData)->getAffectedNumRows(); } diff --git a/lib/SP/Services/AuthToken/AuthTokenService.php b/lib/SP/Services/AuthToken/AuthTokenService.php index f7f0bcad..0744306a 100644 --- a/lib/SP/Services/AuthToken/AuthTokenService.php +++ b/lib/SP/Services/AuthToken/AuthTokenService.php @@ -170,6 +170,7 @@ class AuthTokenService extends Service * @param string $token * * @return AuthTokenData + * @throws ServiceException * @throws \Defuse\Crypto\Exception\CryptoException * @throws \Defuse\Crypto\Exception\EnvironmentIsBrokenException * @throws \SP\Core\Exceptions\ConstraintException diff --git a/lib/SP/Services/Plugin/PluginService.php b/lib/SP/Services/Plugin/PluginService.php index 790e385b..0fa5c059 100644 --- a/lib/SP/Services/Plugin/PluginService.php +++ b/lib/SP/Services/Plugin/PluginService.php @@ -28,6 +28,7 @@ use SP\Core\Exceptions\SPException; use SP\DataModel\ItemData; use SP\DataModel\ItemSearchData; use SP\DataModel\PluginData; +use SP\Repositories\NoSuchItemException; use SP\Repositories\Plugin\PluginRepository; use SP\Services\Service; use SP\Services\ServiceException; @@ -81,10 +82,17 @@ class PluginService extends Service * @return PluginData * @throws \SP\Core\Exceptions\ConstraintException * @throws \SP\Core\Exceptions\QueryException + * @throws NoSuchItemException */ public function getById($id) { - return $this->pluginRepository->getById($id)->getData(); + $result = $this->pluginRepository->getById($id); + + if ($result->getNumRows() === 0) { + throw new NoSuchItemException(__u('Plugin no encontrado'), NoSuchItemException::INFO); + } + + return $result->getData(); } /** @@ -125,7 +133,9 @@ class PluginService extends Service */ public function deleteByIdBatch(array $ids) { - $this->pluginRepository->deleteByIdBatch($ids); + if ($this->pluginRepository->deleteByIdBatch($ids) !== count($ids)) { + throw new ServiceException(__u('Error al eliminar los plugins')); + } } /** @@ -140,7 +150,7 @@ class PluginService extends Service public function delete($id) { if ($this->pluginRepository->delete($id) === 0) { - throw new ServiceException(__u('Plugin no encontrado'), ServiceException::INFO); + throw new NoSuchItemException(__u('Plugin no encontrado'), NoSuchItemException::INFO); } } @@ -164,12 +174,19 @@ class PluginService extends Service * @param string $name * * @return PluginData + * @throws NoSuchItemException * @throws \SP\Core\Exceptions\ConstraintException * @throws \SP\Core\Exceptions\QueryException */ public function getByName($name) { - return $this->pluginRepository->getByName($name)->getData(); + $result = $this->pluginRepository->getByName($name); + + if ($result->getNumRows() === 0) { + throw new NoSuchItemException(__u('Plugin no encontrado'), NoSuchItemException::INFO); + } + + return $result->getData(); } /** @@ -178,13 +195,16 @@ class PluginService extends Service * @param $id * @param $enabled * - * @return bool + * @return void + * @throws NoSuchItemException * @throws \SP\Core\Exceptions\ConstraintException * @throws \SP\Core\Exceptions\QueryException */ public function toggleEnabled($id, $enabled) { - return $this->pluginRepository->toggleEnabled($id, $enabled); + if ($this->pluginRepository->toggleEnabled($id, $enabled) === 0) { + throw new NoSuchItemException(__u('Plugin no encontrado'), NoSuchItemException::INFO); + } } /** @@ -193,13 +213,16 @@ class PluginService extends Service * @param $name * @param $enabled * - * @return bool + * @return void + * @throws NoSuchItemException * @throws \SP\Core\Exceptions\ConstraintException * @throws \SP\Core\Exceptions\QueryException */ public function toggleEnabledByName($name, $enabled) { - return $this->pluginRepository->toggleEnabledByName($name, $enabled); + if ($this->pluginRepository->toggleEnabledByName($name, $enabled) === 0) { + throw new NoSuchItemException(__u('Plugin no encontrado'), NoSuchItemException::INFO); + } } /** @@ -208,13 +231,15 @@ class PluginService extends Service * @param $id * @param $available * - * @return bool + * @throws NoSuchItemException * @throws \SP\Core\Exceptions\ConstraintException * @throws \SP\Core\Exceptions\QueryException */ public function toggleAvailable($id, $available) { - return $this->pluginRepository->toggleAvailable($id, $available); + if ($this->pluginRepository->toggleAvailable($id, $available) === 0) { + throw new NoSuchItemException(__u('Plugin no encontrado'), NoSuchItemException::INFO); + } } /** @@ -223,13 +248,15 @@ class PluginService extends Service * @param $name * @param $available * - * @return bool + * @throws NoSuchItemException * @throws \SP\Core\Exceptions\ConstraintException * @throws \SP\Core\Exceptions\QueryException */ public function toggleAvailableByName($name, $available) { - return $this->pluginRepository->toggleAvailableByName($name, $available); + if ($this->pluginRepository->toggleAvailableByName($name, $available) === 0) { + throw new NoSuchItemException(__u('Plugin no encontrado'), NoSuchItemException::INFO); + } } /** @@ -238,12 +265,17 @@ class PluginService extends Service * @param int $id Id del plugin * * @return bool + * @throws NoSuchItemException * @throws \SP\Core\Exceptions\ConstraintException * @throws \SP\Core\Exceptions\QueryException */ public function resetById($id) { - return $this->pluginRepository->resetById($id); + if (($count = $this->pluginRepository->resetById($id)) === 0) { + throw new NoSuchItemException(__u('Plugin no encontrado'), NoSuchItemException::INFO); + } + + return $count; } /** diff --git a/lib/SP/Services/PublicLink/PublicLinkKey.php b/lib/SP/Services/PublicLink/PublicLinkKey.php new file mode 100644 index 00000000..48bd65fd --- /dev/null +++ b/lib/SP/Services/PublicLink/PublicLinkKey.php @@ -0,0 +1,87 @@ +. + */ + +namespace SP\Services\PublicLink; + +use SP\Util\Util; + +/** + * Class PublicLinkKey + * + * @package SP\Services\PublicLink + */ +class PublicLinkKey +{ + /** + * @var string + */ + private $hash; + /** + * @var string + */ + private $salt; + + /** + * PublicLinkKey constructor. + * + * @param string $salt + * @param string $hash + * + * @throws \Defuse\Crypto\Exception\EnvironmentIsBrokenException + */ + public function __construct(string $salt, string $hash = null) + { + $this->salt = $salt; + + if ($hash === null) { + $this->hash = Util::generateRandomBytes(); + } else { + $this->hash = $hash; + } + } + + /** + * @return string + */ + public function getKey(): string + { + return sha1($this->salt . $this->hash); + } + + /** + * @return string + */ + public function getHash(): string + { + return $this->hash; + } + + /** + * @return string + */ + public function getSalt(): string + { + return $this->salt; + } +} \ No newline at end of file diff --git a/lib/SP/Services/PublicLink/PublicLinkService.php b/lib/SP/Services/PublicLink/PublicLinkService.php index 8fd34369..e9e9ce9b 100644 --- a/lib/SP/Services/PublicLink/PublicLinkService.php +++ b/lib/SP/Services/PublicLink/PublicLinkService.php @@ -27,11 +27,11 @@ namespace SP\Services\PublicLink; use SP\Bootstrap; use SP\Config\Config; use SP\Core\Crypt\Crypt; -use SP\Core\Crypt\Session as CryptSession; use SP\Core\Crypt\Vault; use SP\Core\Exceptions\SPException; use SP\DataModel\ItemSearchData; use SP\DataModel\PublicLinkData; +use SP\DataModel\PublicLinkListData; use SP\Http\Request; use SP\Repositories\NoSuchItemException; use SP\Repositories\PublicLink\PublicLinkRepository; @@ -40,7 +40,6 @@ use SP\Services\Service; use SP\Services\ServiceException; use SP\Services\ServiceItemTrait; use SP\Storage\Database\QueryResult; -use SP\Util\Util; /** * Class PublicLinkService @@ -86,6 +85,17 @@ class PublicLinkService extends Service return hash('sha256', uniqid('sysPassPublicLink', true)); } + /** + * @param string $salt + * @param PublicLinkData $publicLinkData + * + * @return string + */ + public static function getKeyForHash($salt, PublicLinkData $publicLinkData) + { + return sha1($salt . $publicLinkData->getHash()); + } + /** * @param ItemSearchData $itemSearchData * @@ -106,7 +116,13 @@ class PublicLinkService extends Service */ public function getById($id) { - return $this->publicLinkRepository->getById($id)->getData(); + $result = $this->publicLinkRepository->getById($id); + + if ($result->getNumRows() === 0) { + throw new NoSuchItemException(__u('Enlace no encontrado')); + } + + return $result->getData(); } /** @@ -123,18 +139,17 @@ class PublicLinkService extends Service */ public function refresh($id) { - $salt = $this->config->getConfigData()->getPasswordSalt(); - $key = self::getNewKey($salt); - $result = $this->publicLinkRepository->getById($id); if ($result->getNumRows() === 0) { - throw new NoSuchItemException(__u('El enlace no existe')); + throw new NoSuchItemException(__u('Enlace no encontrado')); } + $key = $this->getPublicLinkKey(); + /** @var PublicLinkData $publicLinkData */ $publicLinkData = $result->getData(); - $publicLinkData->setHash(self::getHashForKey($key, $salt)); + $publicLinkData->setHash($key->getHash()); $publicLinkData->setData($this->getSecuredLinkData($publicLinkData->getItemId(), $key)); $publicLinkData->setDateExpire(self::calcDateExpire($this->config)); $publicLinkData->setCountViews($this->config->getConfigData()->getPublinksMaxViews()); @@ -143,52 +158,39 @@ class PublicLinkService extends Service } /** - * @param string $salt + * @param string|null $hash * - * @return string + * @return PublicLinkKey * @throws \Defuse\Crypto\Exception\EnvironmentIsBrokenException */ - public static function getNewKey($salt) + public function getPublicLinkKey(string $hash = null) { - return $salt . Util::generateRandomBytes(); - } - - /** - * Returns the hash from a composed key - * - * @param string $key - * @param string $salt - * - * @return mixed - */ - public static function getHashForKey($key, $salt) - { - return str_replace($salt, '', $key); + return new PublicLinkKey($this->config->getConfigData()->getPasswordSalt(), $hash); } /** * Obtener los datos de una cuenta y encriptarlos para el enlace * - * @param int $itemId - * @param string $linkKey + * @param int $itemId + * @param PublicLinkKey $key * * @return Vault + * @throws NoSuchItemException + * @throws ServiceException * @throws \Defuse\Crypto\Exception\CryptoException - * @throws \Psr\Container\ContainerExceptionInterface - * @throws \Psr\Container\NotFoundExceptionInterface - * @throws SPException + * @throws \SP\Core\Exceptions\ConstraintException + * @throws \SP\Core\Exceptions\QueryException */ - protected function getSecuredLinkData($itemId, $linkKey) + private function getSecuredLinkData($itemId, PublicLinkKey $key) { // Obtener los datos de la cuenta $accountData = $this->dic->get(AccountService::class)->getDataForLink($itemId); // Desencriptar la clave de la cuenta - $accountData->setPass(Crypt::decrypt($accountData->getPass(), $accountData->getKey(), CryptSession::getSessionKey($this->context))); + $accountData->setPass(Crypt::decrypt($accountData->getPass(), $accountData->getKey(), $this->getMasterKeyFromContext())); $accountData->setKey(null); - $vault = new Vault(); - return serialize($vault->saveData(serialize($accountData), $linkKey)); + return (new Vault())->saveData(serialize($accountData), $key->getKey())->getSerialized(); } /** @@ -207,14 +209,14 @@ class PublicLinkService extends Service * @param $id * * @return $this + * @throws NoSuchItemException * @throws \SP\Core\Exceptions\ConstraintException * @throws \SP\Core\Exceptions\QueryException - * @throws \SP\Core\Exceptions\SPException */ public function delete($id) { if ($this->publicLinkRepository->delete($id) === 0) { - throw new ServiceException(__u('Enlace no encontrado'), ServiceException::INFO); + throw new NoSuchItemException(__u('Enlace no encontrado'), NoSuchItemException::INFO); } return $this; @@ -252,7 +254,10 @@ class PublicLinkService extends Service */ public function create(PublicLinkData $itemData) { - $itemData->setData($this->getSecuredLinkData($itemData->getItemId(), self::getKeyForHash($this->config->getConfigData()->getPasswordSalt(), $itemData))); + $key = $this->getPublicLinkKey(); + + $itemData->setHash($key->getHash()); + $itemData->setData($this->getSecuredLinkData($itemData->getItemId(), $key)); $itemData->setDateExpire(self::calcDateExpire($this->config)); $itemData->setMaxCountViews($this->config->getConfigData()->getPublinksMaxViews()); $itemData->setUserId($this->context->getUserData()->getId()); @@ -260,21 +265,10 @@ class PublicLinkService extends Service return $this->publicLinkRepository->create($itemData)->getLastId(); } - /** - * @param string $salt - * @param PublicLinkData $publicLinkData - * - * @return string - */ - public static function getKeyForHash($salt, PublicLinkData $publicLinkData) - { - return $salt . $publicLinkData->getHash(); - } - /** * Get all items from the service's repository * - * @return array + * @return PublicLinkListData[] * @throws \SP\Core\Exceptions\ConstraintException * @throws \SP\Core\Exceptions\QueryException */ @@ -288,7 +282,7 @@ class PublicLinkService extends Service * * @param PublicLinkData $publicLinkData * - * @return bool + * @throws NoSuchItemException * @throws \SP\Core\Exceptions\ConstraintException * @throws \SP\Core\Exceptions\QueryException */ @@ -313,7 +307,9 @@ class PublicLinkService extends Service // Email::sendEmail($LogMessage); // } - return $this->publicLinkRepository->addLinkView($publicLinkData); + if ($this->publicLinkRepository->addLinkView($publicLinkData) === 0) { + throw new NoSuchItemException(__u('Enlace no encontrado')); + } } /** @@ -347,7 +343,7 @@ class PublicLinkService extends Service $result = $this->publicLinkRepository->getByHash($hash); if ($result->getNumRows() === 0) { - throw new NoSuchItemException(__u('El enlace no existe')); + throw new NoSuchItemException(__u('Enlace no encontrado')); } return $result->getData(); @@ -368,7 +364,7 @@ class PublicLinkService extends Service $result = $this->publicLinkRepository->getHashForItem($itemId); if ($result->getNumRows() === 0) { - throw new NoSuchItemException(__u('El enlace no existe')); + throw new NoSuchItemException(__u('Enlace no encontrado')); } return $result->getData(); diff --git a/lib/SP/Services/Upgrade/UpgradePublicLink.php b/lib/SP/Services/Upgrade/UpgradePublicLink.php index 07e4fe39..0423de00 100644 --- a/lib/SP/Services/Upgrade/UpgradePublicLink.php +++ b/lib/SP/Services/Upgrade/UpgradePublicLink.php @@ -49,8 +49,6 @@ class UpgradePublicLink extends Service /** * upgrade_300_18010101 - * - * @throws \SP\Core\Exceptions\SPException */ public function upgrade_300_18010101() { diff --git a/test/Repositories/PluginRepositoryTest.php b/test/Repositories/PluginRepositoryTest.php index d3dbff71..e0e7741b 100644 --- a/test/Repositories/PluginRepositoryTest.php +++ b/test/Repositories/PluginRepositoryTest.php @@ -137,7 +137,7 @@ class PluginRepositoryTest extends DatabaseTestCase $this->assertEquals(1, $data->getAvailable()); $this->assertEquals(0, $data->getEnabled()); - $this->assertEquals(0, self::$repository->getById('Authenticator 2')->getNumRows()); + $this->assertEquals(0, self::$repository->getByName('Authenticator 2')->getNumRows()); } /** @@ -258,6 +258,16 @@ class PluginRepositoryTest extends DatabaseTestCase $this->expectException(ConstraintException::class); self::$repository->create($data); + } + + /** + * @depends testGetById + * @throws ConstraintException + * @throws \SP\Core\Exceptions\QueryException + */ + public function testCreateBlank() + { + $this->expectException(ConstraintException::class); self::$repository->create(new PluginData()); } diff --git a/test/Repositories/PublicLinkRepositoryTest.php b/test/Repositories/PublicLinkRepositoryTest.php index cbf4d9cf..3cf8e43e 100644 --- a/test/Repositories/PublicLinkRepositoryTest.php +++ b/test/Repositories/PublicLinkRepositoryTest.php @@ -253,7 +253,7 @@ class PublicLinkRepositoryTest extends DatabaseTestCase $hash = pack('H*', '313065363937306666653833623531393234356635333433333732626366663433376461623565356134386238326131653238636131356235346635'); $useInfo = [ - 'who' => '127.0.0.1', + 'who' => SELF_IP_ADDRESS, 'time' => time(), 'hash' => $hash, 'agent' => 'Mozilla/Firefox', diff --git a/test/Services/Plugin/PluginServiceTest.php b/test/Services/Plugin/PluginServiceTest.php new file mode 100644 index 00000000..ae1a3b50 --- /dev/null +++ b/test/Services/Plugin/PluginServiceTest.php @@ -0,0 +1,384 @@ +. + */ + +namespace SP\Tests\Services\Plugin; + +use SP\Core\Exceptions\ConstraintException; +use SP\DataModel\ItemData; +use SP\DataModel\ItemSearchData; +use SP\DataModel\PluginData; +use SP\Repositories\NoSuchItemException; +use SP\Services\Plugin\PluginService; +use SP\Services\ServiceException; +use SP\Storage\Database\DatabaseConnectionData; +use SP\Test\DatabaseTestCase; +use function SP\Test\setupContext; + +/** + * Class PluginServiceTest + * + * @package SP\Tests\Services\Plugin + */ +class PluginServiceTest extends DatabaseTestCase +{ + /** + * @var PluginService + */ + private static $service; + + /** + * @throws \DI\NotFoundException + * @throws \SP\Core\Context\ContextException + * @throws \DI\DependencyException + */ + public static function setUpBeforeClass() + { + $dic = setupContext(); + + self::$dataset = 'syspass_plugin.xml'; + + // Datos de conexión a la BBDD + self::$databaseConnectionData = $dic->get(DatabaseConnectionData::class); + + // Inicializar el repositorio + self::$service = $dic->get(PluginService::class); + } + + /** + * @throws ConstraintException + * @throws NoSuchItemException + * @throws \SP\Core\Exceptions\QueryException + */ + public function testUpdate() + { + $data = new PluginData(); + $data->setId(1); + $data->setName('Authenticator 2'); + $data->setAvailable(1); + $data->setEnabled(1); + $data->setData('data'); + + $this->assertEquals(1, self::$service->update($data)); + + $result = self::$service->getById(1); + + $this->assertEquals($data, $result); + + $data->setId(null); + $data->setName('Authenticator'); + + $this->assertEquals(0, self::$service->update($data)); + + $data->setId(2); + $data->setName('DokuWiki'); + + $this->expectException(ConstraintException::class); + + self::$service->update($data); + } + + /** + * @throws ConstraintException + * @throws \SP\Core\Exceptions\QueryException + * @throws \SP\Core\Exceptions\SPException + */ + public function testDeleteByIdBatch() + { + self::$service->deleteByIdBatch([1, 2]); + + $this->assertEquals(1, $this->conn->getRowCount('Plugin')); + + $this->expectException(ServiceException::class); + + self::$service->deleteByIdBatch([4]); + } + + /** + * @throws ConstraintException + * @throws \SP\Core\Exceptions\QueryException + * @throws \SP\Repositories\NoSuchItemException + */ + public function testToggleAvailable() + { + self::$service->toggleAvailable(1, 0); + + $data = self::$service->getById(1); + + $this->assertEquals(0, $data->getAvailable()); + + $this->expectException(NoSuchItemException::class); + + self::$service->toggleAvailable(4, 1); + } + + /** + * @throws ConstraintException + * @throws \SP\Core\Exceptions\QueryException + * @throws NoSuchItemException + */ + public function testResetById() + { + $this->assertEquals(1, self::$service->resetById(2)); + + $data = self::$service->getById(2); + + $this->assertNull($data->getData()); + + $this->expectException(NoSuchItemException::class); + + self::$service->resetById(4); + } + + /** + * @throws ConstraintException + * @throws NoSuchItemException + * @throws \SP\Core\Exceptions\QueryException + */ + public function testGetByName() + { + $data = self::$service->getByName('Authenticator'); + + $this->assertInstanceOf(PluginData::class, $data); + $this->assertEquals(1, $data->getId()); + $this->assertEquals('Authenticator', $data->getName()); + $this->assertNull($data->getData()); + $this->assertEquals(1, $data->getAvailable()); + $this->assertEquals(0, $data->getEnabled()); + + $this->expectException(NoSuchItemException::class); + + self::$service->getByName('Authenticator 2'); + } + + /** + * @throws ConstraintException + * @throws \SP\Core\Exceptions\QueryException + * @throws \SP\Core\Exceptions\SPException + */ + public function testDelete() + { + self::$service->delete(1); + + $this->assertEquals(2, $this->conn->getRowCount('Plugin')); + + $this->expectException(NoSuchItemException::class); + + self::$service->getById(1); + } + + /** + * @throws ConstraintException + * @throws \SP\Core\Exceptions\QueryException + */ + public function testSearch() + { + $itemSearchData = new ItemSearchData(); + $itemSearchData->setLimitCount(10); + $itemSearchData->setSeachString('Auth'); + + $result = self::$service->search($itemSearchData); + + $this->assertEquals(1, $result->getNumRows()); + + /** @var PluginData[] $data */ + $data = $result->getDataAsArray(); + + $this->assertCount(1, $data); + $this->assertEquals(1, $data[0]->getId()); + $this->assertEquals('Authenticator', $data[0]->getName()); + $this->assertEquals(0, $data[0]->getEnabled()); + $this->assertEquals(1, $data[0]->getAvailable()); + + $itemSearchData->setSeachString('test'); + + $result = self::$service->search($itemSearchData); + $this->assertEquals(0, $result->getNumRows()); + + $itemSearchData->setSeachString(''); + + $result = self::$service->search($itemSearchData); + $this->assertEquals(3, $result->getNumRows()); + } + + /** + * @throws ConstraintException + * @throws NoSuchItemException + * @throws \SP\Core\Exceptions\QueryException + */ + public function testGetById() + { + $data = self::$service->getById(1); + + $this->assertInstanceOf(PluginData::class, $data); + $this->assertEquals(1, $data->getId()); + $this->assertEquals('Authenticator', $data->getName()); + $this->assertNull($data->getData()); + $this->assertEquals(1, $data->getAvailable()); + $this->assertEquals(0, $data->getEnabled()); + + $this->expectException(NoSuchItemException::class); + + self::$service->getById(4); + } + + /** + * @throws ConstraintException + * @throws NoSuchItemException + * @throws \SP\Core\Exceptions\QueryException + */ + public function testCreate() + { + $data = new PluginData(); + $data->setId(4); + $data->setName('Authenticator 2'); + $data->setAvailable(1); + $data->setEnabled(1); + $data->setData('data'); + + $this->assertEquals(4, self::$service->create($data)); + + $this->assertEquals($data, self::$service->getById(4)); + + $this->expectException(ConstraintException::class); + + self::$service->create($data); + } + + /** + * @throws ConstraintException + * @throws \SP\Core\Exceptions\QueryException + */ + public function testCreateBlank() + { + $this->expectException(ConstraintException::class); + + self::$service->create(new PluginData()); + } + + /** + * @throws ConstraintException + * @throws \SP\Core\Exceptions\QueryException + */ + public function testGetEnabled() + { + $data = self::$service->getEnabled(); + + $this->assertCount(2, $data); + $this->assertInstanceOf(ItemData::class, $data[0]); + $this->assertEquals(2, $data[0]->getId()); + $this->assertEquals('XML Exporter', $data[0]->getName()); + $this->assertInstanceOf(ItemData::class, $data[1]); + $this->assertEquals(3, $data[1]->getId()); + $this->assertEquals('DokuWiki', $data[1]->getName()); + } + + /** + * @throws ConstraintException + * @throws \SP\Core\Exceptions\QueryException + */ + public function testGetAll() + { + $data = self::$service->getAll(); + + $this->assertCount(3, $data); + $this->assertEquals(1, $data[0]->getId()); + $this->assertEquals('Authenticator', $data[0]->getName()); + $this->assertNull($data[0]->getData()); + $this->assertEquals(1, $data[0]->getAvailable()); + $this->assertEquals(0, $data[0]->getEnabled()); + + $this->assertEquals(3, $data[1]->getId()); + $this->assertEquals(2, $data[2]->getId()); + } + + /** + * @throws ConstraintException + * @throws NoSuchItemException + * @throws \SP\Core\Exceptions\QueryException + */ + public function testToggleEnabledByName() + { + self::$service->toggleEnabledByName('Authenticator', 1); + + $data = self::$service->getByName('Authenticator'); + + $this->assertEquals(1, $data->getEnabled()); + + $this->expectException(NoSuchItemException::class); + + self::$service->toggleEnabledByName('Test', 0); + } + + /** + * @throws ConstraintException + * @throws NoSuchItemException + * @throws \SP\Core\Exceptions\QueryException + */ + public function testToggleAvailableByName() + { + self::$service->toggleAvailableByName('Authenticator', 0); + + $data = self::$service->getByName('Authenticator'); + + $this->assertEquals(0, $data->getAvailable()); + + $this->expectException(NoSuchItemException::class); + + self::$service->toggleAvailableByName('Authenticator 2', 1); + } + + /** + * @throws ConstraintException + * @throws \SP\Core\Exceptions\QueryException + */ + public function testGetByIdBatch() + { + $data = self::$service->getByIdBatch([1, 2, 3]); + + $this->assertCount(3, $data); + $this->assertEquals(1, $data[0]->getId()); + $this->assertEquals(2, $data[1]->getId()); + $this->assertEquals(3, $data[2]->getId()); + + $this->assertCount(0, self::$service->getByIdBatch([4])); + } + + /** + * @throws ConstraintException + * @throws NoSuchItemException + * @throws \SP\Core\Exceptions\QueryException + */ + public function testToggleEnabled() + { + self::$service->toggleEnabled(1, 1); + + $data = self::$service->getById(1); + + $this->assertEquals(1, $data->getEnabled()); + + $this->expectException(NoSuchItemException::class); + + self::$service->toggleEnabled(4, 0); + } +} diff --git a/test/Services/PublicLink/PublicLinkServiceTest.php b/test/Services/PublicLink/PublicLinkServiceTest.php new file mode 100644 index 00000000..7ee3a040 --- /dev/null +++ b/test/Services/PublicLink/PublicLinkServiceTest.php @@ -0,0 +1,459 @@ +. + */ + +namespace SP\Tests\Services\PublicLink; + +use SP\Config\ConfigData; +use SP\Core\Crypt\Vault; +use SP\Core\Exceptions\ConstraintException; +use SP\DataModel\AccountExtData; +use SP\DataModel\ItemSearchData; +use SP\DataModel\PublicLinkData; +use SP\DataModel\PublicLinkListData; +use SP\Repositories\DuplicatedItemException; +use SP\Repositories\NoSuchItemException; +use SP\Services\PublicLink\PublicLinkService; +use SP\Services\ServiceException; +use SP\Storage\Database\DatabaseConnectionData; +use SP\Test\DatabaseTestCase; +use SP\Util\Util; +use function SP\Test\setupContext; + +/** + * Class PublicLinkServiceTest + * + * @package SP\Tests\Services\PublicLink + */ +class PublicLinkServiceTest extends DatabaseTestCase +{ + /** + * @var string + */ + private static $salt; + /** + * @var PublicLinkService + */ + private static $service; + + /** + * @throws \DI\NotFoundException + * @throws \SP\Core\Context\ContextException + * @throws \DI\DependencyException + */ + public static function setUpBeforeClass() + { + $dic = setupContext(); + + self::$dataset = 'syspass_publicLink.xml'; + + // Datos de conexión a la BBDD + self::$databaseConnectionData = $dic->get(DatabaseConnectionData::class); + + // Inicializar el repositorio + self::$service = $dic->get(PublicLinkService::class); + + self::$salt = $dic->get(ConfigData::class)->getPasswordSalt(); + } + + /** + * @throws \SP\Core\Exceptions\ConstraintException + * @throws \SP\Core\Exceptions\QueryException + */ + public function testGetAllBasic() + { + $data = self::$service->getAllBasic(); + + $this->assertInstanceOf(PublicLinkListData::class, $data[0]); + $this->assertEquals(2, $data[0]->getId()); + + $this->assertInstanceOf(PublicLinkListData::class, $data[1]); + $this->assertEquals(3, $data[1]->getId()); + $this->assertEquals(2, $data[1]->getItemId()); + $this->assertEquals('ac744b6948823cb0514546c567981ce4fe7240df396826d99f24fd9a1344', $data[1]->getHash()); + $this->assertNotEmpty($data[1]->getData()); + $this->assertEquals(1, $data[1]->getUserId()); + $this->assertEquals(1, $data[1]->getTypeId()); + $this->assertEquals(0, $data[1]->isNotify()); + $this->assertEquals(1529276100, $data[1]->getDateAdd()); + $this->assertEquals(1532280828, $data[1]->getDateExpire()); + $this->assertEquals(0, $data[1]->getDateUpdate()); + $this->assertEquals(0, $data[1]->getCountViews()); + $this->assertEquals(3, $data[1]->getMaxCountViews()); + $this->assertEquals(0, $data[1]->getTotalCountViews()); + $this->assertNull($data[1]->getUseInfo()); + $this->assertEquals('Apple', $data[1]->getAccountName()); + $this->assertEquals('admin', $data[1]->getUserLogin()); + } + + /** + * @throws \SP\Core\Exceptions\SPException + */ + public function testGetByHash() + { + $hash = 'ced3400ea170619ad7d2589488b6b60747ea99f12e220f5a910ede6d834f'; + + $data = self::$service->getByHash($hash); + + $this->assertInstanceOf(PublicLinkData::class, $data); + $this->assertEquals(2, $data->getId()); + $this->assertEquals(1, $data->getItemId()); + $this->assertEquals($hash, $data->getHash()); + $this->assertNotEmpty($data->getData()); + $this->assertEquals(1, $data->getUserId()); + $this->assertEquals(1, $data->getTypeId()); + $this->assertEquals(0, $data->isNotify()); + $this->assertEquals(1529228863, $data->getDateAdd()); + $this->assertEquals(1532280825, $data->getDateExpire()); + $this->assertEquals(0, $data->getDateUpdate()); + $this->assertEquals(0, $data->getCountViews()); + $this->assertEquals(3, $data->getMaxCountViews()); + $this->assertEquals(0, $data->getTotalCountViews()); + $this->assertNull($data->getUseInfo()); + + $this->expectException(NoSuchItemException::class); + + self::$service->getByHash(''); + } + + /** + * @throws \SP\Core\Exceptions\ConstraintException + * @throws \SP\Core\Exceptions\QueryException + */ + public function testSearch() + { + $itemSearchData = new ItemSearchData(); + $itemSearchData->setLimitCount(10); + $itemSearchData->setSeachString('Google'); + + $result = self::$service->search($itemSearchData); + /** @var PublicLinkListData[] $data */ + $data = $result->getDataAsArray(); + + $this->assertEquals(1, $result->getNumRows()); + $this->assertCount(1, $data); + $this->assertInstanceOf(PublicLinkListData::class, $data[0]); + $this->assertEquals(2, $data[0]->getId()); + $this->assertEquals(1, $data[0]->getItemId()); + $this->assertEquals('ced3400ea170619ad7d2589488b6b60747ea99f12e220f5a910ede6d834f', $data[0]->getHash()); + $this->assertNotEmpty($data[0]->getData()); + $this->assertEquals(1, $data[0]->getUserId()); + $this->assertEquals(1, $data[0]->getTypeId()); + $this->assertEquals(0, $data[0]->isNotify()); + $this->assertEquals(1529228863, $data[0]->getDateAdd()); + $this->assertEquals(1532280825, $data[0]->getDateExpire()); + $this->assertEquals(0, $data[0]->getDateUpdate()); + $this->assertEquals(0, $data[0]->getCountViews()); + $this->assertEquals(3, $data[0]->getMaxCountViews()); + $this->assertEquals(0, $data[0]->getTotalCountViews()); + $this->assertNull($data[0]->getUseInfo()); + $this->assertEquals('Google', $data[0]->getAccountName()); + $this->assertEquals('admin', $data[0]->getUserLogin()); + + $itemSearchData->setSeachString('Apple'); + + $result = self::$service->search($itemSearchData); + /** @var PublicLinkListData[] $data */ + $data = $result->getDataAsArray(); + + $this->assertEquals(1, $result->getNumRows()); + $this->assertCount(1, $data); + $this->assertInstanceOf(PublicLinkListData::class, $data[0]); + $this->assertEquals(3, $data[0]->getId()); + $this->assertEquals(2, $data[0]->getItemId()); + + $itemSearchData->setSeachString(''); + + $result = self::$service->search($itemSearchData); + + $this->assertEquals(2, $result->getNumRows()); + } + + /** + * @throws NoSuchItemException + * @throws \SP\Core\Exceptions\ConstraintException + * @throws \SP\Core\Exceptions\QueryException + */ + public function testGetHashForItem() + { + $data = self::$service->getHashForItem(2); + + $this->assertEquals(3, $data->getId()); + $this->assertEquals('ac744b6948823cb0514546c567981ce4fe7240df396826d99f24fd9a1344', $data->getHash()); + + $this->expectException(NoSuchItemException::class); + + self::$service->getHashForItem(3); + } + + /** + * @throws \Defuse\Crypto\Exception\CryptoException + * @throws \SP\Core\Exceptions\ConstraintException + * @throws \SP\Core\Exceptions\QueryException + * @throws \SP\Core\Exceptions\SPException + */ + public function testCreate() + { + self::$service->delete(2); + + $data = new PublicLinkData(); + $data->setItemId(1); + $data->setHash(Util::generateRandomBytes()); + $data->setUserId(1); + $data->setTypeId(1); + $data->setNotify(1); + $data->setDateExpire(time() + 600); + $data->setDateAdd(time()); + $data->setMaxCountViews(3); + + $this->assertEquals(4, self::$service->create($data)); + + /** @var PublicLinkListData $resultData */ + $resultData = self::$service->getById(4); + + $this->assertEquals(4, $resultData->getId()); + $this->assertEquals($data->getItemId(), $resultData->getItemId()); + $this->assertEquals($data->getHash(), $resultData->getHash()); + $this->assertEquals($data->getUserId(), $resultData->getUserId()); + $this->assertEquals($data->getTypeId(), $resultData->getTypeId()); + $this->assertEquals($data->isNotify(), $resultData->isNotify()); + $this->assertEquals($data->getDateExpire(), $resultData->getDateExpire()); + $this->assertEquals($data->getMaxCountViews(), $resultData->getMaxCountViews()); + + $this->checkVaultData($resultData); + + $this->expectException(DuplicatedItemException::class); + + self::$service->create($data); + } + + /** + * @param PublicLinkListData $data + * + * @throws \Defuse\Crypto\Exception\CryptoException + */ + private function checkVaultData(PublicLinkListData $data) + { + $this->assertNotEmpty($data->getData()); + + /** @var Vault $vault */ + $vault = Util::unserialize(Vault::class, $data->getData()); + + $this->assertInstanceOf(Vault::class, $vault); + + /** @var AccountExtData $accountData */ + $accountData = Util::unserialize(AccountExtData::class, $vault->getData(self::$service->getPublicLinkKey($data->getHash())->getKey())); + $this->assertInstanceOf(AccountExtData::class, $accountData); + $this->assertEquals($data->getItemId(), $accountData->getId()); + } + + /** + * @throws \Defuse\Crypto\Exception\CryptoException + * @throws \SP\Core\Exceptions\ConstraintException + * @throws \SP\Core\Exceptions\QueryException + * @throws \SP\Core\Exceptions\SPException + */ + public function testCreateSameItemId() + { + $data = new PublicLinkData(); + $data->setItemId(2); + $data->setHash(Util::generateRandomBytes()); + $data->setData('data'); + $data->setUserId(1); + $data->setTypeId(1); + $data->setNotify(1); + $data->setDateExpire(time() + 600); + $data->setDateAdd(time()); + $data->setMaxCountViews(3); + + $this->expectException(DuplicatedItemException::class); + + self::$service->create($data); + } + + /** + * @throws ConstraintException + * @throws \SP\Core\Exceptions\QueryException + * @throws \SP\Core\Exceptions\SPException + */ + public function testDelete() + { + self::$service->delete(2); + self::$service->delete(3); + + $this->assertEquals(0, $this->conn->getRowCount('PublicLink')); + + $this->expectException(NoSuchItemException::class); + + $this->assertEquals(0, self::$service->delete(10)); + } + + /** + * @throws ConstraintException + * @throws \Defuse\Crypto\Exception\CryptoException + * @throws \Defuse\Crypto\Exception\EnvironmentIsBrokenException + * @throws \SP\Core\Exceptions\QueryException + * @throws \SP\Core\Exceptions\SPException + */ + public function testRefresh() + { + $this->assertEquals(1, self::$service->refresh(2)); + + /** @var PublicLinkListData $data */ + $data = self::$service->getById(2); + + $this->checkVaultData($data); + + $this->expectException(NoSuchItemException::class); + + self::$service->refresh(4); + } + + /** + * @throws ConstraintException + * @throws ServiceException + * @throws \SP\Core\Exceptions\QueryException + */ + public function testDeleteByIdBatch() + { + $this->assertEquals(2, self::$service->deleteByIdBatch([2, 3])); + + $this->assertEquals(0, $this->conn->getRowCount('PublicLink')); + + $this->expectException(ServiceException::class); + + self::$service->deleteByIdBatch([10]); + } + + /** + * @throws ConstraintException + * @throws \SP\Core\Exceptions\QueryException + * @throws \SP\Core\Exceptions\SPException + */ + public function testAddLinkView() + { + $hash = 'ac744b6948823cb0514546c567981ce4fe7240df396826d99f24fd9a1344'; + + $useInfo[] = [ + 'who' => SELF_IP_ADDRESS, + 'time' => time(), + 'hash' => $hash, + 'agent' => 'Mozilla/Firefox', + 'https' => true + ]; + + $data = new PublicLinkData(); + $data->setHash($hash); + $data->setUseInfo($useInfo); + + self::$service->addLinkView($data); + + /** @var PublicLinkData $resultData */ + $resultData = self::$service->getByHash($hash); + + $this->assertEquals(1, $resultData->getCountViews()); + $this->assertEquals(1, $resultData->getTotalCountViews()); + $this->assertCount(2, unserialize($resultData->getUseInfo())); + + $this->expectException(NoSuchItemException::class); + + $data->setHash('123'); + + self::$service->addLinkView($data); + } + + /** + * @throws ConstraintException + * @throws \Defuse\Crypto\Exception\EnvironmentIsBrokenException + * @throws \SP\Core\Exceptions\QueryException + * @throws \SP\Core\Exceptions\SPException + */ + public function testUpdate() + { + $data = new PublicLinkData(); + $data->setId(3); + $data->setItemId(2); + $data->setHash(Util::generateRandomBytes()); + $data->setData('data'); + $data->setUserId(2); + $data->setTypeId(1); + $data->setNotify(0); + $data->setDateExpire(time() + 3600); + $data->setDateAdd(time()); + $data->setMaxCountViews(6); + + $this->assertEquals(1, self::$service->update($data)); + + /** @var PublicLinkListData $resultData */ + $resultData = self::$service->getById(3); + + $this->assertEquals(3, $resultData->getId()); + $this->assertEquals($data->getItemId(), $resultData->getItemId()); + $this->assertEquals($data->getHash(), $resultData->getHash()); + $this->assertEquals($data->getData(), $resultData->getData()); + $this->assertEquals($data->getUserId(), $resultData->getUserId()); + $this->assertEquals($data->getTypeId(), $resultData->getTypeId()); + $this->assertEquals($data->isNotify(), $resultData->isNotify()); + $this->assertEquals($data->getDateExpire(), $resultData->getDateExpire()); + $this->assertEquals($data->getDateAdd(), $resultData->getDateAdd()); + $this->assertEquals($data->getMaxCountViews(), $resultData->getMaxCountViews()); + + $this->expectException(ConstraintException::class); + + $data->setItemId(1); + + self::$service->update($data); + } + + /** + * @throws \SP\Core\Exceptions\SPException + * @throws \Defuse\Crypto\Exception\CryptoException + */ + public function testGetById() + { + $data = self::$service->getById(2); + + $this->assertInstanceOf(PublicLinkListData::class, $data); + $this->assertEquals(2, $data->getId()); + $this->assertEquals(1, $data->getItemId()); + $this->assertEquals('ced3400ea170619ad7d2589488b6b60747ea99f12e220f5a910ede6d834f', $data->getHash()); + $this->assertEquals(1, $data->getUserId()); + $this->assertEquals(1, $data->getTypeId()); + $this->assertEquals(0, $data->isNotify()); + $this->assertEquals(1529228863, $data->getDateAdd()); + $this->assertEquals(1532280825, $data->getDateExpire()); + $this->assertEquals(0, $data->getDateUpdate()); + $this->assertEquals(0, $data->getCountViews()); + $this->assertEquals(3, $data->getMaxCountViews()); + $this->assertEquals(0, $data->getTotalCountViews()); + $this->assertNull($data->getUseInfo()); + $this->assertEquals('Google', $data->getAccountName()); + $this->assertEquals('admin', $data->getUserLogin()); + + $this->checkVaultData($data); + + $this->expectException(NoSuchItemException::class); + + self::$service->getById(10); + } +} diff --git a/test/res/datasets/syspass_publicLink.xml b/test/res/datasets/syspass_publicLink.xml new file mode 100644 index 00000000..6776ee15 --- /dev/null +++ b/test/res/datasets/syspass_publicLink.xml @@ -0,0 +1,39 @@ + + + + + + 2 + 1 + 636564333430306561313730363139616437643235383934383862366236303734376561393966313265323230663561393130656465366438333466 + 4F3A31393A2253505C436F72655C43727970745C5661756C74223A343A7B733A32353A220053505C436F72655C43727970745C5661756C740064617461223B733A313833303A22646566353032303036636430326564663336666265623963383631616264643662396131373061376432303631323836353435303031666230396664636565663665633264313438623663336334373134643337616239333665353738613763393365333035613663623763396665383438646335626439656630303034323665346631663932653831323330623765616132323564306438303336613139363330656361613561336232626530333236336435353564363234323261323539346234306637623864623863396662316361376232616637313865626338316636356635633432333965663439626563653961643334363861353961613665303336613036663836383261343038376562323261383536373464626264613531623533633738633166333366616161663765383866366561626464666431626636643564653932656138616139376130633466313035373166653637303235313237653139333832623562643835666264366438393235383064653934373832653532343264653338626564613439383063616337306530306335353263343338616166663866343335373665656333356564386561356139643263643736616663316637363233633732323065653663303337623766343264626234393134653532313732373337623936373039333866396230303762353933356637613666393562306332366237663535643132393234316630616339373736356161323134376239363931666666636337356332643734616434323534323030646463346132353535323562356634633866626261363838396233383265366539333462633864363165363036323865366235313864346536613662613837303630626432626465363839366462623431333438313333613039663632363333343365613061353937333739613530393665316263616637356236633365656366326631306230653438376434346333346562323061303133633832656431373662613762316661653465386138313336636131336363363231613538326532303736306166623965616638363262373633373136626330363135303062323766306230626339623436373665393934623666623934356134626439323734393137316265646662306563636166666432646134666137386163376533663036306539313163613366616132616636396338336135363732616237636662636563643162653561336131323134336630346364353061316632383334346535646439396463613862343433383564363362653066393834333236373664313539383761313334313661363637363932333136343733303837663437396532316330633635363164333832623463613830313232613830313035326538643730313336616432353231363035323735333865306666646433353130643337323064656334653631363931666232656635383466636237353736633336303432656566343732633437383431306339393166373964623331353865316232396333633830346462353932323362373534316233376638636430306531393565333332663937306531303038326130613638353236333362373763316133626161623632623332616339646636646138623962333038616137333866616533353765376630356430343963333335653961376533356338623531633863343964653937373031653532323931616135633637356433363134343936326462646437363731656134643037343237646235386366303664666630333964313338643135343732326164313734663865363034663539306562346361633362313263636436626430303261323561653139363234363763386266393733376639666139643031303862306134303439666663633963666562303766323236303838363432313761366339383637633564363331353932616635383135313162393961643335316335333763346262663734383666613964663362633432666565383664393931613233623635306664383837386535346164393932356565653738633638373035323030643861313566383038393331323832666538316634326139326235373933626163353362663462326636316234373238366434343731646662393363383532613534626366633739663264373266376336383064656431663436313431656335366533343034396665636464303538613937613465376664303964616662373632613663656539636637386439343937303434393561333838306233353134386636363238636533363437306337656635613937346335343833656132383963633166333966363635383663373036653032363166626564653763376636616437303234623462663139623333666362653066333763376537363139616637303235336533396163636461353135306161613832326438323238636531336465386432383462326364323466353131643032303435343737663335316632373734646537223B733A32343A220053505C436F72655C43727970745C5661756C74006B6579223B733A3531323A226465663130303030646566353032303037633239663335326261346335623130653135643930386262303734376632373934386433323831306538306266396232303965663835366135303436353762323533393036333834343335623236373537653565376464613662323434303038363131643333396330333266346139363632373765643139363831363330623731373465366232643436376664313137393063343234373634313766656530333338636233303562396431306333653964663733356664633935393366633463653938623137633961373138343337353338326665633335343965326233373232616237386365363537323866323162343336656136326266366437333033656339336432663735316335303661623162306338636238323139656564376435626133363666643961333833383930326333333931626536373436373638613063333137663865363066343463646139653634643737323833373836663664653565653531323932396463313033613131346565343563643363393262323962343463613738363832353264336661323135663961353263303732393034326366333231633231356634633737613733653331396365383835633738333565326332353363656532663736346138393037353035313235393232663130616166623866626666356562633161376332223B733A32383A220053505C436F72655C43727970745C5661756C740074696D65536574223B693A313533323238303232353B733A33323A220053505C436F72655C43727970745C5661756C740074696D6555706461746564223B693A303B7D + 1 + 1 + 0 + 1529228863 + 1532280825 + 0 + 0 + 0 + 3 + + + + 3 + 2 + 616337343462363934383832336362303531343534366335363739383163653466653732343064663339363832366439396632346664396131333434 + 4F3A31393A2253505C436F72655C43727970745C5661756C74223A343A7B733A32353A220053505C436F72655C43727970745C5661756C740064617461223B733A313834343A226465663530323030376339353039326234633263383533353734663131343039373138363831386435373135656137663734643534393632356136313032616538623262613339636133616365353234656361643466316431616130306262653965643063316331666236303732643062343733333631363637346235646336313231356333666436623335353961666633666639383761353934323966633963313637373462336235613938363265653761636135653137646536313635306132343533653632613261393362613838353764663436656261353138363363663436386336373434643363303633353738643930363965306634313533646163616131376463633465383335626138666430613563363131393734393665306261336264373139633165343664616666636137313430643434326263663965303564663639393434373765353566363536376666366138633265656639313863623863323930653063613762656337376237376335306337303530396638633436653465356130363933663438306137313639393663393331613539353636363663343466633837393862336536323939616638353966623736353233316566363837656665316530316635623065616237636138363465323637366134663533643032313565343731303338353435356634383463366332636663303336386262333330353631666166343162366433333034373533326565303930653434323833386337613835373035633333383136616363376139623433313237363064623332623638363038666432313138616161666631623366323865356635643061663030636532633564633930366662623337626331333863653238656631383262656664356461623037363265623062343932383030363336303263306237333264656364343538386536326331353636353437383037663761343132626532646663633930623638333461336364376165616237363239376364363162366130346436383665303537373936623538626664356361396666333539323939643464333961646339646535393765313839363239343766623366656361376631356365656634343862336335393938623964323032343463393032656263383362646439306637353366653337636638326137613365303664613366646532616138663865613462316535356331393436653861626634306165663936333566313166323264303064626133373662656134333963346532333238303837303165336431346263323761633431623138303966643732626462316638346533343930366233386665316433306461653831623832656133326632316532383663653233373530623430353765643965303463376134623762666261393537333733623236646336663063633939366130396131386538626234343736656263666663616666326437623164326230643466653166323264346262663366373537646631613664393162396564303732313836656138366633346665343238393731663661373732396330353366303266646531343531376166356361386130663233316335383435636564303430306136313134623462353936393564386132623730336562646237643731333539326137356634396532663231326163386231343232646232336464333361373337303362396437363837373830653132643365316439313235313434623062326334613865633237326334373161643763373364356166623735363661663034386130613332633632346163383238336131626466313434363661663538306634346236643934326266643837333663653663323139306431373439643032376533346264356231616231313762353037633237633833353434613661303039326364353039363535323865663964306234386262626138383038313436336666313163303036373932396335363736353435663937616636363039323232373262313762343938313836383530623565376638663230353235383363306235353430313639303165353832613066643164653635336233373035313663653564646330376261313632366335383639306362663832633530346335306635353430373333616164333033656432656133313538656661633966393538316238633166386436396133316631616537616339623337393539363633323834613233316639616633333439623939643630373733383731396561636563326335383466353635383031656532393737653631336437386135636561633036353661346663343133393236613139376430323463303639633762613732366435623938633638343336323765386337373939653961613564333736333035666239303738346363663764643536636632323132386534346537666565333762633461343139326235666332646437386261643336333736313739616335303163343335613336373336393261393531306163326137343961323065663636316564353833223B733A32343A220053505C436F72655C43727970745C5661756C74006B6579223B733A3531323A226465663130303030646566353032303062336137653530303338396635343239663939656266376365366131636534666461656633666236613439366532333739653931643930393335616336353362663239326331353066353233643335326537643463373237643133363365303135396564316333656261386530363065343032646561653838666432323363383433386663373438333436633237356637663536626636306363323033666666373466346665643533623738336662323264376438653838343431303539636332653430616538633066616433326138333534623937396364333439323634336563366130303438353061376135396564316638393364623762356664393865373432653532643630643134346139346332663865646665353834616136646432343831636632616632376662393662376633653437346265313439333363663364313837373230343931316163656438656665353239303361333336656336653138323263326131663539383539313133333830663735376263636432306266653362623631346631333535326535323235316530626436313864333838363030383633376331323262336231303631613061386166396465646138646630393331643463353464313162623632623834613962353534326261303961343938306336613831386230626131336237223B733A32383A220053505C436F72655C43727970745C5661756C740074696D65536574223B693A313533323238303232383B733A33323A220053505C436F72655C43727970745C5661756C740074696D6555706461746564223B693A303B7D + 1 + 1 + 0 + 1529276100 + 1532280828 + 0 + 0 + 0 + 3 + + + + +