Index: trunk/lib/App.inc.php
===================================================================
--- trunk/lib/App.inc.php	(revision 684)
+++ trunk/lib/App.inc.php	(revision 685)
@@ -764,18 +764,18 @@
                 switch ($m['type']) {
                 case MSG_ERR:
-                    echo '<div data-alert class="sc-msg-error alert-box alert">' . $m['message'] . '<a href="#" class="close">&times;</a></div>';
+                    echo '<div data-alert data-closable class="sc-msg-error alert-box callout alert">' . $m['message'] . '<a class="close close-button" aria-label="Dismiss alert" data-close><span aria-hidden="true">&times;</span></a></div>';
                     break;
 
                 case MSG_WARNING:
-                    echo '<div data-alert class="sc-msg-warning alert-box warning">' . $m['message'] . '<a href="#" class="close">&times;</a></div>';
+                    echo '<div data-alert data-closable class="sc-msg-warning alert-box callout warning">' . $m['message'] . '<a class="close close-button" aria-label="Dismiss alert" data-close><span aria-hidden="true">&times;</span></a></div>';
                     break;
 
                 case MSG_SUCCESS:
-                    echo '<div data-alert class="sc-msg-success alert-box success">' . $m['message'] . '<a href="#" class="close">&times;</a></div>';
+                    echo '<div data-alert data-closable class="sc-msg-success alert-box callout success">' . $m['message'] . '<a class="close close-button" aria-label="Dismiss alert" data-close><span aria-hidden="true">&times;</span></a></div>';
                     break;
 
                 case MSG_NOTICE:
                 default:
-                    echo '<div data-alert class="sc-msg-notice alert-box info">' . $m['message'] . '<a href="#" class="close">&times;</a></div>';
+                    echo '<div data-alert data-closable class="sc-msg-notice alert-box callout primary info">' . $m['message'] . '<a class="close close-button" aria-label="Dismiss alert" data-close><span aria-hidden="true">&times;</span></a></div>';
                     break;
                 }
@@ -1079,5 +1079,5 @@
      * @return string url with attached queries and, if not using cookies, the session id
      */
-    public function url($url, $carry_args=null, $always_include_sid=false, $include_csrf_token=false)
+    public function url($url='', $carry_args=null, $always_include_sid=false, $include_csrf_token=false)
     {
         if (!$this->running) {
@@ -1118,4 +1118,9 @@
         }
 
+        // If the URL is empty, use REQUEST_URI without the QUERY_STRING.
+        if ('' == $url) {
+            $url = str_replace([getenv('QUERY_STRING'), '?'], '', getenv('REQUEST_URI'));
+        }
+
         // Get the first delimiter that is needed in the url.
         $delim = mb_strpos($url, '?') !== false ? ini_get('arg_separator.output') : '?';
@@ -1141,5 +1146,4 @@
         $url = $parts[0];
         $anchor = isset($parts[1]) ? $parts[1] : '';
-
         // $anchor =
 
@@ -1177,4 +1181,8 @@
         }
 
+        if ('' == $url) {
+            $this->logMsg(sprintf('Generated empty URL. Args: %s', getDump(func_get_args())), LOG_NOTICE, __FILE__, __LINE__);
+        }
+
         return $url;
     }
@@ -1189,5 +1197,5 @@
      * @since   09 Dec 2005 17:58:45
      */
-    public function oHREF($url, $carry_args=null, $always_include_sid=false, $include_csrf_token=false)
+    public function oHREF($url='', $carry_args=null, $always_include_sid=false, $include_csrf_token=false)
     {
         // Process the URL.
@@ -1406,8 +1414,9 @@
         }
 
-        if (preg_match('!^/!', $url)) {
-            // If relative URL is given, prepend site_url, which contains: scheme://hostname[:port]
-            $url = sprintf('%s%s', $this->getParam('site_url'), $url);
-        }
+        // FIXME: actually, it seems better to continue using a relative URL since we can't guess the port at the reverse proxy (e.g., port 3000 for browser-sync).
+        // if (preg_match('!^/!', $url)) {
+        //     // If relative URL is given, prepend site_url, which contains: scheme://hostname[:port]
+        //     $url = sprintf('%s%s', $this->getParam('site_url'), $url);k
+        // }
 
         $url = $this->url($url, $carry_args, $always_include_sid);
