#!/usr/bin/perl -wT
#/*==========================================================================+
#|  Copyleft 2002, wwwdocshare, Santa Cruz CA
#+===========================================================================+
#|
#|    File Name:   
#|    Description:  This file is a CGI script for the wwwdocshare system
#|                  
#|
#|   Revision History:
#|   Date     Name              Revision Description
#|   ======== ================  =============================================
#|   11/1/02  Ian Brown         Layed out the structure of the program --
#|                              generally reworked everything (trying to
#|                              understand how to make CGI scripts.)
#|
#|   11/1/02  Jason Rohrer      Nearly finished all functionality for
#|                              iteration 1.  Moved all HTML into files
#|                              that are read by the script.
#|                              Fixed taint issues in performUpdate.
#|                              Fixed modtime race condition.
#|                              Changed so that file permissions are mirrored
#|                              between repositories.
#|                              Forced paths to end with a trailing /
#|
#|   11/5/02  Jason Rohrer      Fixed so that duplicate author adds are 
#|                              ignored.
#|
#|   11/14/02  Jason Rohrer     Started working on versioning.
#|                              Added version lists.
#|
#|   11/15/02  Jason Rohrer     Added "needs update" flags in file list.
#|                              Fixed crash bugs when author list file does
#|                              not exist.
#|
#|   11/16/02  Jason Rohrer     Improved performance of "needs update" flags
#|                              by fetching the global remote file list once.
#|                              Fixed a few bugs.
#|                              Changed to fetch authors recursively
#|                              when a new author is added.
#|                              Added a fetch=uniqueID method to avoid
#|                              adding ourself to the author list.
#|
#|   11/18/02  Jason Rohrer     Removed links from author list.
#|                              Changed to check against all existing 
#|                              repository unique IDs when adding an author.
#|                              Tagged as iteration 2.
#|                              Added visual locking.
#|                              Added locking semantics.
#|                              Improved lock contention behavior.
#|
#|   11/19/02  Jason Rohrer     Changed to add ourselves to other author's
#|                              author lists.
#|
#|   11/20/02  Jason Rohrer     Changed to trim whitespace on user-supplied
#|                              data strings.
#|                              Added a check for valid author URL on add.
#|
#|   11/21/02  Jason Rohrer     Fixed a bug in query=alive.
#|                              Added all HTML inline.
#|                              Fixed inefficient author addition recursion.
#|
#+==========================================================================*/



###################
#    INCLUDED     #
#    MODULES      #
###################
use strict;
#use CGI qw(:standard);  # procedural
use CGI;                # Object-Oriented
use LWP::Simple;
#use CGI::Carp qw(fatalsToBrowser);
#use LWP::Simple;



###################
#     GLOBAL      #
#    VARIABLES    #
###################
my $config_file = "./repository.ini";     # This repositories config file.
my $authorListFile = "./authorList.ini";
my $uniqueIDFile = "./uniqueID.ini";

my $localRepositoryPath = "";
my $externalRepositoryURL = "";
my $localUniqueID = "";


my $Current_page;                         # The current page

# State table mapping pages to functions
my %States = ( 'Default' => \&configuration_page,
               'Main'    => \&main_page );

#/*==============================================================*\
#|    MAIN:
#|   Description:  This is where the current page is is generated.
#|                 
#|   Notes:   First we decide which page to send.
#|



# Extract possible script parameters
my $cgiQuery = CGI->new();
my $setupDirectory = $cgiQuery->param( "setupDirectory" ) || '';
my $externalURL = $cgiQuery->param( "externalURL" ) || '';
my $action = $cgiQuery->param( "action" ) || '';
my $fetch = $cgiQuery->param( "fetch" ) || '';
my $addAuthor = $cgiQuery->param( "addAuthor" ) || '';
my $query = $cgiQuery->param( "query" ) || '';

my $fileVersionList = $cgiQuery->param( "fileVersionList" ) || '';
my $toggleFileLock = $cgiQuery->param( "toggleFileLock" ) || '';

my $localScriptURL = $cgiQuery->url();


# flag for showing the main page after performing an operation
my $shouldShowMainPage = 0;



# Decide which action to perform based on which parameters are passed in
if( $action eq "update" ) 
{
    loadParameters();
    performUpdate();
    $shouldShowMainPage = 1;
}
elsif( $fetch eq "fileList" ) 
{
    loadParameters();
    printFileList();
}
elsif( $fetch eq "authorList" ) 
{
    printAuthorList();
}
elsif( $fetch eq "uniqueID" ) 
{
    printUniqueID();
}
elsif( $query eq "alive" ) 
{
    print $cgiQuery->header( 'text/plain' );
    print "yes";
}
elsif( $fileVersionList ne "" ) 
{
    loadParameters();
    showFileVersionList( $fileVersionList );
}

