Lines 92.70% 89 / 96
Methods 83.33% 5 / 6
Classes 0.00% 0 / 1
Covered by tests of size
Name Lines Methods CRAP
 __construct 100.00% 2 / 2 100.00% 1 / 1 2
 setUser 100.00% 1 / 1 100.00% 1 / 1 1
 setGroups 100.00% 1 / 1 100.00% 1 / 1 1
 getAllFirstLetters 100.00% 37 / 37 100.00% 1 / 1 6
 getFaqsFromLetter 86.79% 46 / 53 0.00% 0 / 1 6.08
 normalizeIdList 100.00% 2 / 2 100.00% 1 / 1 2
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}