Lines 84.01% 184 / 219
Methods 94.11% 16 / 17
Classes 0.00% 0 / 1
Covered by tests of size
Name Lines Methods CRAP
 __construct 100.00% 1 / 1 100.00% 1 / 1 1
 setSearchTerm 100.00% 1 / 1 100.00% 1 / 1 1
 createAutoCompleteResult 100.00% 44 / 44 100.00% 1 / 1 5
 renderAdminSuggestionResult 100.00% 24 / 24 100.00% 1 / 1 5
 getSearchResult 63.91% 62 / 97 0.00% 0 / 1 30.58
 renderScore 100.00% 8 / 8 100.00% 1 / 1 5
 renderRelatedFaqs 100.00% 28 / 28 100.00% 1 / 1 5
 [phpMyFAQ\Helper\AbstractHelper] setCategory 100.00% 2 / 2 100.00% 1 / 1 1
 [phpMyFAQ\Helper\AbstractHelper] getCategory 100.00% 1 / 1 100.00% 1 / 1 1
 [phpMyFAQ\Helper\AbstractHelper] category 100.00% 1 / 1 100.00% 1 / 1 1
 [phpMyFAQ\Helper\AbstractHelper] plurals 100.00% 1 / 1 100.00% 1 / 1 1
 [phpMyFAQ\Helper\AbstractHelper] setCategoryRelation 100.00% 2 / 2 100.00% 1 / 1 1
 [phpMyFAQ\Helper\AbstractHelper] setTags 100.00% 2 / 2 100.00% 1 / 1 1
 [phpMyFAQ\Helper\AbstractHelper] setPlurals 100.00% 2 / 2 100.00% 1 / 1 1
 [phpMyFAQ\Helper\AbstractHelper] setSessionId 100.00% 2 / 2 100.00% 1 / 1 1
 [phpMyFAQ\Helper\AbstractHelper] setConfiguration 100.00% 2 / 2 100.00% 1 / 1 1
 [phpMyFAQ\Helper\AbstractHelper] getConfiguration 100.00% 1 / 1 100.00% 1 / 1 1
