<?php 

/**
 * Main class.
 * @license http://opensource.org/licenses/gpl-license.php GNU General Public License
 * @copyright (c)2003-2006 Tamlyn Rhodes
 * @version $Id: singapore.class.php,v 1.75 2006/09/12 11:56:11 thepavian Exp $
 */

//define constants for regular expressions
define('SG_REGEXP_PROTOCOLURL', '(?:http://|https://|ftp://|mailto:)(?:[a-zA-Z0-9\-]+\.)+[a-zA-Z]{2,4}(?::[0-9]+)?(?:/[^ \n\r\"\'<]+)?');
define('SG_REGEXP_WWWURL',      'www\.(?:[a-zA-Z0-9\-]+\.)*[a-zA-Z]{2,4}(?:/[^ \n\r\"\'<]+)?');
define('SG_REGEXP_EMAILURL',    '(?:[\w][\w\.\-]+)+@(?:[\w\-]+\.)+[a-zA-Z]{2,4}');

/**
 * Provides functions for handling galleries and images
 * @package singapore
 * @author Tamlyn Rhodes <tam at zenology dot co dot uk>
 */
class Singapore
{
  /**
   * current script version 
   * @var string
   */
  var $version = "0.10.1";
  
  /**
   * instance of a {@link sgConfig} object representing the current 
   * script configuration
   * @var sgConfig
   */
  var $config;
  
  /**
   * instance of the currently selected IO handler object
   * @var sgIO_csv
   */
  var $io;
  
  /**
   * instance of a {@link Translator}
   * @var Translator
   */
  var $translator;
  
  /**
   * instance of a {@link sgGallery} representing the current gallery
   * @var sgGallery
   */
  var $gallery;
  
  /**
   * reference to the currently selected {@link sgImage} object in the 
   * $images array of {@link $gallery}
   * @var sgImage
   */
  var $image;
  
  /**
   * two character code of language currently in use
   * @var string
   */
  var $language = null;
  
  /**
   * name of template currently in use
   * @var string
   */
  var $template = null;
  
  /**
   * details of current user
   * @var sgUser
   */
  var $user = null;
  
  /**
   * name of action requested
   * @var string
   */
  var $action = null;
  
  /**
   * Constructor, does all init type stuff. This code is a total mess.
   * @param string the path to the base singapore directory
   */
  function Singapore($basePath = "")
  {
    //import class definitions
    //io handler class included once config is loaded
    require_once $basePath."includes/translator.class.php";
    require_once $basePath."includes/thumbnail.class.php";
    require_once $basePath."includes/gallery.class.php";
    require_once $basePath."includes/config.class.php";
    require_once $basePath."includes/image.class.php";
    require_once $basePath."includes/user.class.php";
    
    //start execution timer
    $this->scriptStartTime = microtime();
    
    //remove slashes
    if(get_magic_quotes_gpc())
      $_REQUEST = array_map(array("Singapore","arraystripslashes"), $_REQUEST);
    
    //desanitize request
    $_REQUEST = array_map("htmlentities", $_REQUEST);
    
    //load config from singapore root directory
    $this->config =& sgConfig::getInstance();
    $this->config->loadConfig($basePath."singapore.ini");
    $this->config->loadConfig($basePath."secret.ini.php");
    
    //if instantiated remotely...
    if(!empty($basePath)) {
      //...try to guess base path and relative url
      if(empty($this->config->base_path))
        $this->config->base_path = $basePath;
      if(empty($this->config->base_url))
        $this->config->base_url  = $basePath;
      //...load local config if present
      //may over-ride guessed values above
      $this->config->loadConfig("singapore.local.ini");
    }
    
    //set current gallery to root if not specified in url
    $galleryId = isset($_REQUEST[$this->config->url_gallery]) ? $_REQUEST[$this->config->url_gallery] : ".";
    
    //load config from gallery ini file (gallery.ini) if present
    $this->config->loadConfig($basePath.$this->config->pathto_galleries.$galleryId."/gallery.ini");
    
    //set current template from request vars or config
    //first, preset template to default one
    $this->template = $this->config->default_template;
    //then check if requested template exists
    if(!empty($_REQUEST[$this->config->url_template])) {
     $templates = Singapore::getListing($this->config->base_path.$this->config->pathto_templates);
      foreach($templates->dirs as $single) {
        if($single == $_REQUEST[$this->config->url_template]) {
          $this->template = $single;
          break;
        }
      }
    }

    $this->config->pathto_current_template = $this->config->pathto_templates.$this->template.'/';
    //load config from template ini file (template.ini) if present
    $this->config->loadConfig($basePath.$this->config->pathto_current_template."template.ini");
        
    //set runtime values
    $this->config->pathto_logs = $this->config->pathto_data_dir."logs/";
    $this->config->pathto_cache = $this->config->pathto_data_dir."cache/";
    $this->config->pathto_admin_template = $this->config->pathto_templates.$this->config->admin_template_name."/";
    
    //set current language from request vars or config
    if(!empty($_REQUEST[$this->config->url_lang]))
      $this->language = $_REQUEST[$this->config->url_lang];
    else {
      $this->language = $this->config->default_language;
      if($this->config->detect_language)
        foreach($this->getBrowserLanguages() as $lang)
          if($lang=="en" || file_exists($basePath.$this->config->pathto_locale."singapore.".$lang.".pmo")) {
            $this->language = $lang;
            break;
          }
    }
    
    //read the language file
    $this->translator =& Translator::getInstance($this->language);
    $this->translator->readLanguageFile($this->config->base_path.$this->config->pathto_locale."singapore.".$this->language.".pmo");
    
    //clear the UMASK
    umask(0);
    
    //include IO handler class and create instance
    require_once $basePath."includes/io_".$this->config->io_handler.".class.php";
    $ioClassName = "sgIO_".$this->config->io_handler;
    $this->io = new $ioClassName($this->config);
    
    //load gallery and image info
    $this->selectGallery($galleryId);
    
    //set character set
    if(!empty($this->translator->languageStrings[0]["charset"]))
      $this->character_set = $this->translator->languageStrings[0]["charset"];
    else
      $this->character_set = $this->config->default_charset;
    
    //set action to perform
    if(empty($_REQUEST["action"])) $this->action = "view";
    else $this->action = $_REQUEST["action"];
    
    
  }
  
