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

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    }