<?php

//
// qdgal - quick & dirty php gallery
// Copyright (C) 2016-2026 Mateusz Viste
//
// https://mateusz.fr/qdgal
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
// See attached README file for the full MIT license text.
//

// qdgal version
define('PVER', '20260806');

// preload default config variables
$ALBUMTITLE = 'Album';
$PHOTOSREGEX = '/(\.jpg$)|(\.jpeg$)|(\.png$)|(\.gif$)|(\.webp$)/i';
$VIDEOSREGEX = '/(\.mp4$)|(\.webm$)/i';
$GALCOVER_IS_LAST = 1;
$THUMBNAILS_HEIGHT = 0;
$CUSTOM_CSS = 0;
$SORTREV_PHOTOS = 0;
$SORTREV_GALLERIES = 1;
$SLIDESHOW_DELAY = 0;
$ACTION_PREFIX = '&starf; ';
$CACHELIMIT = 1200;
$FAVICON = '';
$PRETHUMB_CMD = '';
$AUTH_PASS = '';
$AUTH_TIME = 0;
$VIDEOTHUMBCMD = '';
$AUTOROTATE = 0;
$CACHEFOLDER = '.qdgal-cache';

// load config settings
include 'qdconf.php';

// load localization strings
$STR_BACKTOALBUM = array();
$STR_PREVPHOTO = array();
$STR_NEXTPHOTO = array();
$STR_EMPTYGAL = array();
$STR_SLIDESHOW_START = array();
$STR_SLIDESHOW_STOP = array();
include 'qdlang.php';


// ============ FUNCTIONS ============

// computes a thumbnail of a video. returns true on success, false on failure
function createthumbvid($src, $dst) {
  global $VIDEOTHUMBCMD;
  global $THUMBNAILS_HEIGHT;
  if (empty($VIDEOTHUMBCMD)) return(false);
  // prepare the command, and execute it
  setlocale(LC_CTYPE, "UTF8", "en_US.UTF-8"); // required by escapeshellarg()
  $src_safename = escapeshellarg($src);
  $dst_safename = escapeshellarg($dst);
  $execstr = str_replace(array('%THUMB', '%VID', '%HEIGHT'), array($dst_safename, $src_safename, $THUMBNAILS_HEIGHT), $VIDEOTHUMBCMD);
  exec($execstr);
  if (!file_exists($dst)) return(false);
  return(true);
}

// computes a thumbnail of a file, in given height (and proportional width)
// also rotates the image, if exif is available and orientation data present
// returns true on success, false on failure
function createthumb($src, $newheight, $dst) {
  global $AUTOROTATE;
  $exif = false; // used later for EXIF data storage to fix jpeg orientation
  $rotated = 0;

  // read image's resolution and type
  $imginfo = getimagesize($src);
  if ($imginfo === false) return false;
  list($width_orig, $height_orig, $imgtype) = $imginfo;
  if (($width_orig < 1) || ($height_orig < 1) || ($newheight < 1)) return false;

  // load the original image
  $image = false;
  switch ($imgtype) {
    case IMAGETYPE_JPEG:
      $image = imagecreatefromjpeg($src);
      if (function_exists('exif_read_data')) $exif = exif_read_data($src);
      break;
    case IMAGETYPE_PNG:
      $image = imagecreatefrompng($src);
      break;
    case IMAGETYPE_GIF:
      if (function_exists('imagecreatefromgif')) $image = imagecreatefromgif($src);
      break;
    case 18: // IMAGETYPE_WEBP (constant since PHP 7.1)
      if (function_exists('imagecreatefromwebp')) $image = imagecreatefromwebp($src);
      break;
  }

  if (!$image) return false; // unsupported file format

  // if exif data is present, fix the rotation (GD doesn't take care of that,
  // while it strips exif data from the image at the same time)
  if (($exif) && (!empty($exif['Orientation']))) {
    $rotated_img = false;
    switch ($exif['Orientation']) {
      case 3:
        $rotated_img = imagerotate($image, 180, 0);
        $rotated = 1;
        break;
      case 6:
        $rotated_img = imagerotate($image, -90, 0);
        list($height_orig, $width_orig) = array($width_orig, $height_orig);
        $rotated = 1;
        break;
      case 8:
        $rotated_img = imagerotate($image, 90, 0);
        list($height_orig, $width_orig) = array($width_orig, $height_orig);
        $rotated = 1;
        break;
    }
    if ($rotated_img !== false) {
      imagedestroy($image);
      $image = $rotated_img;
    } else if ($rotated != 0) {
      imagedestroy($image);
      return false;
    }
  }

  // save the rotated jpg back to disk if AUTOROTATE enabled (quality 92:
  // default imagejpeg quality is 75 and would visibly degrade originals)
  if (($AUTOROTATE != 0) && ($rotated != 0)) imagejpeg($image, $src, 92);

  // compute new width
  $width = (int) round($newheight * ($width_orig / $height_orig));
  if ($width < 1) $width = 1;

  // resample the original image into a new memory buffer
  $image_p = imagecreatetruecolor($width, $newheight);
  if (!$image_p) {
    imagedestroy($image);
    return false;
  }
  imagecopyresampled($image_p, $image, 0, 0, 0, 0, $width, $newheight, $width_orig, $height_orig);
  imagedestroy($image);

  // save the new (thumbnail) image into destination file
  $ok = imagejpeg($image_p, $dst);
  imagedestroy($image_p);
  return $ok;
}