38class SearchHelper extends AbstractHelper
39{
40    /**
41     * Search term.
42     */
43    private string $searchTerm = '';
44
45    /**
46     * Constructor.
47     */
48    public function __construct(Configuration $configuration)
49    {
50        $this->configuration = $configuration;
51    }
52
53    /**
54     * Search term setter.
55     *
56     * @param string $searchTerm Search term
57     */
58    public function setSearchTerm(string $searchTerm): void
59    {
60        $this->searchTerm = $searchTerm;
61    }
62
63    /**
64     * Renders the results for Typehead.
65     *
66     * @param SearchResultSet $searchResultSet Result set object
67     */
68    public function createAutoCompleteResult(SearchResultSet $searchResultSet): array
69    {
70        $results = [];
71        $maxResults = (int) $this->configuration->get(item: 'records.numberOfRecordsPerPage');
72        $numOfResults = $searchResultSet->getNumberOfResults();
73
74        if (0 < $numOfResults) {
75            $i = 0;
76            foreach ($searchResultSet->getResultSet() as $result) {
77                if ($i > $maxResults) {
78                    continue;
79                }
80
81                $resultQuestion = (string) $result->question;
82                $question = html_entity_decode($resultQuestion, ENT_QUOTES | ENT_XML1 | ENT_HTML5, encoding: 'UTF-8');
83
84                // Check if this is a custom page result
85                $isCustomPage = ($result->content_type ?? null) === 'page';
86
87                if ($isCustomPage) {
88                    // Build the link to the custom page
89                    $currentUrl = sprintf(
90                        '%spage/%s.html',
91                        $this->configuration->getDefaultUrl(),
92                        (string) $result->slug,
93                    );
94
95                    $link = new Link($currentUrl, $this->configuration);
96                    $link->setTitle($resultQuestion);
97                    $faq = new stdClass();
98                    $faq->category = ''; // Custom pages don't have categories
99                    $faq->question = Utils::chopString($question, 15);
100                    $faq->url = $link->toString();
101
102                    $results[] = $faq;
103                    ++$i;
104                    continue;
105                }
106
107                // Build the link to the faq record
108                $currentUrl = sprintf(
109                    '%scontent/%d/%d/%s/%s.html?highlight=%s',
110                    $this->configuration->getDefaultUrl(),
111                    (int) $result->category_id,
112                    (int) $result->id,
113                    (string) $result->lang,
114                    TitleSlugifier::slug($resultQuestion),
115                    urlencode($this->searchTerm),
116                );
117
118                $link = new Link($currentUrl, $this->configuration);
119                $link->setTitle($resultQuestion);
120                $faq = new stdClass();
121                $faq->category = $this->category()->getPath((int) $result->category_id);
122                $faq->question = Utils::chopString($question, 15);
123                $faq->url = $link->toString();
124
125                $results[] = $faq;
126
127                ++$i;
128            }
129        }
130
131        return $results;
132    }
133
134    /**
135     * Renders the result page for Instant Response.
136     *
137     * @param SearchResultSet $searchResultSet SearchResultSet object
138     */
139    public function renderAdminSuggestionResult(SearchResultSet $searchResultSet): array
140    {
141        $confPerPage = (int) $this->configuration->get(item: 'records.numberOfRecordsPerPage');
142        $numOfResults = $searchResultSet->getNumberOfResults();
143        $results = [];
144
145        if (0 < $numOfResults) {
146            $i = 0;
147            foreach ($searchResultSet->getResultSet() as $result) {
148                if ($i > $confPerPage) {
149                    continue;
150                }
151
152                $faqId = (int) $result->id;
153                $faqLanguage = (string) $result->lang;
154
155                $solutionId = (int) ($result->solution_id ?? 0);
156                if ($solutionId === 0) {
157                    $faq = new Faq($this->configuration);
158                    $solutionId = $faq->getSolutionIdFromId($faqId, $faqLanguage);
159                }
160
161                // Build the link to the faq record
162                $currentUrl = sprintf('%ssolution_id_%d.html', $this->configuration->getDefaultUrl(), $solutionId);
163                $adminUrl = sprintf(
164                    '%sadmin/faq/edit/%d/%s',
165                    $this->configuration->getDefaultUrl(),
166                    $faqId,
167                    $faqLanguage,
168                );
169
170                $results[] = ['url' => $currentUrl, 'question' => (string) $result->question, 'adminUrl' => $adminUrl];
171                ++$i;
172            }
173        }
174
175        return $results;
176    }
177
178    /**
179     * Returns the result page for the main search page
180     *
181     * @return stdClass[]
182     * @throws Exception|CommonMarkException
183     */
184    public function getSearchResult(SearchResultSet $searchResultSet, int $currentPage): array
185    {
186        $results = [];
187        $confPerPage = (int) $this->configuration->get(item: 'records.numberOfRecordsPerPage');
188        $numOfResults = $searchResultSet->getNumberOfResults();
189
190        $lastPage = $currentPage * $confPerPage;
191        $firstPage = $lastPage - $confPerPage;
192
193        if (0 < $numOfResults) {
194            $counter = 0;
195            $displayedCounter = 0;
196            $faqHelper = new FaqHelper($this->configuration);
197
198            foreach ($searchResultSet->getResultSet() as $resultSet) {
199                $result = new stdClass();
200
201                if ($displayedCounter >= $confPerPage) {
202                    break;
203                }
204
205                ++$counter;
206                if ($counter <= $firstPage) {
207                    continue;
208                }
209
210                ++$displayedCounter;
211
212                // Check if this is a custom page result
213                $isCustomPage = ($resultSet->content_type ?? null) === 'page';
214
215                if ($isCustomPage) {
216                    // Handle custom page results
217                    $question = Utils::chopString(Strings::htmlentities((string) $resultSet->question), 15);
218                    $answerPreview = $faqHelper->renderAnswerPreview((string) $resultSet->answer, 20);
219
220                    $searchTerm = str_replace(
221                        search: ['^', '.', '?', '*', '+', '{', '}', '(', ')', '[', ']', '"'],
222                        replace: '',
223                        subject: $this->searchTerm,
224                    );
225                    $searchTerm = preg_quote($searchTerm, delimiter: '/');
226                    $searchItems = explode(' ', $searchTerm);
227
228                    if (
229                        true === $this->configuration->get(item: 'search.enableHighlighting')
230                        && Strings::strlen($searchItems[0]) > 1
231                    ) {
232                        foreach ($searchItems as $searchItem) {
233                            if (Strings::strlen($searchItem) <= 2) {
234                                continue;
235                            }
236
237                            $question = Utils::setHighlightedString($question, $searchItem);
238                            $answerPreview = Utils::setHighlightedString($answerPreview, $searchItem);
239                        }
240                    }
241
242                    // Build the link to the custom page
243                    $currentUrl = sprintf(
244                        '%spage/%s.html',
245                        $this->configuration->getDefaultUrl(),
246                        (string) $resultSet->slug,
247                    );
248
249                    $oLink = new Link($currentUrl, $this->configuration);
250                    $oLink->setTitle((string) $resultSet->question);
251
252                    $result->renderedScore = $this->renderScore(
253                        (is_numeric($resultSet->score ?? null) ? (float) $resultSet->score : 0.0) * 33,
254                    );
255                    $result->question = $question;
256                    $result->path = ''; // Custom pages don't have category paths
257                    $result->url = $oLink->toString();
258                    $result->answerPreview = $answerPreview;
259                    $result->isCustomPage = true;
260                    $results[] = $result;
261                    continue;
262                }
263
264                // Handle FAQ results
265                // Set language for the current category to fetch the correct category name
266                $this->category()->setLanguage((string) $resultSet->lang);
267
268                $categoryInfo = $this->category()->getCategoriesFromFaq((int) $resultSet->id);
269                $categoryInfo = array_values($categoryInfo); //Reset the array keys
270                $question = Utils::chopString(Strings::htmlentities((string) $resultSet->question), 15);
271                $answerPreview = Strings::htmlentities($faqHelper->renderAnswerPreview(
272                    (string) $resultSet->answer,
273                    20,
274                ));
275
276                $searchTerm = str_replace(
277                    search: ['^', '.', '?', '*', '+', '{', '}', '(', ')', '[', ']', '"'],
278                    replace: '',
279                    subject: $this->searchTerm,
280                );
281                $searchTerm = preg_quote($searchTerm, delimiter: '/');
282                $searchItems = explode(' ', $searchTerm);
283
284                if (
285                    true === $this->configuration->get(item: 'search.enableHighlighting')
286                    && Strings::strlen($searchItems[0]) > 1
287                ) {
288                    foreach ($searchItems as $searchItem) {
289                        if (Strings::strlen($searchItem) <= 2) {
290                            continue;
291                        }
292
293                        $question = Utils::setHighlightedString($question, $searchItem);
294                        $answerPreview = Utils::setHighlightedString($answerPreview, $searchItem);
295                    }
296                }
297
298                // Build the link to the faq record
299                $currentUrl = sprintf(
300                    '%scontent/%d/%d/%s/%s.html?highlight=%s',
301                    $this->configuration->getDefaultUrl(),
302                    (int) $resultSet->category_id,
303                    (int) $resultSet->id,
304                    (string) $resultSet->lang,
305                    TitleSlugifier::slug((string) $resultSet->question),
306                    urlencode($searchTerm),
307                );
308
309                $oLink = new Link($currentUrl, $this->configuration);
310                $oLink->setTitle((string) $resultSet->question);
311
312                $path = ($categoryInfo[0]['id'] ?? null) !== null
313                    ? $this->category()->getPath((int) $categoryInfo[0]['id'])
314                    : '';
315
316                $result->renderedScore = $this->renderScore(
317                    (is_numeric($resultSet->score ?? null) ? (float) $resultSet->score : 0.0) * 33,
318                );
319                $result->question = $question;
320                $result->path = $path;
321                $result->url = $oLink->toString();
322                $result->answerPreview = $answerPreview;
323
324                $results[] = $result;
325            }
326        }
327
328        return $results;
329    }
330
331    /**
332     * Renders the scoring stars
333     */
334    private function renderScore(float $relevance = 0): string
335    {
336        $html = sprintf('<span title="%01.2f%%">', $relevance);
337        $emptyStar = '<i aria-hidden="true" class="bi bi-star-o"></i>';
338        $fullStar = '<i aria-hidden="true" class="bi bi-star"></i>';
339
340        $html .= match (true) {
341            0 === (int) $relevance => $emptyStar . $emptyStar . $emptyStar,
342            $relevance < 33 => $fullStar . $emptyStar . $emptyStar,
343            $relevance < 66 => $fullStar . $fullStar . $emptyStar,
344            default => $fullStar . $fullStar . $fullStar,
345        };
346
347        return $html . '</span> ';
348    }
349
350    public function renderRelatedFaqs(SearchResultSet $searchResultSet, int $recordId): string
351    {
352        $html = '';
353        $numOfResults = $searchResultSet->getNumberOfResults();
354
355        if ($numOfResults > 0) {
356            $html .= '<ul class="list-unstyled">';
357            $counter = 0;
358            foreach ($searchResultSet->getResultSet() as $result) {
359                if ($counter >= 5) {
360                    continue;
361                }
362
363                if ($recordId === (int) $result->id) {
364                    continue;
365                }
366
367                ++$counter;
368
369                $relatedQuestion = Strings::htmlentities((string) $result->question);
370                $url = sprintf(
371                    '%scontent/%d/%d/%s/%s.html',
372                    $this->configuration->getDefaultUrl(),
373                    (int) $result->category_id,
374                    (int) $result->id,
375                    (string) $result->lang,
376                    TitleSlugifier::slug((string) $result->question),
377                );
378                $link = new Link($url, $this->configuration);
379                $link->setTitle($relatedQuestion);
380                $link->text = $relatedQuestion;
381                $link->tooltip = $relatedQuestion;
382                $link->class = 'text-decoration-none';
383                $html .= '<li><i class="bi bi-question-circle"></i> ' . $link->toHtmlAnchor() . '</li>';
384            }
385
386            $html .= '</ul>';
387        }
388
389        return $html;
390    }
391}

