Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
92.71% covered (success)
92.71%
89 / 96
83.33% covered (success)
83.33%
5 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
Sitemap
92.71% covered (success)
92.71%
89 / 96
83.33% covered (success)
83.33%
5 / 6
18.13
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 setUser
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setGroups
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getAllFirstLetters
100.00% covered (success)
100.00%
37 / 37
100.00% covered (success)
100.00%
1 / 1
6
 getFaqsFromLetter
86.79% covered (success)
86.79%
46 / 53
0.00% covered (danger)
0.00%
0 / 1
6.08
 normalizeIdList
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3/**
4 * The main Sitemap class.
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 2007-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     2007-03-30
16 */
17
18declare(strict_types=1);
19
20namespace phpMyFAQ;
21
22use Exception;
23use League\CommonMark\CommonMarkConverter;
24use League\CommonMark\Exception\CommonMarkException;
25use phpMyFAQ\Database\Sqlite3;
26use phpMyFAQ\Link\Util\TitleSlugifier;
27use stdClass;
28
29/**
30 * Class Sitemap
31 *
32 * @package phpMyFAQ
33 */
34class Sitemap
35{
36    /**
37     * User
38     */
39    private int $user = -1;
40
41    /**
42     * Groups.
43     *
44     * @var int[]
45     */
46    private array $groups = [];
47
48    /**
49     * Flag for Group support.
50     */
51    private bool $groupSupport = false;
52
53    /**
54     * Constructor.
55     */
56    public function __construct(
57        private readonly Configuration $configuration,
58    ) {
59        if ($this->configuration->get(item: 'security.permLevel') !== 'basic') {
60            $this->groupSupport = true;
61        }
62    }
63
64    public function setUser(int $userId = -1): void
65    {
66        $this->user = $userId;
67    }
68
69    /**
70     * @param int[] $groups
71     */
72    public function setGroups(array $groups): void
73    {
74        $this->groups = $groups;
75    }
76
77    /**
78     * Returns all available first letters.
79     * @return stdClass[]
80     */
81    public function getAllFirstLetters(): array
82    {
83        $groupList = $this->normalizeIdList($this->groups);
84        $permPart = sprintf('( fdu.user_id = %d OR fdu.user_id = -1 )', $this->user);
85
86        if ($this->groupSupport) {
87            $permPart = sprintf(
88                '( fdg.group_id IN (%s)
89            OR
90                (fdu.user_id = %d AND fdg.group_id IN (%s)))',
91                $groupList,
92                $this->user,
93                $groupList,
94            );
95        }
96
97        $letters = [];
98
99        $query = sprintf(
100            "
101            SELECT
102                DISTINCT UPPER(%s(fd.thema, 1, 1)) AS letters
103            FROM
104                %sfaqdata fd
105            LEFT JOIN
106                %sfaqdata_group AS fdg
107            ON
108                fd.id = fdg.record_id
109            LEFT JOIN
110                %sfaqdata_user AS fdu
111            ON
112                fd.id = fdu.record_id
113            WHERE
114                fd.lang = '%s'
115            AND
116                fd.active = 'yes'
117            AND
118                %s
119            ORDER BY
120                letters",
121            $this->configuration->getDb() instanceof Sqlite3 ? 'SUBSTR' : 'SUBSTRING',
122            Database::getTablePrefix(),
123            Database::getTablePrefix(),
124            Database::getTablePrefix(),
125            $this->configuration->getDb()->escape($this->configuration->getLanguage()->getLanguage()),
126            $permPart,
127        );
128
129        $result = $this->configuration->getDb()->query($query);
130        while (true) {
131            $row = $this->configuration->getDb()->fetchObject($result);
132            if (!$row instanceof stdClass) {
133                break;
134            }
135
136            $letter = new stdClass();
137            $letter->letter = Strings::strtoupper((string) $row->letters);
138
139            if (Strings::preg_match("/^\w+/iu", $letter->letter) !== 0) {
140                $letter->url = sprintf(
141                    '%ssitemap/%s/%s.html',
142                    $this->configuration->getDefaultUrl(),
143                    $letter->letter,
144                    $this->configuration->getLanguage()->getLanguage(),
145                );
146            }
147
148            $letters[] = $letter;
149        }
150
151        return $letters;
152    }
153
154    /**
155     * Returns all records from the current first letter.
156     *
157     * @param string $letter Letter
158     * @return stdClass[]
159     * @throws Exception
160     * @throws CommonMarkException
161     */
162    public function getFaqsFromLetter(string $letter = 'A'): array
163    {
164        $groupList = $this->normalizeIdList($this->groups);
165        $permPart = sprintf('( fdu.user_id = %d OR fdu.user_id = -1 )', $this->user);
166
167        if ($this->groupSupport) {
168            $permPart = sprintf(
169                '( fdg.group_id IN (%s)
170            OR
171                (fdu.user_id = %d AND fdg.group_id IN (%s)))',
172                $groupList,
173                $this->user,
174                $groupList,
175            );
176        }
177
178        $letter = Strings::strtoupper($this->configuration->getDb()->escape(Strings::substr($letter, 0, 1)));
179
180        $faqs = [];
181
182        $query = sprintf(
183            "
184                    SELECT
185                        fd.thema AS thema,
186                        fd.id AS id,
187                        fd.lang AS lang,
188                        fcr.category_id AS category_id,
189                        fd.content AS snap
190                    FROM
191                        %sfaqcategoryrelations fcr,
192                        %sfaqdata fd
193                    LEFT JOIN
194                        %sfaqdata_group AS fdg
195                    ON
196                        fd.id = fdg.record_id
197                    LEFT JOIN
198                        %sfaqdata_user AS fdu
199                    ON
200                        fd.id = fdu.record_id
201                    WHERE
202                        fd.id = fcr.record_id
203                    AND
204                        fd.thema LIKE '%s%%'
205                    AND
206                        fd.lang = '%s'
207                    AND
208                        fd.active = 'yes'
209                    AND
210                        %s",
211            Database::getTablePrefix(),
212            Database::getTablePrefix(),
213            Database::getTablePrefix(),
214            Database::getTablePrefix(),
215            $this->configuration->getDb()->escape($letter),
216            $this->configuration->getDb()->escape($this->configuration->getLanguage()->getLanguage()),
217            $permPart,
218        );
219
220        $result = $this->configuration->getDb()->query($query);
221        $oldId = 0;
222        $commonMarkConverter = new CommonMarkConverter([
223            'html_input' => 'strip',
224            'allow_unsafe_links' => false,
225        ]);
226
227        while (true) {
228            $row = $this->configuration->getDb()->fetchObject($result);
229            if (!$row instanceof stdClass) {
230                break;
231            }
232
233            $faqId = (int) $row->id;
234            if ($oldId !== $faqId) {
235                $question = (string) $row->thema;
236                $faq = new stdClass();
237                $faq->question = $question;
238                $faq->url = sprintf(
239                    '%scontent/%d/%d/%s/%s.html',
240                    $this->configuration->getDefaultUrl(),
241                    (int) $row->category_id,
242                    $faqId,
243                    (string) $row->lang,
244                    TitleSlugifier::slug($question),
245                );
246
247                $answer = strip_tags((string) $row->snap);
248                if ($this->configuration->get(item: 'main.enableMarkdownEditor')) {
249                    $answer = strip_tags($commonMarkConverter->convert((string) $row->snap)->getContent());
250                }
251
252                $faq->answer = Utils::chopString($answer, 25);
253
254                $faqs[] = $faq;
255            }
256
257            $oldId = $faqId;
258        }
259
260        return $faqs;
261    }
262
263    /**
264     * @param array<int|string> $ids
265     */
266    private function normalizeIdList(array $ids): string
267    {
268        $normalizedIds = array_map(static fn($id): int => (int) $id, $ids);
269
270        return $normalizedIds === [] ? '-1' : implode(', ', $normalizedIds);
271    }
272}