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