<?php
/*
 * $RCSfile: GallerySession.class,v $
 *
 * Gallery - a web based photo album viewer and editor
 * Copyright (C) 2000-2005 Bharat Mediratta
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or (at
 * your option) any later version.
 *
 * This program 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 this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA  02110-1301, USA.
 */
/**
 * @package GalleryCore
 * @version $Revision: 1.87 $ $Date: 2005/08/26 23:47:06 $
 * @author Bharat Mediratta <bharat@menalto.com>
 */

/*
 * Define gallery session key for this install
 */
define('SESSION_ID_PARAMETER', 'GALLERYSID');

/**
 * Container for session related data
 *
 * @package GalleryCore
 * @subpackage Classes
 */
class GallerySession {

    /*
     * ****************************************
     *                 Members
     * ****************************************
     */

    /**
     * The time this session was created
     *
     * @var string $_creationTime
     * @access private
     */
    var $_creationTime;

    /**
     * The ID of this session
     *
     * @var string $_sessionId
     * @access private
     */
    var $_sessionId;

    /**
     * Is it ok to rely on cookies for this session?
     *
     * @var bool $_isUsingCookies
     * @access private
     */
    var $_isUsingCookies = false;

    /**
     * The session data as loaded from file
     *
     * @var array $_loadedSessionData;
     * @access private
     */
    var $_loadedSessionData;

    /**
     * The session data
     *
     * @var array $_sessionData
     * @access private
     */
    var $_sessionData;

    /**
     * The domain for our cookie
     *
     * @var string $_cookieDomain
     * @access private
     */
    var $_cookieDomain;

    /**
     * The path for our cookie
     *
     * @var string $_cookiePath
     * @access private
     */
    var $_cookiePath;

    /**
     * A set of identifying values that we can use to verify that the session is coming
     * from the same browser as it used to (to prevent session hijacking).
     *
     * @var array $_remoteIdentifier
     * @access private
     */
    var $_remoteIdentifier;

    /*
     * ****************************************
     *                 Methods
     * ****************************************
     */

