# BEGIN BPS TAGGED BLOCK {{{
# 
# COPYRIGHT:
# 
# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC
#                                          <jesse@bestpractical.com>
# 
# (Except where explicitly superseded by other copyright notices)
# 
# 
# LICENSE:
# 
# This work is made available to you under the terms of Version 2 of
# the GNU General Public License. A copy of that license should have
# been provided with this software, but in any event can be snarfed
# from www.gnu.org.
# 
# This work is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 or visit their web page on the internet at
# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
# 
# 
# CONTRIBUTION SUBMISSION POLICY:
# 
# (The following paragraph is not intended to limit the rights granted
# to you to modify and distribute this software under the terms of
# the GNU General Public License and is only of importance to you if
# you choose to contribute your changes and enhancements to the
# community by submitting them to Best Practical Solutions, LLC.)
# 
# By intentionally submitting any modifications, corrections or
# derivatives to this work, or any other work intended for use with
# Request Tracker, to Best Practical Solutions, LLC, you confirm that
# you are the copyright holder for those contributions and you grant
# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
# royalty-free, perpetual, license to use, copy, create derivative
# works based on those contributions, and sublicense and distribute
# those contributions and any derivatives thereof.
# 
# END BPS TAGGED BLOCK }}}

=head1 NAME

  RT::Users - Collection of RT::User objects

=head1 SYNOPSIS

  use RT::Users;


=head1 DESCRIPTION


=head1 METHODS


=cut


package RT::Users;

use strict;
no warnings qw(redefine);

# {{{ sub _Init 
sub _Init {
    my $self = shift;
    $self->{'table'} = 'Users';
    $self->{'primary_key'} = 'id';
    $self->{'with_disabled_column'} = 1;

    my @result = $self->SUPER::_Init(@_);
    # By default, order by name
    $self->OrderBy( ALIAS => 'main',
                    FIELD => 'Name',
                    ORDER => 'ASC' );

    $self->{'princalias'} = $self->NewAlias('Principals');

    # XXX: should be generalized
    $self->Join( ALIAS1 => 'main',
                 FIELD1 => 'id',
                 ALIAS2 => $self->{'princalias'},
                 FIELD2 => 'id' );
    $self->Limit( ALIAS => $self->{'princalias'},
                  FIELD => 'PrincipalType',
                  VALUE => 'User',
                );

    return (@result);
}

# }}}

=head2 PrincipalsAlias

Returns the string that represents this Users object's primary "Principals" alias.

=cut

# XXX: should be generalized
sub PrincipalsAlias {
    my $self = shift;
    return($self->{'princalias'});

}


=head2 LimitToEnabled

Only find items that haven\'t been disabled

=cut

# XXX: should be generalized
sub LimitToEnabled {
    my $self = shift;

    $self->{'handled_disabled_column'} = 1;
    $self->Limit(
        ALIAS    => $self->PrincipalsAlias,
        FIELD    => 'Disabled',
        VALUE    => '0',
    );
}

=head2 LimitToDeleted

Only find items that have been deleted.

=cut

sub LimitToDeleted {
    my $self = shift;
    
    $self->{'handled_disabled_column'} = $self->{'find_disabled_rows'} = 1;
    $self->Limit(
        ALIAS => $self->PrincipalsAlias,
        FIELD => 'Disabled',
        VALUE => 1,
    );
}


# {{{ LimitToEmail

=head2 LimitToEmail

Takes one argument. an email address. limits the returned set to
that email address

=cut

sub LimitToEmail {
    my $self = shift;
    my $addr = shift;
    $self->Limit( FIELD => 'EmailAddress', VALUE => "$addr" );
}

# }}}

# {{{ MemberOfGroup

=head2 MemberOfGroup PRINCIPAL_ID

takes one argument, a group's principal id. Limits the returned set
to members of a given group

=cut

sub MemberOfGroup {
    my $self  = shift;
    my $group = shift;

    return $self->loc("No group specified") if ( !defined $group );

    my $groupalias = $self->NewAlias('CachedGroupMembers');

    # Join the principal to the groups table
    $self->Join( ALIAS1 => $self->PrincipalsAlias,
                 FIELD1 => 'id',
                 ALIAS2 => $groupalias,
                 FIELD2 => 'MemberId' );

    $self->Limit( ALIAS    => "$groupalias",
                  FIELD    => 'GroupId',
                  VALUE    => "$group",
                  OPERATOR => "=" );
}

# }}}

# {{{ LimitToPrivileged

=head2 LimitToPrivileged

Limits to users who can be made members of ACLs and groups

=cut

sub LimitToPrivileged {
    my $self = shift;

    my $priv = RT::Group->new( $self->CurrentUser );
    $priv->LoadSystemInternalGroup('Privileged');
    unless ( $priv->Id ) {
        $RT::Logger->crit("Couldn't find a privileged users group");
    }
    $self->MemberOfGroup( $priv->PrincipalId );
}

# }}}

# {{{ WhoHaveRight