// this function is used to translate a path/filename to a location into a
// working link, applying proper encoding whenever necessary.
function enclink($img) {
  $arr = array();
  $arr = explode('/', $img);
  $res = '';
  $first = 1;
  foreach ($arr as $token) {
    if ($first == 1) {
      $first = 0;
    } else {
      $res .= '/';
    }
    $res .= rawurlencode($token);
  }
  return($res);
}

// encodes text to html - basically just a wrapper over htmlentities()
function enchtml($txt) {
  return(htmlentities($txt, ENT_QUOTES, 'UTF-8'));
}

// returns path to a thumbnail if available for given image (if not exists,
// it tries to create it and returns false only if thumb creation fails)
function isthumbavailable($img, $type) {
  global $THUMBNAILS_HEIGHT;
  global $PRETHUMB_CMD;
  global $CACHEFOLDER;
  $thumb = $CACHEFOLDER . '/' . md5($img);
  if (! file_exists($img)) return(false);
  $imgmodtime = filemtime($img);
  $thumbmodtime = false;
  if (file_exists($thumb)) $thumbmodtime = filemtime($thumb);
  // directory thumbs: reuse only while the gallery directory mtime matches
  // (add/remove photos updates the directory mtime on typical filesystems)
  if ($type == 'dir') {
    if (($thumbmodtime) && ($imgmodtime) && ($thumbmodtime === $imgmodtime)) {
      return($thumb);
    }
    return(false);
  }
  //
  if (($imgmodtime) && ($imgmodtime === $thumbmodtime)) {
    return($thumb);
  } else if ($type == 'img') { // photo image
    mkdir($CACHEFOLDER, 0755, true);
    // execute the prethumb command, if any
    if (! empty($PRETHUMB_CMD)) {
      setlocale(LC_CTYPE, "UTF8", "en_US.UTF-8"); // required by escapeshellarg()
      $img_safename = escapeshellarg($img);
      $execstr = str_replace('%f', $img_safename, $PRETHUMB_CMD);
      exec($execstr);
      clearstatcache(); // PHP keeps the last result of filemtime() in cache, which is real bad here, so I need to clear the cache first
      $imgmodtime = filemtime($img); // re-read image's modtime, in case it got updated by PRETHUMB_CMD
    }
    if (! createthumb($img, $THUMBNAILS_HEIGHT, $thumb)) return(false);
    touch($thumb, $imgmodtime);
    return($thumb);
  } else if ($type == 'vid') { // video file
    mkdir($CACHEFOLDER, 0755, true);
    if (! createthumbvid($img, $thumb)) return(false);
    touch($thumb, $imgmodtime);
    return($thumb);
  }
  return(false);
}