    /**
     * Either create a new session, or attach to an existing one.
     *
     * return object GalleryStatus a status code
     */
    function init() {
	global $gallery;

	/* Check to see if we have an existing session. */
	$this->_sessionId = null;
	if (!empty($_COOKIE[SESSION_ID_PARAMETER])) {
	    /* Fix php HTTP_COOKIE header bug http://bugs.php.net/bug.php?id=32802 */
	    GalleryUtilities::fixCookieVars();

	    /*
	     * If we get the id parameter as a cookie, then it also means that
	     * cookies are functioning.
	     */
	    $this->_sessionId = $_COOKIE[SESSION_ID_PARAMETER];
	    $this->_isUsingCookies = true;

	    /* Allow the URL to override the cookie, in rare occasions */
	    $sessionId = GalleryUtilities::getRequestVariables(SESSION_ID_PARAMETER);
	    if (!empty($sessionId)) {
		$this->_sessionId = $sessionId;
	    }
	} else {
	    /*
	     * Many search engine crawlers don't use cookies.  Normally this leads to us putting
	     * the session id in the url.  But doing so causes the search engine to do a lot of
	     * extra work to weed out the session id, which they may not do very well.  So if we
	     * detect that this is a search engine, pretend that they accept cookies.  We'll
	     * create a session id like "google-1.2.3.4" where we track the search engine id and
	     * the ip address so that the crawler stays roughly in the same session (and random
	     * users who sneakily use crawler tags in their User-Agent don't interfere).
	     */
	    $searchEngineId = GalleryUtilities::identifySearchEngine();
	    if (isset($searchEngineId)) {
		$this->_isUsingCookies = true;
		$this->_sessionId =
		    sprintf('%s-%s', $searchEngineId, GalleryUtilities::getRemoteHostAddress());
	    } else {
		/* When logging out (resetting the session), we already know if cookies are used */
		if (!$this->isUsingCookies()) {
		    $this->_isUsingCookies = false;
		}
		$this->_sessionId = GalleryUtilities::getRequestVariables(SESSION_ID_PARAMETER);
	    }
	}


	/*
	 * If we don't have a session id at this point, create one. And expire the session if
	 * necessary
	 */
	list ($ret, $valid) = $this->_isSessionValid();
	if ($ret->isError()) {
	    return $ret->wrap(__FILE__, __LINE__);
	}
	if (!$valid) {
	    do {
		$this->_sessionId = md5(microtime() . rand(1, 32767));

		$ret = $this->_loadSessionData();
		if ($ret->isError()) {
		    return $ret->wrap(__FILE__, __LINE__);
		}
		/* Ensure we don't randomly pick an id already in use.. */
	    } while (!empty($this->_sessionData));
	} else {
	    /*
	     * Sanitize the session id (which may have come from user input) to
	     * avoid possibly writing outside the session storage dir.
	     */
	    $this->_sessionId = preg_replace('/[^a-zA-Z0-9]/', '', $this->_sessionId);

	    /* Load session state */
	    $ret = $this->_loadSessionData();
	    if ($ret->isError()) {
		return $ret->wrap(__FILE__, __LINE__);
	    }
	}

	$forceResendCookie = false;
	$forceSessionSave = false;

	/* Verify the remote address to avoid casual session hijacking */
	$currentRemoteIdentifier = $this->_getRemoteIdentifier();

	if (!isset($this->_remoteIdentifier) ||
	    $this->_compareIdentifiers($this->_remoteIdentifier,
				       $currentRemoteIdentifier) == 0) {
	    /* If we upgrade, allowSessionAccess could not be missing */
	    $allowFrom = @$gallery->getConfig('allowSessionAccess');
	    if (!$allowFrom || $currentRemoteIdentifier[0] != $allowFrom) {
		if ($gallery->getDebug()) {
		    $gallery->debug('Session hijack detected: saved vs. current below');
		    $gallery->debug_r($this->_remoteIdentifier);
		    $gallery->debug_r($currentRemoteIdentifier);
		}

		/*
		 * The session was not created from this browser address, so reset
		 * our data to prevent hijacking.
		 */
		$this->_remoteIdentifier = $currentRemoteIdentifier;
		$this->_emptySessionData();
		$this->_sessionId = md5(microtime() . rand(1, 32767));
		$forceResendCookie = true;
		$forceSessionSave = true;
	    }
	}

	/* Don't save session / send cookie for DownloadItem, CSS, migrate.Redirect requests .. */
	if ($this->_shouldSaveSession()) {
	    /* It's not one of the special cases; create session / send cookie if necessary */
	    if ($forceSessionSave) {
		$ret = $this->save();
		if ($ret->isError()) {
		    return $ret->wrap(__FILE__, __LINE__);
		}
	    }

	    /*
	     * As part of the cookie management, we are forced to append the SID to all DownloadItem
	     * URLs in embedded G2 if cookie path / domain are not configured
	     */
	    list ($ret, $this->_cookieDomain) = GalleryCoreApi::getPluginParameter('module', 'core',
										   'cookie.domain');
	    if ($ret->isError()) {
		return $ret->wrap(__FILE__, __LINE__);
	    }
	    $urlGenerator =& $gallery->getUrlGenerator();
	    list ($ret, $this->_cookiePath) = $urlGenerator->getCookiePath();
	    if ($ret->isError()) {
		return $ret->wrap(__FILE__, __LINE__);
	    }

	    if (!isset($_COOKIE[SESSION_ID_PARAMETER]) ||
		$_COOKIE[SESSION_ID_PARAMETER] != $this->_sessionId ||
		$forceResendCookie) {
		/*
		 * Send back a cookie.
		 *
		 * TODO: Need to be able to decide for certain that the browser isn't
		 * accepting cookies so that we can stop sending them.  We can do this
		 * by recording how many times we've sent a cookie, and how many times
		 * that we've received one back in return.  Leave that for later.
		 */
		$cookie = 'Set-Cookie: ' . SESSION_ID_PARAMETER . '=' . $this->_sessionId;

		list ($ret, $sessionLifetime) =
		    GalleryCoreApi::getPluginParameter('module', 'core', 'session.lifetime');
		if ($ret->isError()) {
		    if ($ret->getErrorCode() & ERROR_STORAGE_FAILURE) {
			/*
			 * During installation it's possible that the database isn't around yet.
			 * Just keep going.
			 */
			$sessionLifetime = 0;
		    } else {
			return $ret->wrap(__FILE__, __LINE__);
		    }
		}

		if ($sessionLifetime > 0) {
		    $expirationDate = GalleryUtilities::getHttpDate(time() + $sessionLifetime);
		    $cookie .= '; expires=' . $expirationDate;
		}

		/* Because of short urls, the cookie path must always be set explicitly */
		$cookie .= '; path=' . $this->_cookiePath;

		/*
		 * Set the cookie domain only if needed, i.e. embedded multi-subdomain installs
		 * that is when G2 is installed on a different subdomain than the embedding
		 * application
		 *
		 * Q: Why not set the cookie domain to .example.com (omitting the subdomains) and
		 * and the cookie path to /?
		 * A: This is actually a perfect fix (we had it in cvs between beta 3 and beta 4),
		 * because the case where a browser sends back multiple cookies is completely avoided.
		 * But it has a major flaw: security! When people share a common domain name, e.g.
		 * by example.com/~accountName/ or by accountName.example.com, they will all have
		 * cookies with .example.com and /. To differentiate the cookies, we introduced
		 * the cookieId, i.e. each G2 install had its own unique cookie name.
		 * But when a user accessed multiple accounts on this shared domain, the G2 cookie
		 * is sent to all accounts which opens the door for session hijacking.
		 * This single reason, security, made us not choose this approach.
		 *
		 * Q: Why not set the cookie domain to the actual host string (i.e. .www.example.com
		 * when G2 is accessed like that or .example.com in other requests, ...)?
		 * A: Because in RFC 2965, there is no rule in what order the browser should send back
		 * the cookies. And thus, php/G2 wouldn't know which is the right cookie.
		 *
		 * Q: Why not just omit the cookie domain in the set cookie calls?
		 * A: Actually, this is a good solution. Because if no cookie domain was set, the
		 * browser sends only cookies back that match the requested domain exactly. So it
		 * won't return a example.com cookie for www.example.com and the other way around.
		 * But, and this is a big but, Internet Explorer doesn't conform to the RFC 2965.
		 * IE sends back example.com and www.example.com cookies when it shouldn't.
		 * Together with the php bug (least, most specific cookie match in HTTP_COOKIE),
		 * this results in an unpredictable behavior for various php version / IE
		 * scenarios.
		 * Luckily we can fix this manually with fixCookieVars(). That's why we chose this
		 * approach.
		 *
		 * Q: Why append the session id in embedded G2 to all DownloadItem URLs?
		 * A: In embedded G2, all DownloadItem requests still go directly to G2 and not
		 * through the emApp for performance reasons. If we set the cookie path in
		 * embedded G2 to a path that matches embedded and standalone G2, then the
		 * standalone G2 cookies always have precendence over the cookies from embedded
		 * G2. This leads to cookie conflicts, if the two cookies correspond to different
		 * sessions.
		 * That's why we are forced to append the session id to embedded URLs that require
		 * session management and go directly to standalone G2. DownloadItem is the only
		 * request that falls into this category.
		 *
		 * Q: Why force the G2 base (standalone) path for java applet cookies?
		 * A: Because the applets talk to G2 directly. If the cookie path was set to the
		 * embedded G2 path, then it would not be selected for the HTTP requests of the
		 * applet to G2, because it wouldn't path-match.
		 *
		 * Therefore we don't set the cookie domain by default and offer the option to set
		 * it to a configured value if it is required (embedded multi-subdomain G2).
		 * In embedded G2, we have to append the session id to all DownloadItem unless the
		 * cookie path is configured such that standalone and embedded G2 set the same
		 * cookie path.
		 */
		if (!empty($this->_cookieDomain)) {
		    $cookie .= '; domain=' . $this->_cookieDomain;
		}

		/*
		 * Tag on the HttpOnly modifier.  IE 6.0 SP1 will prevent any cookies
		 * with this in it from being visible to JavaScript, which mitigates
		 * XSS attacks.
		 */
		$cookie .= '; HttpOnly=1';

		/*
		 * Init may be called multiple times (from unit tests) but don't send the headers
		 * more than once.
		 */
		$phpVm = $gallery->getPhpVm();
		/*
		 * Use our PhpVm for testability here
		 */
		if (!$phpVm->headers_sent()) {
		    $phpVm->header($cookie);
		}
	    }
	}

	return GalleryStatus::success();
    }

