#!/usr/bin/perl -U
## Salonify by Adam Rosi-Kessel Copyright 2002-2005
## Permission granted to modify and redistribute under the terms of the GPL v2.0 or later

# You should change this to reflect the name and location of your config file
$::config_file = "/etc/salonify";

use strict;

sub SafeRequire {
  my $mod = shift @_;
  unless (eval "require $mod") {
      die "Couldn't load required module $mod: $@\nPlease install this module and try again, or reconfigure salonify to disable this feature.\n";
  }
}

$::program_title = "salonify";
$::program_version = "0.82 (2005/09/15)";

$SIG{__DIE__} = \&FatalError;

use CGI qw(param :newstyle_urls cookie :standard :html4 start_ul -nosticky -no_xhtml); 
use POSIX;
use File::Basename;
use File::Glob qw(:globally :nocase);
use File::Copy;

use vars qw/%url_prefs %param/;
use vars qw/$salonify_default_thumbnails_columns $salonify_default_thumbnails_rows $salonify_default_show_thumbnail_captions $salonify_default_index_columns/;
use vars qw/$salonify_script_url $salonify_files_url $salonify_cascading_style_sheet_url $salonify_image_filename_extensions $salonify_top_level_url $salonify_files_url $salonify_use_rcs $salonify_collapsible_lists $salonify_start_collapsed $salonify_allow_user_to_edit_descriptions/;
use vars qw/$salonify_up_image $salonify_up_image_alt $salonify_down_image $salonify_down_image_alt $salonify_right_image $salonify_right_image_alt/;
use vars qw/$salonify_folders_file $salonify_names_filename $salonify_top_of_page_file $salonify_top_level_directory $salonify_alternate_index_file/;
use vars qw/$salonify_allow_user_to_change_layout $salonify_allow_user_to_change_captions/;
use vars qw/$salonify_verbose_url/;
use vars qw/$salonify_default_DirectoryNavigationTop $salonify_default_DirectoryNavigationBottom $salonify_default_SlideShowNavigationTop $salonify_default_SlideShowNavigationBottom $salonify_default_SlideShowButtonsTop $salonify_default_SlideShowButtonsBottom $salonify_default_SlideShowCaptionTop $salonify_default_SlideShowCaptionBottom $salonify_default_SlideShowSizeTop $salonify_default_SlideShowSizeBottom $salonify_default_SlideShowRotateTop $salonify_default_SlideShowRotateBottom $salonify_default_ShowPrefs $salonify_default_ShowCreditsTop $salonify_default_ShowCreditsBottom/;
use vars qw/$salonify_show_error_messages/;
use vars qw/$form_method/;
use vars qw/$url_folder/;
use vars qw/$random_string/;   # added onto URL if image is rotated--this is a bit of a hack, but I don't know any other way to do it.
use vars qw/$salonify_generate_static_archive_file $salonify_generate_dynamic_archive_file/;

my $query = new CGI;
my $rcs;
%param = $query->Vars;

foreach (keys %param) {
   if ( (length $_) > 1 and not /^w\d/ ) {
     while (s/^(.)//) {
        $param{$1} = 1;
     }
   }
}
 
&DefaultSettings;
if ($salonify_show_error_messages) {
   SafeRequire "CGI::Carp";
   CGI::Carp->import(qw(fatalsToBrowser));
}
&ProcessConfig;

if ($salonify_use_rcs) {
   SafeRequire "Rcs";
   Rcs->import(qw(nonFatal));
   if (-x '/usr/bin/co') {
      Rcs->bindir('/usr/bin');
   } elsif (-x '/usr/local/bin/co') {
      Rcs->bindir('/usr/local/bin');
   } elsif (my $rcs_path = `which co`) {
      $rcs_path =~ s{^(.*)/.*$}{$1};
      Rcs->bindir($rcs_path);
   } else {
     die 'RCS not found in path. Set $salonify_use_rcs to 0 in the configuration, or install RCS.\n';
   }
}

my $packed_cookie;
my $url_keep_cookie = $param{'q'};
$url_keep_cookie and $packed_cookie = cookie ( -name => 'salonify', -value => ShowPrefsNoForm('smnodpbz'), -expires => '+1y');

print ($packed_cookie ? 
                        header( -cookie => $packed_cookie) 
                      : header()
      );

$form_method = ($salonify_verbose_url ? "POST" : "GET");

if ($salonify_allow_user_to_change_layout) {
  %url_prefs = &ProcessPrefs(keys %param);
  %url_prefs = &ProcessPrefs(CookiePrefs(cookie('salonify'))) unless (scalar (keys %url_prefs));
  %url_prefs = &ProcessPrefs(DefaultPrefs()) unless (scalar (keys %url_prefs));
} else {
  %url_prefs = &ProcessPrefs(DefaultPrefs());
}

if ($param{'s'}) {
   $random_string = "?s=" . &CleanParam('s');
} else {
   $random_string = "";
}

my $url_thumbs = &CleanParam('u') or &CleanParam('t');
my $url_index = &CleanParam('i');
my $url_download_album = &CleanParam('L');
my $url_edit_descriptions = &CleanParam('l');
$url_index = 1 if $url_edit_descriptions;
my $url_current = &CleanParam('x') || 0;
my $url_cols = &CleanParam('m') || 0;
my $url_rows = &CleanParam('n') || 0;
my $url_start_slideshow = &CleanParam('v');
$url_current = 0 if $url_start_slideshow and not $url_current;

$url_cols > 0 and $url_cols < 15 and $salonify_default_thumbnails_columns = $url_cols;
$url_rows > 0 and $url_rows < 15 and $salonify_default_thumbnails_rows = $url_rows;

# I'm not really happy with this logic, it's complicated right now
# What we're trying to do is figure out where we're coming to the thumbnails from
# If it's from the slideshow, and we were deep in, we want to back to the page of thumbnails
# that corresponds to where we were in the slideshow
# But if we're coming from another directory entirely, we want to go to the first page
# Probably needs more testing
my $url_current_thumb = defined &CleanParam('t') ? &CleanParam('t') : (&CleanParam('u') ne 2 ? ThumbNailPage($url_current) : 0); 

$url_folder = &CleanParam('y') || "";
my $url_size = &CleanParam('z') || "";
my $url_title = $param{'k'} || "";
my $url_rotate;
my $url_which_rotate = $param{'r'} || '';

$salonify_default_show_thumbnail_captions = defined $param{'o'};

($url_which_rotate =~ /Clockwise/) and $url_rotate = 1 or
($url_which_rotate =~ /Counterclockwise/) and $url_rotate = -1 or
$url_rotate = 0;
                      
my $size = ".me";

$url_size eq "3" and $size = ".li" or
$url_size eq "2" and $size = ".me" or
$url_size eq "1" and $size = "";

################################################################################

# Show fatal errors to the web user to make debugging easier
# (can be disabled by setting $salonify_show_error_messages = 0)
sub FatalError {
   print start_html() .
         h2('Salonify Fatal Error!') .
         p(join('</p><p>',@_)) .
         p('Contact the site administrator to report this problem.') .
         end_html() if $salonify_show_error_messages;
   die @_;      
}

# These are all the default settings if nothing is specified in the config file
sub DefaultSettings {
  $salonify_script_url = "/cgi-bin/salonify";
  $salonify_folders_file = "/photo/folders.txt";
  $salonify_names_filename = "names.txt";
  $salonify_top_of_page_file = "/photo/index.txt";
  $salonify_top_level_directory = "/var/www/photo";
  $salonify_top_level_url = "/photo";
  $salonify_cascading_style_sheet_url = "style.css";
  $salonify_image_filename_extensions = "jpg gif";
  $salonify_collapsible_lists = 1;
  $salonify_start_collapsed = 1;
  $salonify_files_url = "/photo";
  $salonify_allow_user_to_edit_descriptions = 1;
  $salonify_use_rcs = 1;                                
  $salonify_verbose_url = 0;
  $salonify_allow_user_to_change_captions = 1;
  $salonify_allow_user_to_change_layout = 1;
  $salonify_default_thumbnails_columns = 4;
  $salonify_default_thumbnails_rows = 4;
  $salonify_default_show_thumbnail_captions = 0;
  $salonify_default_index_columns = 2;
  $salonify_default_DirectoryNavigationTop = 0;
  $salonify_default_DirectoryNavigationBottom = 1;
  $salonify_default_SlideShowNavigationTop = 0;
  $salonify_default_SlideShowNavigationBottom = 1;
  $salonify_default_SlideShowButtonsTop = 1;
  $salonify_default_SlideShowButtonsBottom = 0;
  $salonify_default_SlideShowCaptionTop = 0;
  $salonify_default_SlideShowCaptionBottom = 1;
  $salonify_default_SlideShowSizeTop = 0;
  $salonify_default_SlideShowSizeBottom = 1;
  $salonify_default_SlideShowRotateTop = 0;
  $salonify_default_SlideShowRotateBottom = 1;
  $salonify_default_ShowPrefs = 1;
  $salonify_default_ShowCreditsTop = 0;
  $salonify_default_ShowCreditsBottom = 1;
  $salonify_show_error_messages = 1;
  $salonify_generate_static_archive_file = 0;
  $salonify_generate_dynamic_archive_file = 1;
}

