Mercurial > hg > rc2
diff program/lib/Roundcube/rcube_user.php @ 0:4681f974d28b
vanilla 1.3.3 distro, I hope
author | Charlie Root |
---|---|
date | Thu, 04 Jan 2018 15:52:31 -0500 |
parents | |
children |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/program/lib/Roundcube/rcube_user.php Thu Jan 04 15:52:31 2018 -0500 @@ -0,0 +1,844 @@ +<?php + +/** + +-----------------------------------------------------------------------+ + | This file is part of the Roundcube Webmail client | + | Copyright (C) 2005-2012, The Roundcube Dev Team | + | | + | Licensed under the GNU General Public License version 3 or | + | any later version with exceptions for skins & plugins. | + | See the README file for a full license statement. | + | | + | PURPOSE: | + | This class represents a system user linked and provides access | + | to the related database records. | + +-----------------------------------------------------------------------+ + | Author: Thomas Bruederli <roundcube@gmail.com> | + | Author: Aleksander Machniak <alec@alec.pl> | + +-----------------------------------------------------------------------+ +*/ + +/** + * Class representing a system user + * + * @package Framework + * @subpackage Core + */ +class rcube_user +{ + public $ID; + public $data; + public $language; + public $prefs; + + /** + * Holds database connection. + * + * @var rcube_db + */ + private $db; + + /** + * Framework object. + * + * @var rcube + */ + private $rc; + + /** + * Internal identities cache + * + * @var array + */ + private $identities = array(); + + /** + * Internal emails cache + * + * @var array + */ + private $emails; + + + const SEARCH_ADDRESSBOOK = 1; + const SEARCH_MAIL = 2; + + /** + * Object constructor + * + * @param int $id User id + * @param array $sql_arr SQL result set + */ + function __construct($id = null, $sql_arr = null) + { + $this->rc = rcube::get_instance(); + $this->db = $this->rc->get_dbh(); + + if ($id && !$sql_arr) { + $sql_result = $this->db->query( + "SELECT * FROM " . $this->db->table_name('users', true) + . " WHERE `user_id` = ?", $id); + $sql_arr = $this->db->fetch_assoc($sql_result); + } + + if (!empty($sql_arr)) { + $this->ID = $sql_arr['user_id']; + $this->data = $sql_arr; + $this->language = $sql_arr['language']; + } + } + + /** + * Build a user name string (as e-mail address) + * + * @param string $part Username part (empty or 'local' or 'domain', 'mail') + * @return string Full user name or its part + */ + function get_username($part = null) + { + if ($this->data['username']) { + // return real name + if (!$part) { + return $this->data['username']; + } + + list($local, $domain) = explode('@', $this->data['username']); + + // at least we should always have the local part + if ($part == 'local') { + return $local; + } + // if no domain was provided... + if (empty($domain)) { + $domain = $this->rc->config->mail_domain($this->data['mail_host']); + } + + if ($part == 'domain') { + return $domain; + } + + if (!empty($domain)) + return $local . '@' . $domain; + else + return $local; + } + + return false; + } + + /** + * Get the preferences saved for this user + * + * @return array Hash array with prefs + */ + function get_prefs() + { + if (isset($this->prefs)) { + return $this->prefs; + } + + $this->prefs = array(); + + if (!empty($this->language)) + $this->prefs['language'] = $this->language; + + if ($this->ID) { + // Preferences from session (write-master is unavailable) + if (!empty($_SESSION['preferences'])) { + // Check last write attempt time, try to write again (every 5 minutes) + if ($_SESSION['preferences_time'] < time() - 5 * 60) { + $saved_prefs = unserialize($_SESSION['preferences']); + $this->rc->session->remove('preferences'); + $this->rc->session->remove('preferences_time'); + $this->save_prefs($saved_prefs); + } + else { + $this->data['preferences'] = $_SESSION['preferences']; + } + } + + if ($this->data['preferences']) { + $this->prefs += (array)unserialize($this->data['preferences']); + } + } + + return $this->prefs; + } + + /** + * Write the given user prefs to the user's record + * + * @param array $a_user_prefs User prefs to save + * @param bool $no_session Simplified language/preferences handling + * + * @return boolean True on success, False on failure + */ + function save_prefs($a_user_prefs, $no_session = false) + { + if (!$this->ID) + return false; + + $plugin = $this->rc->plugins->exec_hook('preferences_update', array( + 'userid' => $this->ID, 'prefs' => $a_user_prefs, 'old' => (array)$this->get_prefs())); + + if (!empty($plugin['abort'])) { + return; + } + + $a_user_prefs = $plugin['prefs']; + $old_prefs = $plugin['old']; + $config = $this->rc->config; + $defaults = $config->all(); + + // merge (partial) prefs array with existing settings + $this->prefs = $save_prefs = $a_user_prefs + $old_prefs; + unset($save_prefs['language']); + + // don't save prefs with default values if they haven't been changed yet + // Warning: we use result of rcube_config::all() here instead of just get() (#5782) + foreach ($a_user_prefs as $key => $value) { + if ($value === null || (!isset($old_prefs[$key]) && $value === $defaults[$key])) { + unset($save_prefs[$key]); + } + } + + $save_prefs = serialize($save_prefs); + if (!$no_session) { + $this->language = $_SESSION['language']; + } + + $this->db->query( + "UPDATE ".$this->db->table_name('users', true). + " SET `preferences` = ?, `language` = ?". + " WHERE `user_id` = ?", + $save_prefs, + $this->language, + $this->ID); + + // Update success + if ($this->db->affected_rows() !== false) { + $this->data['preferences'] = $save_prefs; + + if (!$no_session) { + $config->set_user_prefs($this->prefs); + + if (isset($_SESSION['preferences'])) { + $this->rc->session->remove('preferences'); + $this->rc->session->remove('preferences_time'); + } + } + + return true; + } + // Update error, but we are using replication (we have read-only DB connection) + // and we are storing session not in the SQL database + // we can store preferences in session and try to write later (see get_prefs()) + else if (!$no_session && $this->db->is_replicated() + && $config->get('session_storage', 'db') != 'db' + ) { + $_SESSION['preferences'] = $save_prefs; + $_SESSION['preferences_time'] = time(); + $config->set_user_prefs($this->prefs); + $this->data['preferences'] = $save_prefs; + } + + return false; + } + + /** + * Generate a unique hash to identify this user whith + */ + function get_hash() + { + $prefs = $this->get_prefs(); + + // generate a random hash and store it in user prefs + if (empty($prefs['client_hash'])) { + $prefs['client_hash'] = rcube_utils::random_bytes(16); + $this->save_prefs(array('client_hash' => $prefs['client_hash'])); + } + + return $prefs['client_hash']; + } + + /** + * Return a list of all user emails (from identities) + * + * @param bool Return only default identity + * + * @return array List of emails (identity_id, name, email) + */ + function list_emails($default = false) + { + if ($this->emails === null) { + $this->emails = array(); + + $sql_result = $this->db->query( + "SELECT `identity_id`, `name`, `email`" + ." FROM " . $this->db->table_name('identities', true) + ." WHERE `user_id` = ? AND `del` <> 1" + ." ORDER BY `standard` DESC, `name` ASC, `email` ASC, `identity_id` ASC", + $this->ID); + + while ($sql_arr = $this->db->fetch_assoc($sql_result)) { + $this->emails[] = $sql_arr; + } + } + + return $default ? $this->emails[0] : $this->emails; + } + + /** + * Get default identity of this user + * + * @param int $id Identity ID. If empty, the default identity is returned + * @return array Hash array with all cols of the identity record + */ + function get_identity($id = null) + { + $id = (int)$id; + // cache identities for better performance + if (!array_key_exists($id, $this->identities)) { + $result = $this->list_identities($id ? "AND `identity_id` = $id" : ''); + $this->identities[$id] = $result[0]; + } + + return $this->identities[$id]; + } + + /** + * Return a list of all identities linked with this user + * + * @param string $sql_add Optional WHERE clauses + * @param bool $formatted Format identity email and name + * + * @return array List of identities + */ + function list_identities($sql_add = '', $formatted = false) + { + $result = array(); + + $sql_result = $this->db->query( + "SELECT * FROM ".$this->db->table_name('identities', true). + " WHERE `del` <> 1 AND `user_id` = ?". + ($sql_add ? " ".$sql_add : ""). + " ORDER BY `standard` DESC, `name` ASC, `email` ASC, `identity_id` ASC", + $this->ID); + + while ($sql_arr = $this->db->fetch_assoc($sql_result)) { + if ($formatted) { + $ascii_email = format_email($sql_arr['email']); + $utf8_email = format_email(rcube_utils::idn_to_utf8($ascii_email)); + + $sql_arr['email_ascii'] = $ascii_email; + $sql_arr['email'] = $utf8_email; + $sql_arr['ident'] = format_email_recipient($ascii_email, $sql_arr['name']); + } + + $result[] = $sql_arr; + } + + return $result; + } + + /** + * Update a specific identity record + * + * @param int $iid Identity ID + * @param array $data Hash array with col->value pairs to save + * @return boolean True if saved successfully, false if nothing changed + */ + function update_identity($iid, $data) + { + if (!$this->ID) + return false; + + $query_cols = $query_params = array(); + + foreach ((array)$data as $col => $value) { + $query_cols[] = $this->db->quote_identifier($col) . ' = ?'; + $query_params[] = $value; + } + $query_params[] = $iid; + $query_params[] = $this->ID; + + $sql = "UPDATE ".$this->db->table_name('identities', true). + " SET `changed` = ".$this->db->now().", ".join(', ', $query_cols). + " WHERE `identity_id` = ?". + " AND `user_id` = ?". + " AND `del` <> 1"; + + call_user_func_array(array($this->db, 'query'), + array_merge(array($sql), $query_params)); + + // clear the cache + $this->identities = array(); + $this->emails = null; + + return $this->db->affected_rows(); + } + + /** + * Create a new identity record linked with this user + * + * @param array $data Hash array with col->value pairs to save + * @return int The inserted identity ID or false on error + */ + function insert_identity($data) + { + if (!$this->ID) + return false; + + unset($data['user_id']); + + $insert_cols = $insert_values = array(); + foreach ((array)$data as $col => $value) { + $insert_cols[] = $this->db->quote_identifier($col); + $insert_values[] = $value; + } + $insert_cols[] = $this->db->quote_identifier('user_id'); + $insert_values[] = $this->ID; + + $sql = "INSERT INTO ".$this->db->table_name('identities', true). + " (`changed`, ".join(', ', $insert_cols).")". + " VALUES (".$this->db->now().", ".join(', ', array_pad(array(), count($insert_values), '?')).")"; + + call_user_func_array(array($this->db, 'query'), + array_merge(array($sql), $insert_values)); + + // clear the cache + $this->identities = array(); + $this->emails = null; + + return $this->db->insert_id('identities'); + } + + /** + * Mark the given identity as deleted + * + * @param int $iid Identity ID + * @return boolean True if deleted successfully, false if nothing changed + */ + function delete_identity($iid) + { + if (!$this->ID) + return false; + + $sql_result = $this->db->query( + "SELECT count(*) AS ident_count FROM ".$this->db->table_name('identities', true). + " WHERE `user_id` = ? AND `del` <> 1", + $this->ID); + + $sql_arr = $this->db->fetch_assoc($sql_result); + + // we'll not delete last identity + if ($sql_arr['ident_count'] <= 1) + return -1; + + $this->db->query( + "UPDATE ".$this->db->table_name('identities', true). + " SET `del` = 1, `changed` = ".$this->db->now(). + " WHERE `user_id` = ?". + " AND `identity_id` = ?", + $this->ID, + $iid); + + // clear the cache + $this->identities = array(); + $this->emails = null; + + return $this->db->affected_rows(); + } + + /** + * Make this identity the default one for this user + * + * @param int $iid The identity ID + */ + function set_default($iid) + { + if ($this->ID && $iid) { + $this->db->query( + "UPDATE ".$this->db->table_name('identities', true). + " SET `standard` = '0'". + " WHERE `user_id` = ? AND `identity_id` <> ?", + $this->ID, + $iid); + + unset($this->identities[0]); + } + } + + /** + * Update user's last_login timestamp + */ + function touch() + { + if ($this->ID) { + $this->db->query( + "UPDATE ".$this->db->table_name('users', true). + " SET `last_login` = ".$this->db->now(). + " WHERE `user_id` = ?", + $this->ID); + } + } + + /** + * Update user's failed_login timestamp and counter + */ + function failed_login() + { + if ($this->ID && ($rate = (int) $this->rc->config->get('login_rate_limit', 3))) { + if (empty($this->data['failed_login'])) { + $failed_login = new DateTime('now'); + $counter = 1; + } + else { + $failed_login = new DateTime($this->data['failed_login']); + $threshold = new DateTime('- 60 seconds'); + + if ($failed_login < $threshold) { + $failed_login = new DateTime('now'); + $counter = 1; + } + } + + $this->db->query( + "UPDATE " . $this->db->table_name('users', true) + . " SET `failed_login` = ?" + . ", `failed_login_counter` = " . ($counter ?: "`failed_login_counter` + 1") + . " WHERE `user_id` = ?", + $failed_login, $this->ID); + } + } + + /** + * Checks if the account is locked, e.g. as a result of brute-force prevention + */ + function is_locked() + { + if (empty($this->data['failed_login'])) { + return false; + } + + if ($rate = (int) $this->rc->config->get('login_rate_limit', 3)) { + $last_failed = new DateTime($this->data['failed_login']); + $threshold = new DateTime('- 60 seconds'); + + if ($last_failed > $threshold && $this->data['failed_login_counter'] >= $rate) { + return true; + } + } + + return false; + } + + /** + * Clear the saved object state + */ + function reset() + { + $this->ID = null; + $this->data = null; + } + + /** + * Find a user record matching the given name and host + * + * @param string $user IMAP user name + * @param string $host IMAP host name + * @return rcube_user New user instance + */ + static function query($user, $host) + { + $dbh = rcube::get_instance()->get_dbh(); + $config = rcube::get_instance()->config; + + // query for matching user name + $sql_result = $dbh->query("SELECT * FROM " . $dbh->table_name('users', true) + ." WHERE `mail_host` = ? AND `username` = ?", $host, $user); + + $sql_arr = $dbh->fetch_assoc($sql_result); + + // username not found, try aliases from identities + if (empty($sql_arr) && $config->get('user_aliases') && strpos($user, '@')) { + $sql_result = $dbh->limitquery("SELECT u.*" + ." FROM " . $dbh->table_name('users', true) . " u" + ." JOIN " . $dbh->table_name('identities', true) . " i ON (i.`user_id` = u.`user_id`)" + ." WHERE `email` = ? AND `del` <> 1", 0, 1, $user); + + $sql_arr = $dbh->fetch_assoc($sql_result); + } + + // user already registered -> overwrite username + if ($sql_arr) { + return new rcube_user($sql_arr['user_id'], $sql_arr); + } + + return false; + } + + /** + * Create a new user record and return a rcube_user instance + * + * @param string $user IMAP user name + * @param string $host IMAP host + * @return rcube_user New user instance + */ + static function create($user, $host) + { + $user_name = ''; + $user_email = ''; + $rcube = rcube::get_instance(); + $dbh = $rcube->get_dbh(); + + // try to resolve user in virtuser table and file + if ($email_list = self::user2email($user, false, true)) { + $user_email = is_array($email_list[0]) ? $email_list[0]['email'] : $email_list[0]; + } + + $data = $rcube->plugins->exec_hook('user_create', array( + 'host' => $host, + 'user' => $user, + 'user_name' => $user_name, + 'user_email' => $user_email, + 'email_list' => $email_list, + 'language' => $_SESSION['language'], + )); + + // plugin aborted this operation + if ($data['abort']) { + return false; + } + + $dbh->query( + "INSERT INTO ".$dbh->table_name('users', true). + " (`created`, `last_login`, `username`, `mail_host`, `language`)". + " VALUES (".$dbh->now().", ".$dbh->now().", ?, ?, ?)", + $data['user'], + $data['host'], + $data['language']); + + if ($user_id = $dbh->insert_id('users')) { + // create rcube_user instance to make plugin hooks work + $user_instance = new rcube_user($user_id, array( + 'user_id' => $user_id, + 'username' => $data['user'], + 'mail_host' => $data['host'], + 'language' => $data['language'], + )); + $rcube->user = $user_instance; + $mail_domain = $rcube->config->mail_domain($data['host']); + $user_name = $data['user_name']; + $user_email = $data['user_email']; + $email_list = $data['email_list']; + + if (empty($email_list)) { + if (empty($user_email)) { + $user_email = strpos($data['user'], '@') ? $user : sprintf('%s@%s', $data['user'], $mail_domain); + } + $email_list[] = $user_email; + } + // identities_level check + else if (count($email_list) > 1 && $rcube->config->get('identities_level', 0) > 1) { + $email_list = array($email_list[0]); + } + + if (empty($user_name)) { + $user_name = $data['user']; + } + + // create new identities records + $standard = 1; + foreach ($email_list as $row) { + $record = array(); + + if (is_array($row)) { + if (empty($row['email'])) { + continue; + } + $record = $row; + } + else { + $record['email'] = $row; + } + + if (empty($record['name'])) { + $record['name'] = $user_name != $record['email'] ? $user_name : ''; + } + + $record['user_id'] = $user_id; + $record['standard'] = $standard; + + $plugin = $rcube->plugins->exec_hook('identity_create', + array('login' => true, 'record' => $record)); + + if (!$plugin['abort'] && $plugin['record']['email']) { + $rcube->user->insert_identity($plugin['record']); + } + $standard = 0; + } + } + else { + rcube::raise_error(array( + 'code' => 500, + 'type' => 'php', + 'line' => __LINE__, + 'file' => __FILE__, + 'message' => "Failed to create new user"), true, false); + } + + return $user_id ? $user_instance : false; + } + + /** + * Resolve username using a virtuser plugins + * + * @param string $email E-mail address to resolve + * @return string Resolved IMAP username + */ + static function email2user($email) + { + $rcube = rcube::get_instance(); + $plugin = $rcube->plugins->exec_hook('email2user', + array('email' => $email, 'user' => NULL)); + + return $plugin['user']; + } + + /** + * Resolve e-mail address from virtuser plugins + * + * @param string $user User name + * @param boolean $first If true returns first found entry + * @param boolean $extended If true returns email as array (email and name for identity) + * @return mixed Resolved e-mail address string or array of strings + */ + static function user2email($user, $first=true, $extended=false) + { + $rcube = rcube::get_instance(); + $plugin = $rcube->plugins->exec_hook('user2email', + array('email' => NULL, 'user' => $user, + 'first' => $first, 'extended' => $extended)); + + return empty($plugin['email']) ? NULL : $plugin['email']; + } + + /** + * Return a list of saved searches linked with this user + * + * @param int $type Search type + * + * @return array List of saved searches indexed by search ID + */ + function list_searches($type) + { + $plugin = $this->rc->plugins->exec_hook('saved_search_list', array('type' => $type)); + + if ($plugin['abort']) { + return (array) $plugin['result']; + } + + $result = array(); + + $sql_result = $this->db->query( + "SELECT `search_id` AS id, `name`" + ." FROM ".$this->db->table_name('searches', true) + ." WHERE `user_id` = ? AND `type` = ?" + ." ORDER BY `name`", + (int) $this->ID, (int) $type); + + while ($sql_arr = $this->db->fetch_assoc($sql_result)) { + $sql_arr['data'] = unserialize($sql_arr['data']); + $result[$sql_arr['id']] = $sql_arr; + } + + return $result; + } + + /** + * Return saved search data. + * + * @param int $id Row identifier + * + * @return array Data + */ + function get_search($id) + { + $plugin = $this->rc->plugins->exec_hook('saved_search_get', array('id' => $id)); + + if ($plugin['abort']) { + return $plugin['result']; + } + + $sql_result = $this->db->query( + "SELECT `name`, `data`, `type`" + . " FROM ".$this->db->table_name('searches', true) + . " WHERE `user_id` = ?" + ." AND `search_id` = ?", + (int) $this->ID, (int) $id); + + while ($sql_arr = $this->db->fetch_assoc($sql_result)) { + return array( + 'id' => $id, + 'name' => $sql_arr['name'], + 'type' => $sql_arr['type'], + 'data' => unserialize($sql_arr['data']), + ); + } + + return null; + } + + /** + * Deletes given saved search record + * + * @param int $sid Search ID + * + * @return boolean True if deleted successfully, false if nothing changed + */ + function delete_search($sid) + { + if (!$this->ID) + return false; + + $this->db->query( + "DELETE FROM ".$this->db->table_name('searches', true) + ." WHERE `user_id` = ?" + ." AND `search_id` = ?", + (int) $this->ID, $sid); + + return $this->db->affected_rows(); + } + + /** + * Create a new saved search record linked with this user + * + * @param array $data Hash array with col->value pairs to save + * + * @return int The inserted search ID or false on error + */ + function insert_search($data) + { + if (!$this->ID) + return false; + + $insert_cols[] = 'user_id'; + $insert_values[] = (int) $this->ID; + $insert_cols[] = $this->db->quote_identifier('type'); + $insert_values[] = (int) $data['type']; + $insert_cols[] = $this->db->quote_identifier('name'); + $insert_values[] = $data['name']; + $insert_cols[] = $this->db->quote_identifier('data'); + $insert_values[] = serialize($data['data']); + + $sql = "INSERT INTO ".$this->db->table_name('searches', true) + ." (".join(', ', $insert_cols).")" + ." VALUES (".join(', ', array_pad(array(), count($insert_values), '?')).")"; + + call_user_func_array(array($this->db, 'query'), + array_merge(array($sql), $insert_values)); + + return $this->db->insert_id('searches'); + } +}