Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
53.41% covered (warning)
53.41%
94 / 176
46.67% covered (danger)
46.67%
7 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
AdminMenuBuilder
53.41% covered (warning)
53.41%
94 / 176
46.67% covered (danger)
46.67%
7 / 15
302.84
0.00% covered (danger)
0.00%
0 / 1
 addMenuEntry
85.71% covered (success)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
4.05
 evaluatePermission
50.00% covered (danger)
50.00%
6 / 12
0.00% covered (danger)
0.00%
0 / 1
16.00
 setUser
72.73% covered (warning)
72.73%
8 / 11
0.00% covered (danger)
0.00%
0 / 1
6.73
 renderMetaRobotsDropdown
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
3
 sortingKeyOptions
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 sortingOrderOptions
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 sortingPopularFaqsOptions
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 searchRelevanceOptions
100.00% covered (success)
100.00%
32 / 32
100.00% covered (success)
100.00%
1 / 1
1
 renderReleaseTypeOptions
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
3
 renderTranslationProviderOptions
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
12
 renderMailProviderOptions
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
12
 renderCacheAdapterOptions
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
12
 renderLayoutModeOptions
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
20
 canAccessContent
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
20
 generateOption
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3/**
4 * Helper class for the administration menu.
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 * @author    Anatoliy Belsky <anatoliy.belsky@mayflower.de>
13 * @copyright 2010-2026 phpMyFAQ Team
14 * @license   https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0
15 * @link      https://www.phpmyfaq.de
16 * @since     2010-01-19
17 */
18
19declare(strict_types=1);
20
21namespace phpMyFAQ\Administration;
22
23use phpMyFAQ\Enums\ReleaseType;
24use phpMyFAQ\Translation;
25use phpMyFAQ\User;
26use phpMyFAQ\User\CurrentUser;
27
28/**
29 * Class Administration
30 *
31 * @package phpMyFAQ\AdminMenuBuilder
32 */
33class AdminMenuBuilder
34{
35    /**
36     * Array with permissions.
37     */
38    private array $permission = [];
39
40    /**
41     * Adds a menu entry according to user permissions.
42     * ',' stands for 'or', '*' stands for 'and'.
43     *
44     * @param string $restrictions Restrictions
45     * @param string $caption      Caption
46     * @param string|null $route   Route will be used if it's not empty
47     * @param bool   $checkPerm    Check permission (default: true)
48     */
49    public function addMenuEntry(
50        string $restrictions = '',
51        string $caption = '',
52        ?string $route = null,
53        bool $checkPerm = true,
54    ): string {
55        $renderedCaption = Translation::get($caption);
56        if (!is_string($renderedCaption)) {
57            $renderedCaption = 'No string for ' . $caption;
58        }
59
60        $output = sprintf('<a class="nav-link" href="./%s">%s</a>%s', $route, $renderedCaption, "\n");
61
62        if ($checkPerm) {
63            return $this->evaluatePermission($restrictions) ? $output : '';
64        }
65
66        return $output;
67    }
68
69    /**
70     * Parse and check a permission string.
71     * Permissions are glued with each other as follows
72     * - '+' stands for 'or'
73     * - '*' stands for 'and'
74     * No braces will be parsed, only simple expressions
75     *
76     * @example right1*right2+right3+right4*right5
77     */
78    private function evaluatePermission(string $restrictions): bool
79    {
80        if (str_contains($restrictions, '+')) {
81            foreach (explode('+', $restrictions) as $restriction) {
82                if ($this->evaluatePermission($restriction)) {
83                    return true;
84                }
85            }
86
87            return false;
88        }
89
90        if (str_contains($restrictions, '*')) {
91            foreach (explode('*', $restrictions) as $restriction) {
92                if (($this->permission[$restriction] ?? false) === true) {
93                    continue;
94                }
95
96                return false;
97            }
98
99            return true;
100        }
101
102        return $restrictions !== '' && (bool) ($this->permission[$restrictions] ?? false);
103    }
104
105    /**
106     * Setter for a permission array.
107     */
108    public function setUser(User $user): void
109    {
110        // read all rights, set them FALSE
111        $allRights = $user->perm->getAllRightsData();
112        foreach ($allRights as $right) {
113            $this->permission[$right['name']] = false;
114        }
115
116        // check user rights, set them TRUE
117        $allUserRights = $user->perm->getAllUserRights($user->getUserId());
118        foreach ($allRights as $allRight) {
119            if (!in_array((int) $allRight['right_id'], $allUserRights, strict: true)) {
120                continue;
121            }
122
123            $this->permission[$allRight['name']] = true;
124        }
125
126        // If a user is super admin, give all rights
127        if ($user->isSuperAdmin()) {
128            foreach ($allRights as $allRight) {
129                $this->permission[$allRight['name']] = true;
130            }
131        }
132    }
133
134    public static function renderMetaRobotsDropdown(string $metaRobots): string
135    {
136        $output = '';
137        $options = [
138            'index, follow',
139            'index, nofollow',
140            'noindex, follow',
141            'noindex, nofollow',
142        ];
143
144        foreach ($options as $option) {
145            $output .= sprintf('<option%s>%s</option>', $option === $metaRobots ? ' selected' : '', $option);
146        }
147
148        return $output;
149    }
150
151    /**
152     * Returns all key sorting possibilities for FAQ records.
153     */
154    public static function sortingKeyOptions(string $current): string
155    {
156        $options = ['id', 'thema', 'visits', 'updated', 'author'];
157        $output = '';
158
159        foreach ($options as $option) {
160            $output .= AdminMenuBuilder::generateOption($current, $option, 'ad_conf_order_' . $option);
161        }
162
163        return $output;
164    }
165
166    /**
167     * Returns all order sorting possibilities for FAQ records.
168     */
169    public static function sortingOrderOptions(string $current): string
170    {
171        $options = ['ASC', 'DESC'];
172        $output = '';
173
174        foreach ($options as $option) {
175            $output .= AdminMenuBuilder::generateOption($current, $option, 'ad_conf_' . strtolower($option));
176        }
177
178        return $output;
179    }
180
181    public static function sortingPopularFaqsOptions(string $current): string
182    {
183        $options = ['visits', 'voting'];
184        $output = '';
185
186        foreach ($options as $option) {
187            $output .= AdminMenuBuilder::generateOption($current, $option, 'records.orderingPopularFaqs.' . $option);
188        }
189
190        return $output;
191    }
192
193    public static function searchRelevanceOptions(string $current): string
194    {
195        $output = '';
196        $output .= AdminMenuBuilder::generateOption(
197            $current,
198            'thema,content,keywords',
199            'search.relevance.thema-content-keywords',
200        );
201        $output .= AdminMenuBuilder::generateOption(
202            $current,
203            'thema,keywords,content',
204            'search.relevance.thema-keywords-content',
205        );
206        $output .= AdminMenuBuilder::generateOption(
207            $current,
208            'content,thema,keywords',
209            'search.relevance.content-thema-keywords',
210        );
211        $output .= AdminMenuBuilder::generateOption(
212            $current,
213            'content,keywords,thema',
214            'search.relevance.content-keywords-thema',
215        );
216        $output .= AdminMenuBuilder::generateOption(
217            $current,
218            'keywords,content,thema',
219            'search.relevance.keywords-content-thema',
220        );
221
222        return $output
223        . AdminMenuBuilder::generateOption(
224            $current,
225            'keywords,thema,content',
226            'search.relevance.keywords-thema-content',
227        );
228    }
229
230    public static function renderReleaseTypeOptions(string $current): string
231    {
232        $releaseTypes = [ReleaseType::DEVELOPMENT, ReleaseType::STABLE, ReleaseType::NIGHTLY];
233        $output = '';
234
235        foreach ($releaseTypes as $releaseType) {
236            $value = $releaseType->value;
237
238            $output .= sprintf(
239                '<option value="%s"%s>%s</option>',
240                $value,
241                $value === $current ? ' selected' : '',
242                ucfirst($releaseType->value),
243            );
244        }
245
246        return $output;
247    }
248
249    public static function renderTranslationProviderOptions(string $current): string
250    {
251        $providers = [
252            'none' => 'None',
253            'google' => 'Google Cloud Translation',
254            'deepl' => 'DeepL',
255            'azure' => 'Azure Translator',
256            'amazon' => 'Amazon Translate',
257            'libretranslate' => 'LibreTranslate',
258        ];
259        $output = '';
260
261        foreach ($providers as $value => $label) {
262            $output .= sprintf(
263                '<option value="%s"%s>%s</option>',
264                $value,
265                $value === $current ? ' selected' : '',
266                $label,
267            );
268        }
269
270        return $output;
271    }
272
273    public static function renderMailProviderOptions(string $current): string
274    {
275        $providers = [
276            'none' => 'None',
277            'smtp' => 'SMTP',
278            'sendgrid' => 'SendGrid',
279            'ses' => 'Amazon SES',
280            'mailgun' => 'Mailgun',
281        ];
282        $output = '';
283
284        foreach ($providers as $value => $label) {
285            $output .= sprintf(
286                '<option value="%s"%s>%s</option>',
287                $value,
288                $value === $current ? ' selected' : '',
289                $label,
290            );
291        }
292
293        return $output;
294    }
295
296    public static function renderCacheAdapterOptions(string $current): string
297    {
298        $adapters = [
299            'filesystem' => 'Filesystem (default)',
300            'redis' => 'Redis',
301        ];
302        $output = '';
303
304        foreach ($adapters as $value => $label) {
305            $output .= sprintf(
306                '<option value="%s"%s>%s</option>',
307                $value,
308                $value === $current ? ' selected' : '',
309                $label,
310            );
311        }
312
313        return $output;
314    }
315
316    public static function renderLayoutModeOptions(string $current): string
317    {
318        $modes = [
319            'auto' => Translation::get('ad_layout_mode_auto'),
320            'light' => Translation::get('ad_layout_mode_light'),
321            'dark' => Translation::get('ad_layout_mode_dark'),
322            'high-contrast' => Translation::get('ad_layout_mode_highContrast'),
323        ];
324        $output = '';
325
326        foreach ($modes as $value => $label) {
327            $output .= sprintf(
328                '<option value="%s"%s>%s</option>',
329                $value,
330                $value === $current ? ' selected' : '',
331                is_string($label) ? $label : '',
332            );
333        }
334
335        return $output;
336    }
337
338    /**
339     * Checks if the current user can access the content.
340     */
341    public function canAccessContent(CurrentUser $currentUser): bool
342    {
343        return (
344            $currentUser->isLoggedIn()
345            && (
346                (
347                    is_countable($currentUser->perm->getAllUserRights($currentUser->getUserId()))
348                        ? count($currentUser->perm->getAllUserRights($currentUser->getUserId()))
349                        : 0
350                )
351                || $currentUser->isSuperAdmin()
352            )
353        );
354    }
355
356    private static function generateOption(string $current, string $value, string $label): string
357    {
358        return sprintf(
359            '<option value="%s"%s>%s</option>',
360            $value,
361            $value === $current ? ' selected' : '',
362            Translation::getString($label),
363        );
364    }
365}