sub ProcessConfig {

# try to load config file and give user helpful debugging output if we can't
  die "Could not read config file $::config_file--check path." unless ( -e $::config_file);
  die "Could not read config file $::config_file--check permissions." unless ( -r $::config_file);
  die "Could not find or load config file--perhaps you deleted a semicolon at the end of a line or the number 1 is missing from the end of the coonfig file.\n" unless (do $::config_file);
  
# add trailing slashes unless there already is one on the path files
  foreach ($salonify_top_level_directory, $salonify_top_level_url, $salonify_files_url) {
    $_ .= '/' unless m{/$};
  }

# set up the outline arrow images if they are not already defined
  $salonify_up_image = $salonify_files_url . "up.png" unless defined $salonify_up_image;
  $salonify_up_image_alt = "+" unless defined $salonify_up_image_alt;
  $salonify_down_image = $salonify_files_url . "down.png" unless defined $salonify_down_image;
  $salonify_down_image_alt = "-" unless defined $salonify_down_image_alt;
  $salonify_right_image = $salonify_files_url . "right.png" unless defined $salonify_right_image;
  $salonify_right_image_alt = ">" unless defined $salonify_right_image_alt;

# set up the stylesheet URL unless it is fully defined in the config file
  unless ($salonify_cascading_style_sheet_url =~ m{/}) {
     $salonify_cascading_style_sheet_url = $salonify_files_url . $salonify_cascading_style_sheet_url;
  }

# the list of accepted filename extensions is turned into a regexp for future matching
  for ($salonify_image_filename_extensions) {
      s/^ *| *$//g;
      s/ /\$\|\\./g;
      s/$/\$/g;
      s/^/\\./g;
  }

}

sub SlideShowSetupJava { 
  my $index = shift;
  my $path = shift;
  my $titles = shift;
  my %titles = %$titles;
  $index > 0 or $index = '0';
  my $slideshow= <<EOF
  <script type="text/javascript">
  <!--

  var index = $index;
  var timerID = null;
  var timerOn = false;
  var speed = 5;

  function updateIndex(x) {
   index = parseInt(x);
   if (index > (photoalbum.length-1)) 
      index = 0;
   if (index < 0) index = photoalbum.length-1;
   document.photo.src = photoalbum[index];
EOF
.
   ($url_prefs{"SlideShowCaptionTop"} || $url_prefs{"SlideShowCaptionBottom"} ? "document.caption.k.value = title[index];\ndocument.caption.x.value = index;\n" : '') .
   ($url_prefs{"SlideShowNavigationTop"} || $url_prefs{"SlideShowNavigationBottom"} ? "document.nav.x.value = index;\n" : '') .
   ($url_prefs{"SlideShowSizeTop"} || $url_prefs{"SlideShowSizeBottom"} ? "document.size.x.value = index;\n" : '') .
   ($url_prefs{"SlideShowRotateTop"} || $url_prefs{"SlideShowRotateBottom"} ? "document.rotate.x.value = index;\n" : '')
. <<EOF
  }

  function getNext() {
   updateIndex(index+1);
   bfr = new Image;
   if (index < (photoalbum.length-1)) {
      bfr.src = photoalbum[index+1];
   }
  }

  function getPrev() {
   updateIndex(index-1);
  }

  function getFirst() {
   updateIndex(0);
  }

  function getLast() {
   updateIndex(photoalbum.length-1);
  }

  function slideShow() {
   timerOn = true;
   rotate();
  }

  function rotate() {
   getNext();
   timerID = setTimeout("rotate()", 1000 * speed); 
   if (index == photoalbum.length-1) {
      index = -1;
   }
  }

  function stopShow() {
   if (timerOn) {
      clearTimeout(timerID);
   }
   if (index == photoalbum.length) {
      index = (photoalbum.length-1);
   }
  }

  function setSpeed() {
   var oldspeed = speed;
   speed = prompt("Please enter a speed from 1 (fastest) to 10 (slowest) for the slide show:",oldspeed);
   if ((speed == "") || (speed == null)) {
      speed = oldspeed;
   }
  }

  function printSave() {
    curphoto = document.photo.src;
    for ( var i = curphoto.length; i > 0; i-- ) {
      if (curphoto.charAt(i) == '/')
         break;
    }
    path = curphoto.substring(0,i+1);
    curphoto = curphoto.substring(i+1);
    prefix = curphoto.substring(0,3);
    if (prefix == ".li" || prefix == ".me") {
      curphoto = curphoto.substring(3);
    }
    location.href = path + curphoto;
  }

  function permaLink(x) {
    location.href = x + ";x=" + index;
  }

  var photoalbum = [];
  var title = [];
EOF
;

  my $counter = 0;
  while (my $image = shift) {
    my $title = $titles{StripSize($image)};
    $image =~ s/'/\\'/g;
    $title =~ s/'/\\'/g;
    $slideshow .= "photoalbum[$counter] = '$path$image$random_string';\n";
    $slideshow .= "title[" . $counter++ . "] = '$title';\n";
  }
  $slideshow .= "// -->
  </script>
  ";
  return $slideshow;
}

sub SlideShowImages {
  my $current = shift or 0;
  my $path = shift || "";
  my $image = $_[$current] || "";
  my $imagename = StripSize($image);
  return 
      p(
        {-align=>'center'},
        img{
               -src => $path . $image . $random_string,
              -name => 'photo',
             -class => 'photo',
               -alt => $imagename
            }
       );
}

sub SlideShowCaption {
  return        
   start_form(-action=>$salonify_script_url,-method=>'GET',-name=>'caption'),
     p({-align=>'center'}, 
     textfield(-name=>'k',
               -default=>shift,
               -size=>60,
               -maxlength=>60) .
     ShowPrefsForm('sxyzmnopdb') .
     submit(-value=>'Change')),
     end_form;
}

# StripSize: Removes the leading /.li, /.me, /.tn or just plain /
# This is needed because files are hashed by 'full sized' filename, not the filename for reduced versions
sub StripSize {
  $_ = shift;
  s{/\.li|/\.me|/\.tn|/}{};
  return $_;
}

# SlideShowButtonsJava: Returns the JavaScript used to paint the buttons to move around in the slideshow
# This all has to be done with document.write so it doesn't show up when a non-JavaScript browser is looking
# at the page.
sub SlideShowButtonsJava {
  my $thumbs_url = $salonify_script_url . "?u" . ShowPrefsNoForm('sxyzmnopdb');
  my $index_url = $salonify_script_url . "?i" . ShowPrefsNoForm('syzmnopdb');
  my $perma_url = $salonify_script_url . "?" . ShowPrefsNoForm('syzmnopdb');
  return <<EOF
<script language="JavaScript" type="text/javascript">
<!-- Hide script
document.write('<center>');
document.write('<form action="$salonify_script_url" name="buttons">');
document.write('<input type="button" value="First" onclick="getFirst()" >');
document.write('<input type="button" value="Previous" onclick="getPrev()" >');
document.write('<input type="button" value="Next" onclick="getNext()" >');
document.write('<input type="button" value="Last" onclick="getLast()" >');
document.write('<input type="button" value="Slide Show" onclick="slideShow()" >');
document.write('<input type="button" value="Stop Show" onclick="stopShow()" >');
document.write('<input type="button" value="Set Speed" onclick="setSpeed()" >');
document.write('<input type="button" value="Thumbnails" onclick="location.href=\\'$thumbs_url\\'">');
document.write('<input type="button" value="Index" onclick="location.href=\\'$index_url\\'">');
document.write('<input type="button" value="Print/Save" onclick=\\"printSave()\\">');
document.write('<input type="button" value="Permalink" onclick=\\"permaLink(\\'$perma_url\\')\\">');
document.write('<\\/form>');
document.write('<\\/center>');
// End script hiding -->
</script>
EOF
}

