<?php
/*
 * $RCSfile: GalleryUrlGenerator.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.
 */
/**
 * @version $Revision: 1.88 $ $Date: 2005/08/23 03:49:03 $
 * @package GalleryCore
 * @author Bharat Mediratta <bharat@menalto.com>
 * @author Alan Harder <alan.harder@sun.com>
 */

/* Don't use GalleryCoreApi::requireOnce here; index.php uses this file without GalleryCoreApi */
require_once(dirname(__FILE__) . '/GalleryUtilities.class');

if (!defined('GALLERY_MAIN_PHP')) {
    define('GALLERY_MAIN_PHP', 'main.php');
}

/**
 * Url Generator
 *
 * @package GalleryCore
 * @subpackage Classes
 */
class GalleryUrlGenerator {
    /*
     * ****************************************
     *                 Members
     * ****************************************
     */

    /**
     * The base filename for the application.
     *
     * @var string $_baseFile
     * @access private
     */
    var $_baseFile;

    /**
     * Url path to embedding application.
     * Only required for non-G2 requests (such as using imageblock on non-G2 pages in embed app).
     *
     * @var string $_embedPath
     * @access private
     */
    var $_embedPath;

    /**
     * The relative path between G2 request urls and the G2 site dir.
     * Should only be nonempty when G2 is embedded.
     *
     * @var string $_relativeG2Path
     * @access private
     */
    var $_relativeG2Path;

    /**
     * The session key=value for the CMS application in which G2 is embedded.
     * Should only be nonempty when cookieless browsing is supported by CMS.
     *
     * @var string $_embedSessionString
     * @access private
     */
    var $_embedSessionString;

    /**
     * The navigation id of the current URL.
     *
     * @var string $_navId
     * @access private
     */
    var $_navId = '';

    /*
     * ****************************************
     *              Static Methods
     * ****************************************
     */

    /**
     * Return the current server domain
     * @return string the host name
     * @static
     */
    function getCurrentDomain() {
	if ($httpXForwardedServer = GalleryUtilities::getServerVar('HTTP_X_FORWARDED_SERVER')) {
	    return $httpXForwardedServer;
	} else {
	    return GalleryUtilities::getServerVar('HTTP_HOST');
	}
    }

    /**
     * Add given path to current protocol/server/port to create full url
     * @param string the url path; leading slash will be added if missing
     * @return string the url
     * @static
     */
    function makeUrl($path) {
	$protocol = (GalleryUtilities::getServerVar('HTTPS') == 'on') ? 'https' : 'http';
	$domain = GalleryUrlGenerator::getCurrentDomain();
	if (empty($path)) {
	    $path = '/';
	} else if ($path{0} != '/') {
	    $path = '/' . $path;
	}

	return sprintf('%s://%s%s', $protocol, $domain, $path);
    }

    /**
     * Return the current request URI.
     * Example: for http://domain.com/gallery2/main.php?g2_view=core.ShowItem
     *          it returns /gallery2/main.php?g2_view=core.ShowItem
     *
     * @return string the current URL path component plus query parameters
     * @static
     */
    function getCurrentRequestUri() {
	if (!($path = GalleryUtilities::getServerVar('REQUEST_URI')) &&
		($path = GalleryUtilities::getServerVar('SCRIPT_NAME'))) {
	    if (($tmp = GalleryUtilities::getServerVar('PATH_INFO')) && $tmp != $path) {
		$path .= $tmp;
	    }
	    if ($tmp = GalleryUtilities::getServerVar('QUERY_STRING')) {
		$path .= '?' . $tmp;
	    }
	}

	return GalleryUtilities::htmlEntityDecode($path);
    }

    /**
     * Append parameters to a url using the G2 prefix and urlencoding the keys and values.
     *
     * @param string the original url
     * @param array key/value pairs to be appended; may contain nested arrays
     * @param boolean (optional) false to not add prefix on parameter names
     * @return string the new url (&amp; separates added params)
     * @static
     */
    function appendParamsToUrl($url, $params, $addPrefix=true) {
	if (empty($params)) {
	    return $url;
	}

	$args = array();
	foreach ($params as $key => $value) {
	    $key = $addPrefix ? GalleryUtilities::prefixFormVariable(urlencode($key))
			      : urlencode($key);
	    if (is_array($value)) {
		GalleryUrlGenerator::_appendNestedParams($args, $value, $key . '[');
	    } else {
		$args[] = $key . '=' . urlencode($value);
	    }
	}

	/* Prefix appended params with ? if the URL doesn't already have a query string */
	return $url . ((strpos($url, '?') === false) ? '?' : '&amp;') . implode('&amp;', $args);
    }

