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

Last change on this file since 71 was 71, checked in by scdev, 18 years ago

minor misc bug fixes

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