source: trunk/lib/Auth_SQL.inc.php @ 266

Last change on this file since 266 was 266, checked in by quinn, 17 years ago

Bugfixes found during strangecode site upgrade.

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