elsif( $toggleFileLock ne "" ) 
{
    loadParameters();
    toggleFileLock( $toggleFileLock );
    $shouldShowMainPage = 1;
}
elsif( $addAuthor ne "" ) 
{
    loadParameters();
    addAuthor( $addAuthor );
    $shouldShowMainPage = 1;
}
elsif( $setupDirectory ne "" and $externalURL ne "" )
{
    setupRepository( $setupDirectory, $externalURL );
    $shouldShowMainPage = 1;
}
else 
{
    # no parameters, so show main page
    $shouldShowMainPage = 1;
}



# now that we've performed an action, if any, we may need
# to display the main page


if( $shouldShowMainPage ) 
{
    # show main page or config page
    
    # Decide which page to display.
    if( not -e $config_file )
    {
        $Current_page = "Default";
    }
    else
    {
        loadParameters();

        $Current_page = "Main";
    }
    die "No page for $Current_page" unless $States{$Current_page};


    #*-------HTML-------*#
    custom_header();
    $States{"$Current_page"}->();
    custom_footer();
    #*------------------*#

    exit 0;
}
#|
#|    EXIT
#\*==============================================================*/



#/*==============================================================*\
#| header, footer, menu functions...
#|   Description:  Functions that go in every page shoud be put
#|                 in this section.
#|   Notes:   
#|



sub custom_header
{
    # HTTP header.
    print $cgiQuery->header( 'text/html' );
}



sub custom_footer
{

}



sub custom_menu
{
    # not yet used.
}



#|
#\*==============================================================*/



##
# Loads parameters $localRepositoryPath and $externalRepositoryURL
# from $config_file.
##
sub loadParameters
{
    open( FILE, "$config_file" ) or die;
    flock( FILE, 1 ) or die;

    my @lineList = <FILE>;

    my $unsafeLocalRepositoryPath = $lineList[0];
    my $unsafeExternalRepositoryURL = $lineList[1];

    chomp( $unsafeLocalRepositoryPath );
    chomp( $unsafeExternalRepositoryURL );

    # untaint these, letting anything pass through 
    # this is safe because they are script-generated anyway
    my ( $safeLocalRepositoryPath ) = 
        ( $unsafeLocalRepositoryPath =~ /(.*)/ );
    
    my ( $safeExternalRepositoryURL ) = 
        ( $unsafeExternalRepositoryURL =~ /(.*)/ );

    $localRepositoryPath = $safeLocalRepositoryPath;
    $externalRepositoryURL = $safeExternalRepositoryURL;
    
    close FILE;


    open( FILE, "$uniqueIDFile" ) or die;
    flock( FILE, 1 ) or die;

    @lineList = <FILE>;

    $localUniqueID = $lineList[0];
    chomp( $localUniqueID );

    close FILE;
}



#/*==============================================================*\
#|   Page Generation Sub-routines.
#|   Description:  These are the functions that generate each 
#|                 page of the webpage.
#|   Notes:   
#|



##
# Prints configuration page HTML.
# Does not print content type.
##
sub configuration_page
{
    printConfigureHTML();
}



##
# Prints main page HTML.
# Does not print content type.
##
sub main_page
{
    printMainPageHeadHTML();
    
    printHTMLAuthorList();
    
    printMainPageMiddleHTML();
    
    printHTMLFileList();
    
    printMainPageFootHTML();
}



##
# Updates the repository by fetching the globally most recent version
# of each file from the repository set.
#
# This subroutine prints nothing.
##
sub performUpdate
{

    my @fileList = getGlobalRemoteFileList();

    # Note:
    # This is naive and simple.  We potentially do more HTTP gets than
    # necessary
    # We go through the repository list, and for each file in each 
    # repository we fetch the file if it is newer than our local copy.
    #
    # At the end of this process, the local repository will be in a correct
    # state.  However, we may have fetched each file several times in 
    # the worst case (if each repository we checked had an even newer
    # version of the file than the previous repository checked).
    #
    # However, we expect particular files to be modified infrequently,
    # so the aforementioned worst case will rarely occur.
    #
    # Keep this code for now, since it's elegant.

    foreach my $fileItem ( @fileList )
    {
        # each line of file list has URL, modification date, 
        # and permissions
        my @fileParts = split( /\s/, $fileItem );
        
        # this check is redundant, but it will save
        # us checking against the entire file list in compareAndFetch
        if( $fileParts[3] ne "locked" )
        {
            # not locked remotely
            compareAndFetch( $fileParts[0], $fileParts[1], $fileParts[2] );
        }
    }
}