  /**
   * Load gallery and image info
   * @param string the id of the gallery to load (optional)
   */
  function selectGallery($galleryId = "")
  {
    if(empty($galleryId)) $galleryId = isset($_REQUEST[$this->config->url_gallery]) ? $_REQUEST[$this->config->url_gallery] : ".";
    
    //try to validate gallery id
    if(strlen($galleryId)>1 && $galleryId{1} != '/') $galleryId = './'.$galleryId;
    
    //detect back-references to avoid file-system walking
    if(strpos($galleryId,"../")!==false) $galleryId = ".";
    
    //find all ancestors to current gallery
    $this->ancestors = array();
    $ancestorNames = explode("/", $galleryId);
    $numberOfAncestors = count($ancestorNames);
    
    //construct fully qualified gallery ids
    $ancestorIds[0] = ".";
    for($i=1; $i<$numberOfAncestors; $i++)
      $ancestorIds[$i] = $ancestorIds[$i-1]."/".$ancestorNames[$i];
    
    //fetch galleries passing previous gallery as parent pointer
    for($i=0; $i<$numberOfAncestors; $i++)
      if(!$this->ancestors[$i] =& $this->io->getGallery(
              $ancestorIds[$i], $this->ancestors[$i-1],
              //only fetch children of bottom level gallery 
              ($i==$numberOfAncestors-1) ? 1 : 0
          )
        )
        break;
    
    //need to remove bogus parent of root gallery created by previous step
    unset($this->ancestors[-1]);
    
    //set reference to current gallery
    $this->gallery = &$this->ancestors[count($this->ancestors)-1];
    
    //check if gallery was successfully fetched
    if($this->gallery == null) {
      $this->gallery = new sgGallery($galleryId, $this->ancestors[0]);
      $this->gallery->name = $this->translator->_g("Gallery not found '%s'",htmlspecialchars($galleryId));
    }
    
    //sort galleries and images
    $GLOBALS["sgSortOrder"] = $this->config->gallery_sort_order;
    if($this->config->gallery_sort_order!="x") usort($this->gallery->galleries, array("Singapore","multiSort"));
    $GLOBALS["sgSortOrder"] = $this->config->image_sort_order;
    if($this->config->image_sort_order!="x") usort($this->gallery->images, array("Singapore","multiSort"));
    unset($GLOBALS["sgSortOrder"]);
    
    //if startat is set then cast to int otherwise startat 0
    $this->gallery->startat = isset($_REQUEST[$this->config->url_startat]) ? (int)$_REQUEST[$this->config->url_startat] : 0;
    $this->startat = $this->gallery->startat; //depreciated
    
    //select the image (if any)
    if(!empty($_REQUEST[$this->config->url_image]))
      $this->selectImage($_REQUEST[$this->config->url_image]);
    
    //load hit data
    if($this->config->track_views || $this->config->show_views)
      $this->io->getHits($this->gallery);
    
    //update and save hit data
    if($this->config->track_views) {
      if($this->isImagePage()) {
        $this->image->hits++;
        $this->image->lasthit = time();
      } elseif($this->gallery->startat == 0) {
        $this->gallery->hits++;
        $this->gallery->lasthit = time();
      }
      $this->io->putHits($this->gallery);
    }
  }
  
