/usr/local/CPAN/Schema-RDBMS-AUS/Schema/RDBMS/AUS/User.pm


#!perl

package Schema::RDBMS::AUS::User;

use strict;
use warnings;
use Carp qw(croak);
use DBIx::Transaction;
use URI;
use URI::QueryParam;
use Schema::RDBMS::AUS;

use vars qw(%ENV);

return 1;

# constructors

sub login {
    my($class, $user, $password, %login_info) = @_;
    %login_info = (%login_info, name => $user, password => $password);
    my $self;
    if($self = eval { $class->_login(%login_info) }) {
        my $txn = delete($login_info{_post_login}) || sub { return shift };
        return $self->dbh->transaction(sub {
            $self->used;
            $self->log('login', %login_info) or return;
            $txn->($self);
        });
    } else {
        my $err = $@;
        $self = $class->load(%login_info); # will die if user doesnt exist
        $login_info{error} = $err;
        $self->log('login_failure', %login_info);
        die $err;
    }
}

sub _login {
    my($class, %login_info) = @_;

    if(my $self = $class->load(%login_info)) {
        die qq{Can not log in as group #$self->{id} "$login_info{name}"\n}
            if($self->{is_group});
        
        die qq{Account #$self->{id} "$login_info{name}" is disabled.\n}
            if($self->flag('Disabled'));
        
        if($self->check_password($login_info{password})) {
            return $self;
        } else {
            die qq{Bad password for user #$self->{id} "$login_info{name}"\n};
        }
    } else {
        die qq{No such user "$login_info{name}"\n};
    }
}

sub load {
    my $class = shift;
    return $class->_new(@_)->_LOAD_user;
}

sub create {
    my($class, %args) = @_;
    my $self = $class->_new(%args);

    $self->{password_crypt} = $self->default_password_crypt
        unless $self->{password_crypt};

    return $self->dbh->transaction(sub {
        $self->_CREATE_user or die "Failed to create user.\n";
        $self->log('create', %args) or die "Failed to log user creation.\n";
        return $self unless defined $self->{_password};
        
        $self->{password} = 
            $self->crypt($self->password(delete $self->{_password}));
            
        $self->save;
    });
}

# methods

sub password {
    my($self, $password) = @_;
    if($self->{_validate_password}->($password)) {
        return $password;
    } else {
        die "Invalid password.\n";
    }
}

sub reset_password {
    my($self, $password) = splice(@_, 0, 2);
    if($self->password($password)) {
        return $self->_reset_password($password, @_);
    }
}

sub _reset_password {
    my($self, $password, $reason, %log_params) = @_;
    $reason ||= 'reset_password';
    $self->{password} = $self->crypt($password);
    return $self->dbh->transaction(sub {
        $self->save &&
        $self->log($reason, %log_params);
    });
}

sub change_password {
    my($self, $old, $new, %log_params) = @_;
    if($self->_check_password($old)) {
        return $self->reset_password($new, 'change_password', %log_params);
    } else {
        die "Old password does not match.\n";
    }
}

sub save {
    my $self = shift;
    return $self->dbh->transaction(sub {
        if($self->_UPDATE_user && $self->_save_flags) {
            return $self;
        } else {
            die "Failed to save user.\n";
        }
    });
}

sub used {
    my $self = shift;
    return $self->{time_used} = $self->_UPDATE_used;
}

sub log {
    my($self, $event, %args) = @_;
    delete @args{qw(password id _dbh)};
    my $uri = URI->new;
    $uri->query_form_hash(\%args);
    my $data = $uri->query;
    return $self->_INSERT_user_log($event, $data);
}

sub crypt {
    my $self = shift;
    return $self->{_crypt_class}->crypt(@_);
}

sub check_password {
    my($self, $password) = @_;
    return unless defined $self->{password} && length $self->{password} &&
        defined $password && length $password;
    return $self->_check_password($password);
}

sub flag {
    my($self, $flag) = @_;
    return $self->{_flags}->{lc $flag};
}

sub permission {
    my($self, $perm) = @_;
    return $self->{_permissions}->{lc $perm};
}

sub set_flag {
    my($self, $flag, $value, $create) = @_;
    if(!defined $value) {
        $value = 1;
    } else {
        $value = (!!$value) || 0;
    }
    $self->_SELECT_or_INSERT_flag($flag) if $create;
    return $self->{_flags}->{lc $flag} = $value;
}

sub clear_flag {
    my($self, $flag) = @_;
    delete $self->{_flags}->{lc $flag};
}

sub add_to_group {
    my($self, $group) = @_;
    
    return $self->dbh->transaction(sub {
        $group = ref($self)->load(name => $group, _dbh => $self->{_dbh})
            unless ref $group;
    
        return
            $self->_INSERT_membership($group->{id}) &&
            $self->_refresh_permissions &&
            $self->_refresh_membership;
    });
}

sub remove_from_group {
    my($self, $group) = @_;
    
    return $self->dbh->transaction(sub {
        $group = ref($self)->load(name => $group, _dbh => $self->{_dbh})
            unless ref $group;
    
        return
            $self->_DELETE_membership($group->{id}) &&
            $self->_refresh_permissions &&
            $self->_refresh_membership;
    });
}

sub refresh {
    my $self = shift;
    return $self->_refresh_meta && $self->_refresh_user;
}

# accessors

sub dbh { return $_[0]->{_dbh}; }

sub default_password_crypt { return "SHA1"; }

# driver constructor

sub driver_new {
    my($class, $driver, %args) = @_;
    
    $args{_flags} ||= {};
    $args{_permissions} ||= {};
    $args{_membership} ||= {};

    return bless \%args, $class;
}

# private class methods

sub _connect_cached {
    my($class, %args) = @_;
    
    if(my $dbh = Schema::RDBMS::AUS->dbh(
        @args{qw(_db_dsn _db_user _db_pass _db_opts)}
    )) {
        return $dbh;
    } else {
        croak(qq{_connect_cached() failed: }, DBI->errstr);
    }
}

# private class/object methods

sub _new {
    my($self, %args) = @_;
    
    my $class;
    if($class = ref($self)) {
        %args = (%$self, %args);
    } else {
        $class = $self;
    }

    $args{_dbh} = $self->_connect_cached(%args)
        unless $args{_dbh};

    croak "a database handle (_dbh) is required for $class"
        unless($args{_dbh});
        
    croak "_dbh does not seem to be a DBIx::Transaction object"
        unless($args{_dbh}->isa('DBIx::Transaction::db'));
        
    $args{_dbh_driver} = $args{_dbh}->{Driver}->{Name}
        unless $args{_dbh_driver};
        
    $args{_validate_password} = sub { return length $_[0]; }
        unless $args{_validate_password};

    return $class->driver_new($args{_dbh_driver}, %args);
}

sub _use_crypt_class {
    my $self = shift;
    
    eval "use $self->{_crypt_class}; 1"
        or croak "Failed to load $self->{_crypt_class}: $@";
    
    return $self;
}

sub _refresh_user {
    my $self = shift;
    if(my $row = $self->_SELECT_user) {
        %$self = (%$self, %$row);
        return $self;
    } else {
        die "Refreshing user failed!";
    }
}

sub _refresh_meta {
    my $self = shift;
    return
        $self->_refresh_flags &&
        $self->_refresh_permissions &&
        $self->_refresh_membership
        or die "Refreshing metadata failed!";
}

# low-level queries

sub _LOAD_user {
    my($self, %args) = @_;
    return $self->dbh->transaction(sub {
        if(my $row = $self->_SELECT_user(%args)) {
            %$self = (%$self, %$row);
            $self->_refresh_flags;
            $self->_refresh_permissions;
            $self->_refresh_membership;
            return $self->_use_crypt_class;
        } else {
            die "User not found.\n";
        }
    });
}

sub _check_password {
    my($self, $password) = @_;
    return $self->crypt($password) eq $self->{password};
}

sub _CREATE_user {
    my($self, %args) = @_;
    return $self->dbh->transaction(sub {
        if($self->_INSERT_user(%args)) {
            my $id = $self->dbh->last_insert_id(undef, undef, 'aus_user', undef);
            $self->{id} = $id;
            return $self->_LOAD_user;
        } else {
            return;
        }
    });
}

sub _sql_UPDATE_used {
    my $self = shift;
    return('UPDATE aus_user SET time_used = now() WHERE id = ?', $self->{id});
}

sub _sql_SELECT_used {
    my $self = shift;
    return('SELECT time_used FROM aus_user WHERE id = ?', $self->{id});
}

sub _UPDATE_used {
    my $self = shift;
    my $dbh = $self->dbh;
    return $dbh->transaction(sub {
        my($query, $id) = $self->_sql_UPDATE_used;
        $dbh->do($query, {}, $id)
            or die "Failed to update last used time: ", $dbh->errstr;
            
        ($query, $id) = $self->_sql_SELECT_used;
        return $dbh->selectrow_array($query, {}, $id)
            or die "Failed to fetch last used time: ", $dbh->errstr;
    });
}

sub _FIELDS_user {
    return qw(id name password password_crypt is_group time_used);
}

sub _sql_SELECT_user {
    my($self, %args) = @_;

    my($k, $v);
    my @fields = (
        (map { "aus_user.$_ AS $_" } $self->_FIELDS_user),
        "aus_password_crypt.class AS _crypt_class"
    );
        
    {
        local $" = ", ";
        $k = qq{
                        SELECT @fields FROM aus_user
                                LEFT OUTER JOIN
                                        aus_password_crypt
                                ON
                                        aus_user.password_crypt = aus_password_crypt.id
                };
    }

    if($args{id}) {
        $k .= "WHERE aus_user.id = ?";
        $v = $args{id};
    } elsif($args{name}) {
        $k .= "WHERE aus_user.name = ?";
        $v = $args{name};
    } else {
        croak q{Neither "name" or "id" were specified to _SELECT_user!};
    }

    return($k, $v);
}

sub _SELECT_user {
    my($self, %args) = @_;
    %args = (%$self, %args);
    
    my($query, $id) = $self->_sql_SELECT_user(%args);
    if(my $sth = $self->dbh->prepare($query)) {
        if($sth->execute($id)) {
            my $rv = $sth->fetchrow_hashref;
            $sth->finish;
            return $rv;
        }
    }

    die "Query $query failed: ", $self->dbh->errstr;
}

sub _SELECT_or_INSERT_flag {
    my($self, $flag) = @_;
    $flag = lc $flag;
    return $self->dbh->transaction(sub {
        my $sth = $self->dbh->prepare_cached(
            "SELECT name FROM aus_flag WHERE name = ?"
        );
        
        $sth->execute($flag) or die $sth->errstr;
        if($sth->fetchrow_array) {
            $sth->finish;
            return $flag;
        } else {
            $sth->finish;
            $self->dbh->do("INSERT INTO aus_flag (name) VALUES (?)", {}, $flag)
                    or die $self->dbh->errstr;
            return $flag;
        }
    });
}

sub _sql_INSERT_user {
    my($self, %args) = @_;
    
    my @keys = sort grep($args{$_}, $self->_FIELDS_user);
    my @qs = (("?") x scalar @keys);

    local $" = ", ";
    return(qq{INSERT INTO aus_user (@keys) VALUES (@qs)}, @keys);
}

sub _INSERT_user {
    my($self, %args) = @_;
    
    %args = (%$self, %args);
    croak "Can't INSERT a user that already has an id" if($args{id});
    
    return $self if($self->dbh->transaction(sub {
        my($query, @keys) = $self->_sql_INSERT_user(%args);
        $self->dbh->do($query, {}, @args{@keys});
    }));
    
    return;
}

sub _INSERT_user_log {
    my($self, $event, $data) = @_;
    return $self->dbh->transaction(sub {
        $self->dbh->do(
            "INSERT INTO aus_user_log (user_id, event, data) VALUES (?, ?, ?)",
            {},
            $self->{id}, $event, $data
        );
    });
}

sub _sql_UPDATE_user {
    my $self = shift;
    my @fields = grep($_ ne 'id' && $_ ne 'time_used', $self->_FIELDS_user);
    my @updates = map { "$_ = ?" } @fields;
    local $" = ", ";
    my $sql = "UPDATE aus_user SET @updates WHERE id = ?";
    return($sql, @fields, 'id');
}

sub _UPDATE_user {
    my $self = shift;
    my($query, @params) = $self->_sql_UPDATE_user;

    return $self->dbh->transaction(
        sub { $self->dbh->do($query, {}, @{$self}{@params}); }
    );
}

sub _sql_DELETE_flags {
    my $self = shift;
    return(
        q{DELETE FROM aus_user_flags WHERE user_id = ?},
        'id'
    );
}

sub _sql_INSERT_flag {
    my $self = shift;
    return(
        q{INSERT INTO aus_user_flags (user_id, flag_name, enabled) VALUES (?, ?, ?)},
        'id'
    );
}

sub _sql_SELECT_flags {
    my $self = shift;
    return(
        q{SELECT flag_name, enabled FROM aus_user_flags WHERE user_id = ?},
        'id'
    );
}

sub _sql_SELECT_permissions {
    my $self = shift;
    return(
        q{SELECT flag_name, enabled FROM aus_all_user_flags WHERE user_id = ?},
        'id'
    );
}

sub _sql_SELECT_membership {
    my $self = shift;
    return(
        q{
                        SELECT
                                ancestor, min(degree) AS degree
                        FROM aus_user_ancestors
                        WHERE user_id = ?
                        GROUP BY user_id, ancestor
                },
        'id'
    );
}

sub _fetch_flags {
    my($self, $sth) = @_;
    my %rv;
    while(my @row = $sth->fetchrow_array) {
        $rv{lc $row[0]} = $row[1];
    }
    return %rv;
}

sub _SELECT_flags {
    my($self, $query, @params) = @_;
    my %rv;
    $self->dbh->transaction(sub {
        my $sth = $self->dbh->prepare_cached($query);
        if($sth->execute(@{$self}{@params})) {
            %rv = $self->_fetch_flags($sth);
            $sth->finish;
        } else {
            die "fetching flags failed: ", $sth->errstr;
        }
    });
    return %rv;
}

sub _INSERT_membership {
    my($self, $gid) = @_;
    return $self->dbh->transaction(sub {
        my $sth = $self->dbh->prepare_cached(
            q{INSERT INTO aus_user_membership (user_id, member_of) VALUES (?, ?)}
        );
    
        $sth->execute($self->{id}, $gid)
            or die $sth->errstr;
        
        return 1;
    });
}

sub _DELETE_membership {
    my($self, $gid) = @_;
    return $self->dbh->transaction(sub {
        my $sth = $self->dbh->prepare_cached(
            q{DELETE FROM aus_user_membership WHERE user_id = ? AND member_of = ?}
        );
    
        $sth->execute($self->{id}, $gid)
            or die $sth->errstr;
        
        return 1;
    });
}

sub _refresh_flags {
    my $self = shift;
    my %rv = $self->_SELECT_flags($self->_sql_SELECT_flags);
    $self->{_flags} = \%rv;
    return $self->{_flags};
}

sub _refresh_permissions {
    my $self = shift;
    my %rv = $self->_SELECT_flags($self->_sql_SELECT_permissions);
    $self->{_permissions} = \%rv;
    return $self->{_permissions};
}

sub _refresh_membership {
    my $self = shift;
    my %rv = $self->_SELECT_flags($self->_sql_SELECT_membership);
    $self->{_membership} = \%rv;
    return $self->{_membership};
}

sub _save_flags {
    my $self = shift;
    return $self->dbh->transaction(sub {
        my($q, @p) = $self->_sql_DELETE_flags;
        
        $self->dbh->do($q, {}, @{$self}{@p}) or die
            "do($q): ", $self->dbh->errstr;
        
        ($q, @p) = $self->_sql_INSERT_flag;
        
        while(my($k, $v) = each(%{$self->{_flags}})) {
            $self->dbh->do($q, {}, @{$self}{@p}, $k, $v) or
                die "do($q, $k, $v): ", $self->dbh->errstr;
        }
        
        $self->_refresh_permissions;
        return 1;
    });
}