package Site::User; ###### 
### 	$Id: User.pm,v 1.8 2003/05/09 19:32:14 steve Exp $
#	This file is part of Combo and is licensed under the GPL and LGPL.
#	Copyright 2001-2002 by the authors.  See README and LICENSE files
#	for more details or see http://penguinsong.org/.

###	This class creates, maintains, and reads user profiles, home 
###	directories, and per-directory access information (.htaccess files).
###	Basically it does everything necessary for user registration,
###	authentication, and access authorization.  One of the tricky bits
###	is making sure that access is the same whether the user is accessing
###	files via WebDAV or the file-manipulation CGI.

use XML::Node;
use Site::Util;

BEGIN {
    use Exporter();
    use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
    $VERSION=1.00;
    @ISA = qw(Exporter XML::Node);
    @EXPORT = qw( $usernameCookieName $passwordCookieName 
		 );
}


#############################################################################
##
## Notes on User Names/ID's:
##

## There are three things we can use for user ID's:
##
##   MemberName:
##
##	Users are identified by their WikiWord-style name in the Members 
##	(/m//) area or equivalent.  
##
##   Unix user name:
##
##	Each user has a Unix-style account name, and we use these names
##	as their usernames for version control as well as for everything
##	else.  Given sufficient privileges, some users could have _real_ 
##	accounts (maybe in a chroot jail) with, for example, a .ssh 
##	subdirectory.  On an intranet or developer site, this may be all 
##	you need.
##
##   Email address:
##
##	This is common for Bugzilla and many other systems; it has the
##	advantage that the user doesn't have to make up a new ID and the
##	system doesn't have to check to ensure that it's unique.  It's
##	useless as the name of a local directory, however.

## Note that these three kinds of ID's are distinguishable:
##
##	MemberNames are capitalized, usually with embedded uppercase.
##	Unix usernames are lowercase
##	Email addresses contain an "@"
##
##   This means that we can actually allow any of these.  In particular, 
##	we can allow non-members to register by email address, and allow
##	privileged (e.g. paying or local) users to use Unix logins (which 
##	are easier to type).  Note, too, that a unix login corresponds to
##	a local email address.

## We could define the _cannonical_ user ID as:
##	MemberName    -- if the user has a local presence
##	Email address -- otherwise
##
##   This allows us to easily distinguish between users who are members,
##	and users who are registered but are not members -- of course, this
##	assumes that every user with a Unix name also has a MemberName.
##	Also, it allows us to assume that directory ownership and so on are
##	specified by MemberName, which is the only reasonable way to do it. 

## See also Version.pm for a related discussion; basically the CVS version-
## control system (and presumably also other Unix-based system) expects
## Unix-like user names, and these can be either generic (e.g. "owner" or
## "member") or actual.  To a large extent these are separable -- one could
## use generic names for VC even if users actually have Unix accounts.


#############################################################################
##
## Notes on User Profiles:
##

## There are a couple of different ways we can store user profile
## information: 
##
##   Text files in a ``home'' directory:
##
##	This is, of course, the natural format if users have Unix-like
##	accounts and home directories.  It's also the most general:  users
##	can have an arbitrary collection of files in their directories, and
##	they can be symlinked into URL-space as needed.  We can even give
##	them a ~foo web directory (Apache allows virtual users to have web
##	directories).  On the other hand, it's pretty costly in terms of 
##	space, especially on a filesystem with large blocks (it's practically
##	free on ReiserFS).
##
##   XML file:
##
##	This makes good sense for a system like Combo that relies on XML
##	processing.  It's especially convenient if we use the Perl
##	representation of parsed XML, and allows embedded HTML (e.g., images 
##	and signatures) and XML (e.g., templates).  XML files are easily
##	stored in either a // index, Unix home directories, or Combo member 
##	directories.  (For that matter, with symlinks they can be in all
##	three places at once.)
##
##   Database (DBM, SQL, or LDAP):
##
##	This is, of course, how everyone else does it.  It works well for
##	limited-size strings, much less well for freeform document fragments.
##