// fetches the list of galleries and photos inside a directory
function getdircontent($dir) {
  global $PHOTOSREGEX;
  global $VIDEOSREGEX;
  global $SORTREV_PHOTOS;
  global $SORTREV_GALLERIES;
  $listdir = scandir($dir, 0); // use 0 instead of SCANDIR_SORT_ASCENDING for PHP 5.3 compatibility
  $res = array();
  $res['dirs'] = array();
  $res['files'] = array();
  foreach ($listdir as $i) {
    if (is_dir($dir . '/' . $i)) {
      if ($i[0] == '.') continue; // skip '.', '..' and all hidden directories
      $res['dirs'][] = $i;
    } else if (preg_match('/\.thumb$/', $i)) {
      continue;
    } else if (preg_match($PHOTOSREGEX, $i)) {
      $res['files'][$i] = 'img';
    } else if (preg_match($VIDEOSREGEX, $i)) {
      $res['files'][$i] = 'vid';
    } else {
      continue;
    }
  }
  // natural order so photo2 sorts before photo10 (scandir is plain lexical)
  natcasesort($res['dirs']);
  $res['dirs'] = array_values($res['dirs']);
  uksort($res['files'], 'strnatcasecmp');
  if ($SORTREV_PHOTOS) $res['files'] = array_reverse($res['files']);
  if ($SORTREV_GALLERIES) $res['dirs'] = array_reverse($res['dirs']);
  return($res);
}

// returns properties of a gallery: images count and cover image, as well as the cover type
function getgalprop($galpath) {
  global $GALCOVER_IS_LAST;
  $res = array();
  $res['cover'] = false;
  $res['covertype'] = '';
  $res['count'] = 0;
  $dircontent = getdircontent($galpath);
  $res['files'] = $dircontent['files'];
  $res['dirs'] = $dircontent['dirs'];

  foreach ($res['files'] as $i => $t) {
    if (($GALCOVER_IS_LAST) || ($res['cover'] == false)) {
      $res['cover'] = $galpath . '/' . $i;
      $res['covertype'] = $t;
    }
    $res['count'] += 1;
  }
  // if no cover found, look recursively into subdirectories
  if ($res['cover'] == false) {
    foreach ($res['dirs'] as $d) {
      $tmpres = getgalprop($galpath . '/' . $d);
      $res['cover'] = $tmpres['cover'];
      $res['covertype'] = $tmpres['covertype'];
      if ($res['cover'] != false) break;
    }
  }
  return($res);
}

// builds a proper self-referencing URL for either a gallery or a file
function buildurl($subgal, $file, $slideshowflag = FALSE) {
  $res = '';
  if (! empty($subgal)) {
    $res = '?g=' . rawurlencode($subgal);
  }
  if (! empty($file)) {
    if (empty($res)) {
      $res = '?';
    } else {
      $res .= '&';
    }
    $res .= 'f=' . rawurlencode($file);
    // if slideshow, then add the sshow flag
    if ($slideshowflag != FALSE) $res .= '&sshow=1';
  }
  $res = './' . $res;
  return($res);
}

// returns an array with the list of languages requested by the browser, in
// the order of preference
function getpreflang() {
  $res = array();
  if (! isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) return($res);
  $langlist = explode(',', $_SERVER['HTTP_ACCEPT_LANGUAGE']);
  foreach ($langlist as $lang) {
    $res[] = strtolower(substr($lang, 0, 2));
  }
  return(array_unique($res));
}


// ============ MAIN ============

$HOMEALBUM = $ALBUMTITLE; // remember the home album title

// first of all, check for php dependencies
if (($THUMBNAILS_HEIGHT > 0) && (! function_exists('gd_info'))) {
  echo "Error: the GD library must be installed when thumbnail support is enabled.\n";
  exit(0);
}

// negotiate language
$lang = 'en'; // preselect english as default language
foreach (getpreflang() as $l) {
  if (isset($STR_BACKTOALBUM[$l])) {
    $lang = $l;
    break;
  }
}

