chore: Use Aura.SqlQuery for building queries.

Refactor AccountRepository and AccountSearchRepository to use Aura.SqlQuery. This will allow to mock queries when testing.

Signed-off-by: Rubén D <nuxsmin@syspass.org>
This commit is contained in:
Rubén D
2022-11-05 09:58:57 +01:00
parent 08ce35a837
commit 8a2bbc8283
31 changed files with 1926 additions and 1592 deletions

View File

@@ -25,16 +25,36 @@
namespace SP\Modules\Api\Controllers\Account;
use Exception;
use Klein\Klein;
use SP\Core\Acl\Acl;
use SP\Core\Acl\ActionsInterface;
use SP\Domain\Account\Services\AccountSearchFilter;
use SP\Core\Application;
use SP\Domain\Account\AccountSearchServiceInterface;
use SP\Domain\Account\Search\AccountSearchConstants;
use SP\Domain\Account\Search\AccountSearchFilter;
use SP\Domain\Api\ApiServiceInterface;
use SP\Domain\Api\Services\ApiResponse;
use SP\Mvc\Model\QueryCondition;
use SP\Modules\Api\Controllers\ControllerBase;
/**
* Class SearchController
*/
final class SearchController extends AccountBase
final class SearchController extends ControllerBase
{
private AccountSearchServiceInterface $accountSearchService;
public function __construct(
Application $application,
Klein $router,
ApiServiceInterface $apiService,
Acl $acl,
AccountSearchServiceInterface $accountSearchService
) {
parent::__construct($application, $router, $apiService, $acl);
$this->accountSearchService = $accountSearchService;
}
/**
* searchAction
*/
@@ -46,7 +66,9 @@ final class SearchController extends AccountBase
$accountSearchFilter = $this->buildAccountSearchFilter();
$this->returnResponse(
ApiResponse::makeSuccess($this->accountService->getByFilter($accountSearchFilter)->getDataAsArray())
ApiResponse::makeSuccess(
$this->accountSearchService->getByFilter($accountSearchFilter)->getDataAsArray()
)
);
} catch (Exception $e) {
processException($e);
@@ -56,40 +78,33 @@ final class SearchController extends AccountBase
}
/**
* @return \SP\Domain\Account\Services\AccountSearchFilter
* @return \SP\Domain\Account\Search\AccountSearchFilter
* @throws \SP\Domain\Common\Services\ServiceException
*/
private function buildAccountSearchFilter(): AccountSearchFilter
{
$accountSearchFilter = new AccountSearchFilter();
$accountSearchFilter->setCleanTxtSearch($this->apiService->getParamString('text'));
$accountSearchFilter->setCategoryId($this->apiService->getParamInt('categoryId'));
$accountSearchFilter->setClientId($this->apiService->getParamInt('clientId'));
$filter = AccountSearchFilter::build($this->apiService->getParamString('text'))
->setCategoryId($this->apiService->getParamInt('categoryId'))
->setClientId($this->apiService->getParamInt('clientId'))
->setTagsId(array_map('intval', $this->apiService->getParamArray('tagsId', false, [])))
->setLimitCount($this->apiService->getParamInt('count', false, 50))
->setSortOrder(
$this->apiService->getParamInt('order', false, AccountSearchConstants::SORT_DEFAULT)
);
$tagsId = array_map('intval', $this->apiService->getParamArray('tagsId', false, []));
if (count($tagsId) !== 0) {
$accountSearchFilter->setTagsId($tagsId);
}
$op = $this->apiService->getParamString('op');
$op = $this->apiService->getParamString('op', false, AccountSearchConstants::FILTER_CHAIN_AND);
if ($op !== null) {
switch ($op) {
case 'and':
$accountSearchFilter->setFilterOperator(QueryCondition::CONDITION_AND);
case AccountSearchConstants::FILTER_CHAIN_AND:
$filter->setFilterOperator(AccountSearchConstants::FILTER_CHAIN_AND);
break;
case 'or':
$accountSearchFilter->setFilterOperator(QueryCondition::CONDITION_OR);
case AccountSearchConstants::FILTER_CHAIN_OR:
$filter->setFilterOperator(AccountSearchConstants::FILTER_CHAIN_OR);
break;
}
}
$accountSearchFilter->setLimitCount($this->apiService->getParamInt('count', false, 50));
$accountSearchFilter->setSortOrder(
$this->apiService->getParamInt('order', false, AccountSearchFilter::SORT_DEFAULT)
);
return $accountSearchFilter;
return $filter;
}
}

View File

@@ -28,7 +28,7 @@ use SP\Core\Acl\ActionsInterface;
use SP\Core\Application;
use SP\Domain\Account\AccountSearchServiceInterface;
use SP\Domain\Account\AccountServiceInterface;
use SP\Domain\Account\Services\AccountSearchFilter;
use SP\Domain\Account\Search\AccountSearchFilter;
use SP\Html\DataGrid\DataGridInterface;
use SP\Http\JsonResponse;
use SP\Modules\Web\Controllers\ControllerBase;
@@ -51,6 +51,10 @@ final class SearchController extends ControllerBase
private AccountSearchServiceInterface $accountSearchService;
private AccountGrid $accountGrid;
/**
* @throws \SP\Core\Exceptions\SessionTimeout
* @throws \SP\Domain\Auth\Services\AuthException
*/
public function __construct(
Application $application,
WebControllerHelper $webControllerHelper,
@@ -67,7 +71,6 @@ final class SearchController extends ControllerBase
/**
* @return bool
* @throws \JsonException
* @throws \SP\Core\Exceptions\ConstraintException
* @throws \SP\Core\Exceptions\QueryException
* @throws \SP\Core\Exceptions\SPException
@@ -100,19 +103,12 @@ final class SearchController extends ControllerBase
{
$itemSearchData = $this->getSearchData($this->configData->getAccountCount(), $this->request);
$filter = new AccountSearchFilter();
$filter->setLimitCount($itemSearchData->getLimitCount());
$filter->setLimitStart($itemSearchData->getLimitStart());
if (!empty($itemSearchData->getSeachString())) {
$filter->setStringFilters(
$this->accountSearchService->analyzeQueryFilters($itemSearchData->getSeachString())
);
$filter->setCleanTxtSearch($this->accountSearchService->getCleanString());
}
$filter = AccountSearchFilter::build($itemSearchData->getSeachString())
->setLimitCount($itemSearchData->getLimitCount())
->setLimitStart($itemSearchData->getLimitStart());
return $this->accountGrid->updatePager(
$this->accountGrid->getGrid($this->accountService->getByFilter($filter)),
$this->accountGrid->getGrid($this->accountSearchService->getByFilter($filter)),
$itemSearchData
);
}

View File

@@ -4,7 +4,7 @@
*
* @author nuxsmin
* @link https://syspass.org
* @copyright 2012-2021, Rubén Domínguez nuxsmin@$syspass.org
* @copyright 2012-2022, Rubén Domínguez nuxsmin@$syspass.org
*
* This file is part of sysPass.
*
@@ -35,7 +35,8 @@ use SP\Core\Exceptions\SPException;
use SP\DataModel\ProfileData;
use SP\DataModel\UserPreferencesData;
use SP\Domain\Account\AccountSearchServiceInterface;
use SP\Domain\Account\Services\AccountSearchFilter;
use SP\Domain\Account\Search\AccountSearchConstants;
use SP\Domain\Account\Search\AccountSearchFilter;
use SP\Domain\Account\Services\AccountSearchItem;
use SP\Domain\Category\CategoryServiceInterface;
use SP\Domain\Client\ClientServiceInterface;
@@ -178,7 +179,7 @@ final class AccountSearchHelper extends HelperBase
}
$dataGrid = $this->getGrid();
$dataGrid->getData()->setData($this->accountSearchService->processSearchResults($this->accountSearchFilter));
$dataGrid->getData()->setData($this->accountSearchService->getByFilter($this->accountSearchFilter));
$dataGrid->updatePager();
$dataGrid->setTime(round(getElapsedTime($this->queryTimeStart), 5));
@@ -275,35 +276,35 @@ final class AccountSearchHelper extends HelperBase
$gridSortCustomer = new DataGridSort();
$gridSortCustomer->setName(__('Client'))
->setTitle(__('Sort by Client'))
->setSortKey(AccountSearchFilter::SORT_CLIENT)
->setSortKey(AccountSearchConstants::SORT_CLIENT)
->setIconUp($icons->getIconUp())
->setIconDown($icons->getIconDown());
$gridSortName = new DataGridSort();
$gridSortName->setName(__('Name'))
->setTitle(__('Sort by Name'))
->setSortKey(AccountSearchFilter::SORT_NAME)
->setSortKey(AccountSearchConstants::SORT_NAME)
->setIconUp($icons->getIconUp())
->setIconDown($icons->getIconDown());
$gridSortCategory = new DataGridSort();
$gridSortCategory->setName(__('Category'))
->setTitle(__('Sort by Category'))
->setSortKey(AccountSearchFilter::SORT_CATEGORY)
->setSortKey(AccountSearchConstants::SORT_CATEGORY)
->setIconUp($icons->getIconUp())
->setIconDown($icons->getIconDown());
$gridSortLogin = new DataGridSort();
$gridSortLogin->setName(__('User'))
->setTitle(__('Sort by Username'))
->setSortKey(AccountSearchFilter::SORT_LOGIN)
->setSortKey(AccountSearchConstants::SORT_LOGIN)
->setIconUp($icons->getIconUp())
->setIconDown($icons->getIconDown());
$gridSortUrl = new DataGridSort();
$gridSortUrl->setName(__('URL / IP'))
->setTitle(__('Sort by URL / IP'))
->setSortKey(AccountSearchFilter::SORT_URL)
->setSortKey(AccountSearchConstants::SORT_URL)
->setIconUp($icons->getIconUp())
->setIconDown($icons->getIconDown());
@@ -350,7 +351,7 @@ final class AccountSearchHelper extends HelperBase
/**
* Set search filters
*
* @return AccountSearchFilter
* @return \SP\Domain\Account\Search\AccountSearchFilter
*/
private function getFilters(): AccountSearchFilter
{

View File

@@ -41,7 +41,8 @@
"league/fractal": "^0.19.2",
"symfony/console": "^v5.1.2",
"symfony/lock": "^v5.0",
"ocramius/proxy-manager": "~2.0"
"ocramius/proxy-manager": "~2.0",
"aura/sqlquery": "~2.8"
},
"require-dev": {
"roave/security-advisories": "dev-latest",

874
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -27,7 +27,7 @@ namespace SP\Core\Context;
use SP\Core\Crypt\Vault;
use SP\DataModel\Dto\AccountCache;
use SP\DataModel\ProfileData;
use SP\Domain\Account\Services\AccountSearchFilter;
use SP\Domain\Account\Search\AccountSearchFilter;
use SP\Domain\User\Services\UserLoginResponse;
/**
@@ -189,7 +189,7 @@ class SessionContext extends ContextBase
}
/**
* @param AccountSearchFilter $searchFilters
* @param \SP\Domain\Account\Search\AccountSearchFilter $searchFilters
*/
public function setSearchFilters(AccountSearchFilter $searchFilters): void
{

View File

@@ -24,6 +24,7 @@
namespace SP\Core\Definitions;
use Aura\SqlQuery\QueryFactory;
use Monolog\Logger;
use PHPMailer\PHPMailer\PHPMailer;
use Psr\Container\ContainerInterface;
@@ -188,6 +189,8 @@ final class CoreDefinitions
$c->get(NotificationHandler::class)
);
}),
QueryFactory::class => create(QueryFactory::class)
->constructor('mysql', QueryFactory::COMMON),
];
}
}

View File

@@ -34,6 +34,7 @@ use SP\DataModel\ItemData;
use SP\DataModel\ItemSearchData;
use SP\Domain\Account\Services\AccountPasswordRequest;
use SP\Domain\Common\Services\ServiceException;
use SP\Infrastructure\Common\Repositories\NoSuchItemException;
use SP\Infrastructure\Database\QueryResult;
/**
@@ -46,8 +47,7 @@ interface AccountHistoryServiceInterface
/**
* Returns the item for given id
*
* @throws SPException
* @throws SPException
* @throws NoSuchItemException
*/
public function getById(int $id): AccountHistoryData;

View File

@@ -28,9 +28,8 @@ namespace SP\Domain\Account;
use SP\Core\Exceptions\ConstraintException;
use SP\Core\Exceptions\QueryException;
use SP\Core\Exceptions\SPException;
use SP\Domain\Account\Services\AccountSearchFilter;
use SP\Domain\Account\Search\AccountSearchFilter;
use SP\Infrastructure\Database\QueryResult;
use SP\Mvc\Model\QueryCondition;
/**
* Class AccountSearchService para la gestión de búsquedas de cuentas
@@ -45,13 +44,13 @@ interface AccountSearchServiceInterface
* @throws QueryException
* @throws SPException
*/
public function processSearchResults(AccountSearchFilter $accountSearchFilter): QueryResult;
public function getByFilter(AccountSearchFilter $accountSearchFilter): QueryResult;
/**
* Analizar la cadena de consulta por eqituetas especiales y devolver un objeto
* QueryCondition con los filtros
*/
public function analyzeQueryFilters(string $string): QueryCondition;
public function analyzeQueryFilters(string $string): void;
public function getCleanString(): ?string;
}

View File