##
# Gets the global remote file list.
#
# @return the global file list as an array, where each element contains
#   the file URL, the modification date, the permission number, the
#   lock flag, and the author number, all separated by whitespace.
##
sub getGlobalRemoteFileList
{
    # starts out empty 
    my @globalArray = ();


    if( not -e "$authorListFile" ) 
    {
        # do nothing
    }
    else
    {
        open( FILE, "$authorListFile" ) or die;
        flock( FILE, 1 ) or die;

        my @authorList = <FILE>;

        close FILE;
    
        my $authorNumber = 1;
        foreach my $authorItem ( @authorList )
        {
            chomp( $authorItem );
            
            my @authorItemParts = split( /\s/, $authorItem );

            # fetch file list from author using HTTP
            my $authorFiles = get( "$authorItemParts[0]?fetch=fileList" );        
            my @fileList = split( /\n/, $authorFiles );
            
            foreach my $fileItem ( @fileList )
            {
                # add each line to our global array, with author number
                push( @globalArray, "$fileItem $authorNumber" );
            
            }
            $authorNumber = $authorNumber + 1;
        }
    }
    
    return @globalArray;
}



##
# Compares a remote file to the local file version by modification date
# and fetches the remote file if it has a later date.
#
# If the remote file is newer, the permissions of the local file
# are set to match those of the remote file.
#
# If the local file is locked, no update is done.
#
# This subroutine prints nothing.
#
# @param0 the URL of the remote file.
# @param1 the modification time since the epoch of the remote file
#         in seconds.
# @param3 the permissions of the remote file.
# @param4 the global remote file list.
#
# Example:
# compareAndFetch( "http://www.cse.ucsc.edu/~ian/project/file.txt", 
#                  "967196166", "33152" );
## 
sub compareAndFetch
{
    ( my $remoteFileURL, my $remoteModTime, my $remotePermissions, 
      my @fileList ) = @_;
    
    # use regexp to untaint and extract last portion of the path
    my ( $safeRemoteFileName ) = ( $remoteFileURL =~ /(\w+\.\w+)$/ );

    if( isLockedRemotely( $safeRemoteFileName, @fileList ) or
        isLockedLocally( $safeRemoteFileName ) )
    {
        # a remote or local lock exists, do nothing.
        return;
    }

    my $localFile = "$localRepositoryPath$safeRemoteFileName";

    # untaint remote mod time too
    my ( $safeRemoteModTime ) = ( $remoteModTime =~ /(\d+)/ );
    
    # untaint permissions too
    my ( $safeRemotePermissions ) = ( $remotePermissions =~ /(\d+)/ );

    if( not -e "$localFile" )
    {
        # always fetch the file if it doesn't exist
        my $fileContent = get( "$remoteFileURL" );
        
        writeFile( "$localFile", $fileContent );

        # set mod time of file to remote mod time to avoid
        # update race conditions
        utime( $safeRemoteModTime, $safeRemoteModTime, "$localFile" );

        # set permissions on file
        chmod( $safeRemotePermissions, "$localFile" );
    }
    else
    {

        my @statList = stat( "$localFile" );
        my $localModTime = $statList[9];
        
        if( $localModTime < $safeRemoteModTime ) 
        {
            # save a backup version
            saveFileVersion( "$localFile" );

            # fetch the remote file, since newer
            my $fileContent = get( "$remoteFileURL" );
        
            writeFile( "$localFile", $fileContent );
            
            # set mod time of file to remote mod time to avoid
            # update race conditions
            utime( $safeRemoteModTime, $safeRemoteModTime, "$localFile" );

            # set permissions on file
            chmod( $safeRemotePermissions, "$localFile" );
        }
    }
}



##
# Gets whether file is locked locally.
#
# This subroutine prints nothing.
#
# @param0 the name of the file.
#
# @return 1 if locked, 0 if not locked.
##
sub isLockedLocally
{
    my $fileName = $_[0];
    
    if( stat( "$localRepositoryPath/WDS/locks/$fileName.lock" ) ) 
    {
        return 1;
    }
    else
    {
        return 0;
    }
}