  /**
   * Selects an image from the current gallery
   * @param mixed either the filename of the image to select or the integer 
   *  index of its position in the images array
   * @return boolean true on success; false otherwise
   */
  function selectImage($image) 
  {
    if(is_string($image)) {
      foreach($this->gallery->images as $index => $img)
        if($img->id == $image) {
          $this->image =& $this->gallery->images[$index];
          return true;
        }
    } elseif(is_int($image) && $image >= 0 && $image < count($this->gallery->images)) {
      $this->image =& $this->gallery->images[$image];
      return true;
    }
    $this->image =& new sgImage("", $this->gallery);
    $this->image->name = $this->translator->_g("Image not found '%s'",htmlspecialchars($image));
    return false;
  }
  
  
  /**
   * Obfuscates the given email address by replacing "." with "dot" and "@" with "at"
   * @param string  email address to obfuscate
   * @param boolean  override the obfuscate_email config setting (optional)
   * @return string  obfuscated email address or HTML mailto link
   */
  function formatEmail($email, $forceObfuscate = false)
  {
    if($this->config->obfuscate_email || $forceObfuscate)
        return strtr($email,array("@" => ' <b>'.$this->translator->_g("email|at").'</b> ', "." => ' <b>'.$this->translator->_g("email|dot").'</b> '));
      else
        return "<a href=\"mailto:".$email."\">".$email."</a>";
  }

  
  /**
   * Returns image name for image pages and gallery name for gallery pages.
   * If either of these is empty, returns gallery_name config option.
   *
   * @return string  Title of current page
   */
  function pageTitle()
  {
    $crumbArray = $this->crumbLineArray();
    
    $ret = "";
    for($i=count($crumbArray)-1;$i>0;$i--)
      $ret .= $crumbArray[$i]->nameForce()." &lt; ";
    $ret .= $crumbArray[$i]->nameForce();
    
    return $ret;
  }
  
  /**
   * @return bool true if this is an image page; false otherwise
   */
  function isImagePage()
  {
    return !empty($this->image);
  }
  
  /**
   * @return bool true if this is a non-album gallery page; false otherwise
   */
  function isGalleryPage()
  {
    return !empty($this->gallery) && $this->gallery->galleryCount()>0;;
  }
  
  /**
   * @return bool true if this is an album page; false otherwise
   */
  function isAlbumPage()
  {
    return !$this->isGalleryPage() && !$this->isImagePage() && !empty($this->gallery);
  }
  
  
  /**
   * @return int the script execution time in seconds rounded to two decimal places
   */
  function scriptExecTime()
  {
    $scriptStartTime = $this->scriptStartTime;
    $scriptEndTime = microtime();
    
    list($usec, $sec) = explode(" ",$scriptStartTime); 
    $scriptStartTime = (float)$usec + (float)$sec; 
    list($usec, $sec) = explode(" ",$scriptEndTime); 
    $scriptEndTime = (float)$usec + (float)$sec; 
    
    $scriptExecTime = floor(($scriptEndTime - $scriptStartTime)*100)/100;
    return $scriptExecTime;
  }
  
  /**
   * Displays the script execution time if configured to do so
   * @returns string  the script execution time
   */
  function scriptExecTimeText()
  {
    if($this->config->show_execution_time)
      return $this->translator->_g("Page created in %s seconds",$this->scriptExecTime());
    else
      return "";
  }
  
