#!/usr/bin/perl

# Copyright Massachusetts Institute of technology, 2000.
# Written by Eric Prud'hommeaux

# $Id: access,v 1.42 2004/11/11 09:13:02 eric Exp $

#####
# What It Does:


#BEGIN {unshift@INC,('../../..');}
package W3C::Annotations::cgibin::Access;
use CGI;
use strict;
use English; # for less cryptic names for "special" variables

# W3C perl modules
use W3C::Util::Exception;
use W3C::Util::W3CDebugCGI;
use W3C::Util::Properties;
use W3C::Http::Message;
use W3C::Util::Exception qw(&throw &catch &DieHandler);
use W3C::Http::Exception;
use W3C::Annotations::UserRecord
    qw($RW_read $RW_write
       $STATUS_fresh $STATUS_NOT_fresh $OPTIONS_idByEmail $OPTIONS_idByName);

# handy extra data types that need definition before our main class
@RetryException::ISA = qw(W3C::Util::Exception);

use vars qw($REVISION $REVISIONDATE $VERSION @ISA @TODO);
$REVISION = '$Id: access,v 1.42 2004/11/11 09:13:02 eric Exp $ ';
$REVISIONDATE = '$Date: 2004/11/11 09:13:02 $ ';
$VERSION = 0.10;
@TODO = ('add plaintext password', 
	 'test with prompted usernames', 
	 'rename to something without "new" in the name'
	 );

use vars qw($DEFAULT_dbClass $DEFAULT_dbParms 
	    $DEFAULT_serviceLink $DEFAULT_serviceDesc);
$DEFAULT_dbClass = "W3C::Annotations::FlatUserRecords";
$DEFAULT_dbParms = "-file => '/etc/apache/users'";
$DEFAULT_serviceLink = "annotations";
$DEFAULT_serviceDesc = "annotation server";

use vars qw($BUSY_WAIT);
$BUSY_WAIT = 20; # seconds to poll for dbm file write access

# paths
use vars qw($SCRIPT_HOME_URI_PROP);
$SCRIPT_HOME_URI_PROP = 'script.home.uri';

use vars qw($FILE_path $FILE_pending $FILE_users $FILE_groups);
$FILE_path = -d '/etc/apache2/' ? '/etc/apache2/' : -d '/etc/apache/' ? '/etc/apache/' : './';
$FILE_pending = 'pending';
$FILE_users = 'users';
$FILE_groups = 'groups';

use vars qw($GROUP_default $EMAIL_from);
$GROUP_default = 'registered';
$EMAIL_from = 'webmaster+annotea@w3.org';

use vars qw($CONFIG_emailIsUser);
$CONFIG_emailIsUser = 1;

&main();

sub main {
    my ($query, $accessScript);
    eval {
	local($SIG{"__DIE__"}) = \&DieHandler;
	$W3C::Util::W3CDebugCGI::DEBUG_SESSION = $ARGV[1]; # use a session id like 957296047.909868;
	$query = new W3C::Util::W3CDebugCGI($0, $ARGV[0] eq 'DEBUG', 
					    {-storeIn => '/tmp', 
					     -dieNoOpen => 1, -logExt => '.log', 
					     -rerun => 'w3c_rerun', 
					     -reconstruct => 'reconstruct'});
	$accessScript = new W3C::Annotations::cgibin::Access($query);
	my $message = $accessScript->execute();
	print $message->toString();
    }; if ($@) {
	my $sessionId = $query ? $query->getSessionId : undef;
	if (my $ex = &catch('W3C::Http::HttpMessageException')) {
	    my $message = $ex->getHttpMessage();
	    $message->addHeader('Session-Id', $sessionId) if (defined $sessionId);
	    print $message->toString;
	} elsif (my $ex = &catch('W3C::Util::Exception')) {
	    print "Status: 500\nContent-Type: text/html\n\n";
	    my $title = $ex->getMessage;
	    print "<html><head><title>$title</title></head><body>";
	    print "<pre>".$ex->toString."</pre>\n";
	    if ($sessionId) {
		print "<p>Session-id: $sessionId</p>\n";
	    }
	    print "</body></html>";
	} else {
	    print "Status: 500\n\n";
	    print "died with $@";
	    if ($sessionId) {
		print "<p>Session-id: $sessionId</p>\n";
	    }
	}
    }
}