@@ -28,9 +28,10 @@ namespace SP\Domain\Account\In;
use SP\Core\Exceptions\ConstraintException;
use SP\Core\Exceptions\QueryException;
use SP\Core\Exceptions\SPException;
use SP\DataModel\AccountHistoryData;
use SP\Domain\Account\Search\AccountSearchFilter;
use SP\Domain\Account\Services\AccountPasswordRequest;
use SP\Domain\Account\Services\AccountRequest;
use SP\Domain\Account\Services\AccountSearchFilter;
use SP\Domain\Common\In\RepositoryInterface;
use SP\Domain\Common\Out\SimpleModel;
use SP\Infrastructure\Database\QueryResult;
@@ -53,22 +54,21 @@ interface AccountRepositoryInterface extends RepositoryInterface
/**
* @param int $id
* @param QueryCondition $queryCondition
*
* @return QueryResult
* @throws \SP\Core\Exceptions\ConstraintException
* @throws \SP\Core\Exceptions\QueryException
*/
public function getPasswordForId(int $id, QueryCondition $queryCondition): QueryResult;
public function getPasswordForId(int $id): QueryResult;
/**
* @param QueryCondition $queryCondition
* @param int $id
*
* @return QueryResult
* @throws ConstraintException
* @throws QueryException
* @throws \SP\Core\Exceptions\ConstraintException
* @throws \SP\Core\Exceptions\QueryException
*/
public function getPasswordHistoryForId(QueryCondition $queryCondition): QueryResult;
public function getPasswordHistoryForId(int $id): QueryResult;
/**
* Incrementa el contador de vista de clave de una cuenta en la BBDD
@@ -106,14 +106,14 @@ interface AccountRepositoryInterface extends RepositoryInterface
/**
* Restaurar una cuenta desde el histórico.
*
* @param int $historyId El Id del registro en el histórico
* @param \SP\DataModel\AccountHistoryData $accountHistoryData
* @param int $userId User's Id
*
* @return bool
* @throws ConstraintException
* @throws QueryException
* @throws \SP\Core\Exceptions\ConstraintException
* @throws \SP\Core\Exceptions\QueryException
*/
public function editRestore(int $historyId, int $userId): bool;
public function editRestore(AccountHistoryData $accountHistoryData, int $userId): bool;
/**
* Updates an item for bulk action
@@ -148,35 +148,22 @@ interface AccountRepositoryInterface extends RepositoryInterface
public function getDataForLink(int $id): QueryResult;
/**
* Obtener las cuentas de una búsqueda.
*
* @param AccountSearchFilter $accountSearchFilter
* @param QueryCondition $queryFilterUser
* @param int|null $accountId
*
* @return QueryResult
* @throws ConstraintException
* @throws QueryException
* @throws SPException
* @throws \SP\Core\Exceptions\ConstraintException
* @throws \SP\Core\Exceptions\QueryException
*/
public function getByFilter(AccountSearchFilter $accountSearchFilter, QueryCondition $queryFilterUser): QueryResult;
public function getForUser(?int $accountId = null): QueryResult;
/**
* @param QueryCondition $queryFilter
* @param int $accountId
*
* @return QueryResult
* @throws ConstraintException
* @throws QueryException
* @throws \SP\Core\Exceptions\ConstraintException
* @throws \SP\Core\Exceptions\QueryException
*/
public function getForUser(QueryCondition $queryFilter): QueryResult;
/**
* @param QueryCondition $queryFilter
*
* @return QueryResult
* @throws ConstraintException
* @throws QueryException
*/
public function getLinked(QueryCondition $queryFilter): QueryResult;
public function getLinked(int $accountId): QueryResult;
/**
* Obtener los datos relativos a la clave de todas las cuentas.

View File

@@ -0,0 +1,56 @@
<?php
/*
* sysPass
*
* @author nuxsmin
* @link https://syspass.org
* @copyright 2012-2022, Rubén Domínguez nuxsmin@$syspass.org
*
* This file is part of sysPass.
*
* sysPass is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* sysPass is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with sysPass. If not, see <http://www.gnu.org/licenses/>.
*/
namespace SP\Domain\Account\Search;
/**
* Interface AccountSearchConstants
*/
interface AccountSearchConstants
{
public const FILTER_OWNER = 'owner';
public const FILTER_MAIN_GROUP = 'mainGroup';
public const FILTER_CATEGORY_NAME = 'categoryName';
public const FILTER_FILE_NAME = 'fileName';
public const FILTER_ACCOUNT_ID = 'accountId';
public const FILTER_USER_NAME = 'userName';
public const FILTER_ACCOUNT_NAME_REGEX = 'accountNameRegex';
public const FILTER_CLIENT_NAME = 'clientName';
public const FILTER_GROUP_NAME = 'groupName';
public const FILTER_CHAIN_AND = 'and';
public const FILTER_CHAIN_OR = 'or';
public const FILTER_IS_PRIVATE = 'is:private';
public const FILTER_NOT_PRIVATE = 'not:private';
public const FILTER_IS_EXPIRED = 'is:expired';
public const FILTER_NOT_EXPIRED = 'is:expired';
public const SORT_DIR_ASC = 0;
public const SORT_DIR_DESC = 1;
public const SORT_CATEGORY = 2;
public const SORT_DEFAULT = 0;
public const SORT_LOGIN = 3;
public const SORT_URL = 4;
public const SORT_NAME = 1;
public const SORT_CLIENT = 5;
}

View File

@@ -22,7 +22,7 @@
* along with sysPass. If not, see <http://www.gnu.org/licenses/>.
*/
namespace SP\Domain\Account\Services;
namespace SP\Domain\Account\Search;
use SP\Mvc\Model\QueryCondition;
@@ -30,21 +30,19 @@ use SP\Mvc\Model\QueryCondition;
/**
* Class AccountSearchFilter
*
* @package SP\Account
* @package SP\Domain\Account\Filters
*/
final class AccountSearchFilter
{
/**
* Constantes de ordenación
* @param string $txtSearch
*
* @return \SP\Domain\Account\Search\AccountSearchFilter
*/
public const SORT_DIR_ASC = 0;
public const SORT_DIR_DESC = 1;
public const SORT_LOGIN = 3;
public const SORT_URL = 4;
public const SORT_CATEGORY = 2;
public const SORT_CLIENT = 5;
public const SORT_NAME = 1;
public const SORT_DEFAULT = 0;
public static function build(string $txtSearch): AccountSearchFilter
{
return (new self())->setTxtSearch($txtSearch);
}
/**
* @var int|null El número de registros de la última consulta
@@ -55,18 +53,17 @@ final class AccountSearchFilter
/**
* @var string|null Search string without special filters
*/
private ?string $cleanTxtSearch = null;
private ?int $clientId = null;
private ?int $categoryId = null;
private ?array $tagsId = null;
private int $sortOrder = self::SORT_DEFAULT;
private int $sortKey = self::SORT_DIR_ASC;
private int $limitStart = 0;
private ?int $limitCount = null;
private ?bool $sortViews = null;
private bool $searchFavorites = false;
private ?QueryCondition $stringFilters = null;
private ?string $filterOperator = null;
private ?string $cleanTxtSearch = null;
private ?int $clientId = null;
private ?int $categoryId = null;
private ?array $tagsId = null;
private int $sortOrder = AccountSearchConstants::SORT_DEFAULT;
private int $sortKey = AccountSearchConstants::SORT_DIR_ASC;
private int $limitStart = 0;
private ?int $limitCount = null;
private ?bool $sortViews = null;
private bool $searchFavorites = false;
private ?string $filterOperator = null;
public function isSearchFavorites(): bool
{
@@ -181,57 +178,6 @@ final class AccountSearchFilter
return null !== $this->tagsId && count($this->tagsId) !== 0;
}
public function getStringFilters(): QueryCondition
{
return $this->stringFilters ?? new QueryCondition();
}
public function setStringFilters(?QueryCondition $stringFilters): void
{
$this->stringFilters = $stringFilters;
}
/**
* Devuelve la cadena de ordenación de la consulta
*/
public function getOrderString(): string
{
switch ($this->sortKey) {
case self::SORT_NAME:
$orderKey[] = 'Account.name';
break;
case self::SORT_CATEGORY:
$orderKey[] = 'Account.categoryName';
break;
case self::SORT_LOGIN:
$orderKey[] = 'Account.login';
break;
case self::SORT_URL:
$orderKey[] = 'Account.url';
break;
case self::SORT_CLIENT:
$orderKey[] = 'Account.clientName';
break;
case self::SORT_DEFAULT:
default:
$orderKey[] = 'Account.clientName, Account.name';
break;
}
if ($this->isSortViews() && !$this->getSortKey()) {
array_unshift($orderKey, 'Account.countView DESC');
$this->setSortOrder(self::SORT_DIR_DESC);
}
$orderDir = $this->sortOrder === self::SORT_DIR_ASC ? 'ASC' : 'DESC';
return sprintf(
'%s %s',
implode(',', $orderKey),
$orderDir
);
}
public function isSortViews(): bool
{
return $this->sortViews ?? false;
@@ -292,7 +238,7 @@ final class AccountSearchFilter
$this->limitCount = null;
$this->sortViews = null;
$this->searchFavorites = false;
$this->sortOrder = self::SORT_DEFAULT;
$this->sortKey = self::SORT_DIR_ASC;
$this->sortOrder = AccountSearchConstants::SORT_DEFAULT;
$this->sortKey = AccountSearchConstants::SORT_DIR_ASC;
}
}

View File

@@ -0,0 +1,148 @@
<?php
/*
* sysPass
*
* @author nuxsmin
* @link https://syspass.org
* @copyright 2012-2022, Rubén Domínguez nuxsmin@$syspass.org
*
* This file is part of sysPass.
*
* sysPass is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* sysPass is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with sysPass. If not, see <http://www.gnu.org/licenses/>.
*/
namespace SP\Domain\Account\Search;
use SP\Util\Filter;
/**
* Class AccountSearchTokenizer
*/
final class AccountSearchTokenizer
{
private const SEARCH_REGEX = /** @lang RegExp */
'/(?<search>(?<!:)\b[^:]+\b(?!:))|(?<filter_subject>[a-zа-я_]+):(?!\s]*)"?(?<filter_condition>[^":]+)"?/u';
private const FILTERS = [
'condition' => [
'subject' => ['is', 'not'],
'condition' => ['expired', 'private'],
],
'items' => [
'subject' => [
'id' => AccountSearchConstants::FILTER_ACCOUNT_ID,
'user' => AccountSearchConstants::FILTER_USER_NAME,
'group' => AccountSearchConstants::FILTER_GROUP_NAME,
'file' => AccountSearchConstants::FILTER_FILE_NAME,
'owner' => AccountSearchConstants::FILTER_OWNER,
'maingroup' => AccountSearchConstants::FILTER_MAIN_GROUP,
'client' => AccountSearchConstants::FILTER_CLIENT_NAME,
'category' => AccountSearchConstants::FILTER_CATEGORY_NAME,
'name_regex' => AccountSearchConstants::FILTER_ACCOUNT_NAME_REGEX,
],
'condition' => null,
],
'operator' => [
'subject' => ['op'],
'condition' => [AccountSearchConstants::FILTER_CHAIN_AND, AccountSearchConstants::FILTER_CHAIN_OR],
],
];
/**
* @param string $search
*
* @return AccountSearchTokens|null
*/
public function tokenizeFrom(string $search): ?AccountSearchTokens
{
$match = preg_match_all(self::SEARCH_REGEX, $search, $filters);
if (empty($match)) {
return null;
}
$filtersAndValues = array_filter(array_combine($filters['filter_subject'], $filters['filter_condition']));
return new AccountSearchTokens(
Filter::safeSearchString(trim($filters['search'][0] ?? '')),
$this->getConditions($filtersAndValues),
$this->getItems($filtersAndValues),
$this->getOperator($filtersAndValues)[0],
);
}
/**
* @param array $filters
*
* @return array
*/
private function getConditions(array $filters): array
{
return array_filter(
array_map(
static function ($subject, $condition) {
if (in_array($subject, self::FILTERS['condition']['subject'], true)
&& in_array($condition, self::FILTERS['condition']['condition'], true)
) {
return sprintf("%s:%s", $subject, $condition);
}
return null;
},
$filters['filter_subject'],
$filters['filter_condition']
)
);
}
/**
* @param array $filtersAndValues
*
* @return array
*/
private function getItems(array $filtersAndValues): array
{
$items = array_filter(
$filtersAndValues,
static function ($value, $key) {
return in_array($key, array_keys(self::FILTERS['items']['subject']), true) && !empty($value);
},
ARRAY_FILTER_USE_BOTH
);
return array_combine(
array_map(function ($key) {
return self::FILTERS['items']['subject'][$key];
}, array_keys($items)),
array_values($items)
);
}
/**
* @param array $filtersAndValues
*
* @return array
*/
private function getOperator(array $filtersAndValues): array
{
return array_filter(
$filtersAndValues,
static function ($value, $key) {
return in_array($key, self::FILTERS['operator']['subject'], true)
&& in_array($value, self::FILTERS['operator']['condition'], true);
},
ARRAY_FILTER_USE_BOTH
);
}
}

View File

@@ -0,0 +1,70 @@
<?php
/*
* sysPass
*
* @author nuxsmin
* @link https://syspass.org
* @copyright 2012-2022, Rubén Domínguez nuxsmin@$syspass.org
*
* This file is part of sysPass.
*
* sysPass is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* sysPass is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with sysPass. If not, see <http://www.gnu.org/licenses/>.
*/
namespace SP\Domain\Account\Search;
/**
* Class AccountSearchTokens
*/
final class AccountSearchTokens
{
private string $search;
private array $conditions;
private array $items;
private string $operator;
/**
* @param string $search
* @param array $conditions
* @param array $items
* @param string $operator
*/
public function __construct(string $search, array $conditions, array $items, string $operator)
{
$this->search = $search;
$this->conditions = $conditions;
$this->items = $items;
$this->operator = $operator;
}
public function getConditions(): array
{
return $this->conditions;
}
public function getItems(): array
{
return $this->items;
}
public function getOperator(): string
{
return $this->operator;
}
public function getSearch(): string
{
return $this->search;
}
}

View File

