Sindbad~EG File Manager

Current Path : /var/www/web3/modules/core/classes/
Upload File :
Current File : /var/www/web3/modules/core/classes/GallerySession.class

<?php
/*
 * Gallery - a web based photo album viewer and editor
 * Copyright (C) 2000-2007 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.
 */
/**
 * Container for session related data.
 * @package GalleryCore
 * @subpackage Classes
 * @author Bharat Mediratta <bharat@menalto.com>
 * @version $Revision: 15641 $
 */

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

/**
 * Define a temporary session id for new guest user sessions.  If the guest needs a session, all
 * HTML already generated will be scanned to replace this temporary id with the correct id.
 */
define('SESSION_TEMP_ID', 'TMP_SESSION_ID_DI_NOISSES_PMT');

/**
 * Container for session related data.
 */
class GallerySession {

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

    /**
     * The time this session was last modified.
     * @var int
     * @access private
     */
    var $_modificationTime;

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

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

    /**
     * The id of the session's user.
     * @var int
     * @access private
     */
    var $_userId;

    /**
     * The serialized session data as loaded from database.
     * @var string
     * @access private
     */
    var $_loadedSessionData;

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

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

    /**
     * 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
     * @access private
     */
    var $_remoteIdentifier;

    /**
     * Whether the session has been saved in the persistent store during the current request
     * handling.  Used to determine whether we need to "touch" the session to prevent it from
     * expiring in case the session data hasn't changed anyway.
     * @var boolean
     * @access private
     */
    var $_isSessionSaved;

    /**
     * Whether this is a session for a search engine.
     * @var boolean
     * @access private
     */
    var $_isSearchEngineSession;

    /**
     * Whether a persistent session has been created (not updated) in this request.
     * @var boolean
     * @access private
     */
    var $_isPersistentSessionNew;

    /**
     * Whether no pseudo/temporary session id should be returned on getId() if there is no real
     * session id yet.
     * @var boolean
     * @access private
     */
    var $_doNotUseTempId;

    /**
     * Whether this is a persistent session or just a session for this single request.
     * @var boolean
     * @access private
     */
    var $_isPersistent;

    /**
     * Whether a persistent session is allowed to be created in this request.
     * @var boolean
     * @access private
     */
    var $_isPersistentSessionAllowedForRequest;

    /**
     * Whether a persistent session should be forced to be created.
     * @var boolean
     * @access private
     */
    var $_forceSaveSession;

    /**
     * How many sessions to delete per expireSessions() call.
     * @var int
     * @access private
     */
    var $_expirationLimit = 500;

    /**
     * Authentication token to verify genuine requests.
     * @var string
     * @access private
     */
    var $_authToken = '';