    /**
     * Append array of (possibly nested) query parameters to flat array of keys/values.
     *
     * @param array output array
     * @param array input key/value pairs; may contain nested arrays
     * @param string prefix; used in recursive calls
     * @static
     * @access private
     */
    function _appendNestedParams(&$args, $params, $prefix) {
	foreach ($params as $key => $value) {
	    if (is_array($value)) {
		GalleryUrlGenerator::_appendNestedParams($args, $value,
							 $prefix . urlencode($key) . '][');
	    } else {
		$args[] = $prefix . urlencode($key) . ']=' . urlencode($value);
	    }
	}
    }

    /**
     * Old multisite system used a galleryId based on the current url.
     * Return a value here so sites won't break before they can upgrade.
     * @static
     * @deprecated
     */
    function getGalleryId() {
	return '';
    }

    /*
     * ****************************************
     *              Object Methods
     * ****************************************
     */

    /**
     * Initializer.  Configure the url generator with all the data it needs.
     *
     * @param string the base URL (optional)
     * @param string (optional) when embedded, this is the path from document root to baseFile
     * @param string (optional) when embedded, relative path between G2 request urls and G2 basedir
     * @param string (optional) when embedded in CMS app that supports cookieless browsing,
     *                          key=value string for CMS session key and id
     */
    function init($baseFile=null, $embedPath=null, $relativeG2Path=null, $embedSessionString=null) {
	$this->_baseFile = empty($baseFile) ? GALLERY_MAIN_PHP : $baseFile;
	$this->_embedPath = $embedPath;
	$this->_relativeG2Path = $relativeG2Path;
	$this->_embedSessionString = $embedSessionString;

	$this->_isBaseFullUrl = preg_match('{^https?:}', $this->_baseFile);
	$this->_isBaseAbsolute = !empty($this->_baseFile) && $this->_baseFile{0} == '/';

	if (!empty($this->_embedPath)) {
	    if ($this->_embedPath{strlen($this->_embedPath) - 1} != '/') {
		$this->_embedPath .= '/';
	    }
	    if ($this->_embedPath{0} != '/') {
		$this->_embedPath = '/' . $this->_embedPath;
	    }
	}

	if (!empty($this->_relativeG2Path) &&
		$this->_relativeG2Path{strlen($this->_relativeG2Path) - 1} != '/') {
	    $this->_relativeG2Path .= '/';
	}
    }

    /**
     * Return the complete current URL
     *
     * @return string the current URL
     */
    function getCurrentUrl() {
	if (!isset($this->_currentUrl)) {
	    $this->_currentUrl =
		GalleryUrlGenerator::makeUrl(GalleryUrlGenerator::getCurrentRequestUri());
	}

	return $this->_currentUrl;
    }

    /**
     * Return the current URL's directory.  Eg, if the url is:
     *   http://example.com/gallery2/main.php
     * Then we return:
     *   http://example.com/gallery2/
     *
     * @param boolean (optional) if true, ensure G2 base url is returned (different when embedded)
     * @return string the current URL directory
     */
    function getCurrentUrlDir($forceG2Base=false) {
	if (isset($this->_currentUrlDir[$forceG2Base])) {
	    return $this->_currentUrlDir[$forceG2Base];
	}

	if (!empty($this->_embedPath)) {
	    /*
	     * Embed request for a non-G2 view sets embedPath for building urls,
	     * as we cannot construct new urls using the current url.
	     */
	    $url = GalleryUrlGenerator::makeUrl($this->_embedPath);
	} else {
	    $url = $this->getCurrentUrl();

	    /*
	     * Remove the base file and any query string or path info after it:
	     *    http://example.com/gallery2/main.php/core.ShowView/foo=bar/...
	     * Extract everything up to but not including main.php:
	     *    http://example.com/gallery2/
	     *
	     * TODO: now that short urls are gone, do we need to do anything
	     *       other than look at the path component of the url?
	     */
	    if (empty($this->_baseFile)) {
		$url = substr($url, 0, strrpos($url, '/') + 1);
	    } else if (($i = strpos($url, $this->_baseFile)) !== false) {
		$url = substr($url, 0, $i);
	    }
	}

	/*
	 * If requested, apply relativeG2Path setting to get G2 base url, eg:
	 *    http://example.com/cms/index.php?module=g2&g2_view=...
	 * to:
	 *    http://example.com/gallery2/
	 * where relativeG2Path is ../gallery2/
	 */
	if ($forceG2Base && !empty($this->_relativeG2Path)) {
	    $relativePath = $this->_relativeG2Path;
	    while (strncmp($relativePath, '../', 3) == 0) {
		$url = preg_replace('|/[^/]+/?$|', '/', $url);
		$relativePath = substr($relativePath, 3);
	    }
	    $url .= $relativePath;
	}

	$this->_currentUrlDir[$forceG2Base] = $url;
	return $url;
    }