// do we have password-protection?
if (!empty($AUTH_PASS)) {
  // HttpOnly session cookie; keep existing path/domain/secure defaults
  $cookie = session_get_cookie_params();
  session_set_cookie_params(
    ($AUTH_TIME > 0) ? $AUTH_TIME : $cookie['lifetime'],
    $cookie['path'],
    $cookie['domain'],
    $cookie['secure'],
    true
  );
  session_start();
  // token changes when AUTH_PASS changes → old sessions stop working
  $auth_token = hash('sha256', 'qdgal-auth|' . $AUTH_PASS);
  if ((isset($_POST['password'])) && (hash_equals($AUTH_PASS, (string) $_POST['password']))) {
    $_SESSION['auth'] = $auth_token;
  }
  if ((!isset($_SESSION['auth'])) || (!hash_equals($auth_token, (string) $_SESSION['auth']))) {
    echo "<!DOCTYPE html>\n";
    echo '<html lang="' . enchtml($lang) . '">' . "\n";
    echo "<head>\n";
    echo "  <title></title>\n";
    echo "  <meta charset=\"utf-8\">\n";
    echo '  <meta name="generator" content="qdgal ' . PVER . "\">\n";
    if (! empty($FAVICON)) echo '  <link rel="icon" href="' . enchtml($FAVICON) . '">' . "\n";
    echo "  <link rel=\"stylesheet\" type=\"text/css\" href=\"qdgal.css\">\n";
    if (file_exists('qdgal-custom.css')) {
      echo "  <link rel=\"stylesheet\" type=\"text/css\" href=\"qdgal-custom.css\">\n";
    }
    echo '  <meta name="viewport" content="width=device-width, initial-scale=1.0">' . "\n";
    echo "</head>\n";
    echo "<body>\n";
    // ask for password and quit
    echo '<p class="auth">' . $STR_AUTH_TITLE[$lang] . "</p>\n";
    echo '<div class="auth">' . "\n";
    // rebuild action from known GET params only (never echo QUERY_STRING — XSS)
    $auth_action = buildurl(
      isset($_GET['g']) ? $_GET['g'] : '',
      isset($_GET['f']) ? $_GET['f'] : '',
      ((! empty($_GET['sshow'])) && ($_GET['sshow'] != 0))
    );
    echo '<form action="' . enchtml($auth_action) . '" method="post" class="auth">' . "\n";
    echo '<input autofocus type="password" name="password" id="password">' . "\n";
    echo '<input type="submit" value="' . $STR_AUTH_BUTTON[$lang] . '">' . "\n";
    echo "</form>\n";
    echo "</div>\n";
    echo "</body>\n";
    echo "</html>";
    session_destroy();
    exit(0);
  }
}

// is this about showing up a sub-gallery?
$subdir = '';
$subdirurl = ''; // this one will be used to build URLs
if (! empty($_GET['g'])) {
  $subdir = $_GET['g'];
  $subdirurl = $subdir . '/';
  $ALBUMTITLE = basename($subdir);
  // security check: gallery path must stay under the album root
  if ($subdir[0] == '.') { // gallery should never start with a dot
    header($_SERVER['SERVER_PROTOCOL'] . ' 403 Forbidden');
    echo "Forbidden\n";
    exit(0);
  }
  $realsubdir = realpath($subdir);
  $realcurdir = realpath('.');
  if (($realsubdir === false) || ($realcurdir === false)) {
    header($_SERVER['SERVER_PROTOCOL'] . ' 404 Not found');
    echo "404 NOT FOUND\n";
    exit(0);
  }
  // require exact match or a path under curdir/ (avoids /gal matching /galicious)
  if (($realsubdir !== $realcurdir) && (strpos($realsubdir, $realcurdir . DIRECTORY_SEPARATOR) !== 0)) {
    header($_SERVER['SERVER_PROTOCOL'] . ' 403 Forbidden');
    echo "Forbidden\n";
    exit(0);
  }
}

