AccountSearchService::processFilterItems()   C
last analyzed

Complexity

Conditions 14
Paths 27

Size

Total Lines 53
Code Lines 46

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 14
eloc 46
c 1
b 0
f 0
nc 27
nop 2
dl 0
loc 53
rs 6.2666

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * sysPass
4
 *
5
 * @author    nuxsmin
6
 * @link      https://syspass.org
7
 * @copyright 2012-2019, Rubén Domínguez nuxsmin@$syspass.org
8
 *
9
 * This file is part of sysPass.
10
 *
11
 * sysPass is free software: you can redistribute it and/or modify
12
 * it under the terms of the GNU General Public License as published by
13
 * the Free Software Foundation, either version 3 of the License, or
14
 * (at your option) any later version.
15
 *
16
 * sysPass is distributed in the hope that it will be useful,
17
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
18
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19
 * GNU General Public License for more details.
20
 *
21
 * You should have received a copy of the GNU General Public License
22
 *  along with sysPass.  If not, see <http://www.gnu.org/licenses/>.
23
 */
24
25
namespace SP\Services\Account;
26
27
use Exception;
28
use Psr\Container\ContainerExceptionInterface;
29
use Psr\Container\NotFoundExceptionInterface;
30
use SP\Config\ConfigData;
31
use SP\Core\Acl\Acl;
32
use SP\Core\Exceptions\ConstraintException;
33
use SP\Core\Exceptions\QueryException;
34
use SP\Core\Exceptions\SPException;
35
use SP\DataModel\AccountSearchVData;
36
use SP\DataModel\Dto\AccountAclDto;
37
use SP\DataModel\Dto\AccountCache;
38
use SP\Mvc\Model\QueryCondition;
39
use SP\Repositories\Account\AccountRepository;
40
use SP\Repositories\Account\AccountToTagRepository;
41
use SP\Repositories\Account\AccountToUserGroupRepository;
42
use SP\Repositories\Account\AccountToUserRepository;
43
use SP\Services\Service;
44
use SP\Services\User\UserService;
45
use SP\Services\UserGroup\UserGroupService;
46
use SP\Storage\Database\QueryResult;
47
use SP\Storage\File\FileCache;
48
use SP\Storage\File\FileCacheInterface;
49
use SP\Storage\File\FileException;
50
use SP\Util\Filter;
51
52
defined('APP_ROOT') || die();
53
54
/**
55
 * Class AccountSearchService para la gestión de búsquedas de cuentas
56
 */
