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\Authorization\Access;
28use Ampache\Config\AmpConfig;
29use Ampache\Module\System\AmpError;
30use Ampache\Module\System\Core;
31use Ampache\Module\System\Dba;
32
33/**
34 * Query Class
35 *
36 * This handles all of the sql/filtering for the ampache database
37 * FIXME: flowerysong didn't know about this when he wrote all the fancy stuff
38 * for newsearch, and they should be merged if possible.
39 *
40 */
41class Query
42{
43    /**
44     * @var integer|string $id
45     */
46    public $id;
47
48    /**
49     * @var integer $catalog
50     */
51    public $catalog;
52
53    /**
54     * @var array $_state
55     */
56    protected $_state = array();
57
58    /**
59     * @var array $_cache
60     */
61    protected $_cache;
62
63    /**
64     * @var int $user_id
65     */
66    private $user_id;
67
68    /**
69     * @var array $allowed_filters
70     */
71    private static $allowed_filters;
72
73    /**
74     * @var array $allowed_sorts
75     */
76    private static $allowed_sorts = [
77        'song' => array(
78            'title',
79            'year',
80            'track',
81            'time',
82            'composer',
83            'total_count',
84            'total_skip',
85            'album',
86            'artist'
87        ),
88        'album' => array(
89            'name',
90            'year',
91            'original_year',
92            'artist',
93            'album_artist',
94            'generic_artist',
95            'song_count',
96            'total_count',
97            'release_type'
98        ),
99        'artist' => array(
100            'name',
101            'album',
102            'placeformed',
103            'yearformed',
104            'song_count',
105            'album_count',
106            'total_count'
107        ),
108        'playlist' => array(
109            'name',
110            'user',
111            'last_update'
112        ),
113        'smartplaylist' => array(
114            'name',
115            'user'
116        ),
117        'shoutbox' => array(
118            'date',
119            'user',
120            'sticky'
121        ),
122        'live_stream' => array(
123            'name',
124            'call_sign',
125            'frequency'
126        ),
127        'tag' => array(
128            'tag',
129            'name'
130        ),
131        'user' => array(
132            'fullname',
133            'username',
134            'last_seen',
135            'create_date'
136        ),
137        'video' => array(
138            'title',
139            'resolution',
140            'length',
141            'codec'
142        ),
143        'wanted' => array(
144            'user',
145            'accepted',
146            'artist',
147            'name',
148            'year'
149        ),
150        'share' => array(
151            'object',
152            'object_type',
153            'user',
154            'creation_date',
155            'lastvisit_date',
156            'counter',
157            'max_counter',
158            'allow_stream',
159            'allow_download',
160            'expire'
161        ),
162        'channel' => array(
163            'id',
164            'name',
165            'interface',
166            'port',
167            'max_listeners',
168            'listeners'
169        ),
170        'broadcast' => array(
171            'name',
172            'user',
173            'started',
174            'listeners'
175        ),
176        'license' => array(
177            'name'
178        ),
179        'tvshow' => array(
180            'name',
181            'year'
182        ),
183        'tvshow_season' => array(
184            'season',
185            'tvshow'
186        ),
187        'tvshow_episode' => array(
188            'title',
189            'resolution',
190            'length',
191            'codec',
192            'episode',
193            'season',
194            'tvshow'
195        ),
196        'movie' => array(
197            'title',
198            'resolution',
199            'length',
200            'codec',
201            'release_date'
202        ),
203        'clip' => array(
204            'title',
205            'artist',
206            'resolution',
207            'length',
208            'codec',
209            'release_date'
210        ),
211        'personal_video' => array(
212            'title',
213            'location',
214            'resolution',
215            'length',
216            'codec',
217            'release_date'
218        ),
219        'label' => array(
220            'name',
221            'category',
222            'user'
223        ),
224        'pvmsg' => array(
225            'subject',
226            'to_user',
227            'creation_date',
228            'is_read'
229        ),
230        'follower' => array(
231            'user',
232            'follow_user',
233            'follow_date'
234        ),
235        'podcast' => array(
236            'title'
237        ),
238        'podcast_episode' => array(
239            'title',
240            'category',
241            'author',
242            'time',
243            'pubDate'
244        )
245    ];
246
247    /**
248     * constructor
249     * This should be called
250     * @param integer|null $query_id
251     * @param boolean $cached
252     */
253    public function __construct($query_id = null, $cached = true)
254    {
255        $sid = session_id();
256
257        if (!$cached) {
258            $this->id = 'nocache';
259
260            return true;
261        }
262        $this->user_id = Core::get_global('user')->id;
263
264        if ($query_id === null) {
265            $this->reset();
266            $data = self::_serialize($this->_state);
267
268            $sql = 'INSERT INTO `tmp_browse` (`sid`, `data`) VALUES(?, ?)';
269            Dba::write($sql, array($sid, $data));
270            $this->id = Dba::insert_id();
271
272            return true;
273        } else {
274            $sql = 'SELECT `data` FROM `tmp_browse` WHERE `id` = ? AND `sid` = ?';
275
276            $db_results = Dba::read($sql, array($query_id, $sid));
277            if ($results = Dba::fetch_assoc($db_results)) {
278                $this->id     = $query_id;
279                $this->_state = (array)self::_unserialize($results['data']);
280
281                return true;
282            }
283        }
284
285        AmpError::add('browse', T_('Browse was not found or expired, try reloading the page'));
286
287        return false;
288    }
289
290    /**
291     * garbage_collection
292     * This cleans old data out of the table
293     */
294    public static function garbage_collection()
295    {
296        $sql = 'DELETE FROM `tmp_browse` USING `tmp_browse` LEFT JOIN `session` ON `session`.`id` = `tmp_browse`.`sid` WHERE `session`.`id` IS NULL';
297        Dba::write($sql);
298    }
299
300    /**
301     * _serialize
302     *
303     * Attempts to produce a more compact representation for large result
304     * sets by collapsing ranges.
305     * @param array $data
306     * @return string
307     */
308    private static function _serialize($data)
309    {
310        return json_encode($data);
311    }
312
313    /**
314     * _unserialize
315     *
316     * Reverses serialization.
317     * @param string $data
318     * @return mixed
319     */
320    private static function _unserialize($data)
321    {
322        return json_decode((string)$data, true);
323    }
324
325    /**
326     * set_filter
327     * This saves the filter data we pass it.
328     * @param string $key
329     * @param mixed $value
330     * @return boolean
331     */
332    public function set_filter($key, $value)
333    {
334        switch ($key) {
335            case 'tag':
336                if (is_array($value)) {
337                    $this->_state['filter'][$key] = $value;
338                } elseif (is_numeric($value)) {
339                    $this->_state['filter'][$key] = array($value);
340                } else {
341                    $this->_state['filter'][$key] = array();
342                }
343                break;
344            case 'artist':
345            case 'album_artist':
346            case 'catalog':
347            case 'album':
348                $this->_state['filter'][$key] = $value;
349                break;
350            case 'min_count':
351            case 'unplayed':
352            case 'rated':
353                break;
354            case 'add_lt':
355            case 'add_gt':
356            case 'update_lt':
357            case 'update_gt':
358            case 'catalog_enabled':
359            case 'year_lt':
360            case 'year_lg':
361            case 'year_eq':
362            case 'season_lt':
363            case 'season_lg':
364            case 'season_eq':
365            case 'user':
366            case 'to_user':
367            case 'enabled':
368                $this->_state['filter'][$key] = (int)($value);
369                break;
370            case 'exact_match':
371            case 'alpha_match':
372            case 'regex_match':
373            case 'regex_not_match':
374            case 'starts_with':
375                if ($this->is_static_content()) {
376                    return false;
377                }
378                $this->_state['filter'][$key] = $value;
379                if ($key == 'regex_match') {
380                    unset($this->_state['filter']['regex_not_match']);
381                }
382                if ($key == 'regex_not_match') {
383                    unset($this->_state['filter']['regex_match']);
384                }
385                break;
386            case 'playlist_type':
387                // Must be a content manager to turn this off
388                if (Access::check('interface', 100)) {
389                    unset($this->_state['filter'][$key]);
390                } else {
391                    $this->_state['filter'][$key] = '1';
392                }
393                break;
394            default:
395                return false;
396        } // end switch
397
398        // If we've set a filter we need to reset the totals
399        $this->reset_total();
400        $this->set_start(0);
401
402        return true;
403    } // set_filter
404
405    /**
406     * reset
407     * Reset everything, this should only be called when we are starting
408     * fresh
409     */
410    public function reset()
411    {
412        $this->reset_base();
413        $this->reset_filters();
414        $this->reset_total();
415        $this->reset_join();
416        $this->reset_select();
417        $this->reset_having();
418        $this->set_static_content(false);
419        $this->set_is_simple(false);
420        $this->set_start(0);
421        $this->set_offset(AmpConfig::get('offset_limit') ? AmpConfig::get('offset_limit') : '25');
422    } // reset
423
424    /**
425     * reset_base
426     * this resets the base string
427     */
428    public function reset_base()
429    {
430        $this->_state['base'] = null;
431    } // reset_base
432
433    /**
434     * reset_select
435     * This resets the select fields that we've added so far
436     */
437    public function reset_select()
438    {
439        $this->_state['select'] = array();
440    } // reset_select
441
442    /**
443     * reset_having
444     * Null out the having clause
445     */
446    public function reset_having()
447    {
448        unset($this->_state['having']);
449    } // reset_having
450
451    /**
452     * reset_join
453     * clears the joins if there are any
454     */
455    public function reset_join()
456    {
457        unset($this->_state['join']);
458    } // reset_join
459
460    /**
461     * reset_filter
462     * This is a wrapper function that resets the filters
463     */
464    public function reset_filters()
465    {
466        $this->_state['filter'] = array();
467    } // reset_filters
468
469    /**
470     * reset_total
471     * This resets the total for the browse type
472     */
473    public function reset_total()
474    {
475        unset($this->_state['total']);
476    } // reset_total
477
478    /**
479     * get_filter
480     * returns the specified filter value
481     * @param string $key
482     * @return string|boolean
483     */
484    public function get_filter($key)
485    {
486        // Simple enough, but if we ever move this crap
487        // If we ever move this crap what?
488        return (isset($this->_state['filter'][$key])) ? $this->_state['filter'][$key] : false;
489    } // get_filter
490
491    /**
492     * get_start
493     * This returns the current value of the start
494     * @return integer
495     */
496    public function get_start()
497    {
498        return $this->_state['start'];
499    } // get_start
500
501    /**
502     * get_offset
503     * This returns the current offset
504     * @return integer
505     */
506    public function get_offset()
507    {
508        return $this->_state['offset'];
509    } // get_offset
510
511    /**
512     * set_total
513     * This sets the total number of objects
514     * @param integer $total
515     */
516    public function set_total($total)
517    {
518        $this->_state['total'] = $total;
519    }
520
521    /**
522     * get_total
523     * This returns the total number of objects for this current sort type.
524     * If it's already cached used it. if they pass us an array then use
525     * that.
526     * @param array $objects
527     * @return integer
528     */
529    public function get_total($objects = null)
530    {
531        // If they pass something then just return that
532        if (is_array($objects) && !$this->is_simple()) {
533            return count($objects);
534        }
535
536        // See if we can find it in the cache
537        if (isset($this->_state['total'])) {
538            return $this->_state['total'];
539        }
540
541        $db_results = Dba::read($this->get_sql(false));
542        $num_rows   = Dba::num_rows($db_results);
543
544        $this->_state['total'] = $num_rows;
545
546        return $num_rows;
547    } // get_total
548
549    /**
550     * get_allowed_filters
551     * This returns an array of the allowed filters based on the type of
552     * object we are working with, this is used to display the 'filter'
553     * sidebar stuff.
554     * @param string $type
555     * @return array
556     */
557    public static function get_allowed_filters($type)
558    {
559        if (empty(self::$allowed_filters)) {
560            self::$allowed_filters = array(
561                'album' => array(
562                    'add_lt',
563                    'add_gt',
564                    'update_lt',
565                    'update_gt',
566                    'show_art',
567                    'starts_with',
568                    'exact_match',
569                    'alpha_match',
570                    'regex_match',
571                    'regex_not_match',
572                    'catalog',
573                    'catalog_enabled'
574                ),
575                'artist' => array(
576                    'add_lt',
577                    'add_gt',
578                    'update_lt',
579                    'update_gt',
580                    'exact_match',
581                    'alpha_match',
582                    'regex_match',
583                    'regex_not_match',
584                    'starts_with',
585                    'tag',
586                    'catalog',
587                    'catalog_enabled'
588                ),
589                'song' => array(
590                    'add_lt',
591                    'add_gt',
592                    'update_lt',
593                    'update_gt',
594                    'exact_match',
595                    'alpha_match',
596                    'regex_match',
597                    'regex_not_match',
598                    'starts_with',
599                    'tag',
600                    'catalog',
601                    'catalog_enabled',
602                    'composer',
603                    'enabled'
604                ),
605                'live_stream' => array(
606                    'alpha_match',
607                    'regex_match',
608                    'regex_not_match',
609                    'starts_with',
610                    'catalog_enabled'
611                ),
612                'playlist' => array(
613                    'alpha_match',
614                    'regex_match',
615                    'regex_not_match',
616                    'starts_with'
617                ),
618                'smartplaylist' => array(
619                    'alpha_match',
620                    'regex_match',
621                    'regex_not_match',
622                    'starts_with'
623                ),
624                'tag' => array(
625                    'tag',
626                    'object_type',
627                    'exact_match',
628                    'alpha_match',
629                    'regex_match',
630                    'regex_not_match'
631                ),
632                'video' => array(
633                    'starts_with',
634                    'exact_match',
635                    'alpha_match',
636                    'regex_match',
637                    'regex_not_match'
638                ),
639                'license' => array(
640                    'alpha_match',
641                    'regex_match',
642                    'regex_not_match',
643                    'starts_with'
644                ),
645                'tvshow' => array(
646                    'alpha_match',
647                    'regex_match',
648                    'regex_not_match',
649                    'starts_with',
650                    'year_lt',
651                    'year_gt',
652                    'year_eq'
653                ),
654                'tvshow_season' => array(
655                    'season_lt',
656                    'season_lg',
657                    'season_eq'
658                ),
659                'user' => array(
660                    'starts_with'
661                ),
662                'label' => array(
663                    'alpha_match',
664                    'regex_match',
665                    'regex_not_match',
666                    'starts_with'
667                ),
668                'pvmsg' => array(
669                    'alpha_match',
670                    'regex_match',
671                    'regex_not_match',
672                    'starts_with',
673                    'user',
674                    'to_user'
675                ),
676                'follower' => array(
677                    'user',
678                    'to_user',
679                ),
680                'podcast' => array(
681                    'alpha_match',
682                    'regex_match',
683                    'regex_not_match',
684                    'starts_with'
685                ),
686                'podcast_episode' => array(
687                    'alpha_match',
688                    'regex_match',
689                    'regex_not_match',
690                    'starts_with'
691                )
692            );
693
694            if (Access::check('interface', 50)) {
695                array_push(self::$allowed_filters['playlist'], 'playlist_type');
696            }
697        }
698
699        return self::$allowed_filters[$type] ?? [];
700    } // get_allowed_filters
701
702    /**
703     * set_type
704     * This sets the type of object that we want to browse by
705     * we do this here so we only have to maintain a single whitelist
706     * and if I want to change the location I only have to do it here
707     * @param string $type
708     * @param string $custom_base
709     */
710    public function set_type($type, $custom_base = '')
711    {
712        switch ($type) {
713            case 'user':
714            case 'video':
715            case 'playlist':
716            case 'playlist_media':
717            case 'smartplaylist':
718            case 'song':
719            case 'catalog':
720            case 'album':
721            case 'artist':
722            case 'tag':
723            case 'playlist_localplay':
724            case 'shoutbox':
725            case 'live_stream':
726            case 'democratic':
727            case 'wanted':
728            case 'share':
729            case 'song_preview':
730            case 'channel':
731            case 'broadcast':
732            case 'license':
733            case 'tvshow':
734            case 'tvshow_season':
735            case 'tvshow_episode':
736            case 'movie':
737            case 'personal_video':
738            case 'clip':
739            case 'label':
740            case 'pvmsg':
741            case 'follower':
742            case 'podcast':
743            case 'podcast_episode':
744                // Set it
745                $this->_state['type'] = $type;
746                $this->set_base_sql(true, $custom_base);
747                break;
748            default:
749                break;
750        } // end type whitelist
751    } // set_type
752
753    /**
754     * get_type
755     * This returns the type of the browse we currently are using
756     * @return string
757     */
758    public function get_type()
759    {
760        return (string)$this->_state['type'];
761    } // get_type
762
763    /**
764     * set_sort
765     * This sets the current sort(s)
766     * @param string $sort
767     * @param string $order
768     * @return boolean
769     */
770    public function set_sort($sort, $order = '')
771    {
772        // If it's not in our list, smeg off!
773        if (!in_array($sort, self::$allowed_sorts[$this->get_type()])) {
774            return false;
775        }
776
777        $this->reset_join();
778
779        if ($order) {
780            $order                       = ($order == 'DESC') ? 'DESC' : 'ASC';
781            $this->_state['sort']        = array();
782            $this->_state['sort'][$sort] = $order;
783        } elseif ($this->_state['sort'][$sort] == 'DESC') {
784            // Reset it till I can figure out how to interface the hotness
785            $this->_state['sort']        = array();
786            $this->_state['sort'][$sort] = 'ASC';
787        } else {
788            // Reset it till I can figure out how to interface the hotness
789            $this->_state['sort']        = array();
790            $this->_state['sort'][$sort] = 'DESC';
791        }
792
793        $this->resort_objects();
794
795        return true;
796    } // set_sort
797
798    /**
799     * set_offset
800     * This sets the current offset of this query
801     * @param integer $offset
802     */
803    public function set_offset($offset)
804    {
805        $this->_state['offset'] = abs($offset);
806    } // set_offset
807
808    /**
809     * set_catalog
810     * @param integer $catalog_number
811     */
812    public function set_catalog($catalog_number)
813    {
814        $this->catalog = $catalog_number;
815    }
816
817    /**
818     * set_select
819     * This appends more information to the select part of the SQL
820     * statement, we're going to move to the %%SELECT%% style queries, as I
821     * think it's the only way to do this...
822     * @param string $field
823     */
824    public function set_select($field)
825    {
826        $this->_state['select'][] = $field;
827    } // set_select
828
829    /**
830     * set_join
831     * This sets the joins for the current browse object
832     * @param string $type
833     * @param string $table
834     * @param string $source
835     * @param string $dest
836     * @param integer $priority
837     */
838    public function set_join($type, $table, $source, $dest, $priority)
839    {
840        $this->_state['join'][$priority][$table] = "$type JOIN $table ON $source = $dest";
841    } // set_join
842
843    /**
844     * set_join_and
845     * This sets the joins for the current browse object and a second option as well
846     * @param string $type
847     * @param string $table
848     * @param string $source1
849     * @param string $dest1
850     * @param string $source2
851     * @param string $dest2
852     * @param integer $priority
853     */
854    public function set_join_and($type, $table, $source1, $dest1, $source2, $dest2, $priority)
855    {
856        $this->_state['join'][$priority][$table] = strtoupper((string)$type) . " JOIN $table ON $source1 = $dest1 AND $source2 = $dest2";
857    } // set_join_and
858
859    /**
860     * set_having
861     * This sets the "HAVING" part of the query, we can only have one..
862     * god this is ugly
863     * @param string $condition
864     */
865    public function set_having($condition)
866    {
867        $this->_state['having'] = $condition;
868    } // set_having
869
870    /**
871     * set_start
872     * This sets the start point for our show functions
873     * We need to store this in the session so that it can be pulled
874     * back, if they hit the back button
875     * @param integer $start
876     */
877    public function set_start($start)
878    {
879        $start                 = (int)($start);
880        $this->_state['start'] = $start;
881    } // set_start
882
883    /**
884     * set_is_simple
885     * This sets the current browse object to a 'simple' browse method
886     * which means use the base query provided and expand from there
887     * @param boolean $value
888     */
889    public function set_is_simple($value)
890    {
891        $value                  = make_bool($value);
892        $this->_state['simple'] = $value;
893    } // set_is_simple
894
895    /**
896     * set_static_content
897     * This sets true/false if the content of this browse
898     * should be static, if they are then content filtering/altering
899     * methods will be skipped
900     * @param boolean $value
901     */
902    public function set_static_content($value)
903    {
904        $value = make_bool($value);
905
906        $this->_state['static'] = $value;
907    } // set_static_content
908
909    /**
910     *
911     * @return boolean
912     */
913    public function is_static_content()
914    {
915        return make_bool($this->_state['static']);
916    }
917
918    /**
919     * is_simple
920     * This returns whether or not the current browse type is set to static.
921     * @return boolean
922     */
923    public function is_simple()
924    {
925        return $this->_state['simple'];
926    } // is_simple
927
928    /**
929     * get_saved
930     * This looks in the session for the saved stuff and returns what it
931     * finds.
932     * @return array
933     */
934    public function get_saved()
935    {
936        // See if we have it in the local cache first
937        if (!empty($this->_cache)) {
938            return $this->_cache;
939        }
940
941        if (!$this->is_simple()) {
942            $sql        = 'SELECT `object_data` FROM `tmp_browse` WHERE `sid` = ? AND `id` = ?';
943            $db_results = Dba::read($sql, array(session_id(), $this->id));
944
945            $row = Dba::fetch_assoc($db_results);
946
947            $this->_cache = (array)self::_unserialize($row['object_data']);
948
949            return $this->_cache;
950        } else {
951            $objects = $this->get_objects();
952        }
953
954        return $objects;
955    } // get_saved
956
957    /**
958     * get_objects
959     * This gets an array of the ids of the objects that we are
960     * currently browsing by it applies the sql and logic based
961     * filters
962     * @return array
963     */
964    public function get_objects()
965    {
966        // First we need to get the SQL statement we are going to run. This has to run against any possible filters (dependent on type)
967        $sql = $this->get_sql();
968
969        $db_results = Dba::read($sql);
970        $results    = array();
971        while ($data = Dba::fetch_assoc($db_results)) {
972            $results[] = $data;
973        }
974
975        $results  = $this->post_process($results);
976        $filtered = array();
977        foreach ($results as $data) {
978            // Make sure that this object passes the logic filter
979            if ($this->logic_filter($data['id'])) {
980                $filtered[] = $data['id'];
981            }
982        } // end while
983
984        // Save what we've found and then return it
985        $this->save_objects($filtered);
986
987        return $filtered;
988    } // get_objects
989
990    /**
991     * set_base_sql
992     * This saves the base sql statement we are going to use.
993     * @param boolean $force
994     * @param string $custom_base
995     * @return boolean
996     */
997    private function set_base_sql($force = false, $custom_base = '')
998    {
999        // Only allow it to be set once
1000        if (strlen((string)$this->_state['base']) && !$force) {
1001            return true;
1002        }
1003
1004        // Custom sql base
1005        if ($force && !empty($custom_base)) {
1006            $this->_state['custom'] = true;
1007            $sql                    = $custom_base;
1008        } else {
1009            switch ($this->get_type()) {
1010                case 'album':
1011                    $this->set_select("`album`.`id`");
1012                    $sql = "SELECT MIN(%%SELECT%%) AS `id` FROM `album` ";
1013                    break;
1014                case 'artist':
1015                    $this->set_select("`artist`.`id`");
1016                    $sql = "SELECT %%SELECT%% FROM `artist` ";
1017                    break;
1018                case 'catalog':
1019                    $this->set_select("`artist`.`name`");
1020                    $sql = "SELECT %%SELECT%% FROM `artist` ";
1021                    break;
1022                case 'user':
1023                    $this->set_select("`user`.`id`");
1024                    $sql = "SELECT %%SELECT%% FROM `user` ";
1025                    break;
1026                case 'live_stream':
1027                    $this->set_select("`live_stream`.`id`");
1028                    $sql = "SELECT %%SELECT%% FROM `live_stream` ";
1029                    break;
1030                case 'playlist':
1031                    $this->set_select("`playlist`.`id`");
1032                    $sql = "SELECT %%SELECT%% FROM `playlist` ";
1033                    break;
1034                case 'smartplaylist':
1035                    $this->set_select('`search`.`id`');
1036                    $sql = "SELECT %%SELECT%% FROM `search` ";
1037                    break;
1038                case 'shoutbox':
1039                    $this->set_select("`user_shout`.`id`");
1040                    $sql = "SELECT %%SELECT%% FROM `user_shout` ";
1041                    break;
1042                case 'video':
1043                    $this->set_select("`video`.`id`");
1044                    $sql = "SELECT %%SELECT%% FROM `video` ";
1045                    break;
1046                case 'tag':
1047                    $this->set_select("`tag`.`id`");
1048                    $this->set_join('LEFT', 'tag_map', '`tag_map`.`tag_id`', '`tag`.`id`', 1);
1049                    $sql = "SELECT %%SELECT%% FROM `tag` ";
1050                    break;
1051                case 'wanted':
1052                    $this->set_select("`wanted`.`id`");
1053                    $sql = "SELECT %%SELECT%% FROM `wanted` ";
1054                    break;
1055                case 'share':
1056                    $this->set_select("`share`.`id`");
1057                    $sql = "SELECT %%SELECT%% FROM `share` ";
1058                    break;
1059                case 'channel':
1060                    $this->set_select("`channel`.`id`");
1061                    $sql = "SELECT %%SELECT%% FROM `channel` ";
1062                    break;
1063                case 'broadcast':
1064                    $this->set_select("`broadcast`.`id`");
1065                    $sql = "SELECT %%SELECT%% FROM `broadcast` ";
1066                    break;
1067                case 'license':
1068                    $this->set_select("`license`.`id`");
1069                    $sql = "SELECT %%SELECT%% FROM `license` ";
1070                    break;
1071                case 'tvshow':
1072                    $this->set_select("`tvshow`.`id`");
1073                    $sql = "SELECT %%SELECT%% FROM `tvshow` ";
1074                    break;
1075                case 'tvshow_season':
1076                    $this->set_select("`tvshow_season`.`id`");
1077                    $sql = "SELECT %%SELECT%% FROM `tvshow_season` ";
1078                    break;
1079                case 'tvshow_episode':
1080                    $this->set_select("`tvshow_episode`.`id`");
1081                    $sql = "SELECT %%SELECT%% FROM `tvshow_episode` ";
1082                    break;
1083                case 'movie':
1084                    $this->set_select("`movie`.`id`");
1085                    $sql = "SELECT %%SELECT%% FROM `movie` ";
1086                    break;
1087                case 'clip':
1088                    $this->set_select("`clip`.`id`");
1089                    $sql = "SELECT %%SELECT%% FROM `clip` ";
1090                    break;
1091                case 'personal_video':
1092                    $this->set_select("`personal_video`.`id`");
1093                    $sql = "SELECT %%SELECT%% FROM `personal_video` ";
1094                    break;
1095                case 'label':
1096                    $this->set_select("`label`.`id`");
1097                    $sql = "SELECT %%SELECT%% FROM `label` ";
1098                    break;
1099                case 'pvmsg':
1100                    $this->set_select("`user_pvmsg`.`id`");
1101                    $sql = "SELECT %%SELECT%% FROM `user_pvmsg` ";
1102                    break;
1103                case 'follower':
1104                    $this->set_select("`user_follower`.`id`");
1105                    $sql = "SELECT %%SELECT%% FROM `user_follower` ";
1106                    break;
1107                case 'podcast':
1108                    $this->set_select("`podcast`.`id`");
1109                    $sql = "SELECT %%SELECT%% FROM `podcast` ";
1110                    break;
1111                case 'podcast_episode':
1112                    $this->set_select("`podcast_episode`.`id`");
1113                    $sql = "SELECT %%SELECT%% FROM `podcast_episode` ";
1114                    break;
1115                case 'playlist_media':
1116                    $sql = '';
1117                    break;
1118                case 'song':
1119                default:
1120                    $this->set_select("`song`.`id`");
1121                    $sql = "SELECT %%SELECT%% FROM `song` ";
1122                    break;
1123            } // end base sql
1124        }
1125
1126        $this->_state['base'] = $sql;
1127
1128        return true;
1129    } // set_base_sql
1130
1131    /**
1132     * get_select
1133     * This returns the selects in a format that is friendly for a sql
1134     * statement.
1135     * @return string
1136     */
1137    private function get_select()
1138    {
1139        return implode(", ", $this->_state['select']);
1140    } // get_select
1141
1142    /**
1143     * get_base_sql
1144     * This returns the base sql statement all parsed up, this should be
1145     * called after all set operations.
1146     * @return string
1147     */
1148    private function get_base_sql()
1149    {
1150        return str_replace("%%SELECT%%", $this->get_select(), $this->_state['base']);
1151    } // get_base_sql
1152
1153    /**
1154     * get_filter_sql
1155     * This returns the filter part of the sql statement
1156     * @return string
1157     */
1158    private function get_filter_sql()
1159    {
1160        if (!is_array($this->_state['filter'])) {
1161            return '';
1162        }
1163
1164        $sql = "WHERE";
1165
1166        foreach ($this->_state['filter'] as $key => $value) {
1167            $sql .= $this->sql_filter($key, $value);
1168        }
1169
1170        if (AmpConfig::get('catalog_disable')) {
1171            // Add catalog enabled filter
1172            switch ($this->get_type()) {
1173                case "video":
1174                case "song":
1175                    $dis = Catalog::get_enable_filter('song', '`' . $this->get_type() . '`.`id`');
1176                    break;
1177                case "tag":
1178                    $dis = Catalog::get_enable_filter('tag', '`' . $this->get_type() . '`.`object_id`');
1179                    break;
1180            }
1181        }
1182        if (AmpConfig::get('catalog_filter')) {
1183            $type = $this->get_type();
1184            // Add catalog user filter
1185            switch ($type) {
1186                case 'video':
1187                case 'artist':
1188                case 'album':
1189                case 'song':
1190                case 'podcast':
1191                case 'podcast_episode':
1192                case 'playlist':
1193                case 'label':
1194                case 'live_stream':
1195                case 'tag':
1196                case 'tvshow':
1197                case 'tvshow_season':
1198                case 'tvshow_episode':
1199                case 'movie':
1200                case 'personal_video':
1201                case 'clip':
1202                case 'share':
1203                    $dis = Catalog::get_user_filter($type, $this->user_id);
1204                    break;
1205            }
1206        }
1207        if (!empty($dis)) {
1208            $sql .= $dis . " AND ";
1209        }
1210
1211        $sql = rtrim((string)$sql, " AND ") . " ";
1212        $sql = rtrim((string)$sql, "WHERE ") . " ";
1213
1214        return $sql;
1215    } // get_filter_sql
1216
1217    /**
1218     * get_sort_sql
1219     * Returns the sort sql part
1220     * @return string
1221     */
1222    private function get_sort_sql()
1223    {
1224        if (!is_array($this->_state['sort'])) {
1225            return '';
1226        }
1227
1228        $sql = 'ORDER BY ';
1229
1230        foreach ($this->_state['sort'] as $key => $value) {
1231            $sql .= $this->sql_sort($key, $value);
1232        }
1233
1234        $sql = rtrim((string)$sql, 'ORDER BY ');
1235        $sql = rtrim((string)$sql, ', ');
1236
1237        return $sql;
1238    } // get_sort_sql
1239
1240    /**
1241     * get_limit_sql
1242     * This returns the limit part of the sql statement
1243     * @return string
1244     */
1245    private function get_limit_sql()
1246    {
1247        if (!$this->is_simple() || $this->get_start() < 0) {
1248            return '';
1249        }
1250
1251        return ' LIMIT ' . (string)($this->get_start()) . ', ' . (string)($this->get_offset());
1252    } // get_limit_sql
1253
1254    /**
1255     * get_join_sql
1256     * This returns the joins that this browse may need to work correctly
1257     * @return string
1258     */
1259    private function get_join_sql()
1260    {
1261        if (!isset($this->_state['join']) || !is_array($this->_state['join'])) {
1262            return '';
1263        }
1264
1265        $sql = '';
1266
1267        foreach ($this->_state['join'] as $joins) {
1268            foreach ($joins as $join) {
1269                $sql .= $join . ' ';
1270            } // end foreach joins at this level
1271        } // end foreach of this level of joins
1272
1273        return $sql;
1274    } // get_join_sql
1275
1276    /**
1277     * get_having_sql
1278     * this returns the having sql stuff, if we've got anything
1279     * @return string
1280     */
1281    public function get_having_sql()
1282    {
1283        return isset($this->_state['having']) ? $this->_state['having'] : '';
1284    } // get_having_sql
1285
1286    /**
1287     * get_sql
1288     * This returns the sql statement we are going to use this has to be run
1289     * every time we get the objects because it depends on the filters and
1290     * the type of object we are currently browsing.
1291     * @param boolean $limit
1292     * @return string
1293     */
1294    public function get_sql($limit = true)
1295    {
1296        $sql = $this->get_base_sql();
1297
1298        $filter_sql = "";
1299        $join_sql   = "";
1300        $having_sql = "";
1301        $order_sql  = "";
1302        if (!isset($this->_state['custom']) || !$this->_state['custom']) {
1303            $filter_sql = $this->get_filter_sql();
1304            $order_sql  = $this->get_sort_sql();
1305            $join_sql   = $this->get_join_sql();
1306            $having_sql = $this->get_having_sql();
1307        }
1308        $limit_sql = $limit ? $this->get_limit_sql() : '';
1309        $final_sql = $sql . $join_sql . $filter_sql . $having_sql;
1310
1311        // filter albums when you have grouped disks!
1312        if ($this->get_type() == 'album' && !$this->_state['custom'] && AmpConfig::get('album_group') && $this->_state['sort']) {
1313            $album_artist = (array_key_exists('album_artist', $this->_state['sort'])) ? " `artist`.`name`," : '';
1314            $final_sql .= " GROUP BY" . $album_artist . " `album`.`prefix`, `album`.`name`, `album`.`album_artist`, `album`.`release_type`, `album`.`release_status`, `album`.`mbid`, `album`.`year`, `album`.`original_year` ";
1315        } elseif (($this->get_type() == 'artist' || $this->get_type() == 'album') && !$this->_state['custom']) {
1316            $final_sql .= " GROUP BY `" . $this->get_type() . "`.`name`, `" . $this->get_type() . "`.`id` ";
1317        }
1318        $final_sql .= $order_sql . $limit_sql;
1319        //debug_event(self::class, "get_sql: " . $final_sql, 5);
1320
1321        return $final_sql;
1322    } // get_sql
1323
1324    /**
1325     * post_process
1326     * This does some additional work on the results that we've received
1327     * before returning them.
1328     * @param array $data
1329     * @return array
1330     */
1331    private function post_process($data)
1332    {
1333        $tags = isset($this->_state['filter']['tag']) ? $this->_state['filter']['tag'] : '';
1334
1335        if (!is_array($tags) || sizeof($tags) < 2) {
1336            return $data;
1337        }
1338
1339        $tag_count = sizeof($tags);
1340        $count     = array();
1341
1342        foreach ($data as $row) {
1343            $count[$row['id']]++;
1344        }
1345
1346        $results = array();
1347
1348        foreach ($count as $key => $value) {
1349            if ($value >= $tag_count) {
1350                $results[] = array('id' => $key);
1351            }
1352        } // end foreach
1353
1354        return $results;
1355    } // post_process
1356
1357    /**
1358     * sql_filter
1359     * This takes a filter name and value and if it is possible
1360     * to filter by this name on this type returns the appropriate sql
1361     * if not returns nothing
1362     * @param string $filter
1363     * @param mixed $value
1364     * @return string
1365     */
1366    private function sql_filter($filter, $value)
1367    {
1368        $filter_sql = '';
1369        switch ($this->get_type()) {
1370            case 'song':
1371                switch ($filter) {
1372                    case 'tag':
1373                        $this->set_join('LEFT', '`tag_map`', '`tag_map`.`object_id`', '`song`.`id`', 100);
1374                        $filter_sql = "`tag_map`.`object_type`='" . $this->get_type() . "' AND (";
1375
1376                        foreach ($value as $tag_id) {
1377                            $filter_sql .= "`tag_map`.`tag_id`='" . Dba::escape($tag_id) . "' AND ";
1378                        }
1379                        $filter_sql = rtrim((string) $filter_sql, 'AND ') . ") AND ";
1380                        break;
1381                    case 'exact_match':
1382                        $filter_sql = " `song`.`title` = '" . Dba::escape($value) . "' AND ";
1383                        break;
1384                    case 'alpha_match':
1385                        $filter_sql = " `song`.`title` LIKE '%" . Dba::escape($value) . "%' AND ";
1386                        break;
1387                    case 'regex_match':
1388                        if (!empty($value)) {
1389                            $filter_sql = " `song`.`title` REGEXP '" . Dba::escape($value) . "' AND ";
1390                        }
1391                        break;
1392                    case 'regex_not_match':
1393                        if (!empty($value)) {
1394                            $filter_sql = " `song`.`title` NOT REGEXP '" . Dba::escape($value) . "' AND ";
1395                        }
1396                        break;
1397                    case 'starts_with':
1398                        $filter_sql = " `song`.`title` LIKE '" . Dba::escape($value) . "%' AND ";
1399                        if ($this->catalog != 0) {
1400                            $filter_sql .= " `song`.`catalog` = '" . $this->catalog . "' AND ";
1401                        }
1402                        break;
1403                    case 'unplayed':
1404                        $filter_sql = " `song`.`played`='0' AND ";
1405                        break;
1406                    case 'album':
1407                        $filter_sql = " `song`.`album` = '" . Dba::escape($value) . "' AND ";
1408                        break;
1409                    case 'artist':
1410                        $filter_sql = " `song`.`artist` = '" . Dba::escape($value) . "' AND ";
1411                        break;
1412                    case 'add_lt':
1413                        $filter_sql = " `song`.`addition_time` <= '" . Dba::escape($value) . "' AND ";
1414                        break;
1415                    case 'add_gt':
1416                        $filter_sql = " `song`.`addition_time` >= '" . Dba::escape($value) . "' AND ";
1417                        break;
1418                    case 'update_lt':
1419                        $filter_sql = " `song`.`update_time` <= '" . Dba::escape($value) . "' AND ";
1420                        break;
1421                    case 'update_gt':
1422                        $filter_sql = " `song`.`update_time` >= '" . Dba::escape($value) . "' AND ";
1423                        break;
1424                    case 'catalog':
1425                        if ($value != 0) {
1426                            $filter_sql = " `song`.`catalog` = '$value' AND ";
1427                        }
1428                        break;
1429                    case 'catalog_enabled':
1430                        $this->set_join('LEFT', '`catalog`', '`catalog`.`id`', '`song`.`catalog`', 100);
1431                        $filter_sql = " `catalog`.`enabled` = '1' AND ";
1432                        break;
1433                    case 'enabled':
1434                        $filter_sql = " `song`.`enabled`= '$value' AND ";
1435                        break;
1436                    default:
1437                        break;
1438                } // end list of sqlable filters
1439                break;
1440            case 'album':
1441                switch ($filter) {
1442                    case 'tag':
1443                        $this->set_join('LEFT', '`tag_map`', '`tag_map`.`object_id`', '`album`.`id`', 100);
1444                        $filter_sql = "`tag_map`.`object_type`='" . $this->get_type() . "' AND (";
1445
1446                        foreach ($value as $tag_id) {
1447                            $filter_sql .= "`tag_map`.`tag_id`='" . Dba::escape($tag_id) . "' AND ";
1448                        }
1449                        $filter_sql = rtrim((string) $filter_sql, 'AND ') . ") AND ";
1450                        break;
1451                    case 'exact_match':
1452                        $filter_sql = " `album`.`name` = '" . Dba::escape($value) . "' AND ";
1453                        break;
1454                    case 'alpha_match':
1455                        $filter_sql = " `album`.`name` LIKE '%" . Dba::escape($value) . "%' AND ";
1456                        break;
1457                    case 'regex_match':
1458                        if (!empty($value)) {
1459                            $filter_sql = " `album`.`name` REGEXP '" . Dba::escape($value) . "' AND ";
1460                        }
1461                        break;
1462                    case 'regex_not_match':
1463                        if (!empty($value)) {
1464                            $filter_sql = " `album`.`name` NOT REGEXP '" . Dba::escape($value) . "' AND ";
1465                        }
1466                        break;
1467                    case 'starts_with':
1468                        $this->set_join('LEFT', '`song`', '`album`.`id`', '`song`.`album`', 100);
1469                        $filter_sql = " `album`.`name` LIKE '" . Dba::escape($value) . "%' AND ";
1470                        if ($this->catalog != 0) {
1471                            $filter_sql .= "`song`.`catalog` = '" . $this->catalog . "' AND ";
1472                        }
1473                        break;
1474                    case 'artist':
1475                        $filter_sql = " `artist`.`id` = '" . Dba::escape($value) . "' AND ";
1476                        break;
1477                    case 'add_lt':
1478                        $this->set_join('LEFT', '`song`', '`song`.`album`', '`album`.`id`', 100);
1479                        $filter_sql = " `song`.`addition_time` <= '" . Dba::escape($value) . "' AND ";
1480                        break;
1481                    case 'add_gt':
1482                        $this->set_join('LEFT', '`song`', '`song`.`album`', '`album`.`id`', 100);
1483                        $filter_sql = " `song`.`addition_time` >= '" . Dba::escape($value) . "' AND ";
1484                        break;
1485                    case 'update_lt':
1486                        $this->set_join('LEFT', '`song`', '`song`.`album`', '`album`.`id`', 100);
1487                        $filter_sql = " `song`.`update_time` <= '" . Dba::escape($value) . "' AND ";
1488                        break;
1489                    case 'update_gt':
1490                        $this->set_join('LEFT', '`song`', '`song`.`album`', '`album`.`id`', 100);
1491                        $filter_sql = " `song`.`update_time` >= '" . Dba::escape($value) . "' AND ";
1492                        break;
1493                    case 'catalog':
1494                        if ($value != 0) {
1495                            $filter_sql = " (`album`.`catalog` = '$value') AND ";
1496                        }
1497                        break;
1498                    case 'catalog_enabled':
1499                        $this->set_join('LEFT', '`catalog`', '`catalog`.`id`', '`album`.`catalog`', 100);
1500                        $filter_sql = " `catalog`.`enabled` = '1' AND ";
1501                        break;
1502                    default:
1503                        break;
1504                }
1505                break;
1506            case 'artist':
1507                switch ($filter) {
1508                    case 'tag':
1509                        $this->set_join('LEFT', '`tag_map`', '`tag_map`.`object_id`', '`artist`.`id`', 100);
1510                        $filter_sql = "`tag_map`.`object_type`='" . $this->get_type() . "' AND (";
1511
1512                        foreach ($value as $tag_id) {
1513                            $filter_sql .= "`tag_map`.`tag_id`='" . Dba::escape($tag_id) . "' AND ";
1514                        }
1515                        $filter_sql = rtrim((string) $filter_sql, 'AND ') . ') AND ';
1516                        break;
1517                    case 'exact_match':
1518                        $filter_sql = " `artist`.`name` = '" . Dba::escape($value) . "' AND ";
1519                        break;
1520                    case 'alpha_match':
1521                        $filter_sql = " `artist`.`name` LIKE '%" . Dba::escape($value) . "%' AND ";
1522                        break;
1523                    case 'regex_match':
1524                        if (!empty($value)) {
1525                            $filter_sql = " `artist`.`name` REGEXP '" . Dba::escape($value) . "' AND ";
1526                        }
1527                        break;
1528                    case 'regex_not_match':
1529                        if (!empty($value)) {
1530                            $filter_sql = " `artist`.`name` NOT REGEXP '" . Dba::escape($value) . "' AND ";
1531                        }
1532                        break;
1533                    case 'starts_with':
1534                        $this->set_join('LEFT', '`song`', '`artist`.`id`', '`song`.`artist`', 100);
1535                        $filter_sql = " `artist`.`name` LIKE '" . Dba::escape($value) . "%' AND ";
1536                        if ($this->catalog != 0) {
1537                            $filter_sql .= "`song`.`catalog` = '" . $this->catalog . "' AND ";
1538                        }
1539                        break;
1540                    case 'add_lt':
1541                        $this->set_join('LEFT', '`song`', '`song`.`artist`', '`artist`.`id`', 100);
1542                        $filter_sql = " `song`.`addition_time` <= '" . Dba::escape($value) . "' AND ";
1543                        break;
1544                    case 'add_gt':
1545                        $this->set_join('LEFT', '`song`', '`song`.`artist`', '`artist`.`id`', 100);
1546                        $filter_sql = " `song`.`addition_time` >= '" . Dba::escape($value) . "' AND ";
1547                        break;
1548                    case 'update_lt':
1549                        $this->set_join('LEFT', '`song`', '`song`.`artist`', '`artist`.`id`', 100);
1550                        $filter_sql = " `song`.`update_time` <= '" . Dba::escape($value) . "' AND ";
1551                        break;
1552                    case 'update_gt':
1553                        $this->set_join('LEFT', '`song`', '`song`.`artist`', '`artist`.`id`', 100);
1554                        $filter_sql = " `song`.`update_time` >= '" . Dba::escape($value) . "' AND ";
1555                        break;
1556                    case 'catalog':
1557                        if ($value != 0) {
1558                            $this->set_join_and('LEFT', '`catalog_map`', '`catalog_map`.`object_id`', '`artist`.`id`', '`catalog_map`.`object_type`', '\'artist\'', 100);
1559                            $filter_sql = " (`catalog_map`.`catalog_id` = '$value') AND ";
1560                        }
1561                        break;
1562                    case 'catalog_enabled':
1563                        $this->set_join_and('LEFT', '`catalog_map`', '`catalog_map`.`object_id`', '`artist`.`id`', '`catalog_map`.`object_type`', '\'artist\'', 100);
1564                        $this->set_join('LEFT', '`catalog`', '`catalog`.`id`', '`catalog_map`.`catalog_id`', 100);
1565                        $filter_sql = " `catalog`.`enabled` = '1' AND ";
1566                        break;
1567                    case 'album_artist':
1568                        $this->set_join('LEFT', '`album`', '`album`.`album_artist`', '`artist`.`id`', 100);
1569                        $filter_sql = " `album`.`album_artist` IS NOT NULL AND ";
1570                        break;
1571                    default:
1572                        break;
1573                } // end filter
1574                break;
1575            case 'live_stream':
1576                switch ($filter) {
1577                    case 'alpha_match':
1578                        $filter_sql = " `live_stream`.`name` LIKE '%" . Dba::escape($value) . "%' AND ";
1579                        break;
1580                    case 'regex_match':
1581                        if (!empty($value)) {
1582                            $filter_sql = " `live_stream`.`name` REGEXP '" . Dba::escape($value) . "' AND ";
1583                        }
1584                        break;
1585                    case 'regex_not_match':
1586                        if (!empty($value)) {
1587                            $filter_sql = " `live_stream`.`name` NOT REGEXP '" . Dba::escape($value) . "' AND ";
1588                        }
1589                        break;
1590                    case 'starts_with':
1591                        $filter_sql = " `live_stream`.`name` LIKE '" . Dba::escape($value) . "%' AND ";
1592                        break;
1593                    case 'catalog_enabled':
1594                        $this->set_join('LEFT', '`catalog`', '`catalog`.`id`', '`live_stream`.`catalog`', 100);
1595                        $filter_sql = " `catalog`.`enabled` = '1' AND ";
1596                        break;
1597                    default:
1598                        break;
1599                } // end filter
1600                break;
1601            case 'playlist':
1602                switch ($filter) {
1603                    case 'alpha_match':
1604                        $filter_sql = " `playlist`.`name` LIKE '%" . Dba::escape($value) . "%' AND ";
1605                        break;
1606                    case 'regex_match':
1607                        if (!empty($value)) {
1608                            $filter_sql = " `playlist`.`name` REGEXP '" . Dba::escape($value) . "' AND ";
1609                        }
1610                        break;
1611                    case 'regex_not_match':
1612                        if (!empty($value)) {
1613                            $filter_sql = " `playlist`.`name` NOT REGEXP '" . Dba::escape($value) . "' AND ";
1614                        }
1615                        break;
1616                    case 'starts_with':
1617                        $filter_sql = " `playlist`.`name` LIKE '" . Dba::escape($value) . "%' AND ";
1618                        break;
1619                    case 'playlist_type':
1620                        $user_id    = ((int) Core::get_global('user')->id > 0) ? Core::get_global('user')->id : $value;
1621                        $filter_sql = " (`playlist`.`type` = 'public' OR `playlist`.`user`='$user_id') AND ";
1622                        break;
1623                    default:
1624                        break;
1625                } // end filter
1626                break;
1627            case 'smartplaylist':
1628                switch ($filter) {
1629                    case 'alpha_match':
1630                        $filter_sql = " `search`.`name` LIKE '%" . Dba::escape($value) . "%' AND ";
1631                        break;
1632                    case 'regex_match':
1633                        if (!empty($value)) {
1634                            $filter_sql = " `search`.`name` REGEXP '" . Dba::escape($value) . "' AND ";
1635                        }
1636                        break;
1637                    case 'regex_not_match':
1638                        if (!empty($value)) {
1639                            $filter_sql = " `search`.`name` NOT REGEXP '" . Dba::escape($value) . "' AND ";
1640                        }
1641                        break;
1642                    case 'starts_with':
1643                        $filter_sql = " `search`.`name` LIKE '" . Dba::escape($value) . "%' AND ";
1644                        break;
1645                    case 'playlist_type':
1646                        $user_id    = ((int) Core::get_global('user')->id > 0) ? Core::get_global('user')->id : $value;
1647                        $filter_sql = " (`search`.`type` = 'public' OR `search`.`user`='$user_id') AND ";
1648                        break;
1649                } // end switch on $filter
1650                break;
1651            case 'tag':
1652                switch ($filter) {
1653                    case 'alpha_match':
1654                        $filter_sql = " `tag`.`name` LIKE '%" . Dba::escape($value) . "%' AND ";
1655                        break;
1656                    case 'regex_match':
1657                        if (!empty($value)) {
1658                            $filter_sql = " `tag`.`name` REGEXP '" . Dba::escape($value) . "' AND ";
1659                        }
1660                        break;
1661                    case 'regex_not_match':
1662                        if (!empty($value)) {
1663                            $filter_sql = " `tag`.`name` NOT REGEXP '" . Dba::escape($value) . "' AND ";
1664                        }
1665                        break;
1666                    case 'exact_match':
1667                        $filter_sql = " `tag`.`name` = '" . Dba::escape($value) . "' AND ";
1668                        break;
1669                    case 'tag':
1670                        $filter_sql = " `tag`.`id` = '" . Dba::escape($value) . "' AND ";
1671                        break;
1672                    default:
1673                        break;
1674                } // end filter
1675                break;
1676            case 'video':
1677                switch ($filter) {
1678                    case 'tag':
1679                        $this->set_join('LEFT', '`tag_map`', '`tag_map`.`object_id`', '`video`.`id`', 100);
1680                        $filter_sql = "`tag_map`.`object_type`='" . $this->get_type() . "' AND (";
1681
1682                        foreach ($value as $tag_id) {
1683                            $filter_sql .= "`tag_map`.`tag_id`='" . Dba::escape($tag_id) . "' AND ";
1684                        }
1685                        $filter_sql = rtrim((string) $filter_sql, 'AND ') . ') AND ';
1686                        break;
1687                    case 'alpha_match':
1688                        $filter_sql = " `video`.`title` LIKE '%" . Dba::escape($value) . "%' AND ";
1689                        break;
1690                    case 'regex_match':
1691                        if (!empty($value)) {
1692                            $filter_sql = " `video`.`title` REGEXP '" . Dba::escape($value) . "' AND ";
1693                        }
1694                        break;
1695                    case 'regex_not_match':
1696                        if (!empty($value)) {
1697                            $filter_sql = " `video`.`title` NOT REGEXP '" . Dba::escape($value) . "' AND ";
1698                        }
1699                        break;
1700                    case 'starts_with':
1701                        $filter_sql = " `video`.`title` LIKE '" . Dba::escape($value) . "%' AND ";
1702                        break;
1703                    default:
1704                        break;
1705                } // end filter
1706                break;
1707            case 'license':
1708                switch ($filter) {
1709                    case 'alpha_match':
1710                        $filter_sql = " `license`.`name` LIKE '%" . Dba::escape($value) . "%' AND ";
1711                        break;
1712                    case 'regex_match':
1713                        if (!empty($value)) {
1714                            $filter_sql = " `license`.`name` REGEXP '" . Dba::escape($value) . "' AND ";
1715                        }
1716                        break;
1717                    case 'regex_not_match':
1718                        if (!empty($value)) {
1719                            $filter_sql = " `license`.`name` NOT REGEXP '" . Dba::escape($value) . "' AND ";
1720                        }
1721                        break;
1722                    case 'exact_match':
1723                        $filter_sql = " `license`.`name` = '" . Dba::escape($value) . "' AND ";
1724                        break;
1725                    default:
1726                        break;
1727                } // end filter
1728                break;
1729            case 'tvshow':
1730                switch ($filter) {
1731                    case 'alpha_match':
1732                        $filter_sql = " `tvshow`.`name` LIKE '%" . Dba::escape($value) . "%' AND ";
1733                        break;
1734                    case 'regex_match':
1735                        if (!empty($value)) {
1736                            $filter_sql = " `tvshow`.`name` REGEXP '" . Dba::escape($value) . "' AND ";
1737                        }
1738                        break;
1739                    case 'regex_not_match':
1740                        if (!empty($value)) {
1741                            $filter_sql = " `tvshow`.`name` NOT REGEXP '" . Dba::escape($value) . "' AND ";
1742                        }
1743                        break;
1744                    case 'exact_match':
1745                        $filter_sql = " `tvshow`.`name` = '" . Dba::escape($value) . "' AND ";
1746                        break;
1747                    case 'year_lt':
1748                        $filter_sql = " `tvshow`.`year` < '" . Dba::escape($value) . "' AND ";
1749                        break;
1750                    case 'year_gt':
1751                        $filter_sql = " `tvshow`.`year` > '" . Dba::escape($value) . "' AND ";
1752                        break;
1753                    case 'year_eq':
1754                        $filter_sql = " `tvshow`.`year` = '" . Dba::escape($value) . "' AND ";
1755                        break;
1756                    default:
1757                        break;
1758                } // end filter
1759                break;
1760            case 'tvshow_season':
1761                switch ($filter) {
1762                    case 'season_lt':
1763                        $filter_sql = " `tvshow_season`.`season_number` < '" . Dba::escape($value) . "' AND ";
1764                        break;
1765                    case 'season_gt':
1766                        $filter_sql = " `tvshow_season`.`season_number` > '" . Dba::escape($value) . "' AND ";
1767                        break;
1768                    case 'season_eq':
1769                        $filter_sql = " `tvshow_season`.`season_number` = '" . Dba::escape($value) . "' AND ";
1770                        break;
1771                    default:
1772                        break;
1773                } // end filter
1774                break;
1775            case 'user':
1776                switch ($filter) {
1777                    case 'starts_with':
1778                        $filter_sql = " (`user`.`fullname` LIKE '" . Dba::escape($value) . "%' OR `user`.`username` LIKE '" . Dba::escape($value) . "%' OR `user`.`email` LIKE '" . Dba::escape($value) . "%') AND ";
1779                        break;
1780                } // end filter
1781                break;
1782            case 'label':
1783                switch ($filter) {
1784                    case 'alpha_match':
1785                        $filter_sql = " `label`.`name` LIKE '%" . Dba::escape($value) . "%' AND ";
1786                        break;
1787                    case 'regex_match':
1788                        if (!empty($value)) {
1789                            $filter_sql = " `label`.`name` REGEXP '" . Dba::escape($value) . "' AND ";
1790                        }
1791                        break;
1792                    case 'regex_not_match':
1793                        if (!empty($value)) {
1794                            $filter_sql = " `label`.`name` NOT REGEXP '" . Dba::escape($value) . "' AND ";
1795                        }
1796                        break;
1797                    case 'starts_with':
1798                        $filter_sql = " `label`.`name` LIKE '" . Dba::escape($value) . "%' AND ";
1799                        break;
1800                    default:
1801                        break;
1802                } // end filter
1803                break;
1804            case 'pvmsg':
1805                switch ($filter) {
1806                    case 'alpha_match':
1807                        $filter_sql = " `user_pvmsg`.`subject` LIKE '%" . Dba::escape($value) . "%' AND ";
1808                        break;
1809                    case 'regex_match':
1810                        if (!empty($value)) {
1811                            $filter_sql = " `user_pvmsg`.`subject` REGEXP '" . Dba::escape($value) . "' AND ";
1812                        }
1813                        break;
1814                    case 'regex_not_match':
1815                        if (!empty($value)) {
1816                            $filter_sql = " `user_pvmsg`.`subject` NOT REGEXP '" . Dba::escape($value) . "' AND ";
1817                        }
1818                        break;
1819                    case 'starts_with':
1820                        $filter_sql = " `user_pvmsg`.`subject` LIKE '" . Dba::escape($value) . "%' AND ";
1821                        break;
1822                    case 'user':
1823                        $filter_sql = " `user_pvmsg`.`from_user` = '" . Dba::escape($value) . "' AND ";
1824                        break;
1825                    case 'to_user':
1826                        $filter_sql = " `user_pvmsg`.`to_user` = '" . Dba::escape($value) . "' AND ";
1827                        break;
1828                    default:
1829                        break;
1830                } // end filter
1831                break;
1832            case 'follower':
1833                switch ($filter) {
1834                    case 'user':
1835                        $filter_sql = " `user_follower`.`user` = '" . Dba::escape($value) . "' AND ";
1836                        break;
1837                    case 'to_user':
1838                        $filter_sql = " `user_follower`.`follow_user` = '" . Dba::escape($value) . "' AND ";
1839                        break;
1840                    default:
1841                        break;
1842                } // end filter
1843                break;
1844            case 'podcast':
1845                switch ($filter) {
1846                    case 'alpha_match':
1847                        $filter_sql = " `podcast`.`title` LIKE '%" . Dba::escape($value) . "%' AND ";
1848                        break;
1849                    case 'regex_match':
1850                        if (!empty($value)) {
1851                            $filter_sql = " `podcast`.`title` REGEXP '" . Dba::escape($value) . "' AND ";
1852                        }
1853                        break;
1854                    case 'regex_not_match':
1855                        if (!empty($value)) {
1856                            $filter_sql = " `podcast`.`title` NOT REGEXP '" . Dba::escape($value) . "' AND ";
1857                        }
1858                        break;
1859                    case 'starts_with':
1860                        $filter_sql = " `podcast`.`title` LIKE '" . Dba::escape($value) . "%' AND ";
1861                        break;
1862                    default:
1863                        break;
1864                } // end filter
1865                break;
1866            case 'podcast_episode':
1867                switch ($filter) {
1868                    case 'alpha_match':
1869                        $filter_sql = " `podcast_episode`.`title` LIKE '%" . Dba::escape($value) . "%' AND ";
1870                        break;
1871                    case 'regex_match':
1872                        if (!empty($value)) {
1873                            $filter_sql = " `podcast_episode`.`title` REGEXP '" . Dba::escape($value) . "' AND ";
1874                        }
1875                        break;
1876                    case 'regex_not_match':
1877                        if (!empty($value)) {
1878                            $filter_sql = " `podcast_episode`.`title` NOT REGEXP '" . Dba::escape($value) . "' AND ";
1879                        }
1880                        break;
1881                    case 'starts_with':
1882                        $filter_sql = " `podcast_episode`.`title` LIKE '" . Dba::escape($value) . "%' AND ";
1883                        break;
1884                    default:
1885                        break;
1886                } // end filter
1887                break;
1888        } // end switch on type
1889
1890        return $filter_sql;
1891    } // sql_filter
1892
1893    /**
1894     * logic_filter
1895     * This runs the filters that we can't easily apply
1896     * to the sql so they have to be done after the fact
1897     * these should be limited as they are often intensive and
1898     * require additional queries per object... :(
1899     *
1900     * @param integer $object_id
1901     * @return boolean
1902     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
1903     */
1904    private function logic_filter($object_id)
1905    {
1906        return true;
1907    } // logic_filter
1908
1909    /**
1910     * sql_sort
1911     * This builds any order bys we need to do
1912     * to sort the results as best we can, there is also
1913     * a logic based sort that will come later as that's
1914     * a lot more complicated
1915     * @param string $field
1916     * @param string $order
1917     * @return string
1918     */
1919    private function sql_sort($field, $order)
1920    {
1921        if ($order != 'DESC') {
1922            $order = 'ASC';
1923        }
1924
1925        // Depending on the type of browsing we are doing we can apply
1926        // different filters that apply to different fields
1927        switch ($this->get_type()) {
1928            case 'song':
1929                switch ($field) {
1930                    case 'title':
1931                    case 'year':
1932                    case 'track':
1933                    case 'time':
1934                    case 'composer':
1935                    case 'total_count':
1936                    case 'total_skip':
1937                        $sql = "`song`.`$field`";
1938                        break;
1939                    case 'album':
1940                    case 'artist':
1941                        $sql = "`$field`.`name`";
1942                        $this->set_join('LEFT', "`$field`", "`$field`.`id`", "`song`.`$field`", 100);
1943                        break;
1944                    default:
1945                        break;
1946                } // end switch
1947                break;
1948            case 'album':
1949                switch ($field) {
1950                    case 'name':
1951                        $sql = "`album`.`name` $order, `album`.`disk`";
1952                        if (AmpConfig::get('album_group')) {
1953                            $sql = "`album`.`name`";
1954                        }
1955                        break;
1956                    case 'generic_artist':
1957                        $sql = "`artist`.`name`";
1958                        $this->set_join('LEFT', '`song`', '`song`.`album`', '`album`.`id`', 100);
1959                        $this->set_join('LEFT', '`artist`', 'COALESCE(`album`.`album_artist`, `song`.`artist`)',
1960                            '`artist`.`id`', 100);
1961                        break;
1962                    case 'album_artist':
1963                        $sql = "`artist`.`name`";
1964                        $this->set_join('LEFT', '`artist`', '`album`.`album_artist`', '`artist`.`id`', 100);
1965                        break;
1966                    case 'artist':
1967                        $sql = "`artist`.`name`";
1968                        $this->set_join('LEFT', '`song`', '`song`.`album`', '`album`.`id`', 100);
1969                        $this->set_join('LEFT', '`artist`', '`song`.`artist`', '`artist`.`id`', 100);
1970                        break;
1971                    case 'year':
1972                    case 'original_year':
1973                    case 'song_count':
1974                    case 'total_count':
1975                    case 'release_type':
1976                        $sql = "`album`.`$field`";
1977                        break;
1978                } // end switch
1979                break;
1980            case 'artist':
1981                switch ($field) {
1982                    case 'name':
1983                    case 'placeformed':
1984                    case 'yearformed':
1985                    case 'song_count':
1986                    case 'album_count':
1987                    case 'total_count':
1988                        $sql = "`artist`.`$field`";
1989                        break;
1990                } // end switch
1991                break;
1992            case 'playlist':
1993                switch ($field) {
1994                    case 'type':
1995                    case 'name':
1996                    case 'user':
1997                    case 'last_update':
1998                        $sql = "`playlist`.`$field`";
1999                        break;
2000                } // end switch
2001                break;
2002            case 'smartplaylist':
2003                switch ($field) {
2004                    case 'type':
2005                    case 'name':
2006                    case 'user':
2007                        $sql = "`search`.`$field`";
2008                        break;
2009                } // end switch on $field
2010                break;
2011            case 'live_stream':
2012                switch ($field) {
2013                    case 'name':
2014                    case 'codec':
2015                        $sql = "`live_stream`.`$field`";
2016                        break;
2017                } // end switch
2018                break;
2019            case 'tag':
2020                switch ($field) {
2021                    case 'tag':
2022                        $sql = "`tag`.`id`";
2023                        break;
2024                    case 'name':
2025                        $sql = "`tag`.`name`";
2026                        break;
2027                } // end switch
2028                break;
2029            case 'user':
2030                switch ($field) {
2031                    case 'username':
2032                    case 'fullname':
2033                    case 'last_seen':
2034                    case 'create_date':
2035                        $sql = "`user`.`$field`";
2036                        break;
2037                } // end switch
2038                break;
2039            case 'video':
2040                $sql = $this->sql_sort_video($field);
2041                break;
2042            case 'wanted':
2043                switch ($field) {
2044                    case 'name':
2045                    case 'artist':
2046                    case 'year':
2047                    case 'user':
2048                    case 'accepted':
2049                        $sql = "`wanted`.`$field`";
2050                        break;
2051                } // end switch on field
2052                break;
2053            case 'share':
2054                switch ($field) {
2055                    case 'object':
2056                        $sql = "`share`.`object_type`, `share`.`object.id`";
2057                        break;
2058                    case 'user':
2059                    case 'object_type':
2060                    case 'creation_date':
2061                    case 'lastvisit_date':
2062                    case 'counter':
2063                    case 'max_counter':
2064                    case 'allow_stream':
2065                    case 'allow_download':
2066                    case 'expire':
2067                        $sql = "`share`.`$field`";
2068                        break;
2069                } // end switch on field
2070                break;
2071            case 'channel':
2072                switch ($field) {
2073                    case 'name':
2074                    case 'interface':
2075                    case 'port':
2076                    case 'max_listeners':
2077                    case 'listeners':
2078                        $sql = "`channel`.`$field`";
2079                        break;
2080                } // end switch on field
2081                break;
2082            case 'broadcast':
2083                switch ($field) {
2084                    case 'name':
2085                    case 'user':
2086                    case 'started':
2087                    case 'listeners':
2088                        $sql = "`broadcast`.`$field`";
2089                        break;
2090                } // end switch on field
2091                break;
2092            case 'license':
2093                switch ($field) {
2094                    case 'name':
2095                        $sql = "`license`.`name`";
2096                        break;
2097                }
2098                break;
2099            case 'tvshow':
2100                switch ($field) {
2101                    case 'name':
2102                    case 'year':
2103                        $sql = "`tvshow`.`$field`";
2104                        break;
2105                }
2106                break;
2107            case 'tvshow_season':
2108                switch ($field) {
2109                    case 'season':
2110                        $sql = "`tvshow_season`.`season_number`";
2111                        break;
2112                    case 'tvshow':
2113                        $sql = "`tvshow`.`name`";
2114                        $this->set_join('LEFT', '`tvshow`', '`tvshow_season`.`tvshow`', '`tvshow`.`id`', 100);
2115                        break;
2116                }
2117                break;
2118            case 'tvshow_episode':
2119                switch ($field) {
2120                    case 'episode':
2121                        $sql = "`tvshow_episode`.`episode_number`";
2122                        break;
2123                    case 'season':
2124                        $sql = "`tvshow_season`.`season_number`";
2125                        $this->set_join('LEFT', '`tvshow_season`', '`tvshow_episode`.`season`', '`tvshow_season`.`id`',
2126                            100);
2127                        break;
2128                    case 'tvshow':
2129                        $sql = "`tvshow`.`name`";
2130                        $this->set_join('LEFT', '`tvshow_season`', '`tvshow_episode`.`season`', '`tvshow_season`.`id`',
2131                            100);
2132                        $this->set_join('LEFT', '`tvshow`', '`tvshow_season`.`tvshow`', '`tvshow`.`id`', 100);
2133                        break;
2134                    default:
2135                        $sql = $this->sql_sort_video($field, 'tvshow_episode');
2136                        break;
2137                }
2138                break;
2139            case 'movie':
2140                $sql = $this->sql_sort_video($field, 'movie');
2141                break;
2142            case 'clip':
2143                switch ($field) {
2144                    case 'location':
2145                        $sql = "`clip`.`artist`";
2146                        break;
2147                    default:
2148                        $sql = $this->sql_sort_video($field, 'clip');
2149                        break;
2150                }
2151                break;
2152            case 'personal_video':
2153                switch ($field) {
2154                    case 'location':
2155                        $sql = "`personal_video`.`location`";
2156                        break;
2157                    default:
2158                        $sql = $this->sql_sort_video($field, 'personal_video');
2159                        break;
2160                }
2161                break;
2162            case 'label':
2163                switch ($field) {
2164                    case 'name':
2165                    case 'category':
2166                    case 'user':
2167                        $sql = "`label`.`$field`";
2168                        break;
2169                }
2170                break;
2171            case 'pvmsg':
2172                switch ($field) {
2173                    case 'subject':
2174                    case 'to_user':
2175                    case 'creation_date':
2176                    case 'is_read':
2177                        $sql = "`user_pvmsg`.`$field`";
2178                        break;
2179                }
2180                break;
2181            case 'follower':
2182                switch ($field) {
2183                    case 'user':
2184                    case 'follow_user':
2185                    case 'follow_date':
2186                        $sql = "`user_follower`.`$field`";
2187                        break;
2188                }
2189                break;
2190            case 'podcast':
2191                switch ($field) {
2192                    case 'title':
2193                        $sql = "`podcast`.`title`";
2194                        break;
2195                }
2196                break;
2197            case 'podcast_episode':
2198                switch ($field) {
2199                    case 'title':
2200                    case 'category':
2201                    case 'author':
2202                    case 'time':
2203                    case 'pubDate':
2204                        $sql = "`podcast_episode`.`$field`";
2205                        break;
2206                }
2207                break;
2208            default:
2209                break;
2210        } // end switch
2211
2212        if (isset($sql) && !empty($sql)) {
2213            return "$sql $order,";
2214        }
2215
2216        return "";
2217    } // sql_sort
2218
2219    /**
2220     *
2221     * @param string $field
2222     * @param string $table
2223     * @return string
2224     */
2225    private function sql_sort_video($field, $table = 'video')
2226    {
2227        $sql = "";
2228        switch ($field) {
2229            case 'title':
2230                $sql = "`video`.`title`";
2231                break;
2232            case 'resolution':
2233                $sql = "`video`.`resolution_x`";
2234                break;
2235            case 'length':
2236                $sql = "`video`.`time`";
2237                break;
2238            case 'codec':
2239                $sql = "`video`.`video_codec`";
2240                break;
2241            case 'release_date':
2242                $sql = "`video`.`release_date`";
2243                break;
2244        }
2245
2246        if (!empty($sql)) {
2247            if ($table != 'video') {
2248                $this->set_join('LEFT', '`video`', '`' . $table . '`.`id`', '`video`.`id`', 100);
2249            }
2250        }
2251
2252        return $sql;
2253    }
2254
2255    /**
2256     * resort_objects
2257     * This takes the existing objects, looks at the current
2258     * sort method and then re-sorts them This is internally
2259     * called by the set_sort() function
2260     * @return boolean
2261     */
2262    private function resort_objects()
2263    {
2264        // There are two ways to do this.. the easy way...
2265        // and the vollmer way, hopefully we don't have to
2266        // do it the vollmer way
2267        if ($this->is_simple()) {
2268            $sql = $this->get_sql();
2269        } else {
2270            // FIXME: this is fragile for large browses
2271            // First pull the objects
2272            $objects = $this->get_saved();
2273
2274            // If there's nothing there don't do anything
2275            if (!count($objects) || !is_array($objects)) {
2276                return false;
2277            }
2278            $type      = $this->get_type();
2279            $where_sql = "WHERE `$type`.`id` IN (";
2280
2281            foreach ($objects as $object_id) {
2282                $object_id = Dba::escape($object_id);
2283                $where_sql .= "'$object_id',";
2284            }
2285            $where_sql = rtrim((string)$where_sql, ', ');
2286
2287            $where_sql .= ")";
2288
2289            $sql = $this->get_base_sql();
2290
2291            $group_sql = " GROUP BY `" . $this->get_type() . '`.`id`';
2292            $order_sql = " ORDER BY ";
2293
2294            foreach ($this->_state['sort'] as $key => $value) {
2295                $sql_sort = $this->sql_sort($key, $value);
2296                $order_sql .= $sql_sort;
2297                $group_sql .= ", " . substr($sql_sort, 0, strpos($sql_sort, " "));
2298            }
2299            // Clean her up
2300            $order_sql = rtrim((string)$order_sql, "ORDER BY ");
2301            $order_sql = rtrim((string)$order_sql, ",");
2302
2303            $sql = $sql . $this->get_join_sql() . $where_sql . $group_sql . $order_sql;
2304        } // if not simple
2305
2306        $db_results = Dba::read($sql);
2307        //debug_event(self::class, "resort_objects: " . $sql, 5);
2308
2309        $results = array();
2310        while ($row = Dba::fetch_assoc($db_results)) {
2311            $results[] = $row['id'];
2312        }
2313
2314        $this->save_objects($results);
2315
2316        return true;
2317    } // resort_objects
2318
2319    /**
2320     * store
2321     * This saves the current state to the database
2322     */
2323    public function store()
2324    {
2325        $browse_id = $this->id;
2326        if ($browse_id != 'nocache') {
2327            $data = self::_serialize($this->_state);
2328
2329            $sql = 'UPDATE `tmp_browse` SET `data` = ? WHERE `sid` = ? AND `id` = ?';
2330            Dba::write($sql, array($data, session_id(), $browse_id));
2331        }
2332    }
2333
2334    /**
2335     * save_objects
2336     * This takes the full array of object ids, often passed into show and
2337     * if necessary it saves them
2338     * @param array $object_ids
2339     * @return boolean
2340     */
2341    public function save_objects($object_ids)
2342    {
2343        // Saving these objects has two operations, one holds it in
2344        // a local variable and then second holds it in a row in the
2345        // tmp_browse table
2346
2347        // Only do this if it's not a simple browse
2348        if (!$this->is_simple()) {
2349            $this->_cache = $object_ids;
2350            $this->set_total(count($object_ids));
2351            $browse_id = $this->id;
2352            if ($browse_id != 'nocache') {
2353                $data = self::_serialize($this->_cache);
2354
2355                $sql = 'UPDATE `tmp_browse` SET `object_data` = ? WHERE `sid` = ? AND `id` = ?';
2356                Dba::write($sql, array($data, session_id(), $browse_id));
2357            }
2358        }
2359
2360        return true;
2361    } // save_objects
2362
2363    /**
2364     * get_state
2365     * This is a debug only function
2366     * @return array
2367     */
2368    public function get_state()
2369    {
2370        return $this->_state;
2371    } // get_state
2372
2373    /**
2374     * Get content div name
2375     * @return string
2376     */
2377    public function get_content_div()
2378    {
2379        $key = 'browse_content_' . $this->get_type();
2380        if ($this->_state['ak']) {
2381            $key .= '_' . $this->_state['ak'];
2382        }
2383
2384        return $key;
2385    }
2386
2387    /**
2388     * Set an additional content div key.
2389     * @param string $key
2390     */
2391    public function set_content_div_ak($key)
2392    {
2393        $this->_state['ak'] = $key;
2394    }
2395}
2396