=head2 WhoHaveRight { Right => 'name', Object => $rt_object , IncludeSuperusers => undef, IncludeSubgroupMembers => undef, IncludeSystemRights => undef, EquivObjects => [ ] }


find all users who the right Right for this group, either individually
or as members of groups

If passed a queue object, with no id, it will find users who have that right for _any_ queue

=cut

# XXX: should be generalized
sub _JoinGroupMembers
{
    my $self = shift;
    my %args = (
        IncludeSubgroupMembers => 1,
        @_
    );

    my $principals = $self->PrincipalsAlias;

    # The cachedgroupmembers table is used for unrolling group memberships
    # to allow fast lookups. if we bind to CachedGroupMembers, we'll find
    # all members of groups recursively. if we don't we'll find only 'direct'
    # members of the group in question
    my $group_members;
    if ( $args{'IncludeSubgroupMembers'} ) {
        $group_members = $self->NewAlias('CachedGroupMembers');
    }
    else {
        $group_members = $self->NewAlias('GroupMembers');
    }

    $self->Join(
        ALIAS1 => $group_members,
        FIELD1 => 'MemberId',
        ALIAS2 => $principals,
        FIELD2 => 'id'
    );

    return $group_members;
}

# XXX: should be generalized
sub _JoinGroups
{
    my $self = shift;
    my %args = (@_);

    my $group_members = $self->_JoinGroupMembers( %args );
    my $groups = $self->NewAlias('Groups');
    $self->Join(
        ALIAS1 => $groups,
        FIELD1 => 'id',
        ALIAS2 => $group_members,
        FIELD2 => 'GroupId'
    );

    return $groups;
}

# XXX: should be generalized
sub _JoinACL
{
    my $self = shift;
    my %args = (
        Right                  => undef,
        IncludeSuperusers      => undef,
        @_,
    );

    if ( $args{'Right'} ) {
        my $canonic = RT::ACE->CanonicalizeRightName( $args{'Right'} );
        unless ( $canonic ) {
            $RT::Logger->error("Invalid right. Couldn't canonicalize right '$args{'Right'}'");
        }
        else {
            $args{'Right'} = $canonic;
        }
    }

    my $acl = $self->NewAlias('ACL');
    $self->Limit(
        ALIAS    => $acl,
        FIELD    => 'RightName',
        OPERATOR => ( $args{Right} ? '=' : 'IS NOT' ),
        VALUE => $args{Right} || 'NULL',
        ENTRYAGGREGATOR => 'OR'
    );
    if ( $args{'IncludeSuperusers'} and $args{'Right'} ) {
        $self->Limit(
            ALIAS           => $acl,
            FIELD           => 'RightName',
            OPERATOR        => '=',
            VALUE           => 'SuperUser',
            ENTRYAGGREGATOR => 'OR'
        );
    }
    return $acl;
}

# XXX: should be generalized
sub _GetEquivObjects
{
    my $self = shift;
    my %args = (
        Object                 => undef,
        IncludeSystemRights    => undef,
        EquivObjects           => [ ],
        @_
    );
    return () unless $args{'Object'};

    my @objects = ($args{'Object'});
    if ( UNIVERSAL::isa( $args{'Object'}, 'RT::Ticket' ) ) {
        # If we're looking at ticket rights, we also want to look at the associated queue rights.
        # this is a little bit hacky, but basically, now that we've done the ticket roles magic,
        # we load the queue object and ask all the rest of our questions about the queue.

        # XXX: This should be abstracted into object itself
        if( $args{'Object'}->id ) {
            push @objects, $args{'Object'}->ACLEquivalenceObjects;
        } else {
            push @objects, 'RT::Queue';
        }
    }

    if( $args{'IncludeSystemRights'} ) {
        push @objects, 'RT::System';
    }
    push @objects, @{ $args{'EquivObjects'} };
    return grep $_, @objects;
}

# XXX: should be generalized
sub WhoHaveRight {
    my $self = shift;
    my %args = (
        Right                  => undef,
        Object                 => undef,
        IncludeSystemRights    => undef,
        IncludeSuperusers      => undef,
        IncludeSubgroupMembers => 1,
        EquivObjects           => [ ],
        @_
    );

    if ( defined $args{'ObjectType'} || defined $args{'ObjectId'} ) {
        $RT::Logger->crit( "WhoHaveRight called with the Obsolete ObjectId/ObjectType API");
        return (undef);
    }

    my $from_role = $self->Clone;
    $from_role->WhoHaveRoleRight( %args );

    my $from_group = $self->Clone;
    $from_group->WhoHaveGroupRight( %args );

    #XXX: DIRTY HACK
    use DBIx::SearchBuilder::Union;
    my $union = new DBIx::SearchBuilder::Union;
    $union->add( $from_group );
    $union->add( $from_role );
    %$self = %$union;
    bless $self, ref($union);

    return;
}
# }}}