Index: trunk/lib/Auth_Simple.inc.php
===================================================================
--- trunk/lib/Auth_Simple.inc.php	(revision 685)
+++ trunk/lib/Auth_Simple.inc.php	(revision 685)
@@ -0,0 +1,280 @@
+<?php
+/*
+* Auth_Simple.inc
+* Code by Strangecode :: www.strangecode.com :: This document contains copyrighted information.
+* @author   Quinn Comendant <quinn@strangecode.com>
+* @version  1.0
+* @since    04 Mar 2009 21:59:03
+*/
+
+class Auth_Simple {
+
+    // Namespace of this auth object.
+    var $_ns;
+
+    // Parameters to be configured by setParam.
+    var $_params = array();
+    var $_default_params = array(
+
+        // The URL to the login script.
+        'login_url' => '/',
+
+        // The maximum amount of time a user is allowed to be logged in. They will be forced to login again if they expire.
+        // In seconds. 21600 seconds = 6 hours.
+        'login_timeout' => 21600,
+
+        // 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.
+        // In seconds. 3600 seconds = 1 hour.
+        'idle_timeout' => 3600,
+    );
+
+    /**
+     * Constructs a new authentication object.
+     *
+     * @access public
+     * @param optional array $params  A hash containing parameters.
+     */
+    function Auth_Simple($namespace='')
+    {
+    	$app =& App::getInstance();
+
+        $this->_ns = $namespace;
+
+        // Initialize default parameters.
+        $this->setParam($this->_default_params);
+
+        // Get create tables config from global context.
+        if (!is_null($app->getParam('db_create_tables'))) {
+            $this->setParam(array('create_table' => $app->getParam('db_create_tables')));
+        }
+
+        if (!isset($_SESSION['_auth_simple'][$this->_ns])) {
+            $this->clear();
+        }
+    }
+
+    /**
+     * Set the params of an auth object.
+     *
+     * @param  array $params   Array of parameter keys and value to set.
+     * @return bool true on success, false on failure
+     */
+    function setParam($params)
+    {
+        if (isset($params) && is_array($params)) {
+            // Merge new parameters with old overriding only those passed.
+            $this->_params = array_merge($this->_params, $params);
+        }
+    }
+
+    /**
+     * Return the value of a parameter, if it exists.
+     *
+     * @access public
+     * @param string $param        Which parameter to return.
+     * @return mixed               Configured parameter value.
+     */
+    function getParam($param)
+    {
+		$app =& App::getInstance();
+
+        if (isset($this->_params[$param])) {
+            return $this->_params[$param];
+        } else {
+            $app->logMsg(sprintf('Parameter is not set: %s', $param), LOG_DEBUG, __FILE__, __LINE__);
+            return null;
+        }
+    }
+
+    /**
+     * Sets a variable into a registered auth session.
+     *
+     * @access public
+     * @param mixed $key      Which value to set.
+     * @param mixed $val      Value to set variable to.
+     */
+    function set($key, $val)
+    {
+        if (!isset($_SESSION['_auth_simple'][$this->_ns]['user_data'])) {
+            $_SESSION['_auth_simple'][$this->_ns]['user_data'] = array();
+        }
+        $_SESSION['_auth_simple'][$this->_ns]['user_data'][$key] = $val;
+    }
+
+    /**
+     * Returns a specified value from a registered auth session.
+     *
+     * @access public
+     * @param mixed $key      Which value to return.
+     * @param mixed $default  Value to return if key not found in user_data.
+     * @return mixed          Value stored in session.
+     */
+    function get($key, $default='')
+    {
+        if (isset($_SESSION['_auth_simple'][$this->_ns][$key])) {
+            return $_SESSION['_auth_simple'][$this->_ns][$key];
+        } else if (isset($_SESSION['_auth_simple'][$this->_ns]['user_data'][$key])) {
+            return $_SESSION['_auth_simple'][$this->_ns]['user_data'][$key];
+        } else {
+            return $default;
+        }
+    }
+
+    /**
+     * Clear any authentication tokens in the current session. A.K.A. logout.
+     *
+     * @access public
+     */
+    function clear()
+    {
+        $_SESSION['_auth_simple'][$this->_ns] = array('authenticated' => false);
+    }
+
+    /*
+    * Runs a full login sequence: clear old session, validate auth, create session.
+    *
+    * @access   public
+    * @param    int     $user_id     User ID.
+    * @return   mixed                   False on failure, true on success.
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @version  1.0
+    * @since    04 Mar 2009 21:07:33
+    */
+    function login($user_id, $password, $callback)
+    {
+        global $acct;
+
+		$app =& App::getInstance();
+
+        $this->clear();
+
+        if (!is_callable($callback)) {
+            $app->logMsg(sprintf('Not callable: %s', getDump($callable)), LOG_ERR, __FILE__, __LINE__);
+            return false;
+        }
+
+        if (!call_user_func($callback, $user_id, $password)) {
+            $app->logMsg(sprintf('Authentication failed for user_id: %s', $user_id), LOG_NOTICE, __FILE__, __LINE__);
+            return false;
+        }
+
+        $app->logMsg(sprintf('Authentication successful for user_id: %s', $user_id), LOG_INFO, __FILE__, __LINE__);
+        return $this->createSession($user_id);
+    }
+
+
+    /*
+    *
+    *
+    * @access   public
+    * @param
+    * @return
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @version  1.0
+    * @since    04 Mar 2009 21:07:33
+    */
+    function createSession($user_id)
+    {
+		$app =& App::getInstance();
+
+        // Register authenticated session.
+        $_SESSION['_auth_simple'][$this->_ns] = array(
+            'authenticated'         => true,
+            'user_id'               => $user_id,
+            'login_datetime'        => date('Y-m-d H:i:s'),
+            'last_access_datetime'  => date('Y-m-d H:i:s'),
+            'remote_ip'             => getRemoteAddr(),
+        );
+
+        return true;
+    }
+
+    /*
+    *
+    *
+    * @access   public
+    * @param
+    * @return
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @version  1.0
+    * @since    04 Mar 2009 21:10:41
+    */
+    function isLoggedIn()
+    {
+        $app =& App::getInstance();
+
+        // Test login with information stored in session. Skip IP matching for users from trusted networks.
+        if (isset($_SESSION['_auth_simple'][$this->_ns]['authenticated'])
+            && true === $_SESSION['_auth_simple'][$this->_ns]['authenticated']
+            && !empty($_SESSION['_auth_simple'][$this->_ns]['user_id'])
+            && strtotime($_SESSION['_auth_simple'][$this->_ns]['login_datetime']) > time() - $this->_params['login_timeout']
+            && strtotime($_SESSION['_auth_simple'][$this->_ns]['last_access_datetime']) > time() - $this->_params['idle_timeout']
+        ) {
+            // User is authenticated!
+            $_SESSION['_auth_simple'][$this->_ns]['last_access_datetime'] = date('Y-m-d H:i:s');
+            return true;
+        } else if (isset($_SESSION['_auth_simple'][$this->_ns]['authenticated']) && true === $_SESSION['_auth_simple'][$this->_ns]['authenticated']) {
+            // User is authenticated, but login has expired.
+            if (strtotime($_SESSION['_auth_simple'][$this->_ns]['last_access_datetime']) > time() - 43200) {
+                // Only raise message if last session is less than 12 hours old.
+                // TODO: Can we move this message to requireLogin() so it doesn't display unless the user expects it to?
+                // $app->raiseMsg(_("Your session has expired. You need to log-in again."), MSG_NOTICE, __FILE__, __LINE__);
+            }
+            // Log the reason for login expiration.
+            $expire_reasons = array();
+            if (empty($_SESSION['_auth_simple'][$this->_ns]['user_id'])) {
+                $expire_reasons[] = 'user_id not found';
+            }
+            if (strtotime($_SESSION['_auth_simple'][$this->_ns]['login_datetime']) <= time() - $this->_params['login_timeout']) {
+                $expire_reasons[] = 'login_timeout expired';
+            }
+            if (strtotime($_SESSION['_auth_simple'][$this->_ns]['last_access_datetime']) <= time() - $this->_params['idle_timeout']) {
+                $expire_reasons[] = 'idle_timeout expired';
+            }
+            $app->logMsg(sprintf('user_id %s session expired: %s', $this->get('user_id'), join(', ', $expire_reasons)), LOG_INFO, __FILE__, __LINE__);
+        }
+
+        // User is not authenticated.
+        $this->clear();
+        return false;
+    }
+
+    /**
+     * Redirect user to login page if they are not logged in.
+     *
+     * @param string $message The text description of a message to raise.
+     * @param int    $type    The type of message: MSG_NOTICE,
+     *                        MSG_SUCCESS, MSG_WARNING, or MSG_ERR.
+     * @param string $file    __FILE__.
+     * @param string $line    __LINE__.
+     * @access public
+     */
+    function requireLogin($message='', $type=MSG_NOTICE, $file=null, $line=null)
+    {
+		$app =& App::getInstance();
+
+        if (!$this->isLoggedIn()) {
+            // Display message for requiring login. (RaiseMsg will ignore empty strings.)
+            if ('' != $message) {
+                $app->raiseMsg($message, $type, $file, $line);
+            }
+
+            // Login scripts must have the same 'login' tag for boomerangURL verification/manipulation.
+            $app->setBoomerangURL(absoluteMe(), 'login');
+            $app->dieURL($this->_params['login_url']);
+        }
+    }
+
+    /**
+     * Stub method to fulfill requirements by Version()
+     *
+     * @param  string   $null
+     * @return string   Empty string
+     */
+    function getUsername($null=null)
+    {
+        return '';
+    }
+}
+
+?>
Index: trunk/lib/FormValidator.inc.php
===================================================================
--- trunk/lib/FormValidator.inc.php	(revision 684)
+++ trunk/lib/FormValidator.inc.php	(revision 685)
@@ -219,18 +219,18 @@
                     switch ($e['type']) {
                     case MSG_ERR:
-                        echo '<div data-alert class="sc-msg-error alert-box alert">' . $e['message'] . '<a href="#" class="close">&times;</a></div>';
+                        echo '<div data-alert data-closable class="sc-msg-error alert-box callout alert">' . $e['message'] . '<a class="close close-button" aria-label="Dismiss alert" data-close><span aria-hidden="true">&times;</span></a></div>';
                         break;
 
                     case MSG_WARNING:
-                        echo '<div data-alert class="sc-msg-warning alert-box warning">' . $e['message'] . '<a href="#" class="close">&times;</a></div>';
+                        echo '<div data-alert data-closable class="sc-msg-warning alert-box callout warning">' . $e['message'] . '<a class="close close-button" aria-label="Dismiss alert" data-close><span aria-hidden="true">&times;</span></a></div>';
                         break;
 
                     case MSG_SUCCESS:
-                        echo '<div data-alert class="sc-msg-success alert-box alert">' . $e['message'] . '<a href="#" class="close">&times;</a></div>';
+                        echo '<div data-alert data-closable class="sc-msg-success alert-box callout success">' . $e['message'] . '<a class="close close-button" aria-label="Dismiss alert" data-close><span aria-hidden="true">&times;</span></a></div>';
                         break;
 
                     case MSG_NOTICE:
                     default:
-                        echo '<div data-alert class="sc-msg-notice alert-box info">' . $e['message'] . '<a href="#" class="close">&times;</a></div>';
+                        echo '<div data-alert data-closable class="sc-msg-notice alert-box callout primary info">' . $e['message'] . '<a class="close close-button" aria-label="Dismiss alert" data-close><span aria-hidden="true">&times;</span></a></div>';
                         break;
                     }