  function poweredByText()
  {
    return $this->translator->_g("Powered by").' <a href="http://www.sgal.org/">singapore</a>';
  }
  
  function allRightsReserved()
  {
    return $this->translator->_g("All rights reserved.");
  }
  
  function licenseText()
  {
    return $this->translator->_g("Images may not be reproduced in any form without the express written permission of the copyright holder.");
  }
  
  function adminURL()
  {
    return '<a href="'.$this->config->base_url.'admin.php">';
  }
  
  function adminLink()
  {
    return $this->adminURL().$this->translator->_g("Log in")."</a>";
  }
  
    
  /**
   * Checks to see if the user is currently logged in to admin mode. Also resets
   * the login timeout to the current time.
   * @returns boolean  true if the user is logged in; false otherwise
   * @static
   */
  function isLoggedIn()
  {
    if(
      isset($this->user) && 
      $_SESSION["sgUser"]["ip"] == $_SERVER["REMOTE_ADDR"] && 
      (time() - $_SESSION["sgUser"]["loginTime"] < 600)
    ) {
      //reset loginTime to current time
      $_SESSION["sgUser"]["loginTime"] = time();
      return true;
    }
    return false;
  }
  
  function loadUser($username = null)
  {
    if($username == null)
      if(isset($_SESSION["sgUser"]))
        $username = $_SESSION["sgUser"]["username"];
      else
        return false;
    
    $users = $this->io->getUsers();
    foreach($users as $user)
      if($user->username == $username) {
        $this->user = $user;
        return $user;
      }
    return false;
  }
  
  /**
   * Creates an array of objects each representing an item in the crumb line.
   * @return array  the items of the crumb line
   */
  function crumbLineArray()
  {
    $crumb = $this->ancestors;
    
    if($this->isImagePage()) $crumb[] = $this->image;
    
    return $crumb;
  }
  
  /**
   * @return string  the complete crumb line with links
   */
  function crumbLineText()
  {
    $crumbArray = $this->crumbLineArray();

    $ret = "";
    for($i=0;$i<count($crumbArray)-1;$i++)
      $ret .= $crumbArray[$i]->nameLink()." &gt;\n";
    $ret .= $crumbArray[$i]->nameForce();

    return $ret;
  }
  
  function crumbLine()
  {
    return $this->translator->_g("crumb line|You are here:")." ".$this->crumbLineText();
  }
  
  /**
   * Generates the HTML code for imagemap_navigation
   * @return string imagemap HTML code
   */
  function imageMap()
  {
    if(!$this->config->imagemap_navigation) return "";
    
    $imageWidth = $this->image->width();
    $imageHeight = $this->image->height();
    $middleX = round($imageWidth/2);
    $middleY = round($imageHeight/2);
    
    $ret  = "<map name=\"sgNavMap\" id=\"sgNavMap\">\n";
    if($this->image->hasNext()) $ret .= '<area href="'.$this->image->nextURL().'" alt="'.$this->image->nextText().'" title="'.$this->image->nextText().'" shape="poly" ';
    else $ret .= '<area href="'.$this->image->parentURL().'" alt="'.$this->image->parentText().'" title="'.$this->image->parentText().'" shape="poly" ';
    $ret .= "coords=\"$middleX,$middleY,$imageWidth,$imageHeight,$imageWidth,0,$middleX,$middleY\" />\n";
    if($this->image->hasPrev()) $ret .= '<area href="'.$this->image->prevURL().'" alt="'.$this->image->prevText().'" title="'.$this->image->prevText().'" shape="poly" ';
    else $ret .= '<area href="'.$this->image->parentURL().'" alt="'.$this->image->parentText().'" title="'.$this->image->parentText().'" shape="poly" ';
    $ret .= "coords=\"$middleX,$middleY,0,0,0,$imageHeight,$middleX,$middleY\" />\n";
    $ret .= '<area href="'.$this->image->parentURL().'" alt="'.$this->image->parentText().'" title="'.$this->image->parentText().'" shape="poly" ';
    $ret .= "coords=\"$middleX,$middleY,0,0,$imageWidth,0,$middleX,$middleY\" />\n";
    $ret .= '</map>';
    
    return $ret;
  }
  