##
# Gets whether file is locked remotely by any author.
#
# This subroutine prints nothing.
#
# @param0 the name of the file.
# @param1 the global remote file list.
#
# @return 1 if locked, 0 if not locked.
##
sub isLockedRemotely
{
    ( my $fileName, my @fileList ) = @_;
    
    my $lockedRemotely = 0;
            
    foreach my $fileItem ( @fileList )
    {
        # each line of file list has URL, modification date, 
        # and permissions
        my @fileParts = split( /\s/, $fileItem );

        my $remoteFileURL = $fileParts[0];            
        my $remoteLockFlag = $fileParts[3] || '';

        # use regexp to untaint and extract last portion of the path
        my ( $safeRemoteFileName ) = 
            ( $remoteFileURL =~ /(\w+\.\w+)$/ );
                
        if( $fileName eq $safeRemoteFileName ) {
            # a match for our local file name
            if( $remoteLockFlag eq "locked" ) 
            {
                # locked
                $lockedRemotely = 1;
            }
        }
        
    }

    return $lockedRemotely;
}



##
# Prints an "needs update" html flag for a file.
#
# @param0 the name of the file.
# @param1 the global remote file list.
##
sub printNeedsUpdateHTML
{
    ( my $fileName, my @fileList ) = @_;
    
    my $localFile = "$localRepositoryPath$fileName";

    my @statList = stat( "$localFile" );
    my $localModTime = $statList[9];
    
    my $needsUpdate = 0;
    
    if( not isLockedRemotely( $fileName, @fileList ) and
        not isLockedLocally( $fileName ) )
    {
        # not locked locally or remotely
        
        foreach my $fileItem ( @fileList )
        {
            # each line of file list has URL, modification date, 
            # and permissions
            my @fileParts = split( /\s/, $fileItem );

            my $remoteFileURL = $fileParts[0];            
            my $remoteModTime = $fileParts[1];
            my $remoteLockFlag = $fileParts[3];
            
            # use regexp to untaint and extract last portion of the path
            my ( $safeRemoteFileName ) = 
                ( $remoteFileURL =~ /(\w+\.\w+)$/ );
                
            if( $fileName eq $safeRemoteFileName ) {
                # a match for our local file name
                
                if( $remoteLockFlag ne "locked" and 
                    $remoteModTime > $localModTime ) 
                {
                    # a newer file that is not locked remotely
                    $needsUpdate = 1;
                }
            }
            
        }
    }


    if( $needsUpdate ) 
    {
        print "<FONT COLOR=#FF0000>*</FONT>";
    }
    else
    {
        print "<FONT COLOR=#808080>-</FONT>";
    }
}



##
# Prints a locking UI in html for a file.
#
# @param0 the name of the file.
# @param1 the global remote file list.
##
sub printLockHTML
{
    ( my $fileName, my @fileList ) = @_;
    
    my $lockedRemotely = 0;
            
    foreach my $fileItem ( @fileList )
    {
        # each line of file list has URL, modification date, 
        # and permissions
        my @fileParts = split( /\s/, $fileItem );

        my $remoteFileURL = $fileParts[0];            
        my $remoteModTime = $fileParts[1];
        my $remoteLockFlag = $fileParts[3] || '';
        my $authorNumber = $fileParts[4];

        # use regexp to untaint and extract last portion of the path
        my ( $safeRemoteFileName ) = 
            ( $remoteFileURL =~ /(\w+\.\w+)$/ );
                
        if( $fileName eq $safeRemoteFileName ) {
            # a match for our local file name
            if( $remoteLockFlag eq "locked" ) 
            {
                # locked
                print "locked by [$authorNumber]";
                $lockedRemotely = 1;
            }
        }
        
    }
    
    if( not $lockedRemotely ) 
    {
        # check if locked locally


        my $localLockFile = "$localRepositoryPath/WDS/locks/$fileName.lock";
        
        
        # the link is the same (toggle) for either locked or unlocked state
        print "<A HREF=\"wds.pl?toggleFileLock=$fileName\">";
        if( stat( $localLockFile ) )
        {
            print "unlock";
        }
        else 
        {
            print "lock";
        }
        print "</A>";
    }
}



##
# Prints the local file list as plain text, including the HTTP content header.
#
# For each file, the text file list include the file's external URL,
# the modification date, and the permissions (all on one line).
## 
sub printFileList
{
    print $cgiQuery->header( 'text/plain' );
    opendir( DIR, "$localRepositoryPath" ) or die;    

    my @dirList = readdir DIR;

    foreach my $dirItem ( @dirList ) 
    {
        # skip special files and file names with weird characters  
        if( $dirItem ne "." and $dirItem ne ".." and 
            $dirItem ne "WDS" and $dirItem =~ /^\w+.\w+$/ )
        {
            my @statList = stat( "$localRepositoryPath$dirItem" );
            my $modTime = $statList[9];
            my $permissions = $statList[2];
 
            my $lockedFlag = "unlocked";
            my $lockFilePath = 
                "$localRepositoryPath/WDS/locks/$dirItem.lock";
            if( stat( $lockFilePath ) ) 
            {
                # a lock file exists
                $lockedFlag = "locked";
            }
                

            print "$externalRepositoryURL$dirItem ";
            print "$modTime $permissions $lockedFlag\n";
        }
    }
    
    closedir DIR;
}



