Lines 98.23% 111 / 113
Methods 89.47% 17 / 19
Classes 0.00% 0 / 1
Covered by tests of size
Name Lines Methods CRAP
 __construct 100.00% 2 / 2 100.00% 1 / 1 1
 search 100.00% 47 / 47 100.00% 1 / 1 9
 setRelevanceRanking 100.00% 5 / 5 100.00% 1 / 1 1
 [phpMyFAQ\Search\SearchDatabase] getResultColumns 100.00% 1 / 1 100.00% 1 / 1 1
 [phpMyFAQ\Search\SearchDatabase] setResultColumns 100.00% 2 / 2 100.00% 1 / 1 1
 [phpMyFAQ\Search\SearchDatabase] getTable 100.00% 1 / 1 100.00% 1 / 1 1
 [phpMyFAQ\Search\SearchDatabase] setTable 100.00% 2 / 2 100.00% 1 / 1 1
 [phpMyFAQ\Search\SearchDatabase] getJoinedTable 100.00% 3 / 3 100.00% 1 / 1 3
 [phpMyFAQ\Search\SearchDatabase] setJoinedTable 100.00% 2 / 2 100.00% 1 / 1 1
 [phpMyFAQ\Search\SearchDatabase] getJoinedColumns 100.00% 4 / 4 100.00% 1 / 1 2
 [phpMyFAQ\Search\SearchDatabase] setJoinedColumns 100.00% 2 / 2 100.00% 1 / 1 1
 [phpMyFAQ\Search\SearchDatabase] getMatchingColumns 100.00% 1 / 1 100.00% 1 / 1 1
 [phpMyFAQ\Search\SearchDatabase] setMatchingColumns 100.00% 2 / 2 100.00% 1 / 1 1
 [phpMyFAQ\Search\SearchDatabase] getConditions 88.88% 8 / 9 0.00% 0 / 1 6.05
 [phpMyFAQ\Search\SearchDatabase] buildInClause 75.00% 3 / 4 0.00% 0 / 1 2.06
 [phpMyFAQ\Search\SearchDatabase] setConditions 100.00% 2 / 2 100.00% 1 / 1 1
 [phpMyFAQ\Search\SearchDatabase] getMatchClause 100.00% 21 / 21 100.00% 1 / 1 6
 [phpMyFAQ\Search\SearchDatabase] disableRelevance 100.00% 1 / 1 100.00% 1 / 1 1
 [phpMyFAQ\Search\SearchDatabase] escapeLikeWildcards 100.00% 2 / 2 100.00% 1 / 1 1
33class Mysqli extends SearchDatabase implements DatabaseInterface
34{
35    /**
36     * Constructor.
37     */
38    public function __construct(Configuration $configuration)
39    {
40        parent::__construct($configuration);
41        $this->relevanceSupport = true;
42    }
43
44    /**
45     * Prepares the search and executes it.
46     *
47     * @param  string $searchTerm Search term
48     * @throws Exception
49     */
50    #[Override]
51    public function search(string $searchTerm): mixed
52    {
53        if (is_numeric($searchTerm) && (bool) $this->configuration->get(item: 'search.searchForSolutionId')) {
54            return parent::search($searchTerm);
55        }
56
57        $relevance = (bool) $this->configuration->get(item: 'search.enableRelevance');
58        $orderBy = '';
59        $columns = $this->getResultColumns();
60        if ($this->relevanceSupport && $relevance) {
61            $columns .= ', ' . $this->setRelevanceRanking($searchTerm);
62            $orderBy = 'ORDER BY score DESC';
63        }
64        if (!$this->relevanceSupport || !$relevance) {
65            $orderBy = '';
66        }
67
68        $chars = [
69            "\xe2\x80\x98",
70            "\xe2\x80\x99",
71            "\xe2\x80\x9c",
72            "\xe2\x80\x9d",
73            "\xe2\x80\x93",
74            "\xe2\x80\x94",
75            "\xe2\x80\xa6",
76        ];
77        $replace = ["'", "'", '"', '"', '-', '--', '...'];
78        $searchTerm = str_replace($chars, $replace, $searchTerm);
79        $query = sprintf(
80            "
81                SELECT
82                    %s
83                FROM 
84                    %s %s %s
85                WHERE
86                    MATCH (%s) AGAINST ('%s' IN BOOLEAN MODE)
87                    %s
88                    %s",
89            $columns,
90            $this->getTable(),
91            $this->getJoinedTable(),
92            $this->getJoinedColumns(),
93            $this->getMatchingColumns(),
94            $this->configuration->getDb()->escape($searchTerm),
95            $this->getConditions(),
96            $orderBy,
97        );
98        $this->resultSet = $this->configuration->getDb()->query($query);
99        // Fallback for searches with less than three characters
100        if (false !== $this->resultSet && 0 === $this->configuration->getDb()->numRows($this->resultSet)) {
101            $query = sprintf(
102                '
103                    SELECT
104                        %s
105                    FROM 
106                        %s %s %s
107                    WHERE
108                        %s
109                        %s',
110                $this->getResultColumns(),
111                $this->getTable(),
112                $this->getJoinedTable(),
113                $this->getJoinedColumns(),
114                $this->getMatchClause($searchTerm),
115                $this->getConditions(),
116            );
117
118            $this->resultSet = $this->configuration->getDb()->query($query);
119        }
120
121        return $this->resultSet;
122    }
123
124    /**
125     * Add the matching columns into the columns for the relevance ranking
126     */
127    public function setRelevanceRanking(string $searchTerm): string
128    {
129        return sprintf(
130            "MATCH (%s) AGAINST ('%s' IN BOOLEAN MODE) as score",
131            $this->getMatchingColumns(),
132            $this->configuration->getDb()->escape($searchTerm),
133        );
134    }
135}