  /**
   * Generates the HTML code for the language select box
   * @return string select box HTML code
   */
  function languageFlipper()
  {
    if(!$this->config->language_flipper) return "";
    
    $languageCache = $this->config->base_path.$this->config->pathto_data_dir."languages.cache";
    // Look for the language file
    if(!file_exists($languageCache))
      return "";
    
    // Open the file
    $fp = @fopen($languageCache, "r");
    if (!$fp) return "";
    
    // Read contents
    $str = '';
    while (!feof($fp)) $str .= fread($fp, 1024);
    // Unserialize
    $availableLanguages = @unserialize($str);
    
    $ret  = '<div class="sgLanguageFlipper">';
    $ret .= '<form method="get" action="'.htmlspecialchars($_SERVER["PHP_SELF"])."\">\n";
    //carry over current get vars
    foreach($_GET as $var => $val)
      $ret .= '<input type="hidden" name="'.$var.'" value="'.htmlspecialchars($val)."\" />\n";
    $ret .= '<select name="'.$this->config->url_lang."\">\n";
    $ret .= '  <option value="'.$this->config->default_language.'">'.$this->translator->_g("Select language...")."</option>\n";
    foreach($availableLanguages as $code => $name) {
      $ret .= '  <option value="'.$code.'"';
      if($code == $this->language && $this->language != $this->config->default_language)
        $ret .= 'selected="true" ';
      $ret .= '>'.htmlentities($name)."</option>\n";
    }
    $ret .= "</select>\n";
    $ret .= '<input type="submit" class="button" value="'.$this->translator->_g("Go")."\" />\n";
    $ret .= "</form></div>\n";
    return $ret;
  }
  
  /**
   * Generates the HTML code for the template select box
   * @return string select box HTML code
   */
  function templateFlipper()
  {
    if(!$this->config->template_flipper) return "";
    
    //get list of installed templates
    $templates = Singapore::getListing($this->config->base_path.$this->config->pathto_templates, "dirs");
    
    $ret  = '<div class="sgTemplateFlipper">';
    $ret .= '<form method="get" action="'.$_SERVER["PHP_SELF"]."\">\n";
    //carry over current get vars
    foreach($_GET as $var => $val)
      $ret .= '<input type="hidden" name="'.$var.'" value="'.htmlspecialchars($val)."\" />\n";
    $ret .= '<select name="'.$this->config->url_template."\">\n";
    $ret .= '  <option value="'.$this->config->default_template.'">'.$this->translator->_g("Select template...")."</option>\n";
    foreach($templates->dirs as $name)
      //do not list admin template(s)
      if(strpos($name, "admin_")===false) {
        $ret .= '  <option value="'.$name.'"';
        if($name == $this->template && $this->template != $this->config->default_template)
          $ret .= 'selected="true" ';
        $ret .= '>'.$name."</option>\n";
      }
    $ret .= "</select>\n";
    $ret .= '<input type="submit" class="button" value="'.$this->translator->_g("Go")."\" />\n";
    $ret .= "</form></div>\n";
    return $ret;
  }
  
  
  /**
   * @param string $seperator optional string to seperate the Gallery Tab Links
   * @return string
   */
  function galleryTab($seperator = " | ")
  {
    $showing = $this->galleryTabShowing();
    return Singapore::conditional($this->galleryTabLinks(), $showing.$seperator."%s", $showing);
  }
  
  /**
   * @return string
   */
  function galleryTabShowing()
  {
    if($this->isAlbumPage()) {
      $total = $this->gallery->imageCount();
      $perPage = $this->config->thumb_number_album;
    } else {
      $total = $this->gallery->galleryCount();
      $perPage = $this->config->thumb_number_gallery;
    }
    
    if($this->gallery->startat+$perPage > $total)
      $last = $total;
    else
      $last = $this->gallery->startat+$perPage;
    
    return $this->translator->_g("Showing %s-%s of %s",($this->gallery->startat+1),$last,$total);
  }
  
  /**
   * @return string
   */
  function galleryTabLinks()
  {
    $ret = "";
    if($this->hasPrevPage())
      $ret .= $this->prevPageLink()." ";
    
    //This is for compatibility with old templates
    //it detects if compatibility mode is on using method_exists()
    if(!$this->gallery->isRoot() && method_exists($this, 'galleryName'))
      $ret .= $this->gallery->parentLink();
    
    if($this->hasNextPage())
      $ret .= " ".$this->nextPageLink();
        
    return $ret;
  }
  