sub new {
    my ($proto, $reader) = @_;
    my $class = ref $proto || $proto;
    my $self = {};
    bless ($self, $class);

    # constructor parameters
    $self->{READ} = $reader;
    $self->{WRITE} = new W3C::Util::W3CDebugCGI(undef, undef, {-noStore => 1});

    $self->{FILE_pending} = File::Spec->catfile($FILE_path, $FILE_pending);
    $self->{FILE_users} = File::Spec->catfile($FILE_path, $FILE_users);
    $self->{FILE_groups} = File::Spec->catfile($FILE_path, $FILE_groups);
    $self->{GROUP_default} = $GROUP_default;
    $self->{EMAIL_from} = $EMAIL_from;

    $self->readProperties();

    # attach to user database
    my $userDatabase = $self->{PROPERTIES}->getI('auth.database.class') || $DEFAULT_dbClass;
    my $parameters = $self->{PROPERTIES}->getI('auth.database.parms') || $DEFAULT_dbParms;
    my $evalMe = "use $userDatabase; \$self->{USER_RECORDS} = new $userDatabase(-errorHandler => \$self, $parameters);";
    eval $evalMe;
    if ($@) {&throw()}

    return $self;
}

sub error {
    my ($self, $exception) = @_;
    my $message = $self->standardError($exception->getMessage);
    &throw(new W3C::Http::HttpMessageException(-httpMessage => $message));
}

sub readProperties {
    my ($self) = @_;
    # load a properties file if available
    eval {
	$self->{PROPERTIES} = new W3C::Util::Properties('annotate.prop');
	$self->{SELF_URI} = $self->{READ}->uriFromProperties($self->{PROPERTIES}, $SCRIPT_HOME_URI_PROP);
	my $filePath = $self->{PROPERTIES}->getI('file.path');
	if (defined $filePath) {
	    $self->{FILE_pending} = $self->constructPath($FILE_pending, $filePath);
	    $self->{FILE_users} = $self->constructPath($FILE_users, $filePath);
	    $self->{FILE_groups} = $self->constructPath($FILE_groups, $filePath);
	} else {
	    $filePath = $FILE_path;
	}
	if (my $file = $self->{PROPERTIES}->getI('file.pending')) {
	    $self->{FILE_pending} = $self->constructPath($file, $filePath);
	}
	if (my $file = $self->{PROPERTIES}->getI('file.users')) {
	    $self->{FILE_users} = $self->constructPath($file, $filePath);
	}
	if (my $file = $self->{PROPERTIES}->getI('file.groups')) {
	    $self->{FILE_groups} = $self->constructPath($file, $filePath);
	}
	if (my $str = $self->{PROPERTIES}->getI('group.default')) {
	    $self->{GROUP_default} = $str;
	}
	if (my $str = $self->{PROPERTIES}->getI('email.from')) {
	    $self->{EMAIL_from} = $str;
	}
	if (my $str = $self->{PROPERTIES}->getI('html.navbar')) {
	    # require quoted string for now; later we might read from files
	    if ( $str =~ m/^".*" *$/ ) {
		$str =~ s/^"(.*)" *$/\1/;
		$self->{HTML_navbar} = $str;
	    }
	}
    }; if ($@) {if (my $ex = &catch('W3C::Util::FileNotFoundException')) {
	$self->{PROPERTIES} = new W3C::Util::Properties();
	$self->{SELF_URI} = $self->{READ}->url;
    } else {&throw()}}
}