## In any event, this class extends XML::Node as a convenient representation
##	for user profile information.  However, the current plan is to start
##	with a "..user.cf" file in the web home directory, plus a couple of
##	tied DB databases to map between the different forms of userID and
##	passwords.


#############################################################################
##
## Notes on User Authentication:
##

## In general, we need to allow two different modes of user authentication:
##	HTTP authentication (Authorized-User header, .htpasswd file, etc.)
##	and cookie authentication.  The latter persists over sessions; the
##	former (if we use Basic authentication and Unix passwords) is
##	portable to CVS password servers, ssh, and so on.

## The information about what operations are permitted needs to be stored
##	in a form that's useable both to Apache's auth_* modules  _and_ to 
##	the CGI's.  This limits what can be done:  Apache can only authorize
##	HTTP request methods based on path and userID or static group 
##	membership.
##
## 	This suggests that we either have to parse the .htaccess files, 
##	include specially-formatted comments in them, or put the information
##	somewhere else.  For testing purposes, of course, we can simply punt.
##	We also need to establish a default policy.

## Note that this class needs to be able to create user profiles and
##	.htaccess files as well as read them -- that means that we can
##	generally assume that it's handling its own data.  What we'll do
##	for now is keep the user profile in a property file, ..user.cf, in
##	the user's web home directory, if they have one.

## The most general technique is to put per-directory information in a 
##	..cf file in property-file format, possibly augmented by an
##	XML file -- see Site::Resource.  

#############################################################################
###
### Constants:
###	These constants can easily be overridden in Site::Config or other 
###	site initialization code. 
###


# The name of the cookie that holds the username
$usernameCookieName = "user";

# The name of the cookie that holds the password
$passwordCookieName = "pass";

# The name of the user configuration file
$userConfigFileName = "..user.cf";




#############################################################################
###
### Constructors:
###

### User->new(a => v)
#	Create a new User with a given set of attributes.
#	Typically used to set up what we know from the maps before 
#	reading in the config file. 
#
sub new {
    my ($class, @attrs) = @_;
    my $self = {@attrs};
    $self = bless $self, $class;
    return $self;
    #return $class->element('User', \@attrs);
}


### User->fromContext($cxt, $userid)
#	Obtain a User instance by analyzing the current request Context.
#	If $userid is supplied, uses that (assumed checked by the caller).
#	Otherwise, looks first at REMOTE_USER, then at the current cookies.
#
#	Two pseudo-users are defined:  UNKNOWN and INVALID.  The latter
#	is returned when the password cookie does not match the username.
#
sub fromContext {
    my ($class, $cxt, $userid) = @_;
    
    # Get configuration and query info from context

    my $cfg  = $cxt->getConfig;			# the site config
    my $q    = $cxt->getCGI;
    my $root	= $cxt->getDocRoot;
    $userid	= $ENV{'REMOTE_USER'} unless $userid;

    # Get user name/ID
    my $usernameCookie = $q->cookie($usernameCookieName);
    my $passwordCookie = $q->cookie($passwordCookieName);

    if (!$userid && $usernameCookie) {
	if (checkCookie($cxt, $usernameCookie, $passwordCookie)) {
	    $userid = mapCookieNameToId($cfg, $usernameCookie);
	} else {
	    # bad password cookie.
	    $userid = 'INVALID';
	}
    }
    if (!$userid) { $userid = 'UNKNOWN'; }

    # Determine location of user's profile.
    my $userDir  = mapIdToUserDir($cfg, $userid);
    my $homePath = mapIdToHomePath($cfg, $userid);
    $userDir  .= '/' unless $userDir  =~ m|/$|;
    $homePath .= '/' unless $homePath =~ m|/$|;

    my $filepath = (-d $userDir)? $userDir . $userConfigFileName : '';
    $filepath = '' unless -f $filepath;

    # Start by initializing with everything we already know

    my $self = $class->new('_user.ID'   => $userid,
			   );
    $self->{'_user.dir'} = $filePath;
    $self->{'user.home'} = $homePath if -d $root . $homePath;
			   
    if ($filepath) {
	Site::Util::readConfig($self, $filepath);
    } else {
	if ($userid eq 'INVALID') {
	    $self->{'user.name'} = "invalid";
	    $self->{'user.full.name'}  = "Invalid Cookies";

	} elsif ($userid eq 'UNKNOWN') {
	    $self->{'user.name'} = "unknown";
	    $self->{'user.full.name'}  = "Unknown User";

	} else {
	    $self->{'user.name'} = $userid;
	    $self->{'user.full.name'}  = $userid;
	}

    }
    if (! $self->{'user.link'} && $self->homePath) {
	$self->{'user.link'} = 
	    "<a href='" . $self->homePath . "'>" . $self->fullName . "</a>";
    }

    return $self;
}

