package Apache::Authn::RedminePublicRepoControl;

=head1 Apache::Authn::RedminePublicRepoControl

Module for repository access and control to interface with Redmine

=head1 SYNOPSIS

=head1 INSTALLATION

=head1 CONFIGURATION

=cut

use strict;
use warnings FATAL => 'all', NONFATAL => 'redefine';

use DBI;
use Digest::SHA1;

use Apache2::Module;
use Apache2::Access;
use Apache2::ServerRec qw();
use Apache2::RequestRec qw();
use Apache2::RequestUtil qw();
use Apache2::Const qw(:common :override :cmd_how);

my @directives = (
    {
        name         => 'RedmineDSN',
        req_override => OR_AUTHCFG,
        args_how     => TAKE1,
        errmsg       => 'DSN in format used by Perl DBI. eg: "DBI:Pg:dbname=databasename;host=my.db.server"',
    },
    {
        name         => 'RedmineDbUser',
        req_override => OR_AUTHCFG,
        args_how     => TAKE1,
    },
    { 
        name         => 'RedmineDbPass',
        req_override => OR_AUTHCFG,
        args_how     => TAKE1,
    },
);

sub RedmineDSN {
    my ($self, $parms, $arg) = @_;
    $self->{RedmineDSN} = $arg;
    my $query = "SELECT 
    hashed_password, auth_source_id, permissions
    FROM members, projects, users, roles
    WHERE 
    projects.id=members.project_id 
    AND users.id=members.user_id 
    AND roles.id=members.role_id
    AND users.status=1 
    AND login=? 
    AND identifier=? ";
    $self->{RedmineQuery} = trim($query);
}

sub RedmineDbUser { set_val('RedmineDbUser', @_); }
sub RedmineDbPass { set_val('RedmineDbPass', @_); }
sub RedmineDbWhereClause { 
    my ($self, $parms, $arg) = @_;
    $self->{RedmineQuery} = trim($self->{RedmineQuery}.($arg ? $arg : "")." ");
}

sub trim {
    my $string = shift;
    $string =~ s/\s{2,}/ /g;
    return $string;
}

sub set_val {
    my ($key, $self, $parms, $arg) = @_;
    $self->{$key} = $arg;
}

Apache2::Module::add(__PACKAGE__, \@directives);

# This is the list of all apache request methods that we are treating as "read-only" methods
my %read_only_methods = map { $_ => 1 } qw/GET PROPFIND REPORT OPTIONS/;

#
# Access is the first step in the AAA model. This funciton decides whether or not to even ask
# the user to log in. Basically, this is going to always be yes, however, if an anonymous
# person is trying to connect, and we've set that anonymous people can browse public projects
# then we will OK it, and tell apache that the authenicatin handler doesn't need to be called
# or, that the anonymous person doesn't need to log in.
#
sub access_handler {
    my $r = shift;

    if( !defined($read_only_methods{$r->method}) ) {
        return FORBIDDEN;
    }

    my $project_id = get_project_identifier($r);
    my $req_path = get_requested_path($r);

    if( is_anonymous_readable($project_id, $req_path, $r) ) {
        return OK;
    }
    
    return FORBIDDEN;
}

#
# Returns the path requested in the repository
#
sub get_requested_path {
    my $r = shift;

    my $location = $r->location;
    my $path;
    
    if($location ne "/") {
        ($path) = $r->uri =~ m{$location/*[^/]+(/.*)};
    }
    else {
         my @path_items = split /\//, $r->uri;

         if(@path_items <= 2) {
             $path = "/";
         }
         else {
             if($path_items[2] eq "!svn") {
                 if(@path_items <= 5) {
                     return "/!svn";
                 }

                 @path_items = @path_items[5..$#path_items];
             }
             else {
                 @path_items = @path_items[2..$#path_items];
             }

             $path = "/" . join "/", @path_items;
         }
   }

   $path;
}

# 
# Returns the project identifier for a given request
#
sub get_project_identifier {
    my $r = shift;

    my $location = $r->location;
    my $identifier;

    if($location ne "/") {
        ($identifier) = $r->uri =~ m{$location/*([^/]+)};
    }
    else {
        my @path_items = split /\//, $r->uri;
        $identifier = $path_items[1];
    }

    $identifier;
}

#
# Returns a connection to the database
#
sub connect_database {
    my $r = shift;

    my $cfg = Apache2::Module::get_config( __PACKAGE__, $r->server, $r->per_dir_config );
    return DBI->connect( $cfg->{RedmineDSN}, $cfg->{RedmineDbUser},$cfg->{RedmineDbPass} );
}

sub get_role_permission {
    my ($role_id, $project_id, $req_path, $r) = @_;

    if(!defined($project_id)) {
        return '';
    }

    my $dbh = connect_database($r);
    my $sth = $dbh->prepare("SELECT path, permissions FROM repository_controls, projects
                             WHERE repository_controls.role_id='$role_id'
                             AND projects.identifier='$project_id'
                             AND repository_controls.project_id=projects.id");
    $sth->execute;
    
    my $longest_path = 0;
    my $role_perm;
    
    while(my ($path, $perm) = $sth->fetchrow_array()) {
        if($req_path =~ m{^($path).*}) {
            my $path_length = length $path;
            
            if($path_length > $longest_path) {
                $longest_path = $path_length;
                $role_perm = $perm;
            }
        }
    }
    
    $sth->finish();
    $dbh->disconnect();

    return $role_perm;
}

sub is_anonymous_readable {
    my ($project_id, $req_path, $r) = @_;
    my $perm;
    
    if($req_path =~ m{!svn}) {
        return 1;
    }
    else {
        $perm = get_role_permission('2', $project_id, $req_path, $r);
    }

    if($perm and $perm =~ /:browse_repository/) {
        return 1;
    }
    else {
        return 0;
    }
}

1;