<?php

// ---- Apache configuration ----
//
// The following RewriteRules are necessary in the album's parent directory's
// .htaccess file:
//
// RewriteEngine On
// RewriteRule ^album/-(image|thumb)/(.+)/([^/]+)$ album/?album=$2&$1=$3 [B,L]
// RewriteRule ^album/-(.+)$ album/?album=$1 [L]

// ---- Constants ----

$ALBUM_ROOT = '/album';
$IMAGE_EXT = array('.png', '.jpg', '.jpeg', '.gif');	// Image extensions
$OTHER_EXT = array(				// Other media extensions
  '.mp3', '.mpg', '.mp4', '.mov', '.avi',
  '.pdf', '.phps', '.html', '.txt',
  '.kml', '.kmz', '.gpx',
  '.svg',
  '.gz', '.zip', '.bz2'
);

// ---- Utility functions ----

// get a GET parameter and URL-decode it if present
function get($var) {
  return empty($_GET[$var]) ? '' : urldecode($_GET[$var]);
}

// Force a new extension on a file name
function force_fileext($file, $newext) {
  $basename = preg_replace('/\\.[^.]*$/', '', $file);
  return $basename . '.' . $newext;
}

// Remove parent directory references from a file name
function sanitize($file) {
  $file = trim($file, '/');
  return str_replace('../', '', $file);
}

// Check if a file matches any extension from an array
function match_fileext($file, &$exts) {
  foreach ($exts as $ext)
    if (preg_match('/' . preg_quote($ext) . '$/i', $file))
      return true;
  return false;
}

// Check if a string consists of only printable ASCII characters
function is_ascii($s) {
  return is_string($s) && !preg_match('/[^\x09\x0a\x0d -~]/', $s);
}

// Check if a value can be printed
function is_print($s) {
  return is_numeric($s) || is_ascii($s);
}

// Sort strings with dates in them before strings without dates and
// in descending order
function date_sort($a, $b) {
  $a_date = preg_match('(20\d\d(-\d\d(-\d\d)?)?)', $a, $a_match);
  $b_date = preg_match('(20\d\d(-\d\d(-\d\d)?)?)', $b, $b_match);
  if ($a_date != 0 && $b_date != 0)
    // Dates in both strings; compare reverse dates
    // If the dates are equal: compare complete strings
    return (($cmp = strcmp($b_match[0], $a_match[0])) == 0) ? strcmp($a, $b) : $cmp;
  elseif ($a_date == 0 && $b_date == 0)
    // No dates; compare strings
    return strcmp($a, $b);
  else
    // Sort the dated string before the undated one
    return ($a_date == 0) ? 1 : -1;
}

// url-encode a string, but leave slashes alone
function urlencode2($url) {
  $url = urlencode($url);
  $url = str_replace('%2F', '/', $url);
  return $url;
}

// Return the last album component
function last_album($album) {
  $parts = explode('/', $album);
  $last = $parts[sizeof($parts) - 1];
  if (preg_match('/(.*) 20\d\d(-\d\d(-\d\d)?)?/', $last, $match))
    // remove the date
    $last = $match[1];
  return $last;
}

// Return a link to an image, either as an external or
// an EXIF thumbnail, or as the file name if neither is available
function thumbnail($album, $image, &$alt) {
  global $ALBUM_ROOT;
  $alt = "Vorschau auf $image in " . last_album($album);
  if (is_readable("$album/tn_$image"))
    return "$ALBUM_ROOT/$album/tn_$image";
  elseif (
    @exif_thumbnail("$album/$image", $x, $y, $type) !== false ||
    make_thumb($album, $image)
  )
    return "$ALBUM_ROOT/-thumb/$album/$image";
  else
    return "$ALBUM_ROOT/.nothumb.jpg";
}

function make_thumb($album, $image) {
  global $ALBUM_ROOT;
  $dir = "${_SERVER['DOCUMENT_ROOT']}$ALBUM_ROOT/$album";
  // mkthumb is ran as user bb, so no writeable check needed
  $cmd = "sudo -u bb /home/bb/bin/mkthumb " . escapeshellarg("$dir/$image") . " 2>&1";
  exec($cmd, $output, $exit_code);
  if ($exit_code != 0) {
    echo "<!-- make_thumb(): $cmd returns $exit_code:\n\n", htmlentities(implode("\n", $output)), "\n-->\n";
    return False;
  }
  return True;
}

