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

Last change on this file since 15 was 15, checked in by scdev, 19 years ago

M trunk/tests/run_tests.sh
Now can run tests without being in tests dir.

M trunk/tests/_config.inc.php
No change

M trunk/tests/Auth_SQLTest.php
...

M trunk/lib/RecordVersion.inc.php
Removed debugging.

M trunk/lib/DB.inc.php
Added die on connect error only if db_die_on_failure is true.

M trunk/lib/DBSessionHandler.inc.php
Added more accurate error-checking.

M trunk/lib/FormValidator.inc.php
Fixed email regex bugs.

M trunk/lib/SpellCheck.inc.php
Integrated lots of bug fixes from UK update.

M trunk/lib/Auth_SQL.inc.php
Lots of minor bug fixes.

M trunk/lib/App.inc.php
A couple minor bug fixes.

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