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