Index: trunk/lib/HTML.inc.php
===================================================================
--- trunk/lib/HTML.inc.php	(revision 684)
+++ trunk/lib/HTML.inc.php	(revision 685)
@@ -64,5 +64,5 @@
         if (!isset($buttons[0]) || !is_array($buttons[0])) {
             $app =& App::getInstance();
-            $app->logMsg(sprintf('Incorrect parameters passed to HTML::buttons(): %s', getDump($buttons)), LOG_DEBUG, __FILE__, __LINE__);
+            $app->logMsg(sprintf('Incorrect parameters passed to %s: %s', __METHHOD__, getDump($options)), LOG_NOTICE, __FILE__, __LINE__);
             return false;
         }
@@ -237,5 +237,5 @@
         if (!isset($options) || !is_array($options)) {
             $app =& App::getInstance();
-            $app->logMsg(sprintf('Incorrect parameters passed to HTML::printSelectOptions(): %s', getDump($options)), LOG_DEBUG, __FILE__, __LINE__);
+            $app->logMsg(sprintf('Incorrect parameters passed to %s: %s', __METHHOD__, getDump($options)), LOG_NOTICE, __FILE__, __LINE__);
             return false;
         }
Index: trunk/lib/Lock.inc.php
===================================================================
--- trunk/lib/Lock.inc.php	(revision 684)
+++ trunk/lib/Lock.inc.php	(revision 685)
@@ -272,4 +272,9 @@
 
         $this->initDB();