##
# Prints the local file list as an HTML table.
##
sub printHTMLFileList
{
    # fetch the global file list once
    my @globalFileList = getGlobalRemoteFileList();

    print "(in $localRepositoryPath)";

    opendir( DIR, "$localRepositoryPath" ) or die;
    
    
    print "<TABLE BORDER=1>\n";

    print "<TR><TD>needs update</TD><TD>file</TD><TD></TD></TR>\n";

    my @dirList = readdir DIR;

    foreach my $dirItem ( @dirList ) 
    {
        # skip special files and file names with weird characters  
        if( $dirItem ne "." and $dirItem ne ".." and 
            $dirItem ne "WDS" and $dirItem =~ /^\w+\.\w+$/ )
        {
            print "<TR>\n<TD ALIGN=CENTER>";
            printNeedsUpdateHTML( $dirItem, @globalFileList );
            print "</TD>\n<TD>\n";
            print "<A HREF=\"$externalRepositoryURL$dirItem\">$dirItem</A>\n";
            print "</TD>\n";
            print "<TD>[<A HREF=\"wds.pl?fileVersionList=$dirItem\">";
            print "versions</A>]</TD>\n";
            print "<TD ALIGN=CENTER>";
            printLockHTML( $dirItem, @globalFileList );
            print "</TD>\n";
            print "</TR>\n";
        }
    }
    
    closedir DIR;

    print "</TABLE>";
}



##
# Prints the author list as plain text, including the HTTP content header.
#
# For each author, the text author list includes the URL of the
# author's wds.pl script.
## 
sub printAuthorList
{
    print $cgiQuery->header( 'text/plain' );
    
    if( not -e "$authorListFile" ) 
    {
        # print nothing... empty author list
    }
    else 
    {
        printFile( "$authorListFile" );
    }
}



##
# Prints the local author list as an HTML table.
##
sub printHTMLAuthorList
{
    
    if( not -e "$authorListFile" ) 
    {
        print "no authors";
    }
    else 
    {
        open( FILE, "$authorListFile" ) or die;
        flock( FILE, 1 ) or die;

        print "<TABLE BORDER=0>\n";

        # for each line in file (an author URL)
        my $authorNumber = 1;
        while( <FILE> )
        {
            # trim the current line of the file
            chomp;

            my @authorItems = split( /\s/, $_ );

            print "<TR><TD>\n";
            
            # $_ is the current line of the file
            print "[$authorNumber] $authorItems[0]";
            
            print "</TD></TR>\n";
            
            $authorNumber = $authorNumber + 1;
        }
        print "</TABLE>\n";
        
        close( FILE );            
    }
}



##
# Prints the uniqueID as plain text, including the HTTP content header.
## 
sub printUniqueID
{
    print $cgiQuery->header( 'text/plain' );
    
    if( not -e "$uniqueIDFile" ) 
    {
        # print nothing... empty unique ID
    }
    else 
    {
        printFile( "$uniqueIDFile" );
    }
}



##
# Prints the version list for a file as an HTML document.
#
# @param0 the name of the file.
##
sub showFileVersionList
{
    my $fileName = $_[0];

    custom_header();
    
    print "<HTML>\n";
    print "<HEAD><TITLE>$fileName Versions</TITLE></HEAD>\n";
    print "<BODY>\n";
    
    print "Past versions for $fileName:<BR><BR>\n";

    opendir( DIR, "$localRepositoryPath/WDS/versions" ) or die;
    

    my @dirList = readdir DIR;

    foreach my $dirItem ( @dirList ) 
    {
        # skip special files and file names with weird characters
        # skip all files that are not version files
        if( $dirItem ne "." and $dirItem ne ".." and 
            $dirItem ne "WDS" and $dirItem =~ /^\w+\.\w+,\d$/ )
        {
            # make sure our dir item is a version for our file
            if( $dirItem =~ /($fileName)./ ) 
            {
                # extract the version number
                my ( $versionNumber ) = ( $dirItem =~ /(\d)$/ );
                
                print "<A HREF=";
                print "\"$externalRepositoryURL";
                print "WDS/versions/$dirItem\">";
                print "version $versionNumber</A><BR>\n";
            }
        }
    }
    closedir DIR;
    
    print "</BODY></HTML>";
    
    custom_footer();
        
}