  function navigationLinks() {
    $ret = "<link rel=\"Top\" title=\"".$this->config->gallery_name."\" href=\"".$this->ancestors[0]->URL()."\" />\n";
    
    if($this->isImagePage()) {
      $ret .= "<link rel=\"Up\" title=\"".$this->image->parent->name()."\" href=\"".$this->image->parent->URL()."\" />\n";
      if ($this->image->hasPrev()) {
        $first= $this->image->firstImage();
        $prev = $this->image->prevImage();
        $ret .= "<link rel=\"First\" title=\"".$first->name()."\" href=\"".$first->URL()."\" />\n";
        $ret .= "<link rel=\"Prev\" title=\"".$prev->name()."\" href=\"".$prev->URL()."\" />\n";
      }
      if ($this->image->hasNext()) {
        $next = $this->image->nextImage();
        $last = $this->image->lastImage();
        $ret .= "<link rel=\"Next\" title=\"".$next->name()."\" href=\"".$next->URL()."\" />\n";
        $ret .= "<link rel=\"Last\" title=\"".$last->name()."\" href=\"".$last->URL()."\" />\n";
        //prefetch next image
        $ret .= "<link rel=\"Prefetch\" href=\"".$next->imageURL()."\" />\n";
      }
    } else {
      if(!$this->gallery->isRoot())
        $ret .= "<link rel=\"Up\" title=\"".$this->gallery->parent->name()."\" href=\"".$this->gallery->parent->URL()."\" />\n";
      if($this->hasPrevPage()) {
        $ret .= "<link rel=\"Prev\" title=\"".$this->translator->_g("gallery|Previous")."\" href=\"".$this->prevPageURL()."\" />\n";
        $ret .= "<link rel=\"First\" title=\"".$this->translator->_g("gallery|First")."\" href=\"".$this->firstPageURL()."\" />\n";
      }
      if($this->hasNextPage()) {
        $ret .= "<link rel=\"Next\" title=\"".$this->translator->_g("gallery|Next")."\" href=\"".$this->nextPageURL()."\" />\n";
        $ret .= "<link rel=\"Last\" title=\"".$this->translator->_g("gallery|Last")."\" href=\"".$this->lastPageURL()."\" />\n";
      } 
    }
    return $ret;
  }
  
  
  /** 
   * @return int the number of 'pages' or 'screen-fulls'
   */
  function galleryPageCount() {
    if($this->isAlbumPage())
      return intval($this->gallery->imageCount()/$this->config->thumb_number_album)+1;
    else
      return intval($this->gallery->galleryCount()/$this->config->thumb_number_gallery)+1;
  }
  
  /** 
   * @return int
   */
  function lastPageIndex() {
    if($this->isAlbumPage())
      return ($this->galleryPageCount()-1)*
        ($this->isAlbumPage()?$this->config->thumb_number_album:$this->config->thumb_number_gallery);
  }
  
  /**
   * @return bool true if there is at least one more page
   */
  function hasNextPage() {
    if($this->isAlbumPage())
      return count($this->gallery->images)>$this->startat+$this->config->thumb_number_album;
    elseif($this->isGalleryPage())
      return count($this->gallery->galleries)>$this->startat+$this->config->thumb_number_gallery;
    elseif($this->isImagePage())
      return isset($this->gallery->images[$this->image->index+1]);
  }
  
  /**
   * @return bool true if there is at least one previous page
   */
  function hasPrevPage() {
    if($this->isAlbumPage() || $this->isGalleryPage())
      return $this->startat > 0;
    elseif($this->isImagePage())
      return isset($this->gallery->images[$this->image->index-1]);
  }
  
  function firstPageURL() {
    return $this->gallery->URL(0);
  }
  
  function firstPageLink() {
    return "<a href=\"".$this->firstPageURL()."\">".$this->translator->_g("First %s", $this->itemsPerPage())."</a>";
  }
  
  /**
   * @return string the URL of the previous page
   */
  function prevPageURL() {
    return $this->gallery->URL($this->startat - $this->itemsPerPage());
  }
  
