source: trunk/lib/Auth_SQL.inc.php

Last change on this file was 791, checked in by anonymous, 13 months ago

Log authentication failures because of 'multiple users with username'

File size: 54.0 KB
Line 
1<?php
2/**
3 * The Strangecode Codebase - a general application development framework for PHP
4 * For details visit the project site: <http://trac.strangecode.com/codebase/>
5 * Copyright 2001-2012 Strangecode, LLC
6 *
7 * This file is part of The Strangecode Codebase.
8 *
9 * The Strangecode Codebase is free software: you can redistribute it and/or
10 * modify it under the terms of the GNU General Public License as published by the
11 * Free Software Foundation, either version 3 of the License, or (at your option)
12 * any later version.
13 *
14 * The Strangecode Codebase is distributed in the hope that it will be useful, but
15 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
16 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
17 * details.
18 *
19 * You should have received a copy of the GNU General Public License along with
20 * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
21 */
22
23/*
24* The Auth_SQL class provides a SQL implementation for authentication.
25*
26* @author  Quinn Comendant <quinn@strangecode.com>
27* @version 2.1
28*/
29
30require_once dirname(__FILE__) . '/Email.inc.php';
31
32class Auth_SQL
33{
34    // Available hash types for class Auth_SQL.
35    const ENCRYPT_PLAINTEXT = 1;
36    const ENCRYPT_CRYPT = 2;
37    const ENCRYPT_SHA1 = 3;
38    const ENCRYPT_SHA1_HARDENED = 4;
39    const ENCRYPT_MD5 = 5;
40    const ENCRYPT_MD5_HARDENED = 6;
41    const ENCRYPT_PASSWORD_BCRYPT = 7;
42    const ENCRYPT_PASSWORD_DEFAULT = 8;
43
44    // Namespace of this auth object.
45    protected $_ns;
46
47    // Static var for test.
48    protected $_authentication_tested;
49
50    // Parameters to be configured by setParam.
51    protected $_params = array();
52    protected $_default_params = array(
53
54        // Automatically create table and verify columns. Better set to false after site launch.
55        // This value is overwritten by the $app->getParam('db_create_tables') setting if it is available.
56        'create_table' => true,
57
58        // The database table containing users to authenticate.
59        'db_table' => 'user_tbl',
60
61        // The name of the primary key for the db_table.
62        'db_primary_key' => 'user_id',
63
64        // The name of the username key for the db_table.
65        'db_username_column' => 'username',
66
67        // If using the db_login_table feature, specify the db_login_table. The primary key must match the primary key for the db_table.
68        'db_login_table' => 'user_login_tbl',
69
70        // The type of hash to use for passwords stored in the db_table. Use one of the Auth_SQL::ENCRYPT_* types specified above.
71        // Hardened password hashes rely on the same key/salt being used to compare hashes.
72        // Be aware that when using one of the hardened types the App signing_key or $more_salt below cannot change!
73        'hash_type' => self::ENCRYPT_MD5,
74        'encryption_type' => null, // Backwards misnomer compatibility.
75
76        // Automatically update stored user hashes when the user next authenticates if the hash type changes (requires user_tbl with populated userpass_hashtype column).
77        'hash_type_autoupdate' => true,
78
79        // The URL to the login script.
80        'login_url' => '/',
81
82        // The maximum amount of time a user is allowed to be logged in. They will be forced to login again if they expire.
83        // In seconds. 21600 seconds = 6 hours.
84        'login_timeout' => 21600,
85
86        // The maximum amount of time a user is allowed to be idle before their session expires. They will be forced to login again if they expire.
87        // In seconds. 3600 seconds = 1 hour.
88        'idle_timeout' => 3600,
89
90        // The period of time to compare login abuse attempts. If a threshold of logins is reached in this amount of time the account is blocked.
91        // Days and hours, like this: 'DD:HH'
92        'login_abuse_timeframe' => '04:00',
93
94        // The number of warnings a user will receive (and their password reset each time) before their account is completely blocked.
95        'login_abuse_warnings' => 3,
96
97        // The maximum number of IP addresses a user can login with over the timeout period before their account is blocked.
98        'login_abuse_max_ips' => 5,
99
100        // The IP address subnet size threshold. Uses a CIDR notation network mask (see CIDR cheat-sheet at bottom).
101        // Any integer between 0 and 32 is permitted. Setting this to '24' permits any address in a
102        // class C network (255.255.255.0) to be considered the same. Setting to '32' compares each IP absolutely.
103        // Setting to '0' ignores all IPs, thus disabling login_abuse checking.
104        'login_abuse_ip_bitmask' => 32,
105
106        // Specify usernames to exclude from the account abuse detection system. This is specified as a hardcoded array provided at
107        // class instantiation time, or can be saved in the db_table under the login_abuse_exempt field.
108        'login_abuse_exempt_usernames' => array(),
109
110        // Specify usernames to exclude from remote_ip matching. Users behind proxy servers should be appended to this array so their shifting remote IP will not log them out.
111        'match_remote_ip_exempt_usernames' => array(),
112
113        // Match the user's current remote IP against the one they logged in with.
114        'match_remote_ip' => true,
115
116        // An array of IP blocks that are bypass the remote_ip comparison check. Useful for dynamic IPs or those behind proxy servers.
117        'trusted_networks' => array(),
118
119        // Allow user accounts to be blocked? Requires the user table to have the columns 'blocked' and 'blocked_reason'
120        'blocking' => false,
121
122        // Use a db_login_table to detect excessive logins. This requires blocking to be enabled.
123        'abuse_detection' => false,
124
125        // Allow users to save login form passwords in their browser? Setting to 'true' may pose a potential security risk.
126        'login_form_allow_autocomplete' => false,
127    );
128
129    /**
130     * Constructs a new authentication object.
131     *
132     * @access public
133     * @param optional array $params  A hash containing parameters.
134     */
135    public function __construct($namespace='')
136    {
137        $app =& App::getInstance();
138
139        $this->_ns = $namespace;
140
141        // Initialize default parameters.
142        $this->setParam($this->_default_params);
143
144        // Get create tables config from global context.
145        if (!is_null($app->getParam('db_create_tables'))) {
146            $this->setParam(array('create_table' => $app->getParam('db_create_tables')));
147        }
148
149        if (!isset($_SESSION['_auth_sql'][$this->_ns])) {
150            $app->logMsg(sprintf('No _auth_sql session found; initializing', null), LOG_DEBUG, __FILE__, __LINE__);
151            $this->clear();
152        }
153    }
154
155    /**
156     * Setup the database tables for this class.
157     *
158     * @access  public
159     * @author  Quinn Comendant <quinn@strangecode.com>
160     * @since   26 Aug 2005 17:09:36
161     */
162    public function initDB($recreate_db=false)
163    {
164        $app =& App::getInstance();
165        $db =& DB::getInstance();
166
167        static $_db_tested = false;
168
169        if ($recreate_db || !$_db_tested && $this->getParam('create_table')) {
170
171            // User table.
172            if ($recreate_db) {
173                $db->query("DROP TABLE IF EXISTS " . $this->getParam('db_table'));
174                $app->logMsg(sprintf('Dropping and recreating table %s.', $this->getParam('db_table')), LOG_INFO, __FILE__, __LINE__);
175            }
176
177            // The minimal columns for a table compatible with the Auth_SQL class.
178            $db->query(sprintf(
179                "CREATE TABLE IF NOT EXISTS %1\$s (
180                    %2\$s MEDIUMINT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
181                    %3\$s varchar(255) NOT NULL default '',
182                    userpass VARCHAR(255) NOT NULL DEFAULT '',
183                    userpass_hashtype TINYINT UNSIGNED NOT NULL DEFAULT '0',
184                    first_name VARCHAR(50) NOT NULL DEFAULT '',
185                    last_name VARCHAR(50) NOT NULL DEFAULT '',
186                    email VARCHAR(255) NOT NULL DEFAULT '',
187                    login_abuse_exempt ENUM('true') DEFAULT NULL,
188                    blocked ENUM('true') DEFAULT NULL,
189                    blocked_reason VARCHAR(255) NOT NULL DEFAULT '',
190                    abuse_warning_level TINYINT NOT NULL DEFAULT '0',
191                    seconds_online INT NOT NULL DEFAULT '0',
192                    last_login_datetime DATETIME NOT NULL DEFAULT '%4\$s 00:00:00',
193                    last_access_datetime DATETIME NOT NULL DEFAULT '%4\$s 00:00:00',
194                    last_login_ip VARCHAR(45) NOT NULL DEFAULT '0.0.0.0',
195                    added_by_user_id SMALLINT DEFAULT NULL,
196                    modified_by_user_id SMALLINT DEFAULT NULL,
197                    added_datetime DATETIME NOT NULL DEFAULT '%4\$s 00:00:00',
198                    modified_datetime DATETIME NOT NULL DEFAULT '%4\$s 00:00:00',
199                    KEY %5\$s (%5\$s),
200                    KEY userpass (userpass),
201                    KEY email (email),
202                    KEY last_login_datetime (last_login_datetime),
203                    KEY last_access_datetime (last_access_datetime)
204                )",
205                $db->escapeString($this->getParam('db_table')),
206                $this->getParam('db_primary_key'),
207                $this->getParam('db_username_column'),
208                $db->getParam('zero_date'),
209                $this->getParam('db_username_column')
210            ));
211
212            if (!$db->columnExists($this->getParam('db_table'), array(
213                $this->getParam('db_primary_key'),
214                $this->getParam('db_username_column'),
215                'userpass',
216                'first_name',
217                'last_name',
218                'email',
219                'login_abuse_exempt',
220                'blocked',
221                'blocked_reason',
222                'abuse_warning_level',
223                'seconds_online',
224                'last_login_datetime',
225                'last_access_datetime',
226                'last_login_ip',
227                'added_by_user_id',
228                'modified_by_user_id',
229                'added_datetime',
230                'modified_datetime',
231            ), false, false)) {
232                $app->logMsg(sprintf('Database table %s has invalid columns. Please update this table manually.', $this->getParam('db_table')), LOG_ALERT, __FILE__, __LINE__);
233                trigger_error(sprintf('Database table %s has invalid columns. Please update this table manually.', $this->getParam('db_table')), E_USER_ERROR);
234            }
235
236            // Login table is used for abuse_detection features.
237            if ($this->getParam('abuse_detection')) {
238                if ($recreate_db) {
239                    $db->query("DROP TABLE IF EXISTS " . $this->getParam('db_login_table'));
240                    $app->logMsg(sprintf('Dropping and recreating table %s.', $this->getParam('db_login_table')), LOG_INFO, __FILE__, __LINE__);
241                }
242                $db->query(sprintf(
243                    "CREATE TABLE IF NOT EXISTS %1\$s (
244                        %2\$s MEDIUMINT UNSIGNED NOT NULL DEFAULT '0',
245                        login_datetime DATETIME NOT NULL DEFAULT '%3\$s 00:00:00',
246                        remote_ip_binary CHAR(32) NOT NULL DEFAULT '',
247                        KEY %4\$s (%4\$s),
248                        KEY login_datetime (login_datetime),
249                        KEY remote_ip_binary (remote_ip_binary)
250                    )",
251                    $this->getParam('db_login_table'),
252                    $this->getParam('db_primary_key'),
253                    $db->getParam('zero_date'),
254                    $this->getParam('db_primary_key')
255                ));
256
257                if (!$db->columnExists($this->getParam('db_login_table'), array(
258                    $this->getParam('db_primary_key'),
259                    'login_datetime',
260                    'remote_ip_binary',
261                ), false, false)) {
262                    $app->logMsg(sprintf('Database table %s has invalid columns. Please update this table manually.', $this->getParam('db_login_table')), LOG_ALERT, __FILE__, __LINE__);
263                    trigger_error(sprintf('Database table %s has invalid columns. Please update this table manually.', $this->getParam('db_login_table')), E_USER_ERROR);
264                }
265            }
266        }
267        $_db_tested = true;
268    }
269
270    /**
271     * Set the params of an auth object.
272     *
273     * @param  array $params   Array of parameter keys and value to set.
274     * @return bool true on success, false on failure
275     */
276    public function setParam($params)
277    {
278        $app =& App::getInstance();
279
280        if (isset($params['match_remote_ip_exempt_usernames'])) {
281            $params['match_remote_ip_exempt_usernames'] = array_map('strtolower', $params['match_remote_ip_exempt_usernames']);
282        }
283        if (isset($params['login_abuse_exempt_usernames'])) {
284            $params['login_abuse_exempt_usernames'] = array_map('strtolower', $params['login_abuse_exempt_usernames']);
285        }
286        if (isset($params['encryption_type'])) {
287            // Backwards misnomer compatibility.
288            $params['hash_type'] = $params['encryption_type'];
289        }
290        if (isset($params['hash_type']) && version_compare(PHP_VERSION, '5.5.0', '<') && in_array($params['hash_type'], array(self::ENCRYPT_PASSWORD_BCRYPT, self::ENCRYPT_PASSWORD_DEFAULT))) {
291            // These hash types require the password_* userland lib in PHP < 5.5.0
292            $pw_compat_lib = 'vendor/ircmaxell/password-compat/lib/password.php';
293            if (false !== stream_resolve_include_path($pw_compat_lib)) {
294                include_once $pw_compat_lib;
295            } else {
296                $app->logMsg(sprintf('Hash type %s requires password-compat lib in PHP < 5.5.0; falling back to ENCRYPT_SHA1_HARDENED', $params['hash_type']), LOG_ERR, __FILE__, __LINE__);
297                $params['hash_type'] = self::ENCRYPT_SHA1_HARDENED;
298            }
299        }
300        if (isset($params['hash_type']) && !in_array($params['hash_type'], array(self::ENCRYPT_PLAINTEXT, self::ENCRYPT_CRYPT, self::ENCRYPT_SHA1, self::ENCRYPT_SHA1_HARDENED, self::ENCRYPT_MD5, self::ENCRYPT_MD5_HARDENED, self::ENCRYPT_PASSWORD_BCRYPT, self::ENCRYPT_PASSWORD_DEFAULT))) {
301            $app->logMsg(sprintf('Invalid hash type %s; falling back to ENCRYPT_SHA1_HARDENED', $params['hash_type']), LOG_ERR, __FILE__, __LINE__);
302            $params['hash_type'] = self::ENCRYPT_SHA1_HARDENED;
303        }
304        if (isset($params) && is_array($params)) {
305            // Merge new parameters with old overriding only those passed.
306            $this->_params = array_merge($this->_params, $params);
307        }
308    }
309
310    /**
311     * Return the value of a parameter, if it exists.
312     *
313     * @access public
314     * @param string $param        Which parameter to return.
315     * @return mixed               Configured parameter value.
316     */
317    public function getParam($param)
318    {
319        $app =& App::getInstance();
320
321        if (array_key_exists($param, $this->_params)) {
322            return $this->_params[$param];
323        } else {
324            $app->logMsg(sprintf('Parameter is not set: %s', $param), LOG_DEBUG, __FILE__, __LINE__);
325            return null;
326        }
327    }
328
329    /**
330     * Clear any authentication tokens in the current session. A.K.A. logout.
331     *
332     * @access public
333     */
334    public function clear()
335    {
336        $app =& App::getInstance();
337        $db =& DB::getInstance();
338
339        if ($this->get('user_id', false)) {
340
341            $this->initDB();
342
343            // FIX ME: Should we check if the session is active?
344            $db->query(sprintf(
345                "UPDATE %s SET
346                    seconds_online = seconds_online + IFNULL(ABS(UNIX_TIMESTAMP() - UNIX_TIMESTAMP(last_access_datetime)), 0),
347                    last_login_datetime = '%s 00:00:00'
348                    WHERE %s = '%s'
349                ",
350                $this->_params['db_table'],
351                $db->getParam('zero_date'),
352                $this->_params['db_primary_key'],
353                $this->get('user_id')
354            ));
355        }
356        $_SESSION['_auth_sql'][$this->_ns] = array(
357            'authenticated'         => false,
358            'user_id'               => null,
359            'username'              => null,
360            'login_datetime'        => null,
361            'last_access_datetime'  => null,
362            'remote_ip'             => getRemoteAddr(),
363            'login_abuse_exempt'    => null,
364            'match_remote_ip_exempt'=> null,
365            'user_data'             => null,
366        );
367
368        $app->logMsg(sprintf('Cleared %s auth', $this->_ns), LOG_DEBUG, __FILE__, __LINE__);
369    }
370
371    /**
372     * Sets a variable into a registered auth session.
373     *
374     * @access public
375     * @param mixed $key      Which value to set.
376     * @param mixed $val      Value to set variable to.
377     */
378    public function set($key, $val)
379    {
380        if (!isset($_SESSION['_auth_sql'][$this->_ns]['user_data'])) {
381            $_SESSION['_auth_sql'][$this->_ns]['user_data'] = array();
382        }
383
384        if (isset($_SESSION['_auth_sql'][$this->_ns][$key])) {
385            $_SESSION['_auth_sql'][$this->_ns][$key] = $val;
386        } else {
387            $_SESSION['_auth_sql'][$this->_ns]['user_data'][$key] = $val;
388        }
389    }
390
391    /**
392     * Returns a specified value from a registered auth session.
393     *
394     * @access public
395     * @param mixed $key      Which value to return.
396     * @param mixed $default  Value to return if key not found in user_data.
397     * @return mixed          Value stored in session.
398     */
399    public function get($key, $default='')
400    {
401        if (isset($_SESSION['_auth_sql'][$this->_ns][$key])) {
402            return $_SESSION['_auth_sql'][$this->_ns][$key];
403        } else if (isset($_SESSION['_auth_sql'][$this->_ns]['user_data'][$key])) {
404            return $_SESSION['_auth_sql'][$this->_ns]['user_data'][$key];
405        } else {
406            return $default;
407        }
408    }
409
410    /**
411     * Retrieve and verify the given username and password against a matching user record in the database.
412     *
413     * @access private
414     * @param string $username      The username to check.
415     * @param string $password      The password to compare to username.
416     * @return mixed  False if credentials not found in DB, or returns DB row matching credentials.
417     */
418    public function authenticate($username, $password)
419    {
420        $app =& App::getInstance();
421        $db =& DB::getInstance();
422
423        $this->initDB();
424
425        // Get user data for specified username.
426        $qid = $db->query("
427            SELECT *, " . $this->_params['db_primary_key'] . " AS user_id
428            FROM " . $this->_params['db_table'] . "
429            WHERE " . $this->_params['db_username_column'] . " = '" . $db->escapeString($username) . "'
430        ");
431        if (mysql_num_rows($qid) === 0 || !$user_data = mysql_fetch_assoc($qid)) {
432            $app->logMsg(sprintf('Authentication failed; username %s not found', $username), LOG_NOTICE, __FILE__, __LINE__);
433            return false;
434        }
435        if (mysql_num_rows($qid) !== 1) {
436            $app->logMsg(sprintf('Authentication failed; multiple users with username "%s"', $username), LOG_WARNING, __FILE__, __LINE__);
437            return false;
438        }
439
440        // TODO: log all auth attempts to db_login_table, not just successful ones. Then, rate-limit login attempts.
441
442        // Check given password against hashed DB password.
443        $old_hash_type = isset($user_data['userpass_hashtype']) && !empty($user_data['userpass_hashtype']) ? $user_data['userpass_hashtype'] : $this->getParam('hash_type');
444        if ($this->verifyPassword($password, $user_data['userpass'], $old_hash_type)) {
445            $app->logMsg(sprintf('Authentication successful for %s (user_id=%s)', $username, $user_data['user_id']), LOG_INFO, __FILE__, __LINE__);
446            unset($user_data['userpass']); // Avoid revealing the encrypted password in the $user_data.
447            if ($this->getParam('hash_type_autoupdate') && $old_hash_type != $this->getParam('hash_type')) {
448                // Let's update user's password hash to new type (just run setPassword with this authenticated password
).
449                $this->setPassword($user_data['user_id'], $password);
450                $app->logMsg(sprintf('User %s password hash type updated from %s to %s', $username, $old_hash_type, $this->getParam('hash_type')), LOG_INFO, __FILE__, __LINE__);
451            }
452            return $user_data;
453        }
454
455        $app->logMsg(sprintf('Authentication failed for %s (user_id=%s)', $username, $user_data['user_id']), LOG_NOTICE, __FILE__, __LINE__);
456        return false;
457    }
458
459    /**
460     * Check username and password, and create new session if authenticated.
461     *
462     * @access private
463     * @param string $username     The username to check.
464     * @param string $password     The password to compare for username.
465     * @return boolean  Whether or not the credentials are valid.
466     */
467    public function login($username, $password)
468    {
469        $app =& App::getInstance();
470        $db =& DB::getInstance();
471
472        if ($user_data = $this->authenticate($username, $password)) {
473            // The credentials match. Now setup the session.
474            return $this->createSession($user_data);
475        }
476        // No login: failed authentication!
477        return false;
478    }
479
480    /**
481     * Create new login session for given user.
482     *
483     * @access private
484     * @param string $user_data User data that is normally returned from this->authenticate(). If provided manually:
485     *                          Required array values:
486     *                              'user_id' => '1'
487     *                              'username' => 'name'
488     *                          Optional array values:
489     *                              'match_remote_ip_exempt' => true
490     *                              'login_abuse_exempt' => true
491     *                              'abuse_warning_level' => true
492     *                              'blocked' => true
493     *                              'blocked_reason' => ''
494     *                              '
' => '
' (any other values that should be retrievable via this->get())
495     * @return boolean          Whether or not the session was created. It will return true unless abuse detection is enabled and triggered.
496     */
497    public function createSession($user_data)
498    {
499        $app =& App::getInstance();
500        $db =& DB::getInstance();
501
502        $this->initDB();
503
504        $this->clear();
505
506        // Convert 'priv' to 'user_type' nomenclature to support older implementations.
507        if (isset($user_data['priv'])) {
508            $user_data['user_type'] = $user_data['priv'];
509        }
510
511        // Register authenticated session.
512        $_SESSION['_auth_sql'][$this->_ns] = array(
513            'authenticated'         => true,
514            'user_id'               => $user_data['user_id'],
515            'username'              => $user_data['username'],
516            'login_datetime'        => date('Y-m-d H:i:s'),
517            'last_access_datetime'  => date('Y-m-d H:i:s'),
518            'remote_ip'             => getRemoteAddr(),
519            'login_abuse_exempt'    => isset($user_data['login_abuse_exempt']) ? !empty($user_data['login_abuse_exempt']) : in_array(strtolower($user_data['username']), $this->_params['login_abuse_exempt_usernames']),
520            'match_remote_ip_exempt'=> isset($user_data['match_remote_ip_exempt']) ? !empty($user_data['match_remote_ip_exempt']) : in_array(strtolower($user_data['username']), $this->_params['match_remote_ip_exempt_usernames']),
521            'user_data'             => $user_data
522        );
523
524        /**
525         * Check if the account is blocked, respond in context to reason. Cancel the login if blocked.
526         */
527        if ($this->getParam('blocking')) {
528            if (isset($user_data['blocked']) && !empty($user_data['blocked'])) {
529                switch ($this->get('blocked_reason')) {
530                case 'account abuse' :
531                    $app->raiseMsg(sprintf(_("This account has been blocked due to possible account abuse. Please contact an administrator to reactivate."), null), MSG_WARNING, __FILE__, __LINE__);
532                    break;
533                default :
534                    $app->raiseMsg(sprintf(_("This account is currently not active. %s"), $this->get('blocked_reason')), MSG_WARNING, __FILE__, __LINE__);
535                    break;
536                }
537
538                // No login: user is blocked!
539                $app->logMsg(sprintf('User_id %s (%s) login failed due to blocked account: %s', $this->get('user_id'), $this->get('username'), $this->get('blocked_reason')), LOG_NOTICE, __FILE__, __LINE__);
540                $this->clear();
541                return false;
542            }
543        }
544
545        /**
546         * Check the db_login_table for too many logins under this account.
547         * (1) Count the number of unique IP addresses that logged in under this user within the login_abuse_timeframe
548         * (2) If this number exceeds the login_abuse_max_ips, assume multiple people are logging in under the same account.
549        **/
550        // TODO: make this ipv6 compatible. At the moment, ipv6 addresses are converted into zero for remote_ip_binary.
551        // http://www.highonphp.com/5-tips-for-working-with-ipv6-in-php
552        // https://stackoverflow.com/questions/444966/working-with-ipv6-addresses-in-php
553        if ($this->getParam('abuse_detection') && !$this->get('login_abuse_exempt')) {
554            $qid = $db->query("
555                SELECT COUNT(DISTINCT LEFT(remote_ip_binary, " . $this->_params['login_abuse_ip_bitmask'] . "))
556                FROM " . $this->_params['db_login_table'] . "
557                WHERE " . $this->_params['db_primary_key'] . " = '" . $this->get('user_id') . "'
558                AND DATE_ADD(login_datetime, INTERVAL '" . $this->_params['login_abuse_timeframe'] . "' DAY_HOUR) > NOW()
559            ");
560            list($distinct_ips) = mysql_fetch_row($qid);
561            if ($distinct_ips > $this->_params['login_abuse_max_ips']) {
562                if ($this->get('abuse_warning_level') < $this->_params['login_abuse_warnings']) {
563                    // Warn the user with a password reset.
564                    $this->resetPassword(null, _("This is a security precaution. We have detected this account has been accessed from multiple computers simultaneously. It is against policy to share credentials with others. If further account abuse is detected this account will be blocked."));
565                    $app->raiseMsg(_("Your password has been reset as a security precaution. Please check your email for more information."), MSG_NOTICE, __FILE__, __LINE__);
566                    $app->logMsg(sprintf('Account abuse detected for user_id %s (%s) from IP %s', $this->get('user_id'), $this->get('username'), $this->get('remote_ip')), LOG_WARNING, __FILE__, __LINE__);
567                } else {
568                    // Block the account with the reason of account abuse.
569                    $this->blockAccount(null, 'account abuse');
570                    $app->raiseMsg(_("Your account has been blocked as a security precaution. Please contact us for more information."), MSG_NOTICE, __FILE__, __LINE__);
571                    $app->logMsg(sprintf('Account blocked for user_id %s (%s) from IP %s', $this->get('user_id'), $this->get('username'), $this->get('remote_ip')), LOG_ALERT, __FILE__, __LINE__);
572                }
573                // Increment user's warning level.
574                $db->query("UPDATE " . $this->_params['db_table'] . " SET abuse_warning_level = abuse_warning_level + 1 WHERE " . $this->_params['db_primary_key'] . " = '" . $this->get('user_id') . "'");
575                // Reset the login counter for this user.
576                $db->query("DELETE FROM " . $this->_params['db_login_table'] . " WHERE " . $this->_params['db_primary_key'] . " = '" . $this->get('user_id') . "'");
577                // No login: reset password because of account abuse!
578                $this->clear();
579                return false;
580            }
581
582            // Update the login counter table with this login access. Convert IP to binary.
583            // TODO: this query could benefit from INSERT DELAYED.
584            $db->query("
585                INSERT INTO " . $this->_params['db_login_table'] . " (
586                    " . $this->_params['db_primary_key'] . ",
587                    login_datetime,
588                    remote_ip_binary
589                ) VALUES (
590                    '" . $this->get('user_id') . "',
591                    '" . $this->get('login_datetime') . "',
592                    '" . sprintf('%032b', ip2long($this->get('remote_ip'))) . "'
593                )
594            ");
595        }
596
597        // Update user table with this login.
598        $db->query("
599            UPDATE " . $this->_params['db_table'] . " SET
600                last_login_datetime = '" . $this->get('login_datetime') . "',
601                last_access_datetime = '" . $this->get('login_datetime') . "',
602                last_login_ip = '" . $this->get('remote_ip') . "'
603            WHERE " . $this->_params['db_primary_key'] . " = '" . $this->get('user_id') . "'
604        ");
605
606        // Session created! We're logged-in!
607        $app->logMsg(sprintf('“%s” auth session created for user_id %s (%s): %s=%s', $this->_ns, $this->get('user_id'), $this->get('username'), session_name(), session_id()), LOG_DEBUG, __FILE__, __LINE__);
608        return true;
609    }
610
611    /**
612     * Test if user has a currently logged-in session.
613     *  - authentication flag set to true
614     *  - username not empty
615     *  - total logged-in time is not greater than login_timeout
616     *  - idle time is not greater than idle_timeout
617     *  - remote address is the same as the login remote address
618     *
619     * TODO: implement persistent sessions as per https://paragonie.com/blog/2015/04/secure-authentication-php-with-long-term-persistence
620     *
621     * @access public
622     */
623    public function isLoggedIn($user_id=null)
624    {
625        $app =& App::getInstance();
626        $db =& DB::getInstance();
627
628        $this->initDB();
629
630        if (isset($user_id)) {
631            // Check the login status of a specific user.
632            $qid = $db->query("
633                SELECT
634                    TIMESTAMPDIFF(SECOND, last_login_datetime, NOW()) AS seconds_since_last_login,
635                    TIMESTAMPDIFF(SECOND, last_access_datetime, NOW()) AS seconds_since_last_access
636                FROM " . $this->_params['db_table'] . "
637                WHERE " . $this->_params['db_primary_key'] . " = '" . $db->escapeString($user_id) . "'
638                AND last_login_datetime > DATE_SUB(NOW(), INTERVAL '" . $db->escapeString($this->_params['login_timeout']) . "' SECOND)
639                AND last_access_datetime > DATE_SUB(NOW(), INTERVAL '" . $db->escapeString($this->_params['idle_timeout']) . "' SECOND)
640            ");
641            $result = mysql_fetch_assoc($qid);
642            if (mysql_num_rows($qid) > 0 && isset($result['seconds_since_last_login']) && isset($result['seconds_since_last_access'])) {
643                $seconds_until_login_timeout = max(0, $this->_params['login_timeout'] - $result['seconds_since_last_login']);
644                $seconds_until_idle_timeout = max(0, $this->_params['idle_timeout'] - $result['seconds_since_last_access']);
645                $session_expiry_seconds = min($seconds_until_login_timeout, $seconds_until_idle_timeout);
646                $app->logMsg(sprintf('Returning true login status for user_id %s (session expires in %s seconds)', $user_id, $session_expiry_seconds), LOG_DEBUG, __FILE__, __LINE__);
647                return $session_expiry_seconds;
648            } else {
649                $app->logMsg(sprintf('Returning false login status for user_id %s', $user_id), LOG_DEBUG, __FILE__, __LINE__);
650                return false;
651            }
652        }
653
654        // User login test need only be run once per script execution. We cache the result in the session.
655        if ($this->_authentication_tested && isset($_SESSION['_auth_sql'][$this->_ns]['authenticated'])) {
656            $app->logMsg(sprintf('Returning cached authentication status: %s', ($_SESSION['_auth_sql'][$this->_ns]['authenticated'] ? 'true' : 'false')), LOG_DEBUG, __FILE__, __LINE__);
657            return $_SESSION['_auth_sql'][$this->_ns]['authenticated'];
658        }
659
660        // Testing login should occur once. This is the first time. Set flag.
661        $this->_authentication_tested = true;
662
663        // Some users will access from networks with a changing IP number (i.e. behind a proxy server).
664        // These users must be allowed entry by adding their IP to the list of trusted_networks, or their usernames to the list of match_remote_ip_exempt_usernames.
665        if ($trusted_net = ipInRange(getRemoteAddr(), $this->_params['trusted_networks'])) {
666            $user_in_trusted_network = true;
667            $app->logMsg(sprintf('User_id %s accessing from trusted network %s',
668                ($this->get('user_id') ? $this->get('user_id') . ' (' .  $this->get('username') . ')' : 'unknown'),
669                $trusted_net
670            ), LOG_DEBUG, __FILE__, __LINE__);
671        } else {
672            $user_in_trusted_network = false;
673        }
674
675        // Do we match the user's remote IP at all? Yes, if set in config and not disabled for specific user.
676        if ($this->getParam('match_remote_ip') && !$this->get('match_remote_ip_exempt')) {
677            $remote_ip_is_matched = (isset($_SESSION['_auth_sql'][$this->_ns]['remote_ip']) && $_SESSION['_auth_sql'][$this->_ns]['remote_ip'] == getRemoteAddr()) || $user_in_trusted_network;
678        } else {
679            $app->logMsg(sprintf('User_id %s exempt from remote_ip match (comparing %s == %s)',
680                ($this->get('user_id') ? $this->get('user_id') . ' (' .  $this->get('username') . ')' : 'unknown'),
681                $_SESSION['_auth_sql'][$this->_ns]['remote_ip'],
682                getRemoteAddr()
683            ), LOG_DEBUG, __FILE__, __LINE__);
684            $remote_ip_is_matched = true;
685        }
686
687        // Test login with information stored in session. Skip IP matching for users from trusted networks.
688        if (isset($_SESSION['_auth_sql'][$this->_ns]['authenticated'])
689            && true === $_SESSION['_auth_sql'][$this->_ns]['authenticated']
690            && isset($_SESSION['_auth_sql'][$this->_ns]['username'])
691            && !empty($_SESSION['_auth_sql'][$this->_ns]['username'])
692            && isset($_SESSION['_auth_sql'][$this->_ns]['login_datetime'])
693            && strtotime($_SESSION['_auth_sql'][$this->_ns]['login_datetime']) > (time() - $this->_params['login_timeout'])
694            && isset($_SESSION['_auth_sql'][$this->_ns]['last_access_datetime'])
695            && strtotime($_SESSION['_auth_sql'][$this->_ns]['last_access_datetime']) > (time() - $this->_params['idle_timeout'])
696            && $remote_ip_is_matched
697        ) {
698            // User is authenticated!
699
700            // Update the last_access_datetime to now.
701            $this->set('last_access_datetime', date('Y-m-d H:i:s'));
702
703            // Update the DB with the last_access_datetime and increment the seconds_online.
704            $db->query("
705                UPDATE " . $this->_params['db_table'] . " SET
706                seconds_online = seconds_online + IFNULL(ABS(UNIX_TIMESTAMP() - UNIX_TIMESTAMP(last_access_datetime)), 0) + 1,
707                last_access_datetime = '" . $this->get('last_access_datetime') . "'
708                WHERE " . $this->_params['db_primary_key'] . " = '" . $this->get('user_id') . "'
709            ");
710            if (mysql_affected_rows($db->getDBH()) > 0) {
711                // User record still exists in DB. Do this to ensure user was not delete from DB between accesses. Notice "+ 1" in SQL above to ensure record is modified.
712                $app->logMsg(sprintf('Session authenticated for user_id %s (%s).', $this->get('user_id'), $this->get('username')), LOG_DEBUG, __FILE__, __LINE__);
713                // TODO: This auth check doesn't match parity when calling isLoggedIn($user_id) with a user_id, because the latter checks last_login_datetime in DB, and the former only checks SESSION. These two can be out-of-sync after loading DB via sdbdown.
714                return true;
715            } else {
716                $app->logMsg(sprintf('Session update failed; record not found for user_id %s (%s).', $this->get('user_id'), $this->get('username')), LOG_NOTICE, __FILE__, __LINE__);
717            }
718        } else if (isset($_SESSION['_auth_sql'][$this->_ns]['authenticated']) && true === $_SESSION['_auth_sql'][$this->_ns]['authenticated']) {
719            // User is authenticated, but login has expired.
720
721            // Log the reason for login expiration.
722            $expire_reasons = array();
723            $user_notified = false;
724            if (!isset($_SESSION['_auth_sql'][$this->_ns]['username']) || empty($_SESSION['_auth_sql'][$this->_ns]['username'])) {
725                $expire_reasons[] = 'username not found';
726            }
727            if (!isset($_SESSION['_auth_sql'][$this->_ns]['login_datetime']) || strtotime($_SESSION['_auth_sql'][$this->_ns]['login_datetime']) <= (time() - $this->_params['login_timeout'])) {
728                $expire_reasons[] = sprintf('login_timeout expired (%s older than %s seconds ago)', $_SESSION['_auth_sql'][$this->_ns]['login_datetime'], $this->_params['login_timeout']);
729            }
730            if (!isset($_SESSION['_auth_sql'][$this->_ns]['last_access_datetime']) || strtotime($_SESSION['_auth_sql'][$this->_ns]['last_access_datetime']) <= (time() - $this->_params['idle_timeout'])) {
731                $expire_reasons[] = sprintf('idle_timeout expired (%s older than %s seconds ago)', $_SESSION['_auth_sql'][$this->_ns]['last_access_datetime'], $this->_params['idle_timeout']);
732                if (strtotime($_SESSION['_auth_sql'][$this->_ns]['last_access_datetime']) > (time() - 43200)) {
733                    // Only raise message if last session is less than 12 hours old.
734                    // Notify user why they were logged out if they haven't yet been given a reason.
735                    $user_notified || $app->raiseMsg(sprintf(_("For your security, we logged you out after being idle for %s. Please log in again."), humanTime($this->_params['idle_timeout'], 'hour', '%01.0f')), MSG_NOTICE, __FILE__, __LINE__);
736                    $user_notified = true;
737                }
738            }
739            if (!isset($_SESSION['_auth_sql'][$this->_ns]['remote_ip']) || $_SESSION['_auth_sql'][$this->_ns]['remote_ip'] != getRemoteAddr()) {
740                if ($this->getParam('match_remote_ip') && !$this->get('match_remote_ip_exempt') && !$user_in_trusted_network) {
741                    // There are three cases when a remote IP match will be the cause of a session termination:
742                    //   1. match_remote_ip config is enabled
743                    //   2. user is not match_remote_ip_exempt (set in the user_data, or in the match_remote_ip_exempt_usernames list)
744                    //   3. the user is connecting from a trusted network (their IP is listed in the trusted_networks)
745                    $expire_reasons[] = sprintf('remote_ip not matched (%s != %s)', $_SESSION['_auth_sql'][$this->_ns]['remote_ip'], getRemoteAddr());
746                    // Notify user why they were logged out if they haven't yet been given a reason.
747                    $user_notified || $app->raiseMsg(sprintf(_("For your security, we logged you out because your IP address changed. Please log in again."), null), MSG_NOTICE, __FILE__, __LINE__);
748                    $user_notified = true;
749                } else {
750                    $expire_reasons[] = sprintf('remote_ip not matched but user was exempt from this check (%s != %s)', $_SESSION['_auth_sql'][$this->_ns]['remote_ip'], getRemoteAddr());
751                }
752            }
753            $app->logMsg(sprintf('User_id %s (%s) session expired: %s', $this->get('user_id'), $this->get('username'), join(', ', $expire_reasons)), LOG_INFO, __FILE__, __LINE__);
754        } else {
755            $app->logMsg('Session is not authenticated', LOG_DEBUG, __FILE__, __LINE__);
756        }
757
758        // User is not authenticated.
759        $this->clear();
760        return false;
761    }
762
763    /**
764     * Redirect user to login page if they are not logged in.
765     *
766     * @param string $message The text description of a message to raise.
767     * @param int    $type    The type of message: MSG_NOTICE,
768     *                        MSG_SUCCESS, MSG_WARNING, or MSG_ERR.
769     * @param string $file    __FILE__.
770     * @param string $line    __LINE__.
771     * @access public
772     */
773    public function requireLogin($message='', $type=MSG_NOTICE, $file=null, $line=null)
774    {
775        $app =& App::getInstance();
776
777        if (!$this->isLoggedIn()) {
778            // Display message for requiring login. (RaiseMsg will ignore empty strings.)
779            if ('' != $message) {
780                $app->raiseMsg($message, $type, $file, $line);
781            }
782
783            // Login scripts must have the same 'login' tag for boomerangURL verification/manipulation.
784            $app->setBoomerangURL(getenv('REQUEST_URI'), 'login');
785            $app->dieURL($this->_params['login_url']);
786        }
787    }
788
789    /**
790     * This sets the 'blocked' field for a user in the db_table, and also
791     * adds an optional reason
792     *
793     * @param  string   $reason      The reason for blocking the account.
794     */
795    public function blockAccount($user_id=null, $reason='')
796    {
797        $app =& App::getInstance();
798        $db =& DB::getInstance();
799
800        $this->initDB();
801
802        if ($this->getParam('blocking')) {
803            if (mb_strlen($db->escapeString($reason)) > 255) {
804                // blocked_reason field is varchar(255).
805                $app->logMsg(sprintf('Blocked reason provided is greater than 255 characters: %s', $reason), LOG_WARNING, __FILE__, __LINE__);
806            }
807
808            // Get user_id if specified.
809            $user_id = isset($user_id) ? $user_id : $this->get('user_id');
810            $db->query("
811                UPDATE " . $this->_params['db_table'] . " SET
812                blocked = 'true',
813                blocked_reason = '" . $db->escapeString($reason) . "'
814                WHERE " . $this->_params['db_primary_key'] . " = '" . $db->escapeString($user_id) . "'
815            ");
816        }
817    }
818
819    /**
820     * Tests if the "blocked" flag is set for a user.
821     *
822     * @param  int      $user_id    User id to look for.
823     * @return boolean              True if the user is blocked, false otherwise.
824     */
825    public function isBlocked($user_id=null)
826    {
827        $db =& DB::getInstance();
828
829        $this->initDB();
830
831        if ($this->getParam('blocking')) {
832            // Get user_id if specified.
833            $user_id = isset($user_id) ? $user_id : $this->getVal('user_id');
834            $qid = $db->query("
835                SELECT 1
836                FROM " . $this->_params['db_table'] . "
837                WHERE blocked = 'true'
838                AND " . $this->_params['db_primary_key'] . " = '" . $db->escapeString($user_id) . "'
839            ");
840            return mysql_num_rows($qid) === 1;
841        }
842    }
843
844    /**
845     * Unblocks a user in the db_table, and clears any blocked_reason.
846     */
847    public function unblockAccount($user_id=null)
848    {
849        $db =& DB::getInstance();
850
851        $this->initDB();
852
853        if ($this->getParam('blocking')) {
854            // Get user_id if specified.
855            $user_id = isset($user_id) ? $user_id : $this->get('user_id');
856            $db->query("
857                UPDATE " . $this->_params['db_table'] . " SET
858                blocked = NULL,
859                blocked_reason = ''
860                WHERE " . $this->_params['db_primary_key'] . " = '" . $db->escapeString($user_id) . "'
861            ");
862        }
863    }
864
865    /**
866     * Returns true if username already exists in database.
867     *
868     * @param  string  $username    Username to look for.
869     * @return bool                 True if username exists.
870     */
871    public function usernameExists($username)
872    {
873        $db =& DB::getInstance();
874
875        $this->initDB();
876
877        $qid = $db->query("
878            SELECT 1
879            FROM " . $this->_params['db_table'] . "
880            WHERE " . $this->_params['db_username_column'] . " = '" . $db->escapeString($username) . "'
881        ");
882        return (mysql_num_rows($qid) > 0);
883    }
884
885    /**
886     * Returns a username for a specified user id.
887     *
888     * @param  string  $user_id     User id to look for.
889     * @return string               Username, or false if none found.
890     */
891    public function getUsername($user_id)
892    {
893        $db =& DB::getInstance();
894
895        $this->initDB();
896
897        $qid = $db->query("
898            SELECT " . $this->_params['db_username_column'] . "
899            FROM " . $this->_params['db_table'] . "
900            WHERE " . $this->_params['db_primary_key'] . " = '" . $db->escapeString($user_id) . "'
901        ");
902        if (list($username) = mysql_fetch_row($qid)) {
903            return $username;
904        } else {
905            return false;
906        }
907    }
908
909    /**
910     * Returns a user_id for a specified username.
911     *
912     * @param  string  $username    Username to look for.
913     * @return string               User_id, or false if none found.
914     */
915    public function getUserID($username)
916    {
917        $db =& DB::getInstance();
918
919        $this->initDB();
920
921        $qid = $db->query("
922            SELECT " . $this->_params['db_primary_key'] . "
923            FROM " . $this->_params['db_table'] . "
924            WHERE " . $this->_params['db_username_column'] . " = '" . $db->escapeString($username) . "'
925        ");
926        if (list($user_id) = mysql_fetch_row($qid)) {
927            return $user_id;
928        } else {
929            return false;
930        }
931    }
932
933    /*
934    * Generate a cryptographically secure, random password.
935    *
936    * @access   public
937    * @param    int  $bytes     Length of password (in bytes)
938    * @return   string          Random string of characters.
939    * @author   Quinn Comendant <quinn@strangecode.com>
940    * @version  1.0
941    * @since    15 Nov 2014 20:30:27
942    */
943    public function generatePassword($bytes=10)
944    {
945        $app =& App::getInstance();
946
947        $bytes = is_numeric($bytes) ? $bytes : 10;
948        $string = strtok(base64_encode(openssl_random_pseudo_bytes($bytes, $strong)), '=');
949        if (!$strong) {
950            $app->logMsg(sprintf('Password generated was not "cryptographically strong"; check your openssl.', null), LOG_NOTICE, __FILE__, __LINE__);
951        }
952
953        return $string;
954    }
955
956    /**
957     *
958     */
959    public function encryptPassword($password, $salt=null, $hash_type=null)
960    {
961        $app =& App::getInstance();
962
963        $password = (string)$password;
964
965        // Existing password hashes rely on the same key/salt being used to compare hashs.
966        // Don't change this (or the value applied to signing_key) unless you know existing hashes or signatures will not be affected!
967        $more_salt = 'B36D18E5-3FE4-4D58-8150-F26642852B81';
968
969        $hash_type = isset($hash_type) && !empty($hash_type) ? $hash_type : $this->getParam('hash_type');
970
971        switch ($hash_type) {
972        case self::ENCRYPT_PLAINTEXT :
973            $encrypted_password = $password;
974            break;
975
976        case self::ENCRYPT_CRYPT :
977            // If comparing password with an existing hashed password, provide the hashed password as the salt.
978            $encrypted_password = isset($salt) ? crypt($password, $salt) : crypt($password);
979            break;
980
981        case self::ENCRYPT_SHA1 :
982            $encrypted_password = sha1($password);
983            break;
984
985        case self::ENCRYPT_SHA1_HARDENED :
986            $encrypted_password = sha1($app->getParam('signing_key') . $password . $more_salt);
987            for ($i=0; $i < pow(2, 20); $i++) {
988                $encrypted_password = sha1($password . $encrypted_password);
989            }
990            break;
991
992        case self::ENCRYPT_MD5 :
993            $encrypted_password = md5($password);
994            break;
995
996        case self::ENCRYPT_MD5_HARDENED :
997            $encrypted_password = md5($app->getParam('signing_key') . $password . $more_salt);
998            for ($i=0; $i < pow(2, 20); $i++) {
999                $encrypted_password = md5($password . $encrypted_password);
1000            }
1001            break;
1002
1003        case self::ENCRYPT_PASSWORD_BCRYPT :
1004            $encrypted_password = password_hash($password, PASSWORD_BCRYPT, array('cost' => 12));
1005            break;
1006
1007        case self::ENCRYPT_PASSWORD_DEFAULT :
1008            $encrypted_password = password_hash($password, PASSWORD_DEFAULT, array('cost' => 12));
1009            break;
1010
1011        default :
1012            $app->logMsg(sprintf('Unknown hash type: %s', $hash_type), LOG_WARNING, __FILE__, __LINE__);
1013            return false;
1014        }
1015
1016        // In case our hashing function returns 'false' or another empty value, bail out.
1017        if ('' == trim((string)$encrypted_password)) {
1018            $app->logMsg(sprintf('Invalid password hash returned ("%s") for hash type %s; check yo crypto!', $encrypted_password, $hash_type), LOG_ALERT, __FILE__, __LINE__);
1019            return false;
1020        }
1021
1022        return $encrypted_password;
1023    }
1024
1025    /*
1026    *
1027    *
1028    * @access   public
1029    * @param
1030    * @return
1031    * @author   Quinn Comendant <quinn@strangecode.com>
1032    * @version  1.0
1033    * @since    15 Nov 2014 21:37:28
1034    */
1035    public function verifyPassword($password, $encrypted_password, $hash_type=null)
1036    {
1037        $app =& App::getInstance();
1038
1039        $hash_type = isset($hash_type) && !empty($hash_type) ? $hash_type : $this->getParam('hash_type');
1040
1041        switch ($hash_type) {
1042        case self::ENCRYPT_CRYPT :
1043            return $this->encryptPassword($password, $encrypted_password, $hash_type) == $encrypted_password;
1044
1045        case self::ENCRYPT_PLAINTEXT :
1046        case self::ENCRYPT_MD5 :
1047        case self::ENCRYPT_MD5_HARDENED :
1048        case self::ENCRYPT_SHA1 :
1049        case self::ENCRYPT_SHA1_HARDENED :
1050            return $this->encryptPassword($password, $encrypted_password, $hash_type) == $encrypted_password;
1051
1052        case self::ENCRYPT_PASSWORD_BCRYPT :
1053        case self::ENCRYPT_PASSWORD_DEFAULT :
1054            return password_verify($password, $encrypted_password);
1055
1056        default :
1057            $app->logMsg(sprintf('Unknown hash type: %s', $hash_type), LOG_WARNING, __FILE__, __LINE__);
1058            return false;
1059        }
1060
1061    }
1062
1063    /**
1064     *
1065     */
1066    public function setPassword($user_id, $password, $hash_type=null)
1067    {
1068        $app =& App::getInstance();
1069        $db =& DB::getInstance();
1070
1071        $this->initDB();
1072
1073        // Get user_id if specified.
1074        $user_id = isset($user_id) ? $user_id : $this->get('user_id');
1075
1076        // New hash type.
1077        $hash_type = isset($hash_type) ? $hash_type : $this->getParam('hash_type');
1078
1079        // Save the hash method used if a table exists for it.
1080        $userpass_hashtype_clause = '';
1081        if ($db->columnExists($this->_params['db_table'], 'userpass_hashtype', false)) {
1082            $userpass_hashtype_clause = ", userpass_hashtype = '" . $db->escapeString($hash_type) . "'";
1083        }
1084
1085        // Issue the password change query.
1086        $db->query("
1087            UPDATE " . $this->_params['db_table'] . " SET
1088                userpass = '" . $db->escapeString($this->encryptPassword($password, null, $hash_type)) . "',
1089                modified_datetime = NOW(),
1090                modified_by_user_id = '" . $db->escapeString($user_id) . "'
1091                $userpass_hashtype_clause
1092            WHERE " . $this->_params['db_primary_key'] . " = '" . $db->escapeString($user_id) . "'
1093        ");
1094
1095        if (mysql_affected_rows($db->getDBH()) != 1) {
1096            $app->logMsg(sprintf('Failed to update password for user_id %s (no affected rows)', $user_id), LOG_WARNING, __FILE__, __LINE__);
1097            return false;
1098        }
1099
1100        $app->logMsg(sprintf('Password change successful for user_id %s', $user_id), LOG_INFO, __FILE__, __LINE__);
1101        return true;
1102    }
1103
1104    /**
1105     * Resets the password for the user with the specified id.
1106     *
1107     * @param  string $user_id   The id of the user to reset.
1108     * @param  string $reason    Additional message to add to the reset email.
1109     * @return string            The user's new password.
1110     */
1111    public function resetPassword($user_id=null, $reason='')
1112    {
1113        $app =& App::getInstance();
1114        $db =& DB::getInstance();
1115
1116        $this->initDB();
1117
1118        // Get user_id if specified.
1119        $user_id = isset($user_id) ? $user_id : $this->get('user_id');
1120
1121        // Reset password of a specific user.
1122        $qid = $db->query("
1123            SELECT * FROM " . $this->_params['db_table'] . "
1124            WHERE " . $this->_params['db_primary_key'] . " = '" . $db->escapeString($user_id) . "'
1125        ");
1126        if (!$user_data = mysql_fetch_assoc($qid)) {
1127            $app->logMsg(sprintf('Reset password failed. User_id %s not found.', $user_id), LOG_NOTICE, __FILE__, __LINE__);
1128            return false;
1129        }
1130
1131        // Get new password.
1132        $password = $this->generatePassword();
1133
1134        // Update password query.
1135        $this->setPassword($user_id, $password);
1136
1137        // Make sure user has an email on record before continuing.
1138        if (!isset($user_data['email']) || '' == trim($user_data['email'])) {
1139            $app->logMsg(sprintf('Password reset but notification failed, no email address for user_id %s (%s).', $user_data[$this->_params['db_primary_key']], $user_data[$this->_params['db_username_column']]), LOG_NOTICE, __FILE__, __LINE__);
1140        } else {
1141            // Send the new password in an email.
1142            $email = new Email(array(
1143                'to' => $user_data['email'],
1144                'from' => sprintf('"%s" <%s>', addcslashes($app->getParam('site_name'), '"'), $app->getParam('site_email')),
1145                'subject' => sprintf('%s password change', $app->getParam('site_name'))
1146            ));
1147            $email->setTemplate('codebase/services/templates/email_reset_password.txt');
1148            $email->replace(array(
1149                'SITE_NAME' => $app->getParam('site_name'),
1150                'SITE_URL' => $app->getParam('site_url'),
1151                'SITE_EMAIL' => $app->getParam('site_email'),
1152                'NAME' => ('' != $user_data['first_name'] . $user_data['last_name'] ? $user_data['first_name'] . ' ' . $user_data['last_name'] : $user_data[$this->_params['db_username_column']]),
1153                'USERNAME' => $user_data[$this->_params['db_username_column']],
1154                'PASSWORD' => $password,
1155                'REASON' => ('' == trim($reason) ? '' : trim($reason) . ' '), // Add a space after the reason if it exists for better formatting.
1156            ));
1157            $email->send();
1158        }
1159
1160        return array(
1161            'username' => $user_data[$this->_params['db_username_column']],
1162            'userpass' => $password
1163        );
1164    }
1165
1166} // end class
Note: See TracBrowser for help on using the repository browser.