Inherited from phpMyFAQ\Helper\AbstractHelper

47    public function setCategory(Category $Category): AbstractHelper
48    {
49        $this->Category = $Category;
50        return $this;
51    }
53    public function getCategory(): Category
54    {
55        return $this->category();
56    }
61    protected function category(): Category
62    {
63        return $this->Category ?? throw new \LogicException('setCategory() must be called before use.');
64    }
69    protected function plurals(): Plurals
70    {
71        return $this->plurals ?? throw new \LogicException('setPlurals() must be called before use.');
72    }
74    public function setCategoryRelation(Relation $categoryRelation): AbstractHelper
75    {
76        $this->categoryRelation = $categoryRelation;
77        return $this;
78    }
80    public function setTags(Tags $Tags): AbstractHelper
81    {
82        $this->Tags = $Tags;
83        return $this;
84    }
86    public function setPlurals(Plurals $plurals): AbstractHelper
87    {
88        $this->plurals = $plurals;
89        return $this;
90    }
92    public function setSessionId(int|string $sid): AbstractHelper
93    {
94        $this->sessionId = $sid;
95        return $this;
96    }
98    public function setConfiguration(Configuration $configuration): AbstractHelper
99    {
100        $this->configuration = $configuration;
101        return $this;
102    }
104    public function getConfiguration(): Configuration
105    {
106        return $this->configuration;
107    }