    /**
     * Save any session changes to the store
     *
     * @return object GalleryStatus a status code
     */
    function save() {
	global $gallery;
	$platform = $gallery->getPlatform();
	$dieRoll = rand(1, 100);

	if ($this->_shouldSaveSession()) {
	    /* Only bother saving if we've been modified at all */
	    $serialized = serialize(array($this->_creationTime,
					  $this->_remoteIdentifier,
					  $this->_sessionData));
	    if ($serialized != $this->_loadedSessionData) {
		$sessionFile = $gallery->getConfig('data.gallery.sessions') . $this->_sessionId;
		$platform->atomicWrite($sessionFile, $serialized);
	    } else {
		/*
		 * 5% of the time touch the session file so that it doesn't get expired.
		 * We can't count on the atime being set, since you can disable that on
		 * some operating systems to get performance gains
		 */
		if ($dieRoll <= 5) {
		    $sessionFile = $gallery->getConfig('data.gallery.sessions') . $this->_sessionId;
		    $platform->touch($sessionFile);
		}
	    }

	    /* Perform garbage collection 1% of the time. */
	    if ($dieRoll == 1) {
		$ret = $this->_expireSessions();
		if ($ret->isError()) {
		    return $ret->wrap(__FILE__, __LINE__);
		}
	    }
	}

	return GalleryStatus::success();
    }