Inherited from phpMyFAQ\Search\SearchDatabase

112    public function getResultColumns(): string
113    {
114        return implode(', ', $this->resultColumns);
115    }
122    public function setResultColumns(array $columns): SearchDatabase
123    {
124        $this->resultColumns = $columns;
125
126        return $this;
127    }
132    public function getTable(): string
133    {
134        return $this->table;
135    }
142    public function setTable(string $table): SearchDatabase
143    {
144        $this->table = $table;
145
146        return $this;
147    }
152    public function getJoinedTable(): string
153    {
154        if ($this->joinedTable === '' || $this->joinedTable === '0') {
155            return '';
156        }
157
158        return ' LEFT JOIN ' . $this->joinedTable . ' ON ';
159    }
166    public function setJoinedTable(string $joinedTable = ''): SearchDatabase
167    {
168        $this->joinedTable = $joinedTable;
169
170        return $this;
171    }
176    public function getJoinedColumns(): string
177    {
178        $joinedColumns = '';
179
180        foreach ($this->joinedColumns as $joinedColumn) {
181            $joinedColumns .= $joinedColumn . ' AND ';
182        }
183
184        return Strings::substr($joinedColumns, 0, -4);
185    }
192    public function setJoinedColumns(array $joinedColumns): SearchDatabase
193    {
194        $this->joinedColumns = $joinedColumns;
195
196        return $this;
197    }
202    public function getMatchingColumns(): string
203    {
204        return implode(', ', $this->matchingColumns);
205    }
212    public function setMatchingColumns(array $matchingColumns): SearchDatabase
213    {
214        $this->matchingColumns = $matchingColumns;
215
216        return $this;
217    }
222    public function getConditions(): string
223    {
224        $conditions = '';
225        $db = $this->configuration->getDb();
226
227        foreach ($this->conditions as $column => $value) {
228            if (!preg_match('/^[A-Za-z_][A-Za-z0-9_.]*$/', $column)) {
229                continue;
230            }
231
232            $conditions .= match (true) {
233                is_array($value) => $this->buildInClause($column, $value),
234                is_int($value) => ' AND ' . $column . ' = ' . $value,
235                default => ' AND ' . $column . " = '" . $db->escape((string) $value) . "'",
236            };
237        }
238
239        return $conditions;
240    }
245    private function buildInClause(string $column, array $value): string
246    {
247        $ids = array_map(static fn($v): int => (int) $v, $value);
248        if ($ids === []) {
249            return '';
250        }
251
252        return ' AND ' . $column . ' IN (' . implode(', ', $ids) . ')';
253    }
260    public function setConditions(array $conditions): SearchDatabase
261    {
262        $this->conditions = $conditions;
263
264        return $this;
265    }
272    public function getMatchClause(string $searchTerm = ''): string
273    {
274        $splitTerms = Strings::preg_split("/\s+/", $searchTerm);
275        $keys = is_array($splitTerms) ? $splitTerms : [];
276        $numKeys = count($keys);
277        $numMatch = count($this->matchingColumns);
278        $where = '';
279
280        for ($i = 0; $i < $numKeys; ++$i) {
281            if ($where !== '') {
282                $where .= ' OR';
283            }
284
285            $where .= ' (';
286            for ($j = 0; $j < $numMatch; ++$j) {
287                if ($j !== 0) {
288                    $where .= ' OR ';
289                }
290
291                $where = sprintf(
292                    "%s%s LIKE '%%%s%%' ESCAPE '%s'",
293                    $where,
294                    $this->matchingColumns[$j],
295                    self::escapeLikeWildcards($this->configuration->getDb()->escape((string) $keys[$i])),
296                    self::LIKE_ESCAPE_CHARACTER,
297                );
298            }
299
300            $where .= ')';
301        }
302
303        return $where;
304    }
310    public function disableRelevance(): void
311    {
312        $this->relevanceSupport = false;
313    }
319    protected static function escapeLikeWildcards(string $term): string
320    {
321        $escape = self::LIKE_ESCAPE_CHARACTER;
322
323        return str_replace([$escape, '%', '_'], [$escape . $escape, $escape . '%', $escape . '_'], $term);
324    }