57
final class AccountSearchService extends Service
58
{
59
    /**
60
     * Regex filters for special searching
61
     */
62
    const FILTERS = [
63
        'condition' => [
64
            'subject' => ['is', 'not'],
65
            'condition' => ['expired', 'private']
66
        ],
67
        'items' => [
68
            'subject' => ['id', 'user', 'group', 'file', 'owner', 'maingroup', 'client', 'category', 'name_regex'],
69
            'condition' => null
70
        ],
71
        'operator' => [
72
            'subject' => ['op'],
73
            'condition' => ['and', 'or']
74
        ]
75
    ];
76
77
    const COLORS_CACHE_FILE = CACHE_PATH . DIRECTORY_SEPARATOR . 'colors.cache';
78
79
    /**
80
     * Cache expire time
81
     */
82
    const CACHE_EXPIRE = 86400;
83
84
    /**
85
     * Colores para resaltar las cuentas
86
     */
87
    const COLORS = [
88
        '2196F3',
89
        '03A9F4',
90
        '00BCD4',
91
        '009688',
92
        '4CAF50',
93
        '8BC34A',
94
        'CDDC39',
95
        'FFC107',
96
        '795548',
97
        '607D8B',
98
        '9E9E9E',
99
        'FF5722',
100
        'F44336',
101
        'E91E63',
102
        '9C27B0',
103
        '673AB7',
104
        '3F51B5',
105
    ];
106
    /**
107
     * @var AccountFilterUser
108
     */
109
    private $accountFilterUser;
110
    /**
111
     * @var AccountAclService
112
     */
113
    private $accountAclService;
114
    /**
115
     * @var ConfigData
116
     */
117
    private $configData;
118
    /**
119
     * @var AccountToTagRepository
120
     */
121
    private $accountToTagRepository;
122
    /**
123
     * @var AccountToUserRepository
124
     */
125
    private $accountToUserRepository;
126
    /**
127
     * @var AccountToUserGroupRepository
128
     */
129
    private $accountToUserGroupRepository;
130
    /**
131
     * @var FileCacheInterface
132
     */
133
    private $colorCache;
134
    /**
135
     * @var array
136
     */
137
    private $accountColor;
138
    /**
139
     * @var AccountRepository
140
     */
141
    private $accountRepository;
142
    /**
143
     * @var string
144
     */
145
    private $cleanString;
146
    /**
147
     * @var string
148
     */
149
    private $filterOperator;
150
151
    /**
152
     * Procesar los resultados de la búsqueda y crear la variable que contiene los datos de cada cuenta
153
     * a mostrar.
154
     *
155
     * @param AccountSearchFilter $accountSearchFilter
156
     *
157
     * @return QueryResult
158
     * @throws ConstraintException
159
     * @throws QueryException
160
     * @throws SPException
161
     */
162
    public function processSearchResults(AccountSearchFilter $accountSearchFilter)
163
    {
164
        $accountSearchFilter->setStringFilters($this->analyzeQueryFilters($accountSearchFilter->getTxtSearch()));
165
166
        if ($accountSearchFilter->getFilterOperator() === null
167
            || $this->filterOperator !== null
168
        ) {
169
            $accountSearchFilter->setFilterOperator($this->filterOperator);
170
        }
171
172
        $accountSearchFilter->setCleanTxtSearch($this->cleanString);
173
174
        $queryResult = $this->accountRepository->getByFilter($accountSearchFilter, $this->accountFilterUser->getFilter($accountSearchFilter->getGlobalSearch()));
0 ignored issues
show
Bug introduced by
$accountSearchFilter->getGlobalSearch() of type integer is incompatible with the type boolean expected by parameter $useGlobalSearch of SP\Services\Account\AccountFilterUser::getFilter(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

174
        $queryResult = $this->accountRepository->getByFilter($accountSearchFilter, $this->accountFilterUser->getFilter(/** @scrutinizer ignore-type */ $accountSearchFilter->getGlobalSearch()));
Loading history...
175
176
        // Variables de configuración
177
        $maxTextLength = $this->configData->isResultsAsCards() ? 40 : 60;
178
179
        $accountLinkEnabled = $this->context->getUserData()->getPreferences()->isAccountLink() || $this->configData->isAccountLink();
180
        $favorites = $this->dic->get(AccountToFavoriteService::class)->getForUserId($this->context->getUserData()->getId());
181
182
        $accountsData = [];
183
184
        /** @var AccountSearchVData $accountSearchData */
185
        foreach ($queryResult->getDataAsArray() as $accountSearchData) {
186
            $cache = $this->getCacheForAccount($accountSearchData);
187
188
            // Obtener la ACL de la cuenta
189
            $accountAcl = $this->accountAclService->getAcl(
190
                Acl::ACCOUNT_SEARCH,
191
                AccountAclDto::makeFromAccountSearch($accountSearchData, $cache->getUsers(), $cache->getUserGroups())
192
            );
193
194
            // Propiedades de búsqueda de cada cuenta
195
            $accountsSearchItem = new AccountSearchItem($accountSearchData, $accountAcl, $this->configData);
196
197
            if (!$accountSearchData->getIsPrivate()) {
198
                $accountsSearchItem->setUsers($cache->getUsers());
199
                $accountsSearchItem->setUserGroups($cache->getUserGroups());
200
            }
201
202
            $accountsSearchItem->setTags($this->accountToTagRepository->getTagsByAccountId($accountSearchData->getId())->getDataAsArray());
203
            $accountsSearchItem->setTextMaxLength($maxTextLength);
204
            $accountsSearchItem->setColor($this->pickAccountColor($accountSearchData->getClientId()));
205
            $accountsSearchItem->setLink($accountLinkEnabled);
206
            $accountsSearchItem->setFavorite(isset($favorites[$accountSearchData->getId()]));
207
208
            $accountsData[] = $accountsSearchItem;
209
        }
210
211
        return QueryResult::fromResults($accountsData, $queryResult->getTotalNumRows());
212
    }
213
214
    /**
215
     * Analizar la cadena de consulta por eqituetas especiales y devolver un objeto
216
     * QueryCondition con los filtros
217
     *
218
     * @param $string
219
     *
220
     * @return QueryCondition
221
     * @throws ContainerExceptionInterface
222
     * @throws NotFoundExceptionInterface
223
     */
224
    public function analyzeQueryFilters($string)
225
    {
226
        $this->cleanString = null;
227
        $this->filterOperator = null;
228
229
        $queryCondition = new QueryCondition();
230
231
        $match = preg_match_all(
232
            '/(?<search>(?<!:)\b[^:]+\b(?!:))|(?<filter_subject>[a-zа-я_]+):(?!\s]*)"?(?<filter_condition>[^":]+)"?/u',
233
            $string,
234
            $filters
235
        );
236
237
        if ($match !== false && $match > 0) {
238
            if (!empty($filters['search'][0])) {
239
                $this->cleanString = Filter::safeSearchString(trim($filters['search'][0]));
240
            }
241
242
            $filtersAndValues = array_filter(
243
                array_combine(
0 ignored issues
show
Bug introduced by
It seems like array_combine($filters['...rs['filter_condition']) can also be of type false; however, parameter $input of array_filter() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

243
                /** @scrutinizer ignore-type */ array_combine(
Loading history...
244
                    $filters['filter_subject'],
245
                    $filters['filter_condition']
246
                )
247
            );
248
249
            if (!empty($filtersAndValues)) {
250
                $filtersItem = array_filter($filtersAndValues, function ($value, $key) {
251
                    return in_array($key, self::FILTERS['items']['subject'], true)
252
                        && $value !== '';
253
                }, ARRAY_FILTER_USE_BOTH);
254
255
                if (!empty($filtersItem)) {
256
                    $this->processFilterItems($filtersItem, $queryCondition);
257
                }
258
259
                $filtersOperator = array_filter($filtersAndValues, function ($value, $key) {
260
                    return in_array($key, self::FILTERS['operator']['subject'], true)
261
                        && in_array($value, self::FILTERS['operator']['condition'], true);
262
                }, ARRAY_FILTER_USE_BOTH);
263
264
                if (!empty($filtersOperator)) {
265
                    $this->processFilterOperator($filtersOperator);
266
                }
267
268
                $filtersCondition = array_filter(array_map(function ($subject, $condition) {
269
                    if (in_array($subject, self::FILTERS['condition']['subject'], true)
270
                        && in_array($condition, self::FILTERS['condition']['condition'], true)
271
                    ) {
272
                        return $subject . ':' . $condition;
273
                    }
274
275
                    return null;
276
                }, $filters['filter_subject'], $filters['filter_condition']));
277
278
                if (!empty($filtersCondition)) {
279
                    $this->processFilterIs($filtersCondition, $queryCondition);
280
                }
281
            }
282
        }
283
284
        return $queryCondition;
285
    }
286
287
    /**
288
     * @param array          $filters
289
     * @param QueryCondition $queryCondition
290
     */
291
    private function processFilterItems(array $filters, QueryCondition $queryCondition)
292
    {
293
        foreach ($filters as $filter => $text) {
294
            try {
295
                switch ($filter) {
296
                    case 'user':
297
                        $userData = $this->dic->get(UserService::class)->getByLogin(Filter::safeSearchString($text));
298
299
                        if (is_object($userData)) {
300
                            $queryCondition->addFilter(
301
                                'Account.userId = ? OR Account.userGroupId = ? OR Account.id IN 
302
                                        (SELECT AccountToUser.accountId FROM AccountToUser WHERE AccountToUser.accountId = Account.id AND AccountToUser.userId = ? 
303
                                        UNION 
304
                                        SELECT AccountToUserGroup.accountId FROM AccountToUserGroup WHERE AccountToUserGroup.accountId = Account.id AND AccountToUserGroup.userGroupId = ?)',
305
                                [$userData->getId(), $userData->getUserGroupId(), $userData->getId(), $userData->getUserGroupId()]);
306
                        }
307
                        break;
308
                    case 'owner':
309
                        $text = '%' . Filter::safeSearchString($text) . '%';
310
                        $queryCondition->addFilter(
311
                            'Account.userLogin LIKE ? OR Account.userName LIKE ?',
312
                            [$text, $text]);
313
                        break;
314
                    case 'group':
315
                        $userGroupData = $this->dic->get(UserGroupService::class)->getByName(Filter::safeSearchString($text));
316
317
                        if (is_object($userGroupData)) {
318
                            $queryCondition->addFilter(
319
                                'Account.userGroupId = ? OR Account.id IN (SELECT AccountToUserGroup.accountId FROM AccountToUserGroup WHERE AccountToUserGroup.accountId = id AND AccountToUserGroup.userGroupId = ?)',
320
                                [$userGroupData->getId(), $userGroupData->getId()]);
321
                        }
322
                        break;
323
                    case 'maingroup':
324
                        $queryCondition->addFilter('Account.userGroupName LIKE ?', ['%' . Filter::safeSearchString($text) . '%']);
325
                        break;
326
                    case 'file':
327
                        $queryCondition->addFilter('Account.id IN (SELECT AccountFile.accountId FROM AccountFile WHERE AccountFile.name LIKE ?)', ['%' . $text . '%']);
328
                        break;
329
                    case 'id':
330
                        $queryCondition->addFilter('Account.id = ?', [(int)$text]);
331
                        break;
332
                    case 'client':
333
                        $queryCondition->addFilter('Account.clientName LIKE ?', ['%' . Filter::safeSearchString($text) . '%']);
334
                        break;
335
                    case 'category':
336
                        $queryCondition->addFilter('Account.categoryName LIKE ?', ['%' . Filter::safeSearchString($text) . '%']);
337
                        break;
338
                    case 'name_regex':
339
                        $queryCondition->addFilter('Account.name REGEXP ?', [$text]);
340
                        break;
341
                }
342
            } catch (Exception $e) {
343
                processException($e);
344
            }
345
        }
346
    }
347
348
    /**
349
     * @param array $filters
350
     */
351
    private function processFilterOperator(array $filters)
352
    {
353
        switch ($filters['op']) {
354
            case 'and':
355
                $this->filterOperator = QueryCondition::CONDITION_AND;
356
                break;
357
            case 'or':
358
                $this->filterOperator = QueryCondition::CONDITION_OR;
359
                break;
360
        }
361
    }
362
363
    /**
364
     * @param array          $filters
365
     * @param QueryCondition $queryCondition
366
     */
367
    private function processFilterIs(array $filters, QueryCondition $queryCondition)
368
    {
369
        foreach ($filters as $filter) {
370
            switch ($filter) {
371
                case 'is:expired':
372
                    $queryCondition->addFilter(
373
                        'Account.passDateChange > 0 AND UNIX_TIMESTAMP() > Account.passDateChange',
374
                        []);
375
                    break;
376
                case 'not:expired':
377
                    $queryCondition->addFilter(
378
                        'Account.passDateChange = 0 OR Account.passDateChange IS NULL OR UNIX_TIMESTAMP() < Account.passDateChange',
379
                        []);
380
                    break;
381
                case 'is:private':
382
                    $queryCondition->addFilter(
383
                        '(Account.isPrivate = 1 AND Account.userId = ?) OR (Account.isPrivateGroup = 1 AND Account.userGroupId = ?)',
384
                        [$this->context->getUserData()->getId(), $this->context->getUserData()->getUserGroupId()]);
385
                    break;
386
                case 'not:private':
387
                    $queryCondition->addFilter(
388
                        '(Account.isPrivate = 0 OR Account.isPrivate IS NULL) AND (Account.isPrivateGroup = 0 OR Account.isPrivateGroup IS NULL)');
389
                    break;
390
            }
391
        }
392
    }
393
394
    /**
395
     * Devolver los accesos desde la caché
396
     *
397
     * @param AccountSearchVData $accountSearchData
398
     *
399
     * @return AccountCache
400
     * @throws ConstraintException
401
     * @throws QueryException
402
     */
403
    protected function getCacheForAccount(AccountSearchVData $accountSearchData)
404
    {
405
        $accountId = $accountSearchData->getId();
406
407
        /** @var AccountCache[] $cache */
408
        $cache = $this->context->getAccountsCache();
409
410
        $hasCache = $cache !== null;
411
412
        if ($cache === false
413
            || !isset($cache[$accountId])
414
            || $cache[$accountId]->getTime() < (int)strtotime($accountSearchData->getDateEdit())
415
        ) {
416
            $cache[$accountId] = new AccountCache(
417
                $accountId,
418
                $this->accountToUserRepository->getUsersByAccountId($accountId)->getDataAsArray(),
419
                $this->accountToUserGroupRepository->getUserGroupsByAccountId($accountId)->getDataAsArray());
420
421
            if ($hasCache) {
0 ignored issues
show
introduced by
The condition $hasCache is always true.
Loading history...
422
                $this->context->setAccountsCache($cache);
0 ignored issues
show
Bug introduced by
The method setAccountsCache() does not exist on SP\Core\Context\ContextInterface. It seems like you code against a sub-type of SP\Core\Context\ContextInterface such as SP\Core\Context\SessionContext. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

422
                $this->context->/** @scrutinizer ignore-call */ 
423
                                setAccountsCache($cache);
Loading history...
423
            }
424
        }
425
426
        return $cache[$accountId];
427
    }
428
429
    /**
430
     * Seleccionar un color para la cuenta
431
     *
432
     * @param int $id El id del elemento a asignar
433
     *
434
     * @return string
435
     */
436
    private function pickAccountColor($id)
437
    {
438
        if ($this->accountColor !== null && isset($this->accountColor[$id])) {
439
            return $this->accountColor[$id];
440
        }
441
442
        // Se asigna el color de forma aleatoria a cada id
443
        $this->accountColor[$id] = '#' . self::COLORS[array_rand(self::COLORS)];
444
445
        try {
446
            $this->colorCache->save($this->accountColor);
447
448
            logger('Saved accounts color cache');
449
450
            return $this->accountColor[$id];
451
        } catch (FileException $e) {
452
            processException($e);
453
454
            return '';
455
        }
456
    }
457
458
    /**
459
     * @return string
460
     */
461
    public function getCleanString()
462
    {
463
        return $this->cleanString;
464
    }
465
466
    /**
467
     * @throws ContainerExceptionInterface
468
     * @throws NotFoundExceptionInterface
469
     */
470
    protected function initialize()
471
    {
472
        $this->accountRepository = $this->dic->get(AccountRepository::class);
473
        $this->accountToTagRepository = $this->dic->get(AccountToTagRepository::class);
474
        $this->accountToUserRepository = $this->dic->get(AccountToUserRepository::class);
475
        $this->accountToUserGroupRepository = $this->dic->get(AccountToUserGroupRepository::class);
476
        $this->colorCache = new FileCache(self::COLORS_CACHE_FILE);
477
        $this->accountAclService = $this->dic->get(AccountAclService::class);
478
        $this->accountFilterUser = $this->dic->get(AccountFilterUser::class);
479
        $this->configData = $this->config->getConfigData();
480
481
        $this->loadColors();
482
    }
483
484
    /**
485
     * Load colors from cache
486
     */
487
    private function loadColors()
488
    {
489
        try {
490
            $this->accountColor = $this->colorCache->load();
491
492
            logger('Loaded accounts color cache');
493
        } catch (FileException $e) {
494
            processException($e);
495
        }
496
    }
497
}