#############################################################################
###
### Mapping Functions:
###

## === Note: 
#	For the moment we are assuming that the user ID (i.e., what the user
#	types in to log in) is the same as the user "name", and that that in
#	turn is the same as the user's "member directory" name.

### User::mapIdToUserDir($site, $userID)
#	Map a cannonical userID (from a cookie or login) to the filesystem
#	path of the user's home directory.  Note that this need not be
#	web accessible; it could be the user's Unix home directory.
#
# ===	For the moment, though, we're going to assume that the userid,
#	web directory name, and user directory name are identical.
#
sub mapIdToUserDir {
    my ($cfg, $userid) = @_;

    # In the typical case, the home directory is just the web directory:
    my $webhome = mapIdToHomePath($cfg, $userid);
    return ($webhome)? $cfg->getDocRoot . $webhome : '';
}

### User::mapIdToHomePath($site, $id)
#	Map a cannonical userID (from a cookie or login) to the URL
#	path of the user's web home.
#
# ===	For the moment we're going to assume that the userid _is_ the
#	web directory name.
#
sub mapIdToHomePath {
    my ($cfg, $userid) = @_;

    return $cfg->makeRealPath($cfg->getHomePathPrefix . $userid);

    # === Expansion of // paths has to be done in Site::Config, 
    # === not Combo or even Context
}

### User::mapIdToEncryptedPassword($site, $id)
#	Map a cannonical userID (from a cookie or login) to the corresponding
#	_encrypted_ password.  We don't usually need to check it, because it's
#	usually the server that does the checking (except for registration).
#	The encrypted password is used to derive the cookie password.
sub mapIdToEncryptedPassword {
    my ($cfg, $userid) = @_;

}

### User::mapIdToEmail($site, $id)
#	Map a cannonical userID (from a cookie or login) to the user's
#	email address.
sub mapIdToEmail {
    my ($cfg, $userid) = @_;

}

### User::CookieNameToId($site, $cookie)
#	Map a username cookie to a cannonical userID
#
#  ===	For the moment we're going to assume that the cookie _is_ the
#	userid.  That's perfectly reasonable unless the cannonical ID is 
#	an email address and we're trying to keep them private, or if 
#	unregistered users use email addresses while registered users use
#	something else.  Not a problem at the moment.
#
sub mapCookieNameToId {
    my ($site, $cookie) = @_;

    return $cookie;
}


#############################################################################
###
### Access Functions:
###

### $c->get(name) -> value
#	get a site configuration property
sub get {
    my ($self, $n) = (@_);
    return $self->{$n};
}


### $u->userId
#	Returns the "cannonical" user ID.  This might be a Unix login, 
#	memberName, account number, e-mail address, or other id.  May be
#	null for anonymous or not-yet-logged-in visitors.
sub userId {
    my ($self) = @_;
    return $self->get('_user.ID');
}

### $u->name
#	the name by which this user is known on the site -- typically the
#	same as the user's ``home'' directory in /m//.  May be null for
#	anonymous or not-yet-logged-in visitors, or may be some default
#	such as "anon".
sub name {
    my ($self) = @_;
    return $self->get('user.name');
}


### $u->fullName
#	the user's "full" (realspace) name.  May be something like "Anonymous
#	Coward" for anonymous visitors.
sub fullName {
    my ($self) = @_;
    return $self->get('user.full.name');
}


