JasonWoof Got questions, comments, patches, etc.? Contact Jason Woofenden
Fix db_get_value after mysql->mysqli upgrade
[wfpl.git] / session.php
index 4bbaea9..75508c0 100644 (file)
 <?php
 
-#  Copyright (C) 2006 Jason Woofenden
+# This program is in the public domain within the United States. Additionally,
+# we waive copyright and related rights in the work worldwide through the CC0
+# 1.0 Universal public domain dedication, which can be found at
+# http://creativecommons.org/publicdomain/zero/1.0/
+
+
+# The functions in this file assume that you have this database table:
+# drop table if exists wfpl_sessions;
+# create table wfpl_sessions (
+#      id int unique auto_increment,
+#      session_key varchar(16),
+#      idle_timeout int,
+#      expires int,
+#      expires_max int,
+#      value text
+# ) CHARSET=utf8;
+
+# You'll want to use these:
 #
-#  This file is part of wfpl.
+# session_exists()
+# session_new('timeout', 'max_len')
+# session_set('key', 'value')
+# session_sets(['key': 'value', 'key2': 'val2'])
+# session_get('key')
+# session_clear() # removes all set() values
+# session_clear('key')
+# session_kill()
 #
-#  wfpl is free software; you can redistribute it and/or modify it under the
-#  terms of the GNU Lesser General Public License as published by the Free
-#  Software Foundation; either version 2.1 of the License, or (at your option)
-#  any later version.
-#
-#  wfpl 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 Lesser General Public License for
-#  more details.
-#
-#  You should have received a copy of the GNU Lesser General Public License
-#  along with wfpl; if not, write to the Free Software Foundation, Inc., 51
-#  Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
-
-# you'll need this file that calls db_connect()
-if(!isset($GLOBALS['wfpl_db_handle'])) {
-       if(file_exists('db_connect.php')) {
-               require_once('db_connect.php');
-       } elseif(file_exists('code/db_connect.php')) {
-               require_once('code/db_connect.php');
-       } else {
-               die("session.php requires a file db_connect.php or that you call db_connect() first. See code/wfpl/db.php for more information.");
-       }
-}
+# All session data is cached in globals, so:
+# 1.   don't set large amonuts of data
+# 2.   session_get() is very fast (no db access)
 
-# and these database tables:
-# create table wfpl_sessions (id int unique auto_increment, session_key varchar(16), length int, expires int);
-# create table wfpl_session_data (id int unique auto_increment, session_id int, name varchar(100), value text);
-# run this command to install/clear the tables:
-#   mysql DATABASE_NAME < code/wfpl/examples/session.sql
-# note: you may need these parameters for mysql:  -u USERNAME -p
-
-# GLOSSARY
-#
-# session_key  16 digit string identifying the session
-# session_id   integer id of the record in the "sessions" table of the database
-# UNTIL_CLOSE  a constant passed as session length to indicate "until browser window closes"
-
-
-# session_id is kept in $GLOBALS
-# session_key is sent as a cookie, and thus appears in $_REQUEST. The clean version is in $GLOBALS
 
 # generate a new random 16-character string
 function session_generate_key() {
-       $character_set = "abcdefghijklmnopqrstuvwqyzABCDEFGHIJKLMNOPQRSTUVWQYZ0123456789";
-    $id = "                ";
+       $character_set = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
+       $id = "                ";
 
        # PHP 4.2.0 and up seed the random number generator for you.
        # Lets hope that it seeds with something harder to guess than the clock.
-    for($i = 0; $i < 16; ++$i) {
-        $id{$i} = $character_set{mt_rand(0, 61)};
-    }
+       for($i = 0; $i < 16; ++$i) {
+               $id{$i} = $character_set{mt_rand(0, 61)};
+       }
 
-    return $id;
+       return $id;
 }
 