// Return a thumbnail img tag
function image_thumb($album, $image) {
  $alt = '';
  $thumb = thumbnail($album, $image, $alt);
  return thumb_img($thumb, $alt);
}

// Return a linked thumbnail image
function image_thumb_link($album, $image, $class="") {
  $img = image_thumb($album, $image);
  return sprintf("<div class=\"float\">%s</div>",
    image_link($img, $album, $image, $class)
  );
}

// Return a linked thumbnail image with text
function image_thumb_link_text($text, $album, $image, $size, $class="") {
  $img = image_thumb($album, $image);
  return sprintf("<div class=\"float\">%s\n<p>%s%s</p></div>",
    image_link($img, $album, $image, $class), image_text_link($text, $album, $image),
    $size ? ", $size" : ""
  );
}

// Return a text link to an image
function image_text_link($text, $album, $image, $attrs="") {
  return image_link($text, $album, $image, $attrs);
}

// Return a link to an image
function image_link($text, $album, $image, $attrs="") {
  global $ALBUM_ROOT;
  return sprintf("<a%s href=\"$ALBUM_ROOT/-image/%s/%s\">%s</a>",
    $attrs ? " $attrs" : "", urlencode2($album), urlencode2($image), $text
  );
}

// Return a link to a other file, either as a preview called
// tn_<file>.jpg, or as the file name if no preview is available
function other_thumb_link_text($text, $album, $other, $size, $class="") {
  global $ALBUM_ROOT;

  $alt = "Vorschau auf $other in " . last_album($album);
  $thumb = "$album/tn_" . force_fileext($other, 'jpg');
  if (is_readable($thumb))
    $img = other_link(thumb_img($thumb, $alt), $album, $other, $class);
  else
    $img = '';

  if (preg_match('/\\.km[lz]$/', $other)) {
    $url = rawurlencode("http://{$_SERVER['SERVER_NAME']}$ALBUM_ROOT/$album/$other");
    // Due to a decoding quirk at Google, encoded spaces must be encoded again?!?
    $url = str_replace('%20', '%2520', $url);
    $map = " (<a href='http://maps.google.com/maps?q=$url'>Karte</a>)\n";
  }
  else
    $map = '';

  return sprintf("<div class=\"float\">%s\n<p>%s%s%s</p></div>",
    $img, other_text_link($other, $album, $other), $size ? ", $size" : '', $map
  );
}

// Return a text link to an image
function other_text_link($text, $album, $image, $class="") {
  return other_link($text, $album, $image, $class);
}

// Return a link to a "other" file
function other_link($text, $album, $file, $class="") {
  global $ALBUM_ROOT;
  return sprintf("<a%s href=\"$ALBUM_ROOT/%s/%s\">%s</a>",
    $class ? " class=\"$class\"" : "", $album, $file, $text
  );
}

// Return an img tag for a thumbnail image
function thumb_img($file, $alt="", $x=0, $y=0) {
  //$file = urlencode2($file);
  $size = ($x && $y) ? " width=\"$x\" height=\"$y\"": "";
  return "<img class=\"imageThumb\" src=\"$file\"$size alt=\"$alt\" title=\"$alt\" />";
}

// Return an album link
function album_link($text, $album, $attrs="") {
  global $ALBUM_ROOT;
  return sprintf("<a%s href=\"$ALBUM_ROOT/-%s\">%s</a>",
    $attrs ? " $attrs" : "", urlencode2($album), $text
  );
}

// Provide a trail of all intermediate path elements as links
// to the respective subalbum
function breadcrumbs($path) {
  global $ALBUM_ROOT, $NAV_SEP_HIER;
  $parts = explode('/', $path);
  $submenu = new Menu($NAV_SEP_HIER);
  $submenu->addItem(new MenuItem('contents', "Album", "$ALBUM_ROOT/"));
  foreach ($parts as $i => $part)
    if ($part != '.') {
      if ($i < sizeof($parts) - 1) {	// Don't link to the leaf node
        $trail = ($i == 0) ? "$part" : "$trail/$part";
	$rel = ($i == sizeof($parts) - 2) ? 'index' : '';
        $submenu->addItem(new MenuItem($rel, $part, $ALBUM_ROOT . '/-' . urlencode2($trail)));
      }
      else
        $submenu->addItem(new MenuItem('', $part, ''));
    }
  return $submenu;
}

