Index: trunk/bin/module_maker/_config.inc.php
===================================================================
--- trunk/bin/module_maker/_config.inc.php	(revision 499)
+++ trunk/bin/module_maker/_config.inc.php	(revision 500)
@@ -35,5 +35,15 @@
 
 // Make sure necessary files exist.
-if (!file_exists(COMMON_BASE . '/global/db_auth.inc.php')) {
+$db_auth_file = false;
+$rii = new RecursiveIteratorIterator(new RecursiveDirectoryIterator(COMMON_BASE));
+$rii->setMaxDepth(2);
+foreach ($rii as $filename => $file) {
+    if (mb_strpos($filename, 'db_auth.inc.php') !== false) {
+        $db_auth_file = $filename;
+        break;
+    }
+}
+
+if (!$db_auth_file) {
     die("Error: First argument directory must contain the global/db_auth.inc.php file with valid MySQL credentials.\n");
 }
@@ -63,5 +73,5 @@
     'log_screen_priority' => LOG_DEBUG,
 ));
-require_once 'global/db_auth.inc.php';
+require_once $db_auth_file;
 
 // Start application-based functionality: database, session, environment, ini setup, etc.
Index: trunk/bin/module_maker/module.cli.php
===================================================================
--- trunk/bin/module_maker/module.cli.php	(revision 499)
+++ trunk/bin/module_maker/module.cli.php	(revision 500)
@@ -5,5 +5,5 @@
  * For details visit the project site: <http://trac.strangecode.com/codebase/>
  * Copyright 2001-2012 Strangecode, LLC
- * 
+ *
  * This file is part of The Strangecode Codebase.
  *
@@ -12,10 +12,10 @@
  * Free Software Foundation, either version 3 of the License, or (at your option)
  * any later version.
- * 
+ *
  * The Strangecode Codebase is distributed in the hope that it will be useful, but
  * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
  * details.
- * 
+ *
  * You should have received a copy of the GNU General Public License along with
  * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
@@ -39,5 +39,5 @@
 
     " . basename($_SERVER['argv'][0]) . " site_directory name_singular name_plural [operation]
-    
+
 Valid operations include: " . join(', ', $valid_ops) . "
 
@@ -149,5 +149,5 @@
     if (!is_dir("$public_tpl_dir")) {
         die(basename($_SERVER['argv'][0]) . " Error: public_tpl_dir '$public_tpl_dir' directory not found.\n");
-    }    
+    }
 }
 
@@ -196,5 +196,5 @@
         $type = preg_replace('/^(\w+).*$/', '\\1', $col[1]);
         $default = $col[4];
-        
+
         if (mb_strpos($default, '0000') !== false || '0' == $default) {
             $default = '';
Index: trunk/bin/module_maker/skel/admin.php
===================================================================
--- trunk/bin/module_maker/skel/admin.php	(revision 499)
+++ trunk/bin/module_maker/skel/admin.php	(revision 500)
@@ -408,5 +408,5 @@
     ";
 
-    // Use a cash hash to determine if the result-set has changed.
+    // Use a cache hash to determine if the result-set has changed.
     // A unique key for this query, with the total_items in case db records
     // were added since the last cache. This identifies a unique set of
Index: trunk/css/admin2.inc.css
===================================================================
--- trunk/css/admin2.inc.css	(revision 499)
+++ trunk/css/admin2.inc.css	(revision 500)
@@ -392,2 +392,7 @@
     width: auto;
 }
+
+/* Hide foundation elements for sites still using old admin css. */
+.sc-msg .close {
+    display: none;
+}
Index: trunk/js/Msg.js
===================================================================
--- trunk/js/Msg.js	(revision 499)
+++ trunk/js/Msg.js	(revision 500)
@@ -48,5 +48,5 @@
     // Merge options with defaults
     this.o = $.extend({
-        container: '#sc-msg',
+        container: '#sc-msg:last',
         above_msg: '',
         gotohash: true
@@ -74,4 +74,6 @@
     if (!$(this.o.container).find(':contains("' + message + '")').length) {
         $(this.o.container).append($('<div class="sc-js-msg ' + msg_class + '"></div>').text(message)).show();
+        // $(this.o.container).append($('<div data-alert class="alert-box sc-js-msg ' + msg_class + '"></div>').hide().text(message).append('<a href="#" class="close">Ã</a>'));
+        // $('.sc-js-msg').slideDown('fast');
     }
 
Index: trunk/js/Utilities.js
===================================================================
--- trunk/js/Utilities.js	(revision 499)
+++ trunk/js/Utilities.js	(revision 500)
@@ -41,5 +41,5 @@
         var args = arguments;
         return this.replace(/{(\d+)}/g, function(match, number) {
-            return typeof args[number] != 'undefined' ? args[number] : match;
+            return typeof args[number-1] != 'undefined' ? args[number-1] : match;
         });
     };
Index: trunk/lib/App.inc.php
===================================================================
--- trunk/lib/App.inc.php	(revision 499)
+++ trunk/lib/App.inc.php	(revision 500)
@@ -80,4 +80,12 @@
         'ssl_enabled' => false,
 
+        // Use CSRF tokens.
+        'csrf_token_enabled' => true,
+        'csrf_token_name' => 'csrf_token',
+        'csrf_token_timeout' => 86400, // In seconds. This causes form tokens to be unusable after this duration. This might only cause problems when opening forms in multiple tabs left open beyond the timeout duration. But usually their session will timeout first, and they'll receive new tokens when they load the form again..
+
+        // HMAC signing method
+        'signing_method' => 'sha512+base64',
+
         // Character set for page output. Used in the Content-Type header and the HTML <meta content-type> tag.
         'character_set' => 'utf-8',
@@ -450,5 +458,5 @@
 
         if ('' == trim($message)) {
-            $this->logMsg(sprintf('Raised message is an empty string.', __FUNCTION__), LOG_NOTICE, __FILE__, __LINE__);
+            $this->logMsg(sprintf('Raised message is an empty string.', null), LOG_NOTICE, __FILE__, __LINE__);
             return false;
         }
@@ -944,22 +952,4 @@
     }
 