+
+        if ($this->_auth->get('user_id') == '' || filter_var($this->_auth->get('user_id'), FILTER_VALIDATE_INT) === false) {
+            $app->logMsg(sprintf("auth->get('user_id') returns a non-integer: %s", $this->_auth->get('user_id')), LOG_ERR, __FILE__, __LINE__);
+            return false;
+        }
 
         // Expire old locks.
Index: trunk/lib/SortOrder.inc.php
===================================================================
--- trunk/lib/SortOrder.inc.php	(revision 684)
+++ trunk/lib/SortOrder.inc.php	(revision 685)
@@ -213,10 +213,10 @@
             if ($this->sort_by == $col) {
                 if (mb_strtolower($this->order) == 'desc') {
-                    ?><a href="<?php echo $app->oHREF($_SERVER['PHP_SELF'] . '?sort=' . $col . '&order=ASC'); ?>" title="<?php echo _("Change to ascending sort order"); ?>" class="sc-sort sc-desc"><?php echo $this->desc_widget; ?></a><?php echo $col_name; ?><?php
+                    ?><a href="<?php echo $app->oHREF($_SERVER['PHP_SELF'] . '?sort=' . $col . '&order=ASC'); ?>" data-tooltip title="<?php echo _("Change to ascending sort order"); ?>" class="sc-sort sc-desc"><?php echo $this->desc_widget; ?></a><?php echo $col_name; ?><?php
                 } else {
-                    ?><a href="<?php echo $app->oHREF($_SERVER['PHP_SELF'] . '?sort=' . $col . '&order=DESC'); ?>" title="<?php echo _("Change to descending sort order"); ?>" class="sc-sort sc-asc"><?php echo $this->asc_widget; ?></a><?php echo $col_name; ?><?php
+                    ?><a href="<?php echo $app->oHREF($_SERVER['PHP_SELF'] . '?sort=' . $col . '&order=DESC'); ?>" data-tooltip title="<?php echo _("Change to descending sort order"); ?>" class="sc-sort sc-asc"><?php echo $this->asc_widget; ?></a><?php echo $col_name; ?><?php
                 }
             } else {
-                ?><a href="<?php echo $app->oHREF($_SERVER['PHP_SELF'] . '?sort=' . $col . '&order=' . $default_order); ?>" title="<?php echo sprintf(_("Sort by %s"), $col_name); ?>" class="sc-sort"><?php echo $col_name; ?></a><?php
+                ?><a href="<?php echo $app->oHREF($_SERVER['PHP_SELF'] . '?sort=' . $col . '&order=' . $default_order); ?>" data-tooltip title="<?php echo sprintf(_("Sort by %s"), $col_name); ?>" class="sc-sort"><?php echo $col_name; ?></a><?php
             }
         } else {