    /**
     * Clean and reinitialize a session
     *
     * @return object GalleryStatus a status code
     */
    function reset() {
	global $gallery;
	$platform = $gallery->getPlatform();
	$sessionFile = $gallery->getConfig('data.gallery.sessions') . $this->_sessionId;
	if ($platform->file_exists($sessionFile)) {
	    $platform->unlink($sessionFile);
	}

	/*
	 * Unset the cookie and any request variables so that
	 * we'll regenerate a new id in init()
	 */
	unset($_REQUEST[SESSION_ID_PARAMETER]);
	unset($_COOKIE[SESSION_ID_PARAMETER]);

	/* Reset 'cached' variables */
	$this->_cookieDomain = null;

	$ret = $this->init();
	if ($ret->isError()) {
	    return $ret->wrap(__FILE__, __LINE__);
	}

	return GalleryStatus::success();
    }

    /**
     * Regenerate the session ID to prevent a session fixation attack
     * by a hostile website
     *
     * @return object GalleryStatus a status code
     */
    function regenerate() {
	/* Store the current session data */
	$localSessionData = $this->_sessionData;
	$localLoadedSessionData = $this->_loadedSessionData;

	/* Reset the session data to create a new session id */
	$this->reset();

	/* Restore the stored session data */
	$this->_sessionData = $localSessionData;
	$this->_loadedSessionData = $localLoadedSessionData;

	/* Replace old session id with new one in any return or navigation urls */
	$key = GalleryUtilities::prefixFormVariable($this->getKey()) . '=';
	$match = '/' . $key . '[a-fA-F0-9]+/';
	$replace = $key . $this->getId();

	foreach (array('return', 'formUrl') as $key) {
	    if (GalleryUtilities::hasRequestVariable($key)) {
		GalleryUtilities::putRequestVariable($key,
		    preg_replace($match, $replace, GalleryUtilities::getRequestVariables($key)));
	    }
	}

	if ($this->exists('core.navigation')) {
	    $navigation = $this->get('core.navigation');
	    foreach (array_keys($navigation) as $navId) {
		if (isset($navigation[$navId]['data']['returnUrl'])) {
		    $navigation[$navId]['data']['returnUrl'] =
			preg_replace($match, $replace, $navigation[$navId]['data']['returnUrl']);
		}
	    }
	    $this->put('core.navigation', $navigation);
	}

	return GalleryStatus::success();
    }

