Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
93.18% covered (success)
93.18%
41 / 44
85.71% covered (success)
85.71%
6 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
SearchResultSet
93.18% covered (success)
93.18%
41 / 44
85.71% covered (success)
85.71%
6 / 7
26.21
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 reviewResultSet
90.32% covered (success)
90.32%
28 / 31
0.00% covered (danger)
0.00%
0 / 1
14.18
 setResultSet
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getScore
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
7
 getResultSet
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getNumberOfResults
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setNumberOfResults
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3/**
4 * Implements result sets for phpMyFAQ search classes.
5 *
6 * This Source Code Form is subject to the terms of the Mozilla Public License,
7 * v. 2.0. If a copy of the MPL was not distributed with this file, You can
8 * obtain one at https://mozilla.org/MPL/2.0/.
9 *
10 * @package   phpMyFAQ
11 * @author    Thorsten Rinne <thorsten@phpmyfaq.de>
12 * @copyright 2010-2026 phpMyFAQ Team
13 * @license   https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0
14 * @link      https://www.phpmyfaq.de
15 * @since     2010-06-06
16 */
17
18declare(strict_types=1);
19
20namespace phpMyFAQ\Search;
21
22use phpMyFAQ\Configuration;
23use phpMyFAQ\Faq\Permission;
24use phpMyFAQ\User\CurrentUser;
25use stdClass;
26
27/**
28 * Class SearchResultSet
29 *
30 * @package phpMyFAQ\Search
31 */
32class SearchResultSet
33{
34    /**
35     * "Raw" search result set without permission checks and with possible
36     * duplicates.
37     *
38     * @var stdClass[]
39     */
40    protected array $rawResultSet = [];
41
42    /**
43     * "Reviewed" search result set with checked permissions and without
44     * duplicates.
45     *
46     * @var stdClass[]
47     */
48    protected array $reviewedResultSet = [];
49
50    /**
51     * Ordering of result set.
52     */
53    protected string $ordering;
54
55    /**
56     * Number of search results.
57     */
58    protected int $numberOfResults = 0;
59
60    /**
61     * Constructor.
62     *
63     * @param CurrentUser $currentUser User object
64     * @param Configuration $configuration Configuration object
65     */
66    public function __construct(
67        protected CurrentUser $currentUser,
68        private readonly Permission $faqPermission,
69        protected Configuration $configuration,
70    ) {
71    }
72
73    /**
74     * Check on user and group permissions and on duplicate FAQs.
75     *
76     * @param stdClass[] $resultSet Array with search results
77     */
78    public function reviewResultSet(array $resultSet): void
79    {
80        $this->setResultSet($resultSet);
81
82        $duplicateResults = [];
83        $currentGroupIds = [-1];
84
85        if (
86            'basic' !== $this->configuration->get(item: 'security.permLevel')
87            && isset($this->currentUser->perm) // @mago-expect lint:no-isset - typed property may be uninitialized
88        ) {
89            $permission = $this->currentUser->perm;
90            if (method_exists($permission, 'getUserGroups')) {
91                $currentGroupIds = $permission->getUserGroups($this->currentUser->getUserId());
92            }
93        }
94
95        foreach ($this->rawResultSet as $result) {
96            $permission = false;
97
98            // check permissions for groups
99            if ('medium' === $this->configuration->get(item: 'security.permLevel')) {
100                $groupPermissions = $this->faqPermission->get(Permission::GROUP, (int) $result->id);
101                $groupIds = $currentGroupIds;
102                foreach ($groupPermissions as $groupPermission) {
103                    if (!in_array($groupPermission, $groupIds, strict: true)) {
104                        continue;
105                    }
106
107                    $permission = true;
108                }
109            }
110
111            // check permission for a user
112            if ('basic' === $this->configuration->get(item: 'security.permLevel')) {
113                $userPermission = $this->faqPermission->get(Permission::USER, (int) $result->id);
114                $permission =
115                    in_array(-1, $userPermission, strict: true)
116                    || in_array($this->currentUser->getUserId(), $userPermission, strict: true);
117            }
118
119            // check on duplicates
120            $resultId = (int) $result->id;
121            if (array_key_exists($resultId, $duplicateResults)) {
122                continue;
123            }
124
125            $duplicateResults[$resultId] = true;
126
127            if (!property_exists($result, 'score') || $result->score === null) {
128                $result->score = $this->getScore($result);
129            }
130
131            if ($permission) {
132                $this->reviewedResultSet[] = $result;
133            }
134        }
135
136        $this->setNumberOfResults($this->reviewedResultSet);
137    }
138
139    /**
140     * Sets the "raw" search results.
141     *
142     * @param stdClass[] $resultSet Array with search results
143     */
144    public function setResultSet(array $resultSet): void
145    {
146        $this->rawResultSet = $resultSet;
147    }
148
149    public function getScore(stdClass $object): float
150    {
151        $score = 0.0;
152
153        if (property_exists($object, 'relevance_thema') && is_numeric($object->relevance_thema)) {
154            $score += (float) $object->relevance_thema;
155        }
156
157        if (property_exists($object, 'relevance_content') && is_numeric($object->relevance_content)) {
158            $score += (float) $object->relevance_content;
159        }
160
161        if (property_exists($object, 'relevance_keywords') && is_numeric($object->relevance_keywords)) {
162            $score += (float) $object->relevance_keywords;
163        }
164
165        return round(($score / 3) * 100);
166    }
167
168    /**
169     * Returns the "reviewed" search results.
170     *
171     * @return stdClass[]
172     */
173    public function getResultSet(): array
174    {
175        return $this->reviewedResultSet;
176    }
177
178    /**
179     * Returns the number search results.
180     */
181    public function getNumberOfResults(): int
182    {
183        return $this->numberOfResults;
184    }
185
186    /**
187     * Sets the number of search results.
188     *
189     * @param stdClass[] $resultSet
190     */
191    public function setNumberOfResults(array $resultSet): void
192    {
193        $this->numberOfResults = count($resultSet);
194    }
195}