# SlideShowButtonsNoJava: Returns the links for page for non-JavaScript Browser
sub SlideShowButtonsNoJava {
  my $current = shift || 0;
  my $path = shift || "";
  my @images = @_;
  my $big_photo = $images[$current] || "";
  $big_photo =~ s/^\.li|\.me//g;
  $big_photo = $path . $big_photo;
  my $number_of_photos = scalar @images;
  my $first = "$salonify_script_url?x=0" . ShowPrefsNoForm('syzmnopdb');
  my $previous = "$salonify_script_url?x=" . ( (($current-1) >= 0) ? ($current - 1) : ($number_of_photos - 1)) . ShowPrefsNoForm('syzmnopdb'); 
  my $next = "$salonify_script_url?x=" . ( (($current+1) < $number_of_photos) ? ($current + 1) : ("0") ) . ShowPrefsNoForm('syzmnopdb');
  my $last = "$salonify_script_url?x=" . ($number_of_photos-1) . ShowPrefsNoForm('syzmnopdb');
  my $thumbs = "$salonify_script_url?u" . ShowPrefsNoForm('sxyzmnopdb');
  my $index = "$salonify_script_url?i" . ShowPrefsNoForm('szmnopdb');
  my $printsave = $big_photo;
  "<noscript>"
  .
  table(
        { -class => 'navigation' },
        Tr( { -class => 'navigation' },
           td( { -class => 'navigation' }, a( { -href => $first }, "First" ) ),
           td( { -class => 'navigation' }, a( { -href => $previous }, "Previous" ) ),
           td( { -class => 'navigation' }, a( { -href => $next }, "Next" ) ),
           td( { -class => 'navigation' }, a( { -href => $last }, "Last" ) ),
           td( { -class => 'navigation' }, a( { -href => $thumbs }, "Thumbnails" ) ),
           td( { -class => 'navigation' }, a( { -href => $index }, "Index" ) ),
           td( { -class => 'navigation' }, a( { -href => $printsave }, "Print/Save" ) )
        )
  )
  .
  "</noscript>"
}

# ShowPrefsNoForm: This ensures that settings and location are preserved from one click to the next
# Give it a bunch of CGI parameters, and it returns a string to be appended to the URL
# e.g., ShowPrefsNoForm('sabc') returns ;a=1;b=2;c=3 (if a, b, and c, equal 1, 2, and 3 respectively)
sub ShowPrefsNoForm {
   my $params = shift;
   my $string = "";
   while ($params =~ s/^(.)//) {
     my $val = $1;
     if ($1 eq 'p') {
       $string .= ';';
       foreach my $x ('A','a','B','b','C','c','D','d','E','e','F','f','H','h','J','j') {
         $string .= $x if $param{$x};
       }
     } else {
       if ($val eq 'x' and not $param{$val}) {
          $string .= ";x=0" if $val eq 'x' and not $param{$val};
       } else {
          $string .= ";" . $val if defined $param{$val};
          $string .= "=" . &CleanParam($val) if $param{$val};
       }
     }
   }
   return $string;
}

# ShowPrefsForm: Save as above, but this returns hidden fields for submit button controls
# Inserts newlines between fields for better readability in the resulting HTML
sub ShowPrefsForm {
   my $params = shift;
   my $string = "";
   while ($params =~ s/^(.)//) {
     my $val = $1;
     if ($val eq 'p') {
       my $hidden_value .= 'p';
       foreach my $x ('A','a','B','b','C','c','D','d','E','e','F','f','H','h','j','J') {
         $hidden_value .= $x if $param{$x};
       }
       $string .= hidden( -name => $hidden_value );
     } else {
       $string .= hidden(
                           -name => $val,
                          -value => &CleanParam($val)
                  ) if defined $param{$val};
     }
   }
   return $string;
}

sub SlideShowNavigation {
  my $current = (shift || 0) + 1;
  my $path = shift || "";
  my $titles = shift || "";
  my %titles = %$titles;
  my $max_pages = scalar @_ - 1;
  my @images = @_;
  my (@values, %labels);
  my $counter = 0;
  while ($counter <= $max_pages) { 
     push @values, $counter;
     $labels{$counter} = ($counter+1) . " <" . $titles{StripSize($images[$counter])} . ">";
     $counter++;
  }

# These buttons have been put in a centered table to compensate for a Netscape 4.7 bug, which apparently inserts
# a linebreak before any popup menu. Even if a break does appear here, it won't look as messed up. Hopefully
# this will still look fine in other browsers as well. This same problem occurs with all of the various
# buttons which are supposed to be centered, so you'll see this table code repeated.

  return start_form(-action=>$salonify_script_url,-method=>'GET',-name=>'nav'),
         table({ -border => 0, -width => "70%", -align=> 'center' },
         Tr({-align=>"CENTER",-valign=>"TOP"} , 
         td(
         ({-align=>'CENTER'}, "Image " . 
                      popup_menu(-name=>'x',-values=>\@values,-labels=>\%labels,-onchange=>'updateIndex(document.nav.x.value);') 
                                                             . " of " . ($max_pages+1) . ' ' .
         ShowPrefsForm('syzmnopdb') .
         submit(-value=>'Go'))
         ))) .
         end_form;
}

# RotateImage: Uses jpegtran to rotate the image; puts the result in a temporary file and then
# copies it over the original. This function may be somewhat dangerous; perhaps we want to make
# some backup of the original image?  Although it seems fairly innocuous at this point.
# For consistency, it will rotate the small, medium, large, and thumbnail version of the image
sub RotateImage {
  my $direction = shift;
  my $current = shift || 0;
  my $path = shift || "";
  my @images = @_;
  my $x = $path . '/' . StripSize($images[$current]);
  my $tmp = tmpnam();
  $direction == 1 and $direction = 90 or
                      $direction = 270;
  if (system("which jpegtran > /dev/null")) {
     print p("Sorry, JPEG rotation is not installed; you need libjpeg-progs.");
  } else {
    $tmp = tmpnam();
    $x =~ m/^(.*)$/;
    my $untaint = $1; 

    system("jpegtran -rotate $direction -outfile $tmp $x");
    ( -e $tmp and move($tmp,$untaint));

    $x =~ s{(.*)/(.*)}
             {$1/.li$2};
    $x =~ m/^(.*)$/;
    $untaint = $1; 
    system("jpegtran -rotate $direction -outfile $tmp $x");
    ( -e $tmp and move($tmp,$untaint));

    $x =~ s{(.*)/.li(.*)}
             {$1/.me$2};
    $x =~ m/^(.*)$/;
    $untaint = $1; 
    system("jpegtran -rotate $direction -outfile $tmp $x");
    ( -e $tmp and move($tmp,$untaint));

    $x =~ s{(.*)/.me(.*)}
             {$1/.tn$2};
    $x =~ m/^(.*)$/;
    $untaint = $1; 
    system("jpegtran -rotate $direction -outfile $tmp $x");
    ( -e $tmp and move($tmp,$untaint));

  }
  $random_string = "?s=" . &CleanParam('s');
  1;
}

sub ThumbNailButtons {
  my $current = shift || 0;
  my $max_pages = floor ( (scalar @_ -1) / ($salonify_default_thumbnails_columns * $salonify_default_thumbnails_rows) );
  my @nav_cols;
  push @nav_cols, 
                 td( { -class => 'navigation' },
                     a ( { href=> $salonify_script_url . "?u;t=0" . ShowPrefsNoForm('syzmnopdb') } , "First" )
                 ),
                 td( { -class => 'navigation' },
                     a ( { href=> $salonify_script_url . "?u;t=" . ($current > 0 ? $current - 1 : 0) . ShowPrefsNoForm('syzmnopdb') }, "Previous" )
                 ),
                 td( { -class => 'navigation' },
                     a ( { href=> $salonify_script_url . "?u;t=" . ($current < $max_pages ? $current + 1 : $max_pages) . ShowPrefsNoForm('syzmnopdb') }, "Next" )
                 ),
                 td( { -class => 'navigation' },
                     a ( { href=> $salonify_script_url . "?u;t=$max_pages" . ShowPrefsNoForm('syzmnopdb') }, "Last" )
                 )
             if $max_pages;   # if there is more than one page, put up page navigation buttons
  push @nav_cols, 
                 td( { -class => 'navigation' },
                     a ( { href=> $salonify_script_url . "?i" . ShowPrefsNoForm('szmnopdb') }, "Index" )
                 ),
                 td( { -class => 'navigation' },
                     a ( { href=> $salonify_script_url . "?v;x=0" . ShowPrefsNoForm('syzmnopdb') }, "Slideshow" )
                 );           # always include buttons for index and slideshow
  push @nav_cols, 
                 td( { -class => 'navigation' },
                     a ( { href=> $salonify_script_url . "?L" . ShowPrefsNoForm('styzmnopdb') }, "Download Album" )
                 )
              if $salonify_generate_dynamic_archive_file or $salonify_generate_static_archive_file;
  table( 
        { -class => 'navigation' },
        Tr( { -class => 'navigation' }, @nav_cols )
  );
}