# XXX: should be generalized
sub WhoHaveRoleRight
{
    my $self = shift;
    my %args = (
        Right                  => undef,
        Object                 => undef,
        IncludeSystemRights    => undef,
        IncludeSuperusers      => undef,
        IncludeSubgroupMembers => 1,
        EquivObjects           => [ ],
        @_
    );

    my @objects = $self->_GetEquivObjects( %args );
    my @roles = RT::Principal->RolesWithRight( %args );
    unless ( @roles ) {
        $self->_AddSubClause( "WhichRole", "(main.id = 0)" );
        return;
    }

    my $groups = $self->_JoinGroups( %args );

    # no system user
    $self->Limit( ALIAS => $self->PrincipalsAlias,
                  FIELD => 'id',
                  OPERATOR => '!=',
                  VALUE => $RT::SystemUser->id
                );

    $self->_AddSubClause( "WhichRole", "(". join( ' OR ', map "$groups.Type = '$_'", @roles ) .")" );

    my @groups_clauses = $self->_RoleClauses( $groups, @objects );
    $self->_AddSubClause( "WhichObject", "(". join( ' OR ', @groups_clauses ) .")" )
        if @groups_clauses;

    return;
}

sub _RoleClauses {
    my $self = shift;
    my $groups = shift;
    my @objects = @_;

    my @groups_clauses;
    foreach my $obj ( @objects ) {
        my $type = ref($obj)? ref($obj): $obj;
        my $id;
        $id = $obj->id if ref($obj) && UNIVERSAL::can($obj, 'id') && $obj->id;

        my $role_clause = "$groups.Domain = '$type-Role'";
        # XXX: Groups.Instance is VARCHAR in DB, we should quote value
        # if we want mysql 4.0 use indexes here. we MUST convert that
        # field to integer and drop this quotes.
        $role_clause   .= " AND $groups.Instance = '$id'" if $id;
        push @groups_clauses, "($role_clause)";
    }
    return @groups_clauses;
}

# XXX: should be generalized
sub _JoinGroupMembersForGroupRights
{
    my $self = shift;
    my %args = (@_);
    my $group_members = $self->_JoinGroupMembers( %args );
    $self->Limit( ALIAS => $args{'ACLAlias'},
                  FIELD => 'PrincipalId',
                  VALUE => "$group_members.GroupId",
                  QUOTEVALUE => 0,
                );
}

# XXX: should be generalized
sub WhoHaveGroupRight
{
    my $self = shift;
    my %args = (
        Right                  => undef,
        Object                 => undef,
        IncludeSystemRights    => undef,
        IncludeSuperusers      => undef,
        IncludeSubgroupMembers => 1,
        EquivObjects           => [ ],
        @_
    );

    # Find only rows where the right granted is
    # the one we're looking up or _possibly_ superuser
    my $acl = $self->_JoinACL( %args );

    my ($check_objects) = ('');
    my @objects = $self->_GetEquivObjects( %args );

    if ( @objects ) {
        my @object_clauses;
        foreach my $obj ( @objects ) {
            my $type = ref($obj)? ref($obj): $obj;
            my $id;
            $id = $obj->id if ref($obj) && UNIVERSAL::can($obj, 'id') && $obj->id;

            my $object_clause = "$acl.ObjectType = '$type'";
            $object_clause   .= " AND $acl.ObjectId   = $id" if $id;
            push @object_clauses, "($object_clause)";
        }

        $check_objects = join ' OR ', @object_clauses;
    } else {
        if( !$args{'IncludeSystemRights'} ) {
            $check_objects = "($acl.ObjectType != 'RT::System')";
        }
    }
    $self->_AddSubClause( "WhichObject", "($check_objects)" );
    
    $self->_JoinGroupMembersForGroupRights( %args, ACLAlias => $acl );
    # Find only members of groups that have the right.
    $self->Limit( ALIAS => $acl,
                  FIELD => 'PrincipalType',
                  VALUE => 'Group',
                );
    
    # no system user
    $self->Limit( ALIAS => $self->PrincipalsAlias,
                  FIELD => 'id',
                  OPERATOR => '!=',
                  VALUE => $RT::SystemUser->id
                );
    return;
}

# {{{ WhoBelongToGroups

=head2 WhoBelongToGroups { Groups => ARRAYREF, IncludeSubgroupMembers => 1 }

=cut

# XXX: should be generalized
sub WhoBelongToGroups {
    my $self = shift;
    my %args = ( Groups                 => undef,
                 IncludeSubgroupMembers => 1,
                 @_ );

    # Unprivileged users can't be granted real system rights.
    # is this really the right thing to be saying?
    $self->LimitToPrivileged();

    my $group_members = $self->_JoinGroupMembers( %args );

    foreach my $groupid (@{$args{'Groups'}}) {
        $self->Limit( ALIAS           => $group_members,
                      FIELD           => 'GroupId',
                      VALUE           => $groupid,
                      QUOTEVALUE      => 0,
                      ENTRYAGGREGATOR => 'OR',
                    );
    }
}
# }}}


1;
