package LATMOS::Accounts::Bases::Ad::User;

use 5.010000;
use strict;
use warnings;

use base qw(LATMOS::Accounts::Bases::Ad::objects);
use Net::LDAP;
use Net::LDAPS;
use Net::LDAP::Entry;
use Net::LDAP::Control::Paged;
use Net::LDAP::Constant qw( LDAP_CONTROL_PAGED ); 
use Net::LDAP::Util     qw( escape_filter_value );
use LATMOS::Accounts::Log;
use LATMOS::Accounts::Utils;

our $VERSION = (q$Rev$ =~ /^Rev: (\d+) /)[0];

=head1 NAME

LATMOS::Ad - Perl extension for blah blah blah

=head1 SYNOPSIS

  use LATMOS::Ad;
  blah blah blah

=head1 DESCRIPTION

Stub documentation for LATMOS::Ad, created by h2xs. It looks like the
author of the extension was negligent enough to leave the stub
unedited.

Blah blah blah.

=head1 FUNCTIONS

=cut

sub _class_filter { '(&(ObjectClass=user) (!(ObjectClass=computer)))' }

sub _key_attr { 'cn' } 

sub _my_ldap_classes { qw(top person organizationalPerson user) }

sub _get_attr_schema {
    my ($class, $base) = @_;
    my $info = {
        sn => { },
        name => { },
        givenName => { },
        sAMAccountName => { },
        uid => { uniq => 1, },
        gecos => { },
        homeDirectory => { },
        loginShell => { },
        uidNumber => { uniq => 1, },
        gidNumber => {
            mandatory => 1,
            can_values => sub {
                map { $base->get_object('group',
                        $_)->get_attributes('gidNumber') }
                $base->list_objects('group')
            },
            display => sub {
                my ($self, $val) = @_;
                my ($gr) = $self->base->search_objects('group', "gidNumber=$val")
                    or return;
                return $gr;
            },
            reference => 'group',
        },
        shadowLastChange => { },
        shadowMin => { },
        shadowMax => { },
        shadowWarning => { },
        shadowInactive => { },
        shadowExpire => { },
        shadowFlag => { },
        description => { },
        mail => { },
        telephoneNumber => { },
        ipPhone => { },
        otherTelephone => { },
        department => { },
        title => { },
        mobile => { },
        homePhone => { },
        accountExpires => { },
        streetAddress => { },
        postalCode => { },
        postOfficeBox => { },
        l => { },
        physicalDeliveryOfficeName => { },
        company => { },
        st => { },
        displayName => { },
        initials => { },
        manager => {
            delayed => 1,
            can_values => sub { $base->list_objects('user') },
        },
        userAccountControl => { },
        locked => { },
        memberOf => {
            multiple => 1,
            delayed => 1,
        },
        winhomeDirectory => { },
        facsimileTelephoneNumber => { },
        cn => { ro => 1, },
        dn => { ro => 1, },
        uSNCreated => { ro => 1, },
        uSNChanged => { ro => 1, },
        directReports => { ro => 1, },
        objectClass => { ro => 1, },
        msSFU30NisDomain => { },
        msSFU30Name => { },
        labeledURI => {},
        wWWHomePage => {},
        userPassword => { readable => 0 },
    };

    $info->{$class->_key_attribute($base)}{ro} = 1;
    $info->{$class->_dn_attribute($base)}{ro} = 1;

    $info
}

sub _create {
    my ($class, $base, $id, %data) = @_;

    my $entry = Net::LDAP::Entry->new();

    $entry->dn(join(',',
        sprintf('%s=%s',
            $class->_dn_attribute($base), 
            escape_filter_value($id)),
            $base->object_base_dn($class->type),
    ));
    $data{'sAMAccountName'} ||= $id;
    $entry->replace(objectClass => [ $class->_my_ldap_classes ],);
    # Must be 544 for creation...
    $entry->replace(userAccountControl => 544); #66112);
    # This attributes cannot be set via LDAP:
    #$entry->replace(sAMAccountType => 0x30000000);
    $entry->replace(accountExpires => '9223372036854775807'); # TODO hardcoded, burk
    $entry->replace(userPrincipalName => "$id\@" . $base->ad_domain);

    {
        my $charmap = Unicode::Map8->new('latin1')  or  die;
        my $clear_pass = LATMOS::Accounts::Utils::genpassword(lenght => 20, nonalpha => 1);
        my $newUniPW = $charmap->tou('"'.$clear_pass.'"')->byteswap()->utf16();
        $entry->replace(unicodePwd => $newUniPW);
    }

    my %delayed;
    $data{$class->_key_attribute($base)} = $id;
    foreach (keys %data) {
	/^memberOf$/ and do {
	    $delayed{$_} = $data{$_};
	    next;
        };
        $class->_populate_entry($entry, $_, $data{$_}, $base);
    }
    my $msg = $base->ldap->add($entry);
    if ($msg->code) {
    	$base->log(LA_ERR, "Cannot create user: %s", $msg->error);
    	return 0;
    }
    if (my $obj = $base->get_object('user', $id)) {
        my $res = $obj->set_fields(%delayed);
        return defined($res) ? 1 : 0;
    } else {
        $base->log(LA_ERR, "Cannot get just created object %s/%s", 'user', $id);
        return 0;
    }
}