    /**
     * Get rid of any sessions that have not been accessed within our
     * inactivity timeout or have exceeded the max lifetime.
     *
     * @return object GalleryStatus a status code.
     * @access private
     */
    function _expireSessions() {
	global $gallery;

	list ($ret, $sessionInactivityTimeout) =
	    GalleryCoreApi::getPluginParameter('module', 'core', 'session.inactivityTimeout');
	if ($ret->isError()) {
	    return $ret->wrap(__FILE__, __LINE__);
	}

	list ($ret, $lifetime) =
	    GalleryCoreApi::getPluginParameter('module', 'core', 'session.lifetime');
	if ($ret->isError()) {
	    return $ret->wrap(__FILE__, __LINE__);
	}

	$inactiveCutoff = time() - $sessionInactivityTimeout;
	$lifetimeCutoff = time() - $lifetime;

	$platform = $gallery->getPlatform();
	$sessionsDir = $gallery->getConfig('data.gallery.sessions');
	$dir = $platform->opendir($sessionsDir, 'r');
	if (!$dir) {
	    return GalleryStatus::error(ERROR_PLATFORM_FAILURE, __FILE__, __LINE__,
					"Can't access session dir");
	}

	while (($filename = $platform->readdir($dir)) !== false) {
	    if ($filename == '.' || $filename == '..') {
		continue;
	    }

	    $path = $sessionsDir . $filename;
	    $statData = $platform->stat($path);
	    if ($statData['mtime'] < $inactiveCutoff || $statData['ctime'] < $lifetimeCutoff) {
		$platform->unlink($path);
	    }
	}
	$platform->closedir($dir);

	return GalleryStatus::success();
    }

    /**
     * The session key parameter used in URLS and the cookie
     * @return array string
     */
    function getKey() {
	return SESSION_ID_PARAMETER;
    }

    /**
     * The session id
     * @return string an id (like "A124DFE7A90")
     */
    function getId() {
	return $this->_sessionId;
    }

    /*
     * Returns the cookie domain
     *
     * By default, don't set the cookie domain. Only set it, if G2 is configured to set it
     * (e.g. because it is a) embedded AND b) different subdomains are involved)
     *
     * @return string the cookie domain, or '' if no cookie domain should be set
     * @static
     */
    function getCookieDomain() {
	return $this->_cookieDomain;
    }

    /**
     * Get cookie path that will encompass G2 (and CMS app if embedded)
     *
     * The cookie path must be set because of short urls. The user agent would set the cookie path
     * too restrictive with short urls enabled.
     *
     * @return array (object GalleryStatus a status code, string path)
     */
    function getCookiePath() {
	return $this->_cookiePath;
    }

    /**
     * Is this transaction known to be using cookies?
     *
     * @return bool true if yes
     */
    function isUsingCookies() {
	return $this->_isUsingCookies;
    }

    /**
     * Get a value from the session data
     *
     * @param string the key
     * @return string the value or null if it doesn't exist
     */
    function &get($key) {
	if (isset($this->_sessionData[$key])) {
	    return $this->_sessionData[$key];
	}

	$null = null;
	return $null;
    }

    /**
     * Store a value in the session
     *
     * @param string the key
     * @param string the value
     */
    function put($key, $value) {
	$this->_sessionData[$key] = $value;
    }

    /**
     * Remove a value from the session
     *
     * @param string the key
     */
    function remove($key) {
	unset($this->_sessionData[$key]);
    }

    /**
     * Check to see if a value exists in the session
     *
     * @param string the key
     */
    function exists($key) {
	return isset($this->_sessionData[$key]);
    }

    /**
     * Check whether the session is valid
     *
     * Valid := sessionId is non-empty and the session file exists and the
     * session has not expired, or if the session file does not exist
     *
     * @return array (object GalleryStatus a status code, boolean valid)
     * @access private
     */
    function _isSessionValid() {
	global $gallery;
	$platform = $gallery->getPlatform();

	if (!empty($this->_sessionId)) {
	    /* Check if the session has expired */
	    $sessionFile = $gallery->getConfig('data.gallery.sessions') . $this->_sessionId;
	    if ($platform->file_exists($sessionFile)) {
		list ($ret, $lifetime) =
		    GalleryCoreApi::getPluginParameter('module', 'core', 'session.lifetime');
		if ($ret->isError()) {
		    return array($ret->wrap(__FILE__, __LINE__), null);
		}
		list ($ret, $inactivityTimeout) =
		    GalleryCoreApi::getPluginParameter('module', 'core',
						       'session.inactivityTimeout');
		if ($ret->isError()) {
		    return array($ret->wrap(__FILE__, __LINE__), null);
		}
		$lifetimeCutoff = time() - $lifetime;
		$inactiveCutoff = time() - $inactivityTimeout;
		$statData = $platform->stat($sessionFile);
		if ($statData['mtime'] < $inactiveCutoff || $statData['ctime'] < $lifetimeCutoff) {
		    /* The session has timed out, remove it */
		    $platform->unlink($sessionFile);
		} else {
		    return array(GalleryStatus::success(), true);
		}
	    } else {
		return array(GalleryStatus::success(), true);
	    }
	}

	return array(GalleryStatus::success(), false);
    }