sub ThumbNailPage {
  my $current_image = shift or 0;
  my $counter = 0;
  while ($current_image > $salonify_default_thumbnails_columns * $salonify_default_thumbnails_rows - 1) { $current_image -= $salonify_default_thumbnails_columns * $salonify_default_thumbnails_rows; $counter++;}
  return $counter;
}

sub ThumbNailNavigation {
  my $current = (shift || 0) + 1;
  my $max_pages = floor ( (scalar @_ -1) / ($salonify_default_thumbnails_columns * $salonify_default_thumbnails_rows) );
  my (@values, %labels);
  my $counter = 0;
  while ($counter <= $max_pages) { 
     push @values, $counter;
     $labels{$counter} = $counter+1;
     $counter++;
  }

  $max_pages and
  return start_form(-action=>$salonify_script_url,-method=>'GET'),
         table({ -border => 0, -width => "35%", -align=> 'center' },
         Tr({-align=>"CENTER",-valign=>"TOP"} , 
         td(
         ({-align=>'CENTER'}, "Page " . 
                     popup_menu(-name=>'t',-values=>\@values,-default=>$current-1,-labels=>\%labels,-onchange=>'submit()') 
                                                             . " of " . ($max_pages+1) .
         hidden(-name=>'u',-values=>1) . " " . 
         ShowPrefsForm('syzmnopdb') .
         submit(-value=>'Go'))))),
         end_form
  or "";
}

sub ThumbNailImages {
  my $current = shift || 0;
  my $path = shift || "";
  my $titles = shift;
  my %titles = %$titles;
  my @thumbnails = @_;
  my (@rows, @cols);
  my $row_counter = 0;
  my $col_counter = 0;
  my $title_width = floor (120/$salonify_default_thumbnails_columns);
     $title_width > 60 and $title_width=60;

  my $counter = $current * $salonify_default_thumbnails_columns * $salonify_default_thumbnails_rows;

  $counter > scalar @thumbnails and $counter = scalar @thumbnails;

  while ($row_counter++ < $salonify_default_thumbnails_rows and $thumbnails[$counter]) {
    while ($col_counter++ < $salonify_default_thumbnails_columns and $thumbnails[$counter]) {
      push (@cols, a( 
                      { -href => $salonify_script_url . "?x=$counter" . ShowPrefsNoForm('syzmnopdb') },
                      img{
                            -src => $path . $thumbnails[$counter] . $random_string,
                            -alt => $titles{StripSize($thumbnails[$counter])},
                          -class => 'thumbnail',
                           -name => 'thumbnail'
                         }
                    ) 
           .
           ( $salonify_default_show_thumbnail_captions ? 
             br() 
             .
             textfield(
                            -name => "w$counter",
                         -default => $titles{StripSize($thumbnails[$counter])},
                            -size => $title_width,
                       -maxlength => 60
             ) 
           : 
             ''
           )
          );
    $counter++;
    }
    while ($col_counter++ <= $salonify_default_thumbnails_columns) {
      push (@cols, p("&nbsp;")) if $row_counter > 1;
    }
    push (@rows, td({-class=>'thumbnail'},\@cols));
    @cols = ();
    $col_counter = 0;
  }

  start_form(-action=>$salonify_script_url, -method=>$form_method,-name=>'thumbname') 
    .
    ShowPrefsForm('syzmnoptub')
    .
    table(
          { -class => 'thumbnail' },
          Tr(
             { -class => 'thumbnail' },
               \@rows 
            ) 
          . 
          ( $salonify_default_show_thumbnail_captions ?
            Tr(
               { -class => 'thumbnail' }, 
               td(
                  { -colspan => $salonify_default_thumbnails_columns,
                      -class => 'thumbnail'
                  }, 
                  submit(-value=>"Change Titles") 
                 )
            )  
            : 
            ''
          )
         ) 
  .
  end_form;
}

sub ThumbNailPreferences {
  return start_form(-action=>$salonify_script_url, -method=>'GET',-name=>'u'),
  ( 
   $salonify_allow_user_to_change_layout 
  ?
  table(
    { -border => 2, -align=>"center", -cellspacing=>0, -frame=>'box', -cellpadding=>5},
    Tr( {align=>"CENTER",-valign=>"TOP"},
      td({-colspan=>"2"},
         checkbox(   -name => 'o',
                    -label => 'Show Titles',
                  -checked => $salonify_default_show_thumbnail_captions,
                    -value => 1
      ))),
      Tr( {-align=>"CENTER",-valign=>"TOP"} , 
        td({-width=>"50%"}, 'Columns:' . 
                        textfield(-name=>'m',
                                  -default=>$salonify_default_thumbnails_columns,
                                  -size=>2,
                                  -maxlength=>2)
        ), 
        td({-width=>"50%"}, 'Rows:' .
                        textfield(-name=>'n',
                                  -default=>$salonify_default_thumbnails_rows,
                                  -size=>2,
                                  -maxlength=>2)
        )),
    Tr( {align=>"CENTER",-valign=>"TOP"}, 
      td({-colspan=>"2"},
         ShowPrefsForm('suyzpd') .
         checkbox(
                   -name => 'q',
                  -label => 'Save these settings in a cookie on your computer',
                  -value => 1
         )
      )
    ) 
    .
    Tr( {align=>"CENTER",-valign=>"TOP"}, 
      td({-colspan=>"2"},
      submit(-value=>'Change'))
    ))
     :
      ""
    ),
    end_form;
}