-# track this user with a session cookie (ie a cookie that goes away when the
-# user closes the browser). The timestamp is how long to track the session in
-# the database. Defaults to one day.
-function session_new($length = 86400) {
+# start a new session, tracked by a browser "session cookie".
+#
+# args:
+#    $idle_timeout (seconds) session ends after this much inactivity (or up to 10% less)
+#    $max_length (seconds) session ends after this long, regardless of activity
+function session_new($idle_timeout = 129600 /* 36 hours */, $max_length = 604800 /* 1 week */) {
+       kill_session();
+
        $session_key = session_generate_key();
 
-       db_insert('wfpl_sessions', 'session_key,length', $session_key, $length);
-       $GLOBALS['session_id'] = db_auto_id();
-       $GLOBALS['session_key'] = $session_key;
-       $_REQUEST['session_key'] = $session_key; #just in case someone calls session_exists() after session_new()
-       session_touch($length);
+       $now = time();
+       $row = array(
+               'session_key' => $session_key,
+               'idle_timeout' => $idle_timeout,
+               'expires' => $now + $idle_timeout,
+               'expires_max' => $now + $max_length,
+               'value' => ''
+       );
+
+       db_insert_assoc('wfpl_sessions', $row);
+       $session_id = db_auto_id();
+       $GLOBALS['wfpl_session'] = array(
+               'exists' => true,
+               'id' => $session_id,
+               'key' => $session_key,
+               'idle_timeout' => $idle_timeout,
+               'expires' => $now + $idle_timeout,
+               'expires_max' => $now + $max_length,
+               'value' => array()
+       );
+       session_set_cookie();
+       return $session_key;
 }
 
-# call to renew the timeout for the session.
-# assumes there's a session. call session_init() if you'd like one auto-create one if not found.
-function session_touch($length = false) {
-       if(!$length) {
-               $length = db_get_value('wfpl_sessions', 'length', 'where id=%i', $GLOBALS['session_id']);
+function session_set_cookie() {
+       if (session_exists()) {
+               if (!isset($GLOBALS['wfpl_session']['cookie_set'])) {
+                       $GLOBALS['wfpl_session']['cookie_set'] = true;
+                       header('Set-Cookie: session_key=' . $GLOBALS['wfpl_session']['key'] . '; Path=/');
+               }
        }
-       $expires = time() + $length;
-
-       header('Set-Cookie: session_key=' . $GLOBALS['session_key']);
+}
 
-       db_update('wfpl_sessions', 'expires', $expires, 'where id=%i', $GLOBALS['session_id']);
+# this is a helper function. See session_new()
+function session_touch() {
+       if(!session_exists()) {
+               return;
+       }
+       # is the session extendable?
+       if ($GLOBALS['wfpl_session']['expires'] < $GLOBALS['wfpl_session']['expires_max']) {
+               # would this extend the session by at least 10%?
+               $now = time();
+               $last_activity = $GLOBALS['wfpl_session']['expires'] - $GLOBALS['wfpl_session']['idle_timeout'];
+               # don't db_update if only a tiny fraction of the idle timeout has passed
+               $db_threshold = ceil(0.1 * $GLOBALS['wfpl_session']['idle_timeout']);
+               if ($now > $last_activity + $db_threshold) {
+                       $expires = min(
+                               $GLOBALS['wfpl_session']['expires_max'],
+                               $now + $GLOBALS['wfpl_session']['idle_timeout']
+                       );
+                       db_update('wfpl_sessions', 'expires', $expires, 'where id=%i', $GLOBALS['wfpl_session']['id']);
+                       $GLOBALS['wfpl_session']['expires'] = $expires;
+               }
+       }
 }
 
 # delete the current session
@@ -91,54 +118,81 @@ function kill_session() {
        if(!session_exists()) {
            return;
        }
-       _kill_session($GLOBALS['session_id']);
-}
-
-# for internal use. use kill_session() above
-function _kill_session($id) {
-       db_delete('wfpl_session_data', 'where session_id=%i', $id);
-       db_delete('wfpl_sessions', 'where id=%i', $id);
+       db_delete('wfpl_sessions', 'where id=%i', $GLOBALS['wfpl_session']['id']);
+       $GLOBALS['wfpl_session'] = array('exists' => false);
 }
 
 # delete expired sessions from database
 function session_purge_old() {
-       $now = time();
-       $expired_sessions = db_get_column('wfpl_sessions', 'id', 'where expires < %i', $now);
-       if($expired_sessions) foreach($expired_sessions as $expired_session) {
-               _kill_session($expired_session);
-       }
+       db_delete('wfpl_sessions', 'where expires < %i', time());
 }
 
 # return true if a session exists
 function session_exists() {
-       if(!isset($_REQUEST['session_key'])) {
-               return false;
+       if (isset($GLOBALS['wfpl_session'])) {
+               return $GLOBALS['wfpl_session']['exists'];
        }
 
-       if(isset($GLOBALS['session_id'])) {
-               return true;
+       $GLOBALS['wfpl_session'] = array('exists' => false);
+
+       if(!isset($_COOKIE['session_key'])) {
+               return false;
        }
 
-       $session_key = ereg_replace('[^a-zA-Z0-9]', '', $_REQUEST['session_key']);
+       $session_key = preg_replace('|[^a-z0-9]|i', '', $_COOKIE['session_key']);
 
        if(!strlen($session_key) == 16) {
                return false;
        }
 
-       $GLOBALS['session_key'] = $session_key;
-
-       session_purge_old();
-       $id = db_get_value('wfpl_sessions', 'id', 'where session_key=%"', $session_key);
-       if($id === false) {
+       $row = db_get_assoc('wfpl_sessions', 'id,idle_timeout,expires,expires_max,value', 'where session_key=%"', $session_key);
+       if($row === false) {
                return false;
        }
+       $now = time();
+       if ($now >= (int) $row['expires']) {
+               session_purge_old();
+               return false;
+       }
+
+       $GLOBALS['wfpl_session']['exists'] = true;
+       $GLOBALS['wfpl_session']['id'] = $row['id'];
+       $GLOBALS['wfpl_session']['idle_timeout'] = (int) $row['idle_timeout'];
+       $GLOBALS['wfpl_session']['expires'] = (int) $row['expires'];
+       $GLOBALS['wfpl_session']['expires_max'] = (int) $row['expires_max'];
+       $GLOBALS['wfpl_session']['key'] = $session_key;
+
+       if (strlen($row['value']) && is_array($parsed = json_decode($row['value'], true))) {
+               $GLOBALS['wfpl_session']['value'] = $parsed;
+       } else {
+               $GLOBALS['wfpl_session']['value'] = array();
+       }
+
+       # mark session as not idle
+       session_touch();
 
-       $GLOBALS['session_id'] = $id;
        return true;
 }
 
+
+# generate a random password using only letters and numbers that look
+# particularly unique
+function new_readable_password($length = 8) {
+       $character_set = "ABCDEFHJKLMNPQRTUWXY34789";
+       $code = "";
+
+       # PHP 4.2.0 and up seed the random number generator for you.
+       # Lets hope that it seeds with something harder to guess than the clock.
+       while($length--) {
+               $code .= $character_set{mt_rand(0, 24)}; # inclusive
+       }
+
+       return $code;
+}
+
+# depricated
 # return username if a session exists and is authenticated
-function session_exists_and_authed() {
+function logged_in() {
        if(!session_exists()) {
                return false;
        }
@@ -147,27 +201,100 @@ function session_exists_and_authed() {
 }
 
 
-# find existing session, or make one
+# depricated
+function session_exists_and_authed() {
+       return logged_in();
+}
+
+
+# depricated
+# return true if a session exists and is authenticated
+function logged_in_as_admin() {
+       if(!session_exists()) {
+               return false;
+       }
+
+       if(session_get('auth_admin')) {
+               return true;
+       }
+       return false;
+}
+
+
+# find existing session, or make one (name "session_init" was taken)
 function init_session() {
        if(!session_exists()) {
                session_new();
        }
 }
 
-# save a variable into the session
+# internal use only (write session cache to db)
+function _sync_session() {
+       if (count($GLOBALS['wfpl_session']['value']) > 0) {
+               $value = json_encode($GLOBALS['wfpl_session']['value']);
+       } else {
+               $value = '';
+       }
+       db_update('wfpl_sessions', 'value', $value, 'where id=%i', $GLOBALS['wfpl_session']['id']);
+}
+
+# save data into the session
+# $value can be anything json_encode()able
 function session_set($name, $value) {
-       session_clear($name);
-       db_insert('wfpl_session_data', 'session_id,name,value', $GLOBALS['session_id'], $name, $value);
+       init_session();
+       if (isset($GLOBALS['wfpl_session']['value'][$name])) {
+               if ($GLOBALS['wfpl_session']['value'][$name] === $value) {
+                       return;
+               }
+       }
+       $GLOBALS['wfpl_session']['value'][$name] = $value;
+       _sync_session();
+}
+
+# save data into the session
+# values can be anything json_encode()able
+function session_sets($assoc) {
+       init_session();
+       $dirty = false;
+       foreach ($assoc as $name => &$value) {
+               if (isset($GLOBALS['wfpl_session']['value'][$name])) {
+                       if ($GLOBALS['wfpl_session']['value'][$name] === $value) {
+                               continue;
+                       }
+               }
+               $GLOBALS['wfpl_session']['value'][$name] = $value;
+               $dirty = true;
+       }
+       if ($dirty) {
+               _sync_session();
+       }
 }
 
 # remove variable from the session
-function session_clear($name) {
-       db_delete('wfpl_session_data', 'where session_id=%i && name=%"', $GLOBALS['session_id'], $name);
+# with no args: clear all
+function session_clear($name = -1) {
+       if(!session_exists()) {
+               return;
+       }
+       if ($name === -1) {
+               if (count($GLOBALS['wfpl_session']['value']) > 0) {
+                       $GLOBALS['wfpl_session']['value'] = array();
+                       _sync_session();
+               }
+       } elseif (isset($GLOBALS['wfpl_session']['value'][$name])) {
+               unset($GLOBALS['wfpl_session']['value'][$name]);
+               _sync_session();
+       }
 }
 
 # get a variable into the session
 function session_get($name) {
-       return db_get_value('wfpl_session_data', 'value', 'where session_id=%i && name=%"', $GLOBALS['session_id'], $name);
+       if(!session_exists()) {
+               return false;
+       }
+       if (isset($GLOBALS['wfpl_session']['value'][$name])) {
+               return $GLOBALS['wfpl_session']['value'][$name];
+       } else {
+               return false;
+       }
 }
-
-?>