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

Last change on this file since 582 was 582, checked in by anonymous, 7 years ago

Created stand-alone createSession() function from code that was in the login() function (which now just calls createSession() internaly).

File size: 49.8 KB
Line 
1<?php
2/**
3 * The Strangecode Codebase - a general application development framework for PHP
4 * For details visit the project site: <http://trac.strangecode.com/codebase/>
5 * Copyright 2001-2012 Strangecode, LLC
6 *
7 * This file is part of The Strangecode Codebase.
8 *
9 * The Strangecode Codebase is free software: you can redistribute it and/or
10 * modify it under the terms of the GNU General Public License as published by the
11 * Free Software Foundation, either version 3 of the License, or (at your option)
12 * any later version.
13 *
14 * The Strangecode Codebase is distributed in the hope that it will be useful, but
15 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
16 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
17 * details.
18 *
19 * You should have received a copy of the GNU General Public License along with
20 * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
21 */
22
23/*
24* The Auth_SQL class provides a SQL implementation for authentication.
25*
26* @author  Quinn Comendant <quinn@strangecode.com>
27* @version 2.1
28*/
29
30require_once dirname(__FILE__) . '/Email.inc.php';
31
32class Auth_SQL
33{
34    // Available hash types for class Auth_SQL.
35    const ENCRYPT_PLAINTEXT = 1;
36    const ENCRYPT_CRYPT = 2;
37    const ENCRYPT_SHA1 = 3;
38    const ENCRYPT_SHA1_HARDENED = 4;
39    const ENCRYPT_MD5 = 5;
40    const ENCRYPT_MD5_HARDENED = 6;
41    const ENCRYPT_PASSWORD_BCRYPT = 7;
42    const ENCRYPT_PASSWORD_DEFAULT = 8;
43
44    // Namespace of this auth object.
45    protected $_ns;
46
47    // Static var for test.
48    protected $_authentication_tested;
49
50    // Parameters to be configured by setParam.
51    protected $_params = array();
52    protected $_default_params = array(
53
54        // Automatically create table and verify columns. Better set to false after site launch.
55        // This value is overwritten by the $app->getParam('db_create_tables') setting if it is available.
56        'create_table' => true,
57
58        // The database table containing users to authenticate.
59        'db_table' => 'user_tbl',
60
61        // The name of the primary key for the db_table.
62        'db_primary_key' => 'user_id',
63
64        // The name of the username key for the db_table.
65        'db_username_column' => 'username',
66
67        // If using the db_login_table feature, specify the db_login_table. The primary key must match the primary key for the db_table.
68        'db_login_table' => 'user_login_tbl',
69
70        // The type of hash to use for passwords stored in the db_table. Use one of the Auth_SQL::ENCRYPT_* types specified above.
71        // Hardened password hashes rely on the same key/salt being used to compare hashes.
72        // Be aware that when using one of the hardened types the App signing_key or $more_salt below cannot change!
73        'hash_type' => self::ENCRYPT_MD5,
74        'encryption_type' => null, // Backwards misnomer compatibility.
75
76        // Automatically update stored user hashes when the user next authenticates if the hash type changes (requires user_tbl with populated userpass_hashtype column).
77        'hash_type_autoupdate' => true,
78
79        // The URL to the login script.
80        'login_url' => '/',
81
82        // The maximum amount of time a user is allowed to be logged in. They will be forced to login again if they expire.
83        // In seconds. 21600 seconds = 6 hours.
84        'login_timeout' => 21600,
85
86        // 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.
87        // In seconds. 3600 seconds = 1 hour.
88        'idle_timeout' => 3600,
89
90        // 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.
91        // Days and hours, like this: 'DD:HH'
92        'login_abuse_timeframe' => '04:00',
93
94        // The number of warnings a user will receive (and their password reset each time) before their account is completely blocked.
95        'login_abuse_warnings' => 3,
96
97        // The maximum number of IP addresses a user can login with over the timeout period before their account is blocked.
98        'login_abuse_max_ips' => 5,
99
100        // The IP address subnet size threshold. Uses a CIDR notation network mask (see CIDR cheat-sheet at bottom).
101        // Any integer between 0 and 32 is permitted. Setting this to '24' permits any address in a
102        // class C network (255.255.255.0) to be considered the same. Setting to '32' compares each IP absolutely.
103        // Setting to '0' ignores all IPs, thus disabling login_abuse checking.
104        'login_abuse_ip_bitmask' => 32,
105
106        // Specify usernames to exclude from the account abuse detection system. This is specified as a hardcoded array provided at
107        // class instantiation time, or can be saved in the db_table under the login_abuse_exempt field.
108        'login_abuse_exempt_usernames' => array(),
109
110        // Specify usernames to exclude from remote_ip matching. Users behind proxy servers should be appended to this array so their shifting remote IP will not log them out.
111        'match_remote_ip_exempt_usernames' => array(),
112
113        // Match the user's current remote IP against the one they logged in with.
114        'match_remote_ip' => true,
115
116        // An array of IP blocks that are bypass the remote_ip comparison check. Useful for dynamic IPs or those behind proxy servers.
117        'trusted_networks' => array(),
118
119        // Allow user accounts to be blocked? Requires the user table to have the columns 'blocked' and 'blocked_reason'
120        'blocking' => false,
121
122        // Use a db_login_table to detect excessive logins. This requires blocking to be enabled.
123        'abuse_detection' => false,
124
125        // Allow users to save login form passwords in their browser? Setting to 'true' may pose a potential security risk.
126        'login_form_allow_autocomplete' => false,
127    );
128
129    /**
130     * Constructs a new authentication object.
131     *
132     * @access public
133     * @param optional array $params  A hash containing parameters.
134     */
135    public function __construct($namespace='')
136    {
137        $app =& App::getInstance();
138
139        $this->_ns = $namespace;
140
141        // Initialize default parameters.
142        $this->setParam($this->_default_params);
143
144        // Get create tables config from global context.
145        if (!is_null($app->getParam('db_create_tables'))) {
146            $this->setParam(array('create_table' => $app->getParam('db_create_tables')));
147        }
148
149        if (!isset($_SESSION['_auth_sql'][$this->_ns])) {
150            $this->clear();
151        }
152    }
153
154    /**
155     * Setup the database tables for this class.
156     *
157     * @access  public
158     * @author  Quinn Comendant <quinn@strangecode.com>
159     * @since   26 Aug 2005 17:09:36
160     */
161    public function initDB($recreate_db=false)
162    {
163        $app =& App::getInstance();
164        $db =& DB::getInstance();
165
166        static $_db_tested = false;
167
168        if ($recreate_db || !$_db_tested && $this->getParam('create_table')) {
169
170            // User table.
171            if ($recreate_db) {
172                $db->query("DROP TABLE IF EXISTS " . $this->getParam('db_table'));
173                $app->logMsg(sprintf('Dropping and recreating table %s.', $this->getParam('db_table')), LOG_INFO, __FILE__, __LINE__);
174            }
175
176            // The minimal columns for a table compatible with the Auth_SQL class.
177            $db->query("CREATE TABLE IF NOT EXISTS " . $db->escapeString($this->getParam('db_table')) . " (
178                " . $this->getParam('db_primary_key') . " MEDIUMINT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
179                " . $this->getParam('db_username_column') . " varchar(255) NOT NULL default '',
180                userpass VARCHAR(255) NOT NULL DEFAULT '',
181                userpass_hashtype TINYINT UNSIGNED NOT NULL DEFAULT '0',
182                first_name VARCHAR(50) NOT NULL DEFAULT '',
183                last_name VARCHAR(50) NOT NULL DEFAULT '',
184                email VARCHAR(255) NOT NULL DEFAULT '',
185                login_abuse_exempt ENUM('true') DEFAULT NULL,
186                blocked ENUM('true') DEFAULT NULL,
187                blocked_reason VARCHAR(255) NOT NULL DEFAULT '',
188                abuse_warning_level TINYINT NOT NULL DEFAULT '0',
189                seconds_online INT NOT NULL DEFAULT '0',
190                last_login_datetime DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
191                last_access_datetime DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
192                last_login_ip VARCHAR(45) NOT NULL DEFAULT '0.0.0.0',
193                added_by_user_id SMALLINT DEFAULT NULL,
194                modified_by_user_id SMALLINT DEFAULT NULL,
195                added_datetime DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
196                modified_datetime DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
197                KEY " . $this->getParam('db_username_column') . " (" . $this->getParam('db_username_column') . "),
198                KEY userpass (userpass),
199                KEY email (email),
200                KEY last_login_datetime (last_login_datetime),
201                KEY last_access_datetime (last_access_datetime)
202            )");
203
204            if (!$db->columnExists($this->getParam('db_table'), array(
205                $this->getParam('db_primary_key'),
206                $this->getParam('db_username_column'),
207                'userpass',
208                'first_name',
209                'last_name',
210                'email',
211                'login_abuse_exempt',
212                'blocked',
213                'blocked_reason',
214                'abuse_warning_level',
215                'seconds_online',
216                'last_login_datetime',
217                'last_access_datetime',
218                'last_login_ip',
219                'added_by_user_id',
220                'modified_by_user_id',
221                'added_datetime',
222                'modified_datetime',
223            ), false, false)) {
224                $app->logMsg(sprintf('Database table %s has invalid columns. Please update this table manually.', $this->getParam('db_table')), LOG_ALERT, __FILE__, __LINE__);
225                trigger_error(sprintf('Database table %s has invalid columns. Please update this table manually.', $this->getParam('db_table')), E_USER_ERROR);
226            }
227
228            // Login table is used for abuse_detection features.
229            if ($this->getParam('abuse_detection')) {
230                if ($recreate_db) {
231                    $db->query("DROP TABLE IF EXISTS " . $this->getParam('db_login_table'));
232                    $app->logMsg(sprintf('Dropping and recreating table %s.', $this->getParam('db_login_table')), LOG_INFO, __FILE__, __LINE__);
233                }
234                $db->query("CREATE TABLE IF NOT EXISTS " . $this->getParam('db_login_table') . " (
235                    " . $this->getParam('db_primary_key') . " MEDIUMINT UNSIGNED NOT NULL DEFAULT '0',
236                    login_datetime DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
237                    remote_ip_binary CHAR(32) NOT NULL DEFAULT '',
238                    KEY " . $this->getParam('db_primary_key') . " (" . $this->getParam('db_primary_key') . "),
239                    KEY login_datetime (login_datetime),
240                    KEY remote_ip_binary (remote_ip_binary)
241                )");
242
243                if (!$db->columnExists($this->getParam('db_login_table'), array(
244                    $this->getParam('db_primary_key'),
245                    'login_datetime',
246                    'remote_ip_binary',
247                ), false, false)) {
248                    $app->logMsg(sprintf('Database table %s has invalid columns. Please update this table manually.', $this->getParam('db_login_table')), LOG_ALERT, __FILE__, __LINE__);
249                    trigger_error(sprintf('Database table %s has invalid columns. Please update this table manually.', $this->getParam('db_login_table')), E_USER_ERROR);
250                }
251            }
252        }
253        $_db_tested = true;
254    }
255
256    /**
257     * Set the params of an auth object.
258     *
259     * @param  array $params   Array of parameter keys and value to set.
260     * @return bool true on success, false on failure
261     */
262    public function setParam($params)
263    {
264        $app =& App::getInstance();
265
266        if (isset($params['match_remote_ip_exempt_usernames'])) {
267            $params['match_remote_ip_exempt_usernames'] = array_map('strtolower', $params['match_remote_ip_exempt_usernames']);
268        }
269        if (isset($params['login_abuse_exempt_usernames'])) {
270            $params['login_abuse_exempt_usernames'] = array_map('strtolower', $params['login_abuse_exempt_usernames']);
271        }
272        if (isset($params['encryption_type'])) {
273            // Backwards misnomer compatibility.
274            $params['hash_type'] = $params['encryption_type'];
275        }
276        if (isset($params['hash_type']) && version_compare(PHP_VERSION, '5.5.0', '<') && in_array($params['hash_type'], array(self::ENCRYPT_PASSWORD_BCRYPT, self::ENCRYPT_PASSWORD_DEFAULT))) {
277            // These hash types require the password_* userland lib in PHP < 5.5.0
278            $pw_compat_lib = 'vendor/ircmaxell/password-compat/lib/password.php';
279            if (false !== stream_resolve_include_path($pw_compat_lib)) {
280                include_once $pw_compat_lib;
281            } else {
282                $app->logMsg(sprintf('Hash type %s requires password-compat lib in PHP < 5.5.0; falling back to ENCRYPT_SHA1_HARDENED', $params['hash_type']), LOG_ERR, __FILE__, __LINE__);
283                $params['hash_type'] = self::ENCRYPT_SHA1_HARDENED;
284            }
285        }
286        if (isset($params['hash_type']) && !in_array($params['hash_type'], array(self::ENCRYPT_PLAINTEXT, self::ENCRYPT_CRYPT, self::ENCRYPT_SHA1, self::ENCRYPT_SHA1_HARDENED, self::ENCRYPT_MD5, self::ENCRYPT_MD5_HARDENED, self::ENCRYPT_PASSWORD_BCRYPT, self::ENCRYPT_PASSWORD_DEFAULT))) {
287            $app->logMsg(sprintf('Invalid hash type %s; falling back to ENCRYPT_SHA1_HARDENED', $params['hash_type']), LOG_ERR, __FILE__, __LINE__);
288            $params['hash_type'] = self::ENCRYPT_SHA1_HARDENED;
289        }
290        if (isset($params) && is_array($params)) {
291            // Merge new parameters with old overriding only those passed.
292            $this->_params = array_merge($this->_params, $params);
293        }
294    }
295
296    /**
297     * Return the value of a parameter, if it exists.
298     *
299     * @access public
300     * @param string $param        Which parameter to return.
301     * @return mixed               Configured parameter value.
302     */
303    public function getParam($param)
304    {
305        $app =& App::getInstance();
306
307        if (array_key_exists($param, $this->_params)) {
308            return $this->_params[$param];
309        } else {
310            $app->logMsg(sprintf('Parameter is not set: %s', $param), LOG_DEBUG, __FILE__, __LINE__);
311            return null;
312        }
313    }
314
315    /**
316     * Clear any authentication tokens in the current session. A.K.A. logout.
317     *
318     * @access public
319     */
320    public function clear()
321    {
322        $db =& DB::getInstance();
323
324        if ($this->get('user_id', false)) {
325
326            $this->initDB();
327
328            // FIX ME: Should we check if the session is active?
329            $db->query("
330                UPDATE " . $this->_params['db_table'] . " SET
331                seconds_online = seconds_online + ABS(UNIX_TIMESTAMP() - UNIX_TIMESTAMP(last_access_datetime)),
332                last_login_datetime = '0000-00-00 00:00:00'
333                WHERE " . $this->_params['db_primary_key'] . " = '" . $this->get('user_id') . "'
334            ");
335        }
336        $_SESSION['_auth_sql'][$this->_ns] = array(
337            'authenticated'         => false,
338            'user_id'               => null,
339            'username'              => null,
340            'login_datetime'        => null,
341            'last_access_datetime'  => null,
342            'remote_ip'             => getRemoteAddr(),
343            'login_abuse_exempt'    => null,
344            'match_remote_ip_exempt'=> null,
345            'user_data'             => null,
346        );
347    }
348
349    /**
350     * Sets a variable into a registered auth session.
351     *
352     * @access public
353     * @param mixed $key      Which value to set.
354     * @param mixed $val      Value to set variable to.
355     */
356    public function set($key, $val)
357    {
358        if (!isset($_SESSION['_auth_sql'][$this->_ns]['user_data'])) {
359            $_SESSION['_auth_sql'][$this->_ns]['user_data'] = array();
360        }
361        $_SESSION['_auth_sql'][$this->_ns]['user_data'][$key] = $val;
362    }
363
364    /**
365     * Returns a specified value from a registered auth session.
366     *
367     * @access public
368     * @param mixed $key      Which value to return.
369     * @param mixed $default  Value to return if key not found in user_data.
370     * @return mixed          Value stored in session.
371     */
372    public function get($key, $default='')
373    {
374        if (isset($_SESSION['_auth_sql'][$this->_ns][$key])) {
375            return $_SESSION['_auth_sql'][$this->_ns][$key];
376        } else if (isset($_SESSION['_auth_sql'][$this->_ns]['user_data'][$key])) {
377            return $_SESSION['_auth_sql'][$this->_ns]['user_data'][$key];
378        } else {
379            return $default;
380        }
381    }
382
383    /**
384     * Retrieve and verify the given username and password against a matching user record in the database.
385     *
386     * @access private
387     * @param string $username      The username to check.
388     * @param string $password      The password to compare to username.
389     * @return mixed  False if credentials not found in DB, or returns DB row matching credentials.
390     */
391    public function authenticate($username, $password)
392    {
393        $app =& App::getInstance();
394        $db =& DB::getInstance();
395
396        $this->initDB();
397
398        // Get user data for specified username.
399        $qid = $db->query("
400            SELECT *, " . $this->_params['db_primary_key'] . " AS user_id
401            FROM " . $this->_params['db_table'] . "
402            WHERE " . $this->_params['db_username_column'] . " = '" . $db->escapeString($username) . "'
403        ");
404        if (!$user_data = mysql_fetch_assoc($qid)) {
405            $app->logMsg(sprintf('Username %s not found for authentication', $username), LOG_NOTICE, __FILE__, __LINE__);
406            return false;
407        }
408
409        // Check given password against hashed DB password.
410        $old_hash_type = isset($user_data['userpass_hashtype']) && !empty($user_data['userpass_hashtype']) ? $user_data['userpass_hashtype'] : $this->getParam('hash_type');
411        if ($this->verifyPassword($password, $user_data['userpass'], $old_hash_type)) {
412            $app->logMsg(sprintf('Authentication successful for %s (user_id=%s)', $username, $user_data['user_id']), LOG_INFO, __FILE__, __LINE__);
413            unset($user_data['userpass']); // Avoid revealing the encrypted password in the $user_data.
414            if ($this->getParam('hash_type_autoupdate') && $old_hash_type != $this->getParam('hash_type')) {
415                // Let's update user's password hash to new type (just run setPassword with this authenticated password
).
416                $this->setPassword($user_data['user_id'], $password);
417                $app->logMsg(sprintf('User %s password hash type updated from %s to %s', $username, $old_hash_type, $this->getParam('hash_type')), LOG_INFO, __FILE__, __LINE__);
418            }
419            return $user_data;
420        }
421
422        $app->logMsg(sprintf('Authentication failed for %s (user_id=%s)', $username, $user_data['user_id']), LOG_NOTICE, __FILE__, __LINE__);
423        return false;
424    }
425
426    /**
427     * Check username and password, and create new session if authenticated.
428     *
429     * @access private
430     * @param string $username     The username to check.
431     * @param string $password     The password to compare for username.
432     * @return boolean  Whether or not the credentials are valid.
433     */
434    public function login($username, $password)
435    {
436        $app =& App::getInstance();
437        $db =& DB::getInstance();
438
439        if ($user_data = $this->authenticate($username, $password)) {
440            // The credentials match. Now setup the session.
441            return $this->createSession($user_data);
442        }
443        // No login: failed authentication!
444        return false;
445    }
446
447    /**
448     * Create new login session for given user.
449     *
450     * @access private
451     * @param string $user_data User data that is normally returned from this->authenticate(). If provided manually:
452     *                          Required array values:
453     *                              'user_id' => '1'
454     *                              'username' => 'name'
455     *                          Optional array values:
456     *                              'match_remote_ip_exempt' => true
457     *                              'login_abuse_exempt' => true
458     *                              'abuse_warning_level' => true
459     *                              'blocked' => true
460     *                              'blocked_reason' => ''
461     *                              '
' => '
' (any other values that should be retrievable via this->get())
462     * @return boolean          Whether or not the session was created. It will return true unless abuse detection is enabled and triggered.
463     */
464    public function createSession($user_data)
465    {
466        $app =& App::getInstance();
467        $db =& DB::getInstance();
468
469        $this->initDB();
470
471        $this->clear();
472
473        // Convert 'priv' to 'user_type' nomenclature to support older implementations.
474        if (isset($user_data['priv'])) {
475            $user_data['user_type'] = $user_data['priv'];
476        }
477
478        // Register authenticated session.
479        $_SESSION['_auth_sql'][$this->_ns] = array(
480            'authenticated'         => true,
481            'user_id'               => $user_data['user_id'],
482            'username'              => $user_data['username'],
483            'login_datetime'        => date('Y-m-d H:i:s'),
484            'last_access_datetime'  => date('Y-m-d H:i:s'),
485            'remote_ip'             => getRemoteAddr(),
486            'login_abuse_exempt'    => isset($user_data['login_abuse_exempt']) ? !empty($user_data['login_abuse_exempt']) : in_array(strtolower($user_data['username']), $this->_params['login_abuse_exempt_usernames']),
487            'match_remote_ip_exempt'=> isset($user_data['match_remote_ip_exempt']) ? !empty($user_data['match_remote_ip_exempt']) : in_array(strtolower($user_data['username']), $this->_params['match_remote_ip_exempt_usernames']),
488            'user_data'             => $user_data
489        );
490
491        /**
492         * Check if the account is blocked, respond in context to reason. Cancel the login if blocked.
493         */
494        if ($this->getParam('blocking')) {
495            if (isset($user_data['blocked']) && !empty($user_data['blocked'])) {
496                switch ($this->get('blocked_reason')) {
497                case 'account abuse' :
498                    $app->raiseMsg(sprintf(_("This account has been blocked due to possible account abuse. Please contact an administrator to reactivate."), null), MSG_WARNING, __FILE__, __LINE__);
499                    break;
500                default :
501                    $app->raiseMsg(sprintf(_("This account is currently not active. %s"), $this->get('blocked_reason')), MSG_WARNING, __FILE__, __LINE__);
502                    break;
503                }
504
505                // No login: user is blocked!
506                $app->logMsg(sprintf('User_id %s (%s) login failed due to blocked account: %s', $this->get('user_id'), $this->get('username'), $this->get('blocked_reason')), LOG_NOTICE, __FILE__, __LINE__);
507                $this->clear();
508                return false;
509            }
510        }
511
512        /**
513         * Check the db_login_table for too many logins under this account.
514         * (1) Count the number of unique IP addresses that logged in under this user within the login_abuse_timeframe
515         * (2) If this number exceeds the login_abuse_max_ips, assume multiple people are logging in under the same account.
516        **/
517        // TODO: make this ipv6 compatible. At the moment, ipv6 addresses are converted into zero for remote_ip_binary.
518        // http://www.highonphp.com/5-tips-for-working-with-ipv6-in-php
519        // https://stackoverflow.com/questions/444966/working-with-ipv6-addresses-in-php
520        if ($this->getParam('abuse_detection') && !$this->get('login_abuse_exempt')) {
521            $qid = $db->query("
522                SELECT COUNT(DISTINCT LEFT(remote_ip_binary, " . $this->_params['login_abuse_ip_bitmask'] . "))
523                FROM " . $this->_params['db_login_table'] . "
524                WHERE " . $this->_params['db_primary_key'] . " = '" . $this->get('user_id') . "'
525                AND DATE_ADD(login_datetime, INTERVAL '" . $this->_params['login_abuse_timeframe'] . "' DAY_HOUR) > NOW()
526            ");
527            list($distinct_ips) = mysql_fetch_row($qid);
528            if ($distinct_ips > $this->_params['login_abuse_max_ips']) {
529                if ($this->get('abuse_warning_level') < $this->_params['login_abuse_warnings']) {
530                    // Warn the user with a password reset.
531                    $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 credentials with others. If further account abuse is detected this account will be blocked."));
532                    $app->raiseMsg(_("Your password has been reset as a security precaution. Please check your email for more information."), MSG_NOTICE, __FILE__, __LINE__);
533                    $app->logMsg(sprintf('Account abuse detected for user_id %s (%s) from IP %s', $this->get('user_id'), $this->get('username'), $this->get('remote_ip')), LOG_WARNING, __FILE__, __LINE__);
534                } else {
535                    // Block the account with the reason of account abuse.
536                    $this->blockAccount(null, 'account abuse');
537                    $app->raiseMsg(_("Your account has been blocked as a security precaution. Please contact us for more information."), MSG_NOTICE, __FILE__, __LINE__);
538                    $app->logMsg(sprintf('Account blocked for user_id %s (%s) from IP %s', $this->get('user_id'), $this->get('username'), $this->get('remote_ip')), LOG_ALERT, __FILE__, __LINE__);
539                }
540                // Increment user's warning level.
541                $db->query("UPDATE " . $this->_params['db_table'] . " SET abuse_warning_level = abuse_warning_level + 1 WHERE " . $this->_params['db_primary_key'] . " = '" . $this->get('user_id') . "'");
542                // Reset the login counter for this user.
543                $db->query("DELETE FROM " . $this->_params['db_login_table'] . " WHERE " . $this->_params['db_primary_key'] . " = '" . $this->get('user_id') . "'");
544                // No login: reset password because of account abuse!
545                $this->clear();
546                return false;
547            }
548
549            // Update the login counter table with this login access. Convert IP to binary.
550            // TODO: this query could benefit from INSERT DELAYED.
551            $db->query("
552                INSERT INTO " . $this->_params['db_login_table'] . " (
553                    " . $this->_params['db_primary_key'] . ",
554                    login_datetime,
555                    remote_ip_binary
556                ) VALUES (
557                    '" . $this->get('user_id') . "',
558                    '" . $this->get('login_datetime') . "',
559                    '" . sprintf('%032b', ip2long($this->get('remote_ip'))) . "'
560                )
561            ");
562        }
563
564        // Update user table with this login.
565        $db->query("
566            UPDATE " . $this->_params['db_table'] . " SET
567                last_login_datetime = '" . $this->get('login_datetime') . "',
568                last_access_datetime = '" . $this->get('login_datetime') . "',
569                last_login_ip = '" . $this->get('remote_ip') . "'
570            WHERE " . $this->_params['db_primary_key'] . " = '" . $this->get('user_id') . "'
571        ");
572
573        // Session created! We're logged-in!
574        return true;
575    }
576
577    /**
578     * Test if user has a currently logged-in session.
579     *  - authentication flag set to true
580     *  - username not empty
581     *  - total logged-in time is not greater than login_timeout
582     *  - idle time is not greater than idle_timeout
583     *  - remote address is the same as the login remote address
584     *
585     * @access public
586     */
587    public function isLoggedIn($user_id=null)
588    {
589        $app =& App::getInstance();
590        $db =& DB::getInstance();
591
592        $this->initDB();
593
594        if (isset($user_id)) {
595            // Check the login status of a specific user.
596            $qid = $db->query("
597                SELECT 1 FROM " . $this->_params['db_table'] . "
598                WHERE " . $this->_params['db_primary_key'] . " = '" . $db->escapeString($user_id) . "'
599                AND last_login_datetime > DATE_SUB(NOW(), INTERVAL '" . $this->_params['login_timeout'] . "' SECOND)
600                AND last_access_datetime > DATE_SUB(NOW(), INTERVAL '" . $this->_params['idle_timeout'] . "' SECOND)
601            ");
602            $login_status = (mysql_num_rows($qid) > 0);
603            $app->logMsg(sprintf('Returning %s login status for user_id %s', ($login_status ? 'true' : 'false'), $user_id), LOG_DEBUG, __FILE__, __LINE__);
604            return $login_status;
605        }
606
607        // User login test need only be run once per script execution. We cache the result in the session.
608        if ($this->_authentication_tested && isset($_SESSION['_auth_sql'][$this->_ns]['authenticated'])) {
609            $app->logMsg(sprintf('Returning cached authentication status: %s', ($_SESSION['_auth_sql'][$this->_ns]['authenticated'] ? 'true' : 'false')), LOG_DEBUG, __FILE__, __LINE__);
610            return $_SESSION['_auth_sql'][$this->_ns]['authenticated'];
611        }
612
613        // Testing login should occur once. This is the first time. Set flag.
614        $this->_authentication_tested = true;
615
616        // Some users will access from networks with a changing IP number (i.e. behind a proxy server).
617        // These users must be allowed entry by adding their IP to the list of trusted_networks, or their usernames to the list of match_remote_ip_exempt_usernames.
618        if ($trusted_net = ipInRange(getRemoteAddr(), $this->_params['trusted_networks'])) {
619            $user_in_trusted_network = true;
620            $app->logMsg(sprintf('User_id %s accessing from trusted network %s',
621                ($this->get('user_id') ? $this->get('user_id') . ' (' .  $this->get('username') . ')' : 'unknown'),
622                $trusted_net
623            ), LOG_DEBUG, __FILE__, __LINE__);
624        } else {
625            $user_in_trusted_network = false;
626        }
627
628        // Do we match the user's remote IP at all? Yes, if set in config and not disabled for specific user.
629        if ($this->getParam('match_remote_ip') && !$this->get('match_remote_ip_exempt')) {
630            $remote_ip_is_matched = (isset($_SESSION['_auth_sql'][$this->_ns]['remote_ip']) && $_SESSION['_auth_sql'][$this->_ns]['remote_ip'] == getRemoteAddr()) || $user_in_trusted_network;
631        } else {
632            $app->logMsg(sprintf('User_id %s exempt from remote_ip match (comparing %s == %s)',
633                ($this->get('user_id') ? $this->get('user_id') . ' (' .  $this->get('username') . ')' : 'unknown'),
634                $_SESSION['_auth_sql'][$this->_ns]['remote_ip'],
635                getRemoteAddr()
636            ), LOG_DEBUG, __FILE__, __LINE__);
637            $remote_ip_is_matched = true;
638        }
639
640        // Test login with information stored in session. Skip IP matching for users from trusted networks.
641        if (isset($_SESSION['_auth_sql'][$this->_ns]['authenticated'])
642            && true === $_SESSION['_auth_sql'][$this->_ns]['authenticated']
643            && isset($_SESSION['_auth_sql'][$this->_ns]['username'])
644            && !empty($_SESSION['_auth_sql'][$this->_ns]['username'])
645            && isset($_SESSION['_auth_sql'][$this->_ns]['login_datetime'])
646            && strtotime($_SESSION['_auth_sql'][$this->_ns]['login_datetime']) > (time() - $this->_params['login_timeout'])
647            && isset($_SESSION['_auth_sql'][$this->_ns]['last_access_datetime'])
648            && strtotime($_SESSION['_auth_sql'][$this->_ns]['last_access_datetime']) > (time() - $this->_params['idle_timeout'])
649            && $remote_ip_is_matched
650        ) {
651            // User is authenticated!
652            $_SESSION['_auth_sql'][$this->_ns]['last_access_datetime'] = date('Y-m-d H:i:s');
653
654            // Update the DB with the last_access_datetime and increment the seconds_online.
655            $db->query("
656                UPDATE " . $this->_params['db_table'] . " SET
657                seconds_online = seconds_online + ABS(UNIX_TIMESTAMP() - UNIX_TIMESTAMP(last_access_datetime)) + 1,
658                last_access_datetime = '" . $this->get('last_access_datetime') . "'
659                WHERE " . $this->_params['db_primary_key'] . " = '" . $this->get('user_id') . "'
660            ");
661            if (mysql_affected_rows($db->getDBH()) > 0) {
662                // 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.
663                return true;
664            } else {
665                $app->logMsg(sprintf('Session update failed; record not found for user_id %s (%s).', $this->get('user_id'), $this->get('username')), LOG_NOTICE, __FILE__, __LINE__);
666            }
667        } else if (isset($_SESSION['_auth_sql'][$this->_ns]['authenticated']) && true === $_SESSION['_auth_sql'][$this->_ns]['authenticated']) {
668            // User is authenticated, but login has expired.
669            if (strtotime($_SESSION['_auth_sql'][$this->_ns]['last_access_datetime']) > (time() - 43200)) {
670                // Only raise message if last session is less than 12 hours old.
671                $app->raiseMsg(sprintf(_("For your safety we logged you out after being idle for %s. Please log in again."), humanTime($this->_params['idle_timeout'], 'hour', '%01.0f')), MSG_NOTICE, __FILE__, __LINE__);
672            }
673
674            // Log the reason for login expiration.
675            $expire_reasons = array();
676            if (!isset($_SESSION['_auth_sql'][$this->_ns]['username']) || empty($_SESSION['_auth_sql'][$this->_ns]['username'])) {
677                $expire_reasons[] = 'username not found';
678            }
679            if (!isset($_SESSION['_auth_sql'][$this->_ns]['login_datetime']) || strtotime($_SESSION['_auth_sql'][$this->_ns]['login_datetime']) <= (time() - $this->_params['login_timeout'])) {
680                $expire_reasons[] = sprintf('login_timeout expired (%s older than %s seconds ago)', $_SESSION['_auth_sql'][$this->_ns]['login_datetime'], $this->_params['login_timeout']);
681            }
682            if (!isset($_SESSION['_auth_sql'][$this->_ns]['last_access_datetime']) || strtotime($_SESSION['_auth_sql'][$this->_ns]['last_access_datetime']) <= (time() - $this->_params['idle_timeout'])) {
683                $expire_reasons[] = sprintf('idle_timeout expired (%s older than %s seconds ago)', $_SESSION['_auth_sql'][$this->_ns]['last_access_datetime'], $this->_params['idle_timeout']);
684            }
685            if (!isset($_SESSION['_auth_sql'][$this->_ns]['remote_ip']) || $_SESSION['_auth_sql'][$this->_ns]['remote_ip'] != getRemoteAddr()) {
686                if ($this->getParam('match_remote_ip') && !$this->get('match_remote_ip_exempt') && !$user_in_trusted_network) {
687                    // There are three cases when a remote IP match will be the cause of a session termination:
688                    //   1. match_remote_ip config is enabled
689                    //   2. user is not match_remote_ip_exempt (set in the user_data, or in the match_remote_ip_exempt_usernames list)
690                    //   3. the user is connecting from a trusted network (their IP is listed in the trusted_networks)
691                    $expire_reasons[] = sprintf('remote_ip not matched (%s != %s)', $_SESSION['_auth_sql'][$this->_ns]['remote_ip'], getRemoteAddr());
692                } else {
693                    $expire_reasons[] = sprintf('remote_ip not matched but user was exempt from this check (%s != %s)', $_SESSION['_auth_sql'][$this->_ns]['remote_ip'], getRemoteAddr());
694                }
695            }
696            $app->logMsg(sprintf('User_id %s (%s) session expired: %s', $this->get('user_id'), $this->get('username'), join(', ', $expire_reasons)), LOG_INFO, __FILE__, __LINE__);
697        } else {
698            $app->logMsg('Session is not authenticated', LOG_DEBUG, __FILE__, __LINE__);
699        }
700
701        // User is not authenticated.
702        $this->clear();
703        return false;
704    }
705
706    /**
707     * Redirect user to login page if they are not logged in.
708     *
709     * @param string $message The text description of a message to raise.
710     * @param int    $type    The type of message: MSG_NOTICE,
711     *                        MSG_SUCCESS, MSG_WARNING, or MSG_ERR.
712     * @param string $file    __FILE__.
713     * @param string $line    __LINE__.
714     * @access public
715     */
716    public function requireLogin($message='', $type=MSG_NOTICE, $file=null, $line=null)
717    {
718        $app =& App::getInstance();
719
720        if (!$this->isLoggedIn()) {
721            // Display message for requiring login. (RaiseMsg will ignore empty strings.)
722            if ('' != $message) {
723                $app->raiseMsg($message, $type, $file, $line);
724            }
725
726            // Login scripts must have the same 'login' tag for boomerangURL verification/manipulation.
727            $app->setBoomerangURL(absoluteMe(), 'login');
728            $app->dieURL($this->_params['login_url']);
729        }
730    }
731
732    /**
733     * This sets the 'blocked' field for a user in the db_table, and also
734     * adds an optional reason
735     *
736     * @param  string   $reason      The reason for blocking the account.
737     */
738    public function blockAccount($user_id=null, $reason='')
739    {
740        $app =& App::getInstance();
741        $db =& DB::getInstance();
742
743        $this->initDB();
744
745        if ($this->getParam('blocking')) {
746            if (mb_strlen($db->escapeString($reason)) > 255) {
747                // blocked_reason field is varchar(255).
748                $app->logMsg(sprintf('Blocked reason provided is greater than 255 characters: %s', $reason), LOG_WARNING, __FILE__, __LINE__);
749            }
750
751            // Get user_id if specified.
752            $user_id = isset($user_id) ? $user_id : $this->get('user_id');
753            $db->query("
754                UPDATE " . $this->_params['db_table'] . " SET
755                blocked = 'true',
756                blocked_reason = '" . $db->escapeString($reason) . "'
757                WHERE " . $this->_params['db_primary_key'] . " = '" . $db->escapeString($user_id) . "'
758            ");
759        }
760    }
761
762    /**
763     * Tests if the "blocked" flag is set for a user.
764     *
765     * @param  int      $user_id    User id to look for.
766     * @return boolean              True if the user is blocked, false otherwise.
767     */
768    public function isBlocked($user_id=null)
769    {
770        $db =& DB::getInstance();
771
772        $this->initDB();
773
774        if ($this->getParam('blocking')) {
775            // Get user_id if specified.
776            $user_id = isset($user_id) ? $user_id : $this->getVal('user_id');
777            $qid = $db->query("
778                SELECT 1
779                FROM " . $this->_params['db_table'] . "
780                WHERE blocked = 'true'
781                AND " . $this->_params['db_primary_key'] . " = '" . $db->escapeString($user_id) . "'
782            ");
783            return mysql_num_rows($qid) === 1;
784        }
785    }
786
787    /**
788     * Unblocks a user in the db_table, and clears any blocked_reason.
789     */
790    public function unblockAccount($user_id=null)
791    {
792        $db =& DB::getInstance();
793
794        $this->initDB();
795
796        if ($this->getParam('blocking')) {
797            // Get user_id if specified.
798            $user_id = isset($user_id) ? $user_id : $this->get('user_id');
799            $db->query("
800                UPDATE " . $this->_params['db_table'] . " SET
801                blocked = '',
802                blocked_reason = ''
803                WHERE " . $this->_params['db_primary_key'] . " = '" . $db->escapeString($user_id) . "'
804            ");
805        }
806    }
807
808    /**
809     * Returns true if username already exists in database.
810     *
811     * @param  string  $username    Username to look for.
812     * @return bool                 True if username exists.
813     */
814    public function usernameExists($username)
815    {
816        $db =& DB::getInstance();
817
818        $this->initDB();
819
820        $qid = $db->query("
821            SELECT 1
822            FROM " . $this->_params['db_table'] . "
823            WHERE " . $this->_params['db_username_column'] . " = '" . $db->escapeString($username) . "'
824        ");
825        return (mysql_num_rows($qid) > 0);
826    }
827
828    /**
829     * Returns a username for a specified user id.
830     *
831     * @param  string  $user_id     User id to look for.
832     * @return string               Username, or false if none found.
833     */
834    public function getUsername($user_id)
835    {
836        $db =& DB::getInstance();
837
838        $this->initDB();
839
840        $qid = $db->query("
841            SELECT " . $this->_params['db_username_column'] . "
842            FROM " . $this->_params['db_table'] . "
843            WHERE " . $this->_params['db_primary_key'] . " = '" . $db->escapeString($user_id) . "'
844        ");
845        if (list($username) = mysql_fetch_row($qid)) {
846            return $username;
847        } else {
848            return false;
849        }
850    }
851
852    /*
853    * Generate a cryptographically secure, random password.
854    *
855    * @access   public
856    * @param    int  $bytes     Length of password (in bytes)
857    * @return   string          Random string of characters.
858    * @author   Quinn Comendant <quinn@strangecode.com>
859    * @version  1.0
860    * @since    15 Nov 2014 20:30:27
861    */
862    public function generatePassword($bytes=10)
863    {
864        $app =& App::getInstance();
865
866        $bytes = is_numeric($bytes) ? $bytes : 10;
867        $string = strtok(base64_encode(openssl_random_pseudo_bytes($bytes, $strong)), '=');
868        if (!$strong) {
869            $app->logMsg(sprintf('Password generated was not "cryptographically strong"; check your openssl.', null), LOG_NOTICE, __FILE__, __LINE__);
870        }
871
872        return $string;
873    }
874
875    /**
876     *
877     */
878    public function encryptPassword($password, $salt=null, $hash_type=null)
879    {
880        $app =& App::getInstance();
881
882        $password = (string)$password;
883
884        // Existing password hashes rely on the same key/salt being used to compare hashs.
885        // Don't change this (or the value applied to signing_key) unless you know existing hashes or signatures will not be affected!
886        $more_salt = 'B36D18E5-3FE4-4D58-8150-F26642852B81';
887
888        $hash_type = isset($hash_type) && !empty($hash_type) ? $hash_type : $this->getParam('hash_type');
889
890        switch ($hash_type) {
891        case self::ENCRYPT_PLAINTEXT :
892            $encrypted_password = $password;
893            break;
894
895        case self::ENCRYPT_CRYPT :
896            // If comparing password with an existing hashed password, provide the hashed password as the salt.
897            $encrypted_password = isset($salt) ? crypt($password, $salt) : crypt($password);
898            break;
899
900        case self::ENCRYPT_SHA1 :
901            $encrypted_password = sha1($password);
902            break;
903
904        case self::ENCRYPT_SHA1_HARDENED :
905            $encrypted_password = sha1($app->getParam('signing_key') . $password . $more_salt);
906            for ($i=0; $i < pow(2, 20); $i++) {
907                $encrypted_password = sha1($password . $encrypted_password);
908            }
909            break;
910
911        case self::ENCRYPT_MD5 :
912            $encrypted_password = md5($password);
913            break;
914
915        case self::ENCRYPT_MD5_HARDENED :
916            $encrypted_password = md5($app->getParam('signing_key') . $password . $more_salt);
917            for ($i=0; $i < pow(2, 20); $i++) {
918                $encrypted_password = md5($password . $encrypted_password);
919            }
920            break;
921
922        case self::ENCRYPT_PASSWORD_BCRYPT :
923            $encrypted_password = password_hash($password, PASSWORD_BCRYPT, array('cost' => 12));
924            break;
925
926        case self::ENCRYPT_PASSWORD_DEFAULT :
927            $encrypted_password = password_hash($password, PASSWORD_DEFAULT, array('cost' => 12));
928            break;
929
930        default :
931            $app->logMsg(sprintf('Unknown hash type: %s', $hash_type), LOG_WARNING, __FILE__, __LINE__);
932            return false;
933        }
934
935        // In case our hashing function returns 'false' or another empty value, bail out.
936        if ('' == trim((string)$encrypted_password)) {
937            $app->logMsg(sprintf('Invalid password hash returned ("%s") for hash type %s; check yo crypto!', $encrypted_password, $hash_type), LOG_ALERT, __FILE__, __LINE__);
938            return false;
939        }
940
941        return $encrypted_password;
942    }
943
944    /*
945    *
946    *
947    * @access   public
948    * @param
949    * @return
950    * @author   Quinn Comendant <quinn@strangecode.com>
951    * @version  1.0
952    * @since    15 Nov 2014 21:37:28
953    */
954    public function verifyPassword($password, $encrypted_password, $hash_type=null)
955    {
956        $app =& App::getInstance();
957
958        $hash_type = isset($hash_type) && !empty($hash_type) ? $hash_type : $this->getParam('hash_type');
959
960        switch ($hash_type) {
961        case self::ENCRYPT_CRYPT :
962            return $this->encryptPassword($password, $encrypted_password, $hash_type) == $encrypted_password;
963
964        case self::ENCRYPT_PLAINTEXT :
965        case self::ENCRYPT_MD5 :
966        case self::ENCRYPT_MD5_HARDENED :
967        case self::ENCRYPT_SHA1 :
968        case self::ENCRYPT_SHA1_HARDENED :
969            return $this->encryptPassword($password, $encrypted_password, $hash_type) == $encrypted_password;
970
971        case self::ENCRYPT_PASSWORD_BCRYPT :
972        case self::ENCRYPT_PASSWORD_DEFAULT :
973            return password_verify($password, $encrypted_password);
974
975        default :
976            $app->logMsg(sprintf('Unknown hash type: %s', $hash_type), LOG_WARNING, __FILE__, __LINE__);
977            return false;
978        }
979
980    }
981
982    /**
983     *
984     */
985    public function setPassword($user_id=null, $password, $hash_type=null)
986    {
987        $app =& App::getInstance();
988        $db =& DB::getInstance();
989
990        $this->initDB();
991
992        // Get user_id if specified.
993        $user_id = isset($user_id) ? $user_id : $this->get('user_id');
994
995        // New hash type.
996        $hash_type = isset($hash_type) ? $hash_type : $this->getParam('hash_type');
997
998        // Save the hash method used if a table exists for it.
999        $userpass_hashtype_clause = '';
1000        if ($db->columnExists($this->_params['db_table'], 'userpass_hashtype', false)) {
1001            $userpass_hashtype_clause = ", userpass_hashtype = '" . $db->escapeString($hash_type) . "'";
1002        }
1003
1004        // Issue the password change query.
1005        $db->query("
1006            UPDATE " . $this->_params['db_table'] . " SET
1007                userpass = '" . $db->escapeString($this->encryptPassword($password, null, $hash_type)) . "',
1008                modified_datetime = NOW(),
1009                modified_by_user_id = '" . $db->escapeString($user_id) . "'
1010                $userpass_hashtype_clause
1011            WHERE " . $this->_params['db_primary_key'] . " = '" . $db->escapeString($user_id) . "'
1012        ");
1013
1014        if (mysql_affected_rows($db->getDBH()) != 1) {
1015            $app->logMsg(sprintf('Failed to update password for user_id %s (no affected rows)', $user_id), LOG_WARNING, __FILE__, __LINE__);
1016            return false;
1017        }
1018
1019        $app->logMsg(sprintf('Password change successful for user_id %s', $user_id), LOG_INFO, __FILE__, __LINE__);
1020        return true;
1021    }
1022
1023    /**
1024     * Resets the password for the user with the specified id.
1025     *
1026     * @param  string $user_id   The id of the user to reset.
1027     * @param  string $reason    Additional message to add to the reset email.
1028     * @return string            The user's new password.
1029     */
1030    public function resetPassword($user_id=null, $reason='')
1031    {
1032        $app =& App::getInstance();
1033        $db =& DB::getInstance();
1034
1035        $this->initDB();
1036
1037        // Get user_id if specified.
1038        $user_id = isset($user_id) ? $user_id : $this->get('user_id');
1039
1040        // Reset password of a specific user.
1041        $qid = $db->query("
1042            SELECT * FROM " . $this->_params['db_table'] . "
1043            WHERE " . $this->_params['db_primary_key'] . " = '" . $db->escapeString($user_id) . "'
1044        ");
1045        if (!$user_data = mysql_fetch_assoc($qid)) {
1046            $app->logMsg(sprintf('Reset password failed. User_id %s not found.', $user_id), LOG_NOTICE, __FILE__, __LINE__);
1047            return false;
1048        }
1049
1050        // Get new password.
1051        $password = $this->generatePassword();
1052
1053        // Update password query.
1054        $this->setPassword($user_id, $password);
1055
1056        // Make sure user has an email on record before continuing.
1057        if (!isset($user_data['email']) || '' == trim($user_data['email'])) {
1058            $app->logMsg(sprintf('Password reset but notification failed, no email address for user_id %s (%s).', $user_data[$this->_params['db_primary_key']], $user_data[$this->_params['db_username_column']]), LOG_NOTICE, __FILE__, __LINE__);
1059        } else {
1060            // Send the new password in an email.
1061            $email = new Email(array(
1062                'to' => $user_data['email'],
1063                'from' => sprintf('"%s" <%s>', addcslashes($app->getParam('site_name'), '"'), $app->getParam('site_email')),
1064                'subject' => sprintf('%s password change', $app->getParam('site_name'))
1065            ));
1066            $email->setTemplate('codebase/services/templates/email_reset_password.txt');
1067            $email->replace(array(
1068                'SITE_NAME' => $app->getParam('site_name'),
1069                'SITE_URL' => $app->getParam('site_url'),
1070                'SITE_EMAIL' => $app->getParam('site_email'),
1071                'NAME' => ('' != $user_data['first_name'] . $user_data['last_name'] ? $user_data['first_name'] . ' ' . $user_data['last_name'] : $user_data[$this->_params['db_username_column']]),
1072                'USERNAME' => $user_data[$this->_params['db_username_column']],
1073                'PASSWORD' => $password,
1074                'REASON' => ('' == trim($reason) ? '' : trim($reason) . ' '), // Add a space after the reason if it exists for better formatting.
1075            ));
1076            $email->send();
1077        }
1078
1079        return array(
1080            'username' => $user_data[$this->_params['db_username_column']],
1081            'userpass' => $password
1082        );
1083    }
1084
1085} // end class
Note: See TracBrowser for help on using the repository browser.