// if this is about a photo/video file, validate the filename and that it exists
// f= must be a plain basename (no directories / traversal); otherwise any readable
// file under the web root could be embedded via ?f=../secret.jpg
if (! empty($_GET['f'])) {
  $reqfile = $_GET['f'];
  if (($reqfile !== basename($reqfile)) || ($reqfile === '.') || ($reqfile === '..')) {
    header($_SERVER['SERVER_PROTOCOL'] . ' 403 Forbidden');
    echo "Forbidden\n";
    exit(0);
  }
  if (empty($subdir)) {
    $reqfilepath = $reqfile;
  } else {
    $reqfilepath = $subdir . '/' . $reqfile;
  }
  if ((! file_exists($reqfilepath)) || (! is_file($reqfilepath))) {
    header($_SERVER['SERVER_PROTOCOL'] . ' 404 Not found');
    echo "404 NOT FOUND\n";
    exit(0);
  }
}

// check the slideshow flag
if ((! empty($_GET['sshow'])) && ($_GET['sshow'] != 0) && ($SLIDESHOW_DELAY) > 0) {
  $SLIDESHOW_CURSTATE = TRUE;
} else {
  $SLIDESHOW_CURSTATE = FALSE;
}

// fetch the list of all files in the dir, as well as a list of all subdirs
if (empty($subdir)) {
  $curdir = getgalprop('.');
} else {
  $curdir = getgalprop($subdir);
}

// f= must refer to a photo/video known to this gallery (blocks qdconf.php, etc.)
if ((! empty($_GET['f'])) && (! isset($curdir['files'][$_GET['f']]))) {
  header($_SERVER['SERVER_PROTOCOL'] . ' 404 Not found');
  echo "404 NOT FOUND\n";
  exit(0);
}

// if explicit cache control configured, output it now
if ($CACHELIMIT >= 0) {
  header('Cache-Control: max-age=' . $CACHELIMIT . ", must-revalidate");
}

// output the basic html structure with headers and meta
echo "<!DOCTYPE html>\n";
echo '<html lang="' . enchtml($lang) . '">' . "\n";
echo "<head>\n";
echo "  <title>" . enchtml($ALBUMTITLE) . "</title>\n";
echo "  <meta charset=\"utf-8\">\n";
echo '  <meta name="generator" content="qdgal ' . PVER . "\">\n";
echo '  <meta name="viewport" content="width=device-width, initial-scale=1.0">' . "\n";
if (! empty($FAVICON)) {
  echo '  <link rel="icon" href="' . enchtml($FAVICON) . '">' . "\n";
}
echo "  <link rel=\"stylesheet\" type=\"text/css\" href=\"qdgal.css\">\n";
if (file_exists('qdgal-custom.css')) {
  echo "  <link rel=\"stylesheet\" type=\"text/css\" href=\"qdgal-custom.css\">\n";
}
if (($CUSTOM_CSS) && (! empty($subdir))) {
  $fullpath = '';
  foreach (explode('/', $subdir) as $s) {
    $fullpath .= $s . '/';
    if (file_exists($fullpath . 'qdgal.css')) {
      echo '  <link rel="stylesheet" type="text/css" href="' . enchtml($fullpath) . "qdgal.css\">\n";
    }
  }
}
echo "</head>\n";
echo "<body>\n";

