Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
63.27% covered (warning)
63.27%
62 / 98
44.44% covered (danger)
44.44%
12 / 27
CRAP
0.00% covered (danger)
0.00%
0 / 1
SearchProxy
63.27% covered (warning)
63.27%
62 / 98
44.44% covered (danger)
44.44%
12 / 27
124.33
0.00% covered (danger)
0.00%
0 / 1
 getAdvancedSearch
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDefaultSearch
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 withAdvancedSearch
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 withDefaultSearch
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 searchByQuery
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 search
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 query
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 flush
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 index
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 remove
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 supportCustomIndex
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 extendGenerisSearchWhiteList
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 removeFromGenerisSearchWhiteList
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 getGenerisSearchUriWhitelist
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 getSearchSettingsService
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 executeSearch
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
6
 getResultSetResponseNormalizer
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getAdvancedSearchChecker
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getIdentifierSearcher
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getQueryFactory
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFeatureFlagChecker
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isForcingDefaultSearch
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 allowIdentifierSearch
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getIndexSearch
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getService
71.43% covered (warning)
71.43%
5 / 7
0.00% covered (danger)
0.00%
0 / 1
3.21
 getAdvancedSearchQueryString
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
2.01
 applySearchConditions
40.00% covered (danger)
40.00%
2 / 5
0.00% covered (danger)
0.00%
0 / 1
4.94
1<?php
2
3/**
4 * This program is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU General Public License
6 * as published by the Free Software Foundation; under version 2
7 * of the License (non-upgradable).
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17 *
18 * Copyright (c) 2020-2022 (original work) Open Assessment Technologies SA;
19 */
20
21declare(strict_types=1);
22
23namespace oat\tao\model\search;
24
25use Exception;
26use InvalidArgumentException;
27use oat\generis\model\GenerisRdf;
28use oat\generis\model\OntologyAwareTrait;
29use oat\oatbox\service\ConfigurableService;
30use oat\tao\model\AdvancedSearch\AdvancedSearchChecker;
31use oat\tao\model\featureFlag\FeatureFlagChecker;
32use oat\tao\model\featureFlag\FeatureFlagCheckerInterface;
33use oat\tao\model\search\Contract\SearchSettingsServiceInterface;
34use oat\tao\model\search\Service\DefaultSearchSettingsService;
35use oat\tao\model\TaoOntology;
36use Psr\Http\Message\ServerRequestInterface;
37use tao_helpers_Uri;
38
39class SearchProxy extends ConfigurableService implements Search
40{
41    use OntologyAwareTrait;
42
43    public const OPTION_SEARCH_SETTINGS_SERVICE = 'search_settings_service';
44    public const OPTION_ADVANCED_SEARCH_CLASS = 'advanced_search_class';
45    public const OPTION_DEFAULT_SEARCH_CLASS = 'default_search_class';
46    public const OPTION_GENERIS_SEARCH_WHITELIST = 'generis_search_whitelist';
47    public const SAFE_NODES = [GenerisRdf::CLASS_ROLE];
48    public const OPTION_FORCE_CRITERIA = 'force_criteria';
49
50    public const GENERIS_SEARCH_DEFAULT_WHITELIST = [
51        GenerisRdf::CLASS_ROLE,
52        TaoOntology::CLASS_URI_TAO_USER,
53        TaoOntology::CLASS_URI_TREE,
54    ];
55
56    private const IGNORE_CRITERIA_FOR_STRUCTURES = [
57        'results',
58    ];
59
60    private const DISABLE_URI_SEARCH_FOR_ROOT_CLASSES = [
61        'results',
62    ];
63
64    public function getAdvancedSearch(): ?SearchInterface
65    {
66        return $this->getService(self::OPTION_ADVANCED_SEARCH_CLASS);
67    }
68
69    public function getDefaultSearch(): SearchInterface
70    {
71        $defaultSearch = $this->getService(self::OPTION_DEFAULT_SEARCH_CLASS);
72
73        if ($defaultSearch) {
74            return $defaultSearch;
75        }
76
77        throw new InvalidArgumentException(sprintf('Option %s is required', self::OPTION_DEFAULT_SEARCH_CLASS));
78    }
79
80    public function withAdvancedSearch(SearchInterface $search): self
81    {
82        $this->setOption(self::OPTION_ADVANCED_SEARCH_CLASS, $search);
83
84        return $this;
85    }
86
87    public function withDefaultSearch(SearchInterface $search): self
88    {
89        $this->setOption(self::OPTION_DEFAULT_SEARCH_CLASS, $search);
90
91        return $this;
92    }
93
94    /**
95     * @throws Exception
96     */
97    public function searchByQuery(SearchQuery $query): array
98    {
99        $results = $this->executeSearch($query);
100
101        return $this->getResultSetResponseNormalizer()
102            ->normalizeSafeClass($query, $results, '');
103    }
104
105    /**
106     * @throws Exception
107     */
108    public function search(ServerRequestInterface $request): array
109    {
110        $query = $this->getQueryFactory()->create($request);
111        $queryParams = $request->getQueryParams();
112        $results = $this->executeSearch($query);
113
114        if (
115            isset($queryParams['params']['rootNode'])
116            && in_array($queryParams['params']['rootNode'], self::SAFE_NODES, true)
117        ) {
118            return $this->getResultSetResponseNormalizer()
119                ->normalizeSafeClass($query, $results, $queryParams['params']['structure']);
120        }
121
122        return $this->getResultSetResponseNormalizer()
123            ->normalize($query, $results, $queryParams['params']['structure']);
124    }
125
126    /**
127     * @inheritDoc
128     */
129    public function query($queryString, $type, $start = 0, $count = 10, $order = 'id', $dir = 'DESC')
130    {
131        return $this->getIndexSearch()->query($queryString, $type, $start, $count, $order, $dir);
132    }
133
134    /**
135     * @inheritDoc
136     */
137    public function flush()
138    {
139        return $this->getIndexSearch()->flush();
140    }
141
142    /**
143     * @inheritDoc
144     */
145    public function index($documents)
146    {
147        return $this->getIndexSearch()->index($documents);
148    }
149
150    /**
151     * @inheritDoc
152     */
153    public function remove($resourceId)
154    {
155        return $this->getIndexSearch()->remove($resourceId);
156    }
157
158    /**
159     * @inheritDoc
160     */
161    public function supportCustomIndex()
162    {
163        return $this->getAdvancedSearch() !== null;
164    }
165
166    public function extendGenerisSearchWhiteList(array $whiteList): void
167    {
168        $this->setOption(
169            self::OPTION_GENERIS_SEARCH_WHITELIST,
170            array_merge(
171                $this->getOption(self::OPTION_GENERIS_SEARCH_WHITELIST, []),
172                $whiteList
173            )
174        );
175    }
176
177    public function removeFromGenerisSearchWhiteList(array $whiteList): void
178    {
179        $this->setOption(
180            self::OPTION_GENERIS_SEARCH_WHITELIST,
181            array_diff(
182                $this->getOption(self::OPTION_GENERIS_SEARCH_WHITELIST, []),
183                $whiteList
184            )
185        );
186    }
187
188    public function getGenerisSearchUriWhitelist(): array
189    {
190        return array_merge(
191            self::GENERIS_SEARCH_DEFAULT_WHITELIST,
192            $this->getOption(self::OPTION_GENERIS_SEARCH_WHITELIST, [])
193        );
194    }
195
196    public function getSearchSettingsService(): SearchSettingsServiceInterface
197    {
198        return $this->getServiceManager()
199            ->getContainer()
200            ->get($this->getOption(self::OPTION_SEARCH_SETTINGS_SERVICE) ?? DefaultSearchSettingsService::class);
201    }
202
203    private function executeSearch(SearchQuery $query): ResultSet
204    {
205        if ($query->isEmptySearch()) {
206            return new ResultSet([], 0);
207        }
208
209        if ($this->allowIdentifierSearch($query)) {
210            $result = $this->getIdentifierSearcher()->search($query);
211
212            if ($result->getTotalCount() > 0) {
213                return $result;
214            }
215        }
216
217        $this->applySearchConditions($query);
218
219        if ($this->isForcingDefaultSearch($query) || !$this->getAdvancedSearchChecker()->isEnabled()) {
220            return $this->getDefaultSearch()->query(
221                $query->getTerm(),
222                $query->getParentClass(),
223                $query->getStartRow(),
224                $query->getRows()
225            );
226        }
227
228        return $this->getAdvancedSearch()->query(
229            $this->getAdvancedSearchQueryString($query),
230            $query->getStructure(),
231            $query->getStartRow(),
232            $query->getRows(),
233            $query->getSortBy(),
234            $query->getSortOrder()
235        );
236    }
237
238    private function getResultSetResponseNormalizer(): ResultSetResponseNormalizer
239    {
240        return $this->getServiceManager()->getContainer()->get(ResultSetResponseNormalizer::class);
241    }
242
243    private function getAdvancedSearchChecker(): AdvancedSearchChecker
244    {
245        return $this->getServiceManager()->getContainer()->get(AdvancedSearchChecker::class);
246    }
247
248    private function getIdentifierSearcher(): IdentifierSearcher
249    {
250        return $this->getServiceManager()->getContainer()->get(IdentifierSearcher::class);
251    }
252
253    private function getQueryFactory(): SearchQueryFactory
254    {
255        return $this->getServiceManager()->getContainer()->get(SearchQueryFactory::class);
256    }
257
258    private function getFeatureFlagChecker(): FeatureFlagCheckerInterface
259    {
260        return $this->getServiceManager()->getContainer()->get(FeatureFlagChecker::class);
261    }
262
263    private function isForcingDefaultSearch(SearchQuery $query): bool
264    {
265        return in_array($query->getRootClass(), $this->getGenerisSearchUriWhitelist(), true);
266    }
267
268    private function allowIdentifierSearch(SearchQuery $query): bool
269    {
270        return !in_array($query->getStructure(), self::DISABLE_URI_SEARCH_FOR_ROOT_CLASSES, true);
271    }
272
273    private function getIndexSearch(): SearchInterface
274    {
275        return $this->getAdvancedSearchChecker()->isEnabled()
276            ? $this->getAdvancedSearch()
277            : $this->getDefaultSearch();
278    }
279
280    private function getService(string $option): ?SearchInterface
281    {
282        if (!$this->hasOption($option)) {
283            return null;
284        }
285
286        /** @var SearchInterface $search */
287        $search = $this->getOption($option);
288
289        if (is_string($search)) {
290            return $this->getServiceManager()->getContainer()->get($search);
291        }
292
293        $this->propagate($search);
294
295        return $search;
296    }
297
298    private function getAdvancedSearchQueryString(SearchQuery $query): string
299    {
300        if (in_array($query->getStructure(), self::IGNORE_CRITERIA_FOR_STRUCTURES, true)) {
301            return $query->getTerm();
302        }
303
304        return sprintf(
305            '%s AND parent_classes: "%s"',
306            $query->getTerm(),
307            $query->getParentClass()
308        );
309    }
310
311    private function applySearchConditions(SearchQuery $query): void
312    {
313        if (
314            $this->getFeatureFlagChecker()->isEnabled('FEATURE_FLAG_TRANSLATION_ENABLED') &&
315            in_array($query->getRootClass(), [TaoOntology::CLASS_URI_ITEM, TaoOntology::CLASS_URI_TEST], true)
316        ) {
317            $typeUri = tao_helpers_Uri::encode(TaoOntology::PROPERTY_TRANSLATION_TYPE);
318            $typeOriginalUri = tao_helpers_Uri::encode(TaoOntology::PROPERTY_VALUE_TRANSLATION_TYPE_ORIGINAL);
319
320            $query->setTerm(sprintf('%s AND %s:%s', $query->getTerm(), $typeUri, $typeOriginalUri));
321        }
322    }
323}