##
# Toggles the local lock state of a file.
#
# @param0 the name of the file.
##
sub toggleFileLock
{
    my $fileName = $_[0];
    
    my ( $safeFileName ) = ( $fileName =~ /(\w+\.\w+)$/ );

    if( not isLockedRemotely( $fileName, getGlobalRemoteFileList() ) )
    {
        # not locked remotely, so we can lock it

        my $lockFilePath = 
            "$localRepositoryPath/WDS/locks/$safeFileName.lock";

        if( stat( $lockFilePath ) ) 
        {
            # a lock file exists, delete it to unlock
            unlink $lockFilePath;
        }
        else 
        {
            # no file exists, create one to lock
            writeFile( "$lockFilePath", "locked" );
        }
    }
}



##
# Sets up repository.
#
# @param0 the absolute filesystem path of the repository directory.
# @param1 the external URL of the repository.
#
# Example:
# setupRepository( "/cse/grads/rohrer/.html/project/", 
#                  "http://www.cse.ucsc.edu/~rohrer/project/" );
##
sub setupRepository
{
    my $setupDirectory = $_[0];
    my $externalURL = $_[1];

    trimWhitespace( $setupDirectory );
    trimWhitespace( $externalURL );
    
    open( FILE, ">$config_file" ) or die;
    flock( FILE, 2 ) or die;
    
    # make sure both paths end with a trailing "/"
    if( $setupDirectory =~ /\/$/ ) 
    {
        print FILE "$setupDirectory\n";
    }
    else 
    {
        print FILE "$setupDirectory/\n";   
    }
    if( $externalURL =~ /\/$/ )
    { 
        print FILE "$externalURL\n";
    }
    else
    {
        print FILE "$externalURL/\n";
    }

    close FILE;


    # generate and save our unique ID
    my $currentTime = time();
    my $randValue = rand();
    
    my $uniquID = "$currentTime|$randValue";

    open( FILE, ">$uniqueIDFile" ) or die;
    flock( FILE, 2 ) or die;
    
    print FILE "$uniquID\n";
    
    close FILE;
    


    # use regexp to untaint the setup directory
    # we can trust the user here, since mkdir isn't harmful
    # this regexp lets everything pass through
    my ( $safeSetupDirectory ) = ( $setupDirectory =~ /(.*)$/ );

    # make our versions and locks directories
    mkdir( "$safeSetupDirectory/WDS", oct( "0777" ) );
    chmod( oct( "0777" ), "$safeSetupDirectory/WDS" );

    mkdir( "$safeSetupDirectory/WDS/versions", oct( "0777" ) );
    chmod( oct( "0777" ), "$safeSetupDirectory/WDS/versions" );

    mkdir( "$safeSetupDirectory/WDS/locks", oct( "0777" ) );
    chmod( oct( "0777" ), "$safeSetupDirectory/WDS/locks" );
}