sub constructPath {
    my ($self, $file, $filePath) = @_;
    require File::Spec;
    my $ret = $file;
    if (!File::Spec->file_name_is_absolute($ret)) {
	$ret = File::Spec->catfile($filePath, $file);
    }
    if (!File::Spec->file_name_is_absolute($ret)) {
	$ret = File::Spec->catfile($ENV{'HOME'}, $filePath, $file);
    }
    return $ret;
}

sub execute {
    my ($self) = @_;
    my $ret = undef;
    if ($self->{READ}->paramUTF8('editWithPassword')) {
	$ret = $self->parseUserFields(1);
    } elsif ($self->{READ}->paramUTF8('parseUserFields')) {
	$ret = $self->parseUserFields(0);
    } elsif ($self->{READ}->paramUTF8('confirmUser')) {
	my $user = $self->{READ}->paramUTF8('user');
	my $record = $self->accessDBM($self->{FILE_pending}, $user, $RW_read);
	if (defined $record) {
	    $ret = $self->confirmUser();
	} else {
	    $ret = $self->paintUser($user, ['No pending updates.']);
	}
    } elsif ($self->{READ}->paramUTF8('paintConfirm')) {
	my $user = $self->{READ}->paramUTF8('user');
	my $record = $self->accessDBM($self->{FILE_pending}, $user, $RW_read);
	if (defined $record) {
	    my $nonce = $self->{READ}->paramUTF8('nonce');
	    $ret = $self->paintConfirmUser('', $user, $nonce);
	} else {
	    $ret = $self->paintUser($user, ['No pending updates.']);
	}
    } else {
	my $user = $self->{READ}->paramUTF8('user');
	$ret = $self->paintUser($user, []); # not defined for new user
    }
    return $ret;
}

sub makeErrorString {
    my ($self, $errors) = @_;
    if (@$errors > 0) {
	my $heading = @$errors > 1 ? '<h2>Errors:</h2>' : '<h2>Error:</h2>';
	return join ('', ($heading, map {"<p>$_</p>"} @$errors));
    }
    return undef;
}

