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

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