    /**
     * Return a valid Gallery URL.
     *
     * @param array key/value pairs to be included in the URL
     *  special 'href' key specifies path to append to G2 base url instead of a query param
     *  special 'forceDirect' key specifies to generate from G2 site url even if embedded
     * @param boolean (optional) set to true/false to force the session id to be in/not in the url
     *  by default it is included when cookies are not in use (for href urls, default=not included)
     */
    function generateUrl($params=array(), $forceSessionId=null, $baseUrl=null) {
	global $gallery;
	$currentView = $gallery->getCurrentView();

	/*
	 * For non-absolute 'href' urls always use G2 base url
	 * (direct to G2 codebase location, even if multisite or embedded),
	 * For 'core.DownloadItem' urls or if 'forceDirect' param given, use G2 site url
	 * (direct to main.php in directory for active config.php, even if embedded)
	 * Otherwise use application url
	 * (embed url; same as G2 site url if not embedded)
	 */
	if (isset($baseUrl)) {
	    $url = $baseUrl;
	    $appSession = true;
	} else if (isset($params['href'])) {
	    $url = $gallery->getConfig('galleryBaseUrl');
	    if (empty($url)) {
		$url = $this->getCurrentUrlDir(true);
	    }

	    $href = $params['href'];
	    unset($params['href']);
	    if (!isset($forceSessionId)) {
		/* Default to not including session id in href urls */
		$forceSessionId = false;
	    }

	    if (preg_match('{^[a-z]+://}', $href)) {
		/* Absolute URL */
		$url = $href;
	    } elseif(strlen($href) && $href{0} == '/') {
		/* Absolute URL path */
		$url = GalleryUrlGenerator::makeUrl($href);
	    } else {
		/* Check for local override */
		$override = sprintf('%s/local/%s', dirname($href), basename($href));
		$platform = $gallery->getPlatform();
		if ($platform->file_exists(dirname(__FILE__) . '/../../../' . $override)) {
		    $href = $override;
		}
		$url .= $href;
	    }
	} else if ((isset($params['view']) && $params['view'] == 'core.DownloadItem') ||
		   isset($params['forceDirect'])) {
	    $url = $this->getCurrentUrlDir(true) . GALLERY_MAIN_PHP;
	    /* Check if we are forced to append the session id in embedded G2 */
	    if ($this->embedForceSessionId($params)) {
		$forceSessionId = true;
	    }
	} else {
	    $url = $this->_isBaseFullUrl ? $this->_baseFile
		 : ($this->_isBaseAbsolute ? GalleryUrlGenerator::makeUrl($this->_baseFile)
		 : $this->getCurrentUrlDir() . $this->_baseFile);
	    $appSession = true;
	}

	/* Decide whether to include session id in the url */
	if ($session =& $gallery->getSession() &&
		( (!isset($forceSessionId) && !$session->isUsingCookies()) ||
		 (isset($forceSessionId) && $forceSessionId === true))) {
	    if (!empty($this->_embedSessionString) && isset($appSession)) {
		$embedSessionString = $this->_embedSessionString;
	    } else {
		$params[$session->getKey()] = $session->getId();
	    }
	}

	/* Swap in the actual url for the 'return' placeholder */
	if (isset($params['return'])) {
	    list ($ret, $view) = GalleryView::loadView($currentView);
	    if ($ret->isSuccess()) {
		list ($ret, $viewDescription) = $view->getViewDescription();
		if ($ret->isSuccess()) {
		    $params['returnName'] = $viewDescription;
		}
	    }
	    $params['return'] = str_replace('&amp;', '&', $this->getNavigationReturnUrl());
	    if (!empty($this->_navId)) {
		$params['navId'] = $this->_navId;
	    }
	}

	/* Navigation */
	$targetView = isset($params['view']) ? $params['view'] : '';
	if (!empty($this->_navId) && !isset($params['forceDirect']) &&
	    (empty($currentView) || $currentView == $targetView || !empty($params['controller']))) {
	    /*
	     * We are moving around in the same view or we are redirecting to a controller,
	     * who knows where it will redirect to.  Let's keep the navigation.
	     */
	    $params['navId'] = $this->_navId;
	}
	unset($params['forceDirect']);

	/* Replace any known tokens */
	foreach (array_keys($params) as $key) {
	    if ($params[$key] === '%CURRENT_URL%') {
		$params[$key] = $this->getCurrentUrl();
	    }
	}

	/* Add parameters to url */
	$url = GalleryUrlGenerator::appendParamsToUrl($url, $params);

	/* Add embed session id if needed */
	if (isset($embedSessionString)) {
	    $url .= ((strpos($url, '?') === false) ? '?' : '&amp;') . $embedSessionString;
	}

	return $url;
    }

