1<?php
2/*
3 * vim:set softtabstop=4 shiftwidth=4 expandtab:
4 *
5 * LICENSE: GNU Affero General Public License, version 3 (AGPL-3.0-or-later)
6 * Copyright 2001 - 2020 Ampache.org
7 *
8 * This program is free software: you can redistribute it and/or modify
9 * it under the terms of the GNU Affero General Public License as published by
10 * the Free Software Foundation, either version 3 of the License, or
11 * (at your option) any later version.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 * GNU Affero General Public License for more details.
17 *
18 * You should have received a copy of the GNU Affero General Public License
19 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
20 *
21 */
22
23declare(strict_types=0);
24
25namespace Ampache\Module\Util;
26
27use Ampache\Config\ConfigContainerInterface;
28use Ampache\Module\Playback\Localplay\LocalPlay;
29use Ampache\Module\Playback\Localplay\LocalPlayTypeEnum;
30use Ampache\Repository\Model\Metadata\Repository\MetadataField;
31use Ampache\Repository\Model\Playlist;
32use Ampache\Repository\Model\Plugin;
33use Ampache\Config\AmpConfig;
34use Ampache\Module\System\Core;
35use Ampache\Module\System\Dba;
36use Ampache\Repository\Model\Preference;
37
38/**
39 * A collection of methods related to the user interface
40 */
41class Ui implements UiInterface
42{
43    private static $_classes;
44    private static $_ticker;
45    private static $_icon_cache;
46    private static $_image_cache;
47
48    private ConfigContainerInterface $configContainer;
49
50    public function __construct(
51        ConfigContainerInterface $configContainer
52    ) {
53        $this->configContainer = $configContainer;
54    }
55
56    /**
57     * find_template
58     *
59     * Return the path to the template file wanted. The file can be overwritten
60     * by the theme if it's not a php file, or if it is and if option
61     * allow_php_themes is set to true.
62     * @param string $template
63     * @return string
64     */
65    public static function find_template($template, bool $extern = false)
66    {
67        $path      = AmpConfig::get('theme_path') . '/templates/' . $template;
68        $realpath  = __DIR__ . '/../../../public/' . $path;
69        $extension = strtolower(pathinfo($path, PATHINFO_EXTENSION));
70        if (($extension != 'php' || AmpConfig::get('allow_php_themes')) && file_exists($realpath) && is_file($realpath)) {
71            return $path;
72        } else {
73            if ($extern === true) {
74                return '/templates/' . $template;
75            }
76
77            return __DIR__ . '/../../../public/templates/' . $template;
78        }
79    }
80
81    public function accessDenied(string $error = 'Access Denied'): void
82    {
83        // Clear any buffered crap
84        ob_end_clean();
85        header("HTTP/1.1 403 $error");
86        require_once self::find_template('show_denied.inc.php');
87    }
88
89    /**
90     * ajax_include
91     *
92     * Does some trickery with the output buffer to return the output of a
93     * template.
94     * @param string $template
95     * @return string
96     */
97    public static function ajax_include($template)
98    {
99        ob_start();
100        require self::find_template('') . $template;
101        $output = ob_get_contents();
102        ob_end_clean();
103
104        return $output;
105    }
106
107    /**
108     * check_iconv
109     *
110     * Checks to see whether iconv is available;
111     * @return boolean
112     */
113    public static function check_iconv()
114    {
115        if (function_exists('iconv') && function_exists('iconv_substr')) {
116            return true;
117        }
118
119        return false;
120    }
121
122    /**
123     * check_ticker
124     *
125     * Stupid little cutesie thing to ratelimit output of long-running
126     * operations.
127     * @return boolean
128     */
129    public static function check_ticker()
130    {
131        if (!isset(self::$_ticker) || (time() > self::$_ticker + 1)) {
132            self::$_ticker = time();
133
134            return true;
135        }
136
137        return false;
138    }
139
140    /**
141     * clean_utf8
142     *
143     * Removes characters that aren't valid in XML (which is a subset of valid
144     * UTF-8, but close enough for our purposes.)
145     * See http://www.w3.org/TR/2006/REC-xml-20060816/#charsets
146     * @param string $string
147     * @return string
148     */
149    public static function clean_utf8($string)
150    {
151        if ($string) {
152            $clean = preg_replace(
153                '/[^\x{9}\x{a}\x{d}\x{20}-\x{d7ff}\x{e000}-\x{fffd}\x{10000}-\x{10ffff}]|[\x{7f}-\x{84}\x{86}-\x{9f}\x{fdd0}-\x{fddf}\x{1fffe}-\x{1ffff}\x{2fffe}-\x{2ffff}\x{3fffe}-\x{3ffff}\x{4fffe}-\x{4ffff}\x{5fffe}-\x{5ffff}\x{6fffe}-\x{6ffff}\x{7fffe}-\x{7ffff}\x{8fffe}-\x{8ffff}\x{9fffe}-\x{9ffff}\x{afffe}-\x{affff}\x{bfffe}-\x{bffff}\x{cfffe}-\x{cffff}\x{dfffe}-\x{dffff}\x{efffe}-\x{effff}\x{ffffe}-\x{fffff}\x{10fffe}-\x{10ffff}]/u',
154                '',
155                $string
156            );
157
158            if ($clean) {
159                return rtrim((string)$clean);
160            }
161
162            debug_event(self::class, 'Charset cleanup failed, something might break', 1);
163        }
164
165        return '';
166    }
167
168    /**
169     * format_bytes
170     *
171     * Turns a size in bytes into the best human-readable value
172     * @param $value
173     * @param integer $precision
174     * @return string
175     */
176    public static function format_bytes($value, $precision = 2)
177    {
178        $pass = 0;
179        while (strlen((string)floor($value)) > 3) {
180            $value /= 1024;
181            $pass++;
182        }
183
184        switch ($pass) {
185            case 1:
186                $unit = 'kB';
187                break;
188            case 2:
189                $unit = 'MB';
190                break;
191            case 3:
192                $unit = 'GB';
193                break;
194            case 4:
195                $unit = 'TB';
196                break;
197            case 5:
198                $unit = 'PB';
199                break;
200            default:
201                $unit = 'B';
202                break;
203        }
204
205        return ((string)round($value, $precision)) . ' ' . $unit;
206    }
207
208    /**
209     * unformat_bytes
210     *
211     * Parses a human-readable size
212     * @param $value
213     * @return string
214     * @noinspection PhpMissingBreakStatementInspection
215     */
216    public static function unformat_bytes($value)
217    {
218        if (preg_match('/^([0-9]+) *([[:alpha:]]+)$/', (string)$value, $matches)) {
219            $value = $matches[1];
220            $unit  = strtolower(substr($matches[2], 0, 1));
221        } else {
222            return (string)$value;
223        }
224
225        switch ($unit) {
226            case 'p':
227                $value *= 1024;
228                // Intentional break fall-through
229            case 't':
230                $value *= 1024;
231                // Intentional break fall-through
232            case 'g':
233                $value *= 1024;
234                // Intentional break fall-through
235            case 'm':
236                $value *= 1024;
237                // Intentional break fall-through
238            case 'k':
239                $value *= 1024;
240                // Intentional break fall-through
241        }
242
243        return (string)$value;
244    }
245
246    /**
247     * get_icon
248     *
249     * Returns an <img> or <svg> tag for the specified icon
250     * @param string $name
251     * @param string $title
252     * @param string $id_attrib
253     * @param string $class_attrib
254     * @return string
255     */
256    public static function get_icon($name, $title = null, $id_attrib = null, $class_attrib = null)
257    {
258        if (is_array($name)) {
259            $hover_name = $name[1];
260            $name       = $name[0];
261        }
262
263        $title    = $title ?: T_(ucfirst($name));
264        $icon_url = self::_find_icon($name);
265        $icontype = pathinfo($icon_url, 4);
266        if (isset($hover_name)) {
267            $hover_url = self::_find_icon($hover_name);
268        }
269        if ($icontype == 'svg') {
270            // load svg file
271            $svgicon = simplexml_load_file($icon_url);
272
273            if (empty($svgicon->title)) {
274                $svgicon->addChild('title', $title);
275            } else {
276                $svgicon->title = $title;
277            }
278            if (empty($svgicon->desc)) {
279                $svgicon->addChild('desc', $title);
280            } else {
281                $svgicon->desc = $title;
282            }
283
284            if (!empty($id_attrib)) {
285                $svgicon->addAttribute('id', $id_attrib);
286            }
287
288            $class_attrib = ($class_attrib) ?: 'icon icon-' . $name;
289            $svgicon->addAttribute('class', $class_attrib);
290
291            $tag = explode("\n", $svgicon->asXML(), 2)[1];
292        } else {
293            // fall back to png
294            $tag = '<img src="' . $icon_url . '" ';
295            $tag .= 'alt="' . $title . '" ';
296            $tag .= 'title="' . $title . '" ';
297            if ($id_attrib !== null) {
298                $tag .= 'id="' . $id_attrib . '" ';
299            }
300            if ($class_attrib !== null) {
301                $tag .= 'class="' . $class_attrib . '" ';
302            }
303            if (isset($hover_name) && isset($hover_url)) {
304                $tag .= 'onmouseover="this.src=\'' . $hover_url . '\'; return true;"';
305                $tag .= 'onmouseout="this.src=\'' . $icon_url . '\'; return true;" ';
306            }
307            $tag .= '/>';
308        }
309
310        return $tag;
311    }
312
313    /**
314     * _find_icon
315     *
316     * Does the finding icon thing. match svg first over png
317     * @param string $name
318     * @return string
319     */
320    private static function _find_icon($name)
321    {
322        if (isset(self::$_icon_cache[$name])) {
323            return self::$_icon_cache[$name];
324        }
325
326        $path       = AmpConfig::get('theme_path') . '/images/icons/';
327        $filesearch = glob(__DIR__ . '/../../../public/' . $path . 'icon_' . $name . '.{svg,png}', GLOB_BRACE);
328        if (empty($filesearch)) {
329            // if the theme is missing an icon. fall back to default images folder
330            $filename = 'icon_' . $name . '.png';
331            $path     = '/images/';
332        } else {
333            $filename = pathinfo($filesearch[0], 2);
334        }
335        $url = AmpConfig::get('web_path') . $path . $filename;
336        // cache the url so you don't need to keep searching
337        self::$_icon_cache[$name] = $url;
338
339        return $url;
340    }
341
342    /**
343     * get_image
344     *
345     * Returns an <img> or <svg> tag for the specified image
346     * @param string $name
347     * @param string $title
348     * @param string $id_attrib
349     * @param string $class_attrib
350     * @return string
351     */
352    public static function get_image($name, $title = null, $id_attrib = null, $class_attrib = null)
353    {
354        if (is_array($name)) {
355            $hover_name = $name[1];
356            $name       = $name[0];
357        }
358
359        $title = $title ?: ucfirst($name);
360
361        $image_url = self::_find_image($name);
362        $imagetype = pathinfo($image_url, 4);
363        if (isset($hover_name)) {
364            $hover_url = self::_find_image($hover_name);
365        }
366        if ($imagetype == 'svg') {
367            // load svg file
368            $svgimage = simplexml_load_file($image_url);
369
370            $svgimage->addAttribute('class', 'image');
371
372            if (empty($svgimage->title)) {
373                $svgimage->addChild('title', $title);
374            } else {
375                $svgimage->title = $title;
376            }
377            if (empty($svgimage->desc)) {
378                $svgimage->addChild('desc', $title);
379            } else {
380                $svgimage->desc = $title;
381            }
382
383            if (!empty($id_attrib)) {
384                $svgimage->addAttribute('id', $id_attrib);
385            }
386
387            $class_attrib = ($class_attrib) ?: 'image image-' . $name;
388            $svgimage->addAttribute('class', $class_attrib);
389
390            $tag = explode("\n", $svgimage->asXML(), 2)[1];
391        } else {
392            // fall back to png
393            $tag = '<img src="' . $image_url . '" ';
394            $tag .= 'alt="' . $title . '" ';
395            $tag .= 'title="' . $title . '" ';
396            if ($id_attrib !== null) {
397                $tag .= 'id="' . $id_attrib . '" ';
398            }
399            if ($class_attrib !== null) {
400                $tag .= 'class="' . $class_attrib . '" ';
401            }
402            if (isset($hover_name) && isset($hover_url)) {
403                $tag .= 'onmouseover="this.src=\'' . $hover_url . '\'; return true;"';
404                $tag .= 'onmouseout="this.src=\'' . $image_url . '\'; return true;" ';
405            }
406            $tag .= '/>';
407        }
408
409        return $tag;
410    }
411
412    /**
413     * _find_image
414     *
415     * Does the finding image thing. match svg first over png
416     * @param string $name
417     * @return string
418     */
419    private static function _find_image($name)
420    {
421        if (isset(self::$_image_cache[$name])) {
422            return self::$_image_cache[$name];
423        }
424
425        $path       = AmpConfig::get('theme_path') . '/images/';
426        $filesearch = glob(__DIR__ . '/../../../public/' . $path . $name . '.{svg,png}', GLOB_BRACE);
427        if (empty($filesearch)) {
428            // if the theme is missing an image. fall back to default images folder
429            $filename = $name . '.png';
430            $path     = '/images/';
431        } else {
432            $filename = pathinfo($filesearch[0], 2);
433        }
434        $url = AmpConfig::get('web_path') . $path . $filename;
435        // cache the url so you don't need to keep searching
436        self::$_image_cache[$name] = $url;
437
438        return $url;
439    }
440
441    /**
442     * Show the requested template file
443     */
444    public function show(string $template, array $context = []): void
445    {
446        extract($context);
447
448        require_once self::find_template($template);
449    }
450
451    public function showFooter(): void
452    {
453        static::show_footer();
454    }
455
456    public function showHeader(): void
457    {
458        require_once self::find_template('header.inc.php');
459    }
460
461    /**
462     * show_footer
463     *
464     * Shows the footer template and possibly profiling info.
465     *
466     * @deprecated use non-static version
467     */
468    public static function show_footer()
469    {
470        if (!defined("TABLE_RENDERED")) {
471            show_table_render();
472        }
473
474        $plugins = Plugin::get_plugins('display_on_footer');
475        foreach ($plugins as $plugin_name) {
476            $plugin = new Plugin($plugin_name);
477            if ($plugin->load(Core::get_global('user'))) {
478                $plugin->_plugin->display_on_footer();
479            }
480        }
481
482        require_once self::find_template('footer.inc.php');
483        if (Core::get_request('profiling') !== '') {
484            Dba::show_profile();
485        }
486    }
487
488    public function showBoxTop(string $title = '', string $class = ''): void
489    {
490        static::show_box_top($title, $class);
491    }
492
493    public function showBoxBottom(): void
494    {
495        static::show_box_bottom();
496    }
497
498    /**
499     * show_box_top
500     *
501     * This shows the top of the box.
502     * @param string $title
503     * @param string $class
504     *
505     * @deprecated Use non-static version
506     */
507    public static function show_box_top($title = '', $class = '')
508    {
509        require self::find_template('show_box_top.inc.php');
510    }
511
512    /**
513     * show_box_bottom
514     *
515     * This shows the bottom of the box
516     *
517     * @deprecated Use non-static version
518     */
519    public static function show_box_bottom()
520    {
521        require self::find_template('show_box_bottom.inc.php');
522    }
523
524    /**
525     * This shows the query stats
526     */
527    public function showQueryStats(): void
528    {
529        require self::find_template('show_query_stats.inc.php');
530    }
531
532    public static function show_custom_style()
533    {
534        if (AmpConfig::get('custom_login_background')) {
535            echo "<style> body { background-position: center; background-size: cover; background-image: url('" . AmpConfig::get('custom_login_background') . "') !important; }</style>";
536        }
537
538        if (AmpConfig::get('custom_login_logo')) {
539            echo "<style>#loginPage #headerlogo, #registerPage #headerlogo { background-image: url('" . AmpConfig::get('custom_login_logo') . "') !important; }</style>";
540        }
541
542        $favicon = AmpConfig::get('custom_favicon') ?: AmpConfig::get('web_path') . "/favicon.ico";
543        echo "<link rel='shortcut icon' href='" . $favicon . "' />\n";
544    }
545
546    /**
547     * update_text
548     *
549     * Convenience function that, if the output is going to a browser,
550     * blarfs JS to do a fancy update.  Otherwise it just outputs the text.
551     * @param string $field
552     * @param $value
553     */
554    public static function update_text($field, $value)
555    {
556        if (defined('API')) {
557            return;
558        }
559        if (defined('CLI')) {
560            echo $value . "\n";
561
562            return;
563        }
564
565        static $update_id = 1;
566
567        if (defined('SSE_OUTPUT')) {
568            echo "id: " . $update_id . "\n";
569            echo "data: displayNotification('" . json_encode($value) . "', 5000)\n\n";
570        } else {
571            if (!empty($field)) {
572                echo "<script>updateText('" . $field . "', '" . json_encode($value) . "');</script>\n";
573            } else {
574                echo "<br />" . $value . "<br /><br />\n";
575            }
576        }
577
578        ob_flush();
579        flush();
580        $update_id++;
581    }
582
583    /**
584     * get_logo_url
585     *
586     * Get the custom logo or logo relating to your theme color
587     * @param string $color
588     * @return string
589     */
590    public static function get_logo_url($color = null)
591    {
592        if (AmpConfig::get('custom_logo')) {
593            return AmpConfig::get('custom_logo');
594        }
595        if ($color !== null) {
596            return AmpConfig::get('web_path') . AmpConfig::get('theme_path') . '/images/ampache-' . $color . '.png';
597        }
598
599        return AmpConfig::get('web_path') . AmpConfig::get('theme_path') . '/images/ampache-' . AmpConfig::get('theme_color') . '.png';
600    }
601
602    /**
603     * @param $type
604     * @return boolean
605     */
606    public static function is_grid_view($type)
607    {
608        $isgv = true;
609        $name = 'browse_' . $type . '_grid_view';
610        if (filter_has_var(INPUT_COOKIE, $name)) {
611            $isgv = ($_COOKIE[$name] == 'true');
612        }
613
614        return $isgv;
615    }
616
617    /**
618     * shows a confirmation of an action
619     *
620     * @param string $title The Title of the message
621     * @param string $text The details of the message
622     * @param string $next_url Where to go next
623     * @param integer $cancel T/F show a cancel button that uses return_referer()
624     * @param string $form_name
625     * @param boolean $visible
626     */
627    public function showConfirmation(
628        $title,
629        $text,
630        $next_url,
631        $cancel = 0,
632        $form_name = 'confirmation',
633        $visible = true
634    ): void {
635        $webPath = $this->configContainer->getWebPath();
636
637        if (substr_count($next_url, $webPath)) {
638            $path = $next_url;
639        } else {
640            $path = sprintf('%s/%s', $webPath, $next_url);
641        }
642
643        require Ui::find_template('show_confirmation.inc.php');
644    }
645
646    /**
647     * This function is used to escape user data that is getting redisplayed
648     * onto the page, it htmlentities the mojo
649     * This is the inverse of the scrub_in function
650     */
651    public function scrubOut(?string $string): string
652    {
653        if ($string === null) {
654            return '';
655        }
656
657        return htmlentities((string) $string, ENT_NOQUOTES, AmpConfig::get('site_charset'));
658    }
659
660    /**
661     * takes the key and then creates the correct type of input for updating it
662     */
663    public function createPreferenceInput(
664        string $name,
665        $value
666    ) {
667        if (!Preference::has_access($name)) {
668            if ($value == '1') {
669                echo T_("Enabled");
670            } elseif ($value == '0') {
671                echo T_("Disabled");
672            } else {
673                if (preg_match('/_pass$/', $name) || preg_match('/_api_key$/', $name)) {
674                    echo "******";
675                } else {
676                    echo $value;
677                }
678            }
679
680            return;
681        } // if we don't have access to it
682
683        switch ($name) {
684            case 'display_menu':
685            case 'download':
686            case 'quarantine':
687            case 'upload':
688            case 'access_list':
689            case 'lock_songs':
690            case 'xml_rpc':
691            case 'force_http_play':
692            case 'no_symlinks':
693            case 'use_auth':
694            case 'access_control':
695            case 'allow_stream_playback':
696            case 'allow_democratic_playback':
697            case 'allow_localplay_playback':
698            case 'demo_mode':
699            case 'condPL':
700            case 'rio_track_stats':
701            case 'rio_global_stats':
702            case 'direct_link':
703            case 'ajax_load':
704            case 'now_playing_per_user':
705            case 'show_played_times':
706            case 'use_original_year':
707            case 'hide_single_artist':
708            case 'hide_genres':
709            case 'show_skipped_times':
710            case 'show_license':
711            case 'song_page_title':
712            case 'subsonic_backend':
713            case 'webplayer_flash':
714            case 'webplayer_html5':
715            case 'allow_personal_info_now':
716            case 'allow_personal_info_recent':
717            case 'allow_personal_info_time':
718            case 'allow_personal_info_agent':
719            case 'ui_fixed':
720            case 'autoupdate':
721            case 'autoupdate_lastversion_new':
722            case 'webplayer_confirmclose':
723            case 'webplayer_pausetabs':
724            case 'stream_beautiful_url':
725            case 'share':
726            case 'share_social':
727            case 'broadcast_by_default':
728            case 'album_group':
729            case 'topmenu':
730            case 'demo_clear_sessions':
731            case 'show_donate':
732            case 'allow_upload':
733            case 'upload_subdir':
734            case 'upload_user_artist':
735            case 'upload_allow_edit':
736            case 'daap_backend':
737            case 'upnp_backend':
738            case 'album_release_type':
739            case 'home_moment_albums':
740            case 'home_moment_videos':
741            case 'home_recently_played':
742            case 'home_now_playing':
743            case 'browser_notify':
744            case 'allow_video':
745            case 'geolocation':
746            case 'webplayer_aurora':
747            case 'upload_allow_remove':
748            case 'webdav_backend':
749            case 'notify_email':
750            case 'libitem_contextmenu':
751            case 'upload_catalog_pattern':
752            case 'catalogfav_gridview':
753            case 'personalfav_display':
754            case 'ratingmatch_flags':
755            case 'catalog_check_duplicate':
756            case 'browse_filter':
757            case 'sidebar_light':
758            case 'cron_cache':
759            case 'show_lyrics':
760            case 'unique_playlist':
761                $is_true  = '';
762                $is_false = '';
763                if ($value == '1') {
764                    $is_true = "selected=\"selected\"";
765                } else {
766                    $is_false = "selected=\"selected\"";
767                }
768                echo "<select name=\"$name\">\n";
769                echo "\t<option value=\"1\" $is_true>" . T_('On') . "</option>\n";
770                echo "\t<option value=\"0\" $is_false>" . T_('Off') . "</option>\n";
771                echo "</select>\n";
772                break;
773            case 'upload_catalog':
774                show_catalog_select('upload_catalog', $value, '', true);
775                break;
776            case 'play_type':
777                $is_stream     = '';
778                $is_localplay  = '';
779                $is_democratic = '';
780                $is_web_player = '';
781                switch ($value) {
782                    case 'localplay':
783                        $is_localplay = 'selected="selected"';
784                        break;
785                    case 'democratic':
786                        $is_democratic = 'selected="selected"';
787                        break;
788                    case 'web_player':
789                        $is_web_player = 'selected="selected"';
790                        break;
791                    default:
792                        $is_stream = 'selected="selected"';
793                }
794                echo "<select name=\"$name\">\n";
795                echo "\t<option value=\"\">" . T_('None') . "</option>\n";
796                if (AmpConfig::get('allow_stream_playback')) {
797                    echo "\t<option value=\"stream\" $is_stream>" . T_('Stream') . "</option>\n";
798                }
799                if (AmpConfig::get('allow_democratic_playback')) {
800                    echo "\t<option value=\"democratic\" $is_democratic>" . T_('Democratic') . "</option>\n";
801                }
802                if (AmpConfig::get('allow_localplay_playback')) {
803                    echo "\t<option value=\"localplay\" $is_localplay>" . T_('Localplay') . "</option>\n";
804                }
805                echo "\t<option value=\"web_player\" $is_web_player>" . T_('Web Player') . "</option>\n";
806                echo "</select>\n";
807                break;
808            case 'playlist_type':
809                $var_name    = $value . "_type";
810                ${$var_name} = "selected=\"selected\"";
811                echo "<select name=\"$name\">\n";
812                echo "\t<option value=\"m3u\" $m3u_type>" . T_('M3U') . "</option>\n";
813                echo "\t<option value=\"simple_m3u\" $simple_m3u_type>" . T_('Simple M3U') . "</option>\n";
814                echo "\t<option value=\"pls\" $pls_type>" . T_('PLS') . "</option>\n";
815                echo "\t<option value=\"asx\" $asx_type>" . T_('Asx') . "</option>\n";
816                echo "\t<option value=\"ram\" $ram_type>" . T_('RAM') . "</option>\n";
817                echo "\t<option value=\"xspf\" $xspf_type>" . T_('XSPF') . "</option>\n";
818                echo "</select>\n";
819                break;
820            case 'lang':
821                $languages = get_languages();
822                echo '<select name="' . $name . '">' . "\n";
823                foreach ($languages as $lang => $tongue) {
824                    $selected = ($lang == $value) ? 'selected="selected"' : '';
825                    echo "\t<option value=\"$lang\" " . $selected . ">$tongue</option>\n";
826                } // end foreach
827                echo "</select>\n";
828                break;
829            case 'localplay_controller':
830                $controllers = array_keys(LocalPlayTypeEnum::TYPE_MAPPING);
831                echo "<select name=\"$name\">\n";
832                echo "\t<option value=\"\">" . T_('None') . "</option>\n";
833                foreach ($controllers as $controller) {
834                    if (!LocalPlay::is_enabled($controller)) {
835                        continue;
836                    }
837                    $is_selected = '';
838                    if ($value == $controller) {
839                        $is_selected = 'selected="selected"';
840                    }
841                    echo "\t<option value=\"" . $controller . "\" $is_selected>" . ucfirst($controller) . "</option>\n";
842                } // end foreach
843                echo "</select>\n";
844                break;
845            case 'ratingmatch_stars':
846                $is_0 = '';
847                $is_1 = '';
848                $is_2 = '';
849                $is_3 = '';
850                $is_4 = '';
851                $is_5 = '';
852                if ($value == 0) {
853                    $is_0 = 'selected="selected"';
854                } elseif ($value == 1) {
855                    $is_1 = 'selected="selected"';
856                } elseif ($value == 2) {
857                    $is_2 = 'selected="selected"';
858                } elseif ($value == 3) {
859                    $is_3 = 'selected="selected"';
860                } elseif ($value == 4) {
861                    $is_4 = 'selected="selected"';
862                } elseif ($value == 4) {
863                    $is_5 = 'selected="selected"';
864                }
865                echo "<select name=\"$name\">\n";
866                echo "<option value=\"0\" $is_0>" . T_('Disabled') . "</option>\n";
867                echo "<option value=\"1\" $is_1>" . T_('1 Star') . "</option>\n";
868                echo "<option value=\"2\" $is_2>" . T_('2 Stars') . "</option>\n";
869                echo "<option value=\"3\" $is_3>" . T_('3 Stars') . "</option>\n";
870                echo "<option value=\"4\" $is_4>" . T_('4 Stars') . "</option>\n";
871                echo "<option value=\"5\" $is_5>" . T_('5 Stars') . "</option>\n";
872                echo "</select>\n";
873                break;
874            case 'localplay_level':
875                $is_user    = '';
876                $is_admin   = '';
877                $is_manager = '';
878                if ($value == '25') {
879                    $is_user = 'selected="selected"';
880                } elseif ($value == '100') {
881                    $is_admin = 'selected="selected"';
882                } elseif ($value == '50') {
883                    $is_manager = 'selected="selected"';
884                }
885                echo "<select name=\"$name\">\n";
886                echo "<option value=\"0\">" . T_('Disabled') . "</option>\n";
887                echo "<option value=\"25\" $is_user>" . T_('User') . "</option>\n";
888                echo "<option value=\"50\" $is_manager>" . T_('Manager') . "</option>\n";
889                echo "<option value=\"100\" $is_admin>" . T_('Admin') . "</option>\n";
890                echo "</select>\n";
891                break;
892            case 'theme_name':
893                $themes = get_themes();
894                echo "<select name=\"$name\">\n";
895                foreach ($themes as $theme) {
896                    $is_selected = "";
897                    if ($value == $theme['path']) {
898                        $is_selected = "selected=\"selected\"";
899                    }
900                    echo "\t<option value=\"" . $theme['path'] . "\" $is_selected>" . $theme['name'] . "</option>\n";
901                } // foreach themes
902                echo "</select>\n";
903                break;
904            case 'theme_color':
905                // This include a two-step configuration (first change theme and save, then change theme color and save)
906                $theme_cfg = get_theme(AmpConfig::get('theme_name'));
907                if ($theme_cfg !== null) {
908                    echo "<select name=\"$name\">\n";
909                    foreach ($theme_cfg['colors'] as $color) {
910                        $is_selected = "";
911                        if ($value == strtolower((string) $color)) {
912                            $is_selected = "selected=\"selected\"";
913                        }
914                        echo "\t<option value=\"" . strtolower((string) $color) . "\" $is_selected>" . $color . "</option>\n";
915                    } // foreach themes
916                    echo "</select>\n";
917                }
918                break;
919            case 'playlist_method':
920                ${$value} = ' selected="selected"';
921                echo "<select name=\"$name\">\n";
922                echo "\t<option value=\"send\"$send>" . T_('Send on Add') . "</option>\n";
923                echo "\t<option value=\"send_clear\"$send_clear>" . T_('Send and Clear on Add') . "</option>\n";
924                echo "\t<option value=\"clear\"$clear>" . T_('Clear on Send') . "</option>\n";
925                echo "\t<option value=\"default\"$default>" . T_('Default') . "</option>\n";
926                echo "</select>\n";
927                break;
928            case 'transcode':
929                ${$value} = ' selected="selected"';
930                echo "<select name=\"$name\">\n";
931                echo "\t<option value=\"never\"$never>" . T_('Never') . "</option>\n";
932                echo "\t<option value=\"default\"$default>" . T_('Default') . "</option>\n";
933                echo "\t<option value=\"always\"$always>" . T_('Always') . "</option>\n";
934                echo "</select>\n";
935                break;
936            case 'album_sort':
937                $is_sort_year_asc  = '';
938                $is_sort_year_desc = '';
939                $is_sort_name_asc  = '';
940                $is_sort_name_desc = '';
941                $is_sort_default   = '';
942                if ($value == 'year_asc') {
943                    $is_sort_year_asc = 'selected="selected"';
944                } elseif ($value == 'year_desc') {
945                    $is_sort_year_desc = 'selected="selected"';
946                } elseif ($value == 'name_asc') {
947                    $is_sort_name_asc = 'selected="selected"';
948                } elseif ($value == 'name_desc') {
949                    $is_sort_name_desc = 'selected="selected"';
950                } else {
951                    $is_sort_default = 'selected="selected"';
952                }
953
954                echo "<select name=\"$name\">\n";
955                echo "\t<option value=\"default\" $is_sort_default>" . T_('Default') . "</option>\n";
956                echo "\t<option value=\"year_asc\" $is_sort_year_asc>" . T_('Year ascending') . "</option>\n";
957                echo "\t<option value=\"year_desc\" $is_sort_year_desc>" . T_('Year descending') . "</option>\n";
958                echo "\t<option value=\"name_asc\" $is_sort_name_asc>" . T_('Name ascending') . "</option>\n";
959                echo "\t<option value=\"name_desc\" $is_sort_name_desc>" . T_('Name descending') . "</option>\n";
960                echo "</select>\n";
961                break;
962            case 'disabled_custom_metadata_fields':
963                $ids             = explode(',', $value);
964                $options         = array();
965                $fieldRepository = new MetadataField();
966                foreach ($fieldRepository->findAll() as $field) {
967                    $selected  = in_array($field->getId(), $ids) ? ' selected="selected"' : '';
968                    $options[] = '<option value="' . $field->getId() . '"' . $selected . '>' . $field->getName() . '</option>';
969                }
970                echo '<select multiple size="5" name="' . $name . '[]">' . implode("\n", $options) . '</select>';
971                break;
972            case 'personalfav_playlist':
973            case 'personalfav_smartlist':
974                $ids       = explode(',', $value);
975                $options   = array();
976                $playlists = ($name == 'personalfav_smartlist') ? Playlist::get_details('search') : Playlist::get_details();
977                if (!empty($playlists)) {
978                    foreach ($playlists as $list_id => $list_name) {
979                        $selected  = in_array($list_id, $ids) ? ' selected="selected"' : '';
980                        $options[] = '<option value="' . $list_id . '"' . $selected . '>' . $list_name . '</option>';
981                    }
982                    echo '<select multiple size="5" name="' . $name . '[]">' . implode("\n", $options) . '</select>';
983                }
984                break;
985            case 'lastfm_grant_link':
986            case 'librefm_grant_link':
987                // construct links for granting access Ampache application to Last.fm and Libre.fm
988                $plugin_name = ucfirst(str_replace('_grant_link', '', $name));
989                $plugin      = new Plugin($plugin_name);
990                $url         = $plugin->_plugin->url;
991                $api_key     = rawurlencode(AmpConfig::get('lastfm_api_key'));
992                $callback    = rawurlencode(AmpConfig::get('web_path') . '/preferences.php?tab=plugins&action=grant&plugin=' . $plugin_name);
993                /* HINT: Plugin Name */
994                echo "<a href='$url/api/auth/?api_key=$api_key&cb=$callback'>" . Ui::get_icon('plugin', sprintf(T_("Click to grant %s access to Ampache"), $plugin_name)) . '</a>';
995                break;
996            default:
997                if (preg_match('/_pass$/', $name)) {
998                    echo '<input type="password" name="' . $name . '" value="******" />';
999                } else {
1000                    echo '<input type="text" name="' . $name . '" value="' . $value . '" />';
1001                }
1002                break;
1003        }
1004    }
1005
1006    /**
1007     * This shows the preference box for the preferences pages.
1008     *
1009     * @var array<string, mixed> $preferences
1010     */
1011    public function showPreferenceBox(array $preferences): void
1012    {
1013        $this->show(
1014            'show_preference_box.inc.php',
1015            [
1016                'preferences' => $preferences,
1017                'ui' => $this
1018            ]
1019        );
1020    }
1021}
1022