    /**
     * Load the session data
     *
     * @return object GalleryStatus a status code
     * @access private
     */
    function _loadSessionData() {
	global $gallery;

	$platform = $gallery->getPlatform();
	$sessionFile = $gallery->getConfig('data.gallery.sessions') . $this->_sessionId;

	$valid = false;
	if ($platform->file_exists($sessionFile) &&
		$serialized = $platform->file_get_contents($sessionFile)) {
	    $data = unserialize($serialized);
	    if (count($data) == 3) {
		list ($this->_creationTime, $this->_remoteIdentifier, $this->_sessionData) = $data;
		$this->_loadedSessionData = $serialized;
		$valid = true;
	    }
	}

	if (!$valid) {
	    /* No session file */
	    $this->_emptySessionData();
	}

	return GalleryStatus::success();
    }

    /**
     * Get rid of all session data
     *
     * @access private
     */
    function _emptySessionData() {
	$this->_sessionData = array();
	$this->_loadedSessionData = array();
	$this->_creationTime = time();
	$this->_remoteIdentifier = $this->_getRemoteIdentifier();
    }

    /**
     * Return a value that we can use to identify the client.  We can't tie it
     * to the IP address because that changes too frequently (dialup users,
     * users behind proxies) so we have to be creative.  Changing this algorithm
     * will cause all existing sessions to be discarded.
     *
     * @return array
     * @access private
     */
    function _getRemoteIdentifier() {
	$httpUserAgent = GalleryUtilities::getServerVar('HTTP_USER_AGENT');
	return array(GalleryUtilities::getRemoteHostAddress(),
		     isset($httpUserAgent) ? md5($httpUserAgent) : null);
    }

    /**
     * Compare two arrays and return a score consisting of 1 point for every
     * matching element in the arrays.
     * Example input:
     *   $a = array(0, 'x', 2);
     *   $b = array(0, 'y', 2);
     * Example output:
     *   2
     * (indexes 0 and 2 match.  index 1 does not)
     *
     * @return int a score
     */
    function _compareIdentifiers($a, $b) {
	$score = 0;
	if (is_array($a) && is_array($b)) {
	    for ($i = 0; $i < sizeof($a); $i++) {
		if (sizeof($b) > $i) {
		    if ($a[$i] == $b[$i]) {
			$score++;
		    }
		}
	    }
	}
	return $score;
    }

    /**
     * Store a status message
     *
     * @param array status data
     * @return string the status id
     */
    function putStatus($statusData) {
	$tod = gettimeofday();
	/*
	 * Prefix the status id with a character so that it doesn't wind up being
	 * entirely numeric because PHP will renumber numeric keys in associative
	 * arrays when you run it through functions like array_splice()
	 */
	$statusId = 'x' . substr(md5($tod['usec'] + rand(1, 1000)), 0, 8);

	$status =& $this->get('core.status');
	if (!isset($status)) {
	    $status = array();
	}

	$status[$statusId] = $statusData;

	/* Prune extra status messages */
	$maxStatusMessages = 5;
	if (sizeof($status) > $maxStatusMessages) {
	    $status = array_splice($status, -$maxStatusMessages);
	}
	$this->put('core.status', $status);

	return $statusId;
    }

    /**
     * Get a status message
     *
     * @param string the status id
     * @return array the status message
     */
    function getStatus($statusId, $remove=true) {
	$status = $this->get('core.status');
	$statusData = null;
	if (isset($status) && isset($status[$statusId])) {
	    $statusData = $status[$statusId];
	    if ($remove) {
		unset($status[$statusId]);
		$this->put('core.status', $status);
	    }
	}

	return $statusData;
    }

    /**
     * Return the session id
     *
     * @return string the session id
     */
    function getSessionId() {
	return $this->_sessionId;
    }