sub paintUser {
    my ($self, $user, $errors) = @_;
    my $selfUri = $self->{READ}->trimmedPath();
    my $userRecord = new W3C::Annotations::UserRecord();

    # variables constructed from user record
    my ($checked_byEmail, $checked_byName, $checked_byBoth);

    # get existing user record if there is one
    if (defined $user) {
	if (my $record = $self->accessDBM($self->{FILE_users}, $user, $RW_read)) {
	    $userRecord = W3C::Annotations::UserRecord::parseUserEntry($user, $record);
	    if (($userRecord->getOptions & $OPTIONS_idByEmail) == $OPTIONS_idByEmail) {
		if (($userRecord->getOptions & $OPTIONS_idByName) == $OPTIONS_idByName) {
		    $checked_byBoth = 'checked="yes"';
		} else {
		    $checked_byEmail = 'checked="yes"';
		}
	    } elsif (($userRecord->getOptions & $OPTIONS_idByName) == $OPTIONS_idByName) {
		$checked_byName = 'checked="yes"';
	    }
	} else {
	    my $message = $self->standardError("There is no record for user \"$user\".");
	    &throw(new W3C::Http::HttpMessageException(-httpMessage => $message));
	}
    }

    my $tGiven = $userRecord->getGiven;
    my $tFamily = $userRecord->getFamily;
    my $tEmail = $userRecord->getEmail;
    my $tUser = $userRecord->getUser;

    # assign defaults for form input
    my $userDisabled = $user ? 'disabled="disabled"' : '';
    my $hiddenUser = $user ? "<input type=\"hidden\" name=\"user\" value=\"$tUser\">\n" : '';
    my $optionalUser = '';
    my $emailDisabled = '';
    if (!$CONFIG_emailIsUser) {
	$optionalUser = 
	    "user name: <input type=\"text\" name=\"user\" value=\"$tUser\" $userDisabled/><br />\n";
	$emailDisabled = $user ? 'disabled="disabled"' : '';
    }

    # build strings for the message body
    my $passwordInput = '';
    if ($userRecord->getHashed) {
	$passwordInput = <<EOF
<p>
You are manipulating an existing user authorization. You may use the
current password to make user record changes, thereby avoiding a wait
for a token to be sent via email.  If you do not remember the current
password, simply use the 'Submit' button above and a one-time token
will be mailed to the email address above.</p>
<input type="password" name="curPassword" value="" />
$hiddenUser<input type="submit" name="editWithPassword" value="edit with current password" />
EOF
    ;
    }

    my $optionsBoth = $OPTIONS_idByEmail | $OPTIONS_idByName;
    my $errorString = $self->makeErrorString($errors);
    my ($serviceLink, $serviceDesc) = $self->getService();

    # build message body
    my $body = <<EOF
$errorString
<form action="$selfUri" method="post" accept-charset="utf-8">
    <p>You may request authorization to access the
       <a href="$serviceLink">$serviceDesc</a> by completing this form.</p>
    <h3>Please tell us who you are</h3>

    <p>We require that you tell us your name and email address; these
       will be associated with every annotation you store in our service.
       (You tell us below which of these to return when the database
	is queried.)  The email address you specify is the username
	you will enter when your browser prompts for a username and
	password.</p>

    <p>$optionalUser
       given name: <input type="text" name="given" value="$tGiven" /><br />
       family name: <input type="text" name="family" value="$tFamily" /><br />
       email address: <input type="text" name="email" value="$tEmail" $emailDisabled/></p>

    <p>You may choose your own password to be used with the username/email:</p>

    <p>
       password: <input type="password" name="password" value="" /><br />
       confirm password: <input type="password" name="passwordConfirm" value="" /></p>

    <h3>Identification preferences</h3>

    <p>Your name and email address are part of the annotation
       property data that we store in the database.  As a condition
       for use of this service you agree to accurately identify
       yourself in all annotations you make.  If you wish, you
       may have the service omit either your name or your email
       information when it serves the annotations you author.
       If you choose to omit either one you are explicitly granting
       us permission to include the other in the annotation data
       we serve.</p>

    <p>W3C may identify my annotations by:<br />
<input type="radio" name="prefsIdent" value="$OPTIONS_idByEmail" $checked_byEmail/> email only <br />
<input type="radio" name="prefsIdent" value="$OPTIONS_idByName" $checked_byName/> name only<br />
<input type="radio" name="prefsIdent" value="$optionsBoth" $checked_byBoth/> both name and email</p>

    <p>By submitting this request for access to the W3C public
       annotation test service, you indicate your agreement with the
       <a href="/policy.html">W3C Public Annotea Service Acceptable
	Use and Privacy Policy</a>.  After you submit this request,
       a message with a one-time token will be sent to the email
       address you entered above.  The message will also
       contain a link to a form similar to this form in which you
       must enter this one-time token.  Your access will be enabled
       after you complete that second form with the proper token.</p>

    <p>Thank you for your interest in the Annotea service.</p>

    <p><input type="submit" name="parseUserFields" value="Submit" /><input type="reset" name="reset" />$passwordInput</p>
</form>
EOF
    ;
    my $title = $user ? "Edit User $user" : 'New User Access Request';
    return $self->standardMessage(200, $title, $body);
}

