source: trunk/LATMOS-Accounts/lib/LATMOS/Accounts/Bases/Sql/User.pm @ 1768

Last change on this file since 1768 was 1768, checked in by nanardon, 8 years ago

Avoid undef warning

  • Property svn:keywords set to Id Rev
File size: 65.6 KB
Line 
1package LATMOS::Accounts::Bases::Sql::User;
2
3use 5.010000;
4use strict;
5use warnings;
6use overload '""' => 'stringify';
7
8use LATMOS::Accounts::Utils;
9use LATMOS::Accounts::Log;
10use POSIX qw(strftime);
11use Date::Parse qw(str2time);
12use DateTime;
13use DateTime::TimeZone;
14use base qw(LATMOS::Accounts::Bases::Sql::objects);
15use LATMOS::Accounts::I18N;
16use Date::Calc;
17use LATMOS::Accounts::Mail;
18
19our $VERSION = (q$Rev$ =~ /^Rev: (\d+) /)[0];
20
21=head1 NAME
22
23LATMOS::Ad - Perl extension for blah blah blah
24
25=head1 DESCRIPTION
26
27Account base access over standard unix file format.
28
29=head1 FUNCTIONS
30
31=cut
32
33=head2 new(%config)
34
35Create a new LATMOS::Ad object for windows AD $domain.
36
37domain / server: either the Ad domain or directly the server
38
39ldap_args is an optionnal list of arguments to pass to L<Net::LDAP>.
40
41=cut
42
43sub _object_table { 'user' }
44
45sub _key_field { 'name' }
46
47sub _has_extended_attributes { 1 }
48
49sub stringify {
50    my ($self) = @_;
51
52    return join(' ', grep { $_ }
53        (
54            $self->get_field('givenName'),
55            $self->get_field('sn')
56        )
57    )
58    || $self->get_field('description')
59    || $self->id;
60}
61
62sub _get_attr_schema {
63    my ($class, $base) = @_;
64
65    my $subsetaddress = sub {
66        my ($self, $data) = @_;
67        my $fmainaddress = $self->object->_get_c_field('mainaddress');
68
69        # set address attribute => create address object on the fly
70        # except if attr is empty !
71        if (!$fmainaddress && $data) {
72            $fmainaddress = $self->object->id . '-' . join('', map { ('a'..'z')[rand(26)] }
73                (0..4));
74            $self->base->_create_c_object(
75                'address', $fmainaddress,
76                user => $self->object->id,
77                isMainAddress => 1, ) or do {
78                $self->base->log(LA_ERR,
79                    "Cannot create main address for user %s", $self->object->id);
80                return;
81            };
82        }
83        if ($fmainaddress && 
84            (my $address = $self->base->get_object('address', $fmainaddress))) {
85            if ($address->attribute($self->name) &&
86                !$address->attribute($self->name)->ro) {
87                return $address->set_c_fields($self->name => $data) ||0;
88            }
89        }
90    };
91
92    my $attrs = {
93            exported => {
94                post => sub {
95                    my ($self, $value) = @_;
96                    my $attr = $self->name;
97
98                    if (my $obj = $self->base->
99                            get_object('revaliases', $self->object->id)) {
100                        my $ares = $obj->set_c_fields(
101                            ($attr eq 'exported' ? 'exported' : 'unexported') => $value
102                        );
103                        if (!defined($ares)) {
104                            $self->base->log(LA_ERR,
105                                'Cannot set revaliases exported attribute for user %s',
106                                $self->object->id);
107                        }
108                    }
109                    my $must_expire = $self->name eq 'exported'
110                        ? ($value ? 0 : 1 )
111                        : ($value ? 1 : 0 );
112
113                    foreach my $al (grep { $_ } $self->object->get_attributes('aliases'), $self->object->id) {
114                        my $obj = $self->base->get_object('aliases', $al) or next;
115                        $obj->_set_c_fields(
116                            exported => $value,
117                        );
118                    }
119                    foreach my $al ($self->object->get_attributes('otheraddress')) {
120                        my $obj = $self->base->get_object('address', $al) or next;
121                        $obj->_set_c_fields(
122                            exported => $value,
123                        );
124                    }
125                },
126            },
127            uidNumber => {
128                inline => 1,
129                iname => 'uidnumber',
130                uniq => 1,
131                mandatory => 1,
132                formopts => { length => 7 },
133                label => l('UID'),
134            },
135            uidnumber => { inline => 1, hide => 1, monitored => 1 },
136            gidNumber => {
137                inline => 1,
138                iname => 'gidnumber',
139                mandatory => 1,
140                can_values => sub {
141                    map { $_->id, $_->get_attributes('gidNumber') }
142                    map { $base->get_object('group', $_) }
143                    $base->list_objects('group')
144                },
145                can_values => sub {
146                    my $sth = $base->db->prepare_cached(
147                        q{
148                        select name, gidnumber from "group" where exported = true
149                        }
150                    );
151                    $sth->execute();
152                    my @res;
153                    while (my $res = $sth->fetchrow_hashref) {
154                        push(@res, $res->{name});
155                        push(@res, $res->{gidnumber});
156                    }
157                    return @res;
158                },
159                display => sub {
160                    my ($self, $val) = @_;
161                    my ($gr) = $self->base->search_objects('group', "gidNumber=$val")
162                        or return;
163                    return $gr;
164                },
165                input => sub {
166                    my ($val) = @_;
167                    $val =~ /^\d+$/ and return $val;
168                    my ($gr) = $base->search_objects('group', "name=$val") or return;
169                    return $base->get_object('group', $gr)->get_attributes('gidNumber');
170                },
171                reference => 'group',
172                label => l('GID'),
173            },
174            loginShell => {
175                mandatory => 1,
176                label => l('Shell'),
177            },
178            gidnumber => { inline => 1, hide => 1,
179                can_values => sub {
180                    map { $_->id, $_->get_attributes('gidNumber') }
181                    map { $base->get_object('group', $_) }
182                    $base->list_objects('group')
183                },
184                display => sub {
185                    my ($self, $val) = @_;
186                    my ($gr) = $self->base->search_objects('group', "gidNumber=$val")
187                        or return;
188                    return $gr;
189                },
190                input => sub {
191                    my ($val) = @_;
192                    $val =~ /^\d+$/ and return $val;
193                    my ($gr) = $base->search_objects('group', "name=$val") or return;
194                    return $base->get_object('group', $gr)->get_attributes('gidNumber');
195                },
196                mandatory => 1,
197                reference => 'group',
198                monitored => 1,
199            },
200            locked    => {
201                formtype => 'CHECKBOX',
202                formopts => { rawvalue => 1, },
203                monitored => 1,
204                label => l('Locked'),
205            },
206            expire        => {
207                inline => 1,
208                formtype => 'DATETIME',
209                monitored => 1,
210                label => l('Expire on'),
211            },
212            endcircuit    => {
213                inline => 1,
214                formtype => 'DATE',
215                monitored => 1,
216                label => l('End of entrance'),
217            },
218            _endEmployment => {
219                formtype => 'DATETIME',
220                managed => 1,
221                ro => 1,
222                hide => 1,
223                get => sub {
224                    my ($attr) = @_;
225                    my $self = $attr->object;
226                    $self->_computeEndEmployment($self->base->config('employment_delay') || 0);
227                },
228                label => l('End of employment'),
229            },
230            endEmployment => {
231                formtype => 'DATETIME',
232                ro => 1,
233                label => l('End of employment'),
234            },
235            _departure => {
236                formtype => 'DATETIME',
237                managed => 1,
238                ro => 1,
239                hide => 1,
240                get => sub {
241                    my ($attr) = @_;
242                    my $self = $attr->object;
243                    $self->_computeEndEmployment($self->base->config('employment_delay') || 0, 1, 1);
244                },
245                label => l('Start of employment'),
246            },
247            departure => {
248                formtype => 'DATETIME',
249                ro => 1,
250                label => l('Arrival'),
251            },
252            _endStrictEmployment => {
253                formtype => 'DATETIME',
254                managed => 1,
255                ro => 1,
256                hide => 1,
257                get => sub {
258                    my ($attr) = @_;
259                    my $self = $attr->object;
260                    $self->_computeEndEmployment();
261                },
262            },
263            endStrictEmployment => {
264                formtype => 'DATETIME',
265                ro => 1,
266            },
267            _endCurrentEmployment => {
268                formtype => 'DATETIME',
269                managed => 1,
270                ro => 1,
271                hide => 1,
272                get => sub {
273                    my ($attr) = @_;
274                    my $self = $attr->object;
275
276                    my $list_empl = $self->base->db->prepare_cached(q{
277                        SELECT * FROM employment WHERE "user" = ? and
278                            firstday <= now() and
279                            (lastday is null or lastday >= now() - '1 days'::interval)
280                            order by firstday asc
281                    });
282                    $list_empl->execute($self->id);
283                    my $end;
284                    while (my $res = $list_empl->fetchrow_hashref) {
285                        if (!$res->{lastday}) {
286                            # Ultimate employment.
287                            $list_empl->finish;
288                            return undef;
289                        } else {
290                            $end = DateTime->from_epoch(epoch => str2time($res->{lastday}));
291                            $end->set_time_zone( DateTime::TimeZone->new( name => 'local' ) );
292                            $end->add(hours => 23, minutes => 59, seconds => 59);
293                        }
294                        last;
295                    }
296                    $list_empl->finish;
297
298                    return $end ? $end->iso8601 : undef
299                },
300            },
301            endCurrentEmployment => {
302                formtype => 'DATETIME',
303                ro => 1,
304            },
305            _endLastEmployment => {
306                formtype => 'DATETIME',
307                managed => 1,
308                ro => 1,
309                hide => 1,
310                get => sub {
311                    my ($attr) = @_;
312                    my $self = $attr->object;
313
314                    my $list_empl = $self->base->db->prepare_cached(q{
315                        SELECT * FROM employment WHERE "user" = ?
316                        order by lastday desc nulls first
317                    });
318                    $list_empl->execute($self->id);
319                    my $res = $list_empl->fetchrow_hashref;
320                    $list_empl->finish;
321                    my $end;
322                    if ($res && $res->{lastday}) {
323                        $end = DateTime->from_epoch(epoch => str2time($res->{lastday}));
324                        $end->set_time_zone( DateTime::TimeZone->new( name => 'local' ) );
325                        $end->add(hours => 23, minutes => 59, seconds => 59);
326                    }
327                    return $end ? $end->iso8601 : undef
328                },
329                label => l('End of any employment'),
330            },
331            endLastEmployment => {
332                formtype => 'DATETIME',
333                ro => 1,
334                label => l('End of any employment'),
335            },
336            _startEmployment => {
337                formtype => 'DATETIME',
338                managed => 1,
339                ro => 1,
340                hide => 1,
341                get => sub {
342                    my ($attr) = @_;
343                    my $self = $attr->object;
344                    $self->_computeStartEmployment($self->base->config('employment_delay') || 0);
345                },
346                label => l('Start of employment'),
347            },
348            startEmployment => {
349                formtype => 'DATETIME',
350                ro => 1,
351                label => l('Start of employment'),
352            },
353            _arrival => {
354                formtype => 'DATETIME',
355                managed => 1,
356                ro => 1,
357                hide => 1,
358                get => sub {
359                    my ($attr) = @_;
360                    my $self = $attr->object;
361                    $self->_computeStartEmployment($self->base->config('employment_delay') || 0, 1, 1);
362                },
363                label => l('Start of employment'),
364            },
365            arrival => {
366                formtype => 'DATETIME',
367                ro => 1,
368                label => l('Arrival'),
369            },           
370            _startStrictEmployment => {
371                formtype => 'DATETIME',
372                managed => 1,
373                ro => 1,
374                hide => 1,
375                get => sub {
376                    my ($attr) = @_;
377                    my $self = $attr->object;
378                    $self->_computeStartEmployment();
379                },
380            },
381            startStrictEmployment => {
382                formtype => 'DATETIME',
383                ro => 1,
384            },
385            _startCurrentEmployment => {
386                formtype => 'DATETIME',
387                managed => 1,
388                ro => 1,
389                hide => 1,
390                get => sub {
391                    my ($attr) = @_;
392                    my $self = $attr->object;
393
394                    my $list_empl = $self->base->db->prepare_cached(q{
395                        SELECT * FROM employment WHERE "user" = ? and
396                            firstday <= now() and
397                            (lastday is null or lastday >= now() - '1 days'::interval)
398                            order by firstday asc
399                    });
400                    $list_empl->execute($self->id);
401                    my $start;
402                    while (my $res = $list_empl->fetchrow_hashref) {
403                        $start = DateTime->from_epoch(epoch => str2time($res->{firstday}));
404                        $start->set_time_zone( DateTime::TimeZone->new( name => 'local' ) );
405                        last;
406                    }
407                    $list_empl->finish;
408
409                    return $start ? $start->iso8601 : undef
410                },
411            },
412            startCurrentEmployment => {
413                formtype => 'DATETIME',
414                ro => 1,
415            },
416            _startFirstEmployment => {
417                formtype => 'DATETIME',
418                managed => 1,
419                ro => 1,
420                hide => 1,
421                get => sub {
422                    my ($attr) = @_;
423                    my $self = $attr->object;
424
425                    my $list_empl = $self->base->db->prepare_cached(q{
426                        SELECT * FROM employment WHERE "user" = ?
427                        order by firstday asc
428                    });
429                    $list_empl->execute($self->id);
430                    my $res = $list_empl->fetchrow_hashref;
431                    $list_empl->finish;
432                    my $start;
433                    if ($res && $res->{firstday}) {
434                        $start = DateTime->from_epoch(epoch => str2time($res->{firstday}));
435                        $start->set_time_zone( DateTime::TimeZone->new( name => 'local' ) );
436                    }
437                    return $start ? $start->iso8601 : undef
438                },
439                label => l('Start of any employment'),
440            },
441            startFirstEmployment => {
442                formtype => 'DATETIME',
443                ro => 1,
444                label => l('Start of any employment'),
445            },
446            employmentLength => {
447                ro => 1,
448                managed => 1,
449                get => sub {
450                    my ($self) = @_;
451                    my $lastday = $self->object->get_attributes('endEmployment') || DateTime->now->ymd('-');
452                    my $firstday = $self->object->get_attributes('startEmployment') or return;
453
454                    my @fd = split('-', DateTime->from_epoch(epoch => str2time($firstday))->ymd('-'));
455                    my @ld = split('-', DateTime->from_epoch(epoch => str2time($lastday))->ymd('-'));
456
457                    return Date::Calc::Delta_Days(@fd, @ld) +1;
458                },
459                label => l('Work duration'),
460            },
461            employmentLengthText => {
462                ro => 1,
463                managed => 1,
464                get => sub {
465                    my ($self) = @_;
466                    my $firstday = $self->object->get_attributes('startEmployment') or return;
467                    my $lastday = $self->object->get_attributes('endEmployment')|| DateTime->now->ymd('-');
468                    {
469                        my $dtlast = DateTime->from_epoch(epoch => str2time($lastday));
470                        $dtlast->add(days => 1);
471                        $lastday = $dtlast->ymd('-');
472                    }
473
474                    my @fd = split('-', DateTime->from_epoch(epoch => str2time($firstday))->ymd('-'));
475                    my @ld = split('-', DateTime->from_epoch(epoch => str2time($lastday))->ymd('-'));
476
477                    my ($Dy,$Dm,$Dd) = Date::Calc::N_Delta_YMD(@fd, @ld);
478                    return join(', ',
479                        ($Dy ? l('%d years', $Dy)  : ()),
480                        ($Dm ? l('%d months', $Dm) : ()),
481                        ($Dd ? l('%d days', $Dd)   : ()),
482                    );
483                },
484                label => l('Work duration'),
485            },
486            cn        => {
487                inline => 1, ro => 1,
488                get => sub {
489                    my ($self) = @_;
490                    return join(' ', grep { $_ } 
491                        (
492                            $self->object->_get_c_field('givenName'),
493                            $self->object->_get_c_field('sn')
494                        )
495                    )
496                    || $self->object->_get_c_field('description')
497                    || $self->object->id;
498                },
499            },
500            memberOf  => {
501                reference => 'group',
502                multiple => 1, delayed => 1,
503                get => sub {
504                    my ($self) = @_;
505                    my $obj = $self->object;
506                    my $sth = $obj->db->prepare_cached(
507                        q{
508                        select name from "group" join
509                        group_attributes on group_attributes.okey = "group".ikey
510                        where value = ? and attr = ?
511                        } . ($self->base->{wexported} ? '' : ' and "group".exported = true')
512                    );
513                    $sth->execute($obj->id, 'memberUID');
514                    my @res;
515                    while (my $res = $sth->fetchrow_hashref) {
516                        push(@res, $res->{name});
517                    }
518                    return \@res;
519                },
520                set => sub {
521                    my ($self, $values) = @_;
522                    my %old = map { $_ => 'o' } @{ $self->get };
523                    foreach my $group (grep { $_ } ref $values ? @{ $values } : $values) {
524                        if ($old{$group}) {
525                            $old{$group} = undef; # no change
526                        } else {
527                            $old{$group} = 'n';
528                        }
529                    }
530
531                    my $res = 0;
532                    foreach my $group (keys %old) {
533                        $old{$group} or next; # no change
534
535                        my $ogroup = $self->base->get_object('group', $group) or next;
536                        ($ogroup->_get_c_field('sutype') || '') =~ /^(jobtype|contrattype)$/ and next;
537
538                        if ($old{$group} eq 'n') {
539                            $res += $ogroup->_addAttributeValue('memberUID', $self->object->id);
540                        } else {
541                            if (($self->object->_get_c_field('department') || '') eq $ogroup->id) {
542                                $self->base->log(LA_WARN,
543                                    "Don't removing user %s from group %s: is its department",
544                                    $self->object->id, $ogroup->id);
545                                next;
546                            }
547                            $res += $ogroup->_delAttributeValue('memberUID', $self->object->id);
548                        }
549                    }
550                    return $res;
551                },
552                label => l('Member of'),
553            },
554            forward => {
555                managed => 1,
556                get => sub {
557                    my ($self) = @_;
558                    my $sth = $self->base->db->prepare(q{
559                        select forward from aliases where name = ?
560                        } . ($self->base->{wexported} ? '' : ' and exported = true'));
561                    $sth->execute($self->object->id);
562                    my $res = $sth->fetchrow_hashref;
563                    $sth->finish;
564                    return $res->{forward}
565                },
566                set => sub {
567                    my ($self, $data) = @_;
568                    if ($data) {
569                        if (my $f = $self->base->get_object('aliases', $self->object->id)) {
570                            return $f->_set_c_fields(forward => $data);
571                        } else {
572                            if ($self->base->_create_c_object(
573                                    'aliases', $self->object->id,
574                                    forward => $data,
575                                    description => 'automatically created for ' . $self->object->id,
576                                )) {
577                                return 1;
578                            } else {
579                                $self->base->log(LA_ERR, "Cannot add forward for %s",
580                                    $self->object->id);
581                            }
582                        }
583                    } elsif ($self->base->get_object('aliases', $self->object->id)) {
584                        if (my $res = $self->base->_delete_object('aliases', $self->object->id)) {
585                            return $res;
586                        } else {
587                            $self->base->log(LA_ERR, "Cannot remove forward for %s",
588                                $self->object->id);
589                        }
590                    } else {
591                        return 1;
592                    }
593                    return;
594                },
595                label => l('Forward'),
596            },
597            aliases   => {
598                #reference => 'aliases',
599                delayed => 1,
600                formtype => 'TEXT',
601                multiple => 1,
602                get => sub {
603                    my ($self) = @_;
604                    my $sth = $self->base->db->prepare(q{
605                        select name from aliases where lower($1) =
606                        lower(array_to_string("forward", ','))
607                        } . ($self->base->{wexported} ? '' : 'and exported = true'));
608                    $sth->execute($self->object->id);
609                    my @values;
610                    while (my $res = $sth->fetchrow_hashref) {
611                        push(@values, $res->{name});
612                    }
613                    return \@values;
614                },
615                set => sub {
616                    my ($self, $data) = @_;
617
618                    my $res = 0;
619                    my %aliases = map { $_ => 1 } grep { $_ } (ref $data ? @{$data} : $data);
620                    foreach ($self->object->_get_attributes('aliases')) {
621                        $aliases{$_} ||= 0;
622                        $aliases{$_} +=2;
623                    }
624                    foreach (keys %aliases) {
625                        if ($aliases{$_} == 2) {
626                            if ($self->base->_delete_object('aliases', $_)) {
627                                $res++
628                            } else {
629                                $self->base->log(LA_ERR,
630                                    "Cannot remove aliases %s from user %s", $_,
631                                    $self->object->id);
632                            }
633                        } elsif ($aliases{$_} == 1) {
634                            if ($self->base->_create_c_object(
635                                    'aliases', $_,
636                                    forward => [ $self->object->id ],
637                                    description => 'automatically created for ' . $self->object->id,
638                                )) {
639                                $res++
640                            } else {
641                                $self->base->log(LA_ERR, 'Cannot set forward %s to user %s',
642                                    $_, $self->object->id);
643                                return;
644                            }
645                        } # 3 no change
646                    }
647                    $res
648                },
649                label => l('Aliases'),
650            },
651            revaliases => {
652                formtype => 'TEXT',
653                get => sub {
654                    my ($self) = @_;
655
656                    if (my $obj = $self->base->
657                        get_object('revaliases', $self->object->id)) {
658                        return $obj->get_attributes('as');
659                    } else {
660                        return;
661                    }
662                },
663                set => sub {
664                    my ($self, $data) = @_;
665               
666                    my $res = 0;
667                    if ($data) {
668                        if (my $obj = $self->base->
669                            get_object('revaliases', $self->object->id)) {
670                            my $ares = $obj->set_c_fields(
671                                'as' => $data,
672                                'exported' => ($self->object->get_attributes('exported') || 0),
673                            );
674                            if (defined($ares)) {
675                                $res+=$ares;
676                            } else {
677                                $self->base->log(LA_ERR, 'Cannot set revaliases for user %s',
678                                    $self->object->id);
679                            }
680                        } else {
681                            if ($self->base->_create_c_object(
682                                    'revaliases',
683                                    $self->object->id, as => $data,
684                                    'exported' => ($self->object->get_attributes('exported') || 0),
685                                    description => 'automatically created for ' . $self->object->id,
686                                )) {
687                                $res++;
688                            } else {
689                                $self->base->log(LA_ERR, 'Cannot set revaliases for user %s',
690                                    $self->object->id);
691                            }
692                        }
693                    } else {
694                        $self->base->_delete_object('revaliases', $self->object->id);
695                        $res++;
696                    }
697
698                    $res
699                },
700            },
701            manager => {
702                reference => 'user',
703                ro => 1,
704                get => sub {
705                    my ($self) = @_;
706                    if (my $manager = $self->object->_get_c_field('managerContact')) {
707                        return $manager;
708                    } elsif (my $department = $self->object->_get_c_field('department')) {
709                        my $obj = $self->base->get_object('group', $department);
710                        return $obj->_get_c_field('managedBy');
711                    } else {
712                        return;
713                    }
714                },
715                label => l('Responsible'),
716            },
717            department => {
718                reference => 'group',
719                can_values => sub {
720                    $base->search_objects('group', 'sutype=dpmt')
721                },
722                monitored => 1,
723                label => l('Department'),
724            },
725            contratType => {
726                reference => 'group',
727                can_values => sub {
728                    $base->search_objects('group', 'sutype=contrattype')
729                },
730                monitored => 1,
731                label => l('Type of contract'),
732            },
733            site => {
734                reference => 'site',
735                can_values => sub {
736                    $base->search_objects('site')
737                },
738                get => sub {
739                    my ($self) = @_;
740                    if (my $fmainaddress = $self->object->_get_c_field('mainaddress')) {
741                        $self->base->get_object('address', $fmainaddress)
742                            ->_get_c_field($self->name);
743                    } else {
744                        return;
745                    }
746                },
747                set => $subsetaddress,
748                label => l('Site'),
749            },
750            co => {
751                get => sub {
752                    my ($self) = @_;
753                    if (my $fmainaddress = $self->object->_get_c_field('mainaddress')) {
754                        $self->base->get_object('address', $fmainaddress)
755                            ->_get_c_field($self->name);
756                    } else {
757                        return;
758                    }
759                },
760                set => $subsetaddress,
761            },
762            l => {
763                get => sub {
764                    my ($self) = @_;
765                    if (my $fmainaddress = $self->object->_get_c_field('mainaddress')) {
766                        $self->base->get_object('address', $fmainaddress)
767                            ->_get_c_field($self->name);
768                    } else {
769                        return;
770                    }
771                },
772                set => $subsetaddress,
773                label => l('City'),
774            },
775            postalCode => {
776                get => sub {
777                    my ($self) = @_;
778                    if (my $fmainaddress = $self->object->_get_c_field('mainaddress')) {
779                        $self->base->get_object('address', $fmainaddress)
780                            ->_get_c_field($self->name);
781                    } else {
782                        return;
783                    }
784                },
785                set => $subsetaddress,
786                label => l('Postal code'),
787            },
788            streetAddress => {
789                formtype => 'TEXTAREA',
790                get => sub {
791                    my ($self) = @_;
792                    if (my $fmainaddress = $self->object->_get_c_field('mainaddress')) {
793                        $self->base->get_object('address', $fmainaddress)
794                            ->_get_c_field($self->name);
795                    } else {
796                        return;
797                    }
798                },
799                set => $subsetaddress,
800                label => l('Street'),
801            },
802            postOfficeBox => {
803                get => sub {
804                    my ($self) = @_;
805                    if (my $fmainaddress = $self->object->_get_c_field('mainaddress')) {
806                        $self->base->get_object('address', $fmainaddress)
807                            ->_get_c_field($self->name);
808                    } else {
809                        return;
810                    }
811                },
812                set => $subsetaddress,
813                label => l('Post office box'),
814            },
815            st => {
816                get => sub {
817                    my ($self) = @_;
818                    if (my $fmainaddress = $self->object->_get_c_field('mainaddress')) {
819                        $self->base->get_object('address', $fmainaddress)
820                            ->_get_c_field($self->name);
821                    } else {
822                        return;
823                    }
824                },
825                set => $subsetaddress,
826            },
827            facsimileTelephoneNumber => {
828                get => sub {
829                    my ($self) = @_;
830                    if (my $fmainaddress = $self->object->_get_c_field('mainaddress')) {
831                        $self->base->get_object('address', $fmainaddress)
832                            ->_get_c_field($self->name);
833                    } else {
834                        return;
835                    }
836                },
837                set => $subsetaddress,
838                label => l('Fax number'),
839            },
840            o => {
841                ro => 1,
842                iname => 'company',
843                label => l('Company'),
844            },
845            ou => {
846                iname => 'department',
847                ro => 1,
848                label => l('Department'),
849            },
850            telephoneNumber => {
851                get => sub {
852                    my ($self) = @_;
853                    if (my $fmainaddress = $self->object->_get_c_field('mainaddress')) {
854                        $self->base->get_object('address', $fmainaddress)
855                            ->_get_c_field($self->name);
856                    } else {
857                        return;
858                    }
859                },
860                set => $subsetaddress,
861                label => l('Phone number'),
862            },
863            physicalDeliveryOfficeName => {
864                get => sub {
865                    my ($self) = @_;
866                    if (my $fmainaddress = $self->object->_get_c_field('mainaddress')) {
867                        $self->base->get_object('address', $fmainaddress)
868                            ->_get_c_field($self->name);
869                    } else {
870                        return;
871                    }
872                },
873                set => $subsetaddress,
874                label => l('Office'),
875            },
876            uid => {
877                iname => 'name',
878                ro => 1,
879                label => l('Login'),
880            },
881            cn =>  { iname => 'name', ro => 1 },
882            gecos => {
883                managed => 1,
884                ro => 1,
885                get => sub {
886                    my ($self) = @_;
887                    my $obj = $self->object;
888                    my $gecos = sprintf("%s,%s,%s,%s",
889                        join(' ', grep { $_ }
890                                ($obj->_get_c_field('givenName'),
891                                ($obj->_get_c_field('sn'))))
892                            || $obj->_get_c_field('description') || '',
893                        join(' - ', grep { $_ } (($obj->_get_c_field('site') ||
894                                    $obj->_get_c_field('l')),
895                            $obj->_get_c_field('physicalDeliveryOfficeName'))) || '',
896                        $obj->_get_c_field('telephoneNumber') || '',
897                        $obj->_get_c_field('expireText') || '',
898                    );
899                    $gecos =~ s/:/ /g;
900                    return to_ascii($gecos);
901                },
902                label => l('GECOS'),
903            },
904            displayName  => {
905                ro => 1, managed => 1,
906                get => sub {
907                    my ($self) = @_;
908                    return join(' ', grep { $_ } 
909                        (
910                            $self->object->_get_c_field('givenName'),
911                            $self->object->_get_c_field('sn')
912                        )
913                    )
914                    || $self->object->_get_c_field('description')
915                    || $self->object->id;
916                },
917                label => l('Name'),
918            },
919            sAMAccountName  => {
920                ro => 1,
921                managed => 1,
922                iname => 'name',
923            },
924            accountExpires => {
925                ro => 1,
926                managed => 1,
927                get => sub {
928                    my ($self) = @_;
929                    my $obj = $self->object;
930                    my $sth = $obj->db->prepare_cached(
931                        sprintf(
932                            q{select extract(epoch from COALESCE(endcircuit,  expire)) + 11644474161 as expire
933                            from %s where %s = ?},
934                            $obj->db->quote_identifier($obj->_object_table),
935                            $obj->db->quote_identifier($obj->_key_field),
936                        )
937                    );
938                    $sth->execute($obj->id);
939                    my $res = $sth->fetchrow_hashref;
940                    $sth->finish;
941                    return $res->{expire} ? sprintf("%.f", $res->{expire} * 1E7) : '9223372036854775807';
942                }
943            },
944            shadowExpire => {
945                ro => 1,
946                managed => 1,
947                get => sub {
948                    my ($self) = @_;
949                    my $obj = $self->object;
950                    my $sth = $obj->db->prepare_cached(
951                        sprintf(
952                            q{select justify_hours(COALESCE(endcircuit,  expire) - '1/1/1970'::timestamp) as expire
953                            from %s where %s = ?},
954                            $obj->db->quote_identifier($obj->_object_table),
955                            $obj->db->quote_identifier($obj->_key_field),
956                        )
957                    );
958                    $sth->execute($obj->id);
959                    my $res = $sth->fetchrow_hashref;
960                    $sth->finish;
961                    return -1 unless($res->{expire});
962                    $res->{expire} =~ /(\d+) days\s*(\w)?/;
963                    # Add one day is time is not 00H00
964                    return $1 + ($2 ? 1 : 0);
965                }
966            },
967            directReports => {
968                reference => 'user',
969                ro => 1,
970                delayed => 1,
971                get => sub {
972                    my ($self) = @_;
973                    my $obj = $self->object;
974                    my $sth = $obj->db->prepare_cached(
975                        q{
976                        SELECT
977                        "user".name FROM
978                        public."user",
979                        public.user_attributes_groups,
980                        public.group_attributes_users,
981                        public.group_attributes_base gb,
982                        public."group"
983                        WHERE
984                        "user".ikey = user_attributes_groups.okey AND
985                        user_attributes_groups.value = "group".name AND
986                        group_attributes_users.okey = gb.okey AND
987                        "group".ikey = group_attributes_users.okey AND
988                        gb.attr = 'sutype' AND
989                        gb.value = 'dpmt' AND
990                        group_attributes_users.attr = 'managedBy' AND
991                        group_attributes_users.value = ?
992                        } . ($self->base->{wexported} ? '' : ' and "user".exported = true') . q{
993                            and "user".ikey not in
994                        (select okey from user_attributes_users
995                        where attr = 'manager' and value != ? )
996                        union
997                        select "user".name FROM public."user",
998                        user_attributes_users where
999                        user_attributes_users.attr = 'manager' and user_attributes_users.value = ?
1000                            and "user".ikey = user_attributes_users.okey
1001                        } . ($self->base->{wexported} ? '' : ' and "user".exported = true')
1002                    );
1003                    $sth->execute($obj->id, $obj->id, $obj->id);
1004                    my @res;
1005                    while (my $res = $sth->fetchrow_hashref) {
1006                        push(@res, $res->{name});
1007                    }
1008                    return \@res;
1009                },
1010            },
1011            managedObjects => { ro => 1, reference => 'group', },
1012            otheraddress => {
1013                ro => 1,
1014                reference => 'address',
1015                get => sub {
1016                    my ($self) = @_;
1017                    my $sth = $self->base->db->prepare_cached(q{
1018                        select name from address left join address_attributes
1019                        on address.ikey = address_attributes.okey and
1020                        address_attributes.attr = 'isMainAddress'
1021                        where "user" = ?
1022                        order by address_attributes.attr
1023                        });
1024                    $sth->execute($self->object->id);
1025                    my @values;
1026                    while (my $res = $sth->fetchrow_hashref) {
1027                        push(@values, $res->{name});
1028                    }
1029                    return \@values;
1030                },
1031            },
1032            mainaddress => {
1033                ro => 1,
1034                reference => 'address',
1035                get => sub {
1036                    my ($self) = @_;
1037                    my $sth = $self->base->db->prepare_cached(q{
1038                        select name from address join address_attributes on ikey = okey
1039                        where "user" = ? and attr = 'isMainAddress'
1040                        });
1041                    $sth->execute($self->object->id);
1042                    my $res = $sth->fetchrow_hashref;
1043                    $sth->finish;
1044                    return $res->{name};
1045                },
1046            },
1047            postalAddress => {
1048                ro => 1,
1049                get => sub {
1050                    my ($self) = @_;
1051                    if (my $fmainaddress = $self->object->_get_c_field('mainaddress')) {
1052                        $self->base->get_object('address', $fmainaddress)
1053                            ->_get_c_field($self->name);
1054                    } else {
1055                        return;
1056                    }
1057                },
1058                label => l('Postal Address'),
1059            },
1060            facsimileTelephoneNumber => {
1061                ro => 1,
1062                label => l('Fax number'),
1063            },
1064            allsite   => {
1065                ro => 1,
1066                reference => 'site',
1067            },
1068            managerContact => {
1069                delayed => 1,
1070                can_values => sub {
1071                    my %uniq = map { $_ => 1 } grep { $_ }
1072                    ($_[1] ? $_[1]->get_attributes('managerContact') : ()),
1073                    $base->search_objects('user', 'active=*');
1074                    sort keys %uniq;
1075                },
1076                reference => 'user',
1077                monitored => 1,
1078                iname => 'manager',
1079                label => l('Manager'),
1080            },
1081            expireText => {
1082                ro => 1,
1083                get => sub {
1084                    my ($self) = @_;
1085                    my $obj = $self->object;
1086                    my $sth = $obj->db->prepare_cached(
1087                        sprintf(
1088                            q{select to_char(COALESCE(endcircuit,  expire), 'YYYY/MM/DD') as expire
1089                            from %s where %s = ?},
1090                            $obj->db->quote_identifier($obj->_object_table),
1091                            $obj->db->quote_identifier($obj->_key_field),
1092                        )
1093                    );
1094                    $sth->execute($obj->id) or $obj->db->rollback;
1095                    my $res = $sth->fetchrow_hashref;
1096                    $sth->finish;
1097                    return $res->{expire}
1098                },
1099            },
1100            krb5ValidEnd => {
1101                ro => 1,
1102                managed => 1,
1103                get => sub {
1104                    my ($self) = @_;
1105                    my $sth = $self->object->db->prepare_cached(
1106                        sprintf(
1107                            q{select date_part('epoch', COALESCE(endcircuit,  expire))::int as expire
1108                            from %s where %s = ?},
1109                            $self->object->db->quote_identifier($self->object->_object_table),
1110                            $self->object->db->quote_identifier($self->object->_key_field),
1111                        )
1112                    );
1113                    $sth->execute($self->object->id) or $self->object->db->rollback;
1114                    my $res = $sth->fetchrow_hashref;
1115                    $sth->finish;
1116                    return $res->{expire}
1117                },
1118            },
1119            cells  => {
1120                ro => 1,
1121                reference => 'group',
1122            },
1123            departments => {
1124                reference => 'group',
1125                delayed => 1,
1126                ro => 1,
1127                label => l('Departments'),
1128            },
1129            arrivalDate => { },
1130            expired => {
1131                ro => 1,
1132                label => l('Expired'),
1133            },
1134            active => {
1135                ro => 1,
1136                label => l('Active'),
1137            },
1138            pwdAccountLockedTime => {
1139                managed => 1,
1140                ro => 1,
1141                get => sub {
1142                    my ($self) = @_;
1143                    my $obj = $self->object;
1144                    if ($obj->_get_c_field('locked')) {
1145                        return '000001010000Z';
1146                    } else {
1147                        my $sth = $obj->db->prepare_cached(
1148                            sprintf(
1149                                q{select to_char(COALESCE(endcircuit,  expire) AT TIME ZONE 'Z', 'YYYYMMDDHH24MISSZ') as expire
1150                                from %s where %s = ? and expire < now()},
1151                                $obj->db->quote_identifier($obj->_object_table),
1152                                $obj->db->quote_identifier($obj->_key_field),
1153                            )
1154                        );
1155                        $sth->execute($obj->id);
1156                        my $res = $sth->fetchrow_hashref;
1157                        $sth->finish;
1158                        return $res->{expire}
1159                    }
1160                },
1161            },
1162            userPassword => { readable => 0, },
1163            wWWHomePage => {
1164                label => l('Web Page'),
1165            },
1166            title => { },
1167            snNative => {
1168                label => l('Native name'),
1169            },
1170            givenNameNative => {
1171                label => l('Native first name'),
1172            },
1173            sn => {
1174                label => l('Name'),
1175            },
1176            shadowWarning => { },
1177            shadowMin => { },
1178            shadowMax => { },
1179            shadowLastChange => { },
1180            shadowInactive => { },
1181            shadowFlag => { },
1182            otherTelephone => { },
1183            nickname => {
1184                label => l('Nickname'),
1185            },
1186            mobile => { },
1187            mail => {
1188                label => l('Email'),
1189            },
1190            labeledURI => { },
1191            jobType => { },
1192            ipPhone => { },
1193            initials => {
1194                label => l('Initials'),
1195            },
1196            homePhone => { },
1197            homeDirectory => {
1198                label => l('Home directory'),
1199            },
1200            halReference => {
1201                label => l('HAL id'),
1202            },
1203            grade => { },
1204            givenName => {
1205                label => l('First name'),
1206            },
1207            encryptedPassword => { },
1208            description => {
1209                label => l('Description'),
1210            },
1211            company => {
1212                label => l('Company'),
1213            },
1214            comment => {
1215                label => l('Comment'),
1216            },
1217            college => { },
1218            passwordLastSet => {
1219                ro => 1,
1220                label => l('Password set'),
1221            },
1222            oldPassword => {
1223                multiple => 1,
1224            },
1225            bannedPassword => {
1226                multiple => 1,
1227            },
1228            currentEmployment => {
1229                managed => 1,
1230                ro => 1,
1231                reference => 'employment',
1232                get => sub {
1233                    my ($attr) = @_;
1234                    my $self = $attr->object;
1235
1236                    my $sth = $self->base->db->prepare_cached(
1237                        q{
1238                        select name from employment where firstday <= now() and
1239                        (lastday is null or lastday >= now() - '1 days'::interval) and "user" = ?
1240                        limit 1
1241                        }
1242                    );
1243                    $sth->execute($self->id);
1244                    my $res = $sth->fetchrow_hashref;
1245                    $sth->finish;
1246                    if ($res) {
1247                        return $res->{name}
1248                    } else {
1249                        return;
1250                    }
1251                },
1252            },
1253            nextEmployment => {
1254                managed => 1,
1255                ro => 1,
1256                reference => 'employment',
1257                get => sub {
1258                    my ($attr) = @_;
1259                    my $self = $attr->object;
1260
1261                    my $sth = $self->base->db->prepare_cached(
1262                        q{
1263                        select name from employment where firstday > now() and "user" = ?
1264                        order by firstday asc
1265                        limit 1
1266                        }
1267                    );
1268                    $sth->execute($self->id);
1269                    my $res = $sth->fetchrow_hashref;
1270                    $sth->finish;
1271                    if ($res) {
1272                        return $res->{name}
1273                    } else {
1274                        return;
1275                    }
1276                }
1277            },
1278            prevEmployment => {
1279                managed => 1,
1280                ro => 1,
1281                reference => 'employment',
1282                get => sub {
1283                    my ($attr) = @_;
1284                    my $self = $attr->object;
1285
1286                    my $sth = $self->base->db->prepare_cached(
1287                        q{
1288                        select name from employment where
1289                        (lastday is not null and lastday <= now() - '1 days'::interval) and "user" = ?
1290                        order by firstday desc
1291                        limit 1
1292                        }
1293                    );
1294                    $sth->execute($self->id);
1295                    my $res = $sth->fetchrow_hashref;
1296                    $sth->finish;
1297                    if ($res) {
1298                        return $res->{name}
1299                    } else {
1300                        return;
1301                    }
1302                }
1303            },
1304            appliedEmployement => {
1305                hide => 1,
1306                reference => 'employment',
1307            },
1308            hosted => {
1309                formtype => 'CHECKBOX',
1310                label => l('Hosted'),
1311            },
1312
1313    };
1314
1315    my $employmentro = sub {
1316        my $setting = $base->config('employment_lock_user') || 'any';
1317
1318        for ($setting) {
1319            /^always$/ and return 1;
1320            /^never$/ and return 0;
1321
1322            !$_[0] and return 0;
1323
1324            /^any$/i and return $_[0]->listEmployment ? 1 : 0;
1325            /^active/i and do {
1326                return $_[0]->_get_c_field('currentEmployment')
1327                ? 1
1328                : $_[0]->_get_c_field('nextEmployment') ? 1 : 0;
1329            };
1330            /(\S+)=(\S+)/ and do {
1331                my $attr = $_[0]->_get_c_field($1);
1332                if (defined($attr)) {
1333                    if ($2 eq '*') {
1334                        return 1;
1335                    } elsif($2 eq $attr) {
1336                        return 1;
1337                    } else {
1338                        return 0;
1339                    }
1340                } else {
1341                    return 0;
1342                }
1343            };
1344        }
1345        return $_[0]->listEmployment ? 1 : 0; # default is any!
1346    };
1347
1348    foreach (qw(contratType managerContact company endcircuit department hosted)) {
1349        $attrs->{$_}{ro} = $employmentro;
1350    }
1351
1352    $attrs->{expire}{ro} = sub {
1353        my $expireOn = $base->config('expireOn') || '';
1354        if ($expireOn eq 'never') {
1355            return 0;
1356        }
1357
1358        $employmentro->($_[0]);
1359    };
1360
1361    $class->SUPER::_get_attr_schema($base, $attrs)
1362}
1363
1364sub _get_state {
1365    my ($self, $state) = @_;
1366    for ($state) {
1367        /^expired$/ and do {
1368            my $attribute = $self->attribute('expire');
1369            $attribute->check_acl('r') or return;
1370            my $sth = $self->db->prepare_cached(
1371                q{ select coalesce(expire < now(), false) as exp from "user"
1372                where "user".name = ?}
1373            );
1374            $sth->execute($self->id);
1375            my $res = $sth->fetchrow_hashref;
1376            $sth->finish;
1377            return $res->{exp} ? 1 : 0;
1378        };
1379    }
1380}
1381
1382sub set_fields {
1383    my ($self, %data) = @_;
1384
1385    my $old;
1386    if (exists($data{department})) {
1387        $old = $self->_get_attributes('department');
1388    }
1389    if (($data{department} || '') eq ($old || '')) {
1390        # We do not remove the group, there is no change
1391        $old = undef;
1392    }
1393
1394    my $res = $self->SUPER::set_fields(%data) or return;
1395
1396    if ($self->base->config('remove_old_dpmt') && $old) {
1397        $self->base->log(LA_DEBUG,
1398            "Removing %s from group %s (department change to %s)",
1399            $self->id,
1400            $old,
1401            $data{department} || '');
1402        $self->_delAttributeValue('memberOf', $old) or return;
1403        $res++;
1404    }
1405
1406    $res
1407}
1408
1409=head2 listEmployment
1410
1411Return the ordered list of contract
1412
1413=cut
1414
1415sub listEmployment {
1416    my ($self) = @_;
1417
1418    my $sth = $self->base->db->prepare_cached(
1419        q{
1420        select name from employment where "user" = ?
1421        order by lastday desc NULLS first
1422        }
1423    );
1424    $sth->execute($self->id);
1425    my @list = ();
1426    while (my $res = $sth->fetchrow_hashref) {
1427        push(@list, $res->{name});
1428    }
1429
1430    @list
1431}
1432
1433=head2 computeEmploymentDate
1434
1435Compute and copy to user start and end employment date
1436
1437=cut
1438
1439sub computeEmploymentDate {
1440    my ($self) = @_;
1441
1442    my $currentemployment = $self->get_attributes('currentEmployment') || '';
1443
1444    my $expire = str2time($self->_get_attributes('expire') || '1970-01-01T00:00:00');
1445
1446    my %changes;
1447    my @employmentDate = qw(
1448        endEmployment   endStrictEmployment   endCurrentEmployment   endLastEmployment
1449        startEmployment startStrictEmployment startCurrentEmployment startFirstEmployment
1450        arrival departure
1451    );
1452    foreach (@employmentDate) {
1453        my $old = $self->_get_attributes($_) || '';
1454        my $new = $self->_get_attributes("_$_") || '';
1455        if ($old ne $new) {
1456            $changes{$_} = $new || undef;
1457        }
1458    }
1459
1460    # If there is no current employment we try to find any to not let expire
1461    # unset
1462
1463    my $expireOn = $self->base->config('expireOn') || '';
1464    if (!grep { $_ eq $expireOn } (@employmentDate, '', 'never')) {
1465        $self->base->log(LA_ERR, "expireOn set to invalid parameter %s, using endEmployment instead", $expireOn);
1466        $expireOn = undef;
1467    }
1468    $expireOn ||= 'endEmployment';
1469
1470    # We check if matching start exists to know if using end* for expiration is
1471    # safe, even undef
1472    my %end2start = (
1473        endEmployment        => 'startEmployment',
1474        endStrictEmployment  => 'startStrictEmployment',
1475        endCurrentEmployment => 'startCurrentEmployment',
1476        endLastEmployment    => 'startFirstEmployment',
1477    );
1478
1479    if ($expireOn ne 'never') {
1480        my $endemploy = '';
1481        if ($self->_get_attributes("_$end2start{$expireOn}")) {
1482            $endemploy = $self->_get_attributes("_$expireOn") || '';
1483        } elsif (($self->base->config('unemployed_expire') ||'') eq 'no') {
1484            $endemploy = '';
1485        } else {
1486            # due to time zone, 0 is never 0
1487            if ($expire > 86400 && $expire < time) {
1488                $endemploy = 'UNCHANGED';
1489            } else {
1490                $endemploy = $self->_get_attributes('create');
1491            }
1492        }
1493
1494        if ($endemploy ne 'UNCHANGED') {
1495            my $nextexpire = str2time( $endemploy || '1970-01-01T00:00:00' );
1496
1497            if ($expire != $nextexpire) {
1498                $changes{expire} = $endemploy;
1499            }
1500        }
1501    }
1502
1503    if (keys %changes) {
1504        $self->ReportChange('Update', 'Update %s to match employment', join(', ', sort keys %changes));
1505        $self->ReportChange('Update', 'Expire update to %s to match employment', ($changes{expire} || '(none)'))
1506            if (exists($changes{expire}));
1507        $self->set_fields(%changes);
1508    }
1509
1510    return 1;
1511}
1512
1513sub _computeStartEmployment {
1514    my ($self, $delay, $any, $workday) = @_;
1515
1516    $delay ||= 0;
1517    my $start;
1518    my $nstart;
1519
1520    if (my $next = $self->_get_attributes('nextEmployment')) {
1521        my $onext = $self->base->get_object('employment', $next);
1522        $nstart = DateTime->from_epoch(epoch => str2time($onext->_get_attributes('firstday')));
1523        $nstart->set_time_zone( DateTime::TimeZone->new( name => 'local' ) );
1524    }
1525
1526    my $list_empl = $self->base->db->prepare_cached(q{
1527        SELECT *, (lastday is null or lastday >= now() - '1days'::interval) as "current" FROM employment
1528        WHERE "user" = ? and firstday < now()
1529        order by firstday desc
1530        });
1531    $list_empl->execute($self->id);
1532
1533    while (my $res = $list_empl->fetchrow_hashref) {
1534        if ($res->{current}) {
1535        } elsif ($nstart) {
1536            my $prevend = DateTime->from_epoch(epoch => str2time($res->{lastday}));
1537            $prevend->set_time_zone( DateTime::TimeZone->new( name => 'local' ) );
1538            my $tstart = $nstart->clone;
1539            $tstart->subtract(days => $delay + 1);
1540            if ($tstart->ymd gt $prevend->ymd) {
1541                last;
1542            }
1543        } elsif ((!$res->{current}) && (!$any)) {
1544            last;
1545        }
1546        $nstart = DateTime->from_epoch(epoch => str2time($res->{firstday}));
1547        $nstart->set_time_zone( DateTime::TimeZone->new( name => 'local' ) );
1548        $start = $nstart->clone;
1549    }
1550    $list_empl->finish;
1551
1552    $start ||= $nstart if ($any);
1553
1554    if ($start) {
1555        if ($workday) {
1556            my $day_of_week = $start->day_of_week;
1557            $start->add(days =>
1558                $day_of_week == 6 ? 2 :
1559                $day_of_week == 7 ? 1 : 0
1560            );
1561        }
1562    }
1563
1564    return $start ? $start->iso8601 : undef
1565}
1566
1567sub _computeEndEmployment {
1568    my ($self, $delay, $any, $workday) = @_;
1569
1570    $delay ||= 0;
1571    my $end;
1572    my $pend;
1573
1574    if (my $prev = $self->_get_attributes('prevEmployment')) {
1575        my $oprev = $self->base->get_object('employment', $prev);
1576        $pend = DateTime->from_epoch(epoch => str2time($oprev->_get_attributes('lastday')));
1577        $pend->set_time_zone( DateTime::TimeZone->new( name => 'local' ) );
1578        $pend->add(hours => 23, minutes => 59, seconds => 59);
1579    }
1580
1581    my $list_empl = $self->base->db->prepare_cached(q{
1582        SELECT *, firstday <= now() as "current" FROM employment WHERE "user" = ? and
1583        (lastday is null or lastday >= now() - '1 days'::interval)
1584        order by firstday asc
1585        });
1586    $list_empl->execute($self->id);
1587    my $res;
1588    while ($res = $list_empl->fetchrow_hashref) {
1589        if (!$res->{lastday}) {
1590            # Ultimate employment.
1591            $list_empl->finish;
1592            return undef;
1593        }
1594        if ($res->{current}) {
1595        } elsif ($end) {
1596            my $nextstart = DateTime->from_epoch(epoch => str2time($res->{firstday}));
1597            $nextstart->set_time_zone( DateTime::TimeZone->new( name => 'local' ) );
1598            my $tend = $end->clone;
1599            $tend->add(days => $delay + 1);
1600            if ($tend->ymd lt $nextstart->ymd) {
1601                last;
1602            }
1603        } elsif ((!$res->{current}) && (!$any)) {
1604            last;
1605        }
1606        $end = DateTime->from_epoch(epoch => str2time($res->{lastday}));
1607        $end->set_time_zone( DateTime::TimeZone->new( name => 'local' ) );
1608        $end->add(hours => 23, minutes => 59, seconds => 59);
1609    }
1610    $list_empl->finish;
1611
1612    $end ||= $pend if($any);
1613
1614    if ($end) {
1615        if ($workday) {
1616            my $day_of_week = $end->day_of_week;
1617            $end->subtract(days =>
1618                $day_of_week == 7 ? 2 :
1619                $day_of_week == 6 ? 1 : 0
1620            );
1621        }
1622    }
1623
1624    return $end ? $end->iso8601 : undef
1625}
1626
1627=head2 storeBannedPassword($epassword)
1628
1629Add an encrypted password to untrust list
1630
1631=cut
1632
1633sub storeBannedPassword {
1634    my ($self, $EncPass) = @_;
1635
1636    my @banned = sort { $b cmp $a } $self->_get_attributes('bannedPassword');
1637    my $now = DateTime->now; 
1638    unshift(@banned, $now->iso8601 . ';' . $EncPass);
1639    $self->set_fields('bannedPassword', [ grep { $_ } @banned ]);
1640
1641}
1642
1643=head2 banCurrentPassword
1644
1645Store the current password as banned
1646
1647=cut
1648
1649sub banCurrentPassword {
1650    my ($self) = @_;
1651   
1652    my $old = $self->get_field('userPassword') or return;
1653    $self->storeBannedPassword($old);
1654}
1655
1656sub check_password {
1657    my ( $self, $password ) = @_;
1658
1659    my $res = $self->SUPER::check_password($password);
1660    if ($res !~ /^ok$/) {
1661        return $res;
1662    }
1663
1664    foreach my $banned ($self->_get_attributes('bannedPassword')) {
1665        my ($date, $oldPassword) = $banned =~ /^([^;]*);(.*)/;
1666        if (crypt($password, $oldPassword) eq $oldPassword) {
1667            return "Banned password, cannot be used anymore";
1668        }
1669    }
1670
1671    return 'ok';
1672}
1673
1674sub _set_password {
1675    my ($self, $clear_pass) = @_;
1676    if (my $attr = $self->base->attribute($self->type, 'userPassword')) {
1677        my $field = $attr->iname;
1678
1679        # Storing as old password
1680        my @olds = sort { $b cmp $a } $self->_get_attributes('oldPassword');
1681        if (my $old = $self->get_field('userPassword')) {
1682            my $now = DateTime->now; 
1683            unshift(@olds, $now->iso8601 . ';' . $old);
1684            $self->set_fields('oldPassword', [ grep { $_ } @olds[0 .. 14] ]);
1685        }
1686
1687        my @salt_char = (('a' .. 'z'), ('A' .. 'Z'), (0 .. 9), '/', '.');
1688        my $salt = join('', map { $salt_char[rand(scalar(@salt_char))] } (1 .. 8));
1689        my $res = $self->set_fields($field, crypt($clear_pass, '$1$' . $salt));
1690        if ($res) {
1691            if ($self->base->get_global_value('rsa_public_key')) {
1692                $self->setCryptPassword($clear_pass) or return;
1693            }
1694        }
1695
1696        $self->set_fields('passwordLastSet', DateTime->now->datetime);
1697        $self->base->log(LA_NOTICE,
1698            'Mot de passe changé pour %s',
1699            $self->id
1700        );
1701
1702
1703        return $res;
1704    } else {
1705        $self->log(LA_WARN,
1706            "Cannot set password: userPassword attributes is unsupported");
1707    }
1708}
1709
1710=head2 setCryptPassword($clear_pass)
1711
1712Store password encrypted using RSA encryption.
1713
1714=cut
1715
1716sub setCryptPassword {
1717    my ($self, $clear_pass) = @_;
1718    if (my $serialize = $self->base->get_global_value('rsa_public_key')) {
1719        my $public = Crypt::RSA::Key::Public->new;
1720        $public = $public->deserialize(String => [ $serialize ]);
1721        my $rsa = new Crypt::RSA ES => 'PKCS1v15';
1722        my $rsa_password = $rsa->encrypt (
1723            Message    => $clear_pass,
1724            Key        => $public,
1725            Armour     => 1,
1726        ) || die $rsa->errstr();
1727        if (!$self->_set_c_fields('encryptedPassword', $rsa_password)) {
1728            $self->log(LA_ERR,
1729                "Cannot set 'encryptedPassword' attribute for object %s/%s",
1730                $self->type, $self->id,
1731            );
1732            return;
1733        }
1734    }
1735    $self->ReportChange('Password', 'Password stored using internal key');
1736    return 1;
1737}
1738
1739
1740=head2 GenPasswordResetId
1741
1742Return a new id allowing passowrd reset
1743
1744=cut
1745
1746sub GenPasswordResetId {
1747    my ($self) = @_;
1748
1749    my $id = LATMOS::Accounts::Utils::genpassword(length => 32);
1750
1751    my $sth = $self->base->db->prepare_cached(q{
1752        INSERT INTO passwordreset (id, "user") values (?,?)
1753    });
1754
1755    if ($sth->execute($id, $self->id)) {
1756        return $id;
1757    } else {
1758        return;
1759    }
1760}
1761
1762=head2 SendPasswordReset($url)
1763
1764Generate a password reset Id and the to the user.
1765
1766C<$url> is the URL where the password can changed (printf forward, the %s is
1767replaced by the request id)
1768
1769=cut
1770
1771sub SendPasswordReset {
1772    my ($self, $url) = @_;
1773
1774    my $id = $self->GenPasswordResetId;
1775
1776    my $mail = $self->_get_attributes('mail') or do {
1777        $self->base->log(LA_ERR, "Cannot sent reset password mail: no mail found");
1778        return;
1779    };
1780
1781    my %mail = (
1782        Subject => 'LATMOS: pasword reset',
1783        'X-LATMOS-Reason' => 'Account destruction',
1784        to => $mail,
1785    );
1786
1787    my $vars = {
1788        url => sprintf($url, $id),
1789    };
1790    $vars->{id} =  $id;
1791    $vars->{obj} = $self;
1792
1793    my $lamail = LATMOS::Accounts::Mail->new(
1794        $self->base->la,
1795        'passwordreset.mail',
1796    );
1797
1798    if ($lamail->process(\%mail, $vars)) {
1799        $self->base->log(LA_NOTICE,
1800            "Reset password sent to %s for user %s",
1801            $mail{to},
1802            $self->id,
1803        );
1804        return 1;
1805    } else {
1806        return;
1807    }
1808
1809}
1810
1811=head2 CheckPasswordResetId($id)
1812
1813Return True if the reset password ID can be found and is less than one day old
1814
1815=cut
1816
1817sub CheckPasswordResetId {
1818    my ($self, $id) = @_;
1819
1820    my $sth = $self->base->db->prepare_cached(q{
1821        SELECT * FROM passwordreset WHERE
1822            "user" = ? and
1823            id = ? and
1824            "create" >= now() - '1 days'::interval
1825    });
1826    $sth->execute($self->id, $id);
1827
1828    my $res = $sth->fetchrow_hashref;
1829    $sth->finish;
1830
1831    return $res ? 1 : 0;
1832}
1833
1834=head2 DeletePasswordId($id)
1835
1836Delete password reset C<$id> and all expired request
1837
1838=cut
1839
1840sub DeletePasswordId {
1841    my ($self, $id) = @_;
1842
1843    my $sth = $self->base->db->prepare_cached(q{
1844        DELETE FROM passwordreset WHERE
1845        "user" = ? AND (id = ? or "create" < now() - '1 days'::interval)
1846    });
1847
1848    $sth->execute($self->id, $id);
1849}
1850
18511;
1852
1853__END__
1854
1855=head1 SEE ALSO
1856
1857=head1 AUTHOR
1858
1859Olivier Thauvin, E<lt>olivier.thauvin@latmos.ipsl.frE<gt>
1860
1861=head1 COPYRIGHT AND LICENSE
1862
1863Copyright (C) 2008, 2009 CNRS SA/CETP/LATMOS
1864
1865This library is free software; you can redistribute it and/or modify
1866it under the same terms as Perl itself, either Perl version 5.10.0 or,
1867at your option, any later version of Perl 5 you may have available.
1868
1869=cut
Note: See TracBrowser for help on using the repository browser.