@@ -24,128 +24,137 @@
namespace SP\Domain\Account\Services;
use Aura\SqlQuery\Common\SelectInterface;
use Aura\SqlQuery\QueryFactory;
use SP\Core\Context\ContextInterface;
use SP\DataModel\ProfileData;
use SP\Domain\Account\Search\AccountSearchConstants;
use SP\Domain\Config\In\ConfigDataInterface;
use SP\Domain\User\Services\UserLoginResponse;
use SP\Mvc\Model\QueryCondition;
defined('APP_ROOT') || die();
/**
* Class AccountUtil con utilidades para la gestión de cuentas
* Class AccountFilterUser
*/
final class AccountFilterUser
{
private ConfigDataInterface $configData;
private ContextInterface $context;
private ?ProfileData $userProfile = null;
private ?UserLoginResponse $userData = null;
private QueryFactory $queryFactory;
public function __construct(
ContextInterface $context,
ConfigDataInterface $configData
ConfigDataInterface $configData,
QueryFactory $queryFactory
) {
$this->context = $context;
$this->configData = $configData;
$this->queryFactory = $queryFactory;
}
/**
* Devuelve el filtro para la consulta SQL de cuentas que un usuario puede acceder
*/
public function getFilterHistory(bool $useGlobalSearch = false): QueryCondition
public function buildFilterHistory(bool $useGlobalSearch = false, ?SelectInterface $query = null): SelectInterface
{
$this->setUp();
$userData = $this->context->getUserData();
$userProfile = $this->context->getUserProfile();
$queryFilter = new QueryCondition();
if ($query === null) {
$query = $this->queryFactory->newSelect()->from('AccountHistory');
}
if (!$this->userData->getIsAdminApp()
&& !$this->userData->getIsAdminAcc()
&& !($this->configData->isGlobalSearch() && $useGlobalSearch && $this->userProfile->isAccGlobalSearch())
) {
// Filtro usuario y grupo
$filter = '(AccountHistory.userId = ?
OR AccountHistory.userGroupId = ?
OR AccountHistory.accountId IN (SELECT accountId AS accountId FROM AccountToUser WHERE accountId = AccountHistory.accountId AND userId = ? UNION ALL SELECT accountId FROM AccountToUserGroup WHERE accountId = AccountHistory.accountId AND userGroupId = ?)
OR AccountHistory.userGroupId IN (SELECT userGroupId FROM UserToUserGroup WHERE userGroupId = AccountHistory.userGroupId AND userId = ?))';
$params = [
$this->userData->getId(),
$this->userData->getUserGroupId(),
$this->userData->getId(),
$this->userData->getUserGroupId(),
$this->userData->getId(),
if ($this->isFilterByAdminAndGlobalSearch($userData, $useGlobalSearch, $userProfile)) {
$where = [
'AccountHistory.userId = :userId',
'AccountHistory.userGroupId = :userGroupId',
'AccountHistory.accountId IN (SELECT accountId AS accountId FROM AccountToUser WHERE accountId = AccountHistory.accountId AND userId = :userId UNION ALL SELECT accountId FROM AccountToUserGroup WHERE accountId = AccountHistory.accountId AND userGroupId = :userGroupId',
'AccountHistory.userGroupId IN (SELECT userGroupId FROM UserToUserGroup WHERE userGroupId = AccountHistory.userGroupId AND userId = :userId)',
];
if ($this->configData->isAccountFullGroupAccess()) {
// Filtro de grupos secundarios en grupos que incluyen al usuario
$filter .= PHP_EOL
.'OR AccountHistory.accountId = (SELECT accountId FROM AccountToUserGroup aug INNER JOIN UserToUserGroup uug ON uug.userGroupId = aug.userGroupId WHERE aug.accountId = AccountHistory.accountId AND uug.userId = ? LIMIT 1)';
$params[] = $this->userData->getId();
$where[] =
'AccountHistory.accountId = (SELECT accountId FROM AccountToUserGroup aug INNER JOIN UserToUserGroup uug ON uug.userGroupId = aug.userGroupId WHERE aug.accountId = AccountHistory.accountId AND uug.userId = :userId LIMIT 1)';
}
$queryFilter->addFilter($filter, $params);
$query->where(sprintf('(%s)', join(sprintf(' %s ', AccountSearchConstants::FILTER_CHAIN_OR), $where)));
}
$queryFilter->addFilter(
'(AccountHistory.isPrivate IS NULL OR AccountHistory.isPrivate = 0 OR (AccountHistory.isPrivate = 1 AND AccountHistory.userId = ?)) AND (AccountHistory.isPrivateGroup IS NULL OR AccountHistory.isPrivateGroup = 0 OR (AccountHistory.isPrivateGroup = 1 AND AccountHistory.userGroupId = ?))',
[$this->userData->getId(), $this->userData->getUserGroupId()]
$query->where(
'(AccountHistory.isPrivate IS NULL OR AccountHistory.isPrivate = 0 OR (AccountHistory.isPrivate = 1 AND AccountHistory.userId = :userId))'
);
$query->where(
'(AccountHistory.isPrivateGroup IS NULL OR AccountHistory.isPrivateGroup = 0 OR (AccountHistory.isPrivateGroup = 1 AND AccountHistory.userGroupId = :userGroupId))'
);
return $queryFilter;
}
$query->bindValues([
'userId' => $userData->getId(),
'userGroupId' => $userData->getUserGroupId(),
]);
/**
* setUp
*/
private function setUp(): void
{
$this->userData = $this->context->getUserData();
$this->userProfile = $this->context->getUserProfile();
return $query;
}
/**
* Devuelve el filtro para la consulta SQL de cuentas que un usuario puede acceder
*/
public function getFilter(bool $useGlobalSearch = false): QueryCondition
public function buildFilter(bool $useGlobalSearch = false, ?SelectInterface $query = null): SelectInterface
{
$this->setUp();
$userData = $this->context->getUserData();
$userProfile = $this->context->getUserProfile();
$queryFilter = new QueryCondition();
if ($query === null) {
$query = $this->queryFactory->newSelect()->from('Account');
}
if (!$this->userData->getIsAdminApp()
&& !$this->userData->getIsAdminAcc()
&& !($this->configData->isGlobalSearch() && $useGlobalSearch && $this->userProfile->isAccGlobalSearch())
) {
// Filtro usuario y grupo
$filter = '(Account.userId = ?
OR Account.userGroupId = ?
OR Account.id IN (SELECT accountId AS accountId FROM AccountToUser WHERE accountId = Account.id AND userId = ? UNION ALL SELECT accountId FROM AccountToUserGroup WHERE accountId = Account.id AND userGroupId = ?)
OR Account.userGroupId IN (SELECT userGroupId FROM UserToUserGroup WHERE userGroupId = Account.userGroupId AND userId = ?))';
$params = [
$this->userData->getId(),
$this->userData->getUserGroupId(),
$this->userData->getId(),
$this->userData->getUserGroupId(),
$this->userData->getId(),
if ($this->isFilterByAdminAndGlobalSearch($userData, $useGlobalSearch, $userProfile)) {
$where = [
'Account.userId = :userId',
'Account.userGroupId = :userGroupId',
'Account.id IN (SELECT accountId AS accountId FROM AccountToUser WHERE accountId = Account.id AND userId = :userId UNION ALL SELECT accountId FROM AccountToUserGroup WHERE accountId = Account.id AND userGroupId = :userGroupId)',
'Account.userGroupId IN (SELECT userGroupId FROM UserToUserGroup WHERE userGroupId = Account.userGroupId AND userId = :userId)',
];
if ($this->configData->isAccountFullGroupAccess()) {
// Filtro de grupos secundarios en grupos que incluyen al usuario
$filter .= PHP_EOL
.'OR Account.id = (SELECT accountId FROM AccountToUserGroup aug INNER JOIN UserToUserGroup uug ON uug.userGroupId = aug.userGroupId WHERE aug.accountId = Account.id AND uug.userId = ? LIMIT 1)';
$params[] = $this->userData->getId();
$where[] =
'Account.id = (SELECT accountId FROM AccountToUserGroup aug INNER JOIN UserToUserGroup uug ON uug.userGroupId = aug.userGroupId WHERE aug.accountId = Account.id AND uug.userId = :userId LIMIT 1)';
}
$queryFilter->addFilter($filter, $params);
$query->where(sprintf('(%s)', join(sprintf(' %s ', AccountSearchConstants::FILTER_CHAIN_OR), $where)));
}
$queryFilter->addFilter(
'(Account.isPrivate IS NULL OR Account.isPrivate = 0 OR (Account.isPrivate = 1 AND Account.userId = ?)) AND (Account.isPrivateGroup IS NULL OR Account.isPrivateGroup = 0 OR (Account.isPrivateGroup = 1 AND Account.userGroupId = ?))',
[$this->userData->getId(), $this->userData->getUserGroupId()]
$query->where(
'Account.isPrivate IS NULL OR Account.isPrivate = 0 OR (Account.isPrivate = 1 AND Account.userId = :userId)'
);
$query->where(
'Account.isPrivateGroup IS NULL OR Account.isPrivateGroup = 0 OR (Account.isPrivateGroup = 1 AND Account.userGroupId = :userGroupId)'
);
return $queryFilter;
$query->bindValues([
'userId' => $userData->getId(),
'userGroupId' => $userData->getUserGroupId(),
]);
return $query;
}
/**
* @param \SP\Domain\User\Services\UserLoginResponse $userData
* @param bool $useGlobalSearch
* @param \SP\DataModel\ProfileData|null $userProfile
*
* @return bool
*/
private function isFilterByAdminAndGlobalSearch(
UserLoginResponse $userData,
bool $useGlobalSearch,
?ProfileData $userProfile
): bool {
return !$userData->getIsAdminApp()
&& !$userData->getIsAdminAcc()
&& !($this->configData->isGlobalSearch() && $useGlobalSearch && $userProfile->isAccGlobalSearch());
}
}

View File

@@ -68,8 +68,7 @@ final class AccountHistoryService extends Service implements AccountHistoryServi
/**
* Returns the item for given id
*
* @throws SPException
* @throws SPException
* @throws NoSuchItemException
*/
public function getById(int $id): AccountHistoryData
{

View File

@@ -36,19 +36,21 @@ use SP\DataModel\Dto\AccountCache;
use SP\Domain\Account\AccountAclServiceInterface;
use SP\Domain\Account\AccountSearchServiceInterface;
use SP\Domain\Account\AccountToFavoriteServiceInterface;
use SP\Domain\Account\In\AccountRepositoryInterface;
use SP\Domain\Account\In\AccountToTagRepositoryInterface;
use SP\Domain\Account\In\AccountToUserGroupRepositoryInterface;
use SP\Domain\Account\In\AccountToUserRepositoryInterface;
use SP\Domain\Account\Search\AccountSearchConstants;
use SP\Domain\Account\Search\AccountSearchFilter;
use SP\Domain\Account\Search\AccountSearchTokenizer;
use SP\Domain\Common\Services\Service;
use SP\Domain\Config\In\ConfigDataInterface;
use SP\Domain\User\UserGroupServiceInterface;
use SP\Domain\User\UserServiceInterface;
use SP\Infrastructure\Account\Repositories\AccountSearchRepositoryInterface;
use SP\Infrastructure\Database\QueryResult;
use SP\Infrastructure\File\FileCache;
use SP\Infrastructure\File\FileCacheInterface;
use SP\Infrastructure\File\FileException;
use SP\Mvc\Model\QueryCondition;
use SP\Util\Filter;
defined('APP_ROOT') || die();
@@ -58,24 +60,6 @@ defined('APP_ROOT') || die();
*/
final class AccountSearchService extends Service implements AccountSearchServiceInterface
{
/**
* Regex filters for special searching
*/
private const FILTERS = [
'condition' => [
'subject' => ['is', 'not'],
'condition' => ['expired', 'private'],
],
'items' => [
'subject' => ['id', 'user', 'group', 'file', 'owner', 'maingroup', 'client', 'category', 'name_regex'],
'condition' => null,
],
'operator' => [
'subject' => ['op'],
'condition' => ['and', 'or'],
],
];
private const COLORS_CACHE_FILE = CACHE_PATH.DIRECTORY_SEPARATOR.'colors.cache';
/**
@@ -100,7 +84,6 @@ final class AccountSearchService extends Service implements AccountSearchService
'673AB7',
'3F51B5',
];
private AccountFilterUser $accountFilterUser;
private AccountAclServiceInterface $accountAclService;
private ConfigDataInterface $configData;
private AccountToTagRepositoryInterface $accountToTagRepository;
@@ -110,7 +93,7 @@ final class AccountSearchService extends Service implements AccountSearchService
private UserServiceInterface $userService;
private UserGroupServiceInterface $userGroupService;
private FileCacheInterface $colorCache;
private AccountRepositoryInterface $accountRepository;
private AccountSearchRepositoryInterface $accountSearchRepository;
private ?array $accountColor = null;
private ?string $cleanString = null;
private ?string $filterOperator = null;
@@ -124,8 +107,7 @@ final class AccountSearchService extends Service implements AccountSearchService
AccountToFavoriteServiceInterface $accountToFavoriteService,
UserServiceInterface $userService,
UserGroupServiceInterface $userGroupService,
AccountRepositoryInterface $accountRepository,
AccountFilterUser $accountFilterUser
AccountSearchRepositoryInterface $accountSearchRepository,
) {
parent::__construct($application);
$this->accountAclService = $accountAclService;
@@ -135,8 +117,7 @@ final class AccountSearchService extends Service implements AccountSearchService
$this->accountToTagRepository = $accountToTagRepository;
$this->accountToUserRepository = $accountToUserRepository;
$this->accountToUserGroupRepository = $accountToUserGroupRepository;
$this->accountRepository = $accountRepository;
$this->accountFilterUser = $accountFilterUser;
$this->accountSearchRepository = $accountSearchRepository;
// TODO: use IoC
$this->colorCache = new FileCache(self::COLORS_CACHE_FILE);
@@ -168,16 +149,13 @@ final class AccountSearchService extends Service implements AccountSearchService
* @throws QueryException
* @throws SPException
*/
public function processSearchResults(
AccountSearchFilter $accountSearchFilter
): QueryResult {
public function getByFilter(AccountSearchFilter $accountSearchFilter): QueryResult
{
if (!empty($accountSearchFilter->getTxtSearch())) {
$accountSearchFilter->setStringFilters($this->analyzeQueryFilters($accountSearchFilter->getTxtSearch()));
$this->analyzeQueryFilters($accountSearchFilter->getTxtSearch());
}
if ($this->filterOperator !== null
|| $accountSearchFilter->getFilterOperator() === null
) {
if ($this->filterOperator !== null || $accountSearchFilter->getFilterOperator() === null) {
$accountSearchFilter->setFilterOperator($this->filterOperator);
}
@@ -185,224 +163,65 @@ final class AccountSearchService extends Service implements AccountSearchService
$accountSearchFilter->setCleanTxtSearch($this->cleanString);
}
$queryResult = $this->accountRepository->getByFilter(
$accountSearchFilter,
$this->accountFilterUser->getFilter($accountSearchFilter->getGlobalSearch())
);
$queryResult = $this->accountSearchRepository->getByFilter($accountSearchFilter);
// Variables de configuración
$maxTextLength = $this->configData->isResultsAsCards() ? 40 : 60;
$accountLinkEnabled = $this->context->getUserData()->getPreferences()->isAccountLink()
|| $this->configData->isAccountLink();
$favorites = $this->accountToFavoriteService->getForUserId($this->context->getUserData()->getId());
$accountsData = [];
/** @var AccountSearchVData $accountSearchData */
foreach ($queryResult->getDataAsArray() as $accountSearchData) {
$cache = $this->getCacheForAccount($accountSearchData);
// Obtener la ACL de la cuenta
$accountAcl = $this->accountAclService->getAcl(
ActionsInterface::ACCOUNT_SEARCH,
AccountAclDto::makeFromAccountSearch(
$accountSearchData,
$cache->getUsers(),
$cache->getUserGroups()
)
);
// Propiedades de búsqueda de cada cuenta
$accountsSearchItem = new AccountSearchItem(
$accountSearchData,
$accountAcl,
$this->configData
);
if (!$accountSearchData->getIsPrivate()) {
$accountsSearchItem->setUsers($cache->getUsers());
$accountsSearchItem->setUserGroups($cache->getUserGroups());
}
$accountsSearchItem->setTags(
$this->accountToTagRepository
->getTagsByAccountId($accountSearchData->getId())
->getDataAsArray()
);
$accountsSearchItem->setTextMaxLength($maxTextLength);
$accountsSearchItem->setColor(
$this->pickAccountColor($accountSearchData->getClientId())
);
$accountsSearchItem->setLink($accountLinkEnabled);
$accountsSearchItem->setFavorite(
isset($favorites[$accountSearchData->getId()])
);
$accountsData[] = $accountsSearchItem;
}
return QueryResult::fromResults($accountsData, $queryResult->getTotalNumRows());
return QueryResult::fromResults($this->buildAccountsData($queryResult), $queryResult->getTotalNumRows());
}
/**
* Analizar la cadena de consulta por eqituetas especiales y devolver un objeto
* QueryCondition con los filtros
*/
public function analyzeQueryFilters(string $string): QueryCondition
public function analyzeQueryFilters(string $string): void
{
$this->cleanString = null;
$this->filterOperator = null;
$tokenizer = new AccountSearchTokenizer();
$tokens = $tokenizer->tokenizeFrom($string);
$queryCondition = new QueryCondition();
$this->cleanString = $tokens->getSearch();
$this->filterOperator = $tokens->getOperator();
$match = preg_match_all(
'/(?<search>(?<!:)\b[^:]+\b(?!:))|(?<filter_subject>[a-zа-я_]+):(?!\s]*)"?(?<filter_condition>[^":]+)"?/u',
$string,
$filters
);
if ($match !== false && $match > 0) {
if (!empty($filters['search'][0])) {
$this->cleanString = Filter::safeSearchString(trim($filters['search'][0]));
}
$filtersAndValues = array_filter(
array_combine(
$filters['filter_subject'],
$filters['filter_condition']
)
);
if (!empty($filtersAndValues)) {
$filtersItem = array_filter(
$filtersAndValues,
static function ($value, $key) {
return in_array($key, self::FILTERS['items']['subject'], true)
&& $value !== '';
},
ARRAY_FILTER_USE_BOTH
);
if (!empty($filtersItem)) {
$this->processFilterItems($filtersItem, $queryCondition);
}
$filtersOperator = array_filter(
$filtersAndValues,
static function ($value, $key) {
return in_array($key, self::FILTERS['operator']['subject'], true)
&& in_array($value, self::FILTERS['operator']['condition'], true);
},
ARRAY_FILTER_USE_BOTH
);
if (!empty($filtersOperator)) {
$this->processFilterOperator($filtersOperator);
}
$filtersCondition = array_filter(
array_map(
static function ($subject, $condition) {
if (in_array($subject, self::FILTERS['condition']['subject'], true)
&& in_array($condition, self::FILTERS['condition']['condition'], true)
) {
return $subject.':'.$condition;
}
return null;
},
$filters['filter_subject'],
$filters['filter_condition']
)
);
if (count($filtersCondition) !== 0) {
$this->processFilterIs($filtersCondition, $queryCondition);
}
}
}
return $queryCondition;
$this->processFilterItems($tokens->getItems());
$this->processFilterConditions($tokens->getConditions());
}
private function processFilterItems(
array $filters,
QueryCondition $queryCondition
): void {
private function processFilterItems(array $filters): void
{
foreach ($filters as $filter => $text) {
try {
switch ($filter) {
case 'user':
case AccountSearchConstants::FILTER_USER_NAME:
$userData = $this->userService->getByLogin(Filter::safeSearchString($text));
if (null !== $userData) {
$queryCondition->addFilter(
'Account.userId = ? OR Account.userGroupId = ? OR Account.id IN
(SELECT AccountToUser.accountId FROM AccountToUser WHERE AccountToUser.accountId = Account.id AND AccountToUser.userId = ?
UNION
SELECT AccountToUserGroup.accountId FROM AccountToUserGroup WHERE AccountToUserGroup.accountId = Account.id AND AccountToUserGroup.userGroupId = ?)',
[
$userData->getId(),
$userData->getUserGroupId(),
$userData->getId(),
$userData->getUserGroupId(),
]
);
}
break;
case 'owner':
$text = '%'.Filter::safeSearchString($text).'%';
$queryCondition->addFilter(
'Account.userLogin LIKE ? OR Account.userName LIKE ?',
[$text, $text]
$this->accountSearchRepository->withFilterForUser(
$userData->getId(),
$userData->getUserGroupId()
);
break;
case 'group':
case AccountSearchConstants::FILTER_OWNER:
$this->accountSearchRepository->withFilterForOwner($text);
break;
case AccountSearchConstants::FILTER_GROUP_NAME:
$userGroupData = $this->userGroupService->getByName(Filter::safeSearchString($text));
if (is_object($userGroupData)) {
$queryCondition->addFilter(
'Account.userGroupId = ? OR Account.id IN (SELECT AccountToUserGroup.accountId FROM AccountToUserGroup WHERE AccountToUserGroup.accountId = id AND AccountToUserGroup.userGroupId = ?)',
[$userGroupData->getId(), $userGroupData->getId()]
);
}
$this->accountSearchRepository->withFilterForGroup($userGroupData->getId());
break;
case 'maingroup':
$queryCondition->addFilter(
'Account.userGroupName LIKE ?',
['%'.Filter::safeSearchString($text).'%']
);
case AccountSearchConstants::FILTER_MAIN_GROUP:
$this->accountSearchRepository->withFilterForMainGroup($text);
break;
case 'file':
$queryCondition->addFilter(
'Account.id IN (SELECT AccountFile.accountId FROM AccountFile WHERE AccountFile.name LIKE ?)',
['%'.$text.'%']
);
case AccountSearchConstants::FILTER_FILE_NAME:
$this->accountSearchRepository->withFilterForFile($text);
break;
case 'id':
$queryCondition->addFilter(
'Account.id = ?',
[(int)$text]
);
case AccountSearchConstants::FILTER_ACCOUNT_ID:
$this->accountSearchRepository->withFilterForAccountId((int)$text);
break;
case 'client':
$queryCondition->addFilter(
'Account.clientName LIKE ?',
['%'.Filter::safeSearchString($text).'%']
);
case AccountSearchConstants::FILTER_CLIENT_NAME:
$this->accountSearchRepository->withFilterForClient($text);
break;
case 'category':
$queryCondition->addFilter(
'Account.categoryName LIKE ?',
['%'.Filter::safeSearchString($text).'%']
);
case AccountSearchConstants::FILTER_CATEGORY_NAME:
$this->accountSearchRepository->withFilterForCategory($text);
break;
case 'name_regex':
$queryCondition->addFilter(
'Account.name REGEXP ?',
[$text]
);
case AccountSearchConstants::FILTER_ACCOUNT_NAME_REGEX:
$this->accountSearchRepository->withFilterForAccountNameRegex($text);
break;
}
} catch (Exception $e) {
@@ -411,46 +230,24 @@ final class AccountSearchService extends Service implements AccountSearchService
}
}
private function processFilterOperator(array $filters): void
private function processFilterConditions(array $filters,): void
{
switch ($filters['op']) {
case 'and':
$this->filterOperator = QueryCondition::CONDITION_AND;
break;
case 'or':
$this->filterOperator = QueryCondition::CONDITION_OR;
break;
}
}
private function processFilterIs(
array $filters,
QueryCondition $queryCondition
): void {
foreach ($filters as $filter) {
switch ($filter) {
case 'is:expired':
$queryCondition->addFilter(
'Account.passDateChange > 0 AND UNIX_TIMESTAMP() > Account.passDateChange',
[]
case AccountSearchConstants::FILTER_IS_EXPIRED:
$this->accountSearchRepository->withFilterForIsExpired();
break;
case AccountSearchConstants::FILTER_NOT_EXPIRED:
$this->accountSearchRepository->withFilterForIsNotExpired();
break;
case AccountSearchConstants::FILTER_IS_PRIVATE:
$this->accountSearchRepository->withFilterForIsPrivate(
$this->context->getUserData()->getId(),
$this->context->getUserData()->getUserGroupId()
);
break;
case 'not:expired':
$queryCondition->addFilter(
'Account.passDateChange = 0 OR Account.passDateChange IS NULL OR UNIX_TIMESTAMP() < Account.passDateChange',
[]
);
break;
case 'is:private':
$queryCondition->addFilter(
'(Account.isPrivate = 1 AND Account.userId = ?) OR (Account.isPrivateGroup = 1 AND Account.userGroupId = ?)',
[$this->context->getUserData()->getId(), $this->context->getUserData()->getUserGroupId()]
);
break;
case 'not:private':
$queryCondition->addFilter(
'(Account.isPrivate = 0 OR Account.isPrivate IS NULL) AND (Account.isPrivateGroup = 0 OR Account.isPrivateGroup IS NULL)'
);
case AccountSearchConstants::FILTER_NOT_PRIVATE:
$this->accountSearchRepository->withFilterForIsNotPrivate();
break;
}
}
@@ -462,9 +259,8 @@ final class AccountSearchService extends Service implements AccountSearchService
* @throws ConstraintException
* @throws QueryException
*/
protected function getCacheForAccount(
AccountSearchVData $accountSearchData
): AccountCache {
private function getCacheForAccount(AccountSearchVData $accountSearchData): AccountCache
{
$accountId = $accountSearchData->getId();
/** @var AccountCache[] $cache */
@@ -522,4 +318,65 @@ final class AccountSearchService extends Service implements AccountSearchService
{
return $this->cleanString;
}
/**
* @param \SP\Infrastructure\Database\QueryResult $queryResult
*
* @return array
* @throws \SP\Core\Exceptions\ConstraintException
* @throws \SP\Core\Exceptions\QueryException
*/
private function buildAccountsData(QueryResult $queryResult): array
{
$maxTextLength = $this->configData->isResultsAsCards() ? 40 : 60;
$accountLinkEnabled = $this->context->getUserData()->getPreferences()->isAccountLink()
|| $this->configData->isAccountLink();
$favorites = $this->accountToFavoriteService->getForUserId($this->context->getUserData()->getId());
$accountsData = [];
/** @var AccountSearchVData $accountSearchData */
foreach ($queryResult->getDataAsArray() as $accountSearchData) {
$cache = $this->getCacheForAccount($accountSearchData);
// Obtener la ACL de la cuenta
$accountAcl = $this->accountAclService->getAcl(
ActionsInterface::ACCOUNT_SEARCH,
AccountAclDto::makeFromAccountSearch(
$accountSearchData,
$cache->getUsers(),
$cache->getUserGroups()
)
);
// Propiedades de búsqueda de cada cuenta
$accountsSearchItem = new AccountSearchItem(
$accountSearchData,
$accountAcl,
$this->configData
);
if (!$accountSearchData->getIsPrivate()) {
$accountsSearchItem->setUsers($cache->getUsers());
$accountsSearchItem->setUserGroups($cache->getUserGroups());
}
$accountsSearchItem->setTags(
$this->accountToTagRepository
->getTagsByAccountId($accountSearchData->getId())
->getDataAsArray()
);
$accountsSearchItem->setTextMaxLength($maxTextLength);
$accountsSearchItem->setColor(
$this->pickAccountColor($accountSearchData->getClientId())
);
$accountsSearchItem->setLink($accountLinkEnabled);
$accountsSearchItem->setFavorite(
isset($favorites[$accountSearchData->getId()])
);
$accountsData[] = $accountsSearchItem;
}
return $accountsData;
}
}

View File

@@ -164,9 +164,7 @@ final class AccountService extends Service implements AccountServiceInterface
*/
public function getPasswordForId(int $id): AccountPassData
{
$queryFilter = $this->accountFilterUser->getFilter();
$result = $this->accountRepository->getPasswordForId($id, $queryFilter);
$result = $this->accountRepository->getPasswordForId($id);
if ($result->getNumRows() === 0) {
throw new NoSuchItemException(__u('Account not found'));
@@ -418,7 +416,7 @@ final class AccountService extends Service implements AccountServiceInterface
*/
public function update(AccountRequest $accountRequest): void
{
$this->transactionAware(
$this->accountRepository->transactionAware(
function () use ($accountRequest) {
$userData = $this->context->getUserData();
$userProfile = $this->context->getUserProfile() ?? new ProfileData();
@@ -535,7 +533,7 @@ final class AccountService extends Service implements AccountServiceInterface
*/
public function updateBulk(AccountBulkRequest $request): void
{
$this->transactionAware(
$this->accountRepository->transactionAware(
function () use ($request) {
foreach ($request->getItemsId() as $itemId) {
$accountRequest = $request->getAccountRequestForId($itemId);
@@ -557,16 +555,18 @@ final class AccountService extends Service implements AccountServiceInterface
*/
public function editPassword(AccountRequest $accountRequest): void
{
$this->transactionAware(function () use ($accountRequest) {
$this->addHistory($accountRequest->id);
$this->accountRepository->transactionAware(
function () use ($accountRequest) {
$this->addHistory($accountRequest->id);
$pass = $this->getPasswordEncrypted($accountRequest->pass);
$pass = $this->getPasswordEncrypted($accountRequest->pass);
$accountRequest->pass = $pass['pass'];
$accountRequest->key = $pass['key'];
$accountRequest->pass = $pass['pass'];
$accountRequest->key = $pass['key'];
$this->accountRepository->editPassword($accountRequest);
});
$this->accountRepository->editPassword($accountRequest);
}
);
}
/**
@@ -585,14 +585,17 @@ final class AccountService extends Service implements AccountServiceInterface
* @param int $accountId
*
* @throws \SP\Domain\Common\Services\ServiceException
* @throws \SP\Infrastructure\Common\Repositories\NoSuchItemException
*/
public function editRestore(int $historyId, int $accountId): void
{
$this->transactionAware(
$accountHistoryData = $this->accountHistoryService->getById($historyId);
$this->accountRepository->transactionAware(
function () use ($historyId, $accountId) {
$this->addHistory($accountId);
if (!$this->accountRepository->editRestore($historyId, $this->context->getUserData()->getId())) {
if (!$this->accountRepository->editRestore($accountHistoryData, $this->context->getUserData()->getId())) {
throw new ServiceException(__u('Error on restoring the account'));
}
}
@@ -604,13 +607,15 @@ final class AccountService extends Service implements AccountServiceInterface
*/
public function delete(int $id): AccountService
{
$this->transactionAware(function () use ($id) {
$this->addHistory($id, 1);
$this->accountRepository->transactionAware(
function () use ($id) {
$this->addHistory($id, 1);
if ($this->accountRepository->delete($id) === 0) {
throw new NoSuchItemException(__u('Account not found'));
if ($this->accountRepository->delete($id) === 0) {
throw new NoSuchItemException(__u('Account not found'));
}
}
});
);
return $this;
}
@@ -634,18 +639,9 @@ final class AccountService extends Service implements AccountServiceInterface
* @throws QueryException
* @throws ConstraintException
*/
public function getForUser(int $accountId = null): array
public function getForUser(?int $accountId = null): array
{
$queryFilter = $this->accountFilterUser->getFilter();
if (null !== $accountId) {
$queryFilter->addFilter(
'Account.id <> ? AND (Account.parentId = 0 OR Account.parentId IS NULL)',
[$accountId]
);
}
return $this->accountRepository->getForUser($queryFilter)->getDataAsArray();
return $this->accountRepository->getForUser($accountId)->getDataAsArray();
}
/**
@@ -654,11 +650,7 @@ final class AccountService extends Service implements AccountServiceInterface
*/
public function getLinked(int $accountId): array
{
$queryFilter = $this->accountFilterUser->getFilter();
$queryFilter->addFilter('Account.parentId = ?', [$accountId]);
return $this->accountRepository->getLinked($queryFilter)->getDataAsArray();
return $this->accountRepository->getLinked($accountId)->getDataAsArray();
}
/**
@@ -668,10 +660,7 @@ final class AccountService extends Service implements AccountServiceInterface
*/
public function getPasswordHistoryForId(int $id): AccountPassData
{
$queryFilter = $this->accountFilterUser->getFilterHistory();
$queryFilter->addFilter('AccountHistory.id = ?', [$id]);
$result = $this->accountRepository->getPasswordHistoryForId($queryFilter);
$result = $this->accountRepository->getPasswordHistoryForId($id);
if ($result->getNumRows() === 0) {
throw new NoSuchItemException(__u('The account doesn\'t exist'));
@@ -739,22 +728,4 @@ final class AccountService extends Service implements AccountServiceInterface
{
return $this->accountRepository->getAccountsPassData()->getDataAsArray();
}
/**
* Obtener las cuentas de una búsqueda.
*
* @param \SP\Domain\Account\Services\AccountSearchFilter $accountSearchFilter
*
* @return \SP\Infrastructure\Database\QueryResult
* @throws \SP\Core\Exceptions\ConstraintException
* @throws \SP\Core\Exceptions\QueryException
* @throws \SP\Core\Exceptions\SPException
*/
public function getByFilter(AccountSearchFilter $accountSearchFilter): QueryResult
{
return $this->accountRepository->getByFilter(
$accountSearchFilter,
$this->accountFilterUser->getFilter($accountSearchFilter->getGlobalSearch())
);
}
}

View File

@@ -185,6 +185,6 @@ final class ClientService extends Service implements ClientServiceInterface
*/
public function getAllForUser(): array
{
return $this->clientRepository->getAllForFilter($this->accountFilterUser->getFilter())->getDataAsArray();
return $this->clientRepository->getAllForFilter($this->accountFilterUser->buildFilter())->getDataAsArray();
}
}

View File

@@ -24,29 +24,25 @@
namespace SP\Infrastructure\Account\Repositories;
use Aura\SqlQuery\QueryFactory;
use RuntimeException;
use SP\Core\Context\ContextInterface;
use SP\Core\Events\EventDispatcherInterface;
use SP\Core\Exceptions\ConstraintException;
use SP\Core\Exceptions\QueryException;
use SP\Core\Exceptions\SPException;
use SP\DataModel\AccountExtData;
use SP\DataModel\AccountSearchVData;
use SP\DataModel\AccountVData;
use SP\DataModel\ItemData;
use SP\DataModel\AccountHistoryData;
use SP\DataModel\ItemSearchData;
use SP\Domain\Account\In\AccountRepositoryInterface;
use SP\Domain\Account\Out\AccountData;
use SP\Domain\Account\Out\AccountPassData;
use SP\Domain\Account\Services\AccountFilterUser;
use SP\Domain\Account\Services\AccountPasswordRequest;
use SP\Domain\Account\Services\AccountRequest;
use SP\Domain\Account\Services\AccountSearchFilter;
use SP\Domain\Common\Out\SimpleModel;
use SP\Infrastructure\Common\Repositories\Repository;
use SP\Infrastructure\Common\Repositories\RepositoryItemTrait;
use SP\Infrastructure\Database\DatabaseInterface;
use SP\Infrastructure\Database\QueryData;
use SP\Infrastructure\Database\QueryResult;
use SP\Mvc\Model\QueryAssignment;
use SP\Mvc\Model\QueryCondition;
use SP\Mvc\Model\QueryJoin;
/**
* Class AccountRepository
@@ -57,64 +53,77 @@ final class AccountRepository extends Repository implements AccountRepositoryInt
{
use RepositoryItemTrait;
private AccountFilterUser $accountFilterUser;
public function __construct(
DatabaseInterface $database,
ContextInterface $session,
QueryFactory $queryFactory,
EventDispatcherInterface $eventDispatcher,
AccountFilterUser $accountFilterUser
) {
parent::__construct($database, $session, $eventDispatcher, $queryFactory);
$this->accountFilterUser = $accountFilterUser;
}
/**
* Devolver el número total de cuentas
*/
public function getTotalNumAccounts(): SimpleModel
{
$queryData = new QueryData();
$queryData->setMapClassName(SimpleModel::class);
$queryData->setQuery(AccountRepositorySql::TOTAL_NUM_ACCOUNTS);
$query = $this->queryFactory
->newSelect()
->cols(['SUM(n) AS num'])
->fromSubSelect('SELECT COUNT(*) AS n FROM Account UNION SELECT COUNT(*) AS n FROM AccountHistory', 'a');
return $this->db->doSelect($queryData)->getData();
return $this->db->doSelect(QueryData::build($query))->getData();
}
/**
* @param int $id
* @param QueryCondition $queryCondition
*
* @return QueryResult
*/
public function getPasswordForId(int $id, QueryCondition $queryCondition): QueryResult
public function getPasswordForId(int $id): QueryResult
{
$queryCondition->addFilter('Account.id = ?', [$id]);
$query = $this->accountFilterUser
->buildFilter()
->cols([
'Account.id,',
'Account.name',
'Account.login',
'Account.pass',
'Account.key',
'Account.parentId',
])
->where('Account.id = :id', ['id' => $id])
->limit(1);
$queryData = new QueryData();
$queryData->setMapClassName(AccountPassData::class);
$queryData->setLimit(1);
$queryData->setSelect('Account.id, Account.name, Account.login, Account.pass, Account.key, Account.parentId');
$queryData->setFrom('Account');
$queryData->setWhere($queryCondition->getFilters());
$queryData->setParams($queryCondition->getParams());
return $this->db->doSelect($queryData);
return $this->db->doSelect(QueryData::build($query));
}
/**
* @param QueryCondition $queryCondition
* @param int $id
*
* @return QueryResult
*/
public function getPasswordHistoryForId(QueryCondition $queryCondition): QueryResult
public function getPasswordHistoryForId(int $id): QueryResult
{
$query = /** @lang SQL */
'SELECT
AccountHistory.id,
AccountHistory.name,
AccountHistory.login,
AccountHistory.pass,
AccountHistory.key,
AccountHistory.parentId,
AccountHistory.mPassHash
FROM AccountHistory
WHERE '.$queryCondition->getFilters();
$query = $this->accountFilterUser
->buildFilterHistory()
->cols([
'AccountHistory.id,',
'AccountHistory.name',
'AccountHistory.login',
'AccountHistory.pass',
'AccountHistory.key',
'AccountHistory.parentId',
'AccountHistory.mPassHash',
])
->where('AccountHistory.id = :id', ['id' => $id]);
$queryData = new QueryData();
$queryData->setMapClassName(AccountPassData::class);
$queryData->setQuery($query);
$queryData->setParams($queryCondition->getParams());
return $this->db->doSelect($queryData);
return $this->db->doSelect(QueryData::build($query));
}
/**
@@ -128,11 +137,13 @@ final class AccountRepository extends Repository implements AccountRepositoryInt
*/
public function incrementDecryptCounter(int $id): bool
{
$queryData = new QueryData();
$queryData->setQuery(AccountRepositorySql::INCREMENT_DECRYPT_COUNTER);
$queryData->addParam($id);
$query = $this->queryFactory
->newUpdate()
->table('Account')
->set('countDecrypt', '(countDecrypt + 1)')
->where('id = :id', ['id' => $id]);
return $this->db->doQuery($queryData)->getAffectedNumRows() === 1;
return $this->db->doQuery(QueryData::build($query))->getAffectedNumRows() === 1;
}
/**
@@ -146,26 +157,30 @@ final class AccountRepository extends Repository implements AccountRepositoryInt
*/
public function create($itemData): int
{
$queryData = new QueryData();
$queryData->setQuery(AccountRepositorySql::CREATE);
$queryData->setParams([
$itemData->clientId,
$itemData->categoryId,
$itemData->name,
$itemData->login,
$itemData->url,
$itemData->pass,
$itemData->key,
$itemData->notes,
$itemData->userId,
$itemData->userGroupId,
$itemData->userId,
$itemData->isPrivate,
$itemData->isPrivateGroup,
$itemData->passDateChange,
$itemData->parentId,
]);
$queryData->setOnErrorMessage(__u('Error while creating the account'));
$query = $this->queryFactory
->newInsert()
->into('Account')
->cols([
'clientId' => $itemData->clientId,
'categoryId' => $itemData->categoryId,
'name' => $itemData->name,
'login' => $itemData->login,
'url' => $itemData->url,
'pass' => $itemData->pass,
'key' => $itemData->key,
'notes' => $itemData->notes,
'userId' => $itemData->userId,
'userGroupId' => $itemData->userGroupId,
'userEditId' => $itemData->userId,
'isPrivate' => $itemData->isPrivate,
'isPrivateGroup' => $itemData->isPrivateGroup,
'passDateChange' => $itemData->passDateChange,
'parentId' => $itemData->parentId,
])
->set('dateAdd', 'NOW()')
->set('passDate', 'UNIX_TIMESTAMP()');
$queryData = QueryData::build($query)->setOnErrorMessage(__u('Error while creating the account'));
return $this->db->doQuery($queryData)->getLastId();
}
@@ -181,16 +196,20 @@ final class AccountRepository extends Repository implements AccountRepositoryInt
*/
public function editPassword(AccountRequest $accountRequest): int
{
$queryData = new QueryData();
$queryData->setQuery(AccountRepositorySql::EDIT_PASSWORD);
$queryData->setParams([
$accountRequest->pass,
$accountRequest->key,
$accountRequest->userEditId,
$accountRequest->passDateChange,
$accountRequest->id,
]);
$queryData->setOnErrorMessage(__u('Error while updating the password'));
$query = $this->queryFactory
->newUpdate()
->table('Account')
->cols([
'pass' => $accountRequest->pass,
'key' => $accountRequest->key,
'userEditId' => $accountRequest->userEditId,
'passDateChange' => $accountRequest->passDateChange,
])
->set('dateEdit', 'NOW()')
->set('passDate', 'UNIX_TIMESTAMP()')
->where('id = :id', ['id' => $accountRequest->id]);
$queryData = QueryData::build($query)->setOnErrorMessage(__u('Error while updating the password'));
return $this->db->doQuery($queryData)->getAffectedNumRows();
}
@@ -206,10 +225,13 @@ final class AccountRepository extends Repository implements AccountRepositoryInt
*/
public function updatePassword(AccountPasswordRequest $request): bool
{
$queryData = new QueryData();
$queryData->setQuery(AccountRepositorySql::UPDATE_PASSWORD);
$queryData->setParams([$request->pass, $request->key, $request->id]);
$queryData->setOnErrorMessage(__u('Error while updating the password'));
$query = $this->queryFactory
->newUpdate()
->table('Account')
->cols(['pass' => $request->pass, 'key' => $request->key])
->where('id = :id', ['id' => $request->id]);
$queryData = QueryData::build($query)->setOnErrorMessage(__u('Error while updating the password'));
return $this->db->doQuery($queryData)->getAffectedNumRows() === 1;
}
@@ -217,19 +239,39 @@ final class AccountRepository extends Repository implements AccountRepositoryInt
/**
* Restaurar una cuenta desde el histórico.
*
* @param int $historyId El Id del registro en el histórico
* @param \SP\DataModel\AccountHistoryData $accountHistoryData
* @param int $userId User's Id
*
* @return bool
* @throws ConstraintException
* @throws QueryException
* @throws \SP\Core\Exceptions\ConstraintException
* @throws \SP\Core\Exceptions\QueryException
*/
public function editRestore(int $historyId, int $userId): bool
public function editRestore(AccountHistoryData $accountHistoryData, int $userId): bool
{
$queryData = new QueryData();
$queryData->setQuery(AccountRepositorySql::EDIT_RESTORE);
$queryData->setParams([$historyId, $userId]);
$queryData->setOnErrorMessage(__u('Error on restoring the account'));
$query = $this->queryFactory
->newUpdate()
->table('Account')
->cols([
'clientId' => $accountHistoryData->getClientId(),
'categoryId' => $accountHistoryData->getCategoryId(),
'name' => $accountHistoryData->getName(),
'login' => $accountHistoryData->getLogin(),
'url' => $accountHistoryData->getUrl(),
'notes' => $accountHistoryData->getNotes(),
'userGroupId' => $accountHistoryData->getUserGroupId(),
'userEditId' => $userId,
'pass' => $accountHistoryData->getPass(),
'key' => $accountHistoryData->getKey(),
'passDate' => $accountHistoryData->getPassDate(),
'passDateChange' => $accountHistoryData->getPassDateChange(),
'parentId' => $accountHistoryData->getParentId(),
'isPrivate' => $accountHistoryData->getIsPrivate(),
'isPrivateGroup' => $accountHistoryData->getIsPrivateGroup(),
])
->set('dateEdit', 'NOW()')
->where('id = :id', ['id' => $accountHistoryData->getAccountId()]);
$queryData = QueryData::build($query)->setOnErrorMessage(__u('Error on restoring the account'));
return $this->db->doQuery($queryData)->getAffectedNumRows() === 1;
}
@@ -245,10 +287,12 @@ final class AccountRepository extends Repository implements AccountRepositoryInt
*/
public function delete(int $id): int
{
$queryData = new QueryData();
$queryData->setQuery(AccountRepositorySql::DELETE);
$queryData->addParam($id);
$queryData->setOnErrorMessage(__u('Error while deleting the account'));
$query = $this->queryFactory
->newDelete()
->from('Account')
->where('id = :id', ['id' => $id]);
$queryData = QueryData::build($query)->setOnErrorMessage(__u('Error while deleting the account'));
return $this->db->doQuery($queryData)->getAffectedNumRows();
}
@@ -263,53 +307,34 @@ final class AccountRepository extends Repository implements AccountRepositoryInt
*/
public function update($itemData): int
{
$queryAssignment = new QueryAssignment();
$queryAssignment->setFields([
'clientId',
'categoryId',
'name',
'login',
'url',
'notes',
'userEditId',
'dateEdit = NOW()',
'passDateChange',
'isPrivate',
'isPrivateGroup',
'parentId',
], [
$itemData->clientId,
$itemData->categoryId,
$itemData->name,
$itemData->login,
$itemData->url,
$itemData->notes,
$itemData->userEditId,
$itemData->passDateChange,
$itemData->isPrivate,
$itemData->isPrivateGroup,
$itemData->parentId,
]);
$queryData = new QueryData();
$query = $this->queryFactory
->newUpdate()
->table('Account')
->where('id = :id', ['id' => $itemData->id])
->cols([
'clientId' => $itemData->clientId,
'categoryId' => $itemData->categoryId,
'name' => $itemData->name,
'login' => $itemData->login,
'url' => $itemData->url,
'notes' => $itemData->notes,
'userEditId' => $itemData->userEditId,
'passDateChange' => $itemData->passDateChange,
'isPrivate' => $itemData->isPrivate,
'isPrivateGroup' => $itemData->isPrivateGroup,
'parentId' => $itemData->parentId,
])
->set('dateEdit', 'NOW()');
if ($itemData->changeUserGroup) {
$queryAssignment->addField('userGroupId', $itemData->userGroupId);
$query->col('userGroupId', $itemData->userGroupId);
}
if ($itemData->changeOwner) {
$queryAssignment->addField('userId', $itemData->userId);
$query->col('userId', $itemData->userId);
}
$query = /** @lang SQL */
'UPDATE Account SET '.$queryAssignment->getAssignments().' WHERE id = ?';
$queryData->setQuery($query);
$queryData->setParams($queryAssignment->getValues());
$queryData->addParam($itemData->id);
$queryData->setOnErrorMessage(__u('Error while updating the account'));
$queryData = QueryData::build($query)->setOnErrorMessage(__u('Error while updating the account'));
return $this->db->doQuery($queryData)->getAffectedNumRows();
}
@@ -324,16 +349,14 @@ final class AccountRepository extends Repository implements AccountRepositoryInt
*/
public function updateBulk(AccountRequest $itemData): int
{
$queryAssignment = new QueryAssignment();
$queryAssignment->setFields([
'userEditId',
'dateEdit = NOW()',
], [
$itemData->userEditId,
]);
$queryData = new QueryData();
$query = $this->queryFactory
->newUpdate()
->table('Account')
->where('id = :id', ['id' => $itemData->id])
->cols([
'userEditId' => $itemData->userEditId,
])
->set('dateEdit', 'NOW()');
$optional = ['clientId', 'categoryId', 'userId', 'userGroupId', 'passDateChange'];
@@ -341,7 +364,7 @@ final class AccountRepository extends Repository implements AccountRepositoryInt
foreach ($optional as $field) {
if (isset($itemData->{$field}) && !empty($itemData->{$field})) {
$queryAssignment->addField($field, $itemData->{$field});
$query->col($field, $itemData->{$field});
$optionalCount++;
} else {
logger(sprintf('Field \'%s\' not found in $itemData', $field), 'ERROR');
@@ -352,13 +375,7 @@ final class AccountRepository extends Repository implements AccountRepositoryInt
return 0;
}
$query = /** @lang SQL */
'UPDATE Account SET '.$queryAssignment->getAssignments().' WHERE id = ?';
$queryData->setQuery($query);
$queryData->setParams($queryAssignment->getValues());
$queryData->addParam($itemData->id);
$queryData->setOnErrorMessage(__u('Error while updating the account'));
$queryData = QueryData::build($query)->setOnErrorMessage(__u('Error while updating the account'));
return $this->db->doQuery($queryData)->getAffectedNumRows();
}
@@ -372,11 +389,13 @@ final class AccountRepository extends Repository implements AccountRepositoryInt
*/
public function getById(int $id): QueryResult
{
$queryData = new QueryData();
$queryData->setQuery(AccountRepositorySql::EDIT_BY_ID);
$queryData->setMapClassName(AccountVData::class);
$queryData->addParam($id);
$queryData->setOnErrorMessage(__u('Error while retrieving account\'s data'));
$query = $this->queryFactory
->newSelect()
->from('account_data_v')
->where('id = :id', ['id' => $id])
->limit(1);
$queryData = QueryData::build($query)->setOnErrorMessage(__u('Error while retrieving account\'s data'));
return $this->db->doSelect($queryData);
}
@@ -388,11 +407,9 @@ final class AccountRepository extends Repository implements AccountRepositoryInt
*/
public function getAll(): QueryResult
{
$queryData = new QueryData();
$queryData->setMapClassName(AccountData::class);
$queryData->setQuery(AccountRepositorySql::GET_ALL);
$query = $this->queryFactory->newSelect()->from('Account');
return $this->db->doSelect($queryData);
return $this->db->doSelect(QueryData::build($query));
}
/**
@@ -420,11 +437,11 @@ final class AccountRepository extends Repository implements AccountRepositoryInt
return 0;
}
$queryData = new QueryData();
$queryData->setQuery('DELETE FROM Account WHERE id IN ('.$this->getParamsFromArray($ids).')');
$queryData->setParams($ids);
$queryData->setOnErrorMessage(__u('Error while deleting the accounts'));
$query = $this->queryFactory
->newDelete()
->from('Account')
->where('id IN (:id)', $ids);
$queryData = QueryData::build($query)->setOnErrorMessage(__u('Error while deleting the accounts'));
return $this->db->doQuery($queryData)->getAffectedNumRows();
}
@@ -468,34 +485,41 @@ final class AccountRepository extends Repository implements AccountRepositoryInt
*/
public function search(ItemSearchData $itemSearchData): QueryResult
{
$queryData = new QueryData();
$queryData->setSelect('id, name, clientName, categoryName, userName, userGroupName');
$queryData->setFrom('account_search_v');
$queryData->setOrder('name, clientName');
$query = $this->queryFactory
->newSelect()
->from('account_search_v')
->cols([
'id',
'name',
'clientName',
'categoryName',
'userName',
'userGroupName',
])
->orderBy(['name ASC', 'clientName ASC'])
->limit($itemSearchData->getLimitCount())
->offset($itemSearchData->getLimitStart());
if (!empty($itemSearchData->getSeachString())) {
$queryData->setWhere(
'name LIKE ?
OR clientName LIKE ?
OR categoryName LIKE ?
OR userName LIKE ?
OR userGroupName LIKE ?'
);
$query->where('name LIKE :name')
->orWhere('clientName LIKE :clientName')
->orWhere('categoryName LIKE :categoryName')
->orWhere('userName LIKE :userName')
->orWhere('userGroupName LIKE :userGroupName');
$search = '%'.$itemSearchData->getSeachString().'%';
$queryData->addParam($search);
$queryData->addParam($search);
$queryData->addParam($search);
$queryData->addParam($search);
$queryData->addParam($search);
$query->bindValues([
'name' => $search,
'clientName' => $search,
'categoryName' => $search,
'userName' => $search,
'userGroupName' => $search,
]);
}
$queryData->setLimit(
'?,?',
[$itemSearchData->getLimitStart(), $itemSearchData->getLimitCount()]
);
return $this->db->doSelect($queryData, true);
return $this->db->doSelect(QueryData::build($query), true);
}
/**
@@ -509,11 +533,13 @@ final class AccountRepository extends Repository implements AccountRepositoryInt
*/
public function incrementViewCounter(int $id): bool
{
$queryData = new QueryData();
$queryData->setQuery(AccountRepositorySql::INCREMENT_VIEW_COUNTER);
$queryData->addParam($id);
$query = $this->queryFactory
->newUpdate()
->table('Account')
->set('countView', '(countView + 1)')
->where('id = :id', ['id' => $id]);
return $this->db->doQuery($queryData)->getAffectedNumRows() === 1;
return $this->db->doQuery(QueryData::build($query))->getAffectedNumRows() === 1;
}
/**
@@ -525,164 +551,76 @@ final class AccountRepository extends Repository implements AccountRepositoryInt
*/
public function getDataForLink(int $id): QueryResult
{
$queryData = new QueryData();
$queryData->setQuery(AccountRepositorySql::GET_DATA_FOR_LINK);
$queryData->setMapClassName(AccountExtData::class);
$queryData->addParam($id);
$queryData->setOnErrorMessage(__u('Error while retrieving account\'s data'));
$query = $this->queryFactory
->newSelect()
->from('Account')
->join('INNER', 'Client', 'Account.clientId = Client.id')
->join('INNER', 'Category', 'Account.categoryId = Category.id')
->cols([
'Account.name',
'Account.login',
'Account.pass',
'Account.key',
'Account.url',
'Account.notes',
'Client.name AS clientName',
'Category.name AS categoryName',
])
->where('Account.id = :id', ['id' => $id]);
$queryData = QueryData::build($query)->setOnErrorMessage(__u('Error while retrieving account\'s data'));
return $this->db->doSelect($queryData);
}
/**
* Obtener las cuentas de una búsqueda.
*
* @param AccountSearchFilter $accountSearchFilter
* @param QueryCondition $queryFilterUser
* @param int|null $accountId
*
* @return QueryResult
*/
public function getByFilter(
AccountSearchFilter $accountSearchFilter,
QueryCondition $queryFilterUser
): QueryResult {
$queryFilters = new QueryCondition();
// Sets the search text depending on if special search filters are being used
$searchText = $accountSearchFilter->getCleanTxtSearch();
if (!empty($searchText)) {
$queryFilters->addFilter(
'Account.name LIKE ? OR Account.login LIKE ? OR Account.url LIKE ? OR Account.notes LIKE ?',
array_fill(0, 4, '%'.$searchText.'%')
);
}
// Gets special search filters
$stringFilters = $accountSearchFilter->getStringFilters();
if ($stringFilters->hasFilters()) {
$queryFilters->addFilter(
$stringFilters->getFilters($accountSearchFilter->getFilterOperator()),
$stringFilters->getParams()
);
}
if ($accountSearchFilter->getCategoryId() !== null) {
$queryFilters->addFilter(
'Account.categoryId = ?',
[$accountSearchFilter->getCategoryId()]
);
}
if ($accountSearchFilter->getClientId() !== null) {
$queryFilters->addFilter(
'Account.clientId = ?',
[$accountSearchFilter->getClientId()]
);
}
$where = [];
if ($queryFilterUser->hasFilters()) {
$where[] = $queryFilterUser->getFilters();
}
$queryData = new QueryData();
$queryJoins = new QueryJoin();
if ($accountSearchFilter->isSearchFavorites() === true) {
$queryJoins->addJoin(
'INNER JOIN AccountToFavorite ON (AccountToFavorite.accountId = Account.id AND AccountToFavorite.userId = ?)',
[$this->context->getUserData()->getId()]
);
}
if ($accountSearchFilter->hasTags()) {
$queryJoins->addJoin('INNER JOIN AccountToTag ON AccountToTag.accountId = Account.id');
$queryFilters->addFilter(
'AccountToTag.tagId IN ('.$this->getParamsFromArray($accountSearchFilter->getTagsId()).')',
$accountSearchFilter->getTagsId()
);
if (QueryCondition::CONDITION_AND === $accountSearchFilter->getFilterOperator()) {
$groupBy = sprintf(
'Account.id HAVING COUNT(DISTINCT AccountToTag.tagId) = %d',
count($accountSearchFilter->getTagsId())
);
$queryData->setGroupBy($groupBy);
}
}
if ($queryFilters->hasFilters()) {
$where[] = $queryFilters->getFilters($accountSearchFilter->getFilterOperator());
}
$queryData->setWhere($where);
$queryData->setParams(
array_merge(
$queryJoins->getParams(),
$queryFilterUser->getParams(),
$queryFilters->getParams()
public function getForUser(?int $accountId = null): QueryResult
{
$query = $this->accountFilterUser
->buildFilter()
->cols(
[
'Account.id',
'Account.name',
'C.name AS clientName',
]
)
);
$queryData->setSelect('DISTINCT Account.*');
$queryData->setFrom('account_search_v Account '.$queryJoins->getJoins());
$queryData->setOrder($accountSearchFilter->getOrderString());
->join('LEFT', 'Client AS C', 'Account.clientId = C.id')
->orderBy(['Account.name ASC']);
if ($accountSearchFilter->getLimitCount() > 0) {
$queryLimit = '?, ?';
$queryData->addParam($accountSearchFilter->getLimitStart());
$queryData->addParam($accountSearchFilter->getLimitCount());
$queryData->setLimit($queryLimit);
if ($accountId) {
$query->where('Account.id <> :id', ['id' => $accountId]);
$query->where('Account.parentId = 0 OR Account.parentId IS NULL');
}
$queryData->setMapClassName(AccountSearchVData::class);
return $this->db->doSelect($queryData, true);
return $this->db->doSelect(QueryData::build($query));
}
/**
* @param QueryCondition $queryFilter
* @param int $accountId
*
* @return QueryResult
*/
public function getForUser(QueryCondition $queryFilter): QueryResult
public function getLinked(int $accountId): QueryResult
{
$query = /** @lang SQL */
'SELECT Account.id, Account.name, C.name AS clientName
FROM Account
LEFT JOIN Client C ON Account.clientId = C.id
WHERE '.$queryFilter->getFilters().' ORDER BY name';
$query = $this->accountFilterUser
->buildFilter()
->cols(
[
'Account.id',
'Account.name',
'Client.name AS clientName',
]
)
->join('INNER', 'Client', 'Account.clientId = Client.id')
->where('Account.parentId = :parentId', ['parentId' => $accountId])
->orderBy(['Account.name ASC']);
$queryData = new QueryData();
$queryData->setMapClassName(ItemData::class);
$queryData->setQuery($query);
$queryData->setParams($queryFilter->getParams());
return $this->db->doSelect($queryData);
}
/**
* @param QueryCondition $queryFilter
*
* @return QueryResult
*/
public function getLinked(QueryCondition $queryFilter): QueryResult
{
$query = /** @lang SQL */
'SELECT Account.id, Account.name, Client.name AS clientName
FROM Account
INNER JOIN Client ON Account.clientId = Client.id
WHERE '.$queryFilter->getFilters().' ORDER BY Account.name';
$queryData = new QueryData();
$queryData->setQuery($query);
$queryData->setParams($queryFilter->getParams());
return $this->db->doSelect($queryData);
return $this->db->doSelect(QueryData::build($query));
}
/**
@@ -692,9 +630,19 @@ final class AccountRepository extends Repository implements AccountRepositoryInt
*/
public function getAccountsPassData(): QueryResult
{
$queryData = new QueryData();
$queryData->setQuery(AccountRepositorySql::GET_ACCOUNT_PASS_DATA);
$query = $this->queryFactory
->newSelect()
->from('Account')
->cols(
[
'id',
'name',
'pass',
'key',
]
)
->where('BIT_LENGTH(pass) > 0');
return $this->db->doSelect($queryData);
return $this->db->doSelect(QueryData::build($query));
}
}

View File

@@ -1,114 +0,0 @@
<?php
/*
* sysPass
*
* @author nuxsmin
* @link https://syspass.org
* @copyright 2012-2022, Rubén Domínguez nuxsmin@$syspass.org
*
* This file is part of sysPass.
*
* sysPass is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* sysPass is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with sysPass. If not, see <http://www.gnu.org/licenses/>.
*/
namespace SP\Infrastructure\Account\Repositories;
/**
* Class AccountRepositorySql
*/
final class AccountRepositorySql
{
public const TOTAL_NUM_ACCOUNTS = 'SELECT SUM(n) AS num FROM
(SELECT COUNT(*) AS n FROM Account UNION SELECT COUNT(*) AS n FROM AccountHistory) a';
public const INCREMENT_DECRYPT_COUNTER = 'UPDATE Account SET countDecrypt = (countDecrypt + 1) WHERE id = ? LIMIT 1';
public const CREATE = 'INSERT INTO Account SET
clientId = ?,
categoryId = ?,
`name` = ?,
login = ?,
url = ?,
pass = ?,
`key` = ?,
notes = ?,
dateAdd = NOW(),
userId = ?,
userGroupId = ?,
userEditId = ?,
isPrivate = ?,
isPrivateGroup = ?,
passDate = UNIX_TIMESTAMP(),
passDateChange = ?,
parentId = ?';
public const EDIT_PASSWORD = 'UPDATE Account SET
pass = ?,
`key` = ?,
userEditId = ?,
dateEdit = NOW(),
passDate = UNIX_TIMESTAMP(),
passDateChange = ?
WHERE id = ?';
public const UPDATE_PASSWORD = 'UPDATE Account SET
pass = ?,
`key` = ?
WHERE id = ?';
public const EDIT_RESTORE = 'UPDATE Account dst,
(SELECT * FROM AccountHistory AH WHERE AH.id = ?) src SET
dst.clientId = src.clientId,
dst.categoryId = src.categoryId,
dst.name = src.name,
dst.login = src.login,
dst.url = src.url,
dst.notes = src.notes,
dst.userGroupId = src.userGroupId,
dst.userEditId = ?,
dst.dateEdit = NOW(),
dst.pass = src.pass,
dst.key = src.key,
dst.passDate = src.passDate,
dst.passDateChange = src.passDateChange,
dst.parentId = src.parentId,
dst.isPrivate = src.isPrivate,
dst.isPrivateGroup = src.isPrivateGroup
WHERE dst.id = src.accountId';
public const DELETE = 'DELETE FROM Account WHERE id = ? LIMIT 1';
public const EDIT_BY_ID = 'SELECT * FROM account_data_v WHERE id = ? LIMIT 1';
public const GET_ALL = 'SELECT * FROM Account ORDER BY id';
public const INCREMENT_VIEW_COUNTER = 'UPDATE Account SET countView = (countView + 1) WHERE id = ? LIMIT 1';
public const GET_DATA_FOR_LINK = 'SELECT Account.id,
Account.name,
Account.login,
Account.pass,
Account.key,
Account.url,
Account.notes,
Client.name AS clientName,
Category.name AS categoryName
FROM Account
INNER JOIN Client ON Account.clientId = Client.id
INNER JOIN Category ON Account.categoryId = Category.id
WHERE Account.id = ? LIMIT 1';
public const GET_ACCOUNT_PASS_DATA = 'SELECT id, `name`, pass, `key` FROM Account WHERE BIT_LENGTH(pass) > 0';
}

View File

@@ -0,0 +1,391 @@
<?php
/*
* sysPass
*
* @author nuxsmin
* @link https://syspass.org
* @copyright 2012-2022, Rubén Domínguez nuxsmin@$syspass.org
*
* This file is part of sysPass.
*
* sysPass is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* sysPass is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with sysPass. If not, see <http://www.gnu.org/licenses/>.
*/
namespace SP\Infrastructure\Account\Repositories;
use Aura\SqlQuery\Common\SelectInterface;
use Aura\SqlQuery\QueryFactory;
use SP\Core\Context\ContextInterface;
use SP\Core\Events\EventDispatcherInterface;
use SP\DataModel\AccountSearchVData;
use SP\Domain\Account\Search\AccountSearchConstants;
use SP\Domain\Account\Search\AccountSearchFilter;
use SP\Domain\Account\Services\AccountFilterUser;
use SP\Infrastructure\Common\Repositories\Repository;
use SP\Infrastructure\Database\DatabaseInterface;
use SP\Infrastructure\Database\QueryData;
use SP\Infrastructure\Database\QueryResult;
use SP\Util\Filter;
/**
* Class AccountSearchRepository
*/
final class AccountSearchRepository extends Repository implements AccountSearchRepositoryInterface
{
protected SelectInterface $query;
private AccountFilterUser $accountFilterUser;
public function __construct(
DatabaseInterface $database,
ContextInterface $session,
EventDispatcherInterface $eventDispatcher,
QueryFactory $queryFactory,
AccountFilterUser $accountFilterUser
) {
parent::__construct($database, $session, $eventDispatcher, $queryFactory);
$this->accountFilterUser = $accountFilterUser;
}
/**
* Obtener las cuentas de una búsqueda.
*
* @param AccountSearchFilter $accountSearchFilter
*
* @return QueryResult
*/
public function getByFilter(AccountSearchFilter $accountSearchFilter): QueryResult
{
$this->accountFilterUser->buildFilter($accountSearchFilter->getGlobalSearch(), $this->query);
// Sets the search text depending on whether special search filters are being used
$searchText = $accountSearchFilter->getCleanTxtSearch();
if (!empty($searchText)) {
$searchTextLike = '%'.$searchText.'%';
$this->query
->where(
'(Account.name LIKE :name OR Account.login LIKE :login OR Account.url LIKE :url OR Account.notes LIKE :notes)',
[
'name' => $searchTextLike,
'login' => $searchTextLike,
'url' => $searchTextLike,
'notes' => $searchTextLike,
]
);
}
if ($accountSearchFilter->getCategoryId() !== null) {
$this->query
->where(
'Account.categoryId = :categoryId',
[
'categoryId' => $accountSearchFilter->getCategoryId(),
]
);
}
if ($accountSearchFilter->getClientId() !== null) {
$this->query
->where(
'Account.categoryId = :clientId',
[
'categoryId' => $accountSearchFilter->getClientId(),
]
);
}
if ($accountSearchFilter->isSearchFavorites() === true) {
$this->query
->join(
'INNER',
'AccountToFavorite',
'AccountToFavorite.accountId = Account.id AND AccountToFavorite.userId = :userId',
[
'userId' => $this->context->getUserData()->getId(),
]
);
}
if ($accountSearchFilter->hasTags()) {
$this->query->join(
'INNER',
'AccountToTag',
'AccountToTag.accountId = Account.id'
);
$this->query
->where(
'AccountToTag.tagId IN (:tagId)',
[
'tagId' => $accountSearchFilter->getTagsId(),
]
);
if (AccountSearchConstants::FILTER_CHAIN_AND === $accountSearchFilter->getFilterOperator()) {
$this->query
->groupBy(['Account.id'])
->having(
'COUNT(DISTINCT AccountToTag.tagId) = :tagsCount',
[
'tagsCount' => count($accountSearchFilter->getTagsId()),
]
);
}
}
$this->setOrder($accountSearchFilter);
if ($accountSearchFilter->getLimitCount() > 0) {
$this->query->limit($accountSearchFilter->getLimitCount());
$this->query->offset($accountSearchFilter->getLimitStart());
}
return $this->db->doSelect(
QueryData::build($this->query)->setMapClassName(AccountSearchVData::class),
true
);
}
/**
* Devuelve la cadena de ordenación de la consulta
*/
private function setOrder(AccountSearchFilter $filter): void
{
$orderKey = match ($filter->getSortKey()) {
AccountSearchConstants::SORT_NAME => 'Account.name',
AccountSearchConstants::SORT_CATEGORY => 'Account.categoryName',
AccountSearchConstants::SORT_LOGIN => 'Account.login',
AccountSearchConstants::SORT_URL => 'Account.url',
AccountSearchConstants::SORT_CLIENT => 'Account.clientName',
default => 'Account.clientName, Account.name',
};
if ($filter->isSortViews() && !$filter->getSortKey()) {
$this->query->orderBy(['Account.countView DESC']);
} else {
$sortOrder = match ($filter->getSortOrder()) {
AccountSearchConstants::SORT_DIR_ASC => 'ASC',
AccountSearchConstants::SORT_DIR_DESC => 'DESC',
};
$this->query->orderBy([
sprintf('%s %s', $orderKey, $sortOrder),
]);
}
}
/**
* @param int $userId
* @param int $userGroupId
*
* @return \Aura\SqlQuery\Common\SelectInterface
*/
public function withFilterForUser(int $userId, int $userGroupId): SelectInterface
{
$where = [
'Account.userId = :userId',
'Account.userGroupId = :userGroupId',
'Account.id IN (SELECT AccountToUser.accountId FROM AccountToUser WHERE AccountToUser.accountId = Account.id AND AccountToUser.userId = :userId
UNION
SELECT AccountToUserGroup.accountId FROM AccountToUserGroup WHERE AccountToUserGroup.accountId = Account.id AND AccountToUserGroup.userGroupId = :userGroupId)',
];
return $this->query
->where(sprintf('(%s)', join(sprintf(' %s ', AccountSearchConstants::FILTER_CHAIN_OR), $where)))
->bindValues([
'userId' => $userId,
'userGroupId' => $userGroupId,
]);
}
/**
* @param int $userGroupId
*
* @return \Aura\SqlQuery\Common\SelectInterface
*/
public function withFilterForGroup(int $userGroupId): SelectInterface
{
return $this->query
->where('Account.userGroupId = :userGroupId')
->orWhere(
'(Account.id IN (SELECT AccountToUserGroup.accountId FROM AccountToUserGroup WHERE AccountToUserGroup.accountId = id AND AccountToUserGroup.userGroupId = :userGroupId))'
)
->bindValues([
'userGroupId' => $userGroupId,
]);
}
/**
* @param string $userGroupName
*
* @return \Aura\SqlQuery\Common\SelectInterface
*/
public function withFilterForMainGroup(string $userGroupName): SelectInterface
{
$userGroupNameLike = '%'.Filter::safeSearchString($userGroupName).'%';
return $this->query
->where('Account.userGroupName LIKE :userGroupName')
->bindValues([
'userGroupName' => $userGroupNameLike,
]);
}
/**
* @param string $owner
*
* @return \Aura\SqlQuery\Common\SelectInterface
*/
public function withFilterForOwner(string $owner): SelectInterface
{
$ownerLike = '%'.Filter::safeSearchString($owner).'%';
return $this->query
->where('(Account.userLogin LIKE :userLogin OR Account.userName LIKE :userName)')
->bindValues([
'userLogin' => $ownerLike,
'userName' => $ownerLike,
]);
}
/**
* @param string $fileName
*
* @return \Aura\SqlQuery\Common\SelectInterface
*/
public function withFilterForFile(string $fileName): SelectInterface
{
$fileNameLike = '%'.Filter::safeSearchString($fileName).'%';
return $this->query
->where(
'(Account.id IN (SELECT AccountFile.accountId FROM AccountFile WHERE AccountFile.name LIKE :fileName))'
)
->bindValues([
'fileName' => $fileNameLike,
]);
}
/**
* @param int $accountId
*
* @return \Aura\SqlQuery\Common\SelectInterface
*/
public function withFilterForAccountId(int $accountId): SelectInterface
{
return $this->query
->where('Account.id = :id')
->bindValues([
'id' => $accountId,
]);
}
/**
* @param string $clientName
*
* @return \Aura\SqlQuery\Common\SelectInterface
*/
public function withFilterForClient(string $clientName): SelectInterface
{
$clientNameLike = '%'.Filter::safeSearchString($clientName).'%';
return $this->query
->where('Account.clientName LIKE :clientName')
->bindValues([
'clientName' => $clientNameLike,
]);
}
/**
* @param string $categoryName
*
* @return \Aura\SqlQuery\Common\SelectInterface
*/
public function withFilterForCategory(string $categoryName): SelectInterface
{
$categoryNameLike = '%'.Filter::safeSearchString($categoryName).'%';
return $this->query
->where('Account.categoryName LIKE :categoryName')
->bindValues([
'categoryName' => $categoryNameLike,
]);
}
/**
* @param string $accountName
*
* @return \Aura\SqlQuery\Common\SelectInterface
*/
public function withFilterForAccountNameRegex(string $accountName): SelectInterface
{
return $this->query
->where('Account.name REGEXP :name')
->bindValues([
'name' => $accountName,
]);
}
public function withFilterForIsExpired(): SelectInterface
{
return $this->query
->where('(Account.passDateChange > 0 AND UNIX_TIMESTAMP() > Account.passDateChange)');
}
public function withFilterForIsNotExpired(): SelectInterface
{
return $this->query
->where(
'(Account.passDateChange = 0 OR Account.passDateChange IS NULL OR UNIX_TIMESTAMP() < Account.passDateChange)'
);
}
/**
* @param int $userId
* @param int $userGroupId
*
* @return \Aura\SqlQuery\Common\SelectInterface
*/
public function withFilterForIsPrivate(int $userId, int $userGroupId): SelectInterface
{
return $this->query
->where(
'(Account.isPrivate = 1 AND Account.userId = :userId) OR (Account.isPrivateGroup = 1 AND Account.userGroupId = :userGroupId)'
)
->bindValues([
'userId' => $userId,
'userGroupId' => $userGroupId,
]);
}
public function withFilterForIsNotPrivate(): SelectInterface
{
return $this->query
->where(
'(Account.isPrivate = 0 OR Account.isPrivate IS NULL) AND (Account.isPrivateGroup = 0 OR Account.isPrivateGroup IS NULL)'
);
}
protected function initialize()
{
$this->query = $this->queryFactory
->newSelect()
->from('account_search_v AS Account')
->distinct();
}
}

View File

@@ -0,0 +1,123 @@
<?php
/*
* sysPass
*
* @author nuxsmin
* @link https://syspass.org
* @copyright 2012-2022, Rubén Domínguez nuxsmin@$syspass.org
*
* This file is part of sysPass.
*
* sysPass is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* sysPass is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with sysPass. If not, see <http://www.gnu.org/licenses/>.
*/
namespace SP\Infrastructure\Account\Repositories;
use Aura\SqlQuery\Common\SelectInterface;
use SP\Domain\Account\Search\AccountSearchFilter;
use SP\Infrastructure\Database\QueryResult;
/**
* Class AccountSearchRepository
*/
interface AccountSearchRepositoryInterface
{
/**
* Obtener las cuentas de una búsqueda.
*
* @param AccountSearchFilter $accountSearchFilter
*
* @return QueryResult
*/
public function getByFilter(AccountSearchFilter $accountSearchFilter): QueryResult;
/**
* @param int $userId
* @param int $userGroupId
*
* @return \Aura\SqlQuery\Common\SelectInterface
*/
public function withFilterForUser(int $userId, int $userGroupId): SelectInterface;
/**
* @param int $userGroupId
*
* @return \Aura\SqlQuery\Common\SelectInterface
*/
public function withFilterForGroup(int $userGroupId): SelectInterface;
/**
* @param string $userGroupName
*
* @return \Aura\SqlQuery\Common\SelectInterface
*/
public function withFilterForMainGroup(string $userGroupName): SelectInterface;
/**
* @param string $owner
*
* @return \Aura\SqlQuery\Common\SelectInterface
*/
public function withFilterForOwner(string $owner): SelectInterface;
/**
* @param string $fileName
*
* @return \Aura\SqlQuery\Common\SelectInterface
*/
public function withFilterForFile(string $fileName): SelectInterface;
/**
* @param int $accountId
*
* @return \Aura\SqlQuery\Common\SelectInterface
*/
public function withFilterForAccountId(int $accountId): SelectInterface;
/**
* @param string $clientName
*
* @return \Aura\SqlQuery\Common\SelectInterface
*/
public function withFilterForClient(string $clientName): SelectInterface;
/**
* @param string $categoryName
*
* @return \Aura\SqlQuery\Common\SelectInterface
*/
public function withFilterForCategory(string $categoryName): SelectInterface;
/**
* @param string $accountName
*
* @return \Aura\SqlQuery\Common\SelectInterface
*/
public function withFilterForAccountNameRegex(string $accountName): SelectInterface;
public function withFilterForIsExpired(): SelectInterface;
public function withFilterForIsNotExpired(): SelectInterface;
/**
* @param int $userId
* @param int $userGroupId
*
* @return \Aura\SqlQuery\Common\SelectInterface
*/
public function withFilterForIsPrivate(int $userId, int $userGroupId): SelectInterface;
public function withFilterForIsNotPrivate(): SelectInterface;
}

View File

@@ -24,7 +24,14 @@
namespace SP\Infrastructure\Common\Repositories;
use Aura\SqlQuery\QueryFactory;
use Closure;
use Exception;
use SP\Core\Context\ContextInterface;
use SP\Core\Events\Event;
use SP\Core\Events\EventDispatcherInterface;
use SP\Core\Events\EventMessage;
use SP\Domain\Common\Services\ServiceException;
use SP\Infrastructure\Database\DatabaseInterface;
/**
@@ -34,16 +41,59 @@ use SP\Infrastructure\Database\DatabaseInterface;
*/
abstract class Repository
{
protected ContextInterface $context;
protected DatabaseInterface $db;
protected ContextInterface $context;
protected DatabaseInterface $db;
protected QueryFactory $queryFactory;
protected EventDispatcherInterface $eventDispatcher;
final public function __construct(DatabaseInterface $database, ContextInterface $session)
{
public function __construct(
DatabaseInterface $database,
ContextInterface $session,
EventDispatcherInterface $eventDispatcher,
QueryFactory $queryFactory
) {
$this->db = $database;
$this->context = $session;
$this->queryFactory = $queryFactory;
$this->eventDispatcher = $eventDispatcher;
if (method_exists($this, 'initialize')) {
$this->initialize();
}
}
/**
* Bubbles a Closure in a database transaction
*
* @param \Closure $closure
*
* @return mixed
* @throws \SP\Domain\Common\Services\ServiceException
* @throws \Exception
*/
final public function transactionAware(Closure $closure)
{
if ($this->db->beginTransaction()) {
try {
$result = $closure->call($this);
$this->db->endTransaction();
return $result;
} catch (Exception $e) {
$this->db->rollbackTransaction();
logger('Transaction:Rollback');
$this->eventDispatcher->notifyEvent(
'database.rollback',
new Event($this, EventMessage::factory()->addDescription(__u('Rollback')))
);
throw $e;
}
} else {
throw new ServiceException(__u('Unable to start a transaction'));
}
}
}

View File

@@ -24,6 +24,8 @@
namespace SP\Infrastructure\Database;
use Aura\SqlQuery\Common\SelectInterface;
use Aura\SqlQuery\QueryInterface;
use Exception;
use PDO;
use PDOStatement;
@@ -41,12 +43,12 @@ use SP\Core\Exceptions\SPException;
*/
final class Database implements DatabaseInterface
{
protected DbStorageInterface $dbHandler;
protected int $numRows = 0;
protected int $numFields = 0;
protected ?array $lastResult = null;
protected DbStorageInterface $dbHandler;
private ?int $lastId = null;
private EventDispatcher $eventDispatcher;
private ?int $lastId = null;
/**
* DB constructor.
@@ -93,7 +95,7 @@ final class Database implements DatabaseInterface
*/
public function doSelect(QueryData $queryData, bool $fullCount = false): QueryResult
{
if ($queryData->getQuery() === '') {
if ($queryData->getQuery()->getStatement()) {
throw new QueryException($queryData->getOnErrorMessage(), SPException::ERROR, __u('Blank query'));
}
@@ -130,14 +132,14 @@ final class Database implements DatabaseInterface
*/
public function doQuery(QueryData $queryData): QueryResult
{
$stmt = $this->prepareQueryData($queryData);
$stmt = $this->prepareQueryData($queryData->getQuery());
$this->eventDispatcher->notifyEvent(
'database.query',
new Event($this, EventMessage::factory()->addDescription($queryData->getQuery()))
new Event($this, EventMessage::factory()->addDescription($queryData->getQuery()->getStatement()))
);
if (preg_match("/^(select|show)\s/i", $queryData->getQuery())) {
if ($queryData->getQuery() instanceof SelectInterface) {
$this->numFields = $stmt->columnCount();
return new QueryResult($this->fetch($queryData, $stmt));
@@ -149,8 +151,7 @@ final class Database implements DatabaseInterface
/**
* Asociar los parámetros de la consulta utilizando el tipo adecuado
*
* @param QueryData $queryData Los datos de la consulta
* @param bool $isCount Indica si es una consulta de contador de registros
* @param QueryInterface $query Los datos de la consulta
* @param array $options
*
* @return \PDOStatement
@@ -158,25 +159,16 @@ final class Database implements DatabaseInterface
* @throws \SP\Core\Exceptions\QueryException
*/
private function prepareQueryData(
QueryData $queryData,
bool $isCount = false,
QueryInterface $query,
array $options = []
): PDOStatement {
$query = $queryData->getQuery();
$params = $queryData->getParams();
if ($isCount === true) {
$query = $queryData->getQueryCount();
$params = $this->getParamsForCount($queryData);
}
try {
$connection = $this->dbHandler->getConnection();
if (count($params) !== 0) {
$stmt = $connection->prepare($query, $options);
if (count($query->getBindValues()) !== 0) {
$stmt = $connection->prepare($query->getStatement(), $options);
foreach ($params as $param => $value) {
foreach ($query->getBindValues() as $param => $value) {
// Si la clave es un número utilizamos marcadores de posición "?" en
// la consulta. En caso contrario marcadores de nombre
$param = is_int($param) ? $param + 1 : ':'.$param;
@@ -223,6 +215,8 @@ final class Database implements DatabaseInterface
/**
* Strips out the unused params from the query count
*
* TODO: remove??
*/
private function getParamsForCount(QueryData $queryData): array
{
@@ -254,11 +248,7 @@ final class Database implements DatabaseInterface
*/
public function getFullRowCount(QueryData $queryData): int
{
if ($queryData->getQueryCount() === '') {
return 0;
}
$queryRes = $this->prepareQueryData($queryData, true);
$queryRes = $this->prepareQueryData($queryData->getQueryCount());
$num = (int)$queryRes->fetchColumn();
$queryRes->closeCursor();
@@ -290,7 +280,7 @@ final class Database implements DatabaseInterface
);
}
return $this->prepareQueryData($queryData, false, $options);
return $this->prepareQueryData($queryData->getQuery(), $options);
}
/**

View File

@@ -24,6 +24,7 @@
namespace SP\Infrastructure\Database;
use Aura\SqlQuery\QueryInterface;
use PDOStatement;
use SP\Core\Exceptions\ConstraintException;
use SP\Core\Exceptions\QueryException;

View File

@@ -24,6 +24,11 @@
namespace SP\Infrastructure\Database;
use Aura\SqlQuery\Common\Select;
use Aura\SqlQuery\QueryInterface;
use SP\Core\Exceptions\QueryException;
use SP\Domain\Common\Out\SimpleModel;
/**
* Class QueryData
*
@@ -31,18 +36,27 @@ namespace SP\Infrastructure\Database;
*/
final class QueryData
{
protected array $params = [];
protected ?string $query = null;
protected ?string $mapClassName = null;
protected bool $useKeyPair = false;
protected ?string $select = null;
protected ?string $from = null;
protected ?string $where = null;
protected ?string $groupBy = null;
protected ?string $order = null;
protected ?string $limit = null;
protected ?string $queryCount = null;
protected ?string $onErrorMessage = null;
protected array $params = [];
protected QueryInterface $query;
protected ?string $mapClassName = SimpleModel::class;
protected bool $useKeyPair = false;
protected ?string $select = null;
protected ?string $from = null;
protected ?string $where = null;
protected ?string $groupBy = null;
protected ?string $order = null;
protected ?string $limit = null;
protected ?string $onErrorMessage = null;
public function __construct(QueryInterface $query)
{
$this->query = $query;
}
public static function build(QueryInterface $query): QueryData
{
return new self($query);
}
/**
* Añadir un parámetro a la consulta
@@ -69,28 +83,16 @@ final class QueryData
$this->params = $data;
}
public function getQuery(): string
public function getQuery(): QueryInterface
{
if (empty($this->query)) {
return $this->select.
' '.
$this->from.
' '.
$this->where.
' '.
$this->groupBy.
' '.
$this->order.
' '.
$this->limit;
}
return $this->query;
}
public function setQuery(string $query): void
public function setQuery(QueryInterface $query): QueryData
{
$this->query = $query;
return $this;
}
public function getMapClassName(): ?string
@@ -98,9 +100,11 @@ final class QueryData
return $this->mapClassName;
}
public function setMapClassName(string $mapClassName): void
public function setMapClassName(string $mapClassName): QueryData
{
$this->mapClassName = $mapClassName;
return $this;
}
public function isUseKeyPair(): bool
@@ -146,13 +150,26 @@ final class QueryData
$this->params = array_merge($this->params, $params);
}
public function getQueryCount(): string
/**
* @throws \SP\Core\Exceptions\QueryException
*/
public function getQueryCount(): QueryInterface
{
if (empty($this->queryCount)) {
return 'SELECT COUNT(*) '.$this->from.' '.$this->where;
if ($this->query instanceof Select) {
$countQuery = (clone $this->query)
->resetFlags()
->resetCols()
->resetOrderBy()
->resetGroupBy()
->resetHaving()
->page(0);
$countQuery->cols(['COUNT(*)']);
return $countQuery;
}
return $this->queryCount;
throw new QueryException(__u('Invalid query type for count'));
}
public function getFrom(): ?string
@@ -191,9 +208,11 @@ final class QueryData
return $this->onErrorMessage ?: __u('Error while querying');
}
public function setOnErrorMessage(string $onErrorMessage): void
public function setOnErrorMessage(string $onErrorMessage): QueryData
{
$this->onErrorMessage = $onErrorMessage;
return $this;
}
public function setGroupBy(string $groupBy): void

View File

@@ -93,7 +93,7 @@ class AccountRepositoryTest extends UnitaryTestCase
->with($callback, false)
->willReturn($expected);
$this->assertEquals($expected, $this->accountRepository->getPasswordForId(1, new QueryCondition()));
$this->assertEquals($expected, $this->accountRepository->getPasswordForId(1));
}
public function testGetPasswordHistoryForId(): void

View File

@@ -1,10 +1,10 @@
<?php
/**
/*
* sysPass
*
* @author nuxsmin
* @link https://syspass.org
* @copyright 2012-2018, Rubén Domínguez nuxsmin@$syspass.org
* @author nuxsmin
* @link https://syspass.org
* @copyright 2012-2022, Rubén Domínguez nuxsmin@$syspass.org
*
* This file is part of sysPass.
*
@@ -19,7 +19,7 @@
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with sysPass. If not, see <http://www.gnu.org/licenses/>.
* along with sysPass. If not, see <http://www.gnu.org/licenses/>.
*/
namespace SP\Tests\Services\Account;
@@ -34,7 +34,7 @@ use SP\Core\Exceptions\QueryException;
use SP\Core\Exceptions\SPException;
use SP\DataModel\UserPreferencesData;
use SP\Domain\Account\AccountSearchServiceInterface;
use SP\Domain\Account\Services\AccountSearchFilter;
use SP\Domain\Account\Search\AccountSearchFilter;
use SP\Domain\Account\Services\AccountSearchItem;
use SP\Domain\Account\Services\AccountSearchService;
use SP\Domain\User\Services\UserLoginResponse;
@@ -154,7 +154,7 @@ class AccountSearchServiceTest extends DatabaseTestCase
$searchFilter->setCategoryId($id);
// Comprobar un Id de categoría
$result = self::$service->processSearchResults($searchFilter);
$result = self::$service->getByFilter($searchFilter);
$this->assertInstanceOf(QueryResult::class, $result);
if ($rows > 0) {
@@ -181,7 +181,7 @@ class AccountSearchServiceTest extends DatabaseTestCase
$searchFilter->setLimitCount(10);
$searchFilter->setCategoryId(10);
$result = self::$service->processSearchResults($searchFilter);
$result = self::$service->getByFilter($searchFilter);
$this->assertInstanceOf(QueryResult::class, $result);
$this->assertEquals(0, $result->getNumRows());
$this->assertCount(0, $result->getDataAsArray());
@@ -203,7 +203,7 @@ class AccountSearchServiceTest extends DatabaseTestCase
$searchFilter->setLimitCount(10);
$searchFilter->setClientId($id);
$result = self::$service->processSearchResults($searchFilter);
$result = self::$service->getByFilter($searchFilter);
$this->assertInstanceOf(QueryResult::class, $result);
$this->assertEquals($rows, $result->getNumRows());
@@ -240,7 +240,7 @@ class AccountSearchServiceTest extends DatabaseTestCase
$searchFilter->setClientId($clientId);
$searchFilter->setCategoryId($categoryId);
$result = self::$service->processSearchResults($searchFilter);
$result = self::$service->getByFilter($searchFilter);
$this->assertInstanceOf(QueryResult::class, $result);
$this->assertEquals($rows, $result->getNumRows());
@@ -264,7 +264,7 @@ class AccountSearchServiceTest extends DatabaseTestCase
$searchFilter->setLimitCount(10);
$searchFilter->setClientId(10);
$result = self::$service->processSearchResults($searchFilter);
$result = self::$service->getByFilter($searchFilter);
$this->assertInstanceOf(QueryResult::class, $result);
$this->assertEquals(0, $result->getNumRows());
$this->assertCount(0, $result->getDataAsArray());
@@ -286,7 +286,7 @@ class AccountSearchServiceTest extends DatabaseTestCase
$searchFilter->setLimitCount(10);
$searchFilter->setTxtSearch($string);
$result = self::$service->processSearchResults($searchFilter);
$result = self::$service->getByFilter($searchFilter);
$this->assertInstanceOf(QueryResult::class, $result);
$this->assertEquals($rows, $result->getNumRows());
@@ -315,7 +315,7 @@ class AccountSearchServiceTest extends DatabaseTestCase
$searchFilter->setLimitCount(10);
$searchFilter->setSearchFavorites(true);
$result = self::$service->processSearchResults($searchFilter);
$result = self::$service->getByFilter($searchFilter);
$this->assertInstanceOf(QueryResult::class, $result);
$this->assertEquals($rows, $result->getNumRows());
@@ -347,7 +347,7 @@ class AccountSearchServiceTest extends DatabaseTestCase
$searchFilter->setFilterOperator($operator);
$searchFilter->setTagsId($tagsId);
$result = self::$service->processSearchResults($searchFilter);
$result = self::$service->getByFilter($searchFilter);
$this->assertInstanceOf(QueryResult::class, $result);
/** @var AccountSearchItem[] $data */

View File

@@ -40,12 +40,12 @@ use SP\DataModel\AccountVData;
use SP\DataModel\ItemSearchData;
use SP\DataModel\ProfileData;
use SP\Domain\Account\AccountHistoryServiceInterface;
use SP\Domain\Account\Search\AccountSearchFilter;
use SP\Domain\Account\Out\AccountData;
use SP\Domain\Account\Services\AccountBulkRequest;
use SP\Domain\Account\Services\AccountHistoryService;
use SP\Domain\Account\Services\AccountPasswordRequest;
use SP\Domain\Account\Services\AccountRequest;
use SP\Domain\Account\Services\AccountSearchFilter;
use SP\Domain\Account\Services\AccountService;
use SP\Domain\Common\Services\ServiceException;
use SP\Domain\User\Services\UserLoginResponse;

View File

@@ -1,10 +1,10 @@
<?php
/**
/*
* sysPass
*
* @author nuxsmin
* @link https://syspass.org
* @copyright 2012-2018, Rubén Domínguez nuxsmin@$syspass.org
* @author nuxsmin
* @link https://syspass.org
* @copyright 2012-2022, Rubén Domínguez nuxsmin@$syspass.org
*
* This file is part of sysPass.
*
@@ -19,7 +19,7 @@
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with sysPass. If not, see <http://www.gnu.org/licenses/>.
* along with sysPass. If not, see <http://www.gnu.org/licenses/>.
*/
namespace SP\Tests\Services\Import;
@@ -34,7 +34,7 @@ use SP\Core\Exceptions\ConstraintException;
use SP\Core\Exceptions\QueryException;
use SP\Core\Exceptions\SPException;
use SP\DataModel\AccountSearchVData;
use SP\Domain\Account\Services\AccountSearchFilter;
use SP\Domain\Account\Search\AccountSearchFilter;
use SP\Domain\Account\Services\AccountService;
use SP\Domain\Category\Services\CategoryService;
use SP\Domain\Client\Services\ClientService;