  function prevPageLink() {
    return "<a href=\"".$this->prevPageURL()."\">".$this->translator->_g("Previous %s", $this->itemsPerPage())."</a>";
  }
  
  /**
   * @return string the URL of the next page
   */
  function nextPageURL() {
    return $this->gallery->URL($this->startat + $this->itemsPerPage());
  }
  
  function nextPageLink() {
    return "<a href=\"".$this->nextPageURL()."\">".$this->translator->_g("Next %s", $this->itemsPerPage())."</a>";
  }
  
  function lastPageURL() {
    $perpage = $this->isAlbumPage() ? $this->config->thumb_number_album : $this->config->thumb_number_gallery;
    return $this->gallery->URL(floor($this->gallery->itemCount() / $perpage) * $perpage);
  }
  
  function lastPageLink() {
    return "<a href=\"".$this->lastPageURL()."\">".$this->translator->_g("Last %s", $this->itemsPerPage())."</a>";
  }
  
  function itemsPerPage()
  {
    return $this->isAlbumPage() ? $this->config->thumb_number_album : $this->config->thumb_number_gallery;
  }
  
  /**
   * @return string link for adding a comment to image
   */
  function imageCommentLink()
  {
    return "<a href=\"".
      $this->formatURL($this->gallery->idEncoded(), $this->image->id, null, "addcomment").
      "\">".$this->translator->_g("Add a comment")."</a>";
  }
  
    /**
   * @return array  array of sgImage objects
   */
  function &previewThumbnailsArray()
  {
    $ret = array();
    $index = $this->image->index();
    $start = ceil($index - $this->config->thumb_number_preview/2);
    
    for($i = $start; $i < $start + $this->config->thumb_number_preview; $i++)
      if(isset($this->image->parent->images[$i]))
        $ret[$i] =& $this->image->parent->images[$i];
      
    return $ret;
  }
  
  function previewThumbnails()
  {
    $thumbs =& $this->previewThumbnailsArray();
    $index = $this->image->index();
    $ret = "";
    foreach($thumbs as $key => $thumb) {
      $thumbClass = "sgThumbnailPreview";
      if($key==$index-1) $thumbClass .= " sgThumbnailPreviewPrev";
      elseif($key==$index) $thumbClass .= " sgThumbnailPreviewCurrent";
      elseif($key==$index+1) $thumbClass .= " sgThumbnailPreviewNext";
      $ret .= $thumb->thumbnailLink($thumbClass, "preview")."\n";
    }
      
    return $ret;
  }
  
  //////////////////////////////
  //////ex-sgUtils methods//////
  //////////////////////////////
  
  /**
   * Callback function for sorting things
   * @static
   */
  function multiSort($a, $b) {
    switch($GLOBALS["sgSortOrder"]) {
      case "f" :
      case "p" : return strcmp($a->id, $b->id); //path
      case "F" :
      case "P" : return strcmp($b->id, $a->id); //path (reverse)
      case "n" : return strcmp($a->name, $b->name); //name
      case "N" : return strcmp($b->name, $a->name); //name (reverse)
      case "i" : return strcasecmp($a->name, $b->name); //case-insensitive name
      case "I" : return strcasecmp($b->name, $a->name); //case-insensitive name (reverse)
      case "a" : return strcmp($a->artist, $b->artist); //artist
      case "A" : return strcmp($b->artist, $a->artist); //artist (reverse)
      case "d" : return strcmp($a->date, $b->date); //date
      case "D" : return strcmp($b->date, $a->date); //date (reverse)
      case "l" : return strcmp($a->location, $b->location); //location
      case "L" : return strcmp($b->location, $a->location); //location (reverse)
    }
  }
  
  /**
   * Slightly pointless method
   */
  function conditional($conditional, $iftrue, $iffalse = null)
  {
    if($conditional) return sprintf($iftrue, $conditional);
    elseif($iffalse != null) return sprintf($iffalse, $conditional);
    else return "";
  }
  
  /**
   * Callback function for recursively stripping slashes
   * @static
   */
  function arraystripslashes($toStrip)
  {
    if(is_array($toStrip))
      return array_map(array("Singapore","arraystripslashes"), $toStrip);
    else
      return stripslashes($toStrip);
  }
  