    /**
     * Get the cookie path that will encompass G2 (and CMS app if embedded)
     *
     * The forceG2Base parameter forces the function to return the path to G2 and not to the
     * current dir of the URL. This is only relevant when G2 is embedded.
     * forceG2Base is only used for applets because they talk to G2 directly and not through the
     * embedding application. If the cookie path wouldn't be set to the G2 base in this case, the
     * applet wouldn't select the cookie for requests, because it wasn't allowed to (according to
     * the cookie specs).
     *
     * Examples:
     * Current URL                                 forceG2Base        cookie path
     * http://example.com/gallery2/main.php        false              /gallery2/
     * http://example.com/gallery2/main.php        true               /gallery2/
     * http://example.com/applicationXy/index.php  false              /applicationXy/
     * http://example.com/applicationXy/index.php  true               /gallery2/
     *
     * In the last example we assumed that G2 is installed at that location.
     *
     * @param boolean (optional) if true, ensure G2 path of base url is returned
     *                (different when embedded)
     * @return array (object Gallery status, string path)
     */
    function getCookiePath($forceG2Base=false) {
	if (!isset($this->_cookiePath[$forceG2Base])) {
	    /* Return the configured path is it is set */
	    list ($ret, $path) = GalleryCoreApi::getPluginParameter('module', 'core',
								    'cookie.path');
	    if ($ret->isError()) {
		return array($ret->wrap(__FILE__, __LINE__), null);
	    }
	    if (empty($path)) {
		$urlComponents = parse_url($this->getCurrentUrlDir($forceG2Base));
		$path = $urlComponents['path'];
	    } else {
		$this->_cookiePathConfigured = true;
	    }
	    $this->_cookiePath[$forceG2Base] = $path;
	}

	return array(GalleryStatus::success(), $this->_cookiePath[$forceG2Base]);
    }

    /**
     * Initialize the navigation branch
     *
     * If we came here with a 'return' set, we need to branch a new navigation.
     * If we are just navigating through options in the same navigation level,
     * simply pass the navId along.
     * If we are coming back from a navigation, clean up our mess.
     * In other cases we simply have no navigation support.
     *
     * Note: This should be called as soon as we have access to our session
     * and the requestVariables, but before we start generating URLs with
     * generateUrl().  Currently it's called from _GalleryMain().
     *
     * @access private
     */
    function initNavigation() {
	global $gallery;

	list ($returnUrl, $returnName, $navId, $fromNavId) =
	    GalleryUtilities::getRequestVariables('return', 'returnName', 'navId', 'fromNavId');
	if (!empty($returnUrl)) {
	    /* Branch a new navigation */
	    $navData = array('returnUrl' => $returnUrl,
			     'returnName' => $returnName);
	    if (!empty($navId)) {
		$navData['returnNavId'] = $navId;
	    }
	    $session =& $gallery->getSession();
	    $this->_navId = $session->addToNavigation($navData);
	} else {
	    if (!empty($fromNavId)) {
		/* We came back from a navigational branch. Delete it */
		$session =& $gallery->getSession();
		$session->jumpNavigation($fromNavId, $navId);
	    }
	    if (!empty($navId)) {
		/* Just continue in our current navigation level */
		$this->_navId = $navId;
	    }
	}
    }