    /**
     * 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 = '';
	$this->_isSearchEngineSession = $this->_isSessionSaved = $this->_isPersistent = false;
	$this->_isPersistentSessionNew = false;

	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 id parameter as a cookie, then it also means 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 ($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, don't create a session under all circumstances and
	     * don't send cookies / don't append the sessionId to URLs.
	     */
	    $searchEngineId = GalleryUtilities::identifySearchEngine();
	    if (isset($searchEngineId)) {
		$this->_isUsingCookies = true;
		$this->doNotUseTempId();
		$this->_isSearchEngineSession = true;
	    } 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);
	    }
	}

	/* Sanitize the session id */
	$this->_sessionId = is_string($this->_sessionId) ? $this->_sessionId : '';
	$this->_sessionId = preg_replace('/[^a-fA-F0-9]/', '', $this->_sessionId);
	/* Prevent from querying the DB for sessionIds that are incorrect anyway */
	if (strlen($this->_sessionId) != 32) {
	    $this->_sessionId = '';
	}
	$this->_sessionId = GalleryUtilities::strToLower($this->_sessionId);

	/* Load session data if a session with that id exists and expire the session if necessary */
	$ret = $this->_loadSessionData();
	if ($ret) {
	    return $ret;
	}

	$this->_forceSaveSession = false;

	/* Only need to check for session hijacking if the session is not new  */
	if ($this->_isPersistent) {

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

	    if (!isset($this->_remoteIdentifier)) {
		/*
		 * Initialize remoteIdentifier if not yet set (via initEmpty(true) from a previous
		 * request when creating a session for a 3rd party)
		 */
		$this->_remoteIdentifier = $currentRemoteIdentifier;
		$this->_forceSaveSession = true;
	    } else if ($this->compareIdentifiers($this->_remoteIdentifier,
						 $currentRemoteIdentifier) == 0) {
		/* If we upgrade, allowSessionAccess could 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->_sessionId = '';
		    $ret = $this->_emptySessionData();
		    if ($ret) {
			return $ret;
		    }
		}
	    }
	} /* End for existing persistent sessions */

	return null;
    }

    /**
     * Start session by ensuring we've got a valid, unique sessionId and send cookie if necessary.
     * @return object GalleryStatus a status code
     */
    function start() {
	if (!$this->_isPersistentSessionAllowedForRequest() && !$this->_forceSaveSession) {
	    /* No need to send a cookie or to get a new sessionId */
	    return null;
	}

	/* If session hasn't any important data/attributes, we don't need a persistent session */
	list ($ret, $isRequired) = $this->_isPersistentSessionRequired();
	if ($ret) {
	    return $ret;
	}
	if (!$isRequired) {
	    return null;
	}

	/* For new sessions (no sessionId in DB yet), first get a new, collision-free sessionId */
	if (!$this->_isPersistent) {
	    /*
	     * Since getting a new collision-free sessionId requires a DB query anyway, we save the
	     * whole session in this case.  In all typical cases (main.php), there won't be any
	     * session changes after start() was called, thus we actually save the session only once
	     * per request.
	     */
	    $ret = $this->_acquireNewPersistentSession();
	    if ($ret) {
		return $ret;
	    }
	} /* Else: not a new session  */

	/*
	 * Send a cookie to the browser, if necessry ( this must be done before we start outputting
	 * HTML because the DownloadItem requests might come in before we reach $session->save() )
	 */

	/* Don't save session / send cookie for DownloadItem, CSS, migrate.Redirect requests */
	if (!isset($_COOKIE[SESSION_ID_PARAMETER])
		|| $this->_forceSaveSession
		|| $_COOKIE[SESSION_ID_PARAMETER] != $this->_sessionId) {
	    $ret = $this->_setCookie();
	    if ($ret) {
		return $ret;
	    }
	}

	return null;
    }

    /**
     * Save any session changes to the store.  Does not save sessions that don't have a sessionId
     * yet.  Triggers the expiration of existing persistent sessions in 2% of all calls.
     * @return object GalleryStatus a status code
     */
    function save() {
	global $gallery;
	$phpVm = $gallery->getPhpVm();
	$dieRoll = $phpVm->rand(1, 100);

	if (!empty($this->_sessionId)
		&& ($this->_isPersistentSessionAllowedForRequest() || $this->_forceSaveSession)) {
	    $this->_sessionId = GalleryUtilities::strtolower($this->_sessionId);
	    if (empty($this->_userId)) {
		return GalleryCoreApi::error(ERROR_MISSING_VALUE);
	    }

	    /* Only bother saving if we've been modified at all */
	    $serialized = $this->_getSerializedSession();
	    if ($serialized != $this->_loadedSessionData) {
		if (!$this->_isPersistent) {
		    $ret = $this->_acquireNewPersistentSession();
		} else {
		    $this->_modificationTime = $phpVm->time();
		    $ret = GalleryCoreApi::updateMapEntry('GallerySessionMap',
			array('id' => $this->_sessionId), array('userId' => $this->_userId,
			'remoteIdentifier' => serialize($this->_remoteIdentifier),
			'creationTimestamp' => $this->_creationTime,
			'modificationTimestamp' => $this->_modificationTime,
			'data' => serialize($this->_sessionData)));
		}
		if ($ret) {
		    return $ret;
		}
		$this->_isSessionSaved = true;
	    } else if (!$this->_isSessionSaved) {
		/*
		 * 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) {
		    $this->_modificationTime = $phpVm->time();
		    $ret = GalleryCoreApi::updateMapEntry('GallerySessionMap',
			array('id' => $this->_sessionId),
			array('modificationTimestamp' => $this->_modificationTime));
		    if ($ret) {
			return $ret;
		    }
		    $this->_isSessionSaved = true;
		}
	    }

	    $this->_loadedSessionData = $this->_getSerializedSession();
	    $this->_isPersistent = true;
	}

	/* Perform garbage collection 2% of the time when a new session was created  */
	if ($this->_isPersistentSessionNew && $dieRoll <= 2 ) {
	    $ret = $this->_expireSessions();
	    if ($ret) {
		return $ret;
	    }
	}

	return null;
    }

    /**
     * Set a new/unused sessionid.
     * @param boolean $emptyRemoteId (optional) if true don't initialize remoteIdentifier
     * @param int $userId (optional) user for session, defaults to anonymous
     * @return object GalleryStatus a status code
     */
    function initEmpty($emptyRemoteId=false, $userId=null) {
	$this->_emptySessionData();
	$this->_sessionId = '';

	if ($emptyRemoteId) {
	    $this->_remoteIdentifier = null;
	}

	if (empty($userId)) {
	    list ($ret, $userId) = GalleryCoreApi::getAnonymousUserId();
	    if ($ret) {
		return $ret;
	    }
	}
	$this->_userId = (int)$userId;

	/* Get a sessionId, don't send cookies */
	$ret = $this->_acquireNewPersistentSession();
	if ($ret) {
	    return $ret;
	}

	return null;
    }

    /**
     * Clean/remove and reinitialize a session.
     * @return object GalleryStatus a status code
     */
    function reset() {
	global $gallery;
	if (!empty($this->_sessionId)) {
	    $this->_sessionId = GalleryUtilities::strToLower($this->_sessionId);
	    $ret = GalleryCoreApi::removeMapEntry('GallerySessionMap',
						  array('id' => $this->_sessionId));
	    if ($ret) {
		return $ret;
	    }
	}
	$this->_sessionId = '';
	$this->_userId = null;

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

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

	/* Delete the cookie on the browser */
	$ret = $this->_setCookie(true);
	if ($ret) {
	    return $ret;
	}

	$ret = $this->init();
	if ($ret) {
	    return $ret;
	}

	return null;
    }

    /**
     * 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;
	$userId = $this->getUserId();

	/* Reset the session data to create a new session id */
	$ret = $this->reset();
	if ($ret) {
	    return $ret;
	}

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

	/* Start the session again (create a session in the DB, ...) */
	$ret = $this->start();
	if ($ret) {
	    return $ret;
	}

	/* 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 null;
    }

    /**
     * Send back a cookie to the browser.
     * @param boolean $delete (optional) whether to delete the cookie
     * @return object GalleryStatus a status code
     * @access private
     */
    function _setCookie($delete=false) {
	global $gallery;
	$phpVm = $gallery->getPhpVm();

	/*
	 * 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.
	 */

	if (!$delete) {
	    $cookie = 'Set-Cookie: ' . SESSION_ID_PARAMETER . '=' . $this->_sessionId;
	} else {
	    $cookie = 'Set-Cookie: ' . SESSION_ID_PARAMETER . '=';
	}

	/*
	 * As part of the session/cookie management, we are forced to append the SID to all
	 * DownloadItem URLs in embedded Gallery if cookie path/domain are not configured
	 */
	list ($ret, $this->_cookieDomain) = $this->getCookieDomain();
	if ($ret) {
	    return $ret;
	}
	$urlGenerator =& $gallery->getUrlGenerator();
	list ($ret, $cookiePath) = $urlGenerator->getCookiePath();
	if ($ret) {
	    return $ret;
	}

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

	if ($delete) {
	    /* Expires in the past instructs the browser to delete the cookie */
	    $expirationDate = GalleryUtilities::getHttpDate($phpVm->time() - (365 * 24 * 3600));
	    $cookie .= '; expires=' . $expirationDate;
	} else if ($sessionLifetime > 0) {
	    $expirationDate = GalleryUtilities::getHttpDate($phpVm->time() + $sessionLifetime);
	    $cookie .= '; expires=' . $expirationDate;
	}

	/* Because of short URLs, the cookie path must always be set explicitly */
	$cookie .= '; path=' . $cookiePath;

	/*
	 * Set the cookie domain only if needed, ie. embedded multi-subdomain installs that is when
	 * Gallery is installed on a different subdomain than the embedding application.
	 *
	 * Q: Why not set the cookie domain to .example.com (omitting the subdomains) 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, eg.  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, ie. each
	 * Gallery install had its own unique cookie name.  But when a user accessed multiple
	 * accounts on this shared domain, the Gallery 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 (ie. .www.example.com when
	 * Gallery 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/Gallery 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 Gallery to all DownloadItem URLs?
	 * A: In embedded Gallery, all DownloadItem requests still go directly to Gallery and not
	 * through the emApp for performance reasons.  If we set the cookie path in embedded Gallery
	 * to a path that matches embedded and standalone Gallery, then the standalone Gallery
	 * cookies always have precendence over the cookies from embedded Gallery. 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 Gallery.  DownloadItem is the only request that falls into this
	 * category.
	 *
	 * Q: Why force the Gallery base (standalone) path for Java applet cookies?
	 * A: Because the applets talk to Gallery directly.  If the cookie path was set to the
	 * embedded Gallery path, then it would not be selected for the HTTP requests of the applet
	 * to Gallery, 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 Gallery,
	 * we have to append the session id to all DownloadItem unless the cookie path is configured
	 * such that standalone and embedded Gallery 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 headers more than
	 * once.  Use our PhpVm for testability.
	 */
	$phpVm = $gallery->getPhpVm();
	if (!$phpVm->headers_sent()) {
	    GalleryUtilities::setResponseHeader($cookie);
	}

	return null;
    }

    /**
     * Acquire a new persistent session and guarantee we've got a valid, unqiue sessionId.
     * @return object GalleryStatus a status code
     */
    function _acquireNewPersistentSession() {
	global $gallery;
	$phpVm = $gallery->getPhpVm();
	$storage =& $gallery->getStorage();

	/* Assemble the data */
	$this->_modificationTime = $phpVm->time();
	$data = array('userId' => $this->_userId,
		      'remoteIdentifier' => serialize($this->_remoteIdentifier),
		      'creationTimestamp' => $this->_creationTime,
		      'modificationTimestamp' => $this->_modificationTime,
		      'data' => serialize($this->_sessionData));

	/* Get new sessionId, there's a 1:2^128 probability of collision (md5), try it 5 times */
	$remoteHost = $this->_remoteIdentifier[0];
	$attempt = 0;
	$success = false;
	do {
	    /* If there's sessionId given, first try it with this, else generate a new one */
	    if ($attempt != 0 || empty($this->_sessionId)) {
		$this->_sessionId =
		    $phpVm->md5(uniqid(substr($remoteHost . microtime() . rand(1, 32767), 0, 114)));
	    }
	    $this->_sessionId = $data['id'] = GalleryUtilities::strToLower($this->_sessionId);

	    $ret = @GalleryCoreApi::addMapEntry('GallerySessionMap', $data);
	    if ($ret) {
		if (!($ret->getErrorCode() & ERROR_STORAGE_FAILURE)) {
		    /* No luck after x attempts, give up, stop hitting the server with DB queries */
		    return $ret;
		}
	    } else {
		$success = true;
	    }
	    /*
	     * Make sure the session exists before other requests (DownloadItem, printing shops,
	     * ...) arrive that rely on it
	     */
	    $ret = $storage->checkPoint();
	    if ($ret) {
		return $ret;
	    }
	} while (!$success && $attempt++ < 4);

	if (!$success) {
	    return GalleryCoreApi::error(ERROR_COLLISION);
	}

	$this->_isPersistent  = true;
	/* Make sure we don't save the session a 2nd time for vain */
	$this->_loadedSessionData = $this->_getSerializedSession();
	/* Also prevent from doing a "touch" */
	$this->_isSessionSaved = true;
	/* To remember to replace SESSION_TEMP_ID with the real id in the generated HTML */
	$this->_isPersistentSessionNew = true;

	return null;
    }

    /**
     * Check whether this session should be persistent or not.
     *
     * For guest users, we don't create sessions, unless their session has non-default data.  Also,
     * the session based permission cache and the navigation isn't regarded important enough to
     * create a session.
     *
     * @return array object GalleryStatus a status code, boolean session is necessary
     * @access private
     */
    function _isPersistentSessionRequired() {
	/* For existing sessions, the session is necessary */
	if ($this->_isPersistent || $this->_forceSaveSession) {
	    return array(null, true);
	}

	if (!empty($this->_isSearchEngineSession) || empty($this->_userId)) {
	    return array(null, false);
	}

	list ($ret, $isAnonymous) = GalleryCoreApi::isAnonymousUser($this->_userId);
	if ($ret) {
	    return array($ret, null);
	}

	if ($isAnonymous) {
	    $sessionDataCopy = $this->_sessionData;
	    /*
	     * - lastViewed: We don't care about viewed count, we can check that less strict with
	     *                HTTP modified since headers
	     * - permissionCache: we don't care about the permission cache (session based
	     *   permissions are stored as ACLs in another session data entry)
	     * - navigation: no, we don't need navigation
	     * - language: only useful if it's different from what we'd have set anyway
	     * - embed.id.externalUser: not important if mapped to the anonymousUser, else the
	     *                          userId is not == anonymousUserId
	     * - authToken: we only check the authToken for persistent sessions
	     */
	    unset($sessionDataCopy['core.lastViewed']);
	    unset($sessionDataCopy['permissionCache']);
	    unset($sessionDataCopy['core.navigation']);
	    unset($sessionDataCopy['embed.id.externalUser']);
	    unset($sessionDataCopy['core.authToken']);
	    list ($ret, $detectedLanguageCode) = GalleryTranslator::getDefaultLanguageCode();
	    if ($ret) {
		return array($ret, null);
	    }

	    if (isset($sessionDataCopy['core.language']) &&
		    $sessionDataCopy['core.language'] == $detectedLanguageCode) {
		unset($sessionDataCopy['core.language']);
	    }

	    /* If there's anything left in the session data, we should probably create a session */
	    return array(null, !empty($sessionDataCopy));
	} else {
	    return array(null, true);
	}
    }

    /**
     * Whether this controller/view request generally allows creating a session.
     *
     * 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,
     *         eg. for migrate redirects or hotlinked images.
     *
     * @return boolean true if a persistent session can be created in this request
     * @access private
     */
    function _isPersistentSessionAllowedForRequest() {
	if (!isset($this->_isPersistentSessionAllowedForRequest)) {
	    $flag = true; /* Default to true */
	    list ($view, $controller) = GalleryUtilities::getRequestVariables('view', 'controller');

	    if (!empty($controller)) {
		GalleryCoreApi::requireOnce('modules/core/classes/GalleryController.class');
		list ($ret, $controller) = GalleryController::loadController($controller);
		if (!$ret && !$controller->shouldSaveSession()) {
		    $flag = false;
		}
	    } else if (!empty($view)) {
		GalleryCoreApi::requireOnce('modules/core/classes/GalleryView.class');
		list ($ret, $view) = GalleryView::loadView($view);
		if (!$ret && !$view->shouldSaveSession()) {
		    $flag = false;
		}
	    }

	    $this->_isPersistentSessionAllowedForRequest = $flag;
	}

	return $this->_isPersistentSessionAllowedForRequest;
    }

    /**
     * Load the session data or generate a new session with new data.  Also sets
     * $this->_isPersistent to true if loaded from persistent store.
     * @return object GalleryStatus a status code
     * @access private
     */
    function _loadSessionData() {
	global $gallery;

	if (!empty($this->_sessionId)) {
	    $this->_sessionId = GalleryUtilities::strToLower($this->_sessionId);

	    /* Check if the session has expired */
	    list ($ret, $lifetime) =
		GalleryCoreApi::getPluginParameter('module', 'core', 'session.lifetime');
	    if ($ret) {
		return $ret;
	    }
	    list ($ret, $inactivityTimeout) =
		GalleryCoreApi::getPluginParameter('module', 'core',
						   'session.inactivityTimeout');
	    if ($ret) {
		return $ret;
	    }

	    $phpVm = $gallery->getPhpVm();
	    $lifetimeCutoff = $phpVm->time() - $lifetime;
	    $inactiveCutoff = $phpVm->time() - $inactivityTimeout;

	    list ($ret, $results) = GalleryCoreApi::getMapEntry('GallerySessionMap',
		array('userId', 'remoteIdentifier', 'creationTimestamp',
		      'modificationTimestamp', 'data'),
		array('id' => $this->_sessionId));
	    if ($ret) {
		/* When upgrading from versions before 1.0.22, there's no DB table yet */
		list ($ret2, $module) = GalleryCoreApi::loadPlugin('module', 'core');
		if ($ret2) {
		    return $ret;
		}
		$instVersions = $module->getInstalledVersions();
		if (!empty($instVersions['core']) &&
			version_compare($instVersions['core'], '1.0.22', '<')) {
		    $this->_emptySessionData();
		    return null;
		}
		return $ret;
	    }

	    if ($results->resultCount()) {
		$pSession = $results->nextResult();
		if ($pSession[3] > $inactiveCutoff && $pSession[2] > $lifetimeCutoff) {
		    /* A session exists and it's valid */
		    $this->_userId = (int)$pSession[0];
		    $this->_remoteIdentifier = unserialize($pSession[1]);
		    $this->_creationTime = (int)$pSession[2];
		    $this->_modificationTime = (int)$pSession[3];
		    $this->_sessionData = unserialize($pSession[4]);
		    $this->_loadedSessionData = $this->_getSerializedSession();

		    $this->_isPersistent = true;
		} else {
		    /* The session has timed out, remove it */
		    $ret = GalleryCoreApi::removeMapEntry('GallerySessionMap',
							  array('id' => $this->_sessionId));
		    if ($ret) {
			return $ret;
		    }

		    /* Get a new sessionId + session meta data (later) */
		    $this->_sessionId = '';
		}
	    } else { /*  There's no session with this sessionId in the database */
		$this->_sessionId = '';
	    }

	    if (!$this->_isPersistent) {
		/*
		 * The sessionId was invalid.  If we got the sessionId from the cookie, delete the
		 * cookie or we'll try to load the session on each request again.
		 */
		if (isset($_COOKIE[SESSION_ID_PARAMETER])) {
		    unset($_COOKIE[SESSION_ID_PARAMETER]);
		    $ret = $this->_setCookie(true);
		    if ($ret) {
			return $ret;
		    }
		}
	    }
	} /* Else: no sessionId specified, thus we've no session yet */

	if (!$this->_isPersistent) {
	    $this->_emptySessionData();
	}

	return null;
    }

    /**
     * Get rid of all session data.
     * @access private
     */
    function _emptySessionData() {
	/* Don't (re-)set sessionId since we can't ensure a collision-free id without DB queries */
	global $gallery;
	$phpVm = $gallery->getPhpVm();
	$this->_sessionData = array();
	$this->_loadedSessionData = '';
	$this->_creationTime = $phpVm->time();
	$this->_userId = null;
	$this->_modificationTime = $phpVm->time();
	$this->_remoteIdentifier = $this->getRemoteIdentifier();
	/* Don't change userId or isUsingCookies */

	$this->_isPersistentSessionNew = false;
	$this->_isSessionSaved = false;
    }

    /**
     * If we started this request without a sessionId, then we used SESSION_TEMP_ID in all generated
     * URLs etc as a placeholder.  If we still have no sessionId, remove
     * g2_GALLERYSID=SESSION_TEMP_SID from all generated URLs and remove SESSION_TEMP_ID from the
     * HTML.  If a session was created (saved in the persistent store) during the request, replace
     * the SESSION_TEMP_ID with the new/real session id.
     * @param string $html HTML
     * @return string same HTML with replaced or removed sessionId
     */
    function replaceTempSessionIdIfNecessary($html) {
	global $gallery;

	if ($this->_isPersistentSessionNew) {
	    /*
	     * Session was created during request, probably need to replace temporary session id
	     * with real session id
	     */

	    /* Replace temp session id with real/new one */
	    $html = str_replace(SESSION_TEMP_ID, $this->_sessionId, $html);
	} else if (empty($this->_sessionId)) {
	    /* Remove sessionId from URLs for guests that have no session (normal case) */
	    $sessionString =
		GalleryUtilities::prefixFormVariable($this->getKey()) . '=' . SESSION_TEMP_ID;
	    /*
	     * Handling only most cases here, still leaving &&, ?& but handling trailing ? and &
	     * Experimented a lot with str_replace and preg_replace. A perfect solution would be
	     * again 40% slower (e.g. 12 instead of 8.5ms on a slow box for a lot of data).
	     */
	    /* sessionString normal and URL encoded */
	    $regexp = GalleryUtilities::prefixFormVariable($this->getKey()) . '(?:=|%3D)' .
			SESSION_TEMP_ID;
	    /*
	     * Remove trailing & and ? from URLs, also handle JavaScript (no HTML entities) and
	     * URL encoded (return URL) versions of & and ?
	     * This preg_replace takes about the same time as the str_replace that follows
	     */
	    $html = preg_replace('/(?:\\?|%3F|&amp;|&|%26amp%3B|%26)' . $regexp . '(["\'])/S',
				 '\\1', $html);
	    /* Remove sessionStrings that are not at the end of URLs and sessionIds in the HTML */
	    $html = str_replace(array($sessionString,
				      urlencode($sessionString),
				      SESSION_TEMP_ID),
				'', $html);
	}

	return $html;
    }

    /**
     * Replaces the session id in all string members of an object or in all elements of an array.
     *
     * Applies replaceTempSessionIdIfNecessary to all strings if $search and $replace are omitted.
     * Else it applies str_replace($search, $replace, $subject) on all strings.
     *
     * Examples:
     *   $themeData = $session->replaceSessionIdInData($themeData, $sessionId, SESSION_TEMP_ID);
     *
     *   $themeData = $session->replaceSessionIdInData($themeData);
     *
     * @param mixed $subject array, object or string that should be modified
     * @param string $search (optional) string to be replaced
     * @param string $replace (optional) replacement string
     * @return mixed converted subject
     */
    function replaceSessionIdInData($subject, $search=null, $replace=null) {
	if (($isArray = is_array($subject)) || is_object($subject)) {
	    foreach ($subject as $key => $value) {
		$value = $this->replaceSessionIdInData($value, $search, $replace);
		if ($isArray) {
		    $subject[$key] = $value;
		} else {
		    $subject->$key = $value;
		}
	    }
	} else if (is_string($subject)) {
	    if ($search !== null) {
		return str_replace($search, $replace, $subject);
	    } else {
		return $this->replaceTempSessionIdIfNecessary($subject);
	    }
	} else {
	    return $subject;
	}

	return $subject;
    }

    /**
     * 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;
	$storage =& $gallery->getStorage();

	list ($ret, $sessionInactivityTimeout) =
	    GalleryCoreApi::getPluginParameter('module', 'core', 'session.inactivityTimeout');
	if ($ret) {
	    return $ret;
	}

	list ($ret, $lifetime) =
	    GalleryCoreApi::getPluginParameter('module', 'core', 'session.lifetime');
	if ($ret) {
	    return $ret;
	}

	$phpVm = $gallery->getPhpVm();
	$inactiveCutoff = $phpVm->time() - $sessionInactivityTimeout;
	$lifetimeCutoff = $phpVm->time() - $lifetime;
	$lastWeek = $phpVm->time() - 86400 * 7;

	/* Only delete in small chunks, else we may lock the whole Gallery for too long */
	$where = '
	WHERE [GallerySessionMap::creationTimestamp] < ?
	   OR [GallerySessionMap::modificationTimestamp] < ?';
	$data[] = (int)$lifetimeCutoff;
	$data[] = (int)$inactiveCutoff;
	if ($lastWeek > $lifetimeCutoff) {
	    /* Delete guest user sessions more aggressively than other sessions */
	    list ($ret, $anonymousUserId) = GalleryCoreApi::getAnonymousUserId();
	    if ($ret) {
		return $ret;
	    }
	    /* Delete all sessions of guest users that are older than a week*/
	    $where .= ' OR
		([GallerySessionMap::userId] = ? AND [GallerySessionMap::creationTimestamp] < ?)';
	    $data[] = (int)$anonymousUserId;
	    $data[] = (int)$lastWeek;
	}
	/* TODO: Make this more OO, eg. by adding function canLimitDelete() to GalleryStorage */
	if ($storage->getType() == 'mysql') {
	    /*
	     * MySQL supports the LIMIT clause in DELETE statements, other DBMS' don't Since SELECT
	     * 500 sessionIds + DELETE those sessionIds is more expensive we optimize for MySQL by
	     * using DELETE ... LIMIT 500
	     */
	    $query = '
	    DELETE FROM [GallerySessionMap]
	    ' . $where . '
	    LIMIT ' . (int)$this->_expirationLimit;
	    list ($ret, $results) = $storage->execute($query, $data);
	    if ($ret) {
		return $ret;
	    }
	} else {
	    /*
	     * For other DBMS first SELECT the sessionIds with a LIMIT clause then DELETE
	     * ADOdb can't implement the LIMIT clause in subqueries DB independently!
	     */
	    $query = '
	    SELECT [GallerySessionMap::id]
	    FROM [GallerySessionMap]' . $where;
	    $option['limit']['count'] = $this->_expirationLimit;
	    list ($ret, $results) = $gallery->search($query, $data, $option);
	    if ($ret) {
		return $ret;
	    }
	    if ($results->resultCount()) {
		$ids = array();
		while ($row = $results->nextResult()) {
		    $ids[] = $row[0];
		}
		/* Delete the selected sessions */
		$query = sprintf('
		DELETE FROM [GallerySessionMap]
		WHERE [GallerySessionMap::id] IN (%s)',
		    GalleryUtilities::makeMarkers(count($ids)));
		list ($ret, $results) = $storage->execute($query, $ids);
		if ($ret) {
		    return $ret;
		}
	    }
	}

	return null;
    }

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

    /**
     * The session id.
     * @return string an id (like "A124DFE7A90")
     */
    function getId() {
	if (empty($this->_sessionId) && empty($this->_doNotUseTempId)) {
	    return SESSION_TEMP_ID;
	} else {
	    return $this->_sessionId;
	}
    }

    /**
     * Instruct the session to not return a pseudo temporary session id on getId() calls Makes sure
     * that the URL generator and other componennts don't use a pseudo session id for guest users
     * without a real session.  Call this method before starting to output immediate views the
     * progress bar.
     */
    function doNotUseTempId() {
	$this->_doNotUseTempId = true;
    }

    /**
     * Return the user id of the active user of this sesison.
     * @return int the user id
     */
    function getUserId() {
	return $this->_userId;
    }

    /**
     * Set the active user id for this session.
     * @param int $userId
     */
    function setUserId($userId) {
	return $this->_userId = $userId;
    }

    /*
     * Returns the cookie domain.
     *
     * By default, don't set the cookie domain.  Only set it, if Gallery is configured to set it
     * (eg. because it is a) embedded AND b) different subdomains are involved)
     *
     * @return array (object GalleryStatus a status code,
     *                string the cookie domain, or '' if no cookie domain should be set)
     */
    function getCookieDomain() {
	if (!isset($this->_cookieDomain)) {
	    list ($ret, $this->_cookieDomain) = GalleryCoreApi::getPluginParameter('module', 'core',
										   'cookie.domain');
	    if ($ret) {
		return array($ret, null);
	    }
	    if (!isset($this->_cookieDomain)) {
		$this->_cookieDomain = '';
	    }
	}

	return array(null, $this->_cookieDomain);
    }

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

    /**
     * Get a value from the session data.
     * @param string $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 $key
     * @param string $value
     */
    function put($key, $value) {
	$this->_sessionData[$key] = $value;
    }

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

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

    /**
     * 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
     * @static
     */
    function getRemoteIdentifier() {
	$httpUserAgent = GalleryUtilities::getServerVar('HTTP_USER_AGENT');
	return array(GalleryUtilities::getRemoteHostAddress(),
		     isset($httpUserAgent) ? md5($httpUserAgent) : null);
    }

    /**
     * Get the serialized session for comparing purposes.
     * @return string serialized session
     * @access private
     */
    function _getSerializedSession() {
	return serialize(array($this->_sessionId, $this->_userId,
	    serialize($this->_remoteIdentifier), $this->_creationTime, $this->_modificationTime,
	    serialize($this->_sessionData)));
    }

    /**
     * Compare two arrays and return score consisting of 1 point for each matching element.
     * 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 && $a[$i] == $b[$i]) {
		    $score++;
		}
	    }
	}
	return $score;
    }

    /**
     * Store a status message.
     * @param array $statusData
     * @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 $statusId
     * @param boolean $remove (optional)
     * @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
     * @deprecated
     * @todo will be removed in the next API branch
     */
    function getSessionId() {
	return $this->getId();
    }

    /**
     * Start new navigation.
     * @param array $navigationData 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 $navId 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 $fromNavId the source navigation id
     * @param string $destNavId 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);
    }

    /**
     * Return the Unix timestamp from when this session was created.
     * @return int the creation time
     */
    function getCreationTime() {
	return $this->_creationTime;
    }

    /**
     * Return the Unix timestamp from when this session was last modified.
     * @return int the modification time
     */
    function getModificationTime() {
	return $this->_modificationTime;
    }

    /**
     * Whether this session is a persistent session (= stored on the server) or just a session for
     * this single request.  Note that a non-persistent session can become persistent at the end of
     * the request when we evaluate the conditions whether to create a persistent session or not.
     * @return boolean true if the session is persistent, else false
     */
    function isPersistent() {
	return $this->_isPersistent;
    }

    /**
     * Return true if this session is identified as one coming from a search engine.
     * @return bool true if this is a search engine session
     */
    function isSearchEngineSession() {
	return $this->_isSearchEngineSession;
    }

    /**
     * Returns the authentication token associated with this session.
     * @return string the authentication token
     */
    function getAuthToken() {
	$authToken = $this->get('core.authToken');
	if (empty($authToken)) {
	    global $gallery;
	    $phpVm = $gallery->getPhpVm();
	    $authToken = substr($phpVm->md5(uniqid(microtime() . mt_rand())), 0, 12);
	    $this->put('core.authToken', $authToken);
	}
	return $authToken;
    }

    /**
     * Checks the given authentication token and resets the internal token on failure.
     * @param string $authToken Authentication token to be verified
     * @return bool true if the given
     */
    function isCorrectAuthToken($authToken) {
	$internalAuthToken = $this->get('core.authToken');
	if (empty($authToken) || empty($internalAuthToken)
		|| strcmp($internalAuthToken, $authToken)) {
	    $this->put('core.authToken', null);
	    return false;
	} else {
	    return true;
	}
    }
}

/**
 * Get the active user from the session's user id.
 * @package GalleryCore
 * @subpackage Classes
 */
class SessionAuthPlugin /* extends GalleryAuthPlugin */ {

    /**
     * @see GalleryAuthPlugin::getUser
     */
    function getUser() {
	global $gallery;
	$session =& $gallery->getSession();

	$userId = $session->getUserId();
	if (!empty($userId)) {
	    list ($ret, $user) = GalleryCoreApi::loadEntitiesById($userId);

	    /* ERROR_MISSING_OBJECT check to suppress error if user id doesn't exist */
	    if ($ret && !($ret->getErrorCode() & ERROR_MISSING_OBJECT)) {
		return array($ret, null);
	    }

	    return array(null, $user);
	}
    }
}
?>

Sindbad File Manager Version 1.0, Coded By Sindbad EG ~ The Terrorists