  function thumbnailPath($gallery, $image, $width, $height, $forceSize, $mode = 1)
  {
    $config =& sgConfig::getInstance();
    switch($mode) {
      case 0 :
        return $config->pathto_data_dir."cache/".$width."x".$height.($forceSize?"f":"").strtr("-$gallery-$image",":/?\\","----");
      case 1 :
        return $config->pathto_galleries.$gallery."/_thumbs/".$width."x".$height.($forceSize?"f":"").strtr("-$image",":/?\\","----");
    }
  }
  
  /**
   * @param string  relative or absolute path to directory
   * @param string  regular expression of files to return (optional)
   * @param bool    true to get hidden directories too
   * @returns stdClass|false  a data object representing the directory and its contents
   * @static
   */
  function getListing($wd, $mask = null, $getHidden = false)
  {
    $dir = new stdClass;
    $dir->path = realpath($wd)."/";
    $dir->files = array();
    $dir->dirs = array();
    
    $dp = opendir($dir->path);
    if(!$dp) return false;

    while(false !== ($entry = readdir($dp)))
      if(is_dir($dir->path.$entry)) {
        if(($entry{0} != '.' && $entry{0} != '_') || $getHidden)
          $dir->dirs[] = $entry;
      } else {
        if($mask == null || preg_match("/\.($mask)$/i",$entry))
          $dir->files[] = $entry;
      }
    
    sort($dir->files);
    sort($dir->dirs);
    closedir($dp);
    return $dir;
  }
  
  /**
   * Recursively deletes all directories and files in the specified directory.
   * USE WITH EXTREME CAUTION!!
   * @returns boolean true on success; false otherwise
   * @static
   */
  function rmdir_all($wd)
  {
    if(!$dp = opendir($wd)) return false;
    $success = true;
    while(false !== ($entry = readdir($dp))) {
      if($entry == "." || $entry == "..") continue;
      if(is_dir("$wd/$entry")) $success &= Singapore::rmdir_all("$wd/$entry");
      else $success &= unlink("$wd/$entry");
    }
    closedir($dp);
    $success &= rmdir($wd);
    return $success;
  }
  
  /**
   * Returns an array of language codes specified in the Accept-Language HHTP
   * header field of the user's browser. q= components are ignored and removed.
   * hyphens (-) are converted to underscores (_).
   * @return array  accepted language codes
   * @static
   */
  function getBrowserLanguages()
  {
    $langs = array();
    foreach(explode(",",$_SERVER["HTTP_ACCEPT_LANGUAGE"]) as $bit)
      if($pos = strpos($bit,";"))
        $langs[] = strtr(substr($bit,0,$pos),"-","_");
      else
        $langs[] = strtr($bit,"-","_");
    return $langs;
  }
  
  /**
   * Wrapper for mkdir() implementing the safe-mode hack
   */
  function mkdir($path)
  {
    $config =& sgConfig::getInstance();
    if($config->safe_mode_hack) {
      $connection = ftp_connect($config->ftp_server);
      // login to ftp server
      $result = ftp_login($connection, $config->ftp_user, $config->ftp_pass);
   
      // check if connection was made
      if ((!$connection) || (!$result))
        return false;
  
      ftp_chdir($connection, $config->ftp_base_path); // go to destination dir
      if(!ftp_mkdir($connection, $path)) // create directory
        return false;
      
      ftp_site($connection, "CHMOD ".$config->directory_mode." ".$path);
      ftp_close($connection); // close connection
      return true;
    } else 
      return mkdir($path, octdec($config->directory_mode));
  }
  
  /**
   * Tests if $child is within or is the same path as $parent. 
   *
   * @param string  path to parent directory
   * @param string  path to child directory or file
   * @param bool    set false to prevent canonicalisation of paths (optional)
   * @return bool   true if $child is contained within or is $parent 
   */
  function isSubPath($parent, $child, $canonicalise = true) 
  {
    $parentPath = $canonicalise ? realpath($parent) : $parent;
    $childPath = $canonicalise ? realpath($child) : $child;
    return $parentPath && $childPath && substr($childPath,0,strlen($parentPath)) == $parentPath;
  }


  function isInGroup($groups1,$groups2) 
  {
    return (bool) array_intersect(explode(" ",$groups1),explode(" ",$groups2)); 
  }
  
}


?>