// if no file is selected, compute the entire gallery
if (empty($_GET['f'])) {
  // is it a sub-gallery? then compute a link to upper levels
  if (! empty($subdir)) {
    echo '<p class="galbar">';
    echo '<a href="' . buildurl('', '') . '">' . enchtml($HOMEALBUM) . '</a>';
    $fullpath = '';
    foreach (explode('/', $subdir) as $dir) {
      if (!empty($fullpath)) $fullpath .= '/';
      $fullpath .= $dir;
      echo ' &gt; ';
      echo '<a href="' . buildurl($fullpath, '') . '">' . enchtml($dir) . '</a>';
    }
    echo "</p>\n";
  }
  // gallery title
  echo '<p class="albumtitle">' . enchtml($ALBUMTITLE) . "</p>\n";
  // if gallery is empty, just write a message
  if (empty($curdir['files']) && empty($curdir['dirs'])) {
    echo '<p class="emptygal">' . enchtml($STR_EMPTYGAL[$lang]) . "</p>\n";
  } else {
    // display sub-galleries
    echo '<div class="thumbgrid">' . "\n";
    foreach ($curdir['dirs'] as $i) {
      $fulldir = '';
      if (!empty($subdir)) $fulldir = $subdir . '/';
      $fulldir .= $i;
      // try fetching a cached thumbnail for directory, if available
      $cover = isthumbavailable($fulldir, 'dir');
      $covertype = 'img'; /* if anything found, then it must be an image */
      // if no dir thumb found, fetch subgallery properties and look for a cover
      if ($cover === false) {
        $galprop = getgalprop($subdirurl . $i);
        // use thumbnail if thumbnails are enabled (and create it if needed)
        if (($THUMBNAILS_HEIGHT > 0) && ($galprop['cover'])) {
          // default to original cover image
          $cover = $galprop['cover'];
          $covertype = $galprop['covertype'];
          // but use a thumbnail of this cover if possible/available
          $thumb = isthumbavailable($galprop['cover'], $galprop['covertype']);
          if ($thumb != false) {
            $cover = $thumb;
            $covertype = 'img';
          }
        }
      }
      //
      echo '<div class="thumb"><a href="' . buildurl($subdirurl . $i, '') . '">';
      if ($cover) {
        if ($covertype == 'img') {
          echo '<img src="' . enclink($cover) . '" alt="' . enchtml($i) . '" class="thumb">';
        } else { // vid
          echo '<video class="vidthumb" preload="metadata"><source src="' . enclink($cover) . '">' . "</video>";
        }
      } else {
        echo '<p class="fakethumb">' . enchtml($STR_EMPTYGAL[$lang]) . '</p>';
      }
      echo '<p class="thumblabel"><span>' . enchtml($i) . '</span></p></a></div>'. "\n";
    }
    // display photos
    foreach ($curdir['files'] as $i => $t) {
      $thumbfile = false;
      if ($THUMBNAILS_HEIGHT > 0) { // create and use thumbnail if thumbnails are enabled
        $thumbfile = isthumbavailable($subdirurl . $i, $t);
      }
      // fallback when no thumbnail available
      if ($thumbfile === false) {
        if ($t === 'img') {
          $thumbfile = $subdirurl . $i;
        } else {
          // video icon; .pn (not .png) so it is not matched as a gallery photo
          $thumbfile = 'movie.pn';
        }
      }
      $thumbalt = ($thumbfile === 'movie.pn') ? $STR_VIDFILE[$lang] : $i;
      $thumbitem = '<img src="' . enclink($thumbfile) . '" alt="' . enchtml($thumbalt) . '" class="thumb">';
      echo '<div class="thumb"><a href="' . buildurl($subdir, $i) . '">' . $thumbitem . '</a></div>'. "\n";
    }
    echo "</div>\n"; // thumbgrid end
    // save the gallery's thumbnail (if avail)
    if (!empty($curdir['cover'])) {
      $covthumb = isthumbavailable($curdir['cover'], $curdir['covertype']);
      if (!empty($covthumb)) {
        $dirthumb = $CACHEFOLDER . '/' . md5($subdir);
        $galpath = empty($subdir) ? '.' : $subdir;
        copy($covthumb, $dirthumb);
        // match directory mtime so parent listings can detect stale covers
        clearstatcache(false, $galpath);
        $galmtime = filemtime($galpath);
        if ($galmtime) touch($dirthumb, $galmtime);
      }
    }
    // output the 'powered by' footer (or custom footer if defined)
    if (file_exists('footer.html')) {
      readfile('footer.html');
    } else {
      echo '<p class="poweredby"><a href="https://qdgal.sourceforge.net/">powered by qdgal</a></p>' . "\n";
    }
  }
} else { // else compute a page for a single image
  $i = $_GET['f']; // image name
  $itxt = $subdirurl . pathinfo($i, PATHINFO_FILENAME) . '.txt';
  $desc = FALSE;
  if (file_exists($itxt)) {
    $desc = file_get_contents($itxt); // image description
  }

  $first = FALSE;
  $prev = FALSE;
  $next = FALSE;
  $curitem = FALSE;

  // search the files array for our file, and check whether a next/previous record exists
  foreach ($curdir['files'] as $f => $type) {
    if ($first === FALSE) $first = $f;
    if ($f === $i) {
      $curitem = $f;
      $curitem_type = $type;
      continue;
    }
    if ($curitem === FALSE) {
      $prev = $f;
    } else {
      $next = $f;
      break;
    }
  }

  if ($next != FALSE) {
    $next_sshow = $next;
  } else {
    $next_sshow = $first;
  }

  echo '<script type="text/javascript">' . "\n";
  echo "function startcountdown() {\n";
  if ($SLIDESHOW_CURSTATE == TRUE) {
    // pass the function reference — onloadtimer(), would run immediately
    echo '  setTimeout(onloadtimer, ' . $SLIDESHOW_DELAY * 1000 . ");\n";
  }
  echo "}\n";
  echo '</script>' . "\n";

  if ($curitem_type === 'vid') { // it's a video
    $vidurl = enclink($subdirurl . $i);
    $poster = isthumbavailable($subdirurl . $i, 'vid');
    if ($poster !== false) $poster = ' poster="' . enclink($poster) . '"';
    else $poster = '';
    echo '<video controls' . $poster . ' preload="auto" src="' . $vidurl . '" class="big">ERROR: NO VIDEO SUPPORT. <a href="' . $vidurl . '">DOWNLOAD THIS VIDEO</a>.</video>' . "\n";
  } else { // it's a photo
    echo '<img alt="image" class="big" onload="startcountdown()" src="' . enclink($subdirurl . $i) . '">' . "\n"; // photo
  }

  echo '<div class="left">' . "\n";
  echo '<p class="actions"><a href="' . buildurl($subdir, '') . '">' . $ACTION_PREFIX . enchtml($STR_BACKTOALBUM[$lang]) . "</a></p>\n";
  if ($prev != FALSE) {
    echo '<p class="actions">';
    echo '<a href="' . buildurl($subdir, $prev, $SLIDESHOW_CURSTATE) . '">' . $ACTION_PREFIX . enchtml($STR_PREVPHOTO[$lang]) . "</a>";
  } else {
    echo '<p class="actionsinact">';
    echo $ACTION_PREFIX . enchtml($STR_PREVPHOTO[$lang]);
  }
  echo "</p>\n";
  if ($next != FALSE) {
    echo '<p class="actions">';
    echo '<a href="' . buildurl($subdir, $next, $SLIDESHOW_CURSTATE) . '">' . $ACTION_PREFIX . enchtml($STR_NEXTPHOTO[$lang]) . "</a>";
  } else {
    echo '<p class="actionsinact">';
    echo $ACTION_PREFIX . enchtml($STR_NEXTPHOTO[$lang]);
  }
  echo "</p>\n";
  echo '<p class="actions">' . "\n";
  echo '  <script type="text/javascript">' . "\n";
  if ($SLIDESHOW_DELAY > 0) {
    // the slideshow link is output via javascript for consistency, so if no
    // JS is present on the browser, then the link won't be proposed at all.
    if ($SLIDESHOW_CURSTATE == TRUE) {
      echo '    function onloadtimer() {' . "\n";
      echo '      window.location.replace("' . buildurl($subdir, $next_sshow, TRUE) . '");' . "\n";
      echo "    }\n";
      echo '    document.write(\'<a href="' . buildurl($subdir, $curitem) . '">' . $ACTION_PREFIX . enchtml($STR_SLIDESHOW_STOP[$lang]) . "</a>');\n";
      if ($curitem_type === 'vid') {
        echo "    startcountdown();\n";
      }
    } else {
      echo '    function onloadtimer() { }' . "\n";
      echo '    document.write(\'<a href="' . buildurl($subdir, $curitem, TRUE) . '">' . $ACTION_PREFIX . enchtml($STR_SLIDESHOW_START[$lang]) . "</a>');\n";
    }
  } else {
    echo '    function onloadtimer() { }' . "\n";
  }
  echo "  </script>\n";
  echo "</p>\n";
  if ($desc != FALSE) {
    $desc = trim($desc);
    echo '<p class="descr">' . htmlspecialchars($desc) . "</p>\n";
  }
  echo "</div>\n";

}

// terminate
echo "</body>\n";
echo "</html>\n";

?>