-    /*
-    * Return a URL with a version number attached. This is useful for overriding network caches ("cache buster") for sourced media, e.g., /style.css?812763482
-    *
-    * @access   public
-    * @param    string  $url    URL to media (e.g., /foo.js)
-    * @return   string          URL with cache-busting version appended (/foo.js?v=1234567890)
-    * @author   Quinn Comendant <quinn@strangecode.com>
-    * @version  1.0
-    * @since    03 Sep 2014 22:40:24
-    */
-    public function cacheBustURL($url)
-    {
-        // Get the first delimiter that is needed in the url.
-        $delim = mb_strpos($url, '?') !== false ? ini_get('arg_separator.output') : '?';
-        $v = crc32($this->getParam('codebase_version') . '|' . $this->getParam('site_version'));
-        return sprintf('%s%sv=%s', $url, $delim, $v);
-    }
-
     /**
      * Prints a hidden form element with the PHPSESSID when cookies are not used, as well
@@ -971,6 +961,7 @@
      *                                      array('key1'=>'value', key2'='value')  <-- to set keys to default values if not present in form data.
      *                                      false  <-- To not carry any queries. If URL already has queries those will be retained.
-     */
-    public function printHiddenSession($carry_args=null)
+     * @param   bool    $include_csrf_token     Set to true to include the csrf_token in the form. Only use this for forms with action="post" to prevent the token from being revealed in the URL.
+     */
+    public function printHiddenSession($carry_args=null, $include_csrf_token=false)
     {
         if (!$this->running) {
@@ -1023,4 +1014,133 @@
             printf('<input type="hidden" name="%s" value="%s" />', session_name(), session_id());
         }
+
+        // Include the csrf_token in the form.
+        // This token can be validated upon form submission with $app->verifyCSRFToken() or $app->requireValidCSRFToken()
+        if ($this->getParam('csrf_token_enabled') && $include_csrf_token) {
+            printf('<input type="hidden" name="%s" value="%s" />', $this->getParam('csrf_token_name'), $this->recycleCSRFToken());
+        }
+    }
+
+    /*
+    * Return a URL with a version number attached. This is useful for overriding network caches ("cache buster") for sourced media, e.g., /style.css?812763482
+    *
+    * @access   public
+    * @param    string  $url    URL to media (e.g., /foo.js)
+    * @return   string          URL with cache-busting version appended (/foo.js?v=1234567890)
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @version  1.0
+    * @since    03 Sep 2014 22:40:24
+    */
+    public function cacheBustURL($url)
+    {
+        // Get the first delimiter that is needed in the url.
+        $delim = mb_strpos($url, '?') !== false ? ini_get('arg_separator.output') : '?';
+        $v = crc32($this->getParam('codebase_version') . '|' . $this->getParam('site_version'));
+        return sprintf('%s%sv=%s', $url, $delim, $v);
+    }
+
+    /*
+    * Generate a csrf_token, saving it to the session and returning its value.
+    * https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet#General_Recommendation:_Synchronizer_Token_Pattern
+    * @access   public
+    * @return   string  The new csrf_token.
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @version  1.0
+    * @since    15 Nov 2014 17:53:51
+    */
+    public function generateCSRFToken()
+    {
+        return $_SESSION['_app'][$this->_ns]['csrf_tokens'][] = addSignature(time(), null, 64);
+    }
+
+    /*
+    * Update the csrf_token to a new value if it hasn't been set yet or has expired.
+    * Save the previous csrf_token in the session to ensure continuity of currently open sessions.
+    *
+    * @access   public
+    * @return   string The current csrf_token
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @version  1.0
+    * @since    15 Nov 2014 17:57:17
+    */
+    public function recycleCSRFToken()
+    {
+        if (!isset($_SESSION['_app'][$this->_ns]['csrf_tokens'])) {
+            // No token yet; generate one and return it.
+            $_SESSION['_app'][$this->_ns]['csrf_tokens'] = array();
+            $return = $this->generateCSRFToken();
+        }
+        if (removeSignature(end($_SESSION['_app'][$this->_ns]['csrf_tokens'])) + $this->getParam('csrf_token_timeout') < time()) {
+            // Newest token is expired; prune array of tokens and generate new token.
+            // We'll save the 10-most-recent tokens. This allows the user to submit up to 5 forms saved in previously opened tabs with expired tokens (loading a form will prune one token, and submitting a form will prune one token, thus 10 = 5).
+            $_SESSION['_app'][$this->_ns]['csrf_tokens'] = array_slice($_SESSION['_app'][$this->_ns]['csrf_tokens'], -10, 10);
+            $return = $this->generateCSRFToken();
+        }
+        // Current token is not expired; return it.
+        $return = end($_SESSION['_app'][$this->_ns]['csrf_tokens']);
+        $app =& App::getInstance();
+        return $return;
+    }
+
+    /*
+    * Compares the given csrf_token with the current or previous one saved in the session.
+    *
+    * @access   public
+    * @param    string  $csrf_token     The token to compare with the session token.
+    * @return   bool    True if the tokens match, false otherwise.
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @version  1.0
+    * @since    15 Nov 2014 18:06:55
+    */
+    public function verifyCSRFToken($csrf_token)
+    {
+        $app =& App::getInstance();
+
+        if (!$this->getParam('csrf_token_enabled')) {
+            $app->logMsg(sprintf('%s method called, but csrf_token_enabled=false', __FUNCTION__), LOG_ERR, __FILE__, __LINE__);
+            return false;
+        }
+        if ('' == trim($csrf_token)) {
+            $app->logMsg(sprintf('Empty string failed CSRF verification.', null), LOG_NOTICE, __FILE__, __LINE__);
+            return false;
+        }
+        if (!verifySignature($csrf_token, null, 64)) {
+            $app->logMsg(sprintf('Input failed CSRF verification (invalid signature in %s).', $csrf_token), LOG_WARNING, __FILE__, __LINE__);
+            return false;
+        }
+        $this->recycleCSRFToken();
+        if (!in_array($csrf_token, $_SESSION['_app'][$this->_ns]['csrf_tokens'])) {
+            $app->logMsg(sprintf('Input failed CSRF verification (%s not in %s).', $csrf_token, getDump($_SESSION['_app'][$this->_ns]['csrf_tokens'])), LOG_WARNING, __FILE__, __LINE__);
+            return false;
+        }
+        // $app->logMsg(sprintf('Verified token %s is in %s', $csrf_token, getDump($_SESSION['_app'][$this->_ns]['csrf_tokens'])), LOG_DEBUG, __FILE__, __LINE__);
+        return true;
+    }
+
+    /*
+    * Bounce user if they submit a token that doesn't match the one saved in the session.
+    * Because this function calls dieURL() it must be called before any other HTTP header output.
+    *
+    * @access   public
+    * @param    string  $csrf_token The token to compare with the session token.
+    * @param    string  $message    Optional message to display to the user (otherwise default message will display). Set to an empty string to display no message.
+    * @param    int    $type    The type of message: MSG_NOTICE,
+    *                           MSG_SUCCESS, MSG_WARNING, or MSG_ERR.
+    * @param    string $file    __FILE__.
+    * @param    string $line    __LINE__.
+    * @return   void
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @version  1.0
+    * @since    15 Nov 2014 18:10:17
+    */
+    public function requireValidCSRFToken($csrf_token, $message=null, $type=MSG_NOTICE, $file=null, $line=null)
+    {
+        $app =& App::getInstance();
+
+        if (!$this->verifyCSRFToken($csrf_token)) {
+            $message = isset($message) ? $message : _("Something went wrong; please try again.");
+            $app->raiseMsg($message, $type, $file, $line);
+            $app->dieBoomerangURL();
+        }
     }
 
