#!/usr/bin/perl --

# Location of config file
###### CHANGE THIS ######
$configfile="http://www.hasta-pronto.net/cgi-bin/album/album.cfg";
###### CHANGE THIS ######

# Program Version
$ver="4.4";

# NOTE: The doucmentation shown below in the POD (Plain Old Documentation) section
# is meant to be read through album.html.
#
# The POD formatting shown here makes it hard to read, so try album.html instead.

######################## START OF POD ########################

=head1 NAME

Photo Album - A dynamically generated web based photo album.

=head1 SYNOPSIS

 http://www.hasta-pronto.net/cgi-bin/album/album.pl
 http://www.hasta-pronto.net/cgi-bin/album/album.pl?function=config
 http://www.hasta-pronto.net/cgi-bin/album/album.pl?function=enter_desc
 http://www.hasta-pronto.net/cgi-bin/album/album.pl?function=upload
 http://www.hasta-pronto.net/cgi-bin/album/album.pl?slideshow=x
 http://www.hasta-pronto.net/cgi-bin/album/album.pl?configfile=/home/user/www/album.cfg

=head1 DESCRIPTION

Photo Album is a simple web based program that allows you to simply drop new photo files into a directory, and they will automatically be accessible via the web. It does this by building the photo album on the fly.

It can also be used to create static HTML files for certain situations. (This mode is not updated automatically, but is useful for creating a photo CD or viewing pictures without a web server.

Users may upload photos directly into your album, if you permit it. To get a feel for all the features and functions, you should see the Revision History section of this document.

=head1 INFORMATION

Author: Mike Bobbitt (Bobbitt@CdnArmy.ca)

For updates, instructions and examples see http://perl.CdnArmy.ca/album on the web. Sometimes this site is a bit out of date. Try contacting me for the latest version if it's been a while since it was updated.

Please also note that this is a personal project. I work on it in my spare time, on weekends and evenings, so I may not be able to respond immediately to questions or comments.

=head1 MAILING LIST

There is a mailing list set up for discussion of album.pl matters. I would recommend that anyone with a support question try sending to the list first. That way, if I am unable to respond right away, it's entirely likely that a peer user will be able to help out.

To subscribe, simply send an e-mail to majordomo@cdnarmy.ca with "subscribe album-list" in the message body (quotes not required).

To send a message to the list, simply send it to album-list@cdnarmy.ca, however I recommend you subscribe first, so you can receive all responses.

=head1 FILES

Below is a list of the files and directories that are used by this script, their purpose, and their preferred permissions. The items listed here are the files and directories used by default. Your specific configuration is probably different, but this will give you an idea of what is involved. Each of these paths are web paths. That is, they are as if they were from the web root, not from the file system root.

=begin html
<table border="2" cellpadding="2" style="border-collapse: collapse" bordercolor="#111111" bgcolor="#C0C0C0">
  <tr>
    <th align="left">File</th>
    <th align="left">Description</th>
    <th align="left">Permissions</th>
  </tr>
  <tr>
    <td align="left">/Photo_Album.css</td>
    <td align="left">The cascading style sheet for the album. [Optional, set by
    style_sheet]</td>
    <td align="left">World Readable</td>
  </tr>
  <tr>
    <td align="left">/cgi-bin/album.pl</td>
    <td align="left">The script itself.</td>
    <td align="left">World Executable</td>
  </tr>
  <tr>
    <td align="left">/cgi-bin/album.cfg</td>
    <td align="left">The config file. You need to edit this.</td>
    <td align="left">World Readable, or writable to your web server if you intend to use the function=config feature.</td>
  </tr>
  <tr>
    <td align="left">/cgi-bin/album.tml</td>
    <td align="left">The album template file. You may want to edit this.</td>
    <td align="left">World Readable</td>
  </tr>
  <tr>
    <td align="left">/cgi-bin/album_header.tml</td>
    <td align="left">The album header sub-template file. You may want to edit this. It's automatically included into album.tml and photo.tml.</td>
    <td align="left">World Readable</td>
  </tr>
  <tr>
    <td align="left">/cgi-bin/album_footer.tml</td>
    <td align="left">The album footer sub-template file. You may want to edit this. It's automatically included into album.tml and photo.tml.</td>
    <td align="left">World Readable</td>
  </tr>
  <tr>
    <td align="left">/cgi-bin/photo.tml</td>
    <td align="left">The photo template file. You may want to edit this.</td>
    <td align="left">World Readable</td>
  </tr>
  <tr>
    <td align="left">/cgi-bin/album_upload.tml</td>
    <td align="left">The upload form template file. You may want to edit this.</td>
    <td align="left">World Readable</td>
  </tr>
  <tr>
    <td align="left">/cgi-bin/album_rating.tml</td>
    <td align="left">The rating form template file. You may want to edit this.</td>
    <td align="left">World Readable</td>
  </tr>
  <tr>
    <td align="left">/img/thmb_photo.gif</td>
    <td align="left">The icon for a photo. [Optional, set by icons=1]</td>
    <td align="left">World Readable</td>
  </tr
  <tr>
    <td align="left">/img/thmb_album.gif</td>
    <td align="left">The icon for an album. [Optional, set by icons=1]</td>
    <td align="left">World Readable</td>
  </tr>
  <tr>
    <td align="left">/img/thmb_movie.gif</td>
    <td align="left">The icon for a movie. [Optional, set by icons=1]</td>
    <td align="left">World Readable</td>
  </tr>
  <tr>
    <td align="left">/Photo_Album/</td>
    <td align="left">Your actual photo album root directory. Images put in here
    will show up as photos in your gallery.</td>
    <td align="left">World Writable</td>
  </tr>
  <tr>
    <td align="left">/Photo_Album/descriptions.txt</td>
    <td align="left">Used to describe the photos and albums in your gallery. For
    formatting rules see below. [Optional]</td>
    <td align="left">World Writable</td>
  </tr>
  <tr>
    <td align="left">/Photo_Album/views.txt</td>
    <td align="left">Created automatically by this script. Used to keep track of the number of times each photo in this album has been viewed. [Optional, set by viewfile]</td>
    <td align="left">World Writable</td>
  </tr>
  <tr>
    <td align="left">/Photo_Album/subdir</td>
    <td align="left">A sub album. Can contain more photos and a descriptions.txt
    file, as desired.</td>
    <td align="left">World Writable</td>
  </tr>
  <tr>
    <td align="left">/Photo_Album/somephoto.jpg</td>
    <td align="left">A photo in your album.</td>
    <td align="left">World Readable</td>
  </tr>
  <tr>
    <td align="left">/Photo_Album/thmb_somephoto.jpg</td>
    <td align="left">A thumbnail for a photo in your album.</td>
    <td align="left">World Readable</td>
  </tr>
  <tr>
    <td align="left">../logs/gallery.log</td>
    <td align="left">Log file to log all photo uploads. Should be above the web
    root, so people can't browse to it.</td>
    <td align="left">World Writable</td>
  </tr>
  <tr>
    <td align="left">../auth_db</td>
    <td align="left">Authorization database. Should be above the web root, so
    people can't browse to it. [Optional, set by authentication_type=0]</td>
    <td align="left">World Readable</td>
  </tr>
</table>

=end html

I think that's about it. If you are having problems, turn off options like "notify" then you don't have to worry about files such as the photo_address.txt. I'd recommend turning all the "options" off and getting it to work that way, then turn on the ones you want to use.

=head1 FILE FORMAT

This section describes the file format for some of the files used by album.pl.

=head2 F<descriptions.txt>

The descriptions.txt file contains all the information used to display album/photo custom titles and descriptions.

 Root
 Root_Album_Description
 Root_Album_Long_Description (multi-line)
 ~
 Image_File_Name
 Image_Short_Description
 Image_Long_Description (multi-line)
 ~
 Album_File_Name
 Album_Short_Description
 Album_Long_Description (multi-line)
 ~
 Image_File_Name
 ...

For example:

 Root
 The CdnArmy.ca Photo Gallery
 <center><img src="/img/Photo_Gallery.gif"></center>
 This gallery contains photos uploaded by CdnArmy.ca users. To upload a photo, please see the <a href=../cgi-bin/album.pl?function=upload>Photo Upload</a> page.

 Please select from one of the categories below. If you think a new category or sub category is required, please <a href=mailto:CdnArmy@CdnArmy.ca>let me know</a>.

 Enjoy
 ~
 General
 General
 General Pictures of the Canadian Army
 ~
 Training
 Training
 Training Photos
 ~
 Operations
 Operations
 Operational Photos
 ~
 c7.gif
 C7A1 Rifle
 This is the C7 rifle, with Elcan scope mounted.

 It is the standard issue rifle for the Canadian Army.
 ~
 ...

Where ~ is the delimiter by default. Also note that you can use the "album.pl?function=enter_desc" feature to create this file on the fly.

=head2 F<auth_db.txt>

This is the file used to store authentication information (username/password pairs). This information is used to authenticate users when they upload photos, if authentication_type=1. The file format is very simple:

 username1|password1
 username2|password2
 username3|password3
 ...

For example:

 bobbitt|giraffe
 user2|guest
 ...

=head2 F<album.tml>

There are some special tags you should be aware of that you can use witin the template file. Each of these tags will be substituted for dynamicly generated HTML, as shown below. These tags can be used any number of times, and each instance will be substituted.

See album.tml and photo.tml included in this distribution for an example.

=begin html
<table border="2" cellpadding="2" style="border-collapse: collapse" bordercolor="#111111" bgcolor="#C0C0C0">
  <tr>
    <th align="left">Tag</th>
    <th align="left">Description</th>
  </tr>
  <tr>
    <td align="left">####TITLE####</td>
    <td align="left">The object's short title. As defined in descriptions.txt.</td>
  </tr>
  <tr>
    <td align="left">####DESCRIPTION####</td>
    <td align="left">The object's long description. As defined in descriptions.txt.</td>
  </tr>
  <tr>
    <td align="left">####OBJECT####</td>
    <td align="left">The object itself. This is either the photo (for photos), or the list of photos/albums (for albums).</td>
  </tr>
  <tr>
    <td align="left">####NAV####</td>
    <td align="left">The navigation bar. (Buttons or text links to next, previous and parent objects.)</td>
  </tr>
  <tr>
    <td align="left">####STYLESHEET####</td>
    <td align="left">The complete reference to the style sheet. This will substitute with the entire &ltlink rel="stylesheet" type="text/css" href="$style_sheet"&gt tag. NOTE: This tag must be in the HEAD section of your HTML.</td>
  </tr>
  <tr>
    <td align="left">####SLIDESHOW####</td>
    <td align="left">Used to replace the slideshow directive, you really don't need to mess with this. NOTE: This tag must be in the HEAD section of your HTML.</td>
  </tr>
  <tr>
    <td align="left">####LEGEND####</td>
    <td align="left">The icon legend, if you elect to show it at all.</td>
  </tr>
  <tr>
    <td align="left">####NOTIFY####</td>
    <td align="left">The e-mail notification form, if you elect to show it at all.</td>
  </tr>
  <tr>
    <td align="left">####ENTERDESC####</td>
    <td align="left">The form that allows you to enter object titles and descriptions, if you're using "function=enter_desc".</td>
  </tr>
  <tr>
    <td align="left">####VIEWS####</td>
    <td align="left">Displays the number of times the photo has been viewed and the date it was last viewed, if using a viewfile.</td>
  </tr>
  <tr>
    <td align="left">####RATINGS####</td>
    <td align="left">Displays the user ratings given for a photo, and a link to add ratings of your own. Only present if using a ratingfile.</td>
  </tr>
  <tr>
    <td align="left">####ALBUMPROG####</td>
    <td align="left">The web path to album.pl. Meant to be used for linking back to your album.</td>
  </tr>
  <tr>
    <td align="left">####FILE=filename.html####</td>
    <td align="left">Automatically inserts the contents of "filename.html" into the template. (Note that it doesn't have to be a HTML file.) Used for SSI like behaviour, common headers/footers, or even nesting templates.</td>
  </tr>
  <tr>
    <td align="left">####STOPSLIDESHOW####</td>
    <td align="left">The "Stop Slideshow" button. Only displayed during a slide show.</td>
  </tr>
  <tr>
    <td align="left">####STOPDEBUG####</td>
    <td align="left">The "Stop Debugging" button. Only displayed when debugging is on.</td>
  </tr>
  <tr>
    <td align="left">####PAGES####</td>
    <td align="left">The multi-page link set. Only displayed when photos_per_page is non-zero, and the number of photos on the current page is greater than that value.</td>
  </tr>
</table>

=end html

=head2 F<photo.tml>

Same as album.tml.

=head2 F<upload.tml>

Similar to F<album.tml> and F<photo.tml>, F<upload.tml> contains special tags that will automatically be replaced. All of the tags shown above are still valid, and only tags meant specifically for the upload form are shown here. You'll probably want to edit this template to get your own "rules" displayed on the upload page.

=begin html
<table border="2" cellpadding="2" style="border-collapse: collapse" bordercolor="#111111" bgcolor="#C0C0C0">
  <tr>
    <th align="left">Tag</th>
    <th align="left">Description</th>
  </tr>
  <tr>
    <td align="left">####FORMAT####</td>
    <td align="left">The list of valid file formats accepted. Taken from imgexts and/or movieexts as appropriate.</td>
  </tr>
  <tr>
    <td align="left">####SIZE####</td>
    <td align="left">The maximum size allowed for uploaded files.</td>
  </tr>
  <tr>
    <td align="left">####UPLOAD####</td>
    <td align="left">The actual upload form.</td>
  </tr>
</table>

=end html

=head2 F<album_rating.tml>

Similar to the other templates, it contains special tags that will automatically be replaced. All of the tags shown above are still valid, and only tags meant specifically for the rating form are shown here.

=begin html
<table border="2" cellpadding="2" style="border-collapse: collapse" bordercolor="#111111" bgcolor="#C0C0C0">
  <tr>
    <th align="left">Tag</th>
    <th align="left">Description</th>
  </tr>
  <tr>
    <td align="left">####RATING####</td>
    <td align="left">Inserts the rating form.</td>
  </tr>
</table>

=end html

=head1 INSTALLATION

General Installation Instructions

=over 2

=item *
Copy F<album.pl>, F<album.cfg>, F<album.tml> and F<photo.tml> to your cgi-bin directory. Put F<thmb_photo.gif>, F<thmb_album.gif> and F<thmb_movie.gif> in your images directory. Make sure the permissions are set as per the FILES section of this document.

=item *
Create a directory for your photos. Make sure it is writable to the web server (usually world writable) if you want to update descriptions via the web. Any sub directories or images your put in this directory will be automatically viewed as albums and photos by the script.

=item *
Put your photos into your photo directory. Directory trees are supported.

=item *
Make thumbnails of your photos (optional). I use Thotor: http://www.dominativ.com/thotor/. The settings that worked for me are Max Height 75, quality 25.

=item *
Edit F<album.pl> to set the location of the config file. The script tries to make some good guesses about where your config file is, so you may not have to do anything.

=item *
Go to http://www.webserver.com/cgi-bin/album?function=config to set all the configuration items (obviously, insert your servername and path to album.pl as required). You can also edit F<album.cfg> directly to achieve the same result.

=item *
Go to F<album.pl> through the web, and troubleshoot as required. Error messages will be displayed if anything is not working correctly. (See the FAQ section for the most common problems.)

=item *
If you allow uploads to your album, you will want to check that out by going to http://../album.pl?function=upload. If you are using UBB (see http://www.infopop.com), then you will want to set authentication_type=2 in your config file. No hacking of UBB code is required, just set your config variables properly.

=item *
Last but not least, please let me know you're using the software! The more people that I know are using this script, the more time I'll devote to it. So speak up!

=item *
You can contact me if you really get stuck, and I will do my best to respond quickly.

=back

=head1 UPGRADING

General Upgrading Instructions

=over 2

=item *
Upload album.pl into your cgi-bin directory.

=item *
Upload any new files that come with the .zip, to the locations specified in the FILES section of this document.

=item *
Check the REVISION HISTORY section of this document. Manually add any new config items to album.cfg, or simply upload the new one and re-configure.

=back

=head1 FAQ

I'm working on it...

Q: How do I get rid of the filename/path in the title? Also: How do I make a custom header for my album?

A: There are 2 ways. The first way is to simply go to your album through the web as http://www.hasta-pronto.net/cgi-bin/album/album.pl?function=enter_desc. Adding this "?function=enter_desc" will set the album to load in a special mode, where you can enter a title and long description for the album or photo you are currently viewing. (See the bottom of the page.) In this option, you have to enter info for each item by browsing to it.

The second option is to populate a "descriptions.txt" file for each album. This is what the above option does under the covers, but you may want to edit this file directly. See the "File Format" section of this document for details on the descriptions.txt file.

The most common problems are:

=over 2

=item *
The F<album.cfg> file was transferred in FTP binary mode. It needs to be in ASCII mode.

=item *
There are leading/training spaces or funny characters in your F<album.cfg> file.

=item *
Incorrect configuration settings in F<album.cfg>.

=item *
Incorrect file permissions.

=back

If you've checked things out and are still having problems, feel free to contact me. (See above)


=head1 REVISION HISTORY

=head2 V4.4 - 01/10/11

=over 2

=item *
Changed upload_authentication_type config item to authentication_type.

=item *
Added protect_album config item to turn on album viewing security. (Users must authenticate according to authentication_type requirements before they can view the album.)

=item *
Now works with the YaBB security mod, for encrypted passwords. (No configuration required, will check both plaintext and "security mod" passwords silently.)

=item *
Expanded static album metrics.

=back

=head2 V4.3 - 01/09/30

=over 2

=item *
Fixed problem where changing the upload code broke uploads.

=item *
Changed ubb_member_upload config item to per_member_upload, and allowed it to work for any authentication type, not just UBB.

=item *
Fixed permission problem for new upload directories created with per_member_upload.

=back

=head2 V4.2 - 01/09/18

=over 2

=item *
Added debug_code config item, which allows the debug function code to be modified. This closes a potential security hole where a knowledgeable user can set your album into debug mode to obtain information they shouldn't have.

=back

=head2 V4.1 - 01/09/13

=over 2

=item *
Added photos_per_page config item. Limits the number of photos displayed for a given album to the value set here. Uses a multi-page link set to allow the user to view other pages.

=item *
Added ####PAGES#### tag, updated style sheets and album.tml.

=back

=head2 V4.0 Final - 01/08/31

=over 2

=item *
Updated default style sheet. (It's now gray, not blue.)

=item *
Descriptions for albums are now shown for the upload form. Sub-albums are indented.

=item *
Added showsubalbumcount config item. Can be used to show the number of "sub albums" in a given album.

=item *
Added webifyLinks subroutine. Right now it allows & in filenames to be handled correctly.

=item *
Started adding style classes to the style sheet and to the templates. Makes better use of style sheets, and allows all items of a particular type to be changed in a single place. Updated templates to make use of these new classes.

=item *
Added the ####STOPSLIDESHOW#### tag for the "Stop Slideshow" button (which was also added in this release). Updated photo.tml to use this new tag.

=item *
Added the ####STOPDEBUG#### tag for the "Stop Debugging" button (which was also added in this release). Updated photo.tml and album.tml to use this new tag.

=item *
Fixed slideshows - they weren't working!

=item *
Minor bug fixes.

=back

=head2 V4.0 Beta 4 - 01/07/31

=over 2

=item *
Added upload_template config item, and album_upload.tml template, which can be used to customize the upload form.

=item *
Added ####FILE=filename.html#### tag to templates. Automatically inserts the contents of "filename.html" into the template. (Note that it doesn't have to be a HTML file.) Used for SSI like behaviour, common headers/footers, or even nesting templates.

=item *
Added album_header.tml and album_footer.tml sub-templates. Modified album.tml and photo.tml to include them, using FILE tag above.

=item *
Added several new tags to the templates, mostly for the upload form template.

=item *
Added ratingfile, rating_form and update_rating config items, which allow the use of user ratings of photos. Please, I know this feature is crap right now, so bear with me!

=item *
Added rating_template config item and album_rating.tml template, to allow the rating form to be customized as a template.

=item *
Fixed the upper case/lower case thumbnail extension problem.

=item *
Added YABB authentication for uploads. (Set authentication_type=3)

=item *
Updated photo and album templates.

=item *
Improved code performance for tracking views.

=item *
Some random bug fixes, that I forget about now.

=back

=head2 V4.0 Beta 3 - 01/07/23

=over 2

=item *
Configuration is now modifyable through the web. Call http://www.hasta-pronto.net/cgi-bin/album/album.pl?function=config to configure your settings in a web interface. You can change the config code from the default to stop others from re-configuring your board for you. (See notes in the album.cfg file.)

=item *
Added updateconfig and config function codes to the config file, as noted above.

=item *
Added nextprevthumb config item, which allows you to control the display of next and previous photo thumbnails.

=item *
Added constrain_thumbs, thumb_height and thumb_width config items. If constrain_thumbs is set to 1, uses height and width tags to reduce display size of actual photo, and uses that for the thumbnail. The thumb_height and thumb_width config items are used to control the size of the constrained thumbnail shown.  When you use this option, you no longer have to provide seperate thumbnail images to get custom thumbnails, they are generated automatically. However, since the thumbnails are actually the original image constrained, you will in effect be loading the entire image.

=item *
Added "last viewed date" to the views file.

=item *
Added ####VIEWS#### tag and updated the photo.tml template.

=item *
Album and photo icons can now be any valid photo extension, as defined by the imgexts config item.

=item *
Added parseLinks to make generated links more Netscape friendly. Currently, it replaces spaces with %20 in any links.

=back

=head2 V4.0 Beta 2 - 01/07/19

=over 2

=item *
Removed ubb_auth config item, and replaced it with upload_authentication_type config item. The new item allows you to set anonymous (0), flatfile (1) or UBB (2) authentication types, and also allows future types to be added.

=item *
Added "Stop Entering Descriptions" checkbox

=item *
Added thmb_movie.gif image and movie_icon and file_movie_icon config items. Movies now have their own generic icon.

=item *
When photos or albums do not have a description defined, the entire path is no longer used as a default. (Just the photo file name or album directory name.) Makes things a little cleaner.

=back

=head2 V4.0 Beta 1 - 01/07/12

=over 2

=item *
Added F<album.tml> and F<photo.tml> files, which correspond to album_template and photo_template config items. These files are used as the templates for viewing albums and photos respectively, and can be modified by the user.

=item *
Major re-write of the old header/footer functions to use template files.

=item *
Long descriptions are no longer a link to the next picture.

=item *
Fixed bug where body tag was not displayed if a style sheet was used.

=item *
Updated the info in the F<album.cfg> file to be a bit more user friendly.

=back

=head2 V3.7 - 01/07/07

=over 2

=item *
Fixed bug with jumpstation when using debug, a function or a password.

=item *
Added "album_border" config item. Used to define the width of the border shown for album icons.

=item *
Jump station no longer shows up when there are no peer albums now.

=item *
Now uploads also use the updateDesc function - fixes the upload overwrite problem where the description was not updated.

=item *
Photo and album icons are only used if they exist. No more broken image links!

=item *
Before throwing an error, the script will look in a couple likely places for album.pl.

=item *
Fixed another security hole with the album= var.

=back

=head2 V3.6 - 01/07/05

=over 2

=item *
Added "movie_upload" config item. When set to a non-zero value, movie uploads are permitted as well. (See movieexts config item.) Note that the upload_size_limit is still enforced, so you'll probably want to raise this value if you allow movie uploads. You can use this feature to allow your gallery to handle (and upload) binary or custom files (zip, exe, etc).

=item *
Added "jump_to" config item. Allows you to have a drop box displayed for albums to jump between peer albums.

=item *
Added "viewfile" config item. Allows you to keep track of (and display) the number of times each picture has been viewed.

=item *
Albums now have a border, so you can tell them from regular photos when you're using album icons.

=item *
Fixed up some style sheet stuff. (If a style sheet is used, the <body> tag doesn't contain formatting and colour info.)

=item *
When entering descriptions, you automatically advance to the next photo.

=item *
Fixed up this section of the documentation.

=back

=head2 V3.5 - 01/07/03

=over 2

=item *
Added line numbers to every debug statement.

=item *
Fixed (the last?) of the 500 errors.

=item *
Added a couple more (very basic) style sheets.

=item *
Added ability for albums to have their own thumbnails. File must be named thmb_albumname.gif, where thmb_ is the thumbprefix and albumname is the exact name of the directory/folder that is your album. The thumbnail must be put at the same level as the album directory (not in it). Also must be a .gif, at least for now...

=back

=head2 V3.4 - 01/07/01

=over 2

=item *
Updated documentation (slightly).

=item *
Debug flag is now passed through all operations.

=item *
Added album_password option, which lets you password protect your album.

=item *
Fixed the problem that many people were having with ther config file (hopefully).

=back

=head2 V3.3 - 01/06/26

=over 2

=item *
Better use of CGI package.

=item *
The rest of the subtitles have been turned into buttons (if you have usebuttons enabled).

=item *
Added next and previous buttons for album view.

=item *
Sub albums now [finally] work correctly in both online and static modes.

=item *
You can now specify an alternate config file, through the web: album.pl?configfile=/home/user/www/album.cfg.

=item *
Added a security check into the sanity tests.

=item *
And, as usual, random bug fixes.

=back

=head2 V3.2 - 01/06/23

=over 2

=item *
Renamed photo.gif and album.gif to have thmb_ prefix, as some people wanted to put them in their albums. Makes sense.

=item *
Added a very basic sanity test to run at startup (will make it more complete later).

=item *
Started to use CGI package a bit more.

=item *
Added prevnextbutt option, which allows you to use form buttons instead of text links for next/prev photo and album.

=item *
Updated the .css file.

=back

=head2 V3.1 - 01/06/14

=over 2

=item *
"allow_uploads" flag is now honoured.

=item *
Added ubb_auth flag to define the type of authentication for photo uploads (either UBB or flatfile DB).

=item *
Added per_member_upload flag to allow users to upload into their own directory.

=item *
Various bug fixes.

=back

=head2 V3.0 - 01/06/12

=over 2

=item *
Merged old gallery.pl functionality into album.pl.

=item *
This means you can now allow users to upload directly into your photo album: "http://../album.pl?function=upload".

=item *
Also moved configuration information out to album.cfg, to make it easier to install/set up.

=back

=head2 V2.6 - 01/05/24

=over 2

=item *
Added ability to show the number of pictures and/or movies by each album name

=back

=head2 V2.5 - 01/05/08

=over 2

=item *
Can now pass in a slide show timer via the web: "http://../album.pl?slideshow=x" where x is the number of seconds to show a photo.

=back

=head2 V2.4 - 01/04/16

=over 2

=item *
Can now set "static" mode from the command line: ./album.pl static

=back

=head2 V2.3 - 01/01/03

=over 2

=item *
Some minor fixes. Can now view movies and run slide shows.

=back

=head2 V2.2 - 00/12/23

=over 2

=item *
Can now use thumbnails. (I use Thotor to generate the thumbnails: http://www.dominativ.com/thotor/. The settings that worked for me are Max Height 75, quality 25)

=item *
Pictures with a long description are marked with a * in the album.

=item *
Other minor fixes.

=back

=head2 V2.1 - 00/12/20

=over 2

=item *
Creating static HTML will now create an HTML page for each photo, like the online version.

=item *
Updated debugger to allow multiple levels. I was getting way too confused when debugging stuff. (See notes by $debug for more info.)

=back

=head2 V2.0 - 00/09/16

=over 2

=item *
Creating static HTML is now a single operation - it recursively scans all albums and creates an HTML file in each directory.

=back

=head2 V1.9 - 00/07/08

=over 2

=item *
Can use style sheets.

=back

=head2 V1.8 - 00/06/11

=over 2

=item *
Can click on a photo to move to the next one (as per my 2 year old daughter's request).

=back

=head2 V1.7 - 00/04/27

=over 2

=item *
Added the ability to write out static HTML files for the currently viewed album (unfinished).

=back

=head2 V1.6 - 00/01/16

=over 2

=item *
Added "next" and "previous" buttons when viewing photos, now using short descriptions for most "back" buttons.

=back

=head2 V1.5 - 00/01/13

=over 2

=item *
Re-built for IIS and Apache, made some fixes for allowing it to run on Win32 (see comments throughout, some changes required).

=back

=head2 V1.4 - 00/01/09

=over 2

=item *
Added capability to add/edit photo and album descriptions online via "http://../album.pl?function=enter_desc" command.

=back

=head2 V1.3

=over 2

=item *
Added "notify" section to track interested parties' e-mail addresses.

=back

=head2 V1.2

=over 2

=item *
Added icons for photo and album.

=back

=head2 V1.1

=over 2

=item *
I forget what I added for this release.

=back

=head2 V1.0

=over 2

=item *
Initial release.

=back


=head1 TO DO

There are still bugs in this code, so please bear with me, and report any that you find.

=head2 Static HTML To Do's

=over 2

=item *
Fix icons and legend (turn from relative HTTP link to relative file reference)

=item *
Copy .css and images (if used) to root of album, and reference them there

=item *
.css is referenced using full path, which doesn't make code mobile, but if you make it a relative link, each album will have a different value

=item *
Make it run in "non-HTML" mode when run from the command line.

=item *
Figure out how to avoid a timeout for large photo albums (you can run it from the command line for now)

=back


=head2 Upload To Do's

=over 2

=item *
Allow users to create their own custom sub-albums

=item *
Make "uploaded by" line dynamic, read UBB name at display time

=item *
Honour UBB IP bans

=item *
Allow users to delete photos they have uploaded

=back


=head2 UBB To Do's

=over 2

=item *
Add email address when uploading. (Linked to the UBB username that is displayed).

=item *
Display UBB $username (with email link) under the thumbnail file name. (After you click on a specific album.)

=item *
Add option to have album only viewable by UBB/auth_db users.

=item *
Use the UBB template header/footer information for the photo album.

=item *
Use cookies to automatically log in UBB users. (Or any users? Set cookies too?)

=back


=head2 Other To Do's

=over 2

=item *
Have a pics_per_page config item, allows "next page" and "previous page" buttons to avoid large albums.

=item *
Add "favourites" which becomes a virtual album. Any photo marked as a favourite is listed there. (Use favourites.txt to track the photos added to the favourites album. Remove those that are not found...)

=item *
The subalbumcount does not do subsubalbums, etc.

=item *
Set the order in which photos are displayed by the description file (currently alphabetical). By date, by title and by order in $descfile for starters.

=item *
Bug: Thumbnail extensions may not match case if the static HTML is built on an Windows machine (which doesn't care about case) and then moved to a UNIX machine (which does).

=item *
Show next/prev thumbnails for albums too?

=item *
Add "most popular" or 10% most popular by pictures according to # of views. Maybe even add a "most popular" album that contains pics from anywhere.

=item *
Per album security. Give an option to choose between 2 different icons for unprotected and protected albums (it would be nice if the visitor could see immediately that the albums are protected).

=item *
Resolve the "security by obscurity" issue: the path to the actual photos is unknown, but the photos themselves are actually unprotected.

=item *
Show sub albums (indented) in jump station.

=item *
Add jump station for photos.

=item *
Have register function? (I.E. Installations ask to send an e-mail to me or register with an online CGI once setup is complete...)

=item *
Make errors clearer, not just "can't open file" but "can't open $rootdir, please check the config file."

=item *
Allow searching by photo descriptions/titles/filenames/dates/ratings

=item *
Don't require dir to be mode 777 for descriptions.txt or album.cfg changes (read desc file, make changes and re-write: no temp file should be required)

=item *
Generate thumbnails instead of using files (anyone know where I can get an "on the fly" thumbnail generator?)

=item *
Have "E-Mail Notify" list automatically send out update mail


=back

Any place in the code that I've left things off, I tried to mark it with a ***.

=head1 LICENSE

As long as you leave this POD section and my contact info in tact above, feel free to use this as you see fit. If you come up with any good modifications to it, please contact me.

Parts of the photo upload section have been adapted from code by Muhammad Muquit (http://www.muquit.com).

Good luck!

=head1 APOLOGIES

Here goes:

=over 2

=item *
Sorry for the bugs. Hope it doesn't ruin your day.

=item *
Sorry that it's so hard to install.

=item *
Sorry for the state of the code. Please don't learn any of my bad habits!

=item *
Sorry you felt the need to read all of this.

=back

=head1 SUBROUTINES

=cut

######################## END OF POD ########################

######################## START OF SETUP ########################

use CGI;
use POSIX qw(ceil floor);

# Make sure the config file is readable to the web server

# Set debug level:
#	0 = None
#	1 = Special - Nothing has this code, so you can add a ,1 to any debug statement to make it stand out on it's own
#	2 = Standard - No recursive info or detailed file info. Usually just startup info.
#	3 = Detailed - Some recursive and detailed info.
#	4 = Overwhelming - Everything that ever had a debug turned on.
$debug=0;

# Write output immediately
$|=1;

my $query=new CGI;
my $form=new CGI;

# Print HTML Page Header
print <<HTML;
Content-Type: text/html


<html>
HTML

# Check to see if a configfile override was passed in
if ($form->param('configfile'))
{
	$configfile=$form->param('configfile');
	$configfilepassed=1;
}

# Read configuration file settings
readConfig($configfile);

# Was a debug value passed in from the web?
if (!$debug)
{
	$debug=$form->param($debug_code);
}

debug("Starting execution, debugging is on...",2,__LINE__);

# Check some basic stuff to make sure the environment is safe
sanityTest();

# Full path to album.pl - don't change this
$albumprog=$0;

debug("\$0: $0<p>",2,__LINE__);

# Drop the proceeding path (for UNIX) - don't change this
$albumprog=~s/.*\/(.*)/$1/;

# No change? Then this must be a Win32 machine... - don't change this
if ($albumprog eq $0)
{
	$albumprog=~s/.*\\(.*)/$1/;
}

# Check for command-line parameter to set static mode - don't change this
if ($ARGV[0] eq "static")
{
	$create_html_flag=2;
}

# Explicitly turn off buttons, jump station and multi-paging if creating static HTML
if ($create_html_flag)
{
	$usebuttons=0;
	$jump_to=0;
	$photos_per_page=0;
}

if (!$authentication_type)
{
	$protect_album=0;
}

$albumprog=$webroot."/".$relalbum."/".$albumprog;

debug("Web Path to Album (albumprog): $albumprog<p>\n",2,__LINE__);

# Set location of album.pl
$homedir=$rootdir;

# Load these down into a variable, so we can modify them
$username=$form->param('username');
$password=$form->param('password');

# Was a slideshow timer value passed in from the web?
if ($form->param('slideshow'))
{
	$slide_timer=$form->param('slideshow');

	# Set flag to keep the slide timer in the URL...
	$slide_timer_passed=1;
}

# Load this down into a variable, so we can modify it
$function=$form->param('function');

# Load this down into a variable, so we can modify it
$page=$form->param('page');

# Set default page
if (!$page)
{
	$page=1;
}

######################## END OF SETUP ########################

######################## START OF MAIN ########################

# Are we displaying the config screen?
if ($function eq $config)
{
	showConfig();
}

# Are we actually updating the configuration?
if ($function eq $updateconfig)
{
	updateConfig();
}

# Check to see we're creating static HTML files
if ($create_html_flag)
{

	# *** Temporary action to remove icons and legend for static html... (notify is OK)
	$icons=$legend=$notify=0;

	# Copy CSS into root photo album and reference it from there ***
#	copy(x,"$rootdir$style_sheet");

	# *** Title is wrong for photos with no desc (it's last desc in album) - now I forget what that means...
	print "<head><title>album.pl - Generating Static HTML Files</title>";

	$object=printHeader();
	print <<HTML;
$object
<center><h1>Photo Album - Generating Static HTML Files</h1></center>
<p>
This mode is meant to be used to prepare the photo album to be read offline. For example, when a web server
is not practical or available, or if you want to burn your album onto a CD-ROM.
<p>
This will create an HTML file in each directory (album), and one for each photo. This will allow you to view the photos through this static HTML. (Normally this HTML is built on the fly by the web server). The down side to doing this is that you will have to re-run this process if new photos are added.
<p>
HTML

	print "Generating Static HTML for...";
	print "<p>";
	print "$rootdir...";
	print "<br>";

	debug("Calling photoAlbum($rootdir)",4,__LINE__);
	photoAlbum($rootdir);
	debug("Back from photoAlbum($rootdir) call",4,__LINE__);
	debug("Calling recursiveScan($rootdir)",4,__LINE__);
	&recursiveScan($rootdir);
	debug("Back from recursiveScan($rootdir) call",4,__LINE__);

	print "<p>";
	print "Created HTML for $static_photos_done photo(s)";

	# Any movies?
	if ($static_movies_done)
	{
		print " and $static_movies_done movie(s)";
	}

	print " in $static_albums_done album(s).\n";
	if ($static_photos_thumb)
	{
		print "<p>";
		print "$static_photos_thumb photo thumbnails were found.\n";
	}
	if ($static_movies_thumb)
	{
		print "<p>";
		print "$static_movies_thumb movie thumbnails were found.\n";
	}
	if ($static_albums_thumb)
	{
		print "<p>";
		print "$static_albums_thumb album thumbnails were found.\n";
	}
}
else
{
	# Show Login Screen if not already logged in
	if (($album_password && ($password ne $album_password)) || ($protect_album && !Authenticate($username,$password)))
	{
		login();
	}
	&photoAlbum();
}

######################## END OF MAIN ########################

######################## START OF SUBROUTINES ########################


##########################################################################

=head3 photoAlbum()

 photoAlbum($manual_override);

 $manual_override - Start in this album or with this photo (optional)

Does the majority of the processing for the photo album

=cut

sub photoAlbum
{
my $manual_override=shift;

debug("Entered photo_album subroutine<p>",4,__LINE__);

if ($manual_override)
{
	debug("\$manual_override=$manual_override",2,__LINE__);
}

# Read web environment variables

debug("REMOTE_HOST: $ENV{REMOTE_HOST}",3,__LINE__);
debug("REMOTE_USER: $ENV{REMOTE_USER}",3,__LINE__);
debug("REMOTE_IDENT: $ENV{REMOTE_IDENT}",3,__LINE__);
debug("HTTP_USER_AGENT: $ENV{HTTP_USER_AGENT}",3,__LINE__);
debug("REQUEST_METHOD: $ENV{REQUEST_METHOD}",3,__LINE__);
debug("QUERY_STRING: $ENV{QUERY_STRING}",3,__LINE__);
debug("CONTENT_LENGTH: $ENV{CONTENT_LENGTH}",3,__LINE__);
#debug("SSL_CLIENT_CN: $ENV{SSL_CLIENT_CN}",3,__LINE__);
#debug("SSL_CLIENT_IO: $ENV{SSL_CLIENT_IO}",3,__LINE__);

# Check to see if we're being called with function=$upload
if ($function eq "$upload")
{
	uploadPhoto();
	exit();
}

# Check to see if the user is updating a description
if ($function eq "$update_desc")
{
	updateDesc($form->param('object'),$form->param('desc_file_loc'),$form->param('title'),$form->param('description'));
}

# Check to see if the user is adding a rating
if ($function eq "$rating_form")
{
	$shortdesc="Add a Rating: ".$form->param('object');
	$object=buildTemplate();
	display($object);
	exit();
}

# Check to see if the user is adding a rating
if ($function eq "$update_rating")
{
	if ($form->param('comments'))
	{
		$object="Comment from ";
		if ($form->param('name'))
		{
			$object.=$form->param('name');
		}
		else
		{
			$object.="Anonymous";
		}
		$object.=": ".$form->param('comments');
	}

	updateRating($form->param('object'),$form->param('rating_file_loc'),$form->param('rating'),$object);
}

# Have we been told to stop adding descriptions?
if ($form->param('stop_add_desc') eq "stop")
{
	$function="";
}
# Translate into shorter variable names
if ($form->param('album'))
{
	$album=$rootdir."/".$form->param('album');
}

if ($form->param('photo'))
{
	$photo=$form->param('photo');
}

# Override album (for creating static HTML)
if ($manual_override)
{
	debug("Manual Override is [$manual_override]",2,__LINE__);
	if (isAPhoto($manual_override))
	{
		$photo=$manual_override;
		debug("Setting photo to [$photo]",2,__LINE__);
		$static_filename_to_use="$full_directory.html";
		$album="";
	}
	else
	{
		$album=$manual_override;
		debug("Setting album to [$album]",2,__LINE__);
		if ($full_directory)
		{
			# For photo albums to be scanned
			$static_filename_to_use="$full_directory/$static_html_filename";
		}
		else
		{
			# For root photo album
			$static_filename_to_use="$rootdir/$static_html_filename";
		}
		$photo="";
	}
}

# Is this the top level?
if (!($photo || $album))
{
	$album="$rootdir";
}

if ($album)
{
	$middle=$album;
}

if ($photo)
{
	$middle=$photo;
}

if ($middle=~/^$rootdir$/i)
{
	$middle="";
}
else
{
	$middle=~s/$rootdir\/(.*)/$1/;

	# No change? Then this must be a Win32 machine...
	if ($middle eq $rootdir)
	{
		$middle=~s/.*\\(.*)/$1/;
	}
}

# Open static HTML file...
if ($create_html_flag)
{
	# Open the static HTML file
	open(STATIC,">$static_filename_to_use") || error("not_writable","$static_filename_to_use");
	debug("Creating static HTML file at [$static_filename_to_use]",2,__LINE__);
}

debug("The album is: $album",2,__LINE__);
debug("The middle bit is: $middle<p>",2,__LINE__);

$goback=$middle;

# Drop the proceeding path (for UNIX)
$goback=~s/(.*)\/.*/$1/;

# No change? Then this must be a Win32 machine...
if ($goback eq $middle)
{
	$goback=~s/.*\\(.*)/$1/;
}

if ($goback=~/^$middle$/)
{
	$goback="";
}

# Keep a copy, so you don't have all the funny web stuff when you do a compare
$realgoback=$goback;

if (!$usebuttons)
{
	$goback.=passVars(0);
}

debug("GoBack: $goback<p>",2,__LINE__);

if ($photo)
{
	$descfile=$rootdir."/";
	if ($goback)
	{
		$descfile.=$goback."/";
	}
}

if ($album)
{
	#$shortalbum=$rootdir."/".$album;
	$shortalbum=$album;
	if ($realgoback)
	{
		$shortalbum=~s/(.*$realgoback).*/$1\//;
	}
	else
	{
		$shortalbum=$rootdir."/";
	}

	debug("ShortAlbum: $shortalbum<p>",2,__LINE__);

	# If you're updating descriptions, this is the file you want.
	$desc_to_update="$shortalbum";

	openDescfile($shortalbum);

	if ($album=~/^$rootdir$/i)
	{
		$shortalbum="$rootalbumname";
	}
	else
	{
		if ($realgoback)
		{
			$shortalbum=$middle;
			$shortalbum=~s/(.*)$realgoback\//$1/;
		}
		else
		{
			$shortalbum=$middle;
		}
	}

	debug("ShortAlbum: $shortalbum<p>",2,__LINE__);

	$shortobject=$shortalbum;

	getDescription($shortalbum);

	close(DESC);

	# Set the description to pass as a default for editing
	$existing_shortdesc=$shortdesc;

	if (!$shortdesc)
	{
		if ($shortalbum eq $rootalbumname)
		{
			$shortdesc="Photo Album";
		}
		else
		{
			$shortdesc=$shortalbum;
		}
	}

	$descfile="$rootdir/$middle/";
}

# Select directory to read all entries from
if ($album)
{
	$dir_to_read="$rootdir/$goback";
}

if ($photo)
{
	$dir_to_read="$descfile";
	$shortphoto=$photo;
	$shortphoto=~s/^$realgoback\/(.*)/$1/;
}

# find next and previous pictures/albums as appropriate
debug("Looking for next and previous objects in [$dir_to_read]...",2,__LINE__);

@file_list=&readDirectory($dir_to_read);

debug("Done Directory Scan, comparing for $shortphoto or $shortalbum...",3,__LINE__);

# clear re-used vars
$prev_obj_desc="";
$next_obj_desc="";

$this_is_it=0;
$next_obj="";

# Prep the "jump station" for the footer
if ($jump_to && $middle && $album && $contains_dir)
{
	$jump_station="</td></tr><tr><td align=\"middle\" width=\"33%\" height=\"1\"></td>\n";
	$jump_station.="<td align=\"right\" width=\"66%\" colspan=\"2\" height=\"1\">";
	$jump_station.="<form method=\"POST\" action=\"$albumprog\">\n";
	$jump_station.=passVars(1);
	$jump_station.="Jump to: <SELECT name=album>\n";
	# Save vars
	$temp_shortdesc=$shortdesc;
	$temp_longdesc=$longdesc;
	openDescfile($desc_to_update);
}

foreach $object (@file_list)
{
	debug("Current object is [$object]",4,__LINE__);
	if ($this_is_it)
	{
		debug("Is this an album: $rootdir/$goback/$object",4,__LINE__);

		# This is cheap; if $goback="" then we get "$rootdir//$object" which works, but is not clean
		if (($photo && isAPhoto($object)) || ($album && -d "$rootdir/$goback/$object"))
		{
			$next_obj=$object;
			debug("Setting \$next_obj to $next_obj...",3,__LINE__);
		}
	}
	$this_is_it=0;
	if (($photo && ($object eq $shortphoto)) || ($album && ($object eq $shortalbum)))
	{
		$prev_obj=$prev_object;
		$this_is_it=1;
		debug("Setting \$prev_obj to $prev_obj...",3,__LINE__);
	}
	$prev_object=$object;

	# Prep the "jump station" for the footer
	if ($jump_to && $middle && $album && $contains_dir && (-d "$rootdir/$goback/$object"))
	{
		$founddesc=0;
		getDescription($object);
		$jump_station.="<OPTION value=\"";
		if ($goback)
		{
			$jump_station.="$goback/";
		}
		$jump_station.="$object\"";
		if ($object eq $shortalbum)
		{
			$jump_station.=" SELECTED";
		}
		$jump_station.=">";
		if ($usedesc && $shortdesc)
		{
			$jump_station.="$shortdesc";
		}
		else
		{
			$jump_station.="$object";
		}
		$jump_station.="</OPTION>\n";
	}
}

# Prep the "jump station" for the footer
if ($jump_to && $middle && $album && $contains_dir)
{
	# Restore vars
	$shortdesc=$temp_shortdesc;
	$longdesc=$temp_longdesc;
	close(DESC);
	$jump_station.="</SELECT> <INPUT type=submit value=Go name=SUBMIT></form>\n";
	$jump_station.="</td></tr></table>";
}

# Cache descriptions, to be restored later
$temp_shortdesc=$shortdesc;
$temp_longdesc=$longdesc;

$shortdesc=$longdesc="";

# strip off last album, to get descriptions for the current album, next album and previous album
$back_descfile=$descfile;
$back_descfile=~s/(.*\/).*\/.*/$1/;

if ($album)
{
	openDescfile($back_descfile);
}

if ($photo)
{
	openDescfile($descfile);
}

# Get description for the next object
if ($next_obj)
{
	getDescription($next_obj);
	if ($founddesc)
	{
		$next_obj_desc=$shortdesc;
	}
}

# Get description for the previous object
if ($prev_obj)
{
	getDescription($prev_obj);
	if ($founddesc)
	{
		$prev_obj_desc=$shortdesc;
	}
}
close(DESC);

# Restore original descriptions
$shortdesc=$temp_shortdesc;
$longdesc=$temp_longdesc;

debug("Next Object: $next_obj [$next_obj_desc]",3,__LINE__);
debug("Previous Object: $prev_obj [$prev_obj_desc]",3,__LINE__);

# clear array of photos, to save a lot of work later
@file_list="";

if ($album)
{
	# Re-load the list of files from the current album directory
	@file_list=&readDirectory($album);
}

debug("Finished looking for next and previous objects...",2,__LINE__);

# If the current object is an album, you have to go up one more
if ($album && ($middle=~/\//))
{
	$back_descfile=~s/(.*\/).*\/.*/$1/;
}

# Get description of current object's album
openDescfile($back_descfile);
$temp_shortdesc=$shortdesc;
$temp_longdesc=$longdesc;
$shortdesc=$longdesc="";
if (!$realgoback)
{
	$object=$rootalbumname;
}
else
{
	$object=$realgoback;
	if ($object=~/\//)
	{
		$object=~s/.*\/(.*)/$1/;
	}
}
getDescription($object);
$back_desc=$shortdesc;
$shortdesc=$temp_shortdesc;
$longdesc=$temp_longdesc;
debug("Desc for object's parent album is: $back_desc",4,__LINE__);
close(DESC);

# clear re-used vars
$prev_object="";

# Remove any web variables, if present
$descfile=~s/(.*)\&.*/$1\//;

openDescfile($descfile);

if ($photo)
{

	debug("ShortPhoto: $shortphoto<p>",2,__LINE__);

	$shortobject=$shortphoto;

	$shortdesc="";
	$longdesc="";
	getDescription($shortphoto);

	# Set the description to pass as a default for editing
	$existing_shortdesc=$shortdesc;

	if (!$shortdesc)
	{
		$shortdesc=$shortphoto;
	}

	# If you're updating descriptions, this is the file you want.
	$desc_to_update="$descfile";
}

debug("ShortObject: $shortobject",2,__LINE__);

# Make a "text" (not html) version of the long description
$textlongdesc=$longdesc;
$textlongdesc=~s/<br>//g;
$textlongdesc=~s/\r//g;
$textlongdesc=~s/\n+$//g;

# Build the "next photo" url
if ($create_html_flag)
{
	$next_photo_link="$next_obj.html";
}
else
{
	$next_photo_link="$albumprog?photo=$realgoback/$next_obj";
}

$next_photo_link.=passVars(0);

# Build navigation buttons/links
$nav_footer=buildNavFooter();

# When a photo is displayed, it is in an HTML page, with a reference to the actual photo.
# The short description is the photo's title, and the long description is displayed below it.
# Description information is read out of $descfile file, in the photo album.

# Save off descriptions
$temp_shortdesc=$shortdesc;
$temp_longdesc=$longdesc;

$actual_object=buildObject();

# Restoredescriptions
$shortdesc=$temp_shortdesc;
$longdesc=$temp_longdesc;

$object_template=buildTemplate();

$object_template=parseLinks($object_template);

debug("Object template is built! [$object_template]",4,__LINE__);

display("$object_template");

# Scan for files and directories under the root directory. Each directory is considered an album.
# Each photo file (.bmp, .gif or .jpg) is considered an photo.

# Display an HTML page listing all albums and photos in given directory.

# Each album's link will call album.pl with the name of the album as an environment variable.
# (album=01 - The Beginning)

# Each picture's link will call album.pl with the name of the photo file as an environment variable.
# Each picture's short description is displayed as the link text.
# (photo=001.jpg)

debug("Leaving photo_album subroutine<p>",4,__LINE__);

if ($usedesc)
{
	close(DESC);
}

if ($create_html_flag)
{
	close(STATIC);
}


}

##########################################################################

=head3 openDescfile()

 openDescfile($descfilename);

 $descfilename - Path to the album you want to read descriptions from.

Opens up $descfilename so that photo/album titles/descriptions can be loaded.

=cut

sub openDescfile
{
my $descfilename=shift;

	#$descfilename.="/".$descname;
	$descfilename.=$descname;
	debug("Looking for DescFile: [$descfilename]<p>",3,__LINE__);
	if (-r $descfilename)
	{
		$usedesc=1;
		open(DESC,"$descfilename");
		debug("Using DescFile: [$descfilename]<p>",3,__LINE__);
	}
}


##########################################################################

=head3 getDescription()

 getDescription($desctoget);

 $desctoget - Photo or album to get description of

Retrieves the title and description of the provided photo or album. Puts the title in $shortdesc and the description in $longdesc, if present.

=cut

sub getDescription
{
my $desctoget=shift;

	debug("Entering getDescription",3,__LINE__);

	debug("Getting Description for [$desctoget] ==> \$usedesc=[$usedesc]",3,__LINE__);

	# Clear variables, in case they're being re-used
	$shortdesc="";
	$longdesc="";

	# Haven't found a description yet.
	$founddesc=0;

	# See if description exists
	if ($usedesc && $desctoget)
	{
		# Rewind description file
		seek(DESC,0,0);

		while ($descline=<DESC>)
		{
			# Found it
			if ($descline=~/^$desctoget$/i)
			{
				$shortdesc=<DESC>;
				chomp($shortdesc);
				$longdesc="";
				$longdescline="";
				while (($longdescline=<DESC>) && ($longdescline!~/^$desc_delim$/))
				{
					$longdesc.=$longdescline."<br>";
				}
				$founddesc=1;
			}
		}
		if (!$founddesc)
		{
			$shortdesc=$longdesc="";
			debug("No description found.",3,__LINE__);
		}
		debug("ShortDesc: [$shortdesc]",3,__LINE__);
		debug("LongDesc: [$longdesc]",3,__LINE__);
	}

	debug("Leaving getDescription",3,__LINE__);

}


##########################################################################

=head3 error()

 error($error_code,$extra_info);

 $error_code - Pre-defined error code that prints a canned message.
 $extra_info - Additional information that can be passed in

Displayes the error message associated with $error_code along with the $extra_info. Then halts execution.

=cut

sub error
{
	# Pull error code and error fields
	($error,@error_fields)=@_;

	$error_header="<html><head><title>album.pl - ERROR!</title>\n";
	$error_header.=printHeader();
	$error_header.="<center><h1>ERROR!</h1></center><br>\n";

	# Print error header
	display($error_header);

	# Build error info, depending on error code
	if ($error eq "tempdesc_not_writable")
	{
		$errstring="Cannot open temp file for writing: <b>@error_fields</b><p>\nYou probably need to make the directory world writable (chmod 777 for unix).";
	}
	elsif ($error eq "not_readable")
	{
		$errstring="Cannot open file: <b>@error_fields</b>\n";
	}
	elsif ($error eq "not_writable")
	{
		$errstring="Cannot write to file: <b>@error_fields</b>\n";
	}
	elsif ($error eq "sanity")
	{
		$errstring="Failed basic sanity test: <b>@error_fields</b>\n";
	}
	elsif ($error eq "upload_error")
	{
		$errstring="Error uploading photo: <b>@error_fields</b>\n";
	}
	elsif ($error eq "open_db")
	{
		$errstring="Cannot open authentication database: <b>@error_fields</b>\n";
	}
	elsif ($error eq "cant_append")
	{
		$errstring="Cannot append to file: <b>@error_fields</b>\n";
	}
	elsif ($error eq "upload_dir")
	{
		$errstring="Cannot create directory to upload photo into: <b>@error_fields</b>\n";
	}
	elsif ($error eq "conf_error")
	{
		$errstring="Error in configuration file: <b>Line @error_fields</b>\n";
	}
	elsif ($error eq "no_config")
	{
		$errstring="Invalid configuration file specified (@error_fields).\n<p>Please edit <b>$0</b> and modify the \"\$configfile=...\" line to point to your configuration file.\n";
	}
	else
	{
		# Handle error codes not listed above
		$errstring="Unknown error code: $error (@error_fields)\n";
	}

	# Print error info
	display("$errstring<p>\n");
	display("<hr>\n<center>");

	# Print error footer
	showFooter(1);

	# End of error
	exit;
}


##########################################################################

=head3 display()

 display($display_message);

 $display_message - String to print (to STDOUT or to static file)

Prints a message to the designated output steam

=cut

sub display
{
	print {$create_html_flag ? "STATIC" : "STDOUT" } "@_";

	if ($debug && $create_html_flag)
	{
		print "@_";
	}
}


##########################################################################

=head3 debug()

 debug($debug_message,$debug_level,$line);

 $debug_message - String to print if debugging is on
 $debug_level - Only pring string if current debug level is $debug_level or higher (current debug level is set by $debug)
 $line - The line number of the debug statement.

Prints a message, if $debug has a value.

=cut

sub debug
{
my $debug_message=shift;
my $debug_level=shift;
my $line=shift;

	if (($debug_level le $debug) && $debug)
	{
		display("<pre>");
		if ($line)
		{
			display("$line: ");
		}
		display("$debug_message</pre>\n");
	}
}


##########################################################################

=head3 readDirectory()

 readDirectory($dir_to_read);

 $dir_to_read - Directory to read all values from

Reads all entries in specified directory, returns as list

=cut

sub readDirectory
{
my @file_list;
my @final_file_list;
my $scan_directory=shift;
my $shortfile;

	# Clear arrays, because otherwise they appear to have a null element, which really buggers things up
	while (@final_file_list)
	{
		shift @final_file_list;
	}
	while (@file_list)
	{
		shift @file_list;
	}

	debug("Attempting to read entries in [$scan_directory]<p>",2,__LINE__);
	opendir(ENTRIES,"$scan_directory") or error("not_readable","$scan_directory");

	# Change Grep
	@file_list=grep !/^\.\.?$/,readdir ENTRIES;
	close(ENTRIES);

	# clear directory flag
	$contains_dir=0;

	foreach $shortfile (@file_list)
	{
		# Check to see if it's a movie, picture or album
		debug("Checking to see if $scan_directory/$shortfile is a photo/movie/album...",4,__LINE__);
		if (isAPhoto($shortfile) || isAMovie($shortfile) || (-d "$scan_directory/$shortfile"))
		{
			push @final_file_list,$shortfile;
			debug("It's one of the above...",4,__LINE__);

			# Set the flag if we've seen at least one dir
			if (!$contains_dir || (-d "$scan_directory/$shortfile"))
			{
				$contains_dir=1;
			}
		}
	}

	# Here's where to add in different sorting options
	@final_file_list=sort(@final_file_list);

	debug("All Entries:<p>@final_file_list",4,__LINE__);

	return (wantarray) ? @final_file_list : shift(@final_file_list);

}


##########################################################################

=head3 recursiveScan()

 recursiveScan($dir_to_read,$already_scanned);

 $dir_to_read - Directory to search recursively
 $already_scanned - Path of directories above this one (optional)

Walks the directory tree recursively

=cut

sub recursiveScan
{
my @all_dirs;
my $single_directory,$photo_filename,$directory_only;
my $scan_directory=shift;
my $already_scanned=shift;

	debug("Performing recursive scan of $scan_directory...",2,__LINE__);
	debug("\$already_scanned=$already_scanned",2,__LINE__);

	@all_dirs=&readDirectory($scan_directory);

	foreach $single_directory (@all_dirs)
	{
		$full_directory="$rootdir/";
		if ($already_scanned)
		{
			debug("Concatenating \$already_scanned ($already_scanned) to \$full_directory ($full_directory) because \$already_scanned has a value.",2,__LINE__);
			$full_directory.="$already_scanned/";
		}
		$full_directory.=$single_directory;
		debug("\$full_directory=$full_directory",2,__LINE__);
		if (-d "$full_directory")
		{
			# Increment "scan depth" counter
			$scan_depth++;

			debug("$single_directory is a directory.",2,__LINE__);
			$static_albums_done++;
			$directory_only=$single_directory;
			if ($already_scanned)
			{
				debug("Prepending $already_scanned to $single_directory because \$already_scanned has a value.",2,__LINE__);
				$single_directory="$already_scanned/$single_directory";
			}

			# Do any per directory processing here
			if ($doing_upload)
			{
				debug("Building upload category list [$single_directory]...",3,__LINE__);

				# Get description
				openDescfile("$rootdir/$already_scanned/");
				getDescription($directory_only);
				close(DESC);

				$object.="<option value=\"$single_directory\">";

				# Indent sub albums
				$temp=$scan_depth-1;
				while ($temp)
				{
					$object.="--";
					$temp--;
				}
				if ($already_scanned)
				{
					$object.="> ";
				}

				if ($shortdesc)
				{
					$object.="$shortdesc";
				}
				else
				{
					$object.="$single_directory";
				}
				$object.="</option>\n";
			}
			else
			{
				print "$full_directory...<br>\n<ul>\n";

				# Generate static HTML for $full_directory
				debug("Calling photoAlbum($full_directory)",4,__LINE__);
				&photoAlbum($full_directory);
				debug("Back from photoAlbum($full_directory) call.",4,__LINE__);
			}

			$debug_num_recurses++;

# Un comment to stop recursing (added when testing started to run away)
#			if ($debug_num_recurses lt 3)
#			{
			debug("Calling recursiveScan($full_directory,$single_directory)",4,__LINE__);
			&recursiveScan($full_directory,$single_directory);
			debug("Back from recursiveScan($full_directory,$single_directory) call.",4,__LINE__);
			debug("Recursion $debug_num_recurses...",2,__LINE__);
#			}
#			else
#			{
#				debug("Recursion Stopped.",2,__LINE__);
#			}

			# Pop back up a level
			$scan_depth--;
		}

		# Generate HTML for pictures too
		elsif (!$doing_upload && (-s "$full_directory") && (isAPhoto($full_directory)))
		{
			$photo_filename=$full_directory;
			# strip off all but the filename
			$photo_filename=~s/.*\/(.*)/$1/;

			$photo_and_path=$full_directory;
			# strip off the rootdir
			$photo_and_path=~s/$rootdir\/(.*)/$1/;

			if ($already_scanned)
			{
				# This used to be a typo so $photo_filename wasn't being set, and everything seemed to work. If something is broken, take this out. (I haven't tested it)
#				$photo_filename="Picture: $already_scanned/$photo_filename";
				# Maybe this is what I meant:
				$photo_filename="$already_scanned/$photo_filename";
			}

			# Do any per file processing here

			debug("Generating photo HTML for $photo_filename",2,__LINE__);
			print "<li>$photo_filename<br>\n";
			$static_photos_done++;
			debug("Calling photoAlbum($photo_and_path)",4,__LINE__);
			&photoAlbum("$photo_and_path");
			debug("Back from photoAlbum($photo_and_path) call.",4,__LINE__);
		}
	}
	debug("Done with $scan_directory...",2,__LINE__);
	if (!$doing_upload)
	{
		print "</ul>\n";
	}
}


##########################################################################

=head3 isAPhoto()

 isAPhoto($photo_name);

 $photo_name - name of picture file

Returns 1 if the filename passed in is that of a valid "photo", 0 otherwise.

=cut

sub isAPhoto
{
my $flag=0;
my $photo_name=shift;

	debug("Checking to see if $photo_name is a photo...",4,__LINE__);

	# *** I may want to add a test to see if $photo_name is a readable file as well, but that might cause problems for relative paths, if they're used

	if ($photo_name=~/^$thumbprefix/i)
	{
		$flag=0;
	}
	else
	{
		foreach $imgext (@imgexts)
		{
			if ($photo_name=~/.*\.$imgext$/i)
			{
				$flag=1;
				debug("Yep, it's a photo.",4,__LINE__);
			}
		}
	}
	return $flag;
}


##########################################################################

=head3 isAMovie()

 isAMovie($movie_name);

 $movie_name - name of movie file

Returns 1 if the filename passed in is that of a valid "movie", 0 otherwise.

=cut

sub isAMovie
{
my $flag=0;
my $movie_name=shift;

	debug("Checking to see if $movie_name is a movie...",4,__LINE__);

	# *** I may want to add a test to see if $movie_name is a readable file as well, but that might cause problems for relative paths, if they're used

	if ($movie_name=~/^$thumbprefix/i)
	{
		$flag=0;
	}
	else
	{
		foreach $movieext (@movieexts)
		{
			if ($movie_name=~/.*\.$movieext$/i)
			{
				$flag=1;
				debug("Yep, it's a movie.",4,__LINE__);
			}
		}
	}
	return $flag;
}


##########################################################################

=head3 readConfig()

 readConfig($configfile);

 $configfile - path and filename of the config file

Reads configuration information from the specified config file.

=cut

sub readConfig
{
my $confile=shift;
my $data;
my $line=0;
my $confopen=0;
my $couldbe=$0;

	# Set all config vars to null, just so they exist. (Was causing some setup issues)
	# String variables
	$rootdir=$webroot=$relalbum=$notify_file=$photo_icon=$movie_icon=$album_icon=$file_photo_icon=$file_movie_icon=$file_album_icon=$style_sheet=$descname=$upload_logfile=$membersdir=$auth_db=$static_html_filename=$thumbprefix=$rootalbumname=$desc_delim=$album_password=$enter_desc=$update_desc=$upload=$viewfile=$album_template=$photo_template=$updateconfig=$config=$update_rating=$ratingfile=$rating_form=$rating_template=$upload_template=$debug_code="";

	# Numeric variables
	$cfgver=$columns=$column_spacing=$descloc=$usebuttons=$showcount=$showmoviecount=$icons=$legend=$notify=$allow_uploads=$upload_overwrite=$upload_size_limit=$authentication_type=$per_member_upload=$create_html_flag=$slide_timer=$nextprevthumb=$constrain_thumbs=$showsubalbumcount=$photos_per_page=0;

	# Array variables
	@imgexts=("");
	@movieexts=("");

	debug("Reading configuration information from $confile...",2,__LINE__);

	# Trying to open the provided config file
	if (open(CONFIG,"$confile"))
	{
		debug("Opened config file at $confile",2,__LINE__);
		$confopen=1;
	}
	else
	{
		$confopen=0;
	}

	# Nope. Try just album.cfg
	if (!$confopen)
	{
		debug("Trying to read configuration information from album.cfg...",2,__LINE__);
		if (open(CONFIG,"album.cfg"))
		{
			debug("Opened config file at album.cfg",2,__LINE__);
			$confopen=1;
			$configfile="album.cfg";
		}
		else
		{
			$confopen=0;
		}
	}

	# Nope. Try the same directory as album.pl explicitly
	if (!$confopen)
	{
		# Drop the proceeding path (for UNIX) - don't change this
		$couldbe=~s/(.*\/).*/$1/;

		# No change? Then this must be a Win32 machine... - don't change this
		if ($couldbe eq $0)
		{
			$couldbe=~s/(.*\\).*/$1/;
		}
		$couldbe.="album.cfg";
		debug("Trying to read configuration information from $couldbe...",2,__LINE__);
		if (open(CONFIG,"$couldbe"))
		{
			debug("Opened config file at $couldbe",2,__LINE__);
			$confopen=1;
			$configfile=$couldbe;
		}
		else
		{
			$confopen=0;
		}
	}

	# Just couldn't find the bloody thing!
	if (!$confopen)
	{
		error("no_config","$confile");
	}

	while ($data=<CONFIG>)
	{
		$line++;
		chomp($data);

		# Stript out trailing and leading whitespace
		$data=~s/^\s*(\S*)\s*$/$1/;

		# skip comments and blank lines
		if (!(($data eq "") || ($data=~/^#.*/)))
		{
			if ($data!~/.+=.*/)
			{
				error("conf_error","$line");
			}
			$data=~/(@*\w+)=(.*)/;
			$var=$1;
			$value=$2;
			debug("Var [$var] = [$value]",3,__LINE__);

			# set variables
			# is it an array?
			if ($var=~/^\@/)
			{
				@array_values=split(",",$value);
				eval("$var=\@array_values");
				debug("Setting loaded: [$var=@array_values]",3,__LINE__);
			}
			else
			{
				if ($value)
				{
					eval("\$$var=\"\$value\"");
				}
			}
		}
	}
	close(CONFIG);

	# Check to see if any data was loaded at all
	if (!$rootdir)
	{
		error("conf_error","Your config file is missing critical information. Please get a new copy of it.");
	}
}


##########################################################################

=head3 uploadPhoto()

 uploadPhoto();

Called when a user wants to upload a photo. Shows the form and handles the file transfer.

=cut

sub uploadPhoto
{
my $object;

	openDescfile("$rootdir/");
	getDescription($rootalbumname);
	close(DESC);

	if (!$shortdesc)
	{
		$shortdesc="The Uploadable Photo Album";
	}

	if (!$allow_uploads)
	{
		# print the HTML HEADER
		print <<HTML;
<html>
<head>
HTML

		if ($style_sheet)
		{
			print <<HTML;
<link rel="stylesheet" type="text/css" href="$style_sheet">
HTML
		}
		print <<HTML;
<h1>Uploads Not Allowed</h1>
<p>
The site administrator has disabled the upload photo function. Please return to <a href="$albumprog">$shortdesc</a>.
HTML
		exit(0);
	}

	# same as debug=3
	if ($debug gt 2)
	{
		print "Displaying all form values:<p>";
		my @all=$query->param;
		my $name;
		foreach $name (@all)
		{
			print "<pre>Form Data: $name ->", $query->param($name),"</pre><br>\n";
		}
	}

	$object=buildTemplate();

	if ($query->param('upload_file'))
	{

		debug("A photo is being uploaded.",2,__LINE__);

		my $em='';

		# import the paramets into a series of variables in 'q' namespace
		$query->import_names('q');
		#  check if the necessary fields are empty or not
		$em .= "<br>You must specify your Userid!<br>" if !$q::userid;
		if (!$q::upload_password && $authentication_type)
		{
			$em .= "You must specify your Password!<br>"
		}
		$em .= "You must select a file to upload!<br>" if !$q::upload_file;

		print $object;

		if ($em)
		{
			error("upload_error",$em);
		}

		if (!&Authenticate($query->param('userid'),$query->param('upload_password')))
		{
			error("upload_error","Will not upload! Could not validate Userid: $q::userid");
		}

		# now upload file
		fileUpload();
	}
	else
	{
		debug("No photo is being uploaded.",2,__LINE__);

		print $object;
	}
}


##########################################################################

=head3 Authenticate()

 $authenticated=Authenticate($username,$password);

 $username - the username of the user
 $password - the password for the user

Authenticates the username/password against a UBB member. Returns 1 for a successful authentication, otherwise 0.

=cut

sub Authenticate
{
my $username=shift;
my $password=shift;
my $data="";
my $auth="";
my $storedpass="";

	$usernumber="";
	# UBB authorization
	if ($authentication_type eq 2)
	{
		$memberslist="$membersdir/memberslist.cgi";

		open (FILE,"$memberslist") || error("not_readable","$memberslist: $!");
		while ($data=<FILE>)
		{
			if ($data=~/^$username\|/i)
			{
				$usernumber=$data;
				chomp($usernumber);
				$usernumber=~s/.*\|(.*)/$1/;
			}
		}
		close(FILE);

		# Only process if a user was found
		if ($usernumber)
		{
			open (FILE,"$membersdir/$usernumber.cgi") || error("not_readable","$membersdir/$usernumber.cgi: $!");
			$storedpass=<FILE>;
			$storedpass=<FILE>;
			$displayname=<FILE>;
			$displayname=<FILE>;
			$displayname=<FILE>;
			$displayname=<FILE>;
			$displayname=<FILE>;
			$displayname=<FILE>;
			$displayname=<FILE>;
			$displayname=<FILE>;
			$displayname=<FILE>;
			$displayname=<FILE>;
			$displayname=<FILE>;
			$displayname=<FILE>;
			$displayname=<FILE>;
			$displayname=<FILE>;
			chomp($storedpass);
			close(FILE);

			chomp($displayname);

			if (!$displayname)
			{
				$displayname=$username;
			}
		}
	}

	# YABB authorization
	if ($authentication_type eq 3)
	{
		$memberslist="$membersdir/$username.dat";

		open (FILE,"$memberslist") || error("not_readable","$memberslist: $!");
		$storedpass=<FILE>;
		close(FILE);

		chomp($storedpass);

		if ($password ne $storedpass)
		{
			# Code added from clubSTi.com (Gerlando)
			$pwseed ||= 'ya';
			$password=crypt($password,$pwseed);
		}

		$displayname=$username;
	}

	# Flatfile authorization
	if ($authentication_type eq 1)
	{
		# open the text database
		open(AUTH_DB,$auth_db) || error("open_db","$auth_db");

		# first check if user exist
		while (<AUTH_DB>)
		{
			chomp;
			($data,$storedpass)=split('\|',$_);
			if ($username eq $data)
			{
				last;
			}
		}
		close(AUTH_DB);
		$displayname=$username;
	}

	# Anonymous
	if (!$authentication_type)
	{
		$storedpass=$password;
		$displayname=$username;
	}

	if ($password eq $storedpass)
	{
		$auth=1;
	}

	debug("Username: $username",3,__LINE__);
	debug("Entered Password: $password",3,__LINE__);
	debug("Stored Password: $storedpass",3,__LINE__);
	if ($authentication_type eq 2)
	{
		debug("Member #: $usernumber",3,__LINE__);
	}
	debug("Authorized: $auth",3,__LINE__);

	return $auth;
}


##########################################################################

=head3 showUploadForm()

 $upload_form=showUploadForm();

 $upload_form - Variable to return completed upload form into.

Returns the upload form, so it can be substituted into the template.

=cut

sub showUploadForm()
{
my $upload_form;

	$upload_form.="<table>\n";

	$upload_form.=$query->start_multipart_form,"\n";

	# userid field
	$upload_form.="<tr>\n";
	$upload_form.="<td align=\"right\">\n";

	# Display name/userid depending on if we're using anonymous uploads or not
	if ($authentication_type)
	{
		$upload_form.="Userid:\n";
	}
	else
	{
		$upload_form.="Name:\n";
	}
	$upload_form.="</td>\n";
	$upload_form.="<td>\n";
	$upload_form.=$query->textfield(-name=>'userid',-size=>20);
	$upload_form.="</td>\n";
	$upload_form.="</tr>\n";

	# Don't display password field if we're using anonymous uploads
	if ($authentication_type)
	{
		# password field
		$upload_form.="<tr>\n";
		$upload_form.="<td align=\"right\">\n";
		$upload_form.="Password:\n";
		$upload_form.="</td>\n";
		$upload_form.="<td>\n";
		$upload_form.=$query->password_field(-name=>'upload_password',-size=>20);
		$upload_form.="</td>\n";
		$upload_form.="</tr>\n";
	}

	# file field
	$upload_form.="<tr>\n";
	$upload_form.="<td align=\"right\">\n";
	$upload_form.="File:\n";
	$upload_form.="</td>\n";
	$upload_form.="<td>\n";
	$upload_form.=$query->filefield(-name=>'upload_file',-size=>30,-maxlength=>80);
	$upload_form.="</td>\n";
	$upload_form.="</tr>\n";

	$upload_form.=<<HTML;
HTML

	# Use "per member" upload directories
#	if ($per_member_upload && ($authentication_type eq 2))
	if ($per_member_upload)
	{
		$upload_form.=<<HTML;
<input type="hidden" name="category" value="per_member_upload">
HTML

	}
	else
	{
		$upload_form.=<<HTML;
<tr>
<td align="right">
Category:</td><td>
<select name="category">
HTML

		$doing_upload=1;
		recursiveScan($rootdir);
		$doing_upload=0;
		$upload_form.=<<HTML;
$object
</select>
</td></tr>
HTML

	}

	$upload_form.=<<HTML;
<tr>
<td align="right">
Title:</td><td>
<input type="text" name="title" SIZE=30 MAXLENGTH=80>
</td></tr>
<tr>
<td align="right">
Description:</td><td>
<textarea rows="3" name="description" cols="45">
</textarea>
</td></tr>
HTML

	$upload_form.="<tr>\n";
	$upload_form.="<td colspan=2 align=\"center\">\n";
	$upload_form.="<hr noshade size=1>\n";
	$upload_form.=$query->submit(-label=>'Upload',-value=>'Upload',-onClick=>"return ValidateAllFields(this.form)"),"\n";
	$upload_form.="</td>\n";
	$upload_form.="</tr>\n";
	$upload_form.=<<HTML;
<input type="hidden" name="function" value="$upload">
<input type="hidden" name="$debug_code" value="$debug">
HTML

	$upload_form.=$query->endform,"\n";

	$upload_form.="</table>\n";

	return($upload_form);
}


##########################################################################

=head3 fileUpload()

 fileUpload();

Handles the nitty gritty file upload stuff.

=cut

sub fileUpload
{
my $bytes_read=0;
my $size='';
my $buff='';
my $start_time,$time_taken;
my $filepath='';
my $filename='';
my $write_file='';

$filepath=$query->param('upload_file');
my $category=$query->param('category');
my $photo_title=$query->param('title');
my $photo_desc=$query->param('description');

	if ($filepath =~ /([^\/\\]+)$/)
	{
		$filename="$1";
	}
	else
	{
		$filename="$filepath";
	}
	# if there's any space in the filename, get rid of them
	$filename=~s/\s+//g;

	# Is $per_member_upload on? If so, change the category, and create the directory if it's not already there.
	if ($category eq "per_member_upload")
	{
		# UBB categories are user numbers, all others are display names
		if ($authentication_type eq 2)
		{
			$category=$usernumber;
		}
		else
		{
			$category=$displayname;
		}

		$dirtomk="$rootdir"."/"."$category";
		if (!(-d $dirtomk))
		{
			mkdir($dirtomk,777) || error("upload_dir","$dirtomk");
			if (!chmod(0777,$dirtomk))
			{
				error("upload_dir","$dirtomk");
			}
		}

		# Update the description for this new album
		updateDesc("$category","$rootdir/",$displayname,"Photos uploaded by $displayname.");
	}

	# Check for any fooling around
	if (!(-d "$rootdir/$category"))
	{
		error("sanity","Naughty naughty! Are you trying to hack me?");
	}

	$write_file="$rootdir"."/"."$category"."/"."$filename";

	debug("Filename=$filename",2,__LINE__);
	debug("Writefile=$write_file",2,__LINE__);

	debug("Checking to see if $write_file is an photo.",2,__LINE__);

	$object=0;

	if (!(isAPhoto($write_file) || (isAMovie($write_file) && $movie_upload)))
	{
		error("upload_error","File $filename is not a supported file type. Please see the upload rules for more information.");
	}

	if (!$upload_overwrite)
	{
		if (-e $write_file)
		{
			error("upload_error","Photo $filename exists, will not overwrite!");
		}
	}

	if (!open(WFD,">$write_file"))
	{
		error("upload_error","Error opening $write_file file for writing");
		return;
	}

	$start_time=time();
	while ($bytes_read=read($filepath,$buff,2096))
	{
		$size+=$bytes_read;
		binmode WFD;
		print WFD $buff;
	}

	debug("size=$size",2,__LINE__);

	close(WFD);

	# Check to see if file exceeds set limit
	if ($size>($upload_size_limit*1024))
	{
		unlink($write_file);
		error("upload_error","Cannot load files larger than $upload_size_limit kilobytes.");
	}

	if ((stat $write_file)[7] <= 0)
	{
		unlink($write_file);
		error("upload_error","Could not upload file: $filename");
		return;
	}
	else
	{
		$time_taken=time()-$start_time;
	print<<HTML;
<center>
<hr noshade size=1 width="90%">
<table>
<tr>
<td>
<table>
<tr align="center">
<td>
Photo <b>$filename</b> of size <b>$size</b> bytes is uploaded successfully!
</td>
</tr>
</table>
</td>
</tr>
</table>
</center>
HTML

	# Update the description for this new photo
	updateDesc($filename,"$rootdir/$category/",$photo_title,"$photo_desc<p><small><small>Uploaded by $displayname.</small></small>");

	# Log upload

	$sysdate=&setDate(time,1);

	open(UPLOADLOG,">>$upload_logfile") || error("cant_append","$upload_logfile: $!");
	print UPLOADLOG "$sysdate	$ENV{'REMOTE_ADDR'}	$q::userid	$category/$filename\n";
	close(UPLOADLOG);

	}
}


##########################################################################

=head3 showFooter()

 $footer=showFooter($show);

 $show - If set to 0, the footer simply returned, if set to 1, it is displayed and returned.

Rerturns or displays the HTML footer for all pages.

=cut

sub showFooter
{
my @allinfo;
my $revdate;
my $show=shift;

if ($create_html_flag)
{
	$revdate=setDate();
	$revdate="Built on $revdate";
}
else
{
	@allinfo=stat($0);
	$revdate=localtime($allinfo[9]);
}

my $footer=<<HTML;
<p>
<small><small><a href="
HTML

if ($create_html_flag)
{
	$footer.="http://perl.cdnarmy.ca/album";
}
else
{
	$footer.="$albumprog?function=about";
}

	$footer.=<<HTML;
">Album V$ver</a> [$revdate]</small></small>
HTML
	if ($show)
	{
		display($footer);
	}
	return($footer);
}


##########################################################################

=head3 setDate()

 $datestr=setDate($convtime,$return_seconds);

 $datestr        - date and time
 $convtime       - unixtime value which is used to set $datestr (optional)
 $return_seconds - 1=YYYY/MM/DD HH:MM:SS, 0=YYYY/MM/DD HH:MM

Converts $convtime into a human readable format and returns it as $datestring.
If $convtime is not provided, the current time is returned. If $return_seconds
has a value, then the time includes seconds.

=cut

sub setDate
{
my $convtime=shift;
my $return_seconds=shift;

	$convtime=time if (!$convtime);

	debug("Convtime: [$convtime]",2,__LINE__);

	($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst)=localtime($convtime);
	$year+=1900;
	$mon++;
	$mday = "0$mday" if ($mday < 10);
	$mon  = "0$mon"  if ($mon < 10);
	$hour = "0$hour" if ($hour < 10);
	$min  = "0$min"  if ($min < 10);
	$sec  = "0$sec"  if ($sec < 10);
	$sysdate="$year/$mon/$mday $hour:$min";

	$sysdate.=":$sec" if ($return_seconds);

	debug("Sysdate: [$sysdate]",2,__LINE__);
	return($sysdate);
}


##########################################################################

=head3 sanityTest()

 sanityTest();

Runs some basic tests to make sure the environment is clean and secure before the script starts. Right now it's pretty lame, but I intend to add some new tests in to make installation/configuration/troubleshooting easier.

=cut

sub sanityTest
{
my $errstr="";

	if (($cfgver ne $ver) && (($form->param('function') ne "$config") && ($form->param('function') ne "$updateconfig")))
	{
		$errstr="Config file is version $cfgver but you are running version $ver of this program. Please fix!";
	}

	# Check for security holes

	# Relative paths
	if ($form->param('album')=~/^\\*\./)
	{
		$errstr="Sorry, you can't use a relative path for the album.";
	}
	if ($form->param('photo')=~/^\\*\./)
	{
		$errstr="Sorry, you can't use a relative path for the photo.";
	}
	if ($form->param('album')=~/^\/*\./)
	{
		$errstr="Sorry, you can't use a relative path for the album.";
	}
	if ($form->param('photo')=~/^\/*\./)
	{
		$errstr="Sorry, you can't use a relative path for the photo.";
	}

	# Special chars
	if ($form->param('album')=~/[|><]/)
	{
		$errstr="Sorry, you can't use special characters in the album name.";
	}
	if ($form->param('photo')=~/[|><]/)
	{
		$errstr="Sorry, you can't use special characters in the photo name.";
	}

	if ($errstr)
	{
		error("sanity","$errstr");
	}
}


##########################################################################

=head3 buildTemplate()

 $object_template=buildTemplate();

 $object_template = variable to pass the template back into

Builds the object template, and returns it.

=cut
sub buildTemplate
{
my $object_template,$shortdesctitle;

	# Album template
	if ($album)
	{
		open(TEMPLATE,"$album_template") || error("not_readable","$album_template: $!");
		debug("Using Template File: [$album_template]",2,__LINE__);
	}

	# Photo template
	if ($photo)
	{
		open(TEMPLATE,"$photo_template") || error("not_readable","$photo_template: $!");
		debug("Using Template File: [$photo_template]",2,__LINE__);
	}

	# Upload Template
	if ($function eq $upload)
	{
		open(TEMPLATE,"$upload_template") || error("not_readable","$upload_template: $!");
		debug("Using Template File: [$upload_template]",2,__LINE__);
	}

	# Rating Template
	if ($function eq $rating_form)
	{
		open(TEMPLATE,"$rating_template") || error("not_readable","$rating_template: $!");
		debug("Using Template File: [$rating_form]",2,__LINE__);
	}

	$object_template=join("",<TEMPLATE>);
	close(TEMPLATE);

	debug("Loaded object_template: [$object_template]",4,__LINE__);

	# Cut out HTML for title
	$shortdesctitle=$shortdesc;
	$shortdesctitle=~s/<([^>]|\n)*>//g;
	if (!$shortdesctitle)
	{
		$shortdesctitle="Photo Album";
	}

	# Substitute tags for actual data
	$object_template=substituteData($object_template);

		# Add correct style sheet link *** Broken
#		if ($create_html_flag)
#		{
#			$album_header.="$rootdir$style_sheet";
#		}
#		else
#		{
#			$album_header.="$style_sheet";
#		}
#		$album_header.="\">\n";

	return($object_template);
}


##########################################################################

=head3 buildNavFooter()

 $nav_footer=buildNavFooter();

Builds the navigaction footer, and returns it. (The navigation footer is the buttons/links that allow you to visit the next/previous photo or album.)

=cut
sub buildNavFooter
{
my $nav_footer,$temp;

	$nav_footer="<table border=0 cellpadding=0 align=center><tr><td width=\"33%\" align=\"center\">\n";

	# Display the "previous photo" link/button
	if ($prev_obj)
	{
		# Text link and prev photo thumbnail
		if (!$usebuttons || $nextprevthumb)
		{
			$nav_footer.="<a href=\"";
			if (!$create_html_flag)
			{
				$temp=webifyLinks("$realgoback/$prev_obj");
				$nav_footer.="$albumprog?photo=$temp";
				$nav_footer.=passVars(0);
			}
			else
			{
				# Check to see if it's a photo or an album
				if (isAPhoto($prev_obj))
				{
					$nav_footer.="$prev_obj.html";
				}
				else
				{
					$nav_footer.="$prev_obj/$static_html_filename";
				}
			}
			$nav_footer.="\">";

			# Show previous photo thumbnail
			if ($nextprevthumb && $photo)
			{
				$temp=$static_photos_thumb;
				$nav_footer.=showThumb($prev_obj);
				$static_photos_thumb=$temp;

				# Align description
				if ($descloc eq 1)
				{
					$nav_footer.="<br>\n";
				}
			}
			if (!$usebuttons)
			{
				if ($prev_obj_desc)
				{
					$nav_footer.="$prev_obj_desc";
				}
				else
				{
					$nav_footer.="Previous";
				}
			}
			$nav_footer.="</a>\n";
		}

		if ($usebuttons)
		{
			$nav_footer.="<form method=\"POST\" action=\"$albumprog\">\n";
			$nav_footer.="<input type=\"submit\"";
		}
		if ($usebuttons)
		{
			$nav_footer.=" value=\"<< ";
			if ($prev_obj_desc)
			{
				$nav_footer.="$prev_obj_desc";
			}
			else
			{
				$nav_footer.="Previous";
			}
			$nav_footer.="\">\n<input type=\"hidden\" name=\"";
			if ($photo)
			{
				$nav_footer.="photo";
			}
			if ($album)
			{
				$nav_footer.="album";
			}
			$nav_footer.="\" value=\"";
			if ($realgoback)
			{
				$nav_footer.="$realgoback/";
			}
			$nav_footer.="$prev_obj\">\n";
		}
		$nav_footer.=passVars(1);

		if ($usebuttons)
		{
			$nav_footer.="</form>";
		}
	}

	$nav_footer.="</td><td width=\"33%\" align=\"center\">\n";

	# Display the "back to album" link/button
	if ($middle)
	{
		if ($usebuttons)
		{
			$nav_footer.="<form method=\"POST\" action=\"$albumprog\">\n";
			$nav_footer.="<input type=\"submit\"";
		}
		else
		{
			$nav_footer.="<a href=\"";
		}
		if ($create_html_flag)
		{
			if ($album)
			{
				$nav_footer.="../$static_html_filename";
			}
			if ($photo)
			{
				$nav_footer.="./$static_html_filename";
			}
		}
		if ($usebuttons)
		{
			$nav_footer.=" value=\"";
			if ($back_desc)
			{
				$nav_footer.="$back_desc";
			}
			else
			{
				$nav_footer.="Back To Album";
			}
			$nav_footer.="\">\n<input type=\"hidden\" name=\"album\" value=\"";
			$nav_footer.="$goback";
			$nav_footer.="\">\n";
		}
		if (!$create_html_flag && !$usebuttons)
		{
			$nav_footer.="$albumprog?album=";
			$nav_footer.="$goback";
		}
		$nav_footer.=passVars(1);

		if ($usebuttons)
		{
			$nav_footer.="</form>";
		}
		else
		{
			$nav_footer.="\">\n";
			if ($back_desc)
			{
#				if ($create_html_flag)
#				{
#					$nav_footer.="Back To Album";
					#### TODO This doesn't work for static, for some reason...
#				}
#				else
#				{
					$nav_footer.="$back_desc";
#				}
			}
			else
			{
				$nav_footer.="Back To Album";
			}
			$nav_footer.="</a>\n";
		}
	}

	$nav_footer.="</td><td width=\"33%\" align=\"center\">\n";

	# Display the "next photo" link/button
	if ($next_obj)
	{

		# Text link and next photo thumbnail
		if (!$usebuttons || $nextprevthumb)
		{
			$nav_footer.="<a href=\"";
			if (!$create_html_flag)
			{
				$temp=webifyLinks("$realgoback/$next_obj");
				$nav_footer.="$albumprog?photo=$temp";
				$nav_footer.=passVars(0);
			}
			else
			{
				if (isAPhoto($next_obj))
				{
					$nav_footer.="$next_obj.html";
				}
				else
				{
					$nav_footer.="$next_obj/$static_html_filename";
				}
			}
			$nav_footer.="\">\n";

			# Show next photo thumbnail
			if ($nextprevthumb && $photo)
			{
				$temp=$static_photos_thumb;
				$nav_footer.=showThumb($next_obj);
				$static_photos_thumb=$temp;

				# Align description
				if ($descloc eq 1)
				{
					$nav_footer.="<br>\n";
				}
			}
			if (!$usebuttons)
			{
				if ($next_obj_desc)
				{
					$nav_footer.="$next_obj_desc";
				}
				else
				{
					$nav_footer.="Next";
				}
			}
			$nav_footer.="</a>\n";
		}

		if ($usebuttons)
		{
			$nav_footer.="<form method=\"POST\" action=\"$albumprog\">\n";
			$nav_footer.="<input type=\"submit\"";
		}
		if ($usebuttons)
		{
			$nav_footer.=" value=\"";
			if ($next_obj_desc)
			{
				$nav_footer.="$next_obj_desc";
			}
			else
			{
				$nav_footer.="Next";
			}
			$nav_footer.=" >>\">\n<input type=\"hidden\" name=\"";
			if ($photo)
			{
				$nav_footer.="photo";
			}
			if ($album)
			{
				$nav_footer.="album";
			}
			$nav_footer.="\" value=\"";
			if ($realgoback)
			{
				$nav_footer.="$realgoback/";
			}
			$nav_footer.="$next_obj\">\n";
		}
		$nav_footer.=passVars(1);

		if ($usebuttons)
		{
			$nav_footer.="</form>";
		}
	}

	$nav_footer.="</td></tr>";

	# Display jump station
	if ($jump_to)
	{
		$nav_footer.=$jump_station;
	}
	$nav_footer.="</table>\n";
}


##########################################################################

=head3 passVars()

 $vars=passVars($format);

 $vars = variable to pass the formed variables back into
 $format = 0: pass in web URL format (&debug=1&configfile=file, etc); 1: pass in button format (<input type="hidden"...)

Sees which variables must be passed (configfile, debug, static, etc), builds the string containing the data, and passes the value back.

=cut
sub passVars
{
my $vars;
my $format=shift;

	if (!$format)
	{
		if ($function)
		{
			$vars.="&function=$function";
		}
		if ($slide_timer)
		{
			$vars.="&slideshow=$slide_timer";
		}

		if ($configfilepassed)
		{
			$vars.="&configfile=$configfile";
		}
		if ($debug)
		{
			$vars.="&$debug_code=$debug";
		}
		if ($password)
		{
			$vars.="&password=$password";
		}
		if ($username)
		{
			$vars.="&username=$username";
		}
	}
	else
	{
		if ($function)
		{
			if ($usebuttons)
			{
				$vars.="<input type=\"hidden\" name=\"function\" value=\"$function\">\n";
			}
			else
			{
				$vars.="&function=$function";
			}
		}
		if ($slide_timer_passed)
		{
			if ($usebuttons)
			{
				$vars.="<input type=\"hidden\" name=\"slideshow\" value=\"$slide_timer\">\n";
			}
			else
			{
				$vars.="&slideshow=$slide_timer";
			}
		}
		if ($configfilepassed)
		{
			if ($usebuttons)
			{
				$vars.="<input type=\"hidden\" name=\"configfile\" value=\"$configfile\">\n";
			}
			else
			{
				$vars.="&configfile=$configfile";
			}
		}
		if ($debug)
		{
			if ($usebuttons)
			{
				$vars.="<input type=\"hidden\" name=\"$debug_code\" value=\"$debug\">\n";
			}
			else
			{
				$vars.="&$debug_code=$debug";
			}
		}
		if ($password)
		{
			if ($usebuttons)
			{
				$vars.="<input type=\"hidden\" name=\"password\" value=\"$password\">\n";
			}
			else
			{
				$vars.="&password=$password";
			}
		}
		if ($username)
		{
			if ($usebuttons)
			{
				$vars.="<input type=\"hidden\" name=\"username\" value=\"$username\">\n";
			}
			else
			{
				$vars.="&username=$username";
			}
		}
	}
	return($vars);
}


##########################################################################

=head3 printHeader()

 $header=printHeader();

 $header - The formed header, with style sheet or body colour tags, is returned.

Prints out either the style sheet or a generic body tag.

=cut
sub printHeader
{
my $html;

	if ($style_sheet)
	{
		$html="<link rel=\"stylesheet\" type=\"text/css\" href=\"$style_sheet\">\n";
	}

	$html.="</head>\n<body bgcolor=\"#cccccc\" text=\"#FFFFFF\"";

	if (!$style_sheet)
	{
		$html.=" link=\"#008000\" vlink=\"#800000\" alink=\"#0000FF\"";
	}
	$html.=">\n";
	return($html);
}


##########################################################################

=head3 updateDesc()

 updateDesc($object,$desc_file_loc,$title,$description);

 $object - The object whose description is being updated
 $desc_file_loc - The location of the descfile to update
 $title - The new title to apply to the object
 $description - The new long description to apply to the object

Updates the $descfile located in $desc_file_loc for the item $object, with the information passed in through the web form.

=cut
sub updateDesc
{
my $tempfilename,$added_desc,$data,$skip;
my $object=shift;
my $desc_file_loc=shift;
my $title=shift;
my $description=shift;

	# Yep, so update the description file, and then display everything normally
	debug("Updating description for $object in $desc_file_loc. Title is [$title], description is [$description].",2,__LINE__);

	if (!$object || !$desc_file_loc)
	{
		error("sanity","Not enough info to update description.");
	}

	# Open the description file specified
	openDescfile($desc_file_loc);

	$tempfilename=$desc_file_loc."$descname.$$";
	# Open a temp file for writing
	open(TEMPFILE,">$tempfilename") || error("tempdesc_not_writable","$tempfilename");

	# have not added desc
	$added_desc=0;

	# am not skipping
	$skip=0;

	while ($data=<DESC>)
	{
		chomp($data);
		debug("Data: [$data]",4,__LINE__);

		# Check to see if this is the one we want
		if ($data eq $object)
		{
			debug("Found match: $data",4,__LINE__);
			print TEMPFILE $object;
			print TEMPFILE "\n";
			print TEMPFILE $title;
			print TEMPFILE "\n";
			if ($description)
			{
				print TEMPFILE $description;
				print TEMPFILE "\n";
			}

			# skip reading until the next $desc_delim
			$skip=1;

			# don't re-add the desc at the end
			$added_desc=1;
		}

		# we found the next entry, stop skipping
		if ($data eq $desc_delim)
		{
			debug("Stopped skipping.",4,__LINE__);
			$skip=0;
		}

		# Only write if we're not skipping
		if (!$skip)
		{
			debug("Wrote: $data",4,__LINE__);
			print TEMPFILE "$data\n";
		}
	}

	# If the description wasn't added, do it now.
	if (!$added_desc)
	{
		debug("No description found, adding one.",4,__LINE__);
		print TEMPFILE "$desc_delim\n";
		print TEMPFILE $object;
		print TEMPFILE "\n";
		print TEMPFILE $title;
		print TEMPFILE "\n";
		if ($description)
		{
			print TEMPFILE $description;
			print TEMPFILE "\n";
		}
	}

	close(TEMPFILE);
	close(DESC);

	# Replace $descfile with temp file
	unlink($desc_file_loc."$descname");
	rename($tempfilename,$desc_file_loc."$descname");

	# Keep on updating
	$function=$enter_desc;
}


##########################################################################

=head3 trackView()

 $current_views=trackView();

 $current_views - Returns a string containing the number of views and the last viewed date.

Updates the number of views and the last viewed date for the current photo in $viewfile.

=cut
sub trackView
{
my $updated_views,$data,$current_count,$tempfilename,$this_count,$last_viewed_date,$read_date;
my $current_viewfile="$rootdir/";
my $current_view_date=setDate();

	if ($goback)
	{
		$current_viewfile.="$goback/";
	}
	$current_viewfile.="$viewfile";

	debug("Updating views for $shortphoto in $current_viewfile",2,__LINE__);

	# Does it already exist?
	if (-e $current_viewfile)
	{
		# Open $viewfile for reading
		open(VIEWS,"$current_viewfile") || error("not_readable","$current_viewfile");

		# Open a temp file for writing
		$tempfilename="$current_viewfile.$$";
		open(TEMPFILE,">$tempfilename") || error("tempdesc_not_writable","$tempfilename");

		# have not updated views
		$updated_views=0;

		while ($data=<VIEWS>)
		{
			# Check to see if this is the one we want
			if ($data=~/^$shortphoto\t.*/)
			{
				chomp($data);
				debug("Found match: $data",3,__LINE__);
				close(RATINGS);
				($current_count,$current_count,$read_date)=split("\t",$data);

				if ($read_date eq $data || !$read_date)
				{
					$read_date="Never";
				}
				debug("Data: [$data] --> Count:[$current_count] Date:[$read_date]",4,__LINE__);

				$this_count=$current_count;
				$this_count++;
				$last_viewed_date=$read_date;
				print TEMPFILE "$shortphoto\t$this_count\t$current_view_date\n";

				# don't re-add the desc at the end
				$updated_views=1;
			}
			else
			{
				debug("Wrote: $data",4,__LINE__);
				print TEMPFILE "$data";
			}
		}

		# If the photo didn't already hav an entry, add it now.
		if (!$updated_views)
		{
			debug("No entry found, adding one.",2,__LINE__);
			print TEMPFILE "$shortphoto\t1\t$current_view_date\n";
		}

		close(TEMPFILE);
		close(VIEWS);

		# Replace $viewfile with temp file
		unlink($current_viewfile);
		rename($tempfilename,$current_viewfile);
	}
	else
	{
		$last_viewed_date="Never";
		debug("$current_viewfile does not exist, creating...",2,__LINE__);
		open(VIEWS,">$current_viewfile") || error("not_writable","$current_viewfile");
		print VIEWS "$shortphoto\t1\t$current_view_date\n";
		close(VIEWS);
	}

	if (!$this_count)
	{
		$this_count=1;
	}

	$data="Viewed $this_count time";
	if ($this_count gt 1)
	{
		$data.="s";
	}
	$data.=". Last viewed on $last_viewed_date.";

	return($data);
}


##########################################################################

=head3 substituteData()

 $output_data=substituteData($input_data);

 $input_data - Data that is passed in. Each of the "####TAGS####" tags are replaced with actual data. See the file format section of this document for details on the tags.
 $output_data - After substitutions, the data that is returned.

Replaces all the tags in the data passed in, and returns the updated string.

=cut
sub substituteData
{
my $html_data=shift;
my $footer=showFooter(0);
my $legend_html,$notify_html,$add_desc_footer,$slideshow_html,$views,$ratings,$format_html,$upload_form,$insert_file,$temp,$debug_html,$insert_filename;

	debug("Data before substitution: [$html_data]",4,__LINE__);

	# Insert files (this is done first so that all inserted tags are also replaced
	while ($html_data=~/####FILE=(.*)####/)
	{
		$insert_filename=$1;

		# Give full path if we're doing a static load
		if ($create_html_flag)
		{
			$insert_filename="$rootdir/$relalbum/$insert_filename";
		}

		debug("Inserting file: [$insert_filename]",2,__LINE__);
		if (-r $insert_filename)
		{
			open(FILE,$insert_filename);
			$insert_file=join("",<FILE>);
			close(FILE)
		}
		else
		{
			$insert_file="<!-- File ($insert_filename) not found! -->\n";
		}
		$html_data=~s/####FILE=(.*)####/\n<!-- FILE ($insert_filename) tag start -->\n$insert_file\n<!-- FILE ($insert_filename) tag end -->\n/;
	}

	# Perform substitutions of tags for actual data
	$html_data=~s/####TITLE####/$shortdesc/g; # Don't put tags around title, as it screws up the HTML title
#	$html_data=~s/####TITLE####/\n<!-- TITLE tag start -->\n$shortdesc\n<!-- TITLE tag end -->\n/g;
	$html_data=~s/####DESCRIPTION####/\n<!-- DESCRIPTION tag start -->\n$longdesc\n<!-- DESCRIPTION tag end -->\n/g;
	$html_data=~s/####STYLESHEET####/\n<!-- STYLESHEET tag start -->\n<link rel="stylesheet" type="text\/css" href="$style_sheet">\n<!-- STYLESHEET tag end -->\n/g;
	$html_data=~s/####OBJECT####/\n<!-- OBJECT tag start -->\n$actual_object\n<!-- OBJECT tag end -->\n/g;
	$html_data=~s/####NAV####/\n<!-- NAV tag start -->\n$nav_footer\n<!-- NAV tag end -->\n/g;
	$html_data=~s/####FOOTER####/\n<!-- FOOTER tag start -->\n$footer\n<!-- FOOTER tag end -->\n/g;
	$html_data=~s/####SIZE####/\n<!-- SIZE tag start -->\n$upload_size_limit\n<!-- SIZE tag end -->\n/g;
	$html_data=~s/####PAGES####/\n<!-- PAGES tag start -->\n$pages_html\n<!-- PAGES tag end -->\n/g;
	$html_data=~s/####ALBUMPROG####/$albumprog/g; # Don't put tags around albumprob, as it screws up the HTML if it's used in a link

	# Substitute upload formats
	$format_html=join(", ",@imgexts);
	if ($movie_upload)
	{
		if ($imgexts[0])
		{
			$format_html.=", "
		}
		$format_html.=join(", ",@movieexts);
	}
	$format_html=~s/(.*), /$1 or /;

	$html_data=~s/####FORMAT####/\n<!-- FORMAT tag start -->\n$format_html\n<!-- FORMAT tag end -->\n/g;

	# Stop slideshow button
	if ($slide_timer)
	{
		$slideshow_html="<form method=\"POST\" action=\"$albumprog\">\n";
		# Save off slideshow timer
		$temp=$slide_timer;
		$slide_timer=0;
		$slideshow_html.=passVars(1);
		$slide_timer=$temp;
		$slideshow_html.="<input type=\"hidden\" name=\"photo\" value=\"$photo\">\n";
		$slideshow_html.="<input type=\"submit\" value=\"Stop Slideshow\">\n";
		$slideshow_html.="</form>\n";
	}

	$html_data=~s/####STOPSLIDESHOW####/\n<!-- STOPSLIDESHOW tag start -->\n$slideshow_html\n<!-- STOPSLIDESHOW tag end -->\n/g;

	if ($debug)
	{
		$debug_html="<form method=\"POST\" action=\"$albumprog\">\n";
		# Save off slideshow timer
		$temp=$debug;
		$debug=0;
		$debug_html.=passVars(1);
		$debug=$temp;
		if ($photo)
		{
			$debug_html.="<input type=\"hidden\" name=\"photo\" value=\"$photo\">\n";
		}
		if ($album && $shortalbum ne $rootalbumname)
		{
			$debug_html.="<input type=\"hidden\" name=\"album\" value=\"$middle\">\n";
		}
		$debug_html.="<input type=\"submit\" value=\"Stop Debugging\">\n";
		$debug_html.="</form>\n";
	}

	$html_data=~s/####STOPDEBUG####/\n<!-- STOPDEBUG tag start -->\n$debug_html\n<!-- STOPDEBUG tag end -->\n/g;

	# Put slide show directive in
	if ($slide_timer && $next_obj && $photo)
	{
		$slideshow_html="<meta http-equiv=\"Refresh\" content=\"$slide_timer; url=$next_photo_link\">";
	}
	$html_data=~s/####SLIDESHOW####/\n<!-- SLIDESHOW tag start -->\n$slideshow_html\n<!-- SLIDESHOW tag end -->\n/g;

	# Show upload form
	if ($function eq $upload)
	{
		$upload_form=showUploadForm();
	}
	$html_data=~s/####UPLOAD####/\n<!-- UPLOAD tag start -->\n$upload_form\n<!-- UPLOAD tag end -->\n/g;

	# Is this the root album? If so, build legend and notify chunks.
	if (!$middle)
	{
		if ($legend)
		{
			$legend_html="Legend:<br>\n";
			if (-e $file_album_icon)
			{
				$legend_html.="<img src=\"$album_icon\" align=\"middle\"> = Album";
			}
			if (-e $file_photo_icon)
			{
				$legend_html.="&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;";
				$legend_html.="<img src=\"$photo_icon\" align=\"middle\"> = Photo\n";
			}
			if (-e $file_movie_icon)
			{
				$legend_html.="&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;";
				$legend_html.="<img src=\"$movie_icon\" align=\"middle\"> = Movie\n";
			}
		}
		if ($notify)
		{
			$notify_html="If you want to be notified when pictures are added to $shortdesc, please enter your e-mail address below.<br>\n";
			$notify_html.="<form method=\"POST\" action=\"$albumprog\">";
			$notify_html.="<input type=\"text\" name=\"email_notify\" tabindex=\"1\">";
			$notify_html.=" <input type=\"submit\" value=\"Notify Me\" tabindex=\"2\">";
			$notify_html.="</form>\n";
		}
	}
	if ($form->param('email_notify') && $notify)
	{
		open(NOTIFY,">>$notify_file") || error("not_writable","$notify_file");
		print NOTIFY $form->param('email_notify');
		print NOTIFY "\n";
		close(NOTIFY);
		$notify_html.="<center>Your e-mail address (".$form->param('email_notify').") has been added to the list. You will be notified any time pictures are added to the album.<br>\n";
		$notify_html.="</center><hr>\n";
	}

	$html_data=~s/####LEGEND####/\n<!-- LEGEND tag start -->\n$legend_html\n<!-- LEGEND tag end -->\n/g;
	$html_data=~s/####NOTIFY####/\n<!-- NOTIFY tag start -->\n$notify_html\n<!-- NOTIFY tag end -->\n/g;

	# Check to see if we are adding a description
	if ($function eq $enter_desc)
	{
		# Form for entering a new description
		$add_desc_footer="<form method=\"POST\" action=\"$albumprog\">";
		$add_desc_footer.="Enter your new title<br>\n";
		$add_desc_footer.="<input type=\"hidden\" name=\"";
		if ($album)
		{
			$add_desc_footer.="album";
		}
		if ($photo)
		{
			$add_desc_footer.="photo";
		}
		$add_desc_footer.="\" value=\"";
		if (isAPhoto($next_obj))
		{
			$add_desc_footer.="$realgoback/$next_obj";
		}
		else
		{
			$add_desc_footer.="$middle";
		}
		$add_desc_footer.="\">\n";
		$add_desc_footer.="<input type=\"hidden\" name=\"desc_file_loc\" value=\"$desc_to_update\">\n";
		$add_desc_footer.="<input type=\"hidden\" name=\"object\" value=\"$shortobject\">\n";

		# Set the correct function
		$function=$update_desc;
		$add_desc_footer.=passVars(1);

		# Now set it back
		$function=$enter_desc;

		$add_desc_footer.="<input type=\"text\" name=\"title\" size=\"30\" tabindex=\"1\" value=\"$existing_shortdesc\"><p>\n";
		$add_desc_footer.="Enter your new description<br>\n";
		$add_desc_footer.="<textarea rows=\"3\" name=\"description\" cols=\"45\" tabindex=\"2\">$textlongdesc</textarea>\n";
		$add_desc_footer.="<p><input type=\"checkbox\" name=\"stop_add_desc\" value=\"stop\"> Stop entering descriptions\n";
		$add_desc_footer.="<p><input type=\"submit\" value=\"Update Description\">\n";
		$add_desc_footer.="</form>\n";
	}
	$html_data=~s/####ENTERDESC####/\n<!-- ENTERDESC tag start -->\n$add_desc_footer\n<!-- ENTERDESC tag end -->\n/g;

	# Are we tracking the number of photo views?
	if ($viewfile && !$create_html_flag && !$album)
	{
		$views=trackView();
	}
	$html_data=~s/####VIEWS####/\n<!-- VIEWS tag start -->\n$views\n<!-- VIEWS tag end -->\n/g;

	# Are we rating photos?
	if ($ratingfile && !$create_html_flag && !$album)
	{
		$ratings=getRatings();
	}
	$html_data=~s/####RATINGS####/\n<!-- RATINGS tag start -->\n$ratings\n<!-- RATINGS tag end -->\n/g;

	if ($function eq $rating_form)
	{
		$ratings=<<HTML;
<p>
HTML

		$shortphoto=$object;
		if ($ratingfile)
		{
			$ratings.=getRatings(1,$rating_file_loc);
		}

		$ratings.=<<HTML;
<form method="POST" action="$albumprog">
<p>
<SELECT name=rating>
<option value="5">5: Very Good</option>
<option value="4">4: Good</option>
<option  value="3" selected>3: Average</option>
<option value="2">2: Poor</option>
<option value="1">1: Very Poor</option>
</select>
<p>
Name (optional):
<input type="text" name="name">
<p>
<b>Comments (optional):</b>
<br>
<textarea rows="3" name="comments" cols="45">
</textarea>
<p>
HTML
		$function=$update_rating;
		$ratings.=passVars(1);
		$temp=$form->param('rating_file_loc');

		$ratings.=<<HTML;
<input type="hidden" name="rating_file_loc" value="$temp">
HTML
		$temp=$form->param('object');

		$ratings.=<<HTML;
<input type="hidden" name="object" value="$temp">
<input type="SUBMIT" value=" Add Rating ">
</td>
</form>
HTML
	}
	$html_data=~s/####RATING####/\n<!-- RATING tag start -->\n$ratings\n<!-- RATING tag end -->\n/g;


	debug("Data after substitution: [$html_data]",4,__LINE__);

	# Return data
	return($html_data);
}


##########################################################################

=head3 buildObject()

 $output_data=buildObject();

 $output_data - The HTML for displaying the object is build and returned.

Builds the "object" to be displayed. For a photo, it builds the HTML to display the photo, and for an album, it builts the list of objects in that album (photos and sub-albums).

=cut
sub buildObject
{
my $actual_object,$relpath,$multi_page,$num_pics,$num_page_pics,$total_objects,$temppage,$starting_number,$ending_number,$temppage2,$pages_html2;

	if ($function eq "about")
	{
		$actual_object=<<HTML;
</center>
<br>
Version $ver written by Mike Bobbitt ( http://perl.CdnArmy.ca/album ).
<p>
<a href="http://www.hasta-pronto.net/cgi-bin/album/album.pl">zur&uuml;ck zum Album</a>
HTML
		return($actual_object);
	}

	if ($photo)
	{
		# If the next file is also a photo, make a link to it
		if (isAPhoto($next_obj))
		{
			$relpath=$next_obj;

			$relpath=webifyLinks($relpath);

			$actual_object.="<a href=\"";
			if ($create_html_flag)
			{
				# Check to see if it's a photo or an album
				if (isAPhoto($next_obj))
				{
					$actual_object.="$relpath.html";
				}
				else
				{
					$actual_object.="$relpath";
				}
			}
			else
			{
				$relpath="$realgoback/$relpath";

				$relpath=webifyLinks($relpath);

				$actual_object.="$albumprog?photo=$relpath";
			}

			$actual_object.=passVars(0);

			$actual_object.="\">";
		}

		$actual_object.="<img src=\"";
		if ($create_html_flag)
		{
			$actual_object.="$shortphoto";
		}
		else
		{
			$actual_object.="$webroot/$photo";
		}
		$actual_object.="\" border=0 >\n";

		# Close off link for next pic
		if (isAPhoto($next_obj))
		{
			$actual_object.="</a>";
		}
	}

	if ($album)
	{
		$actual_object="<table border=0 cellpadding=$column_spacing align=center><tr>\n";

		# Reset the number of pictures in the current row and on the current page
		$num_pics=$num_page_pics=$multi_page=$total_objects=0;

		$starting_number=$photos_per_page*$page-$photos_per_page;
		$ending_number=$photos_per_page*$page+1;
		debug("Displaying pictures in range: $starting_number to $ending_number",2,__LINE__);

		foreach $shortfile (@file_list)
		{
			# Keep count of total objects
			if ($shortfile)
			{
				$total_objects++;
			}
			debug("\$num_page_pics: $num_page_pics",2,__LINE__);
			debug("\$total_objects: $total_objects (out of $photos_per_page, exclusive range $starting_number to $ending_number)",2,__LINE__);

			# Is this page full?
			if ($shortfile && (!$photos_per_page || (($starting_number < $total_objects) && ($ending_number > $total_objects))))
			{
				debug("\Going to display $shortfile",3,__LINE__);

				getDescription($shortfile);

				$isimage=0;
				$isalbum=0;
				$file="$rootdir";
				if ($middle)
				{
					$file.="/$middle";
				}
				$file.="/$shortfile";

				$webfile="$webroot";
				if ($middle)
				{
					$webfile.="/$middle";
				}
				$webfile.="/$shortfile";

				debug("\$File: $file",3,__LINE__);
				debug("\$WebFile: $webfile",3,__LINE__);

				# Find relative path
				$relpath=$file;
				$relpath=~s/$rootdir\/(.*)/$1/;

				$relpath=webifyLinks($relpath);

				debug("Relpath: $relpath<p>",3,__LINE__);

				# Check to see if file has photo extension
				$isimage=isAPhoto($file);

				# If it's not a photo, check to see if it's a movie
				if (!$isimage)
				{
					$isimage=isAMovie($file);
				}

				debug("Checking to see if $file is a directory",4,__LINE__);
				if (-d "$file")
				{
					debug("$file is a directory",4,__LINE__);
					$isimage=0;
					$isalbum=1;
				}

				# Cheap way to do debug("XX",2,__LINE__);
				if ($debug gt 2)
				{
					display("<pre>UNKNOWN: $file");
					if ($isimage)
					{
						display(" is a photo (so says \$isimage)");
					}
					if ($isalbum)
					{
						display(" is an album (so says \$isalbum)");
					}
					display("</pre>");
				}

				if (!$founddesc)
				{
					$shortdesc=$shortfile;
				}

				# New cell in the table
				$actual_object.="<td>";

				if ($isimage)
				{
					$actual_object.="<center><a href=\"";

					# Keep track of # of pics in this row
					$num_pics++;

					# Keep track of # of pics on this page
					$num_page_pics++;

					# if it's a movie...
					if (isAMovie($file))
					{
						$static_movies_done++;
						if ($create_html_flag)
						{
							$actual_object.="$shortfile";
						}
						else
						{
							$actual_object.="$webroot/$relpath";
						}
					}
					elsif ($create_html_flag)
					{
						$actual_object.="$shortfile.html";
					}
					else
					{
						$actual_object.="$albumprog?photo=$relpath";
					}

					# If you're entering descriptions, keep going.
					$actual_object.=passVars(0);
					$actual_object.="\">\n";

					$actual_object.=showThumb($shortfile);

					# Align description
					if ($descloc eq 1)
					{
						$actual_object.="<br>\n";
					}

					# use $shortfile if no desc avail
					$actual_object.="$shortdesc";

					# Is there a long description too?
					if ($founddesc && $longdesc)
					{
						$actual_object.=" <sup>*</sup>";
					}
					$actual_object.="</a>";

					# Show ratings and long descriptions for movies.
					if (isAMovie($file))
					{
						$actual_object.="<br>\n";
						if ($longdesc)
						{
							$actual_object.="$longdesc<br>\n";
						}
						if ($ratingfile)
						{
							$actual_object.=getRatings();
						}
					}
					$actual_object.="</center>\n";
				}
				if ($isalbum)
				{
					$actual_object.="<center><a href=\"";

					# Keep track of # of pics in this row
					$num_pics++;

					# Keep track of # of pics on this page
					$num_page_pics++;

					if ($create_html_flag)
					{
						$actual_object.="$shortfile/$static_html_filename";
					}
					else
					{
						$actual_object.="$albumprog?album=$relpath";
					}

					# If you're entering descriptions, keep going.
					$actual_object.=passVars(0);

					$actual_object.="\">\n";

					$actual_object.=showThumb($shortfile);

					# Align description
					if ($descloc eq 1)
					{
						$actual_object.="<br>";
					}
					else
					{
						$actual_object.=" ";
					}

					# use $shortfile if no desc avail
					$actual_object.="$shortdesc";
					if ($showcount || $showmoviecount || $showsubalbumcount)
					{
						opendir(ENTRIES,"$file") or error("not_readable","$file");

						# Change Grep
						@count_file_list=grep !/^\.\.?$/,readdir ENTRIES;
						close(ENTRIES);

						$photocount=0;
						$moviecount=0;
						$subalbumcount=0;

						foreach $eachfile (@count_file_list)
						{
							if (isAPhoto($eachfile))
							{
								$photocount++;
							}
							if (isAMovie($eachfile))
							{
								$moviecount++;
							}
							if (-d "$file/$eachfile")
							{
								$subalbumcount++;
							}
						}

						$actual_object.=" <sup>(";
						if ($showcount)
						{
							$actual_object.="$photocount";
						}
						if ($showcount && $showmoviecount)
						{
							$actual_object.=", ";
						}
						if ($showmoviecount)
						{
							$actual_object.="$moviecount";
						}
						if ($showmoviecount && $showsubalbumcount)
						{
							$actual_object.=", ";
						}
						if ($showsubalbumcount)
						{
							$actual_object.="$subalbumcount";
						}
						$actual_object.=")</sup>";
					}
					$actual_object.="</a></center>\n";
				}

				# Close cell in the table
				$actual_object.="</td>\n";

				# Start a new row?
				if ($num_pics eq $columns)
				{
					$num_pics=0;
					$actual_object.="</tr>\n<tr>";
				}
			}
			# Are we looking at multiple pages?
			elsif ($photos_per_page eq $num_page_pics || ($page > 1))
			{
				$multi_page=1;
			}
		}
		$actual_object.="</tr></table>\n";

		# Display total number of pages...
		if ($multi_page)
		{
			$pages_html="";
			$num_page_pics=$total_objects;
			$temppage=1;
			while ($num_page_pics > 0)
			{
				$num_page_pics-=$photos_per_page;
				$pages_html.=" ";
				if ($page ne $temppage)
				{
					$pages_html.="<a href=\"$albumprog?";
					if ($shortalbum ne $rootalbumname)
					{
						$pages_html.="album=$middle&";
					}
					$pages_html.="page=$temppage";
					$pages_html.=passVars(0);
					$pages_html.="\">";
				}
				$pages_html.="$temppage";
				if ($page ne $temppage)
				{
					$pages_html.="</a>";
				}
				$temppage++;
			}
			$temppage--;

			debug("Total pages: $temppage",2,__LINE__);
			debug("Current page: $page",2,__LINE__);

			# Print "Previous" page link
			if (($temppage > 1) && ($page > 1))
			{
				$pages_html2.=" << <a href=\"$albumprog?";
				if ($middle)
				{
					$pages_html2.="album=$middle&";
				}
				$temppage2=$page-1;
				$pages_html2.="page=$temppage2";
				$pages_html2.=passVars(0);
				$pages_html2.="\">";
				$pages_html2.="Previous</a>";
				$pages_html="$pages_html2 $pages_html";
			}

			# Print "Next" page link
			if (($temppage > 1) && ($page < $temppage))
			{
				$pages_html.=" <a href=\"$albumprog?";
				if ($middle)
				{
					$pages_html.="album=$middle&";
				}
				$temppage2=$page+1;
				$pages_html.="page=$temppage2";
				$pages_html.=passVars(0);
				$pages_html.="\">";
				$pages_html.="Next</a> >> ";
			}
			$pages_html="Page: $pages_html";
		}
	}

	return($actual_object);
}


##########################################################################

=head3 parseLinks()

 $object_template=parseLinks($object_template);

 $object_template - Data is passed in, made web safe, and passed out.

Checks the data passed and converts special characters within any links into web safe characters.

=cut
sub parseLinks
{
my $object_template=shift;

	# Replace any spaces in the links to %20
	while ($object_template=~s/ (src|href)="([^"]*?) ([^"]*)"/ $1="$2%20$3"/g)
	{
	}

	return($object_template);
}


##########################################################################

=head3 showThumb()

 $thumbnail_html=showThumb($object);

 $object - Object to search for thumbnail of.
 $thumbnail_html - HTML for the thumbnail.

Returns the proper HTML for the thumbnail of the passed object, honouring all configfile settings.

=cut
sub showThumb
{
my $object=shift;
my $thumbnail_html,$object_no_ext,$imgext;
my $thumb_album=$album;
my $middlebit=$middle;

	debug("showthumb($object) called.",4,__LINE__);

	if ($photo)
	{
		$isimage=1;
		$thumb_album=$dir_to_read;
		$middlebit=$goback;
	}

	if ($isimage)
	{
		# Set photo thumbnail to search for.
		$object_no_ext=$object;
		$object_no_ext=~s/(.+)\..+/$1/;
	}
	if ($isalbum)
	{
		$object_no_ext=$object;
	}

	foreach $imgext (@imgexts)
	{
		# Check upper case extensions
		$imgext="\U$imgext";
		if (-e "$thumb_album/$thumbprefix$object_no_ext.$imgext")
		{
			$alb_thumb_ext=$imgext;
		}

		# Check lower case extensions
		$imgext="\L$imgext";
		if (-e "$thumb_album/$thumbprefix$object_no_ext.$imgext")
		{
			$alb_thumb_ext=$imgext;
		}
	}

	if ($alb_thumb_ext)
	{
		$image_thumb="$thumb_album/$thumbprefix$object_no_ext.$alb_thumb_ext";
		debug("Looking for thumbnail ($image_thumb) ([$thumb_album][/][$thumbprefix][$object_no_ext][.][$alb_thumb_ext])",2,__LINE__);
	}
	else
	{
		debug("No file thimbnail found.",2,__LINE__);
	}
	if (($icons && ((-e $file_photo_icon && $photo) || (-e $file_album_icon && $thumb_album))) || (-e "$image_thumb") || $constrain_thumbs)
	{
		$thumbnail_html.="<img src=\"";

		# Found a thumbnail for this specific image
		if (-e "$image_thumb")
		{
			if ($create_html_flag)
			{
				$thumbnail_html.="$thumbprefix$object_no_ext.$alb_thumb_ext";
			}
			else
			{
				$thumbnail_html.="$webroot/$middlebit/$thumbprefix$object_no_ext.$alb_thumb_ext";
			}
			if ($isimage)
			{
				if (isAMovie($object))
				{
					$static_movies_thumb++;
				}
				else
				{
					$static_photos_thumb++;
				}
			}
			if ($isalbum)
			{
				$static_albums_thumb++;
			}
		}
		else
		# No specific thumbnail found, just use the generic one
		{
			if (isAMovie($object))
			{
				debug("$object is a movie.",2,__LINE__);
				if ($create_html_flag)
				{
					$thumbnail_html.="$file_movie_icon";
				}
				else
				{
					$thumbnail_html.="$movie_icon";
				}
			}
			else
			{
				debug("$object is a photo.",2,__LINE__);
				if ($constrain_thumbs && $isimage)
				{
					if ($create_html_flag)
					{
						$thumbnail_html.="$object";
					}
					else
					{
						$thumbnail_html.="$webroot/$middlebit/$object";
					}
					$thumbnail_html.="\" height=\"$thumb_height\" width=\"$thumb_width";
				}
				else
				{
					if ($create_html_flag)
					{
						if ($isimage)
						{
							$thumbnail_html.="$file_photo_icon";
						}
						if ($isalbum)
						{
							$thumbnail_html.="$file_album_icon";
						}
					}
					else
					{
						if ($isimage)
						{
							$thumbnail_html.="$photo_icon";
						}
						if ($isalbum)
						{
							$thumbnail_html.="$album_icon";
						}
					}
				}
			}
		}
		$thumbnail_html.="\" align=\"middle\" border=\"";
		if ($isalbum && $album_border)
		{
			$thumbnail_html.="$album_border";
		}
		else
		{
			$thumbnail_html.="0";
		}
		$thumbnail_html.="\">";
	}
	return($thumbnail_html);
}


##########################################################################

=head3 showConfig()

 showConfig();

Displays the configuration items from $configfile in a web submittable form.

=cut
sub showConfig
{
my $data,$section,$first_section,$paramsize,$prevline,$var,$value,$line,$help,$sect_header;

	print "<head><title>album.pl - Configuration Management</title>";

	$data=printHeader();

	print <<HTML;
$data
<h1>Configuration Management</h1>
<form method="POST" action="$albumprog">
HTML

	# Open the config file
	open(CONFIG,"$configfile") || error("no_config","$configfile: $!");

	$first_section=1;
	while ($data=<CONFIG>)
	{
		$line++;
		chomp($data);

		# Stript out trailing and leading whitespace
		$data=~s/^\s*(\S*)\s*$/$1/;

		# Read section blocks
		if ($prevline=~/^##+$/)
		{
			$section=$data;
			$section=~s/^# (.*) #$/$1/;

			if ($first_section)
			{
				$first_section=0;
			}
			else
			{
				print "</table>";
			}

			print <<HTML;
<p>
<h2>$section</h2>
<table border=1 cellpadding="5" cellspacing="0" bordercolor="#111111">
HTML

			$sect_header=1;
		}

		# Display comments as help
		if (($data!~/.*#$/) && ($data=~/^# (.*)/))
		{
			if ($help)
			{
				$help.="<br>";
			}
			$help.="$1";
		}

		# Print section header help, if any
		if ($sect_header && (!$data) && $help)
		{
			print <<HTML;
$help
<p>
HTML
			$sect_header=0;
			$help="";
		}

		# skip comments and blank lines
		if (!(($data eq "") || ($data=~/^#.*/)))
		{

			$sect_header=0;

			if ($data!~/.+=.*/)
			{
				error("conf_error","$line");
			}
			$data=~/(@*\w+)=(.*)/;
			$var=$1;
			$value=$2;
			debug("Var [$var] = [$value]",3,__LINE__);

			$paramsize=length($value)+5;
			print <<HTML;
<tr><td>
<h3>$var</h3>
HTML
			if ($help)
			{
				print <<HTML;
$help
HTML
			}
			print <<HTML;
</td><td>
<input type="text" name="$var" size="$paramsize" value="$value">
</td>
</tr><p>
HTML
			$help="";
		}

		$prevline=$data;
	}

	close(CONFIG);

	$function=$updateconfig;
	$data=passVars(1);
	print <<HTML;
</table>
$data
<p>
HTML

	# If configfile is not writable, then warn user
	if (!-w $configfile)
	{
		print "NOTE: $configfile is not writable! No changes can be made.";
	}
	else
	{
		print <<HTML;
<center>
<input type="submit" value="Apply" name="Apply">
<input type="submit" value="Reload" name="ReloadRequested">
</center>
HTML
	}
	showFooter(1);
	exit(0);
}


##########################################################################

=head3 updateConfig()

 updateConfig();

Updates the settings in $configfile, when the form displayed in showConfig is submitted.

=cut
sub updateConfig
{
my $tempfilename,$data,$var,$value;

	if ($form->param('ReloadRequested'))
	{
		# Refresh config
		showConfig();
		exit(0);
	}

	print "<head><title>album.pl - Configuration Management</title>";

	$data=printHeader();

	print <<HTML;
$data
<h1>Configuration Management</h1>
<p>
Configuration updated.
<p>
Options:
<ul>
<li><a href=\"$albumprog\">View your album</a>
<li><a href=\"$albumprog?function=$config\">Configure more settings</a>
</ul>
HTML

	# Open the configfile
	open(CONFIG,"$configfile") || error("no_config","$configfile: $!");

	$tempfilename="$configfile.$$";
	# Open a temp file for writing
	open(TEMPFILE,">$tempfilename") || error("tempdesc_not_writable","$tempfilename");

	while ($data=<CONFIG>)
	{
		chomp($data);
		if ($data=~/(@*\w+)=(.*)/ && $data!~/^#/)
		{
			$var=$1;
			$value=$form->param($var);
			print TEMPFILE "$var=$value\n";
			debug("Writing updated line: [$var=$value]",3,__LINE__);
		}
		else
		{
			print TEMPFILE "$data\n";
			debug("Writing line: [$data]",4,__LINE__);
		}
	}

	close(TEMPFILE);
	close(CONFIG);

	# Replace $configfile with temp file
	unlink($configfile);
	rename($tempfilename,$configfile);

	showFooter(1);

	# Now exit
	exit(0);
}


##########################################################################

=head3 updateRating()

 updateRating($object,$rating_file_loc,$rating,$comments);

 $object - Object to add a rating to
 $rating_file_loc - Location of the ratings file
 $rating - The numerical rating
 $comments - Comments (optional)

Updates the ratings for the object supplied, using the rating and comments supplied.

=cut
sub updateRating
{
my $object=shift;
my $rating_file_loc=shift;
my $rating=shift;
my $new_comments=shift;
my $updated_ratings,$data,$current_rating,$current_photo,$tempfilename,$comments;
my $current_ratingfile="$rootdir/$rating_file_loc/$ratingfile";

	if ($rating_file_loc eq $rootalbumname)
	{
		$current_ratingfile="$rootdir/$ratingfile";
	}

	debug("Updating ratings for $object in $current_ratingfile",2,__LINE__);

	# Strip out HTML from comments
	$new_comments=~s/<([^>]|\n)*>//g;

	# Stip line breaks out of comments.
	$new_comments=~s/[\n\r]/<br>/g;

	# Does it already exist?
	if (-e $current_ratingfile)
	{
		# Open $viewfile for reading
		open(RATINGS,"$current_ratingfile") || error("not_readable","$current_ratingfile");

		# Open a temp file for writing
		$tempfilename="$current_ratingfile.$$";
		open(TEMPFILE,">$tempfilename") || error("tempdesc_not_writable","$tempfilename");

		# have not updated views
		$updated_ratings=0;

		while ($data=<RATINGS>)
		{
			# Check to see if this is the one we want
			if ($data=~/^$object\t.*/)
			{
				chomp($data);
				debug("Found match: $data",3,__LINE__);
				($num_ratings,$num_ratings,$current_rating,$comments)=split("\t",$data);
				debug("Data: [$data] --> Ratings:[$num_ratings] Rating:[$current_rating] Comments:[$comments]",4,__LINE__);
				$comments="$comments<br>$new_comments";

				# Calculate new average rating
				$current_rating=floor((($current_rating*$num_ratings+$rating)/($num_ratings+1))*100)/100;
				$num_ratings++;
				print TEMPFILE "$object\t$num_ratings\t$current_rating\t$comments\n";
				$updated_ratings=1;
			}
			else
			{
				debug("Wrote: $data",4,__LINE__);
				print TEMPFILE $data;
			}
		}

		# If the photo didn't already hav an entry, add it now.
		if (!$updated_ratings)
		{
			debug("No entry found, adding one.",2,__LINE__);
			print TEMPFILE "$object\t1\t$rating\t$new_comments\n";
		}

		close(TEMPFILE);
		close(RATINGS);

		# Replace $viewfile with temp file
		unlink($current_ratingfile);
		rename($tempfilename,$current_ratingfile);
	}
	else
	{
		debug("$current_ratingfile does not exist, creating...",2,__LINE__);
		open(RATINGS,">$current_ratingfile") || error("not_writable","$current_ratingfile");
		print RATINGS "$object\t1\t$rating\t$new_comments\n";
		close(RATINGS);
	}

	# Return to photo/album
	if (isAPhoto($object))
	{
		$album="";
		$photo="$rating_file_loc/$object";
	}
	else
	{
		$album="$rootdir/$rating_file_loc";
		if ($rating_file_loc eq $rootalbumname)
		{
			$album=$rootdir;
		}
	}
	$function="";
}


##########################################################################

=head3 getRatings()

 $ratings=getRatings($mode,$rating_file_loc);

 $ratings - The string of rating information to insert into the template.
 $mode - 0=Add a link to submit a rating to the returned data. 1=Do not return the link to add a rating.
 $rating_file_loc - Location of ratings file (optional, only used if mode=1).

Returns the ratings information for the given photo.

=cut
sub getRatings
{
my $num_ratings,$current_rating,$tempfilename,$comments;
my $current_ratingfile="$rootdir/";
my $mode=shift;
my $rating_file_loc=shift;
my $data="";

	debug("getRatings called with [$mode] and [$rating_file_loc] as parameters.",2,__LINE__);

	if (isAMovie($file))
	{
		$shortphoto=$shortfile;
		$current_ratingfile.="$middle/";
	}
	elsif ($goback)
	{
		$current_ratingfile.="$goback/";
	}
	elsif ($mode)
	{
		$current_ratingfile.="$rating_file_loc/";
	}
	$current_ratingfile.="$ratingfile";

	if ($rating_file_loc eq $rootalbumname || $shortalbum eq $rootalbumname)
	{
		$current_ratingfile="$rootdir/$ratingfile";
	}

	debug("Getting ratings for $shortphoto in $current_ratingfile",2,__LINE__);

	# Does it already exist?
	if (-e $current_ratingfile)
	{
		# Open $viewfile for reading
		open(RATINGS,"$current_ratingfile") || error("not_readable","$current_ratingfile");

		while ($data=<RATINGS>)
		{
			# Check to see if this is the one we want
			if ($data=~/^$shortphoto\t.*/)
			{
				chomp($data);
				debug("Found match: $data",3,__LINE__);
				close(RATINGS);
				($num_ratings,$num_ratings,$current_rating,$comments)=split("\t",$data);
				debug("Data: [$data] --> Ratings:[$num_ratings] Rating:[$current_rating] Comments:[$comments]",4,__LINE__);
			}
		}

		close(RATINGS);
	}

	if (!$num_ratings)
	{
		$data="Not Rated";
	}
	else
	{
		$data="Rated $num_ratings time";
		if ($num_ratings gt 1)
		{
			$data.="s";
		}
		$data.=", current rating is $current_rating.<br>$comments";
	}

	if (!$mode)
	{
		$data.="<br>[<a href=\"$albumprog?function=$rating_form&rating_file_loc=";
		if (isAMovie($file))
		{
			$data.="$middle";
		}
		else
		{
			$data.="$goback";
		}
		$data.="&object=";

		# For movies, the object is not the current object, but the object being added to the album. Whew!
		if (isAMovie($file))
		{
			$data.="$shortfile";
		}
		else
		{
			$data.="$shortobject";
		}
		$data.=passVars(0);
		$data.="\">Rate it</a>]";
	}

	$data="<small><small>$data</small></small>";

	debug("Returning rating of [$data]",4,__LINE__);

	return($data);
}


##########################################################################

=head3 webifyLinks()

 $output=webifyLinks($input);

 $input - The string to webify (make web safe).
 $output - Web safe version returned.

Returns the "websafe" version of the passed filename/path.

=cut
sub webifyLinks
{
my $input=shift;

	# Replace any & chars with %26
	$input=~s/&/\%26/g;

	return($input);
}


##########################################################################

=head3 login()

 login();

Displays login page and authenticates user.

=cut
sub login
{
my $authenticated;

		print <<HTML;
<head><title>album.pl - Login Required</title>
HTML
		$object=printHeader();
		print <<HTML;
$object
<table width="100%" height="100%"><tr><td align="center" valign="center">

<table cellpadding="0" cellspacing="0" border="1" width="375" bordercolor="#000000">
<tr>
<td valign="top">
<table width="100%" cellpadding="10" cellspacing="0" border="0" valign="top">
<tr>
<td valign="top">

<table cellpadding="0" cellspacing="0" border="0" width="100%" border="0">
<tr>
<form method="POST" action="$albumprog">
<td valign="top">
HTML
	$authenticated=Authenticate($username,$password);
	if ($password && !$authenticated)
	{
		print <<HTML;
Incorrect password. Try again.
HTML
	}
	else
	{
		print <<HTML;
This album is password protected. Please enter your password to continue.
HTML
	}
	print <<HTML;
<p>
HTML
	if (!$album_password)
	{
		print <<HTML;
<b>Username:</b> <input type="text" name="username" value="" size="10" maxlength="63" />
<br>
HTML
	}
	print <<HTML;
<b>Password:</b> <input type="password" name="password" value="" size="10" maxlength="63" />
<p>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<input type="SUBMIT" value=" LOGIN "><br />
</td>
HTML
	$object=passVars(1);

	print <<HTML;
$object
</form>
<td align="right" valign="top">
</td>
</tr>
</table>

</td>
</tr>
</table>

</td>
</tr>
</table>
</td></tr></table>
HTML
	exit(0);
}

######################## END OF SUBROUTINES ########################

######################## END OF FILE ########################