    /**
     * Get the current navigation id
     *
     * @return string the navigation id
     */
    function getNavigationId() {
	return $this->_navId;
    }

    /**
     * Get an URL to return to the currently loaded view, stripping all parameters
     * that are of navigational nature.
     *
     * @return string the URL
     */
    function getNavigationReturnUrl() {
	global $gallery;

	$formUrl = GalleryUtilities::getRequestVariables('formUrl');
	if (!empty($formUrl)) {
	    /*
	     * We don't really have an URL, because we are in a POST request.
	     * This is the last known URL when the form was originally rendered:
	     */
	    return $formUrl;
	}

	$url = $this->getCurrentUrl();
	if ($i = strpos($url, '?')) {
	    /* We'll add the query params later */
	    $url = substr($url, 0, $i);
	}
	$params = GalleryUtilities::getUrlVariablesFiltered(
				    array('return', 'returnName', 'navId', 'fromNavId'));
	$url = GalleryUrlGenerator::appendParamsToUrl($url, $params, false);

	/*
	 * In rare cases (like first access of the app) it's possible to not be using cookies,
	 * but not have the session id in the url yet.  Detect that case and add the sid here.
	 */
	$session =& $gallery->getSession();
	if (!$session->isUsingCookies()) {
	    $sessionCheck = !empty($this->_embedSessionString)
			     ? $this->_embedSessionString
			     : (GalleryUtilities::prefixFormVariable(urlencode($session->getKey()))
				. '=' . urlencode($session->getId()));
	    if (strpos($url, $sessionCheck) === false) {
		$url .= (strpos($url, '?') === false ? '?' : '&amp;') . $sessionCheck;
	    }
	}
	return $url;
    }

    /**
     * Check if we currently have a "back" link to where we came from
     *
     * @return boolean true if we can go "back"
     */
    function isNavigationBackPossible() {
	global $gallery;
	if (empty($this->_navId)) {
	    return false;
	}
	$session =& $gallery->getSession();
	$navData = $session->getNavigation($this->_navId);
	return (count($navData) > 0);
    }

    /**
     * Get a list of navigation links to go back to where we came from.
     *
     * @param integer set this to get links back to a certain depth
     * @return array ( object GalleryStatus a status code,
     *                 array of navigational links: array('url' => ...,
     *                                            'name' => 'Back to ...'))
     */
    function getNavigationLinks($depth = null) {
	global $gallery;
	if (empty($this->_navId)) {
	    return array(GalleryStatus::success(), array());
	}

	list ($ret, $core) = GalleryCoreApi::loadPlugin('module', 'core');
	if ($ret->isError()) {
	    return array($ret->wrap(__FILE__, __LINE__), null);
	}

	$session =& $gallery->getSession();
	$navData = $session->getNavigation($this->_navId);
	$links = array();
	$i = 0;
	foreach ($navData as $navItem) {
	    $i++;
	    if (isset($depth) && $i > $depth) {
		break;
	    }
	    $url = $navItem['returnUrl'];
	    $params = array('fromNavId' => $this->_navId);
	    if (isset($navItem['returnNavId'])) {
		$params['navId'] = $navItem['returnNavId'];
	    }
	    if (!empty($navItem['returnName'])) {
		$name = $core->translate(array('text' => 'Back to %s',
					       'arg1' => $navItem['returnName']));
	    } else {
		$name = $core->translate('Back');
	    }
	    $url = GalleryUrlGenerator::appendParamsToUrl($url, $params);
	    $links[] = array('url' => $url, 'name' => $name);
	}
	return array(GalleryStatus::success(), $links);
    }

    /**
     * Decide whether to include session id in the url
     *
     * Force the session id in the url for embedded DownloadItem urls if the cookie.path
     * plugin parameter is not set. See GallerySession::init() for details.
     *
     * @param array params (string param)
     * @return boolean forceSessionId
     * @access protected
     */
    function embedForceSessionId($params) {
	if (isset($params['view']) && $params['view'] == 'core.DownloadItem' &&
		GalleryUtilities::isEmbedded() &&
		(!isset($this->_cookiePathConfigured) || $this->_cookiePathConfigured != true)) {
	    /*
	     * It is assumed that the G2 session is initiated before generateUrl() is called
	     * for the first time.
	     */
	    return true;
	} else {
	    return false;
	}
    }
}
?>