Index: trunk/lib/Auth_SQL.inc.php
===================================================================
--- trunk/lib/Auth_SQL.inc.php	(revision 499)
+++ trunk/lib/Auth_SQL.inc.php	(revision 500)
@@ -39,4 +39,6 @@
     const ENCRYPT_MD5 = 5;
     const ENCRYPT_MD5_HARDENED = 6;
+    const ENCRYPT_PASSWORD_BCRYPT = 7;
+    const ENCRYPT_PASSWORD_DEFAULT = 8;
 
     // Namespace of this auth object.
@@ -174,4 +176,5 @@
                 " . $this->getParam('db_username_column') . " varchar(255) NOT NULL default '',
                 userpass VARCHAR(255) NOT NULL DEFAULT '',
+                userpass_hashtype TINYINT UNSIGNED NOT NULL DEFAULT '0',
                 first_name VARCHAR(255) NOT NULL DEFAULT '',
                 last_name VARCHAR(255) NOT NULL DEFAULT '',
@@ -260,4 +263,15 @@
             $params['login_abuse_exempt_usernames'] = array_map('strtolower', $params['login_abuse_exempt_usernames']);
         }
+        if (isset($params['encryption_type']) && version_compare(PHP_VERSION, '5.5.0', '<') && in_array($params['encryption_type'], array(self::ENCRYPT_PASSWORD_BCRYPT, self::ENCRYPT_PASSWORD_DEFAULT))) {
+            // These hash types require the password_* userland lib in PHP < 5.5.0
+            $pw_compat_lib = 'vendor/ircmaxell/password-compat/lib/password.php';
+            if (false !== stream_resolve_include_path($pw_compat_lib)) {
+                include_once $pw_compat_lib;
+            } else {
+                $app =& App::getInstance();
+                $app->logMsg(sprintf('Encryption type %s requires password-compat lib in PHP < 5.5.0; falling back to ENCRYPT_SHA1', $params['encryption_type']), LOG_ERR, __FILE__, __LINE__);
+                $params['encryption_type'] = self::ENCRYPT_SHA1;
+            }
+        }
         if (isset($params) && is_array($params)) {
             // Merge new parameters with old overriding only those passed.
@@ -353,5 +367,5 @@
 
     /**
-     * Find out if a set of login credentials are valid.
+     * Retrieve and verify the given username and password against a matching user record in the database.
      *
      * @access private
@@ -367,38 +381,24 @@
         $this->initDB();
 
-        switch ($this->_params['encryption_type']) {
-        case self::ENCRYPT_CRYPT :
-            // Query DB for user matching credentials. Compare cyphertext with salted-encrypted password.
-            $qid = $db->query("
-                SELECT *, " . $this->_params['db_primary_key'] . " AS user_id
-                FROM " . $this->_params['db_table'] . "
-                WHERE " . $this->_params['db_username_column'] . " = '" . $db->escapeString($username) . "'
-                AND BINARY userpass = ENCRYPT('" . $db->escapeString($password) . "', LEFT(userpass, 2)))
-            ");
-            break;
-        case self::ENCRYPT_PLAINTEXT :
-        case self::ENCRYPT_MD5 :
-        case self::ENCRYPT_SHA1 :
-        default :
-            // Query DB for user matching credentials. Directly compare cyphertext with result from encryptPassword().
-            $qid = $db->query("
-                SELECT *, " . $this->_params['db_primary_key'] . " AS user_id
-                FROM " . $this->_params['db_table'] . "
-                WHERE " . $this->_params['db_username_column'] . " = '" . $db->escapeString($username) . "'
-                AND BINARY userpass = '" . $db->escapeString($this->encryptPassword($password)) . "'
-            ");
-            break;
-        }
-
-        // Return user data if found.
-        if ($user_data = mysql_fetch_assoc($qid)) {
-            // Don't return password value.
-            unset($user_data['userpass']);
-            $app->logMsg(sprintf('Authentication successful for user_id %s (%s)', $user_data['user_id'], $username), LOG_INFO, __FILE__, __LINE__);
+        // Get user data for specified username.
+        // Query DB for user matching credentials. Compare cyphertext with salted-encrypted password.
+        $qid = $db->query("
+            SELECT *, " . $this->_params['db_primary_key'] . " AS user_id
+            FROM " . $this->_params['db_table'] . "
+            WHERE " . $this->_params['db_username_column'] . " = '" . $db->escapeString($username) . "'
+        ");
+        if (!$user_data = mysql_fetch_assoc($qid)) {
+            $app->logMsg(sprintf('Username %s not found for authentication', $username), LOG_NOTICE, __FILE__, __LINE__);
+            return false;
+        }
+
+        if ($this->verifyPassword($password, $user_data['userpass'])) {
+            $app->logMsg(sprintf('Authentication successful for %s (user_id=%s)', $username, $user_data['user_id']), LOG_INFO, __FILE__, __LINE__);
+            unset($user_data['userpass']); // Avoid revealing the encrypted password in the $user_data.
             return $user_data;
-        } else {
-            $app->logMsg(sprintf('Authentication failed for username %s (encrypted attempted password: %s)', $username, $this->encryptPassword($password)), LOG_NOTICE, __FILE__, __LINE__);
-            return false;
-        }
+        }
+
+        $app->logMsg(sprintf('Authentication failed for %s (user_id=%s)', $username, $user_data['user_id']), LOG_NOTICE, __FILE__, __LINE__);
+        return false;
     }
 
@@ -553,6 +553,6 @@
                 SELECT 1 FROM " . $this->_params['db_table'] . "
                 WHERE " . $this->_params['db_primary_key'] . " = '" . $db->escapeString($user_id) . "'
-                AND DATE_ADD(last_login_datetime, INTERVAL '" . $this->_params['login_timeout'] . "' SECOND) > NOW()
-                AND DATE_ADD(last_access_datetime, INTERVAL '" . $this->_params['idle_timeout'] . "' SECOND) > NOW()
+                AND last_login_datetime > DATE_SUB(NOW(), INTERVAL '" . $this->_params['login_timeout'] . "' SECOND)
+                AND last_access_datetime > DATE_SUB(NOW(), INTERVAL '" . $this->_params['idle_timeout'] . "' SECOND)
             ");
             $login_status = (mysql_num_rows($qid) > 0);
@@ -809,38 +809,25 @@
     }
 
-    /**
-     * Returns a randomly generated password based on $pattern. The pattern is any
-     * sequence of 'x', 'V', 'C', 'v', 'c', or 'd' and if it is something like 'cvccv' this
-     * function will generate a pronounceable password. Recommend using more complex
-     * patterns, at minimum the US State Department standard: cvcddcvc.
-     *
-     * - x    A random upper or lower character, digit, or punctuation.
-     * - C    A random upper or lower consonant.
-     * - V    A random upper or lower vowel.
-     * - c    A random lowercase consonant.
-     * - v    A random lowercase vowel.
-     * - d    A random digit.
-     *
-     * @param  string $pattern  a sequence of character types, above.
-     * @return string           a password
-     */
-    public function generatePassword($pattern='CvcdCvc')
-    {
-        $app =& App::getInstance();
-        if (preg_match('/[^xCVcvd]/', $pattern)) {
-            $app->logMsg(sprintf('Invalid pattern: %s', $pattern), LOG_WARNING, __FILE__, __LINE__);
-            $pattern='CvcdCvc';
-        }
-        $str = '';
-        for ($i=0; $i<mb_strlen($pattern); $i++) {
-            $x = mb_substr('bcdfghjklmnprstvwxzBCDFGHJKLMNPRSTVWXZaeiouyAEIOUY0123456789!@#%&*-=+.?', (mt_rand() % 71), 1);
-            $c = mb_substr('bcdfghjklmnprstvwxz', (mt_rand() % 19), 1);
-            $C = mb_substr('bcdfghjklmnprstvwxzBCDFGHJKLMNPRSTVWXZ', (mt_rand() % 38), 1);
-            $v = mb_substr('aeiouy', (mt_rand() % 6), 1);
-            $V = mb_substr('aeiouyAEIOUY', (mt_rand() % 12), 1);
-            $d = mb_substr('0123456789', (mt_rand() % 10), 1);
-            $str .= $$pattern[$i];
-        }
-        return $str;
+    /*
+    * Generate a cryptographically secure, random password.
+    *
+    * @access   public
+    * @param    int  $bytes     Length of password (in bytes)
+    * @return   string          Random string of characters.
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @version  1.0
+    * @since    15 Nov 2014 20:30:27
+    */
+    public function generatePassword($bytes=10)
+    {
+        $app =& App::getInstance();
+
+        $bytes = is_numeric($bytes) ? $bytes : 10;
+        $string = strtok(base64_encode(openssl_random_pseudo_bytes($bytes, $strong)), '=');
+        if (!$strong) {
+            $app->logMsg(sprintf('Password generated was not "cryptographically strong"; check your openssl.', null), LOG_NOTICE, __FILE__, __LINE__);
+        }
+
+        return $string;
     }
 
@@ -851,4 +838,6 @@
     {
         $app =& App::getInstance();
+
+        $password = (string)$password;
 
         // Existing password hashes rely on the same key/salt being used to compare encryptions.
@@ -858,37 +847,40 @@
         switch ($this->_params['encryption_type']) {
         case self::ENCRYPT_PLAINTEXT :
-            return $password;
+            $encrypted_password = $password;
             break;
 
         case self::ENCRYPT_CRYPT :
-            // If comparing plaintext password with a hash, provide first two chars of the hash as the salt.
-            return isset($salt) ? crypt($password, mb_substr($salt, 0, 2)) : crypt($password);
+            // If comparing password with an existing hashed password, provide the hashed password as the salt.
+            $encrypted_password = isset($salt) ? crypt($password, $salt) : crypt($password);
             break;
 
         case self::ENCRYPT_SHA1 :
-            return sha1($password);
+            $encrypted_password = sha1($password);
             break;
 
         case self::ENCRYPT_SHA1_HARDENED :
-            $hash = sha1($app->getParam('signing_key') . $password . $more_salt);
-            // Increase key strength by 12 bits.
-            for ($i=0; $i < 4096; $i++) {
-                $hash = sha1($hash);
-            }
-            return $hash;
+            $encrypted_password = sha1($app->getParam('signing_key') . $password . $more_salt);
+            for ($i=0; $i < pow(2, 20); $i++) {
+                $encrypted_password = sha1($password . $encrypted_password);
+            }
             break;
 
         case self::ENCRYPT_MD5 :
-            return md5($password);
+            $encrypted_password = md5($password);
             break;
 
         case self::ENCRYPT_MD5_HARDENED :
-            // Include salt to improve hash
-            $hash = md5($app->getParam('signing_key') . $password . $more_salt);
-            // Increase key strength by 12 bits.
-            for ($i=0; $i < 4096; $i++) {
-                $hash = md5($hash);
-            }
-            return $hash;
+            $encrypted_password = md5($app->getParam('signing_key') . $password . $more_salt);
+            for ($i=0; $i < pow(2, 20); $i++) {
+                $encrypted_password = md5($password . $encrypted_password);
+            }
+            break;
+
+        case self::ENCRYPT_PASSWORD_BCRYPT :
+            $encrypted_password = password_hash($password, PASSWORD_BCRYPT, array('cost' => 12));
+            break;
+
+        case self::ENCRYPT_PASSWORD_DEFAULT :
+            $encrypted_password = password_hash($password, PASSWORD_DEFAULT, array('cost' => 12));
             break;
 
@@ -897,4 +889,42 @@
             return false;
             break;
+        }
+
+        // In case our hashing function returns 'false' or another empty value, bail out.
+        if ('' == trim((string)$encrypted_password)) {
+            $app->logMsg(sprintf('Invalid password hash returned; check yo crypto!', null), LOG_ALERT, __FILE__, __LINE__);
+            return false;
+        }
+
+        return $encrypted_password;
+    }
+
+    /*
+    *
+    *
+    * @access   public
+    * @param
+    * @return
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @version  1.0
+    * @since    15 Nov 2014 21:37:28
+    */
+    public function verifyPassword($password, $encrypted_password)
+    {
+        switch ($this->_params['encryption_type']) {
+        case self::ENCRYPT_CRYPT :
+            return $this->encryptPassword($password, $encrypted_password) == $encrypted_password;
+
+        case self::ENCRYPT_PLAINTEXT :
+        case self::ENCRYPT_MD5 :
+        case self::ENCRYPT_MD5_HARDENED :
+        case self::ENCRYPT_SHA1 :
+        case self::ENCRYPT_SHA1_HARDENED :
+        default :
+            return $this->encryptPassword($password) == $encrypted_password;
+
+        case self::ENCRYPT_PASSWORD_BCRYPT :
+        case self::ENCRYPT_PASSWORD_DEFAULT :
+            return password_verify($password, $encrypted_password);
         }
     }
@@ -920,13 +950,18 @@
         ");
         if (!list($old_encrypted_password) = mysql_fetch_row($qid)) {
-            $app->logMsg(sprintf('Cannot set password for nonexistent user_id %s', $user_id), LOG_NOTICE, __FILE__, __LINE__);
+            $app->logMsg(sprintf('Cannot set password for nonexistent user_id %s', $user_id), LOG_WARNING, __FILE__, __LINE__);
             return false;
         }
 
         // Compare old with new to ensure we're actually *changing* the password.
-        $encrypted_password = $this->encryptPassword($password);
-        if ($old_encrypted_password == $encrypted_password) {
+        if ($this->verifyPassword($password, $old_encrypted_password)) {
             $app->logMsg(sprintf('Not setting password: new is the same as old.', null), LOG_INFO, __FILE__, __LINE__);
-            return false;
+            return null;
+        }
+
+        // Save the hash method used if a table exists for it.
+        $userpass_hashtype = '';
+        if ($db->columnExists($this->_params['db_table'], 'userpass_hashtype', false)) {
+            $userpass_hashtype = ", userpass_hashtype = '" . $db->escapeString($this->getParam('encryption_type')) . "'";
         }
 
@@ -934,5 +969,6 @@
         $db->query("
             UPDATE " . $this->_params['db_table'] . "
-            SET userpass = '" . $db->escapeString($encrypted_password) . "'
+            SET userpass = '" . $db->escapeString($this->encryptPassword($password)) . "'
+            $userpass_hashtype
             WHERE " . $this->_params['db_primary_key'] . " = '" . $db->escapeString($user_id) . "'
         ");
@@ -943,4 +979,5 @@
         }
 
+        $app->logMsg(sprintf('Password change successful for user_id %s', $user_id), LOG_INFO, __FILE__, __LINE__);
         return true;
     }
@@ -1008,91 +1045,3 @@
     }
 
-    /**
-     * If the current user has access to the specified $security_zone, return true.
-     * If the optional $user_type is supplied, test that against the zone.
-     *
-     * NOTE: "user_type" used to be called "priv" in some older implementations.
-     *
-     * @param  constant $security_zone   string of comma delimited privileges for the zone
-     * @param  string   $user_type       a privilege that might be found in a zone
-     * @return bool     true if user is a member of security zone, false otherwise
-     */
-    public function inClearanceZone($security_zone, $user_type='')
-    {
-        $zone_members = preg_split('/,\s*/', $security_zone);
-        $user_type = empty($user_type) ? $this->get('user_type') : $user_type;
-
-        // If the current user's privilege level is NOT in that array or if the
-        // user has no privilege, return false. Otherwise the user is clear.
-        if (!in_array($user_type, $zone_members) || empty($user_type)) {
-            return false;
-        } else {
-            return true;
-        }
-    }
-
-    /**
-     * This function tests a list of arguments $security_zone against the priv that the current user has.
-     * If the user doesn't have one of the supplied privs, die.
-     *
-     * NOTE: "user_type" used to be called "priv" in some older implementations.
-     *
-     * @param  constant $security_zone   string of comma delimited privileges for the zone
-     */
-    public function requireAccessClearance($security_zone, $message='')
-    {
-        $app =& App::getInstance();
-
-        $zone_members = preg_split('/,\s*/', $security_zone);
-
-        /* If the current user's privilege level is NOT in that array or if the
-         * user has no privilege, DIE with a message. */
-        if (!in_array($this->get('user_type'), $zone_members) || !$this->get('user_type')) {
-            $message = empty($message) ? _("You have insufficient privileges to view that page.") : $message;
-            $app->raiseMsg($message, MSG_NOTICE, __FILE__, __LINE__);
-            $app->dieBoomerangURL();
-        }
-    }
-
 } // end class