    /**
     * Start new navigation
     *
     * @param array data for this new navigation:
     *              array(  'returnName' => ...
     *                      'returnUrl' => ...
     *                    [ 'returnNavId' => ... ]
     *                   )
     * @return string the navigation id
     */
    function addToNavigation($navigationData) {
	$tod = gettimeofday();
	$navId = 'x' . substr(md5($tod['usec'] + rand(1, 1000)), 0, 8);

	$navigation =& $this->get('core.navigation');
	if (!isset($navigation)) {
	    $navigation = array();
	}
	$navigation[$navId] = array();
	$navigation[$navId]['data'] = $navigationData;
	$navigation[$navId]['nextIds'] = array();

	/* Tell our predecessor that he's got a new successor */
	if (isset($navigationData['returnNavId'])) {
	    $returnNavId = $navigationData['returnNavId'];
	    $navigation[$returnNavId]['nextIds'][$navId] = true;
	}

	/* Prune oldest navigation branches */
	$maxNavBranches = 10;
	if (sizeof($navigation) > $maxNavBranches) {
	    $navigation = array_splice($navigation, -$maxNavBranches);
	}

	$this->put('core.navigation', $navigation);

	return $navId;
    }

    /**
     * Get data for a specific navigation id
     *
     * @param string the navigation id
     * @return array the navigation data
     */
    function getNavigation($navId) {
	$navigation = $this->get('core.navigation');
	$navigationData = array();
	if (isset($navigation[$navId]['data'])) {
	    $navigationData[] = $navigation[$navId]['data'];
	    /* Add data from our predecessors, if available */
	    while (isset($navigation[$navId]['data']['returnNavId'])
			&& isset($navigation[$navigation[$navId]['data']['returnNavId']]['data'])) {
		$navId = $navigation[$navId]['data']['returnNavId'];
		$navigationData[] = $navigation[$navId]['data'];
	    }
	}

	return $navigationData;
    }

    /**
     * Jump back from one navigation point to one of its predecessors
     *
     * @param string the source navigation id
     * @param string the destination navigation id. If empty, go back to root.
     */
    function jumpNavigation($fromNavId, $destNavId = '') {
	global $gallery;
	$gallery->debug("navigation: Jumping back from $fromNavId to $destNavId");

	$navigation = $this->get('core.navigation');
	$currentId = $fromNavId;
	/* Iterate back to root, deleting everything, until we reach
	 * destNavId or an navId that has other successors
	 */
	while (true) {
	    $gallery->debug("navigation: deleting $currentId");
	    $returnNavId = null;
	    if (isset($navigation[$currentId]['data']['returnNavId'])) {
		$returnNavId = $navigation[$currentId]['data']['returnNavId'];
	    }
	    unset($navigation[$currentId]);
	    if ($returnNavId == null) {
		break;
	    }
	    unset($navigation[$returnNavId]['nextIds'][$currentId]);
	    if (count($navigation[$returnNavId]['nextIds']) > 0) {
		break;
	    }
	    if ($returnNavId == $destNavId) {
		break;
	    }
	    $currentId = $returnNavId;
	}
	$this->put('core.navigation', $navigation);
    }

    /**
     * Check if we should create and save a session for this request
     *
     * Don't save session in core.DownloadItem, migrate.Redirect, ... requests
     * Reason: In these requests we don't need to save the session or create a new one because
     *         a) the session is not modified (DownloadItem, CSS)
     *         b) we return an image / css and not a HTML page (DownloadItem, CSS)
     *         c) there will be either a DownloadItem / ShowItem request anyway (migrate.Redirect)
     *         d) in migrate.Redirect requests, the cookie path we would set would most certainly
     *            be wrong, because the internal mod_rewrite redirect doesn't change all PHP SERVER
     *            variables
     *
     *         And if we stored the session, it would result in *a lot* unneeded sessions,
     *         e.g. for migrate redirects or hotlinked images.
     *
     * @return boolean true if the session should be saved, else false
     * @access private
     */
    function _shouldSaveSession() {
	/* Default to true */
	$ret = true;

	list ($view, $controller) = GalleryUtilities::getRequestVariables('view', 'controller');

	/* Check if the current request's view or controller is in the exlude list */
	if (!empty($view) && in_array($view, array('core.DownloadItem',
						   'core:DownloadItem', 'imageframe.CSS'))) {
	    /* view is in the exclude list */
	    $ret = false;
	} elseif (!empty($controller) && in_array($controller, array('migrate.Redirect',
								     'migrate:Redirect'))) {
	    /* controller is in the exclude list */
	    $ret = false;
	}

	return $ret;
    }
}

?>