##
# Adds an author to the repository.
#
# @param0 the URL of the author's wds.pl script.
# @param1 the unique ID of the author's repository (optional).
#
# Examples:
# addAuthor( "http://www.cse.ucsc.edu/~ian/cgi-bin/project/wds.pl" );
# addAuthor( "http://www.cse.ucsc.edu/~ian/cgi-bin/project/wds.pl",
#            "1000949384|8847773" );
##
sub addAuthor
{
    my $authorURL = $_[0];
    my $remoteUniqueID = $_[1] || '';

    trimWhitespace( $authorURL );

    if( $remoteUniqueID eq '' ) 
    {
        # check whether this author is valid
        my $aliveResponse = get( "$authorURL?query=alive" ) || '';
        
        if( $aliveResponse ne "yes" )
        {
            # not a valid author URL
            # return without doing anything
            return;
        }

        # fetch the remote author's unique ID
        $remoteUniqueID = get( "$authorURL?fetch=uniqueID" );
        chomp( $remoteUniqueID );
    }
    

    # build a hash table of all known authors
    my %existingAuthorHash;
    
    # add our local URL to the hash
    $existingAuthorHash{ $localUniqueID } = $localScriptURL;


    # dump author list into hash table
    
    if( -e "$authorListFile" ) 
    {
        open( FILE, "$authorListFile" ) or die;
        flock( FILE, 1 ) or die;

        # for each line in file (an author URL)
        while( <FILE> )
        {
            # trim the current line of the file
            chomp;

            my @authorParts = split( /\s/, $_ );        

            my $alreadyAddedURL = $authorParts[0];
            my $alreadyAddedUniqueID = $authorParts[1];

            $existingAuthorHash{ $alreadyAddedUniqueID } = $alreadyAddedURL;
             
        }
        
        close( FILE );            
    }
    

    if( not exists $existingAuthorHash{ $remoteUniqueID } ) 
    {   
        # add author to our hash table
        $existingAuthorHash{ $remoteUniqueID } = $authorURL;
        
        # add author to our file
        
        open( FILE, ">>$authorListFile" ) or die;
        flock( FILE, 2 ) or die;
    
        print FILE "$authorURL $remoteUniqueID\n";
        close( FILE );
        
        # add ourselves to this new author
        get( "$authorURL?addAuthor=$localScriptURL" );

        # now fetch the remote author's author list 
        my $remoteAuthorListFile = get( "$authorURL?fetch=authorList" );
        my @remoteAuthorList = split( /\n/, $remoteAuthorListFile );
        
        foreach my $authorItem ( @remoteAuthorList )
        {
            my @authorParts = split( /\s/, $authorItem );

            # check if author's ID already exists
            if( not exists $existingAuthorHash{ $authorParts[1] } ) 
            {
                # unknown author, recursively add this author too
                addAuthor( $authorParts[0], $authorParts[1] );
            }
        }
    }

}



##
# Saves the the current contents of a file to a version file.
#
# @param0 the path to the file.
##
sub saveFileVersion
{
    my $filePath = $_[0];
    
    # use regexp to untaint and extract last portion of the path
    my ( $safeFileName ) = ( $filePath =~ /(\w+\.\w+)$/ );

    my $localFile = "$localRepositoryPath$safeFileName";
    
    my $nextFreeVersionNumberFile = 
        "$localRepositoryPath/WDS/versions/$safeFileName,nextFreeVersion";
    
    # read in the next free version number
    my $nextFreeVersionNumber;
    if( open( FILE, "$nextFreeVersionNumberFile" ) ) 
    {
        flock( FILE, 1 ) or die;

        my @lineList = <FILE>;

        $nextFreeVersionNumber = $lineList[0];

        close FILE;
    }
    else 
    {
        $nextFreeVersionNumber = "0";
    }
    
    # use regexp to untaint and extract last portion of the path
    my ( $safeNextFreeVersionNumber ) = 
        ( $nextFreeVersionNumber =~ /(\w+)$/ );

    # increment the next free version number
    open( FILE, ">$nextFreeVersionNumberFile" ) or die;
    flock( FILE, 2 ) or die;

    my $incrementedVersionNumber = $nextFreeVersionNumber + 1;

    print FILE "$incrementedVersionNumber";

    close FILE;


    # copy the file into the next version file
    my $newVersionFile = 
 "$localRepositoryPath/WDS/versions/$safeFileName,$safeNextFreeVersionNumber";
    
    open( MAIN_FILE, "$localFile" ) or die;
    flock( MAIN_FILE, 1 ) or die;

    open( BACKUP_FILE, ">$newVersionFile" ) or die;
    flock( BACKUP_FILE, 2 ) or die;

    my @lineList = <MAIN_FILE>;
    print BACKUP_FILE @lineList;

    close MAIN_FILE;
    close BACKUP_FILE;    

    chmod( oct( "0777" ), "$newVersionFile" );
}



##
# Prints the contents of a file to standard out.
#
# @param0 the name of the file.
#
# Example:
# printFile( "myFile.txt" );
##
sub printFile
{
    my $fileName = $_[0];
    open( FILE, "$fileName" ) or die;
    flock( FILE, 1 ) or die;

    my @lineList = <FILE>;

    print @lineList;

    close FILE;
}



##
# Writes a string to a file.
#
# @param0 the name of the file.
# @param1 the string to print.
#
# Example:
# writeFile( "myFile.txt", "the new contents of this file" );
##
sub writeFile
{
    my $fileName = $_[0];
    my $stringToPrint = $_[1];

    open( FILE, ">$fileName" ) or die;
    flock( FILE, 2 ) or die;

    print FILE $stringToPrint;

    close FILE;
}



##
# Trims any whitespace from the beginning and end of a string.
#
# @param0 the string to trim.
#
# @return the trimmed string.
##
sub trimWhitespace
{   

    foreach( $_[0] )
    {
        # trim from front of string
        s/^\s+//;

        # trim from end of string
        s/\s+$//;
    }
}



