source: branches/2.0singleton/lib/Auth_SQL.inc.php @ 127

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

Updated App.inc.php thru Hierarchy.inc.php

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