sub parseUserFields {
    my ($self, $expectCurPassword) = @_;
    my @errors = ();
    my $userRecord = new W3C::Annotations::UserRecord();

    my ($password, $authPassword);

    # map from user information to the account name
    my $user = $CONFIG_emailIsUser ? $self->{READ}->paramUTF8('email') : $self->{READ}->paramUTF8('user');

    # get any existing record for this user
    if (my $record = $self->accessDBM($self->{FILE_users}, $user, $RW_read)) {
	$userRecord = W3C::Annotations::UserRecord::parseUserEntry($user, $record);
    }

    my $authenticatedPassword = undef;

    # fill in rest of userRecord according to the user's input

    # current password
    if ($expectCurPassword) {
	if ($authPassword = $self->{READ}->paramUTF8('curPassword')) {
	    my $cryptedPassword = crypt($authPassword, $userRecord->getHashed);
	    if ($cryptedPassword eq $userRecord->getHashed) {
		$authenticatedPassword = $authPassword;
	    } else {
		push (@errors, "You supplied an incorrect password.");
	    }
	} else {
	    push (@errors, "You need to supply the current password.");
	}
    }

    # requested new password
    if ($password = $self->{READ}->paramUTF8('password')) {
	if ($password =~ m/^[^\:]{4,}$/) {
	    # authenticate the user against their current password
	    # The path to this is:
	    #   User uses the new user form rather than the user edit form.
	    #   User enters an account name that is already in the DB.
	    #   User enters their current password in the password field.
	    if (!$expectCurPassword && defined $userRecord->getHashed) {
		my $cryptedPassword = crypt($password, $userRecord->getHashed);
		if ($cryptedPassword eq $userRecord->getHashed) {
		    $authenticatedPassword = $password;
		}
	    }
	} else {
	    push (@errors, "Password \"$password\" is badly formatted.");
	}
    } elsif (defined $authenticatedPassword) {
	# user authenticated - default to the one submitted
	$password = $authenticatedPassword;
    } else {
	push (@errors, "You need to supply a password.");
    }

    # password confirmation (typo checker)
    if (my $confirmPassword = $self->{READ}->paramUTF8('passwordConfirm')) {
	if ($confirmPassword eq $password) {
	} else {
	    push (@errors, "Password confirmation does not match entered password.");
	}
    } elsif (!defined $authenticatedPassword) {
	push (@errors, "You need to confirm your password (enter it twice).");
    }

    # given
    if (my $given = $self->{READ}->paramUTF8('given')) {
	if ($given =~ m/^.+$/) {
	    $userRecord->setGiven($given);
	} else {
	    push (@errors, "Given name \"$given\" is badly formatted.");
	}
    } else {
	push (@errors, "You need to supply a given name.");
    }

    # family
    if (my $family = $self->{READ}->paramUTF8('family')) {
	if ($family =~ m/^.+$/) {
	    $userRecord->setFamily($family);
	} else {
	    push (@errors, "Family name \"$family\" is badly formatted.");
	}
    } else {
	push (@errors, "You need to supply a family name.");
    }

    # email
    if (my $email = $self->{READ}->paramUTF8('email')) {
	if ($email =~ m/\A(\w|-|_|\+|\.|\%|\#|\!)+@(\w|-)+(\.(\w|-)+)+\Z/) {
	    $userRecord->setEmail($email);
	} else {
	    push (@errors, "Email address \"$email\" is badly formatted.");
	}
    } else {
	push (@errors, "You need to supply a email address.");
    }

    # alter the options according to the user's selections
    my $options = 0; # override any current options
    if (my $prefsIdent = $self->{READ}->paramUTF8('prefsIdent')) {
	if ($prefsIdent & $OPTIONS_idByEmail) {
	    $options |= $OPTIONS_idByEmail;
	}
	if ($prefsIdent & $OPTIONS_idByName) {
	    $options |= $OPTIONS_idByName;
	}
    }
    $userRecord->setOptions($options);
    if (!($options & ($OPTIONS_idByEmail | $OPTIONS_idByName))) {
	push (@errors, "You must indicate an identification preference (email or name or both).");
    }

    if (@errors) {
	my @body = ();
	foreach my $error (@errors) {
	    push (@body, "<p>$error</p>\n");
	}
	push (@body, "<p>Please use your <strong>BACK</strong> button to correct and then resubmit.</p>\n");
	return $self->standardMessage(200, 'Input error: ', join ('', @body));
    } else {
	my $salt = $userRecord->getHashed;
	if (!defined $salt || $salt eq '') {
	    $salt = join '', ('.', '/', 0..9, 'A'..'Z','a'..'z')[rand 64, rand 64];
	}
	$userRecord->setPassword($password);
	$userRecord->setHashed(crypt($password, $salt));
	if (defined $authenticatedPassword) {
	    my $userEntry = $userRecord->buildUserEntry();
	    return $self->storeConfirmedUser($user, $userEntry);
	} else {
	    return $self->storePendingUser($user, $salt, $userRecord);
	}
    }
}

sub confirmUser {
    my ($self) = @_;
    my @errors = ();

    my $user = $self->{READ}->paramUTF8('user');
    if (defined $user) {
	if ($user =~ m/^[^\:]+$/) {
	} else {
	    push (@errors, "Account name \"$user\" is badly formatted.");
	}
    } else {
	push (@errors, "You need to supply an account name.");
    }

    my $nonce = $self->{READ}->paramUTF8('nonce');
    if (defined $nonce) {
	if ($nonce =~ m/^[^\:]+$/) {
	} else {
	    push (@errors, "Confirmation token \"$nonce\" is badly formatted.");
	}
    } else {
	push (@errors, "You need to supply a nonce.");
    }

    if (@errors) {
	my @body = ();
	foreach my $error (@errors) {
	    push (@body, "<p>$error</p>\n");
	}
	return $self->standardMessage(500, 'Input error: ', join ('', @body));
    } else {
	my $ret;
	eval {
	    $ret = $self->checkNonce($user, $nonce);
	}; if ($@) {if (my $ex = &catch('RetryException')) {
	    my $errorString = $self->makeErrorString([$ex->getMessage]);
	    return $self->paintConfirmUser($errorString, $user, $nonce);
	} else {&throw()}}
	return $ret;
    }
}

sub storePendingUser {
    my ($self, $user, $salt, $userRecord) = @_;
    my $now = time();
    if (!defined $userRecord->getCreate) {
	$userRecord->setCreate($now);
    }
    my $nonce = crypt($now, $salt);
    $userRecord->setNonce($nonce);
    $userRecord->setModify($now);
    my $pendingEntry = $userRecord->buildPendingEntry();
    $self->accessDBM($self->{FILE_pending}, $user, $pendingEntry);

    my $linkUser = CGI::escape($user);
    my $formURI = $self->{READ}->trimmedUri();
    my $returnUri = $formURI."?paintConfirm=1&amp;user=$linkUser";
    my $linkNonce = CGI::escape($nonce);
    # my $withNonce = $formURI."?confirmUser=1&amp;user=$linkUser&amp;nonce=$linkNonce";
    my $withNonce = $formURI."?paintConfirm=1&amp;user=$linkUser&amp;nonce=$linkNonce";
    my $family = $userRecord->getFamily;
    my $given = $userRecord->getGiven;
    my ($serviceLink, $serviceDesc) = $self->getService();
    my $mailBody = <<EOF
Dear $given $family,

You or someone acting on your behalf has entered a request
for access to the W3C's public annotation test service by
completing the form at $formURI

If that form was submitted in error, you may safely ignore
this message.

If you do want access to the $serviceDesc,
$serviceLink then please continue the
process by going to
$returnUri
and enter the following one-time confirmation token in the
indicated form field:

Your confirmation token is:
    $nonce 

If you use the following link the form should be pre-filled
for you (this link and the one-time token will expire soon):

    $withNonce

Thank you for your interest in the Annotea project.
EOF
    ; # ' # added for syntax hilighting in emacs
    my $email = $userRecord->getEmail;
    $self->mailText([$email], [$self->{EMAIL_from}], 'your request for access to our annotation service', $mailBody);

    my $preface = "<p>Access authorization for $user requested. A confirmation token has been sent to $email.</p>";
    return $self->paintConfirmUser($preface, $user);
}

sub paintConfirmUser {
    my ($self, $preface, $user, $nonce) = @_;

    my $selfUri = $self->{READ}->trimmedPath();
    my $linkUser = CGI::escape($user);
    my $returnUri = $self->{READ}->trimmedUri()."?paintConfirm=1&amp;user=$linkUser";

    my $body = <<EOF
$preface
<form action="$selfUri" method="POST">

<p>To complete the update to your W3C Public Annotea Service access,
please enter the confirmation token you received by email in the
space below.</p>

<p>Account name: <input type="text" name="user" value="$user" /><br />
   Confirmation token: <input type="text" name="nonce" value="$nonce"/></p>
<p><input type="submit" name="confirmUser" value="Execute Pending Update"/></p>
</form>

<p>You may bookmark <a href="$returnUri">this location</a> to return here later.</p>
EOF
    ;
    my $ret = $self->standardMessage(200, "Awaiting confirmation token", $body);
    return $ret;
}

sub mailText {
    my($self, $toList, $fromList, $subject, $mesg) = @_;
#    my $cmd = "| /usr/lib/sendmail -f$replyTo $emailId,$cc,$replyTo >/dev/null 2>&1";
    my $to = join(',', @$toList);
    my $from = join(',', @$fromList);
#    print "<br>\n", $cmd, "<br>\n";
    unless (open(MAIL, "| /usr/lib/sendmail -t -f$from >/dev/null 2>&1")) {
	&throw(new W3C::Util::Exception(-message => "couldn't open pipe"));
    }
    print MAIL 'To: ', $to, "\n";
    print MAIL 'From: ', $from, "\n";
    print MAIL 'Subject: ', $subject, "\n";
    print MAIL "Bcc: register-log\@w3.org\n";
    print MAIL 'Reply-to: ', $from, "\n";
    print MAIL "\n";
    print MAIL $mesg;
    close(MAIL);
}

sub handlePendingEntry {
    my ($self, $user, $suppliedNonce, $pUserEntry, $pendingEntry) = @_;
    my $userRecord = W3C::Annotations::UserRecord::parsePendingEntry($user, $pendingEntry);
    if (!defined $userRecord->getNonce) {
	my $message = $self->standardError("There is no pending action for $user.");
	&throw(new W3C::Http::HttpMessageException(-httpMessage => $message));
    }
    if ($userRecord->getNonce ne $suppliedNonce) {
	my $message = "The nonce you supplied \"$suppliedNonce\" was incorrect.";
	&throw(new RetryException(-message => $message));
    }

    # construct the entry for the users table
    my $status = $userRecord->getStatus;
    $status &= $STATUS_NOT_fresh;
    $userRecord->setStatus($status);
    $$pUserEntry = $userRecord->buildUserEntry();

    # Leave the pending entry. We clear it after we succeed in writing
    # the groups and users files.
    return $pendingEntry;
}

sub checkNonce {
    my ($self, $user, $suppliedNonce) = @_;
    my $userEntry;

    # Check the nonce in the pending entry.
    $self->accessDBM($self->{FILE_pending}, $user, 
		     sub {$self->handlePendingEntry($user, $suppliedNonce, \$userEntry, @_)});

    my $ret = $self->storeConfirmedUser($user, $userEntry);

    # Use database-specific method to create groups entry. The format of
    # the groups entry varies by database (per the apache auth modules).
    $self->{USER_RECORDS}->createGroupEntry($user, $self->{GROUP_default}, 
					    $self->{FILE_groups});

    # All updates went well so clear out the pending entry.
    $self->accessDBM($self->{FILE_pending}, $user, undef);
    return $ret;
}

sub storeConfirmedUser {
    my ($self, $user, $userEntry) = @_;
    $self->accessDBM($self->{FILE_users}, $user, $userEntry);

    # compose the editor link
    my $linkUser = CGI::escape($user);
    my $formUri = $self->{READ}->trimmedUri();
    my $editUri = $formUri."?user=$linkUser";
    my ($serviceLink, $serviceDesc) = $self->getService();

    # give the user positive feedback
    my $body = <<EOF
<p>$user has been granted access to the W3C Public Annotea Service.</p>
<p>Your account name is <strong>$user</strong>. Your password is what
   you entered in the access request form.  You may use this account
   when prompted by your browser (or other user agent) to
   access the <a href="$serviceLink">$serviceDesc</a>. Have fun!.</p>

<p>You may edit your access preferences, including changing your password,
   at any time by returning to the <a href="$formUri">access request form</a>
   or by bookmarking a link to the
   <a href="$editUri">pre-filled request form</a>.  If you forget your
   password, you may follow the same procedure to enter a new one.</p>
EOF
    ;
    my $ret = $self->standardMessage(200, "Access Confirmed", $body);
}

sub getService {
    my ($self) = @_;
    my $serviceLink = $self->{PROPERTIES}->getI('auth.service.link') || $DEFAULT_serviceLink;
    my $serviceDesc = $self->{PROPERTIES}->getI('auth.service.desc') || $DEFAULT_serviceDesc;
    return ($serviceLink, $serviceDesc);
}

sub accessDBM {
    my ($self, $file, $key, $value) = @_;
    $self->{USER_RECORDS}->accessDB($file, $key, $value);
}

sub standardError {
    my ($self, $errorString) = @_;
    return $self->standardMessage(500, 'Error', "<p>$errorString</p>");
}

sub standardMessage {
    my ($self, $statusCode, $title, $markup) = @_;

    my $body = <<EOF
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

<!-- !DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd" --> 
<html><head><title>$title</title></head>
      <body>
	  $self->{HTML_navbar}
      <h1>$title</h1>
      $markup
  <hr />
  <p>$REVISIONDATE</p>
</body></html>
EOF
    ;
    my $message = new W3C::Http::Message(-statusCode => $statusCode, 
					 -headers => [['Content-Type', 'text/html;charset=utf-8']], 
					 -body => $body);
    return $message;
}

__END__

=head1 NAME access

W3C::Annotations::cgibin::access - CGI interface for creating user accounts

=head1 SYNOPSIS

  http://<server>/Annotations/access

=head1 DESCRIPTION

The access script provides an interface for creating and modifying apache L<user/"user"> and and L<group/"group"> authentication files. It is designed to administer accoutns for the W3C Annotations project.

This module is used with the W3C::Annotations CPAN module.

=head2 Apache Authentication

Apache authentication modules commonly work from either flat text files or Berkeley DBs. The authentication modules consult these files when authenticating a client access to the server.

=head3 user

The traditional apache user file consists of a set of ':'-separated user/hashed-password records similar to an abbreviated /etc/passwd file. Because apache only reads the first two fields in this file, other applications or scripts are free to extend these records to incude other pieces of information. In principal, multiple applications with differing user records can use the same B<user> file so long as they only access the records that are tailored for them. Thus, a B<user> file could have a record for an annotations user and a record for a search engine user and everything would work as long as the search engine user did not try to use the annotations server.

The B<access> script extends these records to include:

  hashed password: apache expects this field to be hashed with crypt.
  version number: the version of access that created a record (for backward compatibility).
  status: one of (fresh | NOT_fresh | disabled)
  options: one of (idByEmail | idByName)
  cleartxt: rot13'd but unencrypted form of the password for digest authentication
  email: email contact associated with the principle (user)
  family: family name
  give: given name
  modify: last date the record was modified
  create: date the record was created

While the coding of B<user> files may vary with the database format (Berkeley DB or flag text file), the data remains consistent. The flat file has the form:
  username:password[extensions]
and the the Berkeley DB has a key
  username
and a value
  password[extensions]

=head3 group

Unlike B<user> files, the format of the B<group> file differs between apache authentication modules. The flat text file has a format
  group:user1 user2 user3
while the Berkeley DB uses the key "user1" with the value "group", For this reason, the database drivers have a higher-level function B<createGroupEntry> to allow the driver to handle the exact format.

=head1 AUTHOR

Eric Prud'hommeaux <eric@w3.org>

=head1 SEE ALSO

L<W3C::Annotations::cgibin::annotate>
L<W3C::Annotations::UserRecord>

=cut