sub DirectoryIndex {
  my $index = "";
  my $counter = 0;
  my ($id, $path, $images, $desc, $children, $level, $lastlevel, $title, $trimpath);
  my $first_item = 1;
  my $list_id = 1;
  my %folders;
  my $lines = 0;
  my $lastpath = "";

  open IN, $salonify_folders_file || return p("Sorry, no index could be found--check permissions!");
  while (<IN>) {
     /^#/ and next;                                             # skip comment lines
     ($id, $path, $images, $desc) = /^(.*)\t(.*)\t(.*)\t(.*)/;  # parse folder line
     $desc =~ s/&/\&amp;/g;                                     # sanitize HTML
     $folders{$path} = { id => $id, images => $images, desc => $desc, children => 0};
     $trimpath = $path;
     $trimpath =~ s{^(.*)/.*$}{$1};   # trim last part of path to see if this is a child of previous
     $folders{$trimpath}{children} = 1 if ($trimpath eq $lastpath and $lastpath ne "");
     $lastpath = $path;
     $lines++;
  }
  close IN;

  $index = ($url_edit_descriptions ?
            start_form(-action=>$salonify_script_url,-method=>'POST',-name=>'description')
            :""
           )
           .
           ($salonify_allow_user_to_edit_descriptions ?
             ($url_edit_descriptions ?
             "<center>" .
             "<a href='$salonify_script_url?i" . ShowPrefsNoForm('szmnopdb') . "'>Return to Index</a>" .
             "</center>"
             :
             "<center>" .
             "<a href='$salonify_script_url?l" . ShowPrefsNoForm('szmnopdb') . "'>Edit Descriptions</a>" .
             "</center>"
             )
           : ""
           )
           .
           ($salonify_collapsible_lists ?
             "<center>( <a id='expandall' href='javascript:expandAll();'>expand all </a> | " .
             "<a id='collapseall' href='javascript:collapseAll();'>collapse all</a> )</center>"
           : ""
           ) 
           .
           "<table width='100%' border='0'><tr><td valign='top' width='" . int(100/$salonify_default_index_columns) . "%'>\n" .
           "<ul id='collapsibleList1'>\n";
  $lastlevel = 0;
  my $lastid = 0; 
  my $total_columns = 1;
  my @ids_to_collapse;
  foreach (sort keys %folders) {
     ($id, $path, $images, $desc, $children) = ($folders{$_}{id}, $_, $folders{$_}{images}, $folders{$_}{desc}, $folders{$_}{children});
     $level = ($path =~ tr/\//\//) - 1;                    # Count /'s for subdirectory nesting

# if we are less nested than the last cycle, close list the necessary number of levels
if ($level < $lastlevel) {                 
        $index .= "</li>\n" . ((end_ul() . "</li>\n") x ($lastlevel-$level));
        $first_item = 1;
     }

# check if we're at the bottom of the column and unindented; if so start a new column
     if (($counter++ > ($lines / $salonify_default_index_columns)) and not $level) {
        $counter = 0;
        $total_columns++;
        $index .= end_ul() . "</td> <td valign='top' width='" . int(100/$salonify_default_index_columns) . "%'>\n <ul id='collapsibleList$total_columns'>\n";
        $first_item = 1;
     }

     if ($level > $lastlevel) {                                 # if we are more deeply nested,
        $index .= "\n<ul id='list$lastid'>\n";                  # start a new sublist
        push @ids_to_collapse, "$lastid";
        $first_item = 1;
     }

     $index .= "</li>\n" unless $first_item;
     $index .= "<li>\n" .

# This next block creates (with JavaScript) the "open and closed" buttons if
# collapsible lists are enabled.
($children && $salonify_collapsible_lists ? <<HERE
<script type='text/javascript'>
document.writeln('<img id="bullet$id" src="$salonify_up_image" alt="$salonify_up_image_alt" onClick="toggle(\\'bullet$id\\',\\'list$id\\');">');
</script>
HERE
: ($salonify_collapsible_lists ?
<<HERE
<script type='text/javascript'>
document.writeln('<img src="$salonify_right_image" alt="$salonify_right_image_alt">');
</script>
HERE
:
""
)
);
     if ($url_edit_descriptions) {
       $index .= 
     ShowPrefsForm('szmnopdb') .
     textfield(
               -name=>"l$id",
               -default=>$desc,
               -size=>30,
               -maxlength=>60
              );
     } else {
       $index .= ($images ? "<a class='imageFolder' href='$salonify_script_url?y=$id;u" . ShowPrefsNoForm('szmnopdb') . "'>$desc</a>" : 
       $salonify_collapsible_lists ? "<a class='parentFolder' href=\"javascript:toggle('bullet$id','list$id')\">$desc</a>"
       :
       "<span class='emptyFolder'>$desc</span>"
       );
     }
     $lastid = $id if $children;
     $first_item = 0;
     $lastlevel = $level;


  }

# if we're still indented, unindent back to 0
  ($level > 0) and $index .= ((end_ul() . "</li>\n") x ($level));
  $index .= end_ul() . 
            "</td>\n</tr>\n</table>\n" .
            "<script type='text/javascript'>\n";
  if ($salonify_collapsible_lists) {
    
# for JavaScript/collapsible users, make the HTML bullet points disappear
     while ($total_columns) {
       $index .= "document.getElementById('collapsibleList$total_columns').style.listStyle='none';\n";
       $total_columns--;
     }
     foreach (@ids_to_collapse) {
        $index .= "document.getElementById('list$_').style.listStyle='none';\n" .
                  ($salonify_start_collapsed ?
                   "document.getElementById('list$_').style.display='none';\n"
                   :
                   ""
                  );
     }
     $index .= "function expandAll(){
";
# I had the following, but it's actually much more complicated--will come back to it.
#document.getElementById('expandall').style.display='none';
#document.getElementById('collapseall').style.display='inline';
     foreach (@ids_to_collapse) {
        $index .= "document.getElementById('list$_').style.display='block';\n" .
                  "document.getElementById('bullet$_').src='$salonify_down_image';\n" .
                  "document.getElementById('bullet$_').alt='$salonify_down_image_alt'\n";
     }
     $index .= "}\nfunction collapseAll(){
";
#document.getElementById('collapseall').style.display='none';
#document.getElementById('expandall').style.display='inline';
     foreach (@ids_to_collapse) {
        $index .= "document.getElementById('list$_').style.display='none';\n" .
                  "document.getElementById('bullet$_').src='$salonify_up_image';\n" .
                  "document.getElementById('bullet$_').alt='$salonify_up_image_alt'\n";
     }
     $index .= <<HERE
}
function toggle(image,list){
  var listElementStyle=document.getElementById(list).style;
  if (listElementStyle.display=="none"){
    listElementStyle.display="block"; 
    document.getElementById(image).src="$salonify_down_image";
    document.getElementById(image).alt="$salonify_down_image_alt";
  }else{
    listElementStyle.display="none"; 
    document.getElementById(image).src="$salonify_up_image";
    document.getElementById(image).alt="$salonify_up_image_alt";
  }
}
</script>
HERE
;
  }
  $index .= (submit(-value=>'Change Descriptions') . end_form) if $url_edit_descriptions;
  return $index;
}