#|
#\*==============================================================*/




#
# These subroutines print various pieces of UI HTML.
#
# None of these send a content header.
#



sub printConfigureHTML
{
	print <<END_OF_HTML;
    
<!--
Modification History

2002-October-14   Jason Rohrer
Created.

2002-November-18   Jason Rohrer
Added explanations of fields.

-->


<HTML>

<HEAD>
<TITLE>wwwdocshare:  Configure</TITLE>
</HEAD>


<BODY>

<CENTER>


<H2>Configure Repository:</H2>

<FORM ACTION="wds.pl">

<TABLE BORDER=0>

<TR>
<TD ALIGN=RIGHT>
Local Path:
</TD>
<TD ALIGN=LEFT>
<INPUT TYPE="text" MAXLENGTH=256 SIZE=55 NAME="setupDirectory" VALUE="/">
</TD>
<TD ALIGN=LEFT>
</TD>
</TR>
<TR>
<TD></TD>
<TD ALIGN=LEFT>(absolute path to project directory)</TD>
<TD></TD>
</TR>

<TR>
<TD ALIGN=RIGHT>
External URL:
</TD>
<TD ALIGN=LEFT>
<INPUT TYPE="text" MAXLENGTH=256 SIZE=55 NAME="externalURL" VALUE="http://">
</TD>
<TD ALIGN=LEFT>
<INPUT TYPE=submit VALUE="submit">
</TD>
</TR>


</TABLE>

</FORM>

<TABLE BORDER=0 WIDTH=70%>
<TR>
<TD>

<B>Local Path:</B>  this is the path to your repository files (the files you want to share) in your local filesystem.  For example:<BR>
<CENTER>
<FONT COLOR=red>/cse/grads/rohrer/.html/tempProjectA</FONT>
</CENTER>
The files in your repository must be web-accessible, and must each map to a valid URL.<BR>
<BR> 

<B>External URL:</B>  this is the URL pointing to the directory described by Local Path.  For example:<BR>
<CENTER>
<FONT COLOR=red>http://www.cse.ucsc.edu/~rohrer/tempProjectA</FONT>
</CENTER>
Maintaining a repository is similar to maintaining a web page:  you access your repository locally using the filesystem, and your collaborators access your  repository remotely using the web.<BR>
<BR>

<B>Permission issues:</B>  like files for any website, files in your project directory must be world-readable.  Since your wds script needs to update these files, they must also be writable by the user that runs CGI scripts (this might be your username on some systems, or "nobody" on other systems).  To be safe, the files in your repository should be world-writable as well.<BR>
<BR>
Also, your wds script needs to be able to write to your repository directory (tempProcjectA, in the example) so that it can add new files created by your collaborators. 

</TD>
</TR>
</TABLE>


</CENTER>

</BODY>

</HTML>


END_OF_HTML
}



sub printMainPageHeadHTML
{
	print <<END_OF_HTML;

<!--
Modification History

2002-November-2   Jason Rohrer
Created.

2002-November-18   Jason Rohrer
Added a refresh link.

-->


<!-- Header -->

<HTML>

<HEAD>
<TITLE>wwwdocshare</TITLE>
</HEAD>


<BODY>

<A HREF="wds.pl">refresh</A>

<CENTER>
<H2>Authors:</H2>

END_OF_HTML
}



sub printMainPageMiddleHTML
{
	print <<END_OF_HTML;

<!--
Modification History

2002-November-2   Jason Rohrer
Created.

-->


<!--Middle -->
</CENTER>

<CENTER>

<FORM ACTION="wds.pl">

<TABLE BORDER=0>
<TR>
<TD ALIGN=RIGHT>
Add Author:
</TD>
<TD ALIGN=LEFT>
<INPUT TYPE="text" MAXLENGTH=256 SIZE=55 NAME="addAuthor" VALUE="http://">
</TD>
<TD ALIGN=LEFT>
<INPUT TYPE=submit VALUE="add">
</TD>
</TR>
<TR>
<TD></TD>
<TD ALIGN=LEFT>(URL of author's wds script)</TD>
<TD></TD>
</TR>
</TABLE>

</FORM>

<HR>


<H2>Shared files:</H2>

END_OF_HTML
}



sub printMainPageFootHTML
{
	print <<END_OF_HTML;

<!--
Modification History

2002-November-2   Jason Rohrer
Created.

-->



<!-- Footer -->

<HR>

<A HREF="wds.pl?action=update">Update All Files</A>

</CENTER>

</BODY>

</HTML>

END_OF_HTML
}