### $u->userClass
#	Returns the user's "class", typically from the set 
#	{subscriber, member, visitor, unknown, admin}
sub userClass {
    my ($self) = @_;
    return $self->get('user.class');
}

### $u->groups() -> [group, ...]
#	The list of groups of which this user is a member
sub groups {
    my ($self) = @_;
    return $self->get('user.groups');
}

### $u->userDir()
#	the file path of the user's 'user directory'.  Sometimes the 
#	same as the web "home", but in some cases it might not even be in
#	URL-space; in Unix it might be the user's actual home directory.
sub userDir {
    my ($self) = @_;
    return $self->get('_user.dir');
}

### $u->homePath()
#	the expanded URL path of the user's main web directory.  In Combo
#	this is usually the member directory, /m//userName.  Note that
#	non-members typically will not have member directories, but they
#	may have a userName and/or userID.
sub homePath {
    my ($self) = @_;
    return $self->get('user.home');
}

### $u->otherPaths() -> [path, ...]
#	the expanded URL paths of any directories owned by the user that
#	are not descendents of the user's "home" directory.  We need to be
#	able to offer this list to the user for editing.
sub otherPaths {
    my ($self) = @_;
    return $self->get('user.other.paths');
}



#############################################################################
###
### Location-dependent Information:
###	All take a Resource (representing a directory) and an optional
###	filename within that directory.
###

### $u->versionID($resource)
#	ID to use for version control for this resource
#	This is usually some combination of userID, userClass, and userRole

### $u->versionPW($resource)
#	password to use for version control

### $u->userRole($resource)
#	Returns the user "role" for this resource; essentially the
#	user's relationship to this directory.  Typically taken from the
#	set {owner, friend, foe, ''}

### $u->authorized($resource, $op)
#	is this user authorized to perform the given operation on the
#	given resource?  $op is an HTTP/WebDAV request method keyword.  

### $u->canRead($resource)
### $u->canWrite($resource)
### $u->canAppend($resource)


#############################################################################
###
### password checking and database maintenance:
###

### cookiePassword($cxt, $username)
#	password to verify for cookie-based authorization
#	Note that the cookie password may depend on the context, e.g the
#	remote IP address.  Note, too, that it must be a static function,
#	because we need to call it inside the constructor.
#
sub cookiePassword {
    my ($cxt, $username) = @_;
    
    return $username;
}


### checkPassword($cxt, $user, $pass)
#	check a username against a _real_ password, i.e., the one used 
#	for Basic authentication.  This is generally only needed when
#	creating an account or changing the password; the rest of the time
#	the server does it for us.
sub checkPassword {
    my ($cxt, $user, $pass) = @_;

}


### checkCookie($cxt, $userCookie, $passCookie)
sub checkCookie {
    my ($cxt, $user, $pass) = @_;

    return $pass eq cookiePassword($cxt, $user);
}

### $u->getUserGroups


### $u=>setUserGroups


#############################################################################
###
### Low-level password and group maintenance:
###

## We will probably want to maintain a tied database for passwords and groups
#	even if the server insists on files, because we want fast access.
#	Of course, a real database would give us that and locking, too...

# === directory for passwords, account mappings, etc. probably ought to 
# === default to something like $root/../var/$serverName/...

### enterPasswordInFile
#	For use with mod_auth
#	Note that the group file is group: user1 user2 user3

### enterPasswordInDB
#	For use with mod_auth_db 
#	Note that the group file is user => group1,group2,group3
#	See http://httpd.apache.org/docs/howto/auth.html#database
sub enterPasswordInDB {
    my ($username, $password) = @_;

    # Code taken from http://httpd.apache.org/docs/howto/auth.html

    use DB_File;
    tie %database, 'DB_File', "passwords.dat"
	or die "Can't initialize database: $!\n";
    
    @chars=(0..9,'a'..'z');
    $salt = '', map { $chars[int rand @chars] } (0..1);
    
    $crypt = crypt($password, $salt);
    $database{$username} = $crypt;

    untie %database;
}


#############################################################################
1;