// Return all images and subalbums in an album
function image_dir($album, &$subalbums, &$images, &$others) {
  global $IMAGE_EXT, $OTHER_EXT;

  if (($dh = @opendir($album)) !== false) {
    while (($file = readdir($dh)) !== false) {
      if (substr($file, 0, 1) == '.' || substr($file, 0, 3) == 'tn_')
        continue;		// Ignore dot-files and thumbnails
      $path = "$album/$file";
      if (!is_readable($path))
        continue;		// Ignore unreadable entries
      if (is_dir($path))
        $subalbums[] = $file;	// Add directories as subalbums
      elseif (match_fileext($file, $IMAGE_EXT))
        $images[] = $file;	// Add picture extensions as images
      elseif (match_fileext($file, $OTHER_EXT))
        $others[] = $file;	// Add media extensions as others
    }
    closedir($dh);
  }

  usort($subalbums, 'date_sort');
  sort($images);
  sort($others);
}

// Return a human-readable file size
function filesize_human($size) {
  if ($size >= 10e6) {
    $size = (int)($size / 1e6);
    return "$size&#160;MB";
  }
  elseif ($size >= 10e3) {
    $size = (int)($size / 1e3);
    return "$size&#160;kB";
  }
  else
    return "$size&#160;Bytes";
}

// ---- Image functions ----

// Read an info file. Info files are structured like a RFC822 mail
// message; that is, a header of "Name: value" lines, followed by
// an empty line, followed by arbitrary text. The name of a value is
// always converted to lower case.
//
// The currently recognized header fields are:
// - Title: The image's title, most often a short description
// - Subtitle: A subtitle, tagline, etc.
// - Alt: An alternative text displayed when the user has turned off
//   images in his browser
// - Descr: The text following the header lines. You can use all
//   available HTML formatting in this section; if will be output
//   inside a <div> tag with class "imageDescr" or "albumDescr".
// - Width: The image width
// - Height: The image height
// - EXIF: Setting this to any non-empty string outputs EXIF information
//
// The header lines and text are passed back to the calling function in the
// $info associative array.
//
// The image width and height are read from the file itself and also
// passed back as if they had been specified as "width" and "height"
// header fields. It is in fact possible to override an image's
// width and/or height by specifying those two fields in the info file.
function read_info($infofile, &$info) {
  // Read the info file
  $in_header = 1;
  $descr = '';
  if (is_file($infofile) && is_readable($infofile))
    foreach (file($infofile) as $line)
      if ($in_header == 1)
        if (!trim($line))
	  $in_header = 0;
	elseif ($line[0] == '<') {
	  $in_header = 0;
	  $descr = $line;
	}
	else {
	  $parts = explode(':', $line, 2);
	  $info[strtolower(trim($parts[0]))] = trim($parts[1]);
        }
      else
        $descr .= $line;
  if (!empty($descr))
    $info['descr'] = $descr;
}

function image_info($image, &$info) {
  if (!is_file($image) || !is_readable($image))
    return;

  // Determine the image size
  $imagesize = getimagesize($image);
  $info['width'] = $imagesize[0];
  $info['height'] = $imagesize[1];
  $info['descr'] = '';

  // Read the associated info file
  read_info(force_fileext($image, 'info'), $info);

  // Set the combined size attribute
  $info['size'] = "width=\"{$info['width']}\" height=\"{$info['height']}\"";
}

require_once 'php/exifgps.inc.php';

// Display an image's raw EXIF info
function image_exif_raw($file) {
  if (($exif = @exif_read_data($file, 0, true)) !== false) {
    echo "\n<h3 class=\"imageEXIFHeader\" id=\"exif\">EXIF-Informationen</h3>\n";
    foreach ($exif as $key => $section) {
      echo "<p class=\"imageEXIFSection\">";
      foreach ($section as $name => $val)
        if (is_print($val))
          echo "$key.$name: $val<br />\n";
        else if (is_array($val)) {
          echo "$key.$name: ( ";
          foreach ($val as $i => $v)
            echo is_print($v) ? $v : '?', ' ';
          echo ")<br />\n";
        }
        else
          echo "$key.$name: &lt;" . base64_encode($val) . "><br />\n";
      echo "</p>\n";
    }
    echo "<div class=\"clear spacer\"></div>\n\n";
  }
}