-
-// CIDR cheat-sheet
-//
-// Netmask              Netmask (binary)                 CIDR     Notes
-// _____________________________________________________________________________
-// 255.255.255.255  11111111.11111111.11111111.11111111  /32  Host (single addr)
-// 255.255.255.254  11111111.11111111.11111111.11111110  /31  Unusable
-// 255.255.255.252  11111111.11111111.11111111.11111100  /30    2  useable
-// 255.255.255.248  11111111.11111111.11111111.11111000  /29    6  useable
-// 255.255.255.240  11111111.11111111.11111111.11110000  /28   14  useable
-// 255.255.255.224  11111111.11111111.11111111.11100000  /27   30  useable
-// 255.255.255.192  11111111.11111111.11111111.11000000  /26   62  useable
-// 255.255.255.128  11111111.11111111.11111111.10000000  /25  126  useable
-// 255.255.255.0    11111111.11111111.11111111.00000000  /24 "Class C" 254 useable
-//
-// 255.255.254.0    11111111.11111111.11111110.00000000  /23    2  Class C's
-// 255.255.252.0    11111111.11111111.11111100.00000000  /22    4  Class C's
-// 255.255.248.0    11111111.11111111.11111000.00000000  /21    8  Class C's
-// 255.255.240.0    11111111.11111111.11110000.00000000  /20   16  Class C's
-// 255.255.224.0    11111111.11111111.11100000.00000000  /19   32  Class C's
-// 255.255.192.0    11111111.11111111.11000000.00000000  /18   64  Class C's
-// 255.255.128.0    11111111.11111111.10000000.00000000  /17  128  Class C's
-// 255.255.0.0      11111111.11111111.00000000.00000000  /16  "Class B"
-//
-// 255.254.0.0      11111111.11111110.00000000.00000000  /15    2  Class B's
-// 255.252.0.0      11111111.11111100.00000000.00000000  /14    4  Class B's
-// 255.248.0.0      11111111.11111000.00000000.00000000  /13    8  Class B's
-// 255.240.0.0      11111111.11110000.00000000.00000000  /12   16  Class B's
-// 255.224.0.0      11111111.11100000.00000000.00000000  /11   32  Class B's
-// 255.192.0.0      11111111.11000000.00000000.00000000  /10   64  Class B's
-// 255.128.0.0      11111111.10000000.00000000.00000000  /9   128  Class B's
-// 255.0.0.0        11111111.00000000.00000000.00000000  /8   "Class A"
-//
-// 254.0.0.0        11111110.00000000.00000000.00000000  /7
-// 252.0.0.0        11111100.00000000.00000000.00000000  /6
-// 248.0.0.0        11111000.00000000.00000000.00000000  /5
-// 240.0.0.0        11110000.00000000.00000000.00000000  /4
-// 224.0.0.0        11100000.00000000.00000000.00000000  /3
-// 192.0.0.0        11000000.00000000.00000000.00000000  /2
-// 128.0.0.0        10000000.00000000.00000000.00000000  /1
-// 0.0.0.0          00000000.00000000.00000000.00000000  /0   IP space
Index: trunk/lib/Currency.inc.php
===================================================================
--- trunk/lib/Currency.inc.php	(revision 499)
+++ trunk/lib/Currency.inc.php	(revision 500)
@@ -4,5 +4,5 @@
  * For details visit the project site: <http://trac.strangecode.com/codebase/>
  * Copyright 2001-2012 Strangecode, LLC