sub DownloadAlbum {
  my ($image_url, $local_path) = @_;

  my $zip_base = MakeZipBase($local_path);

  my $full_url = $image_url . $zip_base . "full.zip";
  my $full_file = $local_path . $zip_base . "full.zip";
  my $medium_url = $image_url . $zip_base . "medium.zip";
  my $medium_file = $local_path . $zip_base . "medium.zip";
  my $small_url = $image_url . $zip_base . "small.zip";
  my $small_file = $local_path . $zip_base . "small.zip";
  my $full_size = 0;
  my $medium_size = 0;
  my $small_size = 0;
  my $full_exists = ( -r $full_file);
  my $medium_exists = ( -r $medium_file);
  my $small_exists = ( -r $small_file);
  my $x;

  $x = h2("Download entire album");

  if ($full_exists) {
     $full_size = (stat($full_file))[7];
  } else {
    while (<$local_path/*>) {
        next unless (-f and /$salonify_image_filename_extensions/i);
        $full_size += (stat($_))[7];
    }
  }
  if ($full_size > 1000000) {
     $full_size = sprintf("%.2fM",$full_size/1000000);
  } else {
     $full_size = sprintf("%.2fk",$full_size/1000);
  }

  if ($medium_exists) {
     $medium_size = (stat($medium_file))[7];
  } else {
    while (<$local_path/.me*>) {
        next unless (-f and /$salonify_image_filename_extensions/i);
        $medium_size += (stat($_))[7];
    }
  }
  if ($medium_size > 1000000) {
     $medium_size = sprintf("%.2fM",$medium_size/1000000);
  } else {
     $medium_size = sprintf("%.2fk",$medium_size/1000);
  }

  if ($small_exists) {
     $small_size = (stat($small_file))[7];
  } else {
    while (<$local_path/.li*>) {
        next unless (-f and /$salonify_image_filename_extensions/i);
        $small_size += (stat($_))[7];
    }
  }
  if ($small_size > 1000000) {
     $small_size = sprintf("%.2fM",$small_size/1000000);
  } else {
     $small_size = sprintf("%.2fk",$small_size/1000);
  }

  $x .= 
        p ( 
           { -align => 'left' },
             ( $full_exists ?
             a ( { href => ( $full_url ) },
                 "Full-sized Images (${full_size})" )
             :
             a ( { href => ( $salonify_script_url . "?L=2" . ShowPrefsNoForm('styzmnopdb') ),
                   target => "download" },
                 "Full-sized Images (${full_size})" )
             )
          ) .
        p ( 
           { -align => 'left' },
             ( $medium_exists ?
             a ( { href => ( $medium_url ) },
                 "Medium-sized Images (${medium_size})" )
             :
             a ( { href => ( $salonify_script_url . "?L=3" . ShowPrefsNoForm('styzmnopdb') ),
                   target => "download" },
                 "Medium-sized Images (${medium_size})" )
             )
          ) .
        p ( 
           { -align => 'left' },
             ( $small_exists ?
             a ( { href => ( $small_url ) },
                 "Small-sized Images (${small_size})" )
             :
             a ( { href => ( $salonify_script_url . "?L=4" . ShowPrefsNoForm('styzmnopdb') ),
                   target => "download" },
                 "Small-sized Images (${small_size})" )
             )
          )
;

  $x .= a ( { href=> $salonify_script_url . "?u" . ShowPrefsNoForm('styzmnopdb') }, "Return to Thumbntails" );

  return $x;
}

sub MakeZipBase {
   $_ = shift @_;
   s{/*$}{};
   s{^/*}{};
   s{^.*/}{};
   return "/${_}_";
}

sub GenerateArchive {
   my ($path, $url, $size) = @_;
   $path =~ s{//*}{/}g;
   my $dirname = $path;
   $dirname =~ s{/*$}{}g;
   $dirname =~ s{^.*/}{}g;
   my $zip_base = MakeZipBase($path);
   my $zip;
   my $target;

   print h2("Generating archive file");

   $target = $zip_base . "full.zip" if $size == 2;
   $target = $zip_base . "medium.zip" if $size == 3;
   $target = $zip_base . "small.zip" if $size == 4;

   SafeRequire "Archive::Zip";
   Archive::Zip->import(qw( :ERROR_CODES :CONSTANTS ));

   if ($size == 2 &! -e $path . $target) {
     $zip = Archive::Zip->new();
     print "<p>Generating archive file. This may take a while... ";
     while (<$path/*>) {
        next unless (-f and /$salonify_image_filename_extensions/i);
        my $file = basename($_);
        $zip->addFile( $_ , $dirname . "/" . $file);
        print " . ";
     }
     foreach my $member ($zip->members()) {
        $member->desiredCompressionMethod( 0 );
     }
     die "Could not write ZIP file ${path}${target}. Check permissions.\n" if ($zip->writeToFileNamed( $path . $target ));
     print "done.</p>";
   }

   if ($size == 3 &! -e $path . $target) {
     $zip = Archive::Zip->new();
     print "<p>Generating archive file. This may take a while... ";
     while (<$path/.*>) {
        next unless (-f and /$salonify_image_filename_extensions/i);
        my $file = basename($_);
        next unless $file =~ m/^.me/;
        $file =~ s/^.me//g;
        $zip->addFile( $_ , $dirname . "/" . $file);
        print " . ";
     }
     foreach my $member ($zip->members()) {
        $member->desiredCompressionMethod( 0 );
     }
     die "Could not write ZIP file ${path}${target}. Check permissions.\n" if ($zip->writeToFileNamed( $path . $target ));
     print "done.</p>";
   }
 
   if ($size == 4 &! -e $path . $target) {
     $zip = Archive::Zip->new();
     print "<p>Generating archive file. This may take a while... ";
     while (<$path/.*>) {
        next unless (-f and /$salonify_image_filename_extensions/i);
        my $file = basename($_);
        next unless $file =~ m/^.li/;
        $file =~ s/^.li//g;
        $zip->addFile( $_ , $dirname . "/" . $file);
        print " . ";
     }
     foreach my $member ($zip->members()) {
        $member->desiredCompressionMethod( 0 );
     }
     die "Could not write ZIP file ${path}${target}. Check permissions.\n" if ($zip->writeToFileNamed( $path . $target ));
     print "done.</p>";
   }
   print p ( 
             a ( 
                 { href=> $url . $target }, 
                 "Click here if you are not automatically redirected." 
               ) 
             .
             "<script type='text/javascript'>
              location.href='$url$target'
              </script>
             "
           );
}

sub DirectoryNavigation {
  my %labels;
  my @values;
  my ($id, $path, $images, $desc, $level, $lastlevel, $title);

  open IN, $salonify_folders_file or return p("Sorry, no index could be found!");
  while (<IN>) {
     ($id, $path, $images, $desc) = /^(.*)\t(.*)\t(.*)\t(.*)/ or next;      
     $level = ($path =~ tr/\//\//);                              # Count /'s for subdirectory nesting
     $images and push @values, $id or push @values, $url_folder . "-$id";
     $images and $labels{$id} = "- " x $level . ' <' . $desc . '>' or
                 $labels{$url_folder . "-" . $id} = "- " x $level . ' ' . $desc;

# This is basically a kludge, but I couldn't figure out any other way to do it without relying on JavaScript.
# Essentially, if you select an item from the menu that doesn't actually have any images in it (i.e., it's a
# "parent directory"), the value submitted is the current page followed by a dash followed by the empty
# page number. Later on, the script strips out anything after a dash.
# Partially this is because CGI.pm requires a hash for the labels, so you can't have two different labels
# point to the same ID. If I had wanted to hard code the popup-menu, that probably would have worked fine,
# but I'm trying to keep to the CGI.pm functions.

  }
  close IN;
  return start_form(-action=>$salonify_script_url, -method=>'GET',-name=>'i'),
         table({ -border => 0, -width => "100%", -align=> 'center' },
         Tr({-align=>"CENTER",-valign=>"TOP"} , 
         td(
         ({-align=>'CENTER'},
                     "Photo Album: " . 
                     popup_menu(-name=>'y',
                     -values=>\@values,
                     -labels=>\%labels,
                     -default=>$url_folder,
                     -onchange=>'submit()') . ' ' . 
         hidden(-name=>'u',-values=>2) . " " .   # This is 2 rather than 1 so the script knows to use the x
         ShowPrefsForm('sxzmnopdb') .                   # value if we end up back on the same page
         submit(-value=>'Go'))))),
         end_form;
}

sub DirectoryRead {
   my $target = shift;
   my %id;
   my ($counter, $path, $contents, $description);
   die "Folders file $salonify_folders_file does not exist.\n" unless (-e $salonify_folders_file);
   open IN, "$salonify_folders_file" or die "Tree file $salonify_folders_file exists but I could not open it--check file permissions!\n";
   while (<IN>) {
      ($counter, $path, $contents, $description) = m{^(.*)\t/*(.*)\t(.*)\t(.*)$};  # eliminate initial slashes if present in path
      $id{$counter} = $path if $contents;
   }
   close IN;
   return $id{$target};
}

sub SlideShowSize {
  return start_form(-action=>$salonify_script_url, -method=>'GET',-name=>'size'),
         table({ -border => 0, -width => "45%", -align=> 'center' },
         Tr({-align=>"CENTER",-valign=>"TOP"} , 
         td(
         ({-align=>'CENTER'},
                     "Image Size: " . 
                     popup_menu(-name=>'z',
                     -values=>[qw/1 2 3/],
                     -labels=>{"1"=>"Full","2"=>"Medium","3"=>"Small"},
                     -default=>"2",
                     -onchange=>'submit()') . ' ' . 
         ShowPrefsForm('sxymnopdb') .
         submit(-value=>'Go'))))),
         end_form;

}

# SlideShowRotate: returns the buttons for rotating the current image
# This one is 'post' rather than 'get' so the rotation action doesn't get reloaded or bookmarked
sub SlideShowRotate {
  return start_form(-action=>$salonify_script_url,-method=>$form_method,-name=>'rotate'),
         table({ -border => 0, -width => "65%", -align=> 'center' },
         Tr({-align=>"CENTER",-valign=>"TOP"} , 
         td(
         ({-align=>'CENTER'},
         ShowPrefsForm('xyzmnopdb') .
         hidden(-name=>'s', -value=> $param{'s'}+1, -override=> 1) . 
         submit(-name=>'r', -value=>'Rotate Clockwise') .
         submit(-name=>'r', -value=>'Rotate Counterclockwise')),
         ))),
         end_form;
}

sub ReadTitles {
   my $filename = shift || "";
   my @images = @_;
   my ($name, $description);
   my %hash;

   open IN, $filename || return %hash;
   while (<IN>) {
     ($name, $description) = m/^(.*)\t(.*)$/;
     $hash{$name} = $description;
   }
   close IN;
   foreach (@images) {
     s/\/.tn|\/.li|\/.me|\///;
     defined $hash{$_} or $hash{$_} = "Give $_ a name!";
   }
   return %hash;
}

sub RcsOpen {
   return unless $salonify_use_rcs;
   $< = $>;
   $( = $);
   my $filename = shift;
   $rcs = Rcs->new;
   $rcs->rcsdir(dirname($filename) . "/RCS");
   $rcs->workdir(dirname($filename));
   $rcs->file(basename($filename));
   unless (-d dirname($filename) . "/RCS") {
     die "Unable to create directory" . dirname($filename) . "/RCS! Check permissions on directories or make the salonify script setuid.\n" unless mkdir dirname($filename) . "/RCS";
   }
   unless (-e dirname($filename) . "/RCS/" . basename($filename) . ",v") {
     die "Unable to perform initial check-in on file $filename with RCS! Check permissions on data files or make the salonify script setuid.\n" unless $rcs->ci('-u','-t-none');  # initial checkin
   }
   die "Unable to check out and lock $filename with RCS! Check permissions on data files or make the salonify script setuid.\n" unless $rcs->co('-l');
}

sub RcsClose {
   $< = $>;
   $( = $);
   $rcs->ci('-u','-mnone') if ($salonify_use_rcs);
}

sub WriteTitles {
   my $filename = shift;
   my $hash = shift;
   my ($name, $description);
   my %hash = %$hash;

   return 0 unless $salonify_allow_user_to_change_captions;

   RcsOpen($filename);
   open OUT, ">$filename";
   while (($name, $description) = each %hash) {
      print OUT $name . "\t" . $description . "\n";
   }
   close OUT;
   RcsClose();
}

sub WriteFolders {
   my $filename = shift;
   my $hash = shift;
   my ($name, $description);
   my %hash = %$hash;
   my @output;

   return 0 unless $salonify_allow_user_to_edit_descriptions;
   open IN, $filename || die "Sorry, no index could be found--check permissions!\n";
   while (<IN>) {
     my ($id, $path, $images, $desc) = /^(.*)\t(.*)\t(.*)\t(.*)/; 
     if ($id) {
       $desc = $hash{$id} if ($hash{$id});
       push @output, "$id\t$path\t$images\t$desc";
     } else {
       chomp;
       push @output, $_;
     }
   }
   close IN;

   RcsOpen($filename);
   open OUT, ">$filename" || die "Couldn't open output file for writing $filename--check permissions!\n";
   print OUT $_ . "\n" foreach (@output);
   close OUT;
   RcsClose($filename);
}

# CleanParam: This attempts to make CGI input more secure, by turning form or URL parameters into numbers-only
sub CleanParam {
   my $parameter = shift;

   return undef unless defined $param{$parameter};
   return '1' unless $param{$parameter} ne ""; 
                          # Return one if parameter is defined but has no value--this means 'on'

   $_ = $param{$parameter};
   s/\0.*//;              # Remove everything after null (if parameter appears multiple times)
   s/(.*)-.*/$1/gi;       # This is to take care of confusion from selected an album with no photos from the list
   s/change|go/1/gi;      # Change and go are labels for 'do it' buttons--we want that to show up as 'true'
   s/\D//g;               # Remove everything left that's not numbers
   $_;
}

sub CookiePrefs {
   my $cookie = shift || "";
   my @prefs = split(/;|&/,$cookie);
   my %prefs_keys;
   foreach (@prefs) {
     if ( (length $_) > 1 and not /^w\d|=.+/ ) {
       while (s/^(.)//) {
          $param{$1} = 1;
          $prefs_keys{$1} = defined;
       }
     } elsif (/[;&]*([^=]*)(=?)(.*)/) {
       $param{$1} = $3;
       $prefs_keys{$1} = defined;
     }
   }
   keys %prefs_keys;
}

sub ProcessPrefs {
  my %prefs;
  foreach (@_) {
    /j/ and $prefs{"SlideShowPrefs"} = 1;
    /J/ and $prefs{"SlideShowPrefs"} = 0;
    /a/ and $prefs{"DirectoryNavigationTop"} = 1;
    /A/ and $prefs{"DirectoryNavigationBottom"} = 1;
    /b/ and $prefs{"SlideShowNavigationTop"} = 1;
    /B/ and $prefs{"SlideShowNavigationBottom"} = 1;
    /c/ and $prefs{"SlideShowButtonsTop"} = 1;
    /C/ and $prefs{"SlideShowButtonsBottom"} = 1;
    /d/ and $prefs{"SlideShowCaptionTop"} = 1;
    /D/ and $prefs{"SlideShowCaptionBottom"} = 1;
    /e/ and $prefs{"SlideShowSizeTop"} = 1;
    /E/ and $prefs{"SlideShowSizeBottom"} = 1;
    /f/ and $prefs{"SlideShowRotateTop"} = 1;
    /F/ and $prefs{"SlideShowRotateBottom"} = 1;
    /g/ and $prefs{"ShowPrefs"} = 1;
    /h/ and $prefs{"ShowCreditsTop"} = 1;
    /H/ and $prefs{"ShowCreditsBottom"} = 1;
  }
  return %prefs;
}

# DefaultPrefs: if no preferences are given in the URL or a cookie, return these
# Shows everything with the buttons on top and everything else below
# This way the user knows what features are available, and can hide them at will with the preferences
sub DefaultPrefs {
   $_ = "";
   $_ .= "j" if $salonify_allow_user_to_change_layout;
   $_ .= "a" if $salonify_default_DirectoryNavigationTop;
   $_ .= "A" if $salonify_default_DirectoryNavigationBottom;
   $_ .= "b" if $salonify_default_SlideShowNavigationTop;
   $_ .= "B" if $salonify_default_SlideShowNavigationBottom;
   $_ .= "c" if $salonify_default_SlideShowButtonsTop;
   $_ .= "C" if $salonify_default_SlideShowButtonsBottom;
   $_ .= "d" if $salonify_default_SlideShowCaptionTop;
   $_ .= "D" if $salonify_default_SlideShowCaptionBottom;
   $_ .= "e" if $salonify_default_SlideShowSizeTop;
   $_ .= "E" if $salonify_default_SlideShowSizeBottom;
   $_ .= "f" if $salonify_default_SlideShowRotateTop;
   $_ .= "F" if $salonify_default_SlideShowRotateBottom;
   $_ .= "j" if $salonify_default_ShowPrefs;
   $_ .= "h" if $salonify_default_ShowCreditsTop;
   $_ .= "H" if $salonify_default_ShowCreditsBottom;
   
   return CookiePrefs($_);
}

sub SlideShowPrefsButton {
  p(
    { -align => "center"}, 
    a(
      { -href => $salonify_script_url . "?j=Show Preferences" . ShowPrefsNoForm('sxyzmnopdb')}, 
        "Show Preferences"
    )
  );
}

sub SlideShowPrefs {
  return 
  start_form(-action=>$salonify_script_url, -method=>'GET',-name=>'prefs') . 
    table({ -border => 1, -cellspacing=>0, -width => "100%" },
      Tr({-align=>"CENTER",-valign=>"TOP"} , 
            [
               th(['&nbsp;','Photo Album List','Photo List','Navigation Buttons','Photo Captions','Photo Size','Photo Rotate','Credits']),
               th('Above').td([
                   checkbox(-name=>'a',-value=>1,-label=>'',-checked=>$url_prefs{"DirectoryNavigationTop"} ),
                   checkbox(-name=>'b',-value=>1,-label=>'',-checked=>$url_prefs{"SlideShowNavigationTop"} ),
                   checkbox(-name=>'c',-value=>1,-label=>'',-checked=>$url_prefs{"SlideShowButtonsTop"}    ),
                   checkbox(-name=>'d',-value=>1,-label=>'',-checked=>$url_prefs{"SlideShowCaptionTop"}    ),
                   checkbox(-name=>'e',-value=>1,-label=>'',-checked=>$url_prefs{"SlideShowSizeTop"}       ), 
                   checkbox(-name=>'f',-value=>1,-label=>'',-checked=>$url_prefs{"SlideShowRotateTop"}     ),
                   checkbox(-name=>'h',-value=>1,-label=>'',-checked=>$url_prefs{"ShowCreditsTop"}         )
               ]),                                
               th('Below').td([
                   checkbox(-name=>'A',-value=>1,-label=>'',-checked=>$url_prefs{"DirectoryNavigationBottom"} ),
                   checkbox(-name=>'B',-value=>1,-label=>'',-checked=>$url_prefs{"SlideShowNavigationBottom"} ),
                   checkbox(-name=>'C',-value=>1,-label=>'',-checked=>$url_prefs{"SlideShowButtonsBottom"}    ),
                   checkbox(-name=>'D',-value=>1,-label=>'',-checked=>$url_prefs{"SlideShowCaptionBottom"}    ),
                   checkbox(-name=>'E',-value=>1,-label=>'',-checked=>$url_prefs{"SlideShowSizeBottom"}       ),  
                   checkbox(-name=>'F',-value=>1,-label=>'',-checked=>$url_prefs{"SlideShowRotateBottom"}     ),
                   checkbox(-name=>'H',-value=>1,-label=>'',-checked=>$url_prefs{"ShowCreditsBottom"}         )
               ]),
               th('&nbsp;').td({-colspan=>7},
                   checkbox(-name=>'q',-label=>'Save these settings in a cookie on your computer',-value=>1))
               ,
               th('&nbsp;').td({-colspan=>7},ShowPrefsForm('sxyzmnob') . submit(-name=>'j', -value=>'Change')),
               th('&nbsp;').td({-colspan=>7},submit(-name=>'J', -value=>'Hide Preferences'))
            ]
      )) .  
  end_form;
}

sub ShowCredits {
  return p(
           { -align => 'left' },
  a( { -href => 'http://adam.rosi-kessel.org/salonify' } , $::program_title) . " $::program_version by " .
  a( { -href => 'http://adam.rosi-kessel.org' },
  "Adam Rosi-Kessel") . " / please " . 
  a( { -href => 'mailto:ajkessel@debian.org' },
  "report any bugs!") . br .
  " Copyright 2002-2005 permission granted to modify and redistribute under " .
  a( { -href => 'http://www.gnu.org/licenses/gpl.html' },
  'General Public License') . " v2.0 or later.");
}

sub ShowFile {
  my $string;
  $_ = shift;
  open IN, $_ || (print STDERR "Could not open header file $_!\n" and return '');
  $string = join('',<IN>);
  close IN;
  return $string;
}

print start_html(
                  -meta => {
                            'Info' => "Slideshow created with salonify, see http://adam.rosi-kessel.org/salonify for your own free copy."
                           },
                 -title => 'Photos',
                   -dtd => '-//W3C//DTD HTML 4.01 Transitional//EN',
                 -style => {
                            'src' => $salonify_cascading_style_sheet_url
                           }
                );

my $directory_contents = DirectoryRead($url_folder);
my $local_path = $salonify_top_level_directory . $directory_contents;
my $image_url = $salonify_top_level_url . $directory_contents;
my %folders;
my %titles;

if ($url_download_album > 1) {
  GenerateArchive($local_path, $image_url, $url_download_album);
} elsif ($url_index or not $url_folder or not $directory_contents) {
      if ($salonify_allow_user_to_edit_descriptions and $url_edit_descriptions) {
         foreach (keys %param) {
            if (/l(\d{1,4})/ and $param{$_}) {
               $folders{$1} = $param{$_};
            }
         }
         WriteFolders($salonify_folders_file, \%folders) if keys %folders;
      }
      if ($salonify_alternate_index_file) {
         print ShowFile($salonify_alternate_index_file);
      } else {
        print 
        ShowFile($salonify_top_of_page_file), 
        DirectoryIndex, 
         ($url_prefs{"ShowCreditsBottom"} ? hr . ShowCredits : '');
      }
} elsif ($url_download_album and ($salonify_generate_static_archive_file or $salonify_generate_dynamic_archive_file)) {
  print DownloadAlbum($image_url, $local_path);
  print ($url_prefs{"ShowCreditsBottom"} ? hr . ShowCredits : ''),
} else {
  if ($url_thumbs) {
        my @thumbnailfiles = <$local_path/.tn*>;
        my @thumbnails;
        foreach (@thumbnailfiles) {
          push @thumbnails, "/" . basename($_) if /$salonify_image_filename_extensions/i;
        }
        (-e $local_path . "/" . $salonify_names_filename) and %titles = ReadTitles($local_path . "/" . $salonify_names_filename, @thumbnails);
        foreach (keys %param) {
          if (/w(\d{1,4})/ and $param{$_}){
             $titles{StripSize($thumbnails[$1])} = $param{$_};
             WriteTitles($local_path . "/" . $salonify_names_filename, \%titles);
          } 
        }
        print
        ThumbNailButtons ($url_current_thumb, @thumbnails),
        DirectoryNavigation, 
        ThumbNailNavigation ($url_current_thumb, @thumbnails),
        ThumbNailImages ($url_current_thumb, $image_url, \%titles, @thumbnails),
        ThumbNailPreferences, 
        ($url_prefs{"ShowCreditsBottom"} ? hr . ShowCredits : '');
  } else {
        my @imagefiles = <$local_path/$size*>;
        my @images;
        foreach (@imagefiles) {
           push @images, "/" . basename($_) if /$salonify_image_filename_extensions/i;
        }
        (-e $local_path . "/" . $salonify_names_filename) and %titles = ReadTitles($local_path . "/" . $salonify_names_filename, @images);
        $url_current = 0 if $url_current >= @images;
        $url_title and $titles{StripSize($images[$url_current])} = $url_title and WriteTitles($local_path . "/" . $salonify_names_filename, \%titles);
        $url_rotate and RotateImage ($url_rotate, $url_current, $local_path, @images);
        print
        SlideShowSetupJava ($url_current, $image_url, \%titles, @images),
        ($url_prefs{"ShowCreditsTop"} ? ShowCredits . hr : ''),
        ($url_prefs{"SlideShowNavigationTop"} ? SlideShowNavigation ($url_current, $image_url, \%titles, @images) : ''),
        ($url_prefs{"DirectoryNavigationTop"} ? DirectoryNavigation : ''), 
        ($url_prefs{"SlideShowSizeTop"} ? SlideShowSize : ''),
        ($url_prefs{"SlideShowRotateTop"} ? SlideShowRotate : '' ), 
        ($url_prefs{"SlideShowButtonsTop"} ? SlideShowButtonsJava : ''),
        ($url_prefs{"SlideShowButtonsTop"} ? SlideShowButtonsNoJava ($url_current, $image_url, @images) : ''),
        ($url_prefs{"SlideShowCaptionTop"} ? SlideShowCaption($titles{StripSize($images[$url_current])}) : ''),
        SlideShowImages ($url_current, $image_url, @images),
        ($url_prefs{"SlideShowCaptionBottom"} ? SlideShowCaption($titles{StripSize($images[$url_current])}) : ''),
        ($url_prefs{"SlideShowButtonsBottom"} ? SlideShowButtonsJava : ''),
        ($url_prefs{"SlideShowButtonsBottom"} ? SlideShowButtonsNoJava ($url_current, $image_url, @images) : ''),
        ($url_prefs{"SlideShowNavigationBottom"} ? SlideShowNavigation ($url_current, $image_url, \%titles, @images) : ''),
        ($url_prefs{"DirectoryNavigationBottom"} ? DirectoryNavigation : ''), 
        ($url_prefs{"SlideShowSizeBottom"} ? SlideShowSize : ''),
        ($url_prefs{"SlideShowRotateBottom"} ? SlideShowRotate : '' ), 
        ($salonify_allow_user_to_change_layout
         ?
          ($url_prefs{"SlideShowPrefs"}
           ? 
            SlideShowPrefs 
           : 
            SlideShowPrefsButton
          ) 
         :
         ''
        ),
        ($url_prefs{"ShowCreditsBottom"} ? hr . ShowCredits : ''),
        ($url_start_slideshow ?
                                '
                                <script type="text/javascript">
                                <!--
                                slideShow();
                                -->
                                </script>
                                '
                              :
                               ''
        )
  }
}

print end_html;

1;

# Query parameters -- all one letter to make for shorter URLs
# a   boolean -- show album navigation list above photos
# A   boolean -- show album navigation list below photos
# b   boolean -- show slideshow navigation list above photos
# B   boolean -- show slideshow navigation list below photos
# c   boolean -- show slideshow buttons above photos
# C   boolean -- show slideshow buttons below photos
# d   boolean -- show slideshow captions above photos
# D   boolean -- show slideshow captions below photos
# e   boolean -- show slideshow size buttons above photos
# E   boolean -- show slideshow size buttons below photos
# f   boolean -- show slideshow rotate buttons above photos
# F   boolean -- show slideshow rotate buttons below photos
# h   boolean -- show program credits above photos
# H   boolean -- show program credits below photos
# i   boolean -- show index page
# j   boolean -- show preferences box
# J   boolean -- hide preferences box
# k   slideshow caption text string
# l   edit folder descriptions (if permitted by configuration)
# L   download entire album
# m   columns in thumbnail chart
# n   rows in thumbnail chart
# o   boolean -- show titles on thumbnails page
# p   reserved -- starts preferences string (Aa-Jj above)
# q   boolean -- save preferences in a cookie
# r   rotate image (clockwise or counterclockwise)
# s   "random string" -- used to force reload of cached images when rotated
# t   page within thumbnails
# u   boolean -- show thumbnails or not 
# v   boolean -- auto start slideshow at beginning
# w#  caption titles from thumbnails page -- # corresponds to slide number to recaption
# x   photo number within album
# y   album number
# z   size (1=full, 2=medium, 3=small) 