// Display an image's selected EXIF info
function image_exif_cooked($file) {
  global $ALBUM_ROOT;
  if (
    ($exif = @exif_read_data($file, 0, true)) !== false &&
    (isset($exif['IFD0']) || isset($exif['EXIF']))
  ) {
    echo "<h3 class=\"imageEXIFHeader\">Bildinformationen</h3>\n<p>\n";
    if ($exif['IFD0']['Make'])
      echo "Kamera: {$exif['IFD0']['Make']} {$exif['IFD0']['Model']}<br />\n";
    if ($exif['EXIF']['DateTimeOriginal'])
      echo "Datum: {$exif['EXIF']['DateTimeOriginal']}<br />\n";
    if ($exif['FILE']['FileSize'])
      echo "Grösse: ", filesize_human($exif['FILE']['FileSize']), "<br />\n";
    if ($exif['COMPUTED']['ApertureFNumber']) {
      $aperture = str_replace("f", "ƒ", $exif['COMPUTED']['ApertureFNumber']);
      echo "Blende: {$aperture}<br />\n";
    }
    if ($exif['EXIF']['ExposureTime']) {
      $t = $exif['EXIF']['ExposureTime'];
      eval("\$n = $t;");
      if ($n > 1)
        $t = $n;
      elseif ($n > 0)
        $t = '1/' . intval(1 / $n);
      echo "Verschlusszeit: $t s<br />\n";
    }
    if (isset($exif['EXIF']['FocalLengthIn35mmFilm']))
      echo "Brennweite: {$exif['EXIF']['FocalLengthIn35mmFilm']} mm<br />\n";
    else if (isset($exif['EXIF']['FocalLength'])) {
      $t = $exif['EXIF']['FocalLength'];
      eval("\$n = $t;");
      echo "Brennweite: {$n} mm<br />\n";
    }
    if (isset($exif['EXIF']['ISOSpeedRatings']))
      echo "ISO: {$exif['EXIF']['ISOSpeedRatings']}<br />\n";
    if (isset($exif['EXIF']['ExposureBiasValue'])) {
      $t = $exif['EXIF']['ExposureBiasValue'];
      eval("\$n = $t;");
      if ($n != 0) {
        $sign = $n > 0 ? '+' : '';
        echo "Belichtungskorrektur: $sign$n<br />\n";
      }
    }
    if (isset($exif['GPS']) && isset($exif['GPS']['GPSLatitude']) && isset($exif['GPS']['GPSLongitude'])) {
      echo "Ort: ", exif_to_html($exif, true),
        " (<a href=\"/php/exifmap.php?image=$ALBUM_ROOT/$file\">Karte…</a>)<br />\n";
      echo "<a href=\"{$_SERVER['REQUEST_URI']}&amp;exif=1#exif\">Mehr…</a>\n";
    }
    else {
      echo "<a href=\"{$_SERVER['REQUEST_URI']}&amp;exif=1#exif\">Mehr…</a></p>\n";
      echo "<p>Auf Karte <a href=\"/php/exifmap.php?image=$ALBUM_ROOT/$file\">positionieren…</a>\n";
    }
    echo "</p>\n<div class=\"clear\"></div>\n\n";
  }
}

// Return the Prev/Next links of an image
function image_nav(&$images, $album, $image) {

  // Search the current image in the album
  if (sizeof($images) > 1 && ($key = array_search($image, $images)) !== false) {
    // If found, output Previous and Next image links
    if ($key > 0)
      $prev_link = image_link("Zurück", $album, $images[$key - 1], 'accesskey="p" rel="prev"');
    else
      $prev_link = image_link("Ende", $album, $images[sizeof($images) - 1], 'accesskey="p"');
    if ($key + 1 < sizeof($images))
      $next_link = image_link("Weiter", $album, $images[$key + 1], 'accesskey="n" rel="next"');
    else
      $next_link = image_link("Anfang", $album, $images[0], 'accesskey="n" rel="start"');
    return sprintf("%s\n<strong>«</strong> %d von %d <strong>»</strong>\n%s",
      $prev_link, $key + 1, sizeof($images), $next_link
    );
  }
  return '';
}

