source: tags/2.0.2/lib/Auth_SQL.inc.php @ 480

Last change on this file since 480 was 319, checked in by quinn, 16 years ago
File size: 41.9 KB
RevLine 
[1]1<?php
2/**
3 * The Auth_SQL:: class provides a SQL implementation for authentication.
4 *
5 * @author  Quinn Comendant <quinn@strangecode.com>
6 * @version 2.0
7 */
8
9// Available encryption types for class Auth_SQL.
[42]10define('AUTH_ENCRYPT_MD5', 'md5');
11define('AUTH_ENCRYPT_CRYPT', 'crypt');
12define('AUTH_ENCRYPT_SHA1', 'sha1');
13define('AUTH_ENCRYPT_PLAINTEXT', 'plaintext');
[1]14
[43]15require_once dirname(__FILE__) . '/Email.inc.php';
16
[1]17class Auth_SQL {
18
19    var $_auth = '';
20    var $_sess = '_auth_';
21    var $_authentication_tested;
22    var $_params = array();
23
24    // Default param values.
25    var $_default_params = array(
[42]26
[1]27        // Automatically create table and verify columns. Better set to false after site launch.
28        'create_table' => true,
[42]29
[1]30        // The database table containing users to authenticate.
31        'db_table' => 'user_tbl',
[42]32
[1]33        // The name of the primary key for the db_table.
34        'db_primary_key' => 'user_id',
[42]35
[1]36        // The name of the username key for the db_table.
37        'db_username_column' => 'username',
[42]38
[1]39        // If using the db_login_table feature, specify the db_login_table. The primary key must match the primary key for the db_table.
[14]40        'db_login_table' => 'user_login_tbl',
[42]41
[1]42        // The type of encryption to use for passwords stored in the db_table. Use one of the AUTH_ENCRYPT_* types specified above.
43        'encryption_type' => AUTH_ENCRYPT_MD5,
44
[25]45        // The URL to the login script.
[1]46        'login_url' => '/',
47
48        // The maximum amount of time a user is allowed to be logged in. They will be forced to login again if they expire.
49        // This applies to admins and users. In seconds. 21600 seconds = 6 hours.
50        'login_timeout' => 21600,
[42]51
[1]52        // 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.
53        // This applies to admins and users. In seconds. 3600 seconds = 1 hour.
54        'idle_timeout' => 3600,
55
56        // 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.
57        // Days and hours, like this: 'DD:HH'
58        'login_abuse_timeframe' => '04:00',
59
60        // The number of warnings a user will receive (and their password reset each time) before their account is completely blocked.
61        'login_abuse_warnings' => 3,
62
63        // The maximum number of IP addresses a user can login with over the timeout period before their account is blocked.
64        'login_abuse_max_ips' => 5,
65
[42]66        // The IP address subnet size threshold. Uses a CIDR notation network mask (see CIDR cheatsheet at bottom).
67        // Any integar between 0 and 32 is permitted. Setting this to '24' permits any address in a
[1]68        // class C network (255.255.255.0) to be considered the same. Setting to '32' compares each IP absolutely.
69        // Setting to '0' ignores all IPs, thus disabling login_abuse checking.
70        'login_abuse_ip_bitmask' => 32,
71
[42]72        // Specify usernames to exclude from the account abuse detection system. This is specified as a hardcoded array provided at
[1]73        // class instantiation time, or can be saved in the db_table under the login_abuse_exempt field.
74        'login_abuse_exempt_usernames' => array(),
[42]75
[103]76        // An array of IP blocks that are bypass the remote_ip comparison check. Useful for dynamic IPs or those behind proxy servers.
[1]77        'trusted_networks' => array(),
78
[319]79        // Match the user's current remote IP against the one they logged in with.
80        'match_remote_ip' => true,
81
[1]82        // Allow user accounts to be blocked? Requires the user table to have the columns 'blocked' and 'blocked_reason'
83        'blocking' => false,
[42]84
[1]85        // Use a db_login_table to detect excessive logins. This requires blocking to be enabled.
86        'abuse_detection' => false,
87    );
88
89    /**
90     * Constructs a new authentication object.
91     *
92     * @access public
93     * @param optional array $params  A hash containing parameters.
94     */
95    function Auth_SQL($auth_name=null)
96    {
97        if (isset($auth_name)) {
98            $this->_auth = $auth_name;
99            $this->_sess .= $auth_name;
100        }
101
102        // Initialize default parameters.
103        $this->setParam($this->_default_params);
104
105        // Get create tables config from global context.
106        if (!is_null(App::getParam('db_create_tables'))) {
107            $this->setParam(array('create_table' => App::getParam('db_create_tables')));
108        }
109    }
[42]110
[1]111    /**
112     * Setup the database tables for this class.
113     *
114     * @access  public
115     * @author  Quinn Comendant <quinn@strangecode.com>
116     * @since   26 Aug 2005 17:09:36
117     */
118    function initDB($recreate_db=false)
119    {
120        static $_db_tested = false;
[42]121
[1]122        if ($recreate_db || !$_db_tested && $this->getParam('create_table')) {
[42]123
[1]124            // User table.
125            if ($recreate_db) {
126                DB::query("DROP TABLE IF EXISTS " . $this->getParam('db_table'));
127                App::logMsg(sprintf('Dropping and recreating table %s.', $this->getParam('db_table')), LOG_DEBUG, __FILE__, __LINE__);
128            }
129
[15]130            // The minimal columns for a table compatable with the Auth_SQL class.
[1]131            DB::query("CREATE TABLE IF NOT EXISTS " . $this->getParam('db_table') . " (
132                " . $this->getParam('db_primary_key') . " smallint(11) NOT NULL auto_increment,
133                " . $this->getParam('db_username_column') . " varchar(255) NOT NULL default '',
134                userpass varchar(255) NOT NULL default '',
135                first_name varchar(255) NOT NULL default '',
136                last_name varchar(255) NOT NULL default '',
137                email varchar(255) NOT NULL default '',
138                user_type enum('public', 'editor', 'admin', 'root') default NULL,
139                login_abuse_exempt enum('true') default NULL,
140                blocked enum('true') default NULL,
141                blocked_reason varchar(255) NOT NULL default '',
142                abuse_warning_level tinyint(4) NOT NULL default '0',
143                seconds_online int(11) NOT NULL default '0',
144                last_login_datetime datetime NOT NULL default '0000-00-00 00:00:00',
145                last_access_datetime datetime NOT NULL default '0000-00-00 00:00:00',
146                last_login_ip varchar(255) NOT NULL default '0.0.0.0',
147                added_by_user_id smallint(11) default NULL,
148                modified_by_user_id smallint(11) default NULL,
149                added_datetime datetime NOT NULL default '0000-00-00 00:00:00',
150                modified_datetime datetime NOT NULL default '0000-00-00 00:00:00',
151                PRIMARY KEY (" . $this->getParam('db_primary_key') . "),
152                KEY " . $this->getParam('db_username_column') . " (" . $this->getParam('db_username_column') . "),
153                KEY userpass (userpass),
154                KEY email (email)
155            )");
156
157            if (!DB::columnExists($this->getParam('db_table'), array(
[42]158                $this->getParam('db_primary_key'),
159                $this->getParam('db_username_column'),
160                'userpass',
161                'first_name',
162                'last_name',
163                'email',
164                'user_type',
165                'login_abuse_exempt',
166                'blocked',
167                'blocked_reason',
168                'abuse_warning_level',
169                'seconds_online',
170                'last_login_datetime',
171                'last_access_datetime',
172                'last_login_ip',
173                'added_by_user_id',
174                'modified_by_user_id',
175                'added_datetime',
176                'modified_datetime',
[1]177            ), false, false)) {
178                App::logMsg(sprintf('Database table %s has invalid columns. Please update this table manually.', $this->getParam('db_table')), LOG_ALERT, __FILE__, __LINE__);
179                trigger_error(sprintf('Database table %s has invalid columns. Please update this table manually.', $this->getParam('db_table')), E_USER_ERROR);
180            }
[42]181
[17]182            // Login table is used for abuse_detection features.
183            if ($this->getParam('abuse_detection')) {
184                if ($recreate_db) {
185                    DB::query("DROP TABLE IF EXISTS " . $this->getParam('db_login_table'));
186                    App::logMsg(sprintf('Dropping and recreating table %s.', $this->getParam('db_login_table')), LOG_DEBUG, __FILE__, __LINE__);
187                }
188                DB::query("CREATE TABLE IF NOT EXISTS " . $this->getParam('db_login_table') . " (
189                    " . $this->getParam('db_primary_key') . " smallint(11) NOT NULL default '0',
190                    login_datetime datetime NOT NULL default '0000-00-00 00:00:00',
191                    remote_ip_binary char(32) NOT NULL default '',
192                    KEY " . $this->getParam('db_primary_key') . " (" . $this->getParam('db_primary_key') . "),
193                    KEY login_datetime (login_datetime),
194                    KEY remote_ip_binary (remote_ip_binary)
195                )");
[42]196
[17]197                if (!DB::columnExists($this->getParam('db_login_table'), array(
198                    $this->getParam('db_primary_key'),
199                    'login_datetime',
200                    'remote_ip_binary',
201                ), false, false)) {
202                    App::logMsg(sprintf('Database table %s has invalid columns. Please update this table manually.', $this->getParam('db_login_table')), LOG_ALERT, __FILE__, __LINE__);
203                    trigger_error(sprintf('Database table %s has invalid columns. Please update this table manually.', $this->getParam('db_login_table')), E_USER_ERROR);
204                }
[1]205            }
[42]206        }
[1]207        $_db_tested = true;
208    }
209
210    /**
211     * Set the params of an auth object.
212     *
213     * @param  array $params   Array of parameter keys and value to set.
214     * @return bool true on success, false on failure
215     */
216    function setParam($params)
217    {
218        if (isset($params) && is_array($params)) {
219            // Merge new parameters with old overriding only those passed.
220            $this->_params = array_merge($this->_params, $params);
221        }
222    }
223
224    /**
225     * Return the value of a parameter, if it exists.
226     *
227     * @access public
228     * @param string $param        Which parameter to return.
229     * @return mixed               Configured parameter value.
230     */
231    function getParam($param)
232    {
233        if (isset($this->_params[$param])) {
234            return $this->_params[$param];
235        } else {
236            App::logMsg(sprintf('Parameter is not set: %s', $param), LOG_DEBUG, __FILE__, __LINE__);
237            return null;
238        }
239    }
240
241    /**
242     * Clear any authentication tokens in the current session. A.K.A. logout.
243     *
244     * @access public
245     */
246    function clearAuth()
247    {
248        $this->initDB();
[42]249
[1]250        DB::query("
[42]251            UPDATE " . $this->_params['db_table'] . " SET
[1]252            seconds_online = seconds_online + (UNIX_TIMESTAMP() - UNIX_TIMESTAMP(last_access_datetime)),
253            last_login_datetime = '0000-00-00 00:00:00'
254            WHERE " . $this->_params['db_primary_key'] . " = '" . $this->getVal('user_id') . "'
255        ");
[126]256        $_SESSION[$this->_sess] = array('authenticated' => false);
[1]257    }
258
259    /**
[103]260     * Sets a variable into a registered auth session.
261     *
262     * @access public
263     * @param mixed $key      Which value to set.
264     * @param mixed $val      Value to set variable to.
265     */
266    function setVal($key, $val)
267    {
268        if (!isset($_SESSION[$this->_sess]['user_data'])) {
269            $_SESSION[$this->_sess]['user_data'] = array();
270        }
271        $_SESSION[$this->_sess]['user_data'][$key] = $val;
272    }
273
274    /**
275     * Returns a specified value from a registered auth session.
276     *
277     * @access public
278     * @param mixed $key      Which value to return.
279     * @param mixed $default  Value to return if key not found in user_data.
280     * @return mixed          Value stored in session.
281     */
282    function getVal($key, $default='')
283    {
284        if (isset($_SESSION[$this->_sess][$key])) {
285            return $_SESSION[$this->_sess][$key];
286        } else if (isset($_SESSION[$this->_sess]['user_data'][$key])) {
287            return $_SESSION[$this->_sess]['user_data'][$key];
288        } else {
289            return $default;
290        }
291    }
292
293    /**
[1]294     * Find out if a set of login credentials are valid.
295     *
296     * @access private
297     * @param string $username      The username to check.
298     * @param string $password      The password to compare to username.
299     * @return mixed  False if credentials not found in DB, or returns DB row matching credentials.
300     */
301    function authenticate($username, $password)
302    {
303        $this->initDB();
[42]304
[126]305        switch ($this->_params['encryption_type']) {
306        case AUTH_ENCRYPT_CRYPT :
307            // Query DB for user matching credentials. Compare cyphertext with salted-encrypted password.
308            $qid = DB::query("
309                SELECT *, " . $this->_params['db_primary_key'] . " AS user_id
310                FROM " . $this->_params['db_table'] . "
311                WHERE " . $this->_params['db_username_column'] . " = '" . DB::escapeString($username) . "'
312                AND BINARY userpass = ENCRYPT('" . DB::escapeString($password) . "', LEFT(userpass, 2)))
313            ");
314            break;
315        case AUTH_ENCRYPT_PLAINTEXT :
316        case AUTH_ENCRYPT_MD5 :
317        case AUTH_ENCRYPT_SHA1 :
318        default :
319            // Query DB for user matching credentials. Directly compare cyphertext with result from encryptPassword().
320            $qid = DB::query("
321                SELECT *, " . $this->_params['db_primary_key'] . " AS user_id
322                FROM " . $this->_params['db_table'] . "
323                WHERE " . $this->_params['db_username_column'] . " = '" . DB::escapeString($username) . "'
324                AND BINARY userpass = '" . DB::escapeString($this->encryptPassword($password)) . "'
325            ");
326            break;
327        }
[42]328
[1]329        // Return user data if found.
330        if ($user_data = mysql_fetch_assoc($qid)) {
[40]331            App::logMsg(sprintf('Authentication successful for %s %s (%s)', $this->_auth, $user_data['user_id'], $username), LOG_INFO, __FILE__, __LINE__);
[1]332            return $user_data;
333        } else {
[15]334            App::logMsg(sprintf('Authentication failed for %s %s (encrypted attempted password: %s)', $this->_auth, $username, $this->encryptPassword($password)), LOG_NOTICE, __FILE__, __LINE__);
[1]335            return false;
336        }
337    }
338
339    /**
340     * If user authenticated, register login into session.
341     *
342     * @access private
343     * @param string $username     The username to check.
344     * @param string $password     The password to compare to username.
345     * @return boolean  Whether or not the credentials are valid.
346     */
347    function login($username, $password)
348    {
349        $this->initDB();
[42]350
[1]351        $this->clearAuth();
352
353        if (!$user_data = $this->authenticate($username, $password)) {
354            // No login: failed authentication!
355            return false;
356        }
357
358        // Register authenticated session.
359        $_SESSION[$this->_sess] = array(
360            'authenticated'         => true,
361            'user_id'               => $user_data['user_id'],
362            'auth_name'             => $this->_auth,
363            'username'              => $username,
364            'login_datetime'        => date('Y-m-d H:i:s'),
365            'last_access_datetime'  => date('Y-m-d H:i:s'),
366            'remote_ip'             => getRemoteAddr(),
367            'login_abuse_exempt'    => isset($user_data['login_abuse_exempt']) ? !empty($user_data['login_abuse_exempt']) : in_array($username, $this->_params['login_abuse_exempt_usernames']),
368            'user_data'             => $user_data
369        );
[42]370
[1]371        /**
372         * Check if the account is blocked, respond in context to reason. Cancel the login if blocked.
373         */
374        if ($this->getParam('blocking')) {
375            if (!empty($user_data['blocked'])) {
[42]376
[15]377                App::logMsg(sprintf('%s %s (%s) login failed due to blocked account: %s', ucfirst($this->_auth), $this->getVal('user_id'), $this->getVal('username'), $this->getVal('blocked_reason')), LOG_NOTICE, __FILE__, __LINE__);
[42]378
[1]379                switch ($user_data['blocked_reason']) {
380                    case 'account abuse' :
381                        App::raiseMsg(sprintf(_("This account has been blocked due to possible account abuse. Please contact us to reactivate."), null), MSG_WARNING, __FILE__, __LINE__);
382                        break;
383                    default :
384                        App::raiseMsg(sprintf(_("This account is currently not active. %s"), $user_data['blocked_reason']), MSG_WARNING, __FILE__, __LINE__);
385                        break;
386                }
[42]387
[1]388                // No login: user is blocked!
389                $this->clearAuth();
390                return false;
391            }
392        }
[42]393
[1]394        /**
395         * Check the db_login_table for too many logins under this account.
396         * (1) Count the number of unique IP addresses that logged in under this user within the login_abuse_timeframe
397         * (2) If this number exceeds the login_abuse_max_ips, assume multiple people are logging in under the same account.
398        **/
399        if ($this->getParam('abuse_detection') && !$this->getVal('login_abuse_exempt')) {
400            $qid = DB::query("
401                SELECT COUNT(DISTINCT LEFT(remote_ip_binary, " . $this->_params['login_abuse_ip_bitmask'] . "))
402                FROM " . $this->_params['db_login_table'] . "
403                WHERE " . $this->_params['db_primary_key'] . " = '" . $this->getVal('user_id') . "'
404                AND DATE_ADD(login_datetime, INTERVAL '" . $this->_params['login_abuse_timeframe'] . "' DAY_HOUR) > NOW()
405            ");
406            list($distinct_ips) = mysql_fetch_row($qid);
407            if ($distinct_ips > $this->_params['login_abuse_max_ips']) {
408                if ($this->getVal('abuse_warning_level') < $this->_params['login_abuse_warnings']) {
409                    // Warn the user with a password reset.
[15]410                    $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 login information with others. If further account abuse is detected this account will be blocked."));
[1]411                    App::raiseMsg(_("Your password has been reset as a security precaution. Please check your email for more information."), MSG_NOTICE, __FILE__, __LINE__);
[15]412                    App::logMsg(sprintf('Account abuse detected for %s %s (%s) from IP %s', $this->_auth, $this->getVal('user_id'), $this->getVal('username'), $this->getVal('remote_ip')), LOG_WARNING, __FILE__, __LINE__);
[1]413                } else {
414                    // Block the account with the reason of account abuse.
415                    $this->blockAccount(null, 'account abuse');
416                    App::raiseMsg(_("Your account has been blocked as a security precaution. Please contact us for more information."), MSG_NOTICE, __FILE__, __LINE__);
[15]417                    App::logMsg(sprintf('Account blocked for %s %s (%s) from IP %s', $this->_auth, $this->getVal('user_id'), $this->getVal('username'), $this->getVal('remote_ip')), LOG_ALERT, __FILE__, __LINE__);
[1]418                }
419                // Increment user's warning level.
420                DB::query("UPDATE " . $this->_params['db_table'] . " SET abuse_warning_level = abuse_warning_level + 1 WHERE " . $this->_params['db_primary_key'] . " = '" . $this->getVal('user_id') . "'");
421                // Reset the login counter for this user.
422                DB::query("DELETE FROM " . $this->_params['db_login_table'] . " WHERE " . $this->_params['db_primary_key'] . " = '" . $this->getVal('user_id') . "'");
423                // No login: reset password because of account abuse!
424                $this->clearAuth();
425                return false;
426            }
427
428            // Update the login counter table with this login access. Convert IP to binary.
429            DB::query("
430                INSERT INTO " . $this->_params['db_login_table'] . " (
[42]431                    " . $this->_params['db_primary_key'] . ",
432                    login_datetime,
[1]433                    remote_ip_binary
434                ) VALUES (
435                    '" . $this->getVal('user_id') . "',
436                    '" . $this->getVal('login_datetime') . "',
437                    '" . sprintf('%032b', ip2long($this->getVal('remote_ip'))) . "'
438                )
439            ");
440        }
[42]441
[1]442        // Update user table with this login.
443        DB::query("
444            UPDATE " . $this->_params['db_table'] . " SET
445                last_login_datetime = '" . $this->getVal('login_datetime') . "',
446                last_access_datetime = '" . $this->getVal('login_datetime') . "',
447                last_login_ip = '" . $this->getVal('remote_ip') . "'
448            WHERE " . $this->_params['db_primary_key'] . " = '" . $this->getVal('user_id') . "'
449        ");
[42]450
[1]451        // We're logged-in!
452        return true;
453    }
454
455    /**
456     * Test if user has a currently logged-in session.
457     *  - authentication flag set to true
458     *  - username not empty
459     *  - total logged-in time is not greater than login_timeout
460     *  - idle time is not greater than idle_timeout
461     *  - remote address is the same as the login remote address (aol users excluded).
462     *
463     * @access public
464     */
465    function isLoggedIn($user_id=null)
466    {
467        $this->initDB();
[42]468
[1]469        if (isset($user_id)) {
470            // Check the login status of a specific user.
471            $qid = DB::query("
472                SELECT 1 FROM " . $this->_params['db_table'] . "
[111]473                WHERE " . $this->_params['db_primary_key'] . " = '" . DB::escapeString($user_id) . "'
[1]474                AND DATE_ADD(last_login_datetime, INTERVAL '" . $this->_params['login_timeout'] . "' SECOND) > NOW()
475                AND DATE_ADD(last_access_datetime, INTERVAL '" . $this->_params['idle_timeout'] . "' SECOND) > NOW()
476            ");
477            return (mysql_num_rows($qid) > 0);
478        }
479
480        // User login test need only be run once per script execution. We cache the result in the session.
481        if ($this->_authentication_tested && isset($_SESSION[$this->_sess]['authenticated'])) {
482            return $_SESSION[$this->_sess]['authenticated'];
483        }
[42]484
[1]485        // Tesing login should occur once. This is the first time. Set flag.
486        $this->_authentication_tested = true;
[35]487
[1]488        // Some users will access from networks with a changing IP number (i.e. behind a proxy server). These users must be allowed entry by adding their IP to the list of trusted_networks.
489        if ($trusted_net = ipInRange(getRemoteAddr(), $this->_params['trusted_networks'])) {
490            $user_in_trusted_network = true;
[42]491            App::logMsg(sprintf('%s%s accessing from trusted network %s',
492                ucfirst($this->_auth),
[1]493                ($this->getVal('user_id') ? ' ' . $this->getVal('user_id') . ' (' .  $this->getVal('username') . ')' : ''),
494                $trusted_net
[71]495            ), LOG_DEBUG, __FILE__, __LINE__);
[1]496        } else if (preg_match('/proxy.aol.com$/i', getRemoteAddr(true))) {
497            $user_in_trusted_network = true;
[42]498            App::logMsg(sprintf('%s%s accessing from trusted network proxy.aol.com',
499                ucfirst($this->_auth),
[1]500                ($this->getVal('user_id') ? ' ' . $this->getVal('user_id') . ' (' .  $this->getVal('username') . ')' : '')
[71]501            ), LOG_DEBUG, __FILE__, __LINE__);
[1]502        } else {
503            $user_in_trusted_network = false;
504        }
[42]505
[1]506        // Test login with information stored in session. Skip IP matching for users from trusted networks.
507        if (isset($_SESSION[$this->_sess])
[223]508            && isset($_SESSION[$this->_sess]['authenticated']) && true === $_SESSION[$this->_sess]['authenticated']
[1]509            && !empty($_SESSION[$this->_sess]['username'])
510            && strtotime($_SESSION[$this->_sess]['login_datetime']) > time() - $this->_params['login_timeout']
511            && strtotime($_SESSION[$this->_sess]['last_access_datetime']) > time() - $this->_params['idle_timeout']
[319]512            && (!$this->_params['match_remote_ip'] || $_SESSION[$this->_sess]['remote_ip'] == getRemoteAddr() || $user_in_trusted_network)
[1]513        ) {
514            // User is authenticated!
515            $_SESSION[$this->_sess]['last_access_datetime'] = date('Y-m-d H:i:s');
516
517            // Update the DB with the last_access_datetime and increment the seconds_online.
518            DB::query("
[42]519                UPDATE " . $this->_params['db_table'] . " SET
[1]520                seconds_online = seconds_online + (UNIX_TIMESTAMP() - UNIX_TIMESTAMP(last_access_datetime)) + 1,
521                last_access_datetime = '" . $this->getVal('last_access_datetime') . "'
522                WHERE " . $this->_params['db_primary_key'] . " = '" . $this->getVal('user_id') . "'
523            ");
524            if (mysql_affected_rows(DB::getDBH()) > 0) {
525                // 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.
526                return true;
527            } else {
528                App::logMsg(sprintf('User update failed. Record not found for %s %s (%s).', $this->_auth, $this->getVal('user_id'), $this->getVal('username')), LOG_NOTICE, __FILE__, __LINE__);
529            }
[223]530        } else if (isset($_SESSION[$this->_sess]['authenticated']) && true === $_SESSION[$this->_sess]['authenticated']) {
[1]531            // User is authenticated, but login has expired.
[35]532            if (strtotime($_SESSION[$this->_sess]['last_access_datetime']) > time() - 43200) {
533                // Only raise message if last session is less than 12 hours old.
534                App::raiseMsg(sprintf(_("Your %s session has closed. You need to log-in again."), strtolower($this->_auth)), MSG_NOTICE, __FILE__, __LINE__);
535            }
[42]536
[1]537            // Log the reason for login expiration.
538            $expire_reasons = array();
539            if (empty($_SESSION[$this->_sess]['username'])) {
540                $expire_reasons[] = 'username not found';
541            }
542            if (strtotime($_SESSION[$this->_sess]['login_datetime']) <= time() - $this->_params['login_timeout']) {
543                $expire_reasons[] = 'login_timeout expired';
544            }
545            if (strtotime($_SESSION[$this->_sess]['last_access_datetime']) <= time() - $this->_params['idle_timeout']) {
546                $expire_reasons[] = 'idle_timeout expired';
547            }
[126]548            if ($_SESSION[$this->_sess]['remote_ip'] != getRemoteAddr() && !$user_in_trusted_network) {
[1]549                $expire_reasons[] = sprintf('remote_ip not matched (%s != %s)', $_SESSION[$this->_sess]['remote_ip'], getRemoteAddr());
550            }
551            App::logMsg(sprintf('%s %s (%s) session expired: %s', ucfirst($this->_auth), $this->getVal('user_id'), $this->getVal('username'), join(', ', $expire_reasons)), LOG_INFO, __FILE__, __LINE__);
552        }
553
554        // User is not authenticated.
555        $this->clearAuth();
556        return false;
557    }
558
559    /**
560     * Redirect user to login page if they are not logged in.
561     *
[32]562     * @param string $message The text description of a message to raise.
[1]563     * @param int    $type    The type of message: MSG_NOTICE,
564     *                        MSG_SUCCESS, MSG_WARNING, or MSG_ERR.
565     * @param string $file    __FILE__.
566     * @param string $line    __LINE__.
567     * @access public
568     */
[32]569    function requireLogin($message='', $type=MSG_NOTICE, $file=null, $line=null)
[1]570    {
571        if (!$this->isLoggedIn()) {
[103]572            // Display message for requiring login. (RaiseMsg will ignore empty strings.)
[32]573            App::raiseMsg($message, $type, $file, $line);
574
[28]575            // Login scripts must have the same 'login' tag for boomerangURL verification/manipulation.
576            App::setBoomerangURL(absoluteMe(), 'login');
[1]577            App::dieURL($this->_params['login_url']);
578        }
579    }
580
581    /**
[266]582     * Tests if the "blocked" flag is set for a user.
[42]583     *
[266]584     * @param  int      $user_id    User id to look for.
585     * @return boolean              True if the user is blocked, false otherwise.
[1]586     */
[251]587    function isBlocked($user_id=null)
588    {
589        $this->initDB();
590
591        if ($this->getParam('blocking')) {
592            // Get user_id if specified.
593            $user_id = isset($user_id) ? $user_id : $this->getVal('user_id');
594            $qid = DB::query("
595                SELECT 1
596                FROM " . $this->_params['db_table'] . "
597                WHERE blocked = 'true'
598                AND " . $this->_params['db_primary_key'] . " = '" . DB::escapeString($user_id) . "'
599            ");
600            return mysql_num_rows($qid) === 1;
601        }
602    }
603
604    /**
605     * This sets the 'blocked' field for a user in the db_table, and also
606     * adds an optional reason
607     *
608     * @param  string   $reason      The reason for blocking the account.
609     */
[1]610    function blockAccount($user_id=null, $reason='')
611    {
612        $this->initDB();
[42]613
[1]614        if ($this->getParam('blocking')) {
[111]615            if (strlen(DB::escapeString($reason)) > 255) {
[1]616                // blocked_reason field is varchar(255).
617                App::logMsg(sprintf('Blocked reason provided is greater than 255 characters: %s', $reason), LOG_WARNING, __FILE__, __LINE__);
618            }
[42]619
[1]620            // Get user_id if specified.
621            $user_id = isset($user_id) ? $user_id : $this->getVal('user_id');
622            DB::query("
623                UPDATE " . $this->_params['db_table'] . " SET
624                blocked = 'true',
[111]625                blocked_reason = '" . DB::escapeString($reason) . "'
626                WHERE " . $this->_params['db_primary_key'] . " = '" . DB::escapeString($user_id) . "'
[1]627            ");
628        }
629    }
630
631    /**
[42]632     * Unblocks a user in the db_table, and clears any blocked_reason.
[1]633     */
634    function unblockAccount($user_id=null)
635    {
636        $this->initDB();
[42]637
[1]638        if ($this->getParam('blocking')) {
639            // Get user_id if specified.
640            $user_id = isset($user_id) ? $user_id : $this->getVal('user_id');
641            DB::query("
642                UPDATE " . $this->_params['db_table'] . " SET
643                blocked = '',
644                blocked_reason = ''
[111]645                WHERE " . $this->_params['db_primary_key'] . " = '" . DB::escapeString($user_id) . "'
[1]646            ");
647        }
648    }
649
650    /**
651     * Returns true if username already exists in database.
652     *
653     * @param  string  $username    Username to look for.
654     * @return bool                 True if username exists.
655     */
656    function usernameExists($username)
[42]657    {
[1]658        $this->initDB();
[42]659
[15]660        $qid = DB::query("
[42]661            SELECT 1
[15]662            FROM " . $this->_params['db_table'] . "
[111]663            WHERE " . $this->_params['db_username_column'] . " = '" . DB::escapeString($username) . "'
[15]664        ");
[1]665        return (mysql_num_rows($qid) > 0);
666    }
667
668    /**
669     * Returns a username for a specified user id.
670     *
671     * @param  string  $user_id     User id to look for.
672     * @return string               Username, or false if none found.
673     */
674    function getUsername($user_id)
[42]675    {
[1]676        $this->initDB();
[42]677
[15]678        $qid = DB::query("
679            SELECT " . $this->_params['db_username_column'] . "
680            FROM " . $this->_params['db_table'] . "
[111]681            WHERE " . $this->_params['db_primary_key'] . " = '" . DB::escapeString($user_id) . "'
[15]682        ");
[1]683        if (list($username) = mysql_fetch_row($qid)) {
684            return $username;
685        } else {
686            return false;
687        }
688    }
689
690    /**
691     * Returns a randomly generated password based on $pattern. The pattern is any
692     * sequence of 'x', 'V', 'C', 'v', 'c', or 'd' and if it is something like 'cvccv' this
693     * function will generate a pronouncable password. Recommend using more complex
694     * patterns, at minimum the US State Department standard: cvcddcvc.
695     *
696     * - x    a random upper or lower alpha character or digit
697     * - C    a random upper or lower consanant
698     * - V    a random upper or lower vowel
699     * - c    a random lowercase consanant
700     * - v    a random lowercase vowel
701     * - d    a random digit
702     *
703     * @param  string $pattern  a sequence of character types, above.
704     * @return string           a password
705     */
706    function generatePassword($pattern='CvccvCdd')
707    {
708        mt_srand((double) microtime() * 10000000);
709        $str = '';
710        for ($i=0; $i<strlen($pattern); $i++) {
711            $x = substr('bcdfghjklmnprstvwxzBCDFGHJKLMNPRSTVWXZaeiouyAEIOUY0123456789', (mt_rand() % 60), 1);
712            $c = substr('bcdfghjklmnprstvwxz', (mt_rand() % 19), 1);
713            $C = substr('bcdfghjklmnprstvwxzBCDFGHJKLMNPRSTVWXZ', (mt_rand() % 38), 1);
714            $v = substr('aeiouy', (mt_rand() % 6), 1);
715            $V = substr('aeiouyAEIOUY', (mt_rand() % 12), 1);
716            $d = substr('0123456789', (mt_rand() % 10), 1);
717            $str .= $$pattern{$i};
718        }
719        return $str;
720    }
[42]721
[1]722    /**
723     *
724     */
[126]725    function encryptPassword($password, $salt=null)
[1]726    {
727        switch ($this->_params['encryption_type']) {
728        case AUTH_ENCRYPT_PLAINTEXT :
729            return $password;
730            break;
[42]731
[1]732        case AUTH_ENCRYPT_CRYPT :
[126]733            // If comparing clear-text password with encrypted text, provide encrypted text as the salt.
734            return isset($salt) ? crypt($password, substr($salt, 0, 2)) : crypt($password);
[1]735            break;
[42]736
[1]737        case AUTH_ENCRYPT_SHA1 :
[15]738            return sha1($password);
739            break;
[42]740
[1]741        case AUTH_ENCRYPT_MD5 :
742        default :
743            return md5($password);
744            break;
745        }
746    }
747
748    /**
[42]749     *
[1]750     */
751    function setPassword($user_id=null, $password)
[42]752    {
[1]753        $this->initDB();
[42]754
[1]755        // Get user_id if specified.
756        $user_id = isset($user_id) ? $user_id : $this->getVal('user_id');
[42]757
[228]758        // Get old password.
759        $qid = DB::query("
760            SELECT userpass
761            FROM " . $this->_params['db_table'] . "
762            WHERE " . $this->_params['db_primary_key'] . " = '" . DB::escapeString($user_id) . "'
763        ");
764        if (!list($old_encrypted_password) = mysql_fetch_row($qid)) {
765            App::logMsg(sprintf('Cannot set password for nonexistant user_id %s', $user_id), LOG_NOTICE, __FILE__, __LINE__);
766            return false;
767        }
768       
769        // Compare old with new to ensure we're actually *changing* the password.
770        $encrypted_password = $this->encryptPassword($password);
771        if ($old_encrypted_password == $encrypted_password) {
772            App::logMsg(sprintf('Not setting password: new is the same as old.', null), LOG_INFO, __FILE__, __LINE__);
773            return false;
774        }
775
[1]776        // Issue the password change query.
777        DB::query("
[42]778            UPDATE " . $this->_params['db_table'] . "
[228]779            SET userpass = '" . DB::escapeString($encrypted_password) . "'
[111]780            WHERE " . $this->_params['db_primary_key'] . " = '" . DB::escapeString($user_id) . "'
[1]781        ");
[126]782       
783        if (mysql_affected_rows(DB::getDBH()) != 1) {
[228]784            App::logMsg(sprintf('Failed to update password for user %s', $user_id), LOG_WARNING, __FILE__, __LINE__);
785            return false;
[126]786        }
[228]787
788        return true;
[1]789    }
790
791    /**
792     * Resets the password for the user with the specified id.
793     *
794     * @param  string $user_id   The id of the user to reset.
795     * @param  string $reason    Additional message to add to the reset email.
796     * @return string            The user's new password.
797     */
798    function resetPassword($user_id=null, $reason='')
799    {
800        $this->initDB();
[42]801
[1]802        // Get user_id if specified.
803        $user_id = isset($user_id) ? $user_id : $this->getVal('user_id');
[42]804
[1]805        // Reset password of a specific user.
806        $qid = DB::query("
807            SELECT * FROM " . $this->_params['db_table'] . "
[111]808            WHERE " . $this->_params['db_primary_key'] . " = '" . DB::escapeString($user_id) . "'
[1]809        ");
[15]810        if (!$user_data = mysql_fetch_assoc($qid)) {
811            App::logMsg(sprintf('Reset password failed. %s %s not found.', ucfirst($this->_auth), $user_id), LOG_NOTICE, __FILE__, __LINE__);
812            return false;
813        }
[42]814
[1]815        // Get new password.
816        $password = $this->generatePassword();
[42]817
[1]818        // Update password query.
819        $this->setPassword($user_id, $password);
[41]820
[43]821        // Make sure user has an email on record before continuing.
822        if (!isset($user_data['email']) || '' == trim($user_data['email'])) {
823            App::logMsg(sprintf('Password reset but notification failed, no email address for %s %s (%s).', $this->_auth, $user_data[$this->_params['db_primary_key']], $user_data[$this->_params['db_username_column']]), LOG_NOTICE, __FILE__, __LINE__);
824        } else {
825            // Body for email.
826            $email_body = <<<EOF
[41]827Hello {NAME},
[1]828
[41]829Your password at {SITE_NAME} has been reset. {REASON}
[1]830Your new login information is:
831
[41]832USERNAME: {USERNAME}
833PASSWORD: {PASSWORD}
[1]834
[41]835If you have any questions or concerns please reply to this email or visit the website below.
[1]836
837Thank you,
[41]838{SITE_NAME}
839{SITE_URL}/
[1]840
841EOF;
[43]842            $email = new Email(array(
843                'to' => $user_data['email'],
844                'from' => sprintf('%s <%s>', App::getParam('site_name'), App::getParam('site_email')),
845                'subject' => sprintf('%s password change', App::getParam('site_name'))
846            ));
847            $email->setString($email_body);
848            $email->replace(array(
849                'site_name' => App::getParam('site_name'),
850                'site_url' => App::getParam('site_url'),
851                'name' => ('' != $user_data['first_name'] . $user_data['last_name'] ? $user_data['first_name'] . ' ' . $user_data['last_name'] : $user_data[$this->_params['db_username_column']]),
852                'username' => $user_data[$this->_params['db_username_column']],
853                'password' => $password,
854                'reason' => $reason,
855            ));
856            $email->send();
857        }
[41]858
[15]859        return array(
[42]860            'username' => $user_data[$this->_params['db_username_column']],
[15]861            'userpass' => $password
862        );
[1]863    }
[42]864
[1]865    /**
866     * If the current user has access to the specified $security_zone, return true.
[42]867     * If the optional $priv is supplied, test that against the zone.
[1]868     *
869     * @param  constant $security_zone   string of comma delimited priviliges for the zone
870     * @param  string   $priv            a privilege that might be found in a zone
871     * @return bool     true if user is a member of security zone, false otherwise
872     */
873    function inClearanceZone($security_zone, $priv='')
874    {
875        return true;
876        $zone_members = preg_split('/,\s*/', $security_zone);
877        $priv = empty($priv) ? $this->getVal('priv') : $priv;
[42]878
879        // If the current user's privilege level is NOT in that array or if the
[1]880        // user has no privilege, return false. Otherwise the user is clear.
881        if (!in_array($priv, $zone_members) || empty($priv)) {
882            return false;
883        } else {
884            return true;
885        }
886    }
[42]887
[1]888    /**
889     * This function tests a list of arguments $security_zone against the priv that the current user has.
[42]890     * If the user doesn't have one of the supplied privs, die.
[1]891     *
892     * @param  constant $security_zone   string of comma delimited priviliges for the zone
893     */
[32]894    function requireAccessClearance($security_zone, $message='')
[1]895    {
896        return true;
897        $zone_members = preg_split('/,\s*/', $security_zone);
[42]898
899        /* If the current user's privilege level is NOT in that array or if the
[1]900         * user has no privilege, DIE with a message. */
901        if (!in_array($this->getVal('priv'), $zone_members) || !$this->getVal('priv')) {
[32]902            $message = empty($message) ? _("You have insufficient privileges to view that page.") : $message;
903            App::raiseMsg($message, MSG_NOTICE, __FILE__, __LINE__);
[1]904            App::dieBoomerangURL();
905        }
906    }
907
908} // end class
909
[51]910// CIDR cheat-sheet
[1]911//
[42]912// Netmask              Netmask (binary)                 CIDR     Notes
[1]913// _____________________________________________________________________________
914// 255.255.255.255  11111111.11111111.11111111.11111111  /32  Host (single addr)
[51]915// 255.255.255.254  11111111.11111111.11111111.11111110  /31  Unusable
[1]916// 255.255.255.252  11111111.11111111.11111111.11111100  /30    2  useable
917// 255.255.255.248  11111111.11111111.11111111.11111000  /29    6  useable
918// 255.255.255.240  11111111.11111111.11111111.11110000  /28   14  useable
919// 255.255.255.224  11111111.11111111.11111111.11100000  /27   30  useable
920// 255.255.255.192  11111111.11111111.11111111.11000000  /26   62  useable
921// 255.255.255.128  11111111.11111111.11111111.10000000  /25  126  useable
922// 255.255.255.0    11111111.11111111.11111111.00000000  /24 "Class C" 254 useable
[42]923//
[1]924// 255.255.254.0    11111111.11111111.11111110.00000000  /23    2  Class C's
925// 255.255.252.0    11111111.11111111.11111100.00000000  /22    4  Class C's
926// 255.255.248.0    11111111.11111111.11111000.00000000  /21    8  Class C's
927// 255.255.240.0    11111111.11111111.11110000.00000000  /20   16  Class C's
928// 255.255.224.0    11111111.11111111.11100000.00000000  /19   32  Class C's
929// 255.255.192.0    11111111.11111111.11000000.00000000  /18   64  Class C's
930// 255.255.128.0    11111111.11111111.10000000.00000000  /17  128  Class C's
931// 255.255.0.0      11111111.11111111.00000000.00000000  /16  "Class B"
[42]932//
[1]933// 255.254.0.0      11111111.11111110.00000000.00000000  /15    2  Class B's
934// 255.252.0.0      11111111.11111100.00000000.00000000  /14    4  Class B's
935// 255.248.0.0      11111111.11111000.00000000.00000000  /13    8  Class B's
936// 255.240.0.0      11111111.11110000.00000000.00000000  /12   16  Class B's
937// 255.224.0.0      11111111.11100000.00000000.00000000  /11   32  Class B's
938// 255.192.0.0      11111111.11000000.00000000.00000000  /10   64  Class B's
939// 255.128.0.0      11111111.10000000.00000000.00000000  /9   128  Class B's
940// 255.0.0.0        11111111.00000000.00000000.00000000  /8   "Class A"
[42]941//
[1]942// 254.0.0.0        11111110.00000000.00000000.00000000  /7
943// 252.0.0.0        11111100.00000000.00000000.00000000  /6
944// 248.0.0.0        11111000.00000000.00000000.00000000  /5
945// 240.0.0.0        11110000.00000000.00000000.00000000  /4
946// 224.0.0.0        11100000.00000000.00000000.00000000  /3
947// 192.0.0.0        11000000.00000000.00000000.00000000  /2
948// 128.0.0.0        10000000.00000000.00000000.00000000  /1
949// 0.0.0.0          00000000.00000000.00000000.00000000  /0   IP space
[223]950?>
Note: See TracBrowser for help on using the repository browser.