sub get_field {
    my ($self, $field) = @_;
    require LATMOS::Accounts::Bases::Ad::Group;

    $field eq 'memberOf' and do {
        my @res;
        $self->base->_unlimited_search(
            base => $self->base->object_base_dn('group'),
            filter => sprintf(
                '(&(objectClass=group)(member=%s))',
                escape_filter_value($self->{entry}->dn),
            ),
            callback => sub {
                my ($mesg, $entry) = @_;
                ref $entry eq 'Net::LDAP::Entry' or return;
                push(@res, $entry->get_value(
                        LATMOS::Accounts::Bases::Ad::Group->_key_attribute($self->base)
                    )
                );
            },
        );
        return [ sort(@res) ];
    };
    $field eq 'directReports' and do {
        my $res = $self->SUPER::get_field($field) or return;
        return [
            map { $self->base->_get_object_from_dn($_)->get_value(
                LATMOS::Accounts::Bases::Ad::User->_key_attribute($self->base)
            ) }
            @{ ref $res ? $res : [ $res ] }
        ];
    };
    $field eq 'manager' and do {
        my $dn = $self->SUPER::get_field($field) or return;
        return $self->base->_get_object_from_dn($dn)->get_value(
            LATMOS::Accounts::Bases::Ad::User->_key_attribute($self->base)
        );
    };
    $field eq 'homeDirectory' and do {
        $field = 'unixHomeDirectory';
    };
    $field eq 'winhomeDirectory' and do {
        $field = 'homeDirectory';
    };
    $self->SUPER::get_field($field);
}

sub _populate_entry {
    my ($self, $entry, $field, $value, $base) = @_;
    $base ||= $self->base;
    # MS Ad sucks, does not respect RFC
    # homeDirectory => unixHomeDirectory
    # Win home => homeDirectory...
    # The question is now: do we have to manage Ad homeDirectory field ?
    for ($field) {
        /^homeDirectory$/ and do {
            $field = 'unixHomeDirectory';
            next;
        };
        /^winhomeDirectory$/ and do {
            $field = 'homeDirectory';
            next;
        };
        /^manager$/ && $value and do {
            my $user = $base->get_object('user', $value) or do {
                $value = undef;
                next;
            };
            $value = $user->get_field('dn');
            next;
        };
        /^locked$/ and do {
            my $uac = $entry->get_value('userAccountControl') || 0;
            if ($value) {
                $uac |= 0x00000002;
            } else {
                $uac &= (0xFFFFFFFF ^ 0x00000002);
            }
            $self->SUPER::_populate_entry($entry, 'userAccountControl', "$uac",
                $base);
            return; # nothing more to do
        };
        /^gecos$/ and do {
            $value = to_ascii($value);
        };
    }
    $self->SUPER::_populate_entry($entry, $field, $value, $base);
}

sub set_fields {
    my ($self, %data) = @_;
    my %ndata;
    while (my ($f, $val) = each(%data)) {
        $f eq 'memberOf' and do {
            my %users;
            $users{$_}{e} = 1 foreach (@{ $self->get_field('memberOf') || []});
            $users{$_}{n} = 1 foreach (@{ $val || []});
            foreach (keys %users) {
                $users{$_}{e} && $users{$_}{n} and next;
                my $group = $self->base->get_object('group', $_) or next;
                # memberOf field is read only
                # so we modify the peer entry into peer object
                if ($users{$_}{e}) {
                    $group->{entry}->delete(member => $self->get_field('dn'));
                } elsif ($users{$_}{n}) {
                    $group->{entry}->add(member => $self->get_field('dn'));
                } # else {} # can't happen
                my $mesg = $group->{entry}->update($self->base->ldap);
                if ($mesg->code) {
                    $self->base->log(LA_ERR, "Cannot set attributes: %s", $mesg->error);
                    return;
                }
            }
            next;
        };
        $ndata{$f} = $val;
    }
    $self->SUPER::set_fields(%ndata);
}

sub _set_password {
    my ($self, $clear_pass) = @_;
    my $charmap = Unicode::Map8->new('latin1')  or  die;
    my $newUniPW = $charmap->tou('"'.$clear_pass.'"')->byteswap()->utf16();
    my $mesg = $self->base->ldap->modify(
        $self->_get_c_field('dn'),
        changes => [
        #replace => [ userPassword => $clear_pass ],
        replace => [ unicodePwd => $newUniPW ],

        ]
    );
    if ($mesg->code) {
        $self->base->log(LA_ERR, "Cannot set password: %s", $mesg->error);
        return;
    }

    my $userAccountControl = $self->get_field('userAccountControl');
    # http://msdn.microsoft.com/en-us/library/ms680832(VS.85).aspx
    $userAccountControl |= 0x00010000; # ADS_UF_DONT_EXPIRE_PASSWD
                                       # The password for this account will never expire.
    $userAccountControl |= 0x00000040; # ADS_UF_PASSWD_CANT_CHANGE
                                       # The user cannot change the password.
    $self->set_fields(userAccountControl => $userAccountControl);

    $self->base->log(LA_NOTICE, 'Mot de passe changé pour %s', $self->id);

    1;
}

1;

__END__

=head1 SEE ALSO

=head1 AUTHOR

Olivier Thauvin, E<lt>olivier.thauvin@aerov.jussieu.frE<gt>

=head1 COPYRIGHT AND LICENSE

Copyright (C) 2008 CNRS SA/CETP/LATMOS

This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself, either Perl version 5.10.0 or,
at your option, any later version of Perl 5 you may have available.


=cut