// Display an image
function album_image($file, $info) {

  global $ALBUM_ROOT;

  if (!empty($info['subtitle']))
    echo "<h3 class=\"imageSubtitle\">{$info['subtitle']}</h3>\n";

  echo "<div class=\"image\">
<img class=\"bordered image\" src=\"$ALBUM_ROOT/$file\" alt=\"{$info['alt']}\" />
</div>\n";
  if (!empty($info['descr']))
    echo "<div class=imageDescr>\n{$info['descr']}</div>\n";
  echo "<div class=\"clear\"></div>\n\n";

  if (!empty($info['exif']))
    image_exif_raw($file);
  else if (empty($info['noexif']))
    image_exif_cooked($file);
}

// ---- Album functions ----

// Print a list of image links and sizes
function album_thumbs($album, &$subalbums, &$images, &$others) {
  global $IMAGE_EXT, $OTHER_EXT;

  // First, display album info
  $info = array();
  read_info("$album/.info", $info);
  if (!empty($info['title']))
    echo "\n<h2>{$info['title']}</h2>\n";
  if (!empty($info['subtitle']))
    echo "\n<h3>{$info['subtitle']}</h3>\n";
  if (!empty($info['descr']))
    echo "\n<div class=\"albumDescr\">{$info['descr']}</div>\n";

  // Next, list the subalbums
  if (sizeof($subalbums) > 0) {
    echo "\n<h2>Albumauswahl</h2>\n\n<ol>\n";
    foreach ($subalbums as $i => $file) {
      $link = ($album == '.') ? $file : "$album/$file";
      if (album_allowed($link)) {
        if (++$i == 10) $i = 0;	// assign access keys 1..9, 0 for items 1-10
        $attrs = 'class="album"' . (($i <= 9) ? " accesskey=\"$i\"" : '');
        echo "<li>" . album_link($file, $link, $attrs) . "</li>\n";
      }
    }
    echo "</ol>\n";
  }

  // Then list the images as thumbnails
  $total = $n = 0;
  if (sizeof($images) > 0) {
    echo "\n<h2 class=\"clear spacer\">Bilder</h2>\n\n";
    foreach ($images as $file) {
      $size = filesize("$album/$file");
      $total += $size;
      $n++;
      echo image_thumb_link_text($file, $album, $file, filesize_human($size), "class=\"image\""), "\n";
    }
  }

  // Finally list other media
  if (sizeof($others) > 0 && $album != '.') {
    echo "\n<h2 class=\"clear spacer\">Andere Dateien</h2>\n\n";
    foreach ($others as $file) {
      $size = filesize("$album/$file");
      $total += $size;
      $n++;
      echo other_thumb_link_text($file, $album, $file, filesize_human($size), "class=\"image\""), "\n";
    }
  }

  // Display statistics
  if (sizeof($subalbums) == 0 && $n == 0)
    echo "\n<p>Dieses Album ist noch leer.</p>\n\n";
  elseif ($n > 0) {
    $total = filesize_human($total);
    echo "\n<p class=\"clear spacer\">$n Dateien in $total</p>\n\n";
  }
  else
    echo "\n";
}

// Security check
function album_allowed($album) {
  $srv = $_SERVER['REMOTE_ADDR'];
  if ($album == 'test' && 
    !preg_match('/^192\.168\.11\./', $srv) &&
    !preg_match('/^127\.0\.0\.1$/', $srv) &&
    $srv != '213.3.3.203'
  )
    return false;
  return true;
}

// ---- Random images ----

// Return a random album and image name
function random(&$album, &$image) {
  // rebuild the image list with:
  // find \( -name \*.jpg -o -name \*.png -o -name \*.gif \) -a ! -name tn_\* >imglist.txt
  $files = explode("\n", file_get_contents('.imglist.txt'));
  $image = $files[array_rand($files)];
  $album = substr($image, 2, strrpos($image, '/') - 2);
  $image = substr($image, strlen($album) + 3);
}

function random_image(&$album, &$image) {
  $album = '.';
  $image = '';
  random($album, $image);
}

// Return a random image thumbnail
function random_thumb($top_album='.') {
  global $ALBUM_ROOT;
  $tries = 0;
  while (++$tries < 50) {
    $album = $top_album;
    $image = '';
    random($album, $image);
    $thumb = thumbnail($album, $image, $alt);
    if ($thumb != "$ALBUM_ROOT/.nothumb.jpg")
      return image_thumb_link($album, $image, "");
    // Restart if the image has no thumbnail
  }
  return '';
}

?>