- * 
+ *
  * This file is part of The Strangecode Codebase.
  *
@@ -11,10 +11,10 @@
  * Free Software Foundation, either version 3 of the License, or (at your option)
  * any later version.
- * 
+ *
  * The Strangecode Codebase is distributed in the hope that it will be useful, but
  * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
  * details.
- * 
+ *
  * You should have received a copy of the GNU General Public License along with
  * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
@@ -28,5 +28,5 @@
  * @author  Quinn Comendant <quinn@strangecode.com>
  * @version 1.5
- * 
+ *
  * Example of use:
 ---------------------------------------------------------------------
@@ -37,5 +37,5 @@
 ---------------------------------------------------------------------
  */
- 
+
 class Currency {
 
@@ -48,5 +48,5 @@
         'api_key' => '', // Used only by xurrency API.
     );
-    
+
     /**
      * Cart constructor.
@@ -58,5 +58,5 @@
         // Set custom parameters.
         $this->setParam($params);
-        
+
         // Setup cache directory.
         if ($this->getParam('cache_result')) {
@@ -66,7 +66,7 @@
             }
             if (!is_dir($this->getParam('cache_dir'))) {
-                $app->logMsg(sprintf('Creating cache_dir: %s', $this->getParam('cache_dir')), LOG_INFO, __FILE__, __LINE__);                
+                $app->logMsg(sprintf('Creating cache_dir: %s', $this->getParam('cache_dir')), LOG_INFO, __FILE__, __LINE__);
                 if (!mkdir($this->getParam('cache_dir'))) {
-                    $app->logMsg(sprintf('Could not create cache_dir: %s', $this->getParam('cache_dir')), LOG_WARNING, __FILE__, __LINE__);                
+                    $app->logMsg(sprintf('Could not create cache_dir: %s', $this->getParam('cache_dir')), LOG_WARNING, __FILE__, __LINE__);
                 }
             }
@@ -97,5 +97,5 @@
     {
         $app =& App::getInstance();
-    
+
         if (array_key_exists($param, $this->_params)) {
             return $this->_params[$param];
@@ -105,5 +105,5 @@
         }
     }
-    
+
     /*
     * Return the exchange value between the two given currencies for given amount.
@@ -113,5 +113,5 @@
     * @param    string  $base   3-letter currency code to convert from.
     * @param    string  $target 3-letter currency code to convert to.
-    * @return   mixed   Float converted currency value, or false on error. 
+    * @return   mixed   Float converted currency value, or false on error.
     * @author   Quinn Comendant <quinn@strangecode.com>
     * @version  1.0
@@ -121,10 +121,10 @@
     {
         if (false !== $rate = $this->getRate($base, $target)) {
-            return abs($rate * $amount);            
+            return abs($rate * $amount);
         } else {
             return false;
         }
     }
-    
+
     /*
     * Return the currency conversion rate as a ratio.
@@ -133,5 +133,5 @@
     * @param    string  $base   3-letter currency code to convert from.
     * @param    string  $target 3-letter currency code to convert to.
-    * @return   mixed   Float exchange rate value, or false on error. 
+    * @return   mixed   Float exchange rate value, or false on error.
     * @author   Quinn Comendant <quinn@strangecode.com>
     * @version  1.0
@@ -173,5 +173,5 @@
         return trim($value);
     }
-    
+
     /**
      * @param  string
@@ -215,9 +215,9 @@
             $api_url = 'http://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml';
             if (false === $sXML = file_get_contents($api_url)) {
-                $app->logMsg(sprintf('Failed to load ECB XML data from ', $api_url), LOG_WARNING, __FILE__, __LINE__);
+                $app->logMsg(sprintf('Failed to load ECB XML data from: %s', $api_url), LOG_WARNING, __FILE__, __LINE__);
                 return false;
             }
             if (false === $oXML = simplexml_load_string($sXML)) {
-                $app->logMsg(sprintf('Failed to decode ECB XML data: ', truncate($sXML, 200, 'end')), LOG_WARNING, __FILE__, __LINE__);
+                $app->logMsg(sprintf('Failed to decode ECB XML data: %s', truncate($sXML, 200, 'end')), LOG_WARNING, __FILE__, __LINE__);
                 return false;
             }
Index: trunk/lib/Email.inc.php
===================================================================
--- trunk/lib/Email.inc.php	(revision 499)
+++ trunk/lib/Email.inc.php	(revision 500)
@@ -314,6 +314,10 @@
         foreach ($headers as $key => $val) {
             // Validate key and values.
-            if (empty($key) || empty($val) || !is_string($key) || !is_string($val) || preg_match("/[\n\r]/", $key . $val) || preg_match('/[^\w-]/', $key)) {
-                $app->logMsg(sprintf('Broken headers provided: %s=%s', $key, $val), LOG_WARNING, __FILE__, __LINE__);
+            if (empty($val)) {
+                $app->logMsg(sprintf('Empty email header provided: %s', $key), LOG_DEBUG, __FILE__, __LINE__);
+                continue;
+            }
+            if (empty($key) || !is_string($key) || !is_string($val) || preg_match("/[\n\r]/", $key . $val) || preg_match('/[^\w-]/', $key)) {
+                $app->logMsg(sprintf('Broken email header provided: %s=%s', $key, $val), LOG_WARNING, __FILE__, __LINE__);
                 continue;
             }
Index: trunk/lib/HTML.inc.php
===================================================================
--- trunk/lib/HTML.inc.php	(revision 499)
+++ trunk/lib/HTML.inc.php	(revision 500)
@@ -39,4 +39,9 @@
 
 class HTML {
+
+    // Browsers add names and ids of form controls as properties to the FORM. This results in the properties of the form being replaced.
+    // Use this list to warn the programmer if he uses an unsafe name.
+    // http://jibbering.com/faq/names/unsafe_names.html
+    static $unsafe_form_control_names = array('accept','acceptCharset','action','addBehavior','addEventListener','addEventSource','addRepetitionBlock','addRepetitionBlockByIndex','all','appendChild','applyElement','ariaBusy','ariaChecked','ariaControls','ariaDescribability','ariaDisabled','ariaExpanded','ariaFlowto','ariaHaspopup','ariaHidden','ariaInvalid','ariaLabelledby','ariaLevel','ariaMultiselect','ariaOwns','ariaPosinset','ariaPressed','ariaReadonly','ariaRequired','ariaSecret','ariaSelected','ariaSetsize','ariaValuemax','ariaValuemin','ariaValuenow','attachEvent','attributes','ATTRIBUTE_NODE','autocomplete','baseURI','behaviorUrns','blockDiraction','blur','canHaveChildren','canHaveHTML','CDATA_SECTION_NODE','checkValidity','childElementCount','childNodes','children','className','clearAttributes','click','clientHeight','clientLeft','clientTop','clientWidth','cloneNode','COMMENT_NODE','compareDocumentPosition','componentFromPoint','constructor','contains','contentEditable','currentStyle','data','detachEvent','dir','dispatchEvent','dispatchFormChange','dispatchFormInput','document','DOCUMENT_FRAGMENT_NODE','DOCUMENT_NODE','DOCUMENT_POSITION_CONTAINED_BY','DOCUMENT_POSITION_CONTAINS','DOCUMENT_POSITION_DISCONNECTED','DOCUMENT_POSITION_FOLLOWING','DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC','DOCUMENT_POSITION_PRECEDING','DOCUMENT_TYPE_NODE','dragDrop','elements','ELEMENT_NODE','encoding','enctype','ENTITY_NODE','ENTITY_REFERENCE_NODE','fireEvent','firstChild','firstElementChild','focus','getAdjacentText','getAttribute','getAttributeNode','getAttributeNodeNS','getAttributeNS','getBoundingClientRect','getClientRects','getElementsByClassName','getElementsByTagName','getElementsByTagNameNS','getExpression','getFeature','getUserData','hasAttribute','hasAttributeNS','hasAttributes','hasChildNodes','hasOwnProperty','hideFocus','id','innerHTML','innerText','insertAdjacentElement','insertAdjacentHTML','insertAdjacentText','insertBefore','isContentEditable','isDefaultNamespace','isDefaultNamespaceURI','isDisabled','isEqualNode','isMultiLine','isPrototypeOf','isSameNode','isSupported','isTextEdit','item','lang','language','lastChild','lastElementChild','length','localName','lookupPrefix','mergeAttributes','method','moveRepetitionBlock','msBlockProgression','msBoxSizing','name','namedItem','namespaceURI','nextSibling','nodeName','nodeType','nodeValue','normalize','NOTATION_NODE','offsetHeight','offsetWidth','onabort','onactivate','onbeforeactivate','onbeforecopy','onbeforecut','onbeforedeactivate','onbeforeeditfocus','onbeforepaste','onblur','onchage','onclick','onclosecapture','oncontextmenu','oncopy','oncut','ondblclick','ondeactivate','ondrag','ondragend','ondragenter','ondragleave','ondragover','onerror','onfocus','onfocusin','onfocusout','onhelp','oninput','onkeydown','onkeypress','onkeyup','onmousedown','onmouseenter','onmouseleave','onmousemove','onmousemultiwheel','onmouseout','onmouseover','onmouseup','onmousewheel','onmove','onmoveend','onmovestart','onOffBehavior','onpaste','onpropertychange','onreadystatechange','onresize','onresizeend','onresizestart','onscroll','onsearch','onselect','onselectstart','ontimeerror','onunload','outerHTML','outerText','ownerDocument','parentNode','parentTextEdit','prefix','previousElementSibling','previousSibling','PROCESSING_INSTRUCTION_NODE','propertyIsEnumerable','querySelector','querySelectorAll','quotes','releaseCapture','removeAttribute','removeAttributeNode','removeAttributeNS','removeBehavior','removeChild','removeEventListener','removeEventSource','removeExpression','removeNode','removeRepetitionBlock','repeatMax','repeatMin','repeatStart','repetitionBlocks','repetitionIndex','repetitionTemplate','repetitionType','replace','replaceAdjacentText','replaceChild','replaceNode','reset','resetFromData','role','runtimeStyle','schemaTypeInfo','scopeName','scrollByLines','scrollByPages','scrollHeight','scrollIntoView','scrollLeft','scrollTop','scrollWidth','selectNodes','selectSingleNode','setActive','setAttributeNode','setAttributeNodeNS','setAttributeNS','setCapture','setExpression','setIdAttribute','setIdAttributeNode','setIdAttributeNS','setUserData','sourceIndex','spellcheck','style','submit','swapNode','tabIndex','tagName','tagUrn','target','templateElements','text','textContent','TEXT_NODE','title','toLocaleString','toString','uniqueID','unselectable','unwatch','urns','valueOf','watch','window');
 
     /**
@@ -53,4 +58,6 @@
     static public function printButtons($buttons=array(), $class='button-group')
     {
+        $app =& App::getInstance();
+
         if (!isset($buttons[0]) || !is_array($buttons[0])) {
             $app =& App::getInstance();
@@ -73,4 +80,7 @@
                 echo '>' . oTxt($b['value']) . '</a></li>';
             } else if (isset($b['name'])) {
+                if (in_array($b['name'], self::$unsafe_form_control_names)) {
+                    $app->logMsg(sprintf('Unsafe form control name: %s', $b['name']), LOG_NOTICE, __FILE__, __LINE__);
+                }
                 $defaults['id'] = isset($b['id']) ? $b['id'] : sprintf('sc-%s-button', $b['name']);
                 echo '<li><input';
Index: trunk/lib/Utilities.inc.php
===================================================================
--- trunk/lib/Utilities.inc.php	(revision 499)
+++ trunk/lib/Utilities.inc.php	(revision 500)
@@ -945,4 +945,8 @@
  * environment variable set in httpd.conf is a good place.
  *
+ * TODO: consider using more bits-per-character, such as done with:
+ * http://www.php.net/manual/en/function.sha1.php#86239
+ * http://blog.kevburnsjr.com/php-unique-hash
+ *
  * @access  public
  * @param   string  $val    The string to sign.
@@ -964,8 +968,12 @@
     }
 
-    // TODO: consider using more bits-per-character, such as done with:
-    // http://www.php.net/manual/en/function.sha1.php#86239
-    // http://blog.kevburnsjr.com/php-unique-hash
-    return $val . '-' . mb_strtolower(mb_substr(md5($salt . md5($val . $salt)), 0, $length));
+    switch ($app->getParam('signing_method')) {
+    case 'sha512+base64':
+        return $val . '-' . mb_substr(preg_replace('/[^\w]/', '', base64_encode(hash('sha512', $val . $salt, true))), 0, $length);
+
+    case 'md5':
+    default:
+        return $val . '-' . mb_strtolower(mb_substr(md5($salt . md5($val . $salt)), 0, $length));
+    }
 }
 
@@ -986,5 +994,5 @@
 
 /**
- * Verifies a signature appened to a value by addSignature().
+ * Verifies a signature appended to a value by addSignature().
  *
  * @access  public
@@ -995,6 +1003,4 @@
 function verifySignature($signed_val, $salt=null, $length=18)
 {
-    // All comparisons are done using lower-case strings.
-    $signed_val = mb_strtolower($signed_val);
     // Strip the value from the signed value.
     $val = removeSignature($signed_val);
@@ -1004,4 +1010,6 @@
         return true;
     } else {
+        $app =& App::getInstance();
+        $app->logMsg(sprintf('Failed signature (%s should be %s)', $signed_val, addSignature($val, $salt, $length)), LOG_DEBUG, __FILE__, __LINE__);
         return false;
     }
@@ -1255,3 +1263,2 @@
     }
 }
-
Index: trunk/lib/Validator.inc.php
===================================================================
--- trunk/lib/Validator.inc.php	(revision 499)
+++ trunk/lib/Validator.inc.php	(revision 500)
@@ -162,5 +162,5 @@
     static public function stringLength($val, $min, $max)
     {
-        return mb_strlen(trim((string)$val)) >= $min && mb_strlen($val) <= $max;
+        return mb_strlen((string)$val) >= $min && mb_strlen((string)$val) <= $max;
     }
 
