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\Repository\Model;
26
27use Ampache\Module\Api\Ajax;
28use Ampache\Config\AmpConfig;
29use Ampache\Module\Util\AjaxUriRetrieverInterface;
30use Ampache\Module\Util\ObjectTypeToClassNameMapper;
31use Ampache\Module\Util\Ui;
32
33/**
34 * Browse Class
35 *
36 * This handles all of the sql/filtering
37 * on the data before it's thrown out to the templates
38 * it also handles pulling back the object_ids and then
39 * calling the correct template for the object we are displaying
40 *
41 */
42class Browse extends Query
43{
44    /**
45     * @var boolean $show_header
46     */
47    public $show_header;
48
49    /**
50     * @var integer $duration
51     */
52    public $duration;
53
54    /**
55     * Constructor.
56     *
57     * @param integer|null $browse_id
58     * @param boolean $cached
59     */
60    public function __construct($browse_id = null, $cached = true)
61    {
62        parent::__construct($browse_id, $cached);
63
64        if (!$browse_id) {
65            $this->set_use_pages(true);
66            $this->set_use_alpha(false);
67            $this->set_grid_view(true);
68        }
69        $this->show_header = true;
70    }
71
72    public function getId(): int
73    {
74        return (int) $this->id;
75    }
76
77    /**
78     * set_simple_browse
79     * This sets the current browse object to a 'simple' browse method
80     * which means use the base query provided and expand from there
81     *
82     * @param boolean $value
83     */
84    public function set_simple_browse($value)
85    {
86        $this->set_is_simple($value);
87    } // set_simple_browse
88
89    /**
90     * add_supplemental_object
91     * Legacy function, need to find a better way to do that
92     *
93     * @param string $class
94     * @param integer $uid
95     * @return boolean
96     */
97    public function add_supplemental_object($class, $uid)
98    {
99        $_SESSION['browse']['supplemental'][$this->id][$class] = (int)($uid);
100
101        return true;
102    } // add_supplemental_object
103
104    /**
105     * get_supplemental_objects
106     * This returns an array of 'class', 'id' for additional objects that
107     * need to be created before we start this whole browsing thing.
108     *
109     * @return array
110     */
111    public function get_supplemental_objects()
112    {
113        $objects = isset($_SESSION['browse']['supplemental'][$this->id]) ? $_SESSION['browse']['supplemental'][$this->id] : '';
114
115        if (!is_array($objects)) {
116            $objects = array();
117        }
118
119        return $objects;
120    } // get_supplemental_objects
121
122    /**
123     * update_browse_from_session
124     * Restore the previous start index from something saved into the current session.
125     */
126    public function update_browse_from_session()
127    {
128        if ($this->is_simple() && $this->get_start() == 0) {
129            $name = 'browse_current_' . $this->get_type();
130            if (isset($_SESSION[$name]) && isset($_SESSION[$name]['start']) && $_SESSION[$name]['start'] > 0) {
131                // Checking if value is suitable
132                $start = $_SESSION[$name]['start'];
133                if ($this->get_offset() > 0) {
134                    $set_page = floor($start / $this->get_offset());
135                    if ($this->get_total() > $this->get_offset()) {
136                        $total_pages = ceil($this->get_total() / $this->get_offset());
137                    } else {
138                        $total_pages = 0;
139                    }
140
141                    if ($set_page >= 0 && $set_page <= $total_pages) {
142                        $this->set_start($start);
143                    }
144                }
145            }
146        }
147    }
148
149    /**
150     * show_objects
151     * This takes an array of objects
152     * and requires the correct template based on the
153     * type that we are currently browsing
154     *
155     * @param array $object_ids
156     * @param boolean|array|string $argument
157     */
158    public function show_objects($object_ids = array(), $argument = false)
159    {
160        if ($this->is_simple() || !is_array($object_ids) || empty($object_ids)) {
161            $object_ids = $this->get_saved();
162        } else {
163            $this->save_objects($object_ids);
164        }
165
166        // Limit is based on the user's preferences if this is not a
167        // simple browse because we've got too much here
168        if ($this->get_start() >= 0 && (count($object_ids) > $this->get_start()) && !$this->is_simple()) {
169            $object_ids = array_slice($object_ids, $this->get_start(), $this->get_offset(), true);
170        } else {
171            if (!count($object_ids)) {
172                $this->set_total(0);
173            }
174        }
175
176        // Load any additional object we need for this
177        $extra_objects = $this->get_supplemental_objects();
178        $browse        = $this;
179
180        foreach ($extra_objects as $type => $id) {
181            $class_name = ObjectTypeToClassNameMapper::map($type);
182            ${$type}    = new $class_name($id);
183        }
184
185        $match = '';
186        // Format any matches we have so we can show them to the masses
187        if ($filter_value = $this->get_filter('alpha_match')) {
188            $match = ' (' . (string)$filter_value . ')';
189        } elseif ($filter_value = $this->get_filter('starts_with')) {
190            $match = ' (' . (string)$filter_value . ')';
191        /*} elseif ($filter_value = $this->get_filter('regex_match')) {
192            $match = ' (' . (string) $filter_value . ')';
193        } elseif ($filter_value = $this->get_filter('regex_not_match')) {
194            $match = ' (' . (string) $filter_value . ')';*/
195        } elseif ($filter_value = $this->get_filter('catalog')) {
196            // Get the catalog title
197            $catalog = Catalog::create_from_id((int)((string)$filter_value));
198            $match   = ' (' . $catalog->name . ')';
199        }
200
201        $type = $this->get_type();
202
203        // Update the session value only if it's allowed on the current browser
204        if ($this->is_update_session()) {
205            $_SESSION['browse_current_' . $type]['start'] = $browse->get_start();
206        }
207
208        // Set the correct classes based on type
209        $class = "box browse_" . $type;
210        debug_event(self::class, 'Show objects called for type {' . $type . '}', 5);
211
212        // hide some of the useless columns in a browse
213        $hide_columns   = array();
214        $argument_param = '';
215        if (is_array($argument)) {
216            if (is_array($argument['hide'])) {
217                $hide_columns = $argument['hide'];
218            }
219            if (!empty($hide_columns)) {
220                $argument_param = '&hide=';
221                foreach ($hide_columns as $column) {
222                    $argument_param .= scrub_in((string)$column) . ',';
223                }
224                $argument_param = rtrim($argument_param, ',');
225            }
226        } else {
227            $argument_param = ($argument)
228                ? '&argument=' . scrub_in((string)$argument)
229                : '';
230        }
231
232        $limit_threshold = $this->get_threshold();
233        // Switch on the type of browsing we're doing
234        switch ($type) {
235            case 'song':
236                $box_title = T_('Songs') . $match;
237                Song::build_cache($object_ids, $limit_threshold);
238                $box_req = Ui::find_template('show_songs.inc.php');
239                break;
240            case 'album':
241                Album::build_cache($object_ids);
242                $box_title         = T_('Albums') . $match;
243                $allow_group_disks = false;
244                if (is_array($argument)) {
245                    $allow_group_disks = $argument['group_disks'];
246                    if ($argument['title']) {
247                        $box_title = $argument['title'];
248                    }
249                }
250                if (AmpConfig::get('album_group')) {
251                    $allow_group_disks = true;
252                }
253                $box_req = Ui::find_template('show_albums.inc.php');
254                break;
255            case 'user':
256                $box_title = T_('Browse Users') . $match;
257                $box_req   = Ui::find_template('show_users.inc.php');
258                break;
259            case 'artist':
260                $box_title = ($this->is_album_artist())
261                    ? T_('Album Artists') . $match
262                    : T_('Artists') . $match;
263                Artist::build_cache($object_ids, true, $limit_threshold);
264                $box_req = Ui::find_template('show_artists.inc.php');
265                break;
266            case 'live_stream':
267                $box_title = T_('Radio Stations') . $match;
268                $box_req   = Ui::find_template('show_live_streams.inc.php');
269                break;
270            case 'playlist':
271                Playlist::build_cache($object_ids);
272                $box_title = T_('Playlists') . $match;
273                $box_req   = Ui::find_template('show_playlists.inc.php');
274                break;
275            case 'playlist_media':
276                $box_title = T_('Playlist Items') . $match;
277                $box_req   = Ui::find_template('show_playlist_medias.inc.php');
278                break;
279            case 'playlist_localplay':
280                $box_title = T_('Current Playlist');
281                $box_req   = Ui::find_template('show_localplay_playlist.inc.php');
282                Ui::show_box_bottom();
283                break;
284            case 'smartplaylist':
285                $box_title = T_('Smart Playlists') . $match;
286                $box_req   = Ui::find_template('show_searches.inc.php');
287                break;
288            case 'catalog':
289                $box_title = T_('Catalogs');
290                $box_req   = Ui::find_template('show_catalogs.inc.php');
291                break;
292            case 'shoutbox':
293                $box_title = T_('Shoutbox Records');
294                $box_req   = Ui::find_template('show_manage_shoutbox.inc.php');
295                break;
296            case 'tag':
297                Tag::build_cache($object_ids);
298                $box_title = T_('Genres');
299                $box_req   = Ui::find_template('show_tagcloud.inc.php');
300                break;
301            case 'video':
302                Video::build_cache($object_ids);
303                $video_type = 'video';
304                $box_title  = T_('Videos');
305                $box_req    = Ui::find_template('show_videos.inc.php');
306                break;
307            case 'democratic':
308                $box_title = T_('Democratic Playlist');
309                $box_req   = Ui::find_template('show_democratic_playlist.inc.php');
310                break;
311            case 'wanted':
312                $box_title = T_('Wanted Albums');
313                $box_req   = Ui::find_template('show_wanted_albums.inc.php');
314                break;
315            case 'share':
316                $box_title = T_('Shares');
317                $box_req   = Ui::find_template('show_shared_objects.inc.php');
318                break;
319            case 'song_preview':
320                $box_title = T_('Songs');
321                $box_req   = Ui::find_template('show_song_previews.inc.php');
322                break;
323            case 'channel':
324                $box_title = T_('Channels');
325                $box_req   = Ui::find_template('show_channels.inc.php');
326                break;
327            case 'broadcast':
328                $box_title = T_('Broadcasts');
329                $box_req   = Ui::find_template('show_broadcasts.inc.php');
330                break;
331            case 'license':
332                $box_title = T_('Media Licenses');
333                $box_req   = Ui::find_template('show_manage_license.inc.php');
334                break;
335            case 'tvshow':
336                $box_title = T_('TV Shows');
337                $box_req   = Ui::find_template('show_tvshows.inc.php');
338                break;
339            case 'tvshow_season':
340                $box_title = T_('Seasons');
341                $box_req   = Ui::find_template('show_tvshow_seasons.inc.php');
342                break;
343            case 'tvshow_episode':
344                $box_title  = T_('Episodes');
345                $video_type = $type;
346                $box_req    = Ui::find_template('show_videos.inc.php');
347                break;
348            case 'movie':
349                $box_title  = T_('Movies');
350                $video_type = $type;
351                $box_req    = Ui::find_template('show_videos.inc.php');
352                break;
353            case 'clip':
354                $box_title  = T_('Clips');
355                $video_type = $type;
356                $box_req    = Ui::find_template('show_videos.inc.php');
357                break;
358            case 'personal_video':
359                $box_title  = T_('Personal Videos');
360                $video_type = $type;
361                $box_req    = Ui::find_template('show_videos.inc.php');
362                break;
363            case 'label':
364                $box_title = T_('Labels');
365                $box_req   = Ui::find_template('show_labels.inc.php');
366                break;
367            case 'pvmsg':
368                $box_title = T_('Private Messages');
369                $box_req   = Ui::find_template('show_pvmsgs.inc.php');
370                break;
371            case 'podcast':
372                $box_title = T_('Podcasts');
373                $box_req   = Ui::find_template('show_podcasts.inc.php');
374                break;
375            case 'podcast_episode':
376                $box_title = T_('Podcast Episodes');
377                $box_req   = Ui::find_template('show_podcast_episodes.inc.php');
378                break;
379            default:
380                break;
381        } // end switch on type
382
383        Ajax::start_container($this->get_content_div(), 'browse_content');
384        if ($this->is_show_header()) {
385            if (isset($box_req) && isset($box_title)) {
386                Ui::show_box_top($box_title, $class);
387            }
388        }
389
390        if (isset($box_req)) {
391            require $box_req;
392        }
393
394        if ($this->is_show_header()) {
395            if (isset($box_req)) {
396                Ui::show_box_bottom();
397            }
398            echo '<script>';
399            echo Ajax::action('?page=browse&action=get_filters&browse_id=' . $this->id . $argument_param, '');
400            echo ';</script>';
401        } else {
402            if (!$this->is_use_pages()) {
403                $this->show_next_link($argument);
404            }
405        }
406        Ajax::end_container();
407    } // show_object
408
409    /**
410     * @param $argument
411     */
412    public function show_next_link($argument = null)
413    {
414        // FIXME Can be removed if Browse gets instantiated by the factory
415        global $dic;
416
417        $limit       = $this->get_offset();
418        $start       = $this->get_start();
419        $total       = $this->get_total();
420        $next_offset = $start + $limit;
421        if ($next_offset <= $total) {
422            echo '<a class="jscroll-next" href="' . $dic->get(AjaxUriRetrieverInterface::class)->getAjaxUri() . '?page=browse&action=page&browse_id=' . $this->id . '&start=' . $next_offset . '&xoutput=raw&xoutputnode=' . $this->get_content_div() . '&show_header=false' . $argument . '">' . T_('More') . '</a>';
423        }
424    }
425
426    /**
427     *
428     * @param string $type
429     * @param string $custom_base
430     */
431    public function set_type($type, $custom_base = '')
432    {
433        $name = 'browse_' . $type . '_pages';
434        if ((filter_has_var(INPUT_COOKIE, $name))) {
435            $this->set_use_pages(filter_input(INPUT_COOKIE, $name, FILTER_SANITIZE_STRING,
436                    FILTER_FLAG_NO_ENCODE_QUOTES) == 'true');
437        }
438        $name = 'browse_' . $type . '_alpha';
439        if ((filter_has_var(INPUT_COOKIE, $name))) {
440            $this->set_use_alpha(filter_input(INPUT_COOKIE, $name, FILTER_SANITIZE_STRING,
441                    FILTER_FLAG_NO_ENCODE_QUOTES) == 'true');
442        } else {
443            $default_alpha = (!AmpConfig::get('libitem_browse_alpha')) ? array() : explode(",",
444                AmpConfig::get('libitem_browse_alpha'));
445            if (in_array($type, $default_alpha)) {
446                $this->set_use_alpha(true, false);
447            }
448        }
449        $name = 'browse_' . $type . '_grid_view';
450        if ((filter_has_var(INPUT_COOKIE, $name))) {
451            $this->set_grid_view(filter_input(INPUT_COOKIE, $name, FILTER_SANITIZE_STRING,
452                    FILTER_FLAG_NO_ENCODE_QUOTES) == 'true');
453        }
454
455        parent::set_type($type, $custom_base);
456    }
457
458    /**
459     *
460     * @param string $option
461     * @param string $value
462     */
463    public function save_cookie_params($option, $value)
464    {
465        if ($this->get_type()) {
466            $remember_length = time() + 31536000;
467            $cookie_options  = [
468                'expires' => $remember_length,
469                'path' => AmpConfig::get('cookie_path'),
470                'domain' => AmpConfig::get('cookie_domain'),
471                'secure' => make_bool(AmpConfig::get('cookie_secure')),
472                'samesite' => 'Strict'
473            ];
474            setcookie('browse_' . $this->get_type() . '_' . $option, $value, $cookie_options);
475        }
476    }
477
478    /**
479     *
480     * @param boolean $use_pages
481     * @param boolean $savecookie
482     */
483    public function set_use_pages($use_pages, $savecookie = true)
484    {
485        if ($savecookie) {
486            $this->save_cookie_params('pages', $use_pages ? 'true' : 'false');
487        }
488        $this->_state['use_pages'] = $use_pages;
489    }
490
491    /**
492     *
493     * @return boolean
494     */
495    public function is_use_pages()
496    {
497        return make_bool($this->_state['use_pages']);
498    }
499
500    /**
501     *
502     * @param boolean $mashup
503     */
504    public function set_mashup($mashup)
505    {
506        $this->_state['mashup'] = $mashup;
507    }
508
509    /**
510     *
511     * @return boolean
512     */
513    public function is_mashup()
514    {
515        return make_bool($this->_state['mashup']);
516    }
517
518    /**
519     *
520     * @param boolean $album_artist
521     */
522    public function set_album_artist($album_artist)
523    {
524        $this->_state['album_artist'] = $album_artist;
525    }
526
527    /**
528     *
529     * @return boolean
530     */
531    public function is_album_artist()
532    {
533        return make_bool($this->_state['album_artist']);
534    }
535
536    /**
537     *
538     * @param boolean $grid_view
539     * @param boolean $savecookie
540     */
541    public function set_grid_view($grid_view, $savecookie = true)
542    {
543        if ($savecookie) {
544            $this->save_cookie_params('grid_view', $grid_view ? 'true' : 'false');
545        }
546        $this->_state['grid_view'] = $grid_view;
547    }
548
549    /**
550     *
551     * @return boolean
552     */
553    public function is_grid_view()
554    {
555        return make_bool($this->_state['grid_view']);
556    }
557
558    /**
559     *
560     * @param boolean $use_alpha
561     * @param boolean $savecookie
562     */
563    public function set_use_alpha($use_alpha, $savecookie = true)
564    {
565        if ($savecookie) {
566            $this->save_cookie_params('alpha', $use_alpha ? 'true' : 'false');
567        }
568        $this->_state['use_alpha'] = $use_alpha;
569
570        if ($use_alpha) {
571            if (count($this->_state['filter']) == 0) {
572                $this->set_filter('regex_match', '^A');
573            }
574        } else {
575            $this->set_filter('regex_not_match', '');
576        }
577    }
578
579    /**
580     *
581     * @return boolean
582     */
583    public function is_use_alpha()
584    {
585        return make_bool($this->_state['use_alpha']);
586    }
587
588    /**
589     *
590     * @param boolean $show_header
591     */
592    public function set_show_header($show_header)
593    {
594        $this->show_header = $show_header;
595    }
596
597    /**
598     * Allow the current page to be save into the current session
599     * @param boolean $update_session
600     */
601    public function set_update_session($update_session)
602    {
603        $this->_state['update_session'] = $update_session;
604    }
605
606    /**
607     *
608     * @return boolean
609     */
610    public function is_show_header()
611    {
612        return $this->show_header;
613    }
614
615    /**
616     *
617     * @return boolean
618     */
619    public function is_update_session()
620    {
621        return make_bool($this->_state['update_session']);
622    }
623
624    /**
625     *
626     * @param string $threshold
627     */
628    public function set_threshold($threshold)
629    {
630        $this->_state['threshold'] = $threshold;
631    }
632
633    /**
634     *
635     * @return string
636     */
637    public function get_threshold()
638    {
639        return (string)$this->_state['threshold'];
640    }
641
642    /**
643     *
644     * @return string
645     */
646    public function get_css_class()
647    {
648        $css = '';
649        if (!$this->_state['grid_view']) {
650            $css = 'disablegv';
651        }
652
653        return $css;
654    }
655}
656