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\Config\AmpConfig;
28use Ampache\Module\Authorization\AccessLevelEnum;
29use Ampache\Module\Statistics\Stats;
30use Ampache\Module\System\AmpError;
31use Ampache\Module\System\Core;
32use Ampache\Module\System\Dba;
33use Ampache\Module\Util\Ui;
34use Ampache\Repository\IpHistoryRepositoryInterface;
35use Ampache\Repository\UserRepositoryInterface;
36use Exception;
37use PDOStatement;
38
39/**
40 * This class handles all of the user related functions including the creation
41 * and deletion of the user objects from the database by default you construct it
42 * with a user_id from user.id
43 */
44class User extends database_object
45{
46    protected const DB_TABLENAME = 'user';
47
48    // Basic Components
49    /**
50     * @var integer $id
51     */
52    public $id;
53    /**
54     * @var string $username
55     */
56    public $username;
57    /**
58     * @var string $fullname
59     */
60    public $fullname;
61    /**
62     * @var boolean $fullname_public
63     */
64    public $fullname_public;
65    /**
66     * @var integer $access
67     */
68    public $access;
69    /**
70     * @var boolean $disabled
71     */
72    public $disabled;
73    /**
74     * @var string $email
75     */
76    public $email;
77    /**
78     * @var integer $last_seen
79     */
80    public $last_seen;
81    /**
82     * @var integer $create_date
83     */
84    public $create_date;
85    /**
86     * @var string $validation
87     */
88    public $validation;
89    /**
90     * @var string $website
91     */
92    public $website;
93    /**
94     * @var string $state
95     */
96    public $state;
97    /**
98     * @var string $city
99     */
100    public $city;
101    /**
102     * @var string $apikey
103     */
104    public $apikey;
105    /**
106     * @var string $rsstoken
107     */
108    public $rsstoken;
109
110    // Constructed variables
111    /**
112     * @var array $prefs
113     */
114    public $prefs = array();
115
116    /**
117     * @var Tmp_Playlist $playlist
118     */
119    public $playlist;
120
121    /**
122     * @var string $f_name
123     */
124    public $f_name;
125    /**
126     * @var string $f_last_seen
127     */
128    public $f_last_seen;
129    /**
130     * @var string $f_create_date
131     */
132    public $f_create_date;
133    /**
134     * @var string $link
135     */
136    public $link;
137    /**
138     * @var string $f_link
139     */
140    public $f_link;
141    /**
142     * @var string $f_usage
143     */
144    public $f_usage;
145    /**
146     * @var string $ip_history
147     */
148    public $ip_history;
149    /**
150     * @var string $f_avatar
151     */
152    public $f_avatar;
153    /**
154     * @var string $f_avatar_mini
155     */
156    public $f_avatar_mini;
157    /**
158     * @var string $f_avatar_medium
159     */
160    public $f_avatar_medium;
161
162    /**
163     * Constructor
164     * This function is the constructor object for the user
165     * class, it currently takes a username
166     * @param integer $user_id
167     */
168    public function __construct($user_id = 0)
169    {
170        if (!$user_id) {
171            return false;
172        }
173
174        $this->id = (int)($user_id);
175
176        $info = $this->has_info();
177
178        foreach ($info as $key => $value) {
179            // Let's not save the password in this object :S
180            if ($key == 'password') {
181                continue;
182            }
183            $this->$key = $value;
184        }
185
186        // Make sure the Full name is always filled
187        if (strlen((string)$this->fullname) < 1) {
188            $this->fullname = $this->username;
189        }
190
191        return true;
192    } // Constructor
193
194    public function getId(): int
195    {
196        return (int) $this->id;
197    }
198
199    /**
200     * count
201     *
202     * This returns the number of user accounts that exist.
203     */
204    public static function count()
205    {
206        $sql              = 'SELECT COUNT(`id`) FROM `user`';
207        $db_results       = Dba::read($sql);
208        $data             = Dba::fetch_row($db_results);
209        $results          = array();
210        $results['users'] = $data[0];
211
212        $time                 = time();
213        $last_seen            = $time - 1200;
214        $sql                  = "SELECT COUNT(DISTINCT `session`.`username`) FROM `session` INNER JOIN `user` ON `session`.`username` = `user`.`username` WHERE `session`.`expire` > ? AND `user`.`last_seen` > ?";
215        $db_results           = Dba::read($sql, array($time, $last_seen));
216        $data                 = Dba::fetch_row($db_results);
217        $results['connected'] = $data[0];
218
219        return $results;
220    }
221
222    /**
223     * has_info
224     * This function returns the information for this object
225     * @return array
226     */
227    private function has_info()
228    {
229        $user_id = (int)($this->id);
230
231        if (User::is_cached('user', $user_id)) {
232            return User::get_from_cache('user', $user_id);
233        }
234
235        $data = array();
236        // If the ID is -1 then
237        if ($user_id == '-1') {
238            $data['username'] = 'System';
239            $data['fullname'] = 'Ampache User';
240            $data['access']   = '25';
241
242            return $data;
243        }
244
245        $sql        = "SELECT * FROM `user` WHERE `id`='$user_id'";
246        $db_results = Dba::read($sql);
247
248        $data = Dba::fetch_assoc($db_results);
249
250        User::add_to_cache('user', $user_id, $data);
251
252        return $data;
253    } // has_info
254
255    /**
256     * load_playlist
257     * This is called once per page load it makes sure that this session
258     * has a tmp_playlist, creating it if it doesn't, then sets $this->playlist
259     * as a tmp_playlist object that can be fiddled with later on
260     */
261    public function load_playlist()
262    {
263        $session_id = session_id();
264
265        $this->playlist = Tmp_Playlist::get_from_session($session_id);
266    } // load_playlist
267
268    /**
269     * get_from_username
270     * This returns a built user from a username. This is a
271     * static function so it doesn't require an instance
272     * @param string $username
273     * @return User $user
274     */
275    public static function get_from_username($username)
276    {
277        $sql        = "SELECT `id` FROM `user` WHERE `username` = ? OR `fullname` = ?";
278        $db_results = Dba::read($sql, array($username, $username));
279        $results    = Dba::fetch_assoc($db_results);
280
281        return new User($results['id']);
282    } // get_from_username
283
284    /**
285     * get_catalogs
286     * This returns the catalogs as an array of ids that this user is allowed to access
287     * @return integer[]
288     */
289    public function get_catalogs()
290    {
291        if (parent::is_cached('user_catalog', $this->id)) {
292            return parent::get_from_cache('user_catalog', $this->id);
293        }
294
295        $sql        = "SELECT * FROM `user_catalog` WHERE `user` = ?";
296        $db_results = Dba::read($sql, array($this->id));
297
298        $catalogs = array();
299        while ($row = Dba::fetch_assoc($db_results)) {
300            $catalogs[] = (int)$row['catalog'];
301        }
302
303        parent::add_to_cache('user_catalog', $this->id, $catalogs);
304
305        return $catalogs;
306    } // get_catalogs
307
308    /**
309     * get_preferences
310     * This is a little more complicate now that we've got many types of preferences
311     * This function pulls all of them an arranges them into a spiffy little array
312     * You can specify a type to limit it to a single type of preference
313     * []['title'] = uppercase type name
314     * []['prefs'] = array(array('name', 'display', 'value'));
315     * []['admin'] = t/f value if this is an admin only section
316     * @param integer $type
317     * @param boolean $system
318     * @return array
319     */
320    public function get_preferences($type = 0, $system = false)
321    {
322        // Fill out the user id
323        $user_id = $system ? Dba::escape(-1) : Dba::escape($this->id);
324
325        $user_limit = "";
326        if (!$system) {
327            $user_limit = "AND preference.catagory != 'system'";
328        } else {
329            if ($type != '0') {
330                $user_limit = "AND preference.catagory = '" . Dba::escape($type) . "'";
331            }
332        }
333
334        $sql = "SELECT `preference`.`name`, `preference`.`description`, `preference`.`catagory`, `preference`.`subcatagory`, preference.level, user_preference.value FROM `preference` INNER JOIN `user_preference` ON `user_preference`.`preference` = `preference`.`id` WHERE `user_preference`.`user` = '$user_id' " . $user_limit . " ORDER BY `preference`.`catagory`, `preference`.`subcatagory`, `preference`.`description`";
335
336        $db_results = Dba::read($sql);
337        $results    = array();
338        $type_array = array();
339        /* Ok this is crappy, need to clean this up or improve the code FIXME */
340        while ($row = Dba::fetch_assoc($db_results)) {
341            $type  = $row['catagory'];
342            $admin = false;
343            if ($type == 'system') {
344                $admin = true;
345            }
346            $type_array[$type][$row['name']] = array(
347                'name' => $row['name'],
348                'level' => $row['level'],
349                'description' => $row['description'],
350                'value' => $row['value'],
351                'subcategory' => $row['subcatagory']
352            );
353            $results[$type] = array(
354                'title' => ucwords((string)$type),
355                'admin' => $admin,
356                'prefs' => $type_array[$type]
357            );
358        } // end while
359
360        return $results;
361    } // get_preferences
362
363    /**
364     * set_preferences
365     * sets the prefs for this specific user
366     */
367    public function set_preferences()
368    {
369        $user_id    = Dba::escape($this->id);
370        $sql        = "SELECT `preference`.`name`, `user_preference`.`value` FROM `preference`, `user_preference` WHERE `user_preference`.`user` = ? AND `user_preference`.`preference` = `preference`.`id` AND `preference`.`type` != 'system';";
371        $db_results = Dba::read($sql, array($user_id));
372
373        while ($row = Dba::fetch_assoc($db_results)) {
374            $key               = $row['name'];
375            $this->prefs[$key] = $row['value'];
376        }
377    } // set_preferences
378
379    /**
380     * get_favorites
381     * returns an array of your $type favorites
382     * @param string $type
383     * @return array
384     */
385    public function get_favorites($type)
386    {
387        $count   = AmpConfig::get('popular_threshold', 10);
388        $results = Stats::get_user($count, $type, $this->id, 1);
389
390        $items = array();
391
392        foreach ($results as $row) {
393            // If its a song
394            if ($type == 'song') {
395                $data        = new Song($row['object_id']);
396                $data->count = $row['count'];
397                $data->format();
398                $items[] = $data;
399            } elseif ($type == 'album') {
400                // If its an album
401                $data = new Album($row['object_id']);
402                $data->format();
403                $items[] = $data;
404            } elseif ($type == 'artist') {
405                // If its an artist
406                $data = new Artist($row['object_id']);
407                $data->format();
408                $data->f_name = $data->f_link;
409                $items[]      = $data;
410            } elseif (($type == 'genre' || $type == 'tag')) {
411                // If it's a genre
412                $data    = new Tag($row['object_id']);
413                $items[] = $data;
414            }
415        } // end foreach
416
417        return $items;
418    } // get_favorites
419
420    /**
421     * is_logged_in
422     * checks to see if $this user is logged in returns their current IP if they
423     * are logged in
424     */
425    public function is_logged_in()
426    {
427        $username = Dba::escape($this->username);
428
429        $sql        = "SELECT `id`, `ip` FROM `session` WHERE `username`='$username' AND `expire` > " . time();
430        $db_results = Dba::read($sql);
431
432        if ($row = Dba::fetch_assoc($db_results)) {
433            return $row['ip'] ? $row['ip'] : null;
434        }
435
436        return false;
437    } // is_logged_in
438
439    /**
440     * has_access
441     * this function checks to see if this user has access
442     * to the passed action (pass a level requirement)
443     * @param integer $needed_level
444     * @return boolean
445     */
446    public function has_access($needed_level)
447    {
448        if (AmpConfig::get('demo_mode')) {
449            return true;
450        }
451
452        if ($this->access >= $needed_level) {
453            return true;
454        }
455
456        return false;
457    } // has_access
458
459    /**
460     * is_registered
461     * Check if the user is registered
462     * @return boolean
463     */
464    public static function is_registered()
465    {
466        if (!Core::get_global('user')->id) {
467            return false;
468        }
469
470        if (!AmpConfig::get('use_auth') && Core::get_global('user')->access < 5) {
471            return false;
472        }
473
474        return true;
475    }
476
477    /**
478     * set_user_data
479     * This updates some background data for user specific function
480     * @param string $key
481     * @param string|integer $value
482     */
483    public static function set_user_data($user_id, $key, $value)
484    {
485        Dba::write("REPLACE INTO `user_data` SET `user`= ?, `key`= ?, `value`= ?;", array($user_id, $key, $value));
486    } // set_user_data
487
488    /**
489     * get_user_data
490     * This updates some background data for user specific function
491     * @param string $user_id
492     * @param string $key
493     * @return array
494     */
495    public static function get_user_data($user_id, $key = null)
496    {
497        $sql    = "SELECT `key`, `value` FROM `user_data` WHERE `user` = ?";
498        $params = array($user_id);
499        if ($key) {
500            $sql .= " AND `key` = ?";
501            $params[] = $key;
502        }
503
504        $db_results = Dba::read($sql, $params);
505        $results    = array();
506        while ($row = Dba::fetch_assoc($db_results)) {
507            $results[$row['key']] = $row['value'];
508        }
509
510        return $results;
511    } // get_user_data
512
513    /**
514     * update
515     * This function is an all encompassing update function that
516     * calls the mini ones does all the error checking and all that
517     * good stuff
518     * @param array $data
519     * @return boolean|int
520     */
521    public function update(array $data)
522    {
523        if (empty($data['username'])) {
524            AmpError::add('username', T_('Username is required'));
525        }
526
527        if ($data['password1'] != $data['password2'] && !empty($data['password1'])) {
528            AmpError::add('password', T_("Passwords do not match"));
529        }
530
531        if (AmpError::occurred()) {
532            return false;
533        }
534
535        if (!isset($data['fullname_public'])) {
536            $data['fullname_public'] = false;
537        }
538
539        foreach ($data as $name => $value) {
540            if ($name == 'password1') {
541                $name = 'password';
542            } else {
543                $value = scrub_in($value);
544            }
545
546            switch ($name) {
547                case 'password':
548                case 'access':
549                case 'email':
550                case 'username':
551                case 'fullname':
552                case 'fullname_public':
553                case 'website':
554                case 'state':
555                case 'city':
556                    if ($this->$name != $value) {
557                        $function = 'update_' . $name;
558                        $this->$function($value);
559                    }
560                    break;
561                case 'clear_stats':
562                    Stats::clear($this->id);
563                    break;
564                default:
565                    break;
566            }
567        }
568
569        return $this->id;
570    }
571
572    /**
573     * update_username
574     * updates their username
575     * @param $new_username
576     */
577    public function update_username($new_username)
578    {
579        $sql            = "UPDATE `user` SET `username` = ? WHERE `id` = ?";
580        $this->username = $new_username;
581
582        debug_event(self::class, 'Updating username', 4);
583
584        Dba::write($sql, array($new_username, $this->id));
585    } // update_username
586
587    /**
588     * update_validation
589     * This is used by the registration mumbojumbo
590     * Use this function to update the validation key
591     * NOTE: crap this doesn't have update_item the humanity of it all
592     * @param $new_validation
593     * @return PDOStatement|boolean
594     */
595    public function update_validation($new_validation)
596    {
597        $sql              = "UPDATE `user` SET `validation` = ?, `disabled`='1' WHERE `id` = ?";
598        $db_results       = Dba::write($sql, array($new_validation, $this->id));
599        $this->validation = $new_validation;
600
601        return $db_results;
602    } // update_validation
603
604    /**
605     * update_fullname
606     * updates their fullname
607     * @param $new_fullname
608     */
609    public function update_fullname($new_fullname)
610    {
611        $sql = "UPDATE `user` SET `fullname` = ? WHERE `id` = ?";
612
613        debug_event(self::class, 'Updating fullname', 4);
614
615        Dba::write($sql, array($new_fullname, $this->id));
616    } // update_fullname
617
618    /**
619     * update_fullname_public
620     * updates their fullname public
621     * @param $new_fullname_public
622     */
623    public function update_fullname_public($new_fullname_public)
624    {
625        $sql = "UPDATE `user` SET `fullname_public` = ? WHERE `id` = ?";
626
627        debug_event(self::class, 'Updating fullname public', 4);
628
629        Dba::write($sql, array($new_fullname_public ? '1' : '0', $this->id));
630    } // update_fullname_public
631
632    /**
633     * update_email
634     * updates their email address
635     * @param string $new_email
636     */
637    public function update_email($new_email)
638    {
639        $sql = "UPDATE `user` SET `email` = ? WHERE `id` = ?";
640
641        debug_event(self::class, 'Updating email', 4);
642
643        Dba::write($sql, array($new_email, $this->id));
644    } // update_email
645
646    /**
647     * update_website
648     * updates their website address
649     * @param $new_website
650     */
651    public function update_website($new_website)
652    {
653        $new_website = rtrim((string)$new_website, "/");
654        $sql         = "UPDATE `user` SET `website` = ? WHERE `id` = ?";
655
656        debug_event(self::class, 'Updating website', 4);
657
658        Dba::write($sql, array($new_website, $this->id));
659    } // update_website
660
661    /**
662     * update_state
663     * updates their state
664     * @param $new_state
665     */
666    public function update_state($new_state)
667    {
668        $sql = "UPDATE `user` SET `state` = ? WHERE `id` = ?";
669
670        debug_event(self::class, 'Updating state', 4);
671
672        Dba::write($sql, array($new_state, $this->id));
673    } // update_state
674
675    /**
676     * update_city
677     * updates their city
678     * @param $new_city
679     */
680    public function update_city($new_city)
681    {
682        $sql = "UPDATE `user` SET `city` = ? WHERE `id` = ?";
683
684        debug_event(self::class, 'Updating city', 4);
685
686        Dba::write($sql, array($new_city, $this->id));
687    } // update_city
688
689    /**
690     * update_counts for individual users
691     */
692    public static function update_counts()
693    {
694        $catalog_disable = AmpConfig::get('catalog_disable');
695        $catalog_filter  = AmpConfig::get('catalog_filter');
696        $sql             = "SELECT `id` FROM `user`";
697        $db_results      = Dba::read($sql);
698        $user_list       = array();
699        while ($results  = Dba::fetch_assoc($db_results)) {
700            $user_list[] = (int)$results['id'];
701        }
702        if (!$catalog_filter) {
703            // no filter means no need for filtering or counting per user
704            $count_array   = array('song', 'video', 'podcast_episode', 'artist', 'album', 'search', 'playlist', 'live_stream', 'podcast', 'user', 'catalog', 'label', 'tag', 'share', 'license', 'album_group', 'items', 'time', 'size');
705            $server_counts = Catalog::get_server_counts(0);
706            foreach ($user_list as $user_id) {
707                debug_event(self::class, 'Update counts for ' . $user_id, 5);
708                foreach ($server_counts as $table => $count) {
709                    if (in_array($table, $count_array)) {
710                        self::set_count($user_id, $table, $count);
711                    }
712                }
713            }
714
715            return;
716        }
717
718        $count_array = array('song', 'video', 'podcast_episode', 'artist', 'album', 'search', 'playlist', 'live_stream', 'podcast', 'user', 'catalog', 'label', 'tag', 'share', 'license');
719        foreach ($user_list as $user_id) {
720            debug_event(self::class, 'Update counts for ' . $user_id, 5);
721            // get counts per user (filtered catalogs aren't counted)
722            foreach ($count_array as $table) {
723                $sql        = (in_array($table, array('search', 'user', 'license')))
724                    ? "SELECT COUNT(`id`) FROM `$table`"
725                    : "SELECT COUNT(`id`) FROM `$table` WHERE" . Catalog::get_user_filter($table, $user_id);
726                $db_results = Dba::read($sql);
727                $data       = Dba::fetch_row($db_results);
728
729                self::set_count($user_id, $table, (int)$data[0]);
730            }
731            // tables with media items to count, song-related tables and the rest
732            $media_tables = array('song', 'video', 'podcast_episode');
733            $items        = 0;
734            $time         = 0;
735            $size         = 0;
736            foreach ($media_tables as $table) {
737                $enabled_sql = ($catalog_disable && $table !== 'podcast_episode')
738                    ? " WHERE `$table`.`enabled`='1' AND"
739                    : ' WHERE';
740                $sql         = "SELECT COUNT(`id`), IFNULL(SUM(`time`), 0), IFNULL(SUM(`size`), 0) FROM `$table`" . $enabled_sql . Catalog::get_user_filter($table, $user_id);
741                $db_results  = Dba::read($sql);
742                $data        = Dba::fetch_row($db_results);
743                // save the object and add to the current size
744                $items += (int)$data[0];
745                $time += (int)$data[1];
746                $size += (int)$data[2];
747                self::set_count($user_id, $table, (int)$data[0]);
748            }
749            self::set_count($user_id, 'items', $items);
750            self::set_count($user_id, 'time', $time);
751            self::set_count($user_id, 'size', $size);
752            // grouped album counts
753            $sql        = "SELECT COUNT(DISTINCT(`album`.`id`)) AS `count` FROM `album` WHERE `id` in (SELECT MIN(`id`) from `album` GROUP BY `album`.`prefix`, `album`.`name`, `album`.`album_artist`, `album`.`release_type`, `album`.`release_status`, `album`.`mbid`, `album`.`year`, `album`.`original_year`) AND" . Catalog::get_user_filter('album', $user_id);
754            $db_results = Dba::read($sql);
755            $data       = Dba::fetch_row($db_results);
756            self::set_count($user_id, 'album_group', (int)$data[0]);
757        }
758    } // update_counts
759
760    /**
761     * set_count
762     *
763     * write the total_counts to update_info
764     * @param int $user_id
765     * @param string $key
766     * @param int $value
767     */
768    public static function set_count(int $user_id, string $key, int $value)
769    {
770        Dba::write("REPLACE INTO `user_data` SET `user` = ?, `key`= ?, `value`=?;", array($user_id, $key, $value));
771    } // set_count
772
773    /**
774     * disable
775     * This disables the current user
776     */
777    public function disable()
778    {
779        // Make sure we aren't disabling the last admin
780        $sql        = "SELECT `id` FROM `user` WHERE `disabled` = '0' AND `id` != '" . $this->id . "' AND `access`='100'";
781        $db_results = Dba::read($sql);
782
783        if (!Dba::num_rows($db_results)) {
784            return false;
785        }
786
787        $sql = "UPDATE `user` SET `disabled`='1' WHERE id='" . $this->id . "'";
788        Dba::write($sql);
789
790        // Delete any sessions they may have
791        $sql = "DELETE FROM `session` WHERE `username`='" . Dba::escape($this->username) . "'";
792        Dba::write($sql);
793
794        return true;
795    } // disable
796
797    /**
798     * update_access
799     * updates their access level
800     * @param $new_access
801     * @return boolean
802     */
803    public function update_access($new_access)
804    {
805        /* Prevent Only User accounts */
806        if ($new_access < '100') {
807            $sql        = "SELECT `id` FROM `user` WHERE `access`='100' AND `id` != '$this->id'";
808            $db_results = Dba::read($sql);
809            if (!Dba::num_rows($db_results)) {
810                return false;
811            }
812        }
813
814        $new_access = Dba::escape($new_access);
815        $sql        = "UPDATE `user` SET `access`='$new_access' WHERE `id`='$this->id'";
816
817        debug_event(self::class, 'Updating access level for ' . $this->id, 4);
818
819        Dba::write($sql);
820
821        return true;
822    } // update_access
823
824    /**
825     * save_mediaplay
826     * @param User $user
827     * @param Song $media
828     */
829    public static function save_mediaplay($user, $media)
830    {
831        foreach (Plugin::get_plugins('save_mediaplay') as $plugin_name) {
832            try {
833                $plugin = new Plugin($plugin_name);
834                if ($plugin->load($user)) {
835                    debug_event(self::class, 'save_mediaplay... ' . $plugin->_plugin->name, 5);
836                    $plugin->_plugin->save_mediaplay($media);
837                }
838            } catch (Exception $error) {
839                debug_event(self::class, 'save_mediaplay plugin error: ' . $error->getMessage(), 1);
840            }
841        }
842    }
843
844    /**
845     * insert_ip_history
846     * This inserts a row into the IP History recording this user at this
847     * address at this time in this place, doing this thing.. you get the point
848     */
849    public function insert_ip_history()
850    {
851        $sip = (filter_has_var(INPUT_SERVER, 'HTTP_X_FORWARDED_FOR'))
852            ? filter_var(Core::get_server('HTTP_X_FORWARDED_FOR'), FILTER_VALIDATE_IP)
853            : filter_var(Core::get_server('REMOTE_ADDR'), FILTER_VALIDATE_IP);
854        debug_event(self::class, 'Login from IP address: ' . (string) $sip, 3);
855
856        // Remove port information if any
857        if (!empty($sip)) {
858            // Use parse_url to support easily ipv6
859            if (filter_var($sip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) === true) {
860                $sipar = parse_url("http://" . $sip);
861            } else {
862                $sipar = parse_url("http://[" . $sip . "]");
863            }
864            $sip = $sipar['host'];
865        }
866
867        $uip     = (!empty($sip)) ? Dba::escape(inet_pton(trim((string)$sip, "[]"))) : '';
868        $date    = time();
869        $user_id = (int)$this->id;
870        $agent   = Dba::escape(Core::get_server('HTTP_USER_AGENT'));
871
872        $sql = "INSERT INTO `ip_history` (`ip`, `user`, `date`, `agent`) VALUES ('$uip', '$user_id', '$date', '$agent')";
873        Dba::write($sql);
874
875        /* Clean up old records... sometimes  */
876        if (rand(1, 100) > 60) {
877            $date = time() - (86400 * AmpConfig::get('user_ip_cardinality'));
878            $sql  = "DELETE FROM `ip_history` WHERE `date` < $date";
879            Dba::write($sql);
880        }
881
882        return true;
883    } // insert_ip_history
884
885    /**
886     * create
887     * inserts a new user into Ampache
888     * @param string $username
889     * @param string $fullname
890     * @param string $email
891     * @param string $website
892     * @param string $password
893     * @param integer $access
894     * @param string $state
895     * @param string $city
896     * @param boolean $disabled
897     * @param boolean $encrypted
898     * @return integer
899     */
900    public static function create(
901        $username,
902        $fullname,
903        $email,
904        $website,
905        $password,
906        $access,
907        $state = '',
908        $city = '',
909        $disabled = false,
910        $encrypted = false
911    ) {
912        $website = rtrim((string)$website, "/");
913        if (!$encrypted) {
914            $password = hash('sha256', $password);
915        }
916        $disabled = $disabled ? 1 : 0;
917
918        /* Now Insert this new user */
919        $sql    = "INSERT INTO `user` (`username`, `disabled`, `fullname`, `email`, `password`, `access`, `create_date`";
920        $params = array($username, $disabled, $fullname, $email, $password, $access, time());
921
922        if (!empty($website)) {
923            $sql .= ", `website`";
924            $params[] = $website;
925        }
926        if (!empty($state)) {
927            $sql .= ", `state`";
928            $params[] = $state;
929        }
930        if (!empty($city)) {
931            $sql .= ", `city`";
932            $params[] = $city;
933        }
934
935        $sql .= ") VALUES(?, ?, ?, ?, ?, ?, ?";
936
937        if (!empty($website)) {
938            $sql .= ", ?";
939        }
940        if (!empty($state)) {
941            $sql .= ", ?";
942        }
943        if (!empty($city)) {
944            $sql .= ", ?";
945        }
946
947        $sql .= ")";
948        $db_results = Dba::write($sql, $params);
949
950        if (!$db_results) {
951            return null;
952        }
953
954        // Get the insert_id
955        $insert_id = (int)Dba::insert_id();
956
957        // Populates any missing preferences, in this case all of them
958        self::fix_preferences($insert_id);
959
960        return (int)$insert_id;
961    } // create
962
963    /**
964     * update_password
965     * updates a users password
966     * @param string $new_password
967     * @param string $hashed_password
968     */
969    public function update_password($new_password, $hashed_password = null)
970    {
971        debug_event(self::class, 'Updating password', 1);
972        if (!$hashed_password) {
973            $hashed_password = hash('sha256', $new_password);
974        }
975
976        $escaped_password = Dba::escape($hashed_password);
977        $sql              = "UPDATE `user` SET `password` = ? WHERE `id` = ?";
978        $db_results       = Dba::write($sql, array($escaped_password, $this->id));
979
980        // Clear this (temp fix)
981        if ($db_results) {
982            unset($_SESSION['userdata']['password']);
983        }
984    } // update_password
985
986    /**
987     * format
988     * This function sets up the extra variables we need when we are displaying a
989     * user for an admin, these should not be normally called when creating a
990     * user object
991     * @param boolean $details
992     */
993    public function format($details = true)
994    {
995        if (!$this->id) {
996            return;
997        }
998        /* If they have a last seen date */
999        if (!$this->last_seen) {
1000            $this->f_last_seen = T_('Never');
1001        } else {
1002            $this->f_last_seen = get_datetime((int)$this->last_seen);
1003        }
1004
1005        /* If they have a create date */
1006        if (!$this->create_date) {
1007            $this->f_create_date = T_('Unknown');
1008        } else {
1009            $this->f_create_date = get_datetime((int)$this->create_date);
1010        }
1011
1012        $this->f_name = ($this->fullname_public)
1013            ? filter_var($this->fullname, FILTER_SANITIZE_STRING, FILTER_FLAG_NO_ENCODE_QUOTES)
1014            : filter_var($this->username, FILTER_SANITIZE_STRING, FILTER_FLAG_NO_ENCODE_QUOTES);
1015
1016        // Base link
1017        $this->link   = AmpConfig::get('web_path') . '/stats.php?action=show_user&user_id=' . $this->id;
1018        $this->f_link = '<a href="' . $this->link . '">' . scrub_out($this->f_name) . '</a>';
1019
1020        if ($details) {
1021            $user_data = self::get_user_data($this->id);
1022            if (!isset($user_data['play_size'])) {
1023                // Calculate their total Bandwidth Usage
1024                $sql        = "SELECT SUM(`song`.`size`) as `play_size` FROM `object_count` LEFT JOIN `song` ON `song`.`id`=`object_count`.`object_id` WHERE `object_count`.`user` = ? AND `object_count`.`object_type` IN ('song', 'video', 'podcast_episode') GROUP BY `user`;";
1025                $db_results = Dba::read($sql, array($this->id));
1026                $result     = Dba::fetch_assoc($db_results);
1027                // set the value for next time
1028                self::set_user_data($this->id, 'play_size', (int)$result['play_size']);
1029                $user_data['play_size'] = $result['play_size'];
1030            }
1031
1032            $this->f_usage = Ui::format_bytes((int)$user_data['play_size']);
1033
1034            // Get Users Last ip
1035            if (count($data = $this->getIpHistoryRepository()->getHistory($this->getId()))) {
1036                $user_ip          = inet_ntop($data['0']['ip']);
1037                $this->ip_history = (!empty($user_ip) && filter_var($user_ip, FILTER_VALIDATE_IP)) ? $user_ip : T_('Invalid');
1038            } else {
1039                $this->ip_history = T_('Not Enough Data');
1040            }
1041        }
1042
1043        $avatar = $this->get_avatar();
1044        if (!empty($avatar['url'])) {
1045            $this->f_avatar = '<img src="' . $avatar['url'] . '" title="' . $avatar['title'] . '"' . ' width="256px" height="auto" />';
1046        }
1047        if (!empty($avatar['url_mini'])) {
1048            $this->f_avatar_mini = '<img src="' . $avatar['url_mini'] . '" title="' . $avatar['title'] . '" style="width: 32px; height: 32px;" />';
1049        }
1050        if (!empty($avatar['url_medium'])) {
1051            $this->f_avatar_medium = '<img src="' . $avatar['url_medium'] . '" title="' . $avatar['title'] . '" style="width: 64px; height: 64px;" />';
1052        }
1053    } // format_user
1054
1055    /**
1056     * access_name_to_level
1057     * This takes the access name for the user and returns the level
1058     * @param string $name
1059     * @return integer
1060     */
1061    public static function access_name_to_level($name)
1062    {
1063        switch ($name) {
1064            case 'admin':
1065                return AccessLevelEnum::LEVEL_ADMIN;
1066            case 'user':
1067                return AccessLevelEnum::LEVEL_USER;
1068            case 'manager':
1069                return AccessLevelEnum::LEVEL_MANAGER;
1070            // FIXME why is content manager not here?
1071            //case 'manager':
1072                //return AccessLevelEnum::LEVEL_CONTENT_MANAGER;
1073            case 'guest':
1074                return AccessLevelEnum::LEVEL_GUEST;
1075            default:
1076                return AccessLevelEnum::LEVEL_DEFAULT;
1077        }
1078    } // access_name_to_level
1079
1080    /**
1081     * access_level_to_name
1082     * This takes the access level for the user and returns the translated name for that level
1083     * @param string $level
1084     * @return string
1085     */
1086    public static function access_level_to_name($level)
1087    {
1088        switch ($level) {
1089            case '100':
1090                return T_('Admin');
1091            case '75':
1092                return T_('Catalog Manager');
1093            case '50':
1094                return T_('Content Manager');
1095            case '25':
1096                return T_('User');
1097            case '5':
1098                return T_('Guest');
1099            default:
1100                return T_('Unknown');
1101        }
1102    } // access_level_to_name
1103
1104    /**
1105     * fix_preferences
1106     * This is the new fix_preferences function, it does the following
1107     * Remove Duplicates from user, add in missing
1108     * If -1 is passed it also removes duplicates from the `preferences`
1109     * table.
1110     * @param integer $user_id
1111     */
1112    public static function fix_preferences($user_id)
1113    {
1114        $user_id = Dba::escape($user_id);
1115
1116        // Delete that system pref that's not a user pref...
1117        if ($user_id > 0) {
1118            // TODO, remove before next release. ('custom_login_logo' needs to be here a while at least so 5.0.0+1)
1119            $sql = "DELETE FROM `user_preference` WHERE `preference` IN (SELECT `id` from `preference` where `name` IN ('custom_login_background', 'custom_login_logo')) AND `user` = $user_id";
1120            Dba::write($sql);
1121        }
1122
1123        /* Get All Preferences for the current user */
1124        $sql        = "SELECT * FROM `user_preference` WHERE `user`='$user_id'";
1125        $db_results = Dba::read($sql);
1126
1127        $results      = array();
1128        $zero_results = array();
1129
1130        while ($row = Dba::fetch_assoc($db_results)) {
1131            $pref_id = $row['preference'];
1132            /* Check for duplicates */
1133            if (isset($results[$pref_id])) {
1134                $row['value'] = Dba::escape($row['value']);
1135                $sql          = "DELETE FROM `user_preference` WHERE `user`='$user_id' AND `preference`='" . $row['preference'] . "' AND `value`='" . Dba::escape($row['value']) . "'";
1136                Dba::write($sql);
1137            } else {
1138                // if its set
1139                $results[$pref_id] = 1;
1140            }
1141        } // end while
1142
1143        /* If we aren't the -1 user before we continue grab the -1 users values */
1144        if ($user_id != '-1') {
1145            $sql        = "SELECT `user_preference`.`preference`, `user_preference`.`value` FROM `user_preference`, `preference` WHERE `user_preference`.`preference` = `preference`.`id` AND `user_preference`.`user`='-1' AND `preference`.`catagory` !='system' AND `preference`.`name` NOT IN ('custom_login_background', 'custom_login_logo') ";
1146            $db_results = Dba::read($sql);
1147            /* While through our base stuff */
1148            while ($row = Dba::fetch_assoc($db_results)) {
1149                $key                = $row['preference'];
1150                $zero_results[$key] = $row['value'];
1151            }
1152        } // if not user -1
1153
1154        // get me _EVERYTHING_
1155        $sql = "SELECT * FROM `preference`";
1156
1157        // If not system, exclude system... *gasp*
1158        if ($user_id != '-1') {
1159            $sql .= " WHERE catagory !='system'";
1160            $sql .= " AND `preference`.`name` NOT IN ('custom_login_background', 'custom_login_logo')";
1161        }
1162        $db_results = Dba::read($sql);
1163
1164        while ($row = Dba::fetch_assoc($db_results)) {
1165            $key = $row['id'];
1166
1167            /* Check if this preference is set */
1168            if (!isset($results[$key])) {
1169                if (isset($zero_results[$key])) {
1170                    $row['value'] = $zero_results[$key];
1171                }
1172                $value = Dba::escape($row['value']);
1173                $sql   = "INSERT INTO user_preference (`user`, `preference`, `value`) VALUES ('$user_id', '$key', '$value')";
1174                Dba::write($sql);
1175            }
1176        } // while preferences
1177    } // fix_preferences
1178
1179    /**
1180     * delete
1181     * deletes this user and everything associated with it. This will affect
1182     * ratings and total stats
1183     * @return boolean
1184     */
1185    public function delete()
1186    {
1187        // Before we do anything make sure that they aren't the last admin
1188        if ($this->has_access(100)) {
1189            $sql        = "SELECT `id` FROM `user` WHERE `access`='100' AND id != ?";
1190            $db_results = Dba::read($sql, array($this->id));
1191            if (!Dba::num_rows($db_results)) {
1192                return false;
1193            }
1194        } // if this is an admin check for others
1195
1196        // simple deletion queries.
1197        $user_tables = array(
1198            'playlist',
1199            'object_count',
1200            'ip_history',
1201            'access_list',
1202            'rating',
1203            'tag_map',
1204            'user_preference',
1205            'user_vote'
1206        );
1207        foreach ($user_tables as $table_id) {
1208            $sql = "DELETE FROM `" . $table_id . "` WHERE `user` = ?";
1209            Dba::write($sql, array($this->id));
1210        }
1211        // Clean up the playlist data table
1212        $sql = "DELETE FROM `playlist_data` USING `playlist_data` LEFT JOIN `playlist` ON `playlist`.`id`=`playlist_data`.`playlist` WHERE `playlist`.`id` IS NULL";
1213        Dba::write($sql);
1214
1215        // Clean out the tags
1216        $sql = "DELETE FROM `tag` WHERE `tag`.`id` NOT IN (SELECT `tag_id` FROM `tag_map`)";
1217        Dba::write($sql);
1218
1219        // Delete their following/followers
1220        $sql = "DELETE FROM `user_follower` WHERE `user` = ? OR `follow_user` = ?";
1221        Dba::write($sql, array($this->id, $this->id));
1222
1223        // Delete the user itself
1224        $sql = "DELETE FROM `user` WHERE `id` = ?";
1225        Dba::write($sql, array($this->id));
1226
1227        $sql = "DELETE FROM `session` WHERE `username` = ?";
1228        Dba::write($sql, array($this->username));
1229
1230        return true;
1231    } // delete
1232
1233    /**
1234     * is_online
1235     * delay how long since last_seen in seconds default of 20 min
1236     * calculates difference between now and last_seen
1237     * if less than delay, we consider them still online
1238     * @param integer $delay
1239     * @return boolean
1240     */
1241    public function is_online($delay = 1200)
1242    {
1243        return time() - $this->last_seen <= $delay;
1244    } // is_online
1245
1246    /**
1247     * get_recently_played
1248     * This gets the recently played items for this user respecting
1249     * the limit passed. ger recent by default or oldest if $newest is false.
1250     * @param string $type
1251     * @param integer $count
1252     * @param integer $offset
1253     * @param boolean $newest
1254     * @return array
1255     */
1256    public function get_recently_played($type, $count, $offset = 0, $newest = true)
1257    {
1258        $ordersql = ($newest === true) ? 'DESC' : 'ASC';
1259        $limit    = ($offset < 1) ? $count : $offset . "," . $count;
1260
1261        $sql        = "SELECT `object_id`, MAX(`date`) AS `date` FROM `object_count` WHERE `object_type` = ? AND `user` = ? GROUP BY `object_id` ORDER BY `date` " . $ordersql . " LIMIT " . $limit . " ";
1262        $db_results = Dba::read($sql, array($type, $this->id));
1263
1264        $results = array();
1265        while ($row = Dba::fetch_assoc($db_results)) {
1266            $results[] = $row['object_id'];
1267        }
1268
1269        return $results;
1270    } // get_recently_played
1271
1272    /**
1273     * Get item fullname.
1274     * @return string
1275     */
1276    public function get_fullname()
1277    {
1278        return $this->f_name;
1279    }
1280
1281    /**
1282     * Get item name based on whether they allow public fullname access.
1283     * @param int $user_id
1284     * @return string
1285     */
1286    public static function get_username($user_id)
1287    {
1288        $users    = static::getUserRepository()->getValidArray(true);
1289        $username = (isset($users[$user_id]))
1290            ? $users[$user_id]
1291            : T_('System');
1292
1293        return $username;
1294    }
1295
1296    /**
1297     * get_avatar
1298     * Get the user avatar
1299     * @param boolean $local
1300     * @param array $session
1301     * @return array
1302     */
1303    public function get_avatar($local = false, $session = array())
1304    {
1305        $avatar = array();
1306        $auth   = '';
1307        if ($session['t'] && $session['s']) {
1308            $auth = '&t=' . $session['t'] . '&s=' . $session['s'];
1309        } elseif ($session['auth']) {
1310            $auth = '&auth=' . $session['auth'];
1311        }
1312
1313        $avatar['title'] = T_('User avatar');
1314        $upavatar        = new Art($this->id, 'user');
1315        if ($upavatar->has_db_info()) {
1316            $avatar['url']        = ($local ? AmpConfig::get('local_web_path') : AmpConfig::get('web_path')) . '/image.php?object_type=user&object_id=' . $this->id . $auth;
1317            $avatar['url_mini']   = $avatar['url'];
1318            $avatar['url_medium'] = $avatar['url'];
1319            $avatar['url'] .= '&thumb=4';
1320            $avatar['url_mini'] .= '&thumb=5';
1321            $avatar['url_medium'] .= '&thumb=3';
1322        } else {
1323            foreach (Plugin::get_plugins('get_avatar_url') as $plugin_name) {
1324                $plugin = new Plugin($plugin_name);
1325                if ($plugin->load(Core::get_global('user'))) {
1326                    $avatar['url'] = $plugin->_plugin->get_avatar_url($this);
1327                    if (!empty($avatar['url'])) {
1328                        $avatar['url_mini']   = $plugin->_plugin->get_avatar_url($this, 32);
1329                        $avatar['url_medium'] = $plugin->_plugin->get_avatar_url($this, 64);
1330                        $avatar['title'] .= ' (' . $plugin->_plugin->name . ')';
1331                        break;
1332                    }
1333                }
1334            }
1335        }
1336
1337        if ($avatar['url'] === null) {
1338            $avatar['url']        = ($local ? AmpConfig::get('local_web_path') : AmpConfig::get('web_path')) . '/images/blankuser.png';
1339            $avatar['url_mini']   = $avatar['url'];
1340            $avatar['url_medium'] = $avatar['url'];
1341        }
1342
1343        return $avatar;
1344    } // get_avatar
1345
1346    /**
1347     * @param string $data
1348     * @param string $mime
1349     * @return boolean
1350     */
1351    public function update_avatar($data, $mime = '')
1352    {
1353        debug_event(self::class, 'Updating avatar for ' . $this->id, 4);
1354
1355        $art = new Art($this->id, 'user');
1356
1357        return $art->insert($data, $mime);
1358    }
1359
1360    /**
1361     *
1362     * @return boolean
1363     */
1364    public function upload_avatar()
1365    {
1366        $upload = array();
1367        if (!empty($_FILES['avatar']['tmp_name']) && $_FILES['avatar']['size'] <= AmpConfig::get('max_upload_size')) {
1368            $path_info      = pathinfo($_FILES['avatar']['name']);
1369            $upload['file'] = $_FILES['avatar']['tmp_name'];
1370            $upload['mime'] = 'image/' . $path_info['extension'];
1371            $image_data     = Art::get_from_source($upload, 'user');
1372
1373            if ($image_data !== '') {
1374                return $this->update_avatar($image_data, $upload['mime']);
1375            }
1376        }
1377
1378        return true; // only worry about failed uploads
1379    }
1380
1381    public function delete_avatar()
1382    {
1383        $art = new Art($this->id, 'user');
1384        $art->reset();
1385    }
1386
1387    /**
1388     * rebuild_all_preferences
1389     * This rebuilds the user preferences for all installed users, called by the plugin functions
1390     */
1391    public static function rebuild_all_preferences()
1392    {
1393        // Clean out any preferences garbage left over
1394        $sql = "DELETE `user_preference`.* FROM `user_preference` LEFT JOIN `user` ON `user_preference`.`user` = `user`.`id` WHERE `user_preference`.`user` != -1 AND `user`.`id` IS NULL";
1395        Dba::write($sql);
1396
1397        // Get only users who has less preferences than excepted
1398        // otherwise it would have significant performance issue with large user database
1399        $sql        = "SELECT `user` FROM `user_preference` GROUP BY `user` HAVING COUNT(*) < (SELECT COUNT(`id`) FROM `preference` WHERE `catagory` != 'system')";
1400        $db_results = Dba::read($sql);
1401        while ($row = Dba::fetch_assoc($db_results)) {
1402            self::fix_preferences($row['user']);
1403        }
1404
1405        return true;
1406    } // rebuild_all_preferences
1407
1408    /**
1409     * stream_control
1410     * Check all stream control plugins
1411     * @param array $media_ids
1412     * @param User|null $user
1413     * @return boolean
1414     */
1415    public static function stream_control($media_ids, User $user = null)
1416    {
1417        if ($user === null) {
1418            $user = Core::get_global('user');
1419        }
1420
1421        foreach (Plugin::get_plugins('stream_control') as $plugin_name) {
1422            $plugin = new Plugin($plugin_name);
1423            if ($plugin->load($user)) {
1424                if (!$plugin->_plugin->stream_control($media_ids)) {
1425                    return false;
1426                }
1427            }
1428        }
1429
1430        return true;
1431    }
1432
1433    /**
1434     * @deprecated inject dependency
1435     */
1436    private function getIpHistoryRepository(): IpHistoryRepositoryInterface
1437    {
1438        global $dic;
1439
1440        return $dic->get(IpHistoryRepositoryInterface::class);
1441    }
1442
1443    /**
1444     * @deprecated inject dependency
1445     */
1446    private static function getUserRepository(): UserRepositoryInterface
1447    {
1448        global $dic;
1449
1450        return $dic->get(UserRepositoryInterface::class);
1451    }
1452}
1453