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

Last change on this file since 2221 was 2221, checked in by nanardon, 5 years ago

Cache more attributes

  • Property svn:keywords set to Id Rev
File size: 80.3 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: 2158 $ =~ /^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 GetOtypeDef {
63    my ($class) = @_;
64
65    {
66        address => 'user',
67        employment => 'user',
68    },
69}
70
71sub _get_attr_schema {
72    my ($class, $base) = @_;
73
74    my $subsetaddress = sub {
75        my ($self, $data) = @_;
76        my $fmainaddress = $self->object->_get_c_field('mainaddress');
77
78        # set address attribute => create address object on the fly
79        # except if attr is empty !
80        if (!$fmainaddress && $data) {
81            $fmainaddress = $self->object->id . '-' . join('', map { ('a'..'z')[rand(26)] }
82                (0..4));
83            $self->base->_create_c_object(
84                'address', $fmainaddress,
85                user => $self->object->id,
86                isMainAddress => 1, ) or do {
87                $self->base->log(LA_ERR,
88                    "Cannot create main address for user %s", $self->object->id);
89                return;
90            };
91        }
92        if ($fmainaddress && 
93            (my $address = $self->base->get_object('address', $fmainaddress))) {
94            if ($address->attribute($self->name) &&
95                !$address->attribute($self->name)->ro) {
96                return $address->set_c_fields($self->name => $data) ||0;
97            }
98        }
99    };
100
101    my $attrs = {
102            exported => {
103                post => sub {
104                    my ($self, $value) = @_;
105                    my $attr = $self->name;
106
107                    if (my $obj = $self->base->
108                            get_object('revaliases', $self->object->id)) {
109                        my $ares = $obj->set_c_fields(
110                            ($attr eq 'exported' ? 'exported' : 'unexported') => $value
111                        );
112                        if (!defined($ares)) {
113                            $self->base->log(LA_ERR,
114                                'Cannot set revaliases exported attribute for user %s',
115                                $self->object->id);
116                        }
117                    }
118                    my $must_expire = $self->name eq 'exported'
119                        ? ($value ? 0 : 1 )
120                        : ($value ? 1 : 0 );
121
122                    foreach my $al (grep { $_ } $self->object->get_attributes('aliases'), $self->object->id) {
123                        my $obj = $self->base->get_object('aliases', $al) or next;
124                        $obj->_set_c_fields(
125                            exported => $value,
126                        );
127                    }
128                    foreach my $al ($self->object->get_attributes('otheraddress')) {
129                        my $obj = $self->base->get_object('address', $al) or next;
130                        $obj->_set_c_fields(
131                            exported => $value,
132                        );
133                    }
134                },
135            },
136            uidNumber => {
137                inline => 1,
138                iname => 'uidnumber',
139                uniq => 1,
140                mandatory => 1,
141                formopts => { length => 7 },
142                label => l('UID'),
143            },
144            uidnumber => { inline => 1, hide => 1, monitored => 1 },
145            gidNumber => {
146                inline => 1,
147                iname => 'gidnumber',
148                mandatory => 1,
149                can_values => sub {
150                    map { $_->id, $_->get_attributes('gidNumber') }
151                    map { $base->get_object('group', $_) }
152                    $base->list_objects('group')
153                },
154                can_values => sub {
155                    my $sth = $base->db->prepare_cached(
156                        q{
157                        select name, gidnumber from "group" where exported = true
158                        }
159                    );
160                    $sth->execute();
161                    my @res;
162                    while (my $res = $sth->fetchrow_hashref) {
163                        push(@res, $res->{name});
164                        push(@res, $res->{gidnumber});
165                    }
166                    return @res;
167                },
168                display => sub {
169                    my ($self, $val) = @_;
170                    if ($val =~ /^\d+$/) {
171                        my ($gr) = $self->base->search_objects('group', "gidNumber=$val")
172                            or return;
173                        return $gr;
174                    } else {
175                        my ($gr) = $self->base->search_objects('group', "name=$val")
176                            or return;
177                        return $gr;
178                    }
179                },
180                input => sub {
181                    my ($val) = @_;
182                    $val =~ /^\d+$/ and return $val;
183                    my ($gr) = $base->search_objects('group', "name=$val") or return;
184                    return $base->get_object('group', $gr)->get_attributes('gidNumber');
185                },
186                #reference => 'group',
187                label => l('GID'),
188            },
189            loginShell => {
190                mandatory => 1,
191                label => l('Shell'),
192            },
193            gidnumber => { inline => 1, hide => 1,
194                can_values => sub {
195                    map { $_->id, $_->get_attributes('gidNumber') }
196                    map { $base->get_object('group', $_) }
197                    $base->list_objects('group')
198                },
199                display => sub {
200                    my ($self, $val) = @_;
201                    my ($gr) = $self->base->search_objects('group', "gidNumber=$val")
202                        or return;
203                    return $gr;
204                },
205                input => sub {
206                    my ($val) = @_;
207                    $val =~ /^\d+$/ and return $val;
208                    my ($gr) = $base->search_objects('group', "name=$val") or return;
209                    return $base->get_object('group', $gr)->get_attributes('gidNumber');
210                },
211                mandatory => 1,
212                reference => 'group',
213                monitored => 1,
214            },
215            locked    => {
216                formtype => 'CHECKBOX',
217                formopts => { rawvalue => 1, },
218                monitored => 1,
219                label => l('Locked'),
220            },
221            expire        => {
222                inline => 1,
223                formtype => 'DATETIME',
224                monitored => 1,
225                label => l('Expire on'),
226            },
227            endcircuit    => {
228                inline => 1,
229                formtype => 'DATE',
230                monitored => 1,
231                label => l('End of entrance'),
232            },
233            _endEmployment => {
234                formtype => 'DATETIME',
235                managed => 1,
236                ro => 1,
237                hide => 1,
238                get => sub {
239                    my ($attr) = @_;
240                    my $self = $attr->object;
241                    $self->_computeEndEmployment($self->base->config('employment_delay') || 0);
242                },
243                label => l('End of employment'),
244            },
245            endEmployment => {
246                formtype => 'DATETIME',
247                ro => 1,
248                label => l('End of employment'),
249            },
250            _departure => {
251                formtype => 'DATETIME',
252                managed => 1,
253                ro => 1,
254                hide => 1,
255                get => sub {
256                    my ($attr) = @_;
257                    my $self = $attr->object;
258                    $self->_computeEndEmployment($self->base->config('employment_delay') || 0, 1, 1);
259                },
260                label => l('Start of employment'),
261            },
262            departure => {
263                formtype => 'DATETIME',
264                ro => 1,
265                label => l('Leaving'),
266            },
267            _endStrictEmployment => {
268                formtype => 'DATETIME',
269                managed => 1,
270                ro => 1,
271                hide => 1,
272                get => sub {
273                    my ($attr) = @_;
274                    my $self = $attr->object;
275                    $self->_computeEndEmployment();
276                },
277            },
278            endStrictEmployment => {
279                formtype => 'DATETIME',
280                ro => 1,
281            },
282            _endCurrentEmployment => {
283                formtype => 'DATETIME',
284                managed => 1,
285                ro => 1,
286                hide => 1,
287                get => sub {
288                    my ($attr) = @_;
289                    my $self = $attr->object;
290
291                    my $list_empl = $self->base->db->prepare_cached(q{
292                        SELECT * FROM employment WHERE "user" = ? and
293                            firstday <= now() and
294                            (lastday is null or lastday >= now() - '1 days'::interval)
295                            order by firstday asc
296                    });
297                    $list_empl->execute($self->id);
298                    my $end;
299                    while (my $res = $list_empl->fetchrow_hashref) {
300                        if (!$res->{lastday}) {
301                            # Ultimate employment.
302                            $list_empl->finish;
303                            return undef;
304                        } else {
305                            $end = DateTime->from_epoch(epoch => str2time($res->{lastday}));
306                            $end->set_time_zone( DateTime::TimeZone->new( name => 'local' ) );
307                            $end->add(hours => 23, minutes => 59, seconds => 59);
308                        }
309                        last;
310                    }
311                    $list_empl->finish;
312
313                    return $end ? $end->iso8601 : undef
314                },
315            },
316            endCurrentEmployment => {
317                formtype => 'DATETIME',
318                ro => 1,
319            },
320            _endLastEmployment => {
321                formtype => 'DATETIME',
322                managed => 1,
323                ro => 1,
324                hide => 1,
325                get => sub {
326                    my ($attr) = @_;
327                    my $self = $attr->object;
328
329                    my $list_empl = $self->base->db->prepare_cached(q{
330                        SELECT * FROM employment WHERE "user" = ?
331                        order by lastday desc nulls first
332                    });
333                    $list_empl->execute($self->id);
334                    my $res = $list_empl->fetchrow_hashref;
335                    $list_empl->finish;
336                    my $end;
337                    if ($res && $res->{lastday}) {
338                        $end = DateTime->from_epoch(epoch => str2time($res->{lastday}));
339                        $end->set_time_zone( DateTime::TimeZone->new( name => 'local' ) );
340                        $end->add(hours => 23, minutes => 59, seconds => 59);
341                    }
342                    return $end ? $end->iso8601 : undef
343                },
344                label => l('End of any employment'),
345            },
346            endLastEmployment => {
347                formtype => 'DATETIME',
348                ro => 1,
349                label => l('End of any employment'),
350            },
351            _startEmployment => {
352                formtype => 'DATETIME',
353                managed => 1,
354                ro => 1,
355                hide => 1,
356                get => sub {
357                    my ($attr) = @_;
358                    my $self = $attr->object;
359                    $self->_computeStartEmployment($self->base->config('employment_delay') || 0);
360                },
361                label => l('Start of employment'),
362            },
363            startEmployment => {
364                formtype => 'DATETIME',
365                ro => 1,
366                label => l('Start of employment'),
367            },
368            _arrival => {
369                formtype => 'DATETIME',
370                managed => 1,
371                ro => 1,
372                hide => 1,
373                get => sub {
374                    my ($attr) = @_;
375                    my $self = $attr->object;
376                    $self->_computeStartEmployment($self->base->config('employment_delay') || 0, 1, 1);
377                },
378                label => l('Start of employment'),
379            },
380            arrival => {
381                formtype => 'DATETIME',
382                ro => 1,
383                label => l('Arrival'),
384            },           
385            _startStrictEmployment => {
386                formtype => 'DATETIME',
387                managed => 1,
388                ro => 1,
389                hide => 1,
390                get => sub {
391                    my ($attr) = @_;
392                    my $self = $attr->object;
393                    $self->_computeStartEmployment();
394                },
395            },
396            startStrictEmployment => {
397                formtype => 'DATETIME',
398                ro => 1,
399            },
400            _startCurrentEmployment => {
401                formtype => 'DATETIME',
402                managed => 1,
403                ro => 1,
404                hide => 1,
405                get => sub {
406                    my ($attr) = @_;
407                    my $self = $attr->object;
408
409                    my $list_empl = $self->base->db->prepare_cached(q{
410                        SELECT * FROM employment WHERE "user" = ? and
411                            firstday <= now() and
412                            (lastday is null or lastday >= now() - '1 days'::interval)
413                            order by firstday asc
414                    });
415                    $list_empl->execute($self->id);
416                    my $start;
417                    while (my $res = $list_empl->fetchrow_hashref) {
418                        $start = DateTime->from_epoch(epoch => str2time($res->{firstday}));
419                        $start->set_time_zone( DateTime::TimeZone->new( name => 'local' ) );
420                        last;
421                    }
422                    $list_empl->finish;
423
424                    return $start ? $start->iso8601 : undef
425                },
426            },
427            startCurrentEmployment => {
428                formtype => 'DATETIME',
429                ro => 1,
430            },
431            _startFirstEmployment => {
432                formtype => 'DATETIME',
433                managed => 1,
434                ro => 1,
435                hide => 1,
436                get => sub {
437                    my ($attr) = @_;
438                    my $self = $attr->object;
439
440                    my $list_empl = $self->base->db->prepare_cached(q{
441                        SELECT * FROM employment WHERE "user" = ?
442                        order by firstday asc
443                    });
444                    $list_empl->execute($self->id);
445                    my $res = $list_empl->fetchrow_hashref;
446                    $list_empl->finish;
447                    my $start;
448                    if ($res && $res->{firstday}) {
449                        $start = DateTime->from_epoch(epoch => str2time($res->{firstday}));
450                        $start->set_time_zone( DateTime::TimeZone->new( name => 'local' ) );
451                    }
452                    return $start ? $start->iso8601 : undef
453                },
454                label => l('Start of any employment'),
455            },
456            startFirstEmployment => {
457                formtype => 'DATETIME',
458                ro => 1,
459                label => l('Start of any employment'),
460            },
461            employmentLength => {
462                ro => 1,
463                auto => 1,
464            },
465            _employmentLength => {
466                ro => 1,
467                managed => 1,
468                hide => 1,
469                get => sub {
470                    my ($self) = @_;
471                    my $lastday = $self->object->get_attributes('endEmployment') || DateTime->now->ymd('-');
472                    my $firstday = $self->object->get_attributes('startEmployment') or return;
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                    return Date::Calc::Delta_Days(@fd, @ld) +1;
478                },
479                label => l('Work duration'),
480            },
481            employmentLengthText => {
482                ro => 1,
483                auto => 1,
484            },
485            _employmentLengthText => {
486                ro => 1,
487                managed => 1,
488                hide => 1,
489                get => sub {
490                    my ($self) = @_;
491                    my $firstday = $self->object->get_attributes('startEmployment') or return;
492                    my $lastday = $self->object->get_attributes('endEmployment')|| DateTime->now->ymd('-');
493                    {
494                        my $dtlast = DateTime->from_epoch(epoch => str2time($lastday));
495                        $dtlast->add(days => 1);
496                        $lastday = $dtlast->ymd('-');
497                    }
498
499                    my @fd = split('-', DateTime->from_epoch(epoch => str2time($firstday))->ymd('-'));
500                    my @ld = split('-', DateTime->from_epoch(epoch => str2time($lastday))->ymd('-'));
501
502                    my ($Dy,$Dm,$Dd) = Date::Calc::N_Delta_YMD(@fd, @ld);
503                    return join(', ',
504                        ($Dy ? l('%d years', $Dy)  : ()),
505                        ($Dm ? l('%d months', $Dm) : ()),
506                        ($Dd ? l('%d days', $Dd)   : ()),
507                    );
508                },
509                label => l('Work duration'),
510            },
511            cn        => {
512                inline => 1, ro => 1,
513                get => sub {
514                    my ($self) = @_;
515                    return join(' ', grep { $_ } 
516                        (
517                            $self->object->_get_c_field('givenName'),
518                            $self->object->_get_c_field('sn')
519                        )
520                    )
521                    || $self->object->_get_c_field('description')
522                    || $self->object->id;
523                },
524            },
525            memberOf  => {
526                reference => 'group',
527                multiple => 1, delayed => 1,
528                get => sub {
529                    my ($self) = @_;
530                    my $obj = $self->object;
531                    my $sth = $obj->db->prepare_cached(
532                        q{
533                        select name from "group" join
534                        group_attributes on group_attributes.okey = "group".ikey
535                        where value = ? and attr = ? and internobject = false
536                        } . ($self->base->{wexported} ? '' : ' and "group".exported = true')
537                    );
538                    $sth->execute($obj->id, 'memberUID');
539                    my @res;
540                    while (my $res = $sth->fetchrow_hashref) {
541                        push(@res, $res->{name});
542                    }
543                    return \@res;
544                },
545                set => sub {
546                    my ($self, $values) = @_;
547                    my %old = map { $_ => 'o' } @{ $self->get };
548                    foreach my $group (grep { $_ } ref $values ? @{ $values } : $values) {
549                        if ($old{$group}) {
550                            $old{$group} = undef; # no change
551                        } else {
552                            $old{$group} = 'n';
553                        }
554                    }
555
556                    my $res = 0;
557                    foreach my $group (keys %old) {
558                        $old{$group} or next; # no change
559
560                        my $ogroup = $self->base->get_object('group', $group) or next;
561                        ($ogroup->_get_c_field('sutype') || '') =~ /^(jobtype|contrattype)$/ and next;
562
563                        if ($old{$group} eq 'n') {
564                            $res += $ogroup->_addAttributeValue('memberUID', $self->object->id);
565                        } else {
566                            if (($self->object->_get_c_field('department') || '') eq $ogroup->id) {
567                                $self->base->log(LA_WARN,
568                                    "Don't removing user %s from group %s: is its department",
569                                    $self->object->id, $ogroup->id);
570                                next;
571                            }
572                            $res += $ogroup->_delAttributeValue('memberUID', $self->object->id);
573                        }
574                    }
575                    return $res;
576                },
577                label => l('Member of'),
578            },
579            forward => {
580                managed => 1,
581                multiple => 1,
582                get => sub {
583                    my ($self) = @_;
584                    my $sth = $self->base->db->prepare(q{
585                        select forward from aliases where name = ? and internobject = false
586                        } . ($self->base->{wexported} ? '' : ' and exported = true'));
587                    $sth->execute($self->object->id);
588                    my $res = $sth->fetchrow_hashref;
589                    $sth->finish;
590                    return $res->{forward}
591                },
592                set => sub {
593                    my ($self, $data) = @_;
594                    if ($data) {
595                        my @datas = ref $data ? @$data : split(/\s*,\s*/, $data);
596                        if (my $f = $self->base->get_object('aliases', $self->object->id)) {
597                            return $f->_set_c_fields(
598                                forward => \@datas,
599                                comment => undef,
600                                description => 'Forward for user ' . $self->object->id,
601                            );
602                        } else {
603                            if ($self->base->_create_c_object(
604                                    'aliases', $self->object->id,
605                                    forward => \@datas,
606                                    description => 'automatically created for ' . $self->object->id,
607                                )) {
608                                return 1;
609                            } else {
610                                $self->base->log(LA_ERR, "Cannot add forward for %s",
611                                    $self->object->id);
612                            }
613                        }
614                    } elsif ($self->base->get_object('aliases', $self->object->id)) {
615                        if (my $res = $self->base->_delete_object('aliases', $self->object->id)) {
616                            return $res;
617                        } else {
618                            $self->base->log(LA_ERR, "Cannot remove forward for %s",
619                                $self->object->id);
620                        }
621                    } else {
622                        return 1;
623                    }
624                    return;
625                },
626                label => l('Forward'),
627            },
628            aliases   => {
629                #reference => 'aliases',
630                delayed => 1,
631                formtype => 'TEXT',
632                multiple => 1,
633                get => sub {
634                    my ($self) = @_;
635                    my $sth = $self->base->db->prepare(q{
636                        select name from aliases where internobject = false and lower($1) =
637                        lower(array_to_string("forward", ','))
638                        } . ($self->base->{wexported} ? '' : 'and exported = true'));
639                    $sth->execute($self->object->id);
640                    my @values;
641                    while (my $res = $sth->fetchrow_hashref) {
642                        push(@values, $res->{name});
643                    }
644                    return \@values;
645                },
646                set => sub {
647                    my ($self, $data) = @_;
648
649                    my $res = 0;
650                    my %aliases = map { $_ => 1 } grep { $_ } (ref $data ? @{$data} : $data);
651                    foreach ($self->object->_get_attributes('aliases')) {
652                        $aliases{$_} ||= 0;
653                        $aliases{$_} +=2;
654                    }
655                    foreach (keys %aliases) {
656                        if ($aliases{$_} == 2) {
657                            if ($self->base->_delete_object('aliases', $_)) {
658                                $res++
659                            } else {
660                                $self->base->log(LA_ERR,
661                                    "Cannot remove aliases %s from user %s", $_,
662                                    $self->object->id);
663                            }
664                        } elsif ($aliases{$_} == 1) {
665                            if ($self->base->_create_c_object(
666                                    'aliases', $_,
667                                    forward => [ $self->object->id ],
668                                    description => 'automatically created for ' . $self->object->id,
669                                )) {
670                                $res++
671                            } else {
672                                $self->base->log(LA_ERR, 'Cannot set forward %s to user %s',
673                                    $_, $self->object->id);
674                                return;
675                            }
676                        } # 3 no change
677                    }
678                    $res
679                },
680                label => l('Aliases'),
681            },
682            revaliases => {
683                formtype => 'TEXT',
684                get => sub {
685                    my ($self) = @_;
686
687                    if (my $obj = $self->base->
688                        get_object('revaliases', $self->object->id)) {
689                        return $obj->get_attributes('as');
690                    } else {
691                        return;
692                    }
693                },
694                set => sub {
695                    my ($self, $data) = @_;
696               
697                    my $res = 0;
698                    if ($data) {
699                        if (my $obj = $self->base->
700                            get_object('revaliases', $self->object->id)) {
701                            my $ares = $obj->set_c_fields(
702                                'as' => $data,
703                                'exported' => ($self->object->get_attributes('exported') || 0),
704                            );
705                            if (defined($ares)) {
706                                $res+=$ares;
707                            } else {
708                                $self->base->log(LA_ERR, 'Cannot set revaliases for user %s',
709                                    $self->object->id);
710                            }
711                        } else {
712                            if ($self->base->_create_c_object(
713                                    'revaliases',
714                                    $self->object->id, as => $data,
715                                    'exported' => ($self->object->get_attributes('exported') || 0),
716                                    description => 'automatically created for ' . $self->object->id,
717                                )) {
718                                $res++;
719                            } else {
720                                $self->base->log(LA_ERR, 'Cannot set revaliases for user %s',
721                                    $self->object->id);
722                            }
723                        }
724                    } else {
725                        $self->base->_delete_object('revaliases', $self->object->id);
726                        $res++;
727                    }
728
729                    $res
730                },
731            },
732            manager => {
733                reference => 'user',
734                ro => 1,
735                get => sub {
736                    my ($self) = @_;
737                    if (my $manager = $self->object->_get_c_field('managerContact')) {
738                        return $manager;
739                    } elsif (my $department = $self->object->_get_c_field('department')) {
740                        my $obj = $self->base->get_object('group', $department);
741                        return $obj->_get_c_field('managedBy');
742                    } else {
743                        return;
744                    }
745                },
746                label => l('Responsible'),
747            },
748            department => {
749                reference => 'group',
750                can_values => sub {
751                    $base->search_objects('group', 'sutype=dpmt')
752                },
753                monitored => 1,
754                label => l('Department'),
755            },
756            contratType => {
757                reference => 'group',
758                can_values => sub {
759                    $base->search_objects('group', 'sutype=contrattype')
760                },
761                monitored => 1,
762                label => l('Type of contract'),
763            },
764            site => {
765                reference => 'site',
766                can_values => sub {
767                    $base->search_objects('site')
768                },
769                get => sub {
770                    my ($self) = @_;
771                    if (my $fmainaddress = $self->object->_get_c_field('mainaddress')) {
772                        $self->base->get_object('address', $fmainaddress)
773                            ->_get_c_field($self->name);
774                    } else {
775                        return;
776                    }
777                },
778                set => $subsetaddress,
779                label => l('Site'),
780            },
781            co => {
782                get => sub {
783                    my ($self) = @_;
784                    if (my $fmainaddress = $self->object->_get_c_field('mainaddress')) {
785                        $self->base->get_object('address', $fmainaddress)
786                            ->_get_c_field($self->name);
787                    } else {
788                        return;
789                    }
790                },
791                set => $subsetaddress,
792            },
793            l => {
794                get => sub {
795                    my ($self) = @_;
796                    if (my $fmainaddress = $self->object->_get_c_field('mainaddress')) {
797                        $self->base->get_object('address', $fmainaddress)
798                            ->_get_c_field($self->name);
799                    } else {
800                        return;
801                    }
802                },
803                set => $subsetaddress,
804                label => l('City'),
805            },
806            postalCode => {
807                get => sub {
808                    my ($self) = @_;
809                    if (my $fmainaddress = $self->object->_get_c_field('mainaddress')) {
810                        $self->base->get_object('address', $fmainaddress)
811                            ->_get_c_field($self->name);
812                    } else {
813                        return;
814                    }
815                },
816                set => $subsetaddress,
817                label => l('Postal code'),
818            },
819            streetAddress => {
820                formtype => 'TEXTAREA',
821                get => sub {
822                    my ($self) = @_;
823                    if (my $fmainaddress = $self->object->_get_c_field('mainaddress')) {
824                        $self->base->get_object('address', $fmainaddress)
825                            ->_get_c_field($self->name);
826                    } else {
827                        return;
828                    }
829                },
830                set => $subsetaddress,
831                label => l('Street'),
832            },
833            postOfficeBox => {
834                get => sub {
835                    my ($self) = @_;
836                    if (my $fmainaddress = $self->object->_get_c_field('mainaddress')) {
837                        $self->base->get_object('address', $fmainaddress)
838                            ->_get_c_field($self->name);
839                    } else {
840                        return;
841                    }
842                },
843                set => $subsetaddress,
844                label => l('Post office box'),
845            },
846            st => {
847                get => sub {
848                    my ($self) = @_;
849                    if (my $fmainaddress = $self->object->_get_c_field('mainaddress')) {
850                        $self->base->get_object('address', $fmainaddress)
851                            ->_get_c_field($self->name);
852                    } else {
853                        return;
854                    }
855                },
856                set => $subsetaddress,
857            },
858            facsimileTelephoneNumber => {
859                get => sub {
860                    my ($self) = @_;
861                    if (my $fmainaddress = $self->object->_get_c_field('mainaddress')) {
862                        $self->base->get_object('address', $fmainaddress)
863                            ->_get_c_field($self->name);
864                    } else {
865                        return;
866                    }
867                },
868                set => $subsetaddress,
869                label => l('Fax number'),
870            },
871            o => {
872                ro => 1,
873                iname => 'company',
874                label => l('Company'),
875            },
876            ou => {
877                iname => 'department',
878                ro => 1,
879                label => l('Department'),
880            },
881            telephoneNumber => {
882                get => sub {
883                    my ($self) = @_;
884                    if (my $fmainaddress = $self->object->_get_c_field('mainaddress')) {
885                        $self->base->get_object('address', $fmainaddress)
886                            ->_get_c_field($self->name);
887                    } else {
888                        return;
889                    }
890                },
891                set => $subsetaddress,
892                label => l('Phone number'),
893            },
894            physicalDeliveryOfficeName => {
895                get => sub {
896                    my ($self) = @_;
897                    if (my $fmainaddress = $self->object->_get_c_field('mainaddress')) {
898                        $self->base->get_object('address', $fmainaddress)
899                            ->_get_c_field($self->name);
900                    } else {
901                        return;
902                    }
903                },
904                set => $subsetaddress,
905                label => l('Office'),
906            },
907            uid => {
908                iname => 'name',
909                ro => 1,
910                label => l('Login'),
911            },
912            cn =>  { iname => 'name', ro => 1 },
913            gecos => {
914                ro => 1,
915                auto => 1,
916            },
917            _gecos => {
918                managed => 1,
919                hide => 1,
920                ro => 1,
921                get => sub {
922                    my ($self) = @_;
923                    my $obj = $self->object;
924
925                    my $fmt = $self->base->config('gecosformat') || '%{displayName},%{l}-%{physicalDeliveryOfficeName},%{telephoneNumber},%{expireText}';
926                    my $gecos = $obj->queryformat($fmt);
927                    return to_ascii($gecos);
928                },
929                label => l('GECOS'),
930            },
931            displayName => {
932                ro => 1,
933                auto => 1,
934            },
935            _displayName  => {
936                ro => 1, managed => 1,
937                hide => 1,
938                get => sub {
939                    my ($self) = @_;
940                    return join(' ', grep { $_ } 
941                        (
942                            $self->object->_get_c_field('givenName'),
943                            $self->object->_get_c_field('sn')
944                        )
945                    )
946                    || $self->object->_get_c_field('description')
947                    || $self->object->id;
948                },
949                label => l('Name'),
950            },
951            sAMAccountName => {
952                ro => 1,
953                auto => 1,
954            },
955            _sAMAccountName  => {
956                ro => 1,
957                hide => 1,
958                managed => 1,
959                iname => 'name',
960                get => sub {
961                    my ($self) = @_;
962                    substr($self->object->id, 0, 19)
963                },
964            },
965            accountExpires => {
966                ro => 1,
967                auto => 1,
968            },
969            _accountExpires => {
970                ro => 1,
971                managed => 1,
972                hide => 1,
973                get => sub {
974                    my ($self) = @_;
975                    my $obj = $self->object;
976                    my $sth = $obj->db->prepare_cached(
977                        sprintf(
978                            q{select extract(epoch from %s) + 11644474161 as expire
979                            from %s where %s = ?},
980                            $base->config('endCircuitdontExpire') ? 'expire' : 'COALESCE(endcircuit,  expire)',
981                            $obj->db->quote_identifier($obj->_object_table),
982                            $obj->db->quote_identifier($obj->_key_field),
983                        )
984                    );
985                    $sth->execute($obj->id);
986                    my $res = $sth->fetchrow_hashref;
987                    $sth->finish;
988                    return $res->{expire} ? sprintf("%.f", $res->{expire} * 1E7) : '9223372036854775807';
989                }
990            },
991            shadowExpire => {
992                ro => 1,
993                auto => 1,
994            },
995            _shadowExpire => {
996                ro => 1,
997                hide => 1,
998                managed => 1,
999                get => sub {
1000                    my ($self) = @_;
1001                    my $obj = $self->object;
1002                    my $sth = $obj->db->prepare_cached(
1003                        sprintf(
1004                            q{select justify_hours(%s - '1/1/1970'::timestamp) as expire
1005                            from %s where %s = ?},
1006                            $base->config('endCircuitdontExpire') ? 'expire' : 'COALESCE(endcircuit,  expire)',
1007                            $obj->db->quote_identifier($obj->_object_table),
1008                            $obj->db->quote_identifier($obj->_key_field),
1009                        )
1010                    );
1011                    $sth->execute($obj->id);
1012                    my $res = $sth->fetchrow_hashref;
1013                    $sth->finish;
1014                    return -1 unless($res->{expire});
1015                    $res->{expire} =~ /(\d+) days\s*(\w)?/;
1016                    # Add one day is time is not 00H00
1017                    return $1 + ($2 ? 1 : 0);
1018                }
1019            },
1020            directReports => {
1021                reference => 'user',
1022                ro => 1,
1023                delayed => 1,
1024                get => sub {
1025                    my ($self) = @_;
1026                    my $obj = $self->object;
1027                    my $sth = $obj->db->prepare_cached(
1028                        q{
1029                        SELECT
1030                        "user".name FROM
1031                        public."user",
1032                        public.user_attributes_groups,
1033                        public.group_attributes_users,
1034                        public.group_attributes_base gb,
1035                        public."group"
1036                        WHERE
1037                        "user".ikey = user_attributes_groups.okey AND
1038                        user_attributes_groups.value = "group".name AND
1039                        group_attributes_users.okey = gb.okey AND
1040                        "group".ikey = group_attributes_users.okey AND
1041                        gb.attr = 'sutype' AND
1042                        gb.value = 'dpmt' AND
1043                        group_attributes_users.attr = 'managedBy' AND
1044                        group_attributes_users.value = ?
1045                        } . ($self->base->{wexported} ? '' : ' and "user".exported = true') . q{
1046                            and "user".ikey not in
1047                        (select okey from user_attributes_users
1048                        where attr = 'manager' and value != ? )
1049                        union
1050                        select "user".name FROM public."user",
1051                        user_attributes_users where
1052                        user_attributes_users.attr = 'manager' and user_attributes_users.value = ?
1053                            and "user".ikey = user_attributes_users.okey
1054                        } . ($self->base->{wexported} ? '' : ' and "user".exported = true')
1055                    );
1056                    $sth->execute($obj->id, $obj->id, $obj->id);
1057                    my @res;
1058                    while (my $res = $sth->fetchrow_hashref) {
1059                        push(@res, $res->{name});
1060                    }
1061                    return \@res;
1062                },
1063            },
1064            managedObjects => { ro => 1, reference => 'group', },
1065            otheraddress => {
1066                ro => 1,
1067                reference => 'address',
1068                get => sub {
1069                    my ($self) = @_;
1070                    my $sth = $self->base->db->prepare_cached(q{
1071                        select name from address left join address_attributes
1072                        on address.ikey = address_attributes.okey and
1073                        address_attributes.attr = 'isMainAddress'
1074                        where "user" = ?
1075                        } . ($self->base->{wexported} ? '' : ' and "address".exported = true') . q{
1076                        order by address_attributes.attr
1077                        } );
1078                    $sth->execute($self->object->id);
1079                    my @values;
1080                    while (my $res = $sth->fetchrow_hashref) {
1081                        push(@values, $res->{name});
1082                    }
1083                    return \@values;
1084                },
1085            },
1086            mainaddress => {
1087                ro => 1,
1088                reference => 'address',
1089                get => sub {
1090                    my ($self) = @_;
1091                    my $sth = $self->base->db->prepare_cached(q{
1092                        select name from address join address_attributes on ikey = okey
1093                        where "user" = ? and attr = 'isMainAddress'
1094                        } . ($self->base->{wexported} ? '' : ' and "address".exported = true'));
1095                    $sth->execute($self->object->id);
1096                    my $res = $sth->fetchrow_hashref;
1097                    $sth->finish;
1098                    return $res->{name};
1099                },
1100            },
1101            postalAddress => {
1102                ro => 1,
1103                get => sub {
1104                    my ($self) = @_;
1105                    if (my $fmainaddress = $self->object->_get_c_field('mainaddress')) {
1106                        $self->base->get_object('address', $fmainaddress)
1107                            ->_get_c_field($self->name);
1108                    } else {
1109                        return;
1110                    }
1111                },
1112                label => l('Postal Address'),
1113            },
1114            facsimileTelephoneNumber => {
1115                ro => 1,
1116                label => l('Fax number'),
1117            },
1118            allsite   => {
1119                ro => 1,
1120                reference => 'site',
1121            },
1122            managerContact => {
1123                delayed => 1,
1124                can_values => sub {
1125                    my %uniq = map { $_ => 1 } grep { $_ }
1126                    (($_[1] ? $_[1]->get_attributes('managerContact') : ()),
1127                    $base->search_objects('user', 'active=1'));
1128                    sort keys %uniq;
1129                },
1130                reference => 'user',
1131                monitored => 1,
1132                iname => 'manager',
1133                label => l('Manager'),
1134            },
1135            expireTextEC => {
1136                ro => 1,
1137                auto => 1,
1138            },
1139            _expireTextEC => {
1140                ro => 1,
1141                managed => 1,
1142                hide => 1,
1143                get => sub {
1144                    my ($self) = @_;
1145                    my $obj = $self->object;
1146                    my $sth = $obj->db->prepare_cached(
1147                        sprintf(
1148                            q{select to_char(%s, 'YYYY/MM/DD') as expire
1149                            from %s where %s = ?},
1150                            $base->config('endCircuitdontExpire') ? 'expire' : 'COALESCE(endcircuit,  expire)',
1151                            $obj->db->quote_identifier($obj->_object_table),
1152                            $obj->db->quote_identifier($obj->_key_field),
1153                        )
1154                    );
1155                    $sth->execute($obj->id) or $obj->db->rollback;
1156                    my $res = $sth->fetchrow_hashref;
1157                    $sth->finish;
1158                    return $res->{expire}
1159                },
1160            },
1161            expireText => {
1162                ro => 1,
1163                auto => 1,
1164            },
1165            _expireText => {
1166                ro => 1,
1167                hide => 1,
1168                managed => 1,
1169                get => sub {
1170                    my ($self) = @_;
1171                    my $obj = $self->object;
1172                    my $sth = $obj->db->prepare_cached(
1173                        sprintf(
1174                            q{select to_char(expire, 'YYYY/MM/DD') as expire
1175                            from %s where %s = ?},
1176                            $obj->db->quote_identifier($obj->_object_table),
1177                            $obj->db->quote_identifier($obj->_key_field),
1178                        )
1179                    );
1180                    $sth->execute($obj->id) or $obj->db->rollback;
1181                    my $res = $sth->fetchrow_hashref;
1182                    $sth->finish;
1183                    return $res->{expire}
1184                },
1185            },
1186            krb5ValidEnd => {
1187                ro => 1,
1188                auto => 1,
1189            },
1190            _krb5ValidEnd => {
1191                ro => 1,
1192                managed => 1,
1193                hide => 1,
1194                get => sub {
1195                    my ($self) = @_;
1196                    my $sth = $self->object->db->prepare_cached(
1197                        sprintf(
1198                            q{select date_part('epoch', COALESCE(endcircuit,  expire))::int as expire
1199                            from %s where %s = ?},
1200                            $self->object->db->quote_identifier($self->object->_object_table),
1201                            $self->object->db->quote_identifier($self->object->_key_field),
1202                        )
1203                    );
1204                    $sth->execute($self->object->id) or $self->object->db->rollback;
1205                    my $res = $sth->fetchrow_hashref;
1206                    $sth->finish;
1207                    return $res->{expire}
1208                },
1209            },
1210            cells  => {
1211                ro => 1,
1212                reference => 'group',
1213            },
1214            departments => {
1215                reference => 'group',
1216                delayed => 1,
1217                ro => 1,
1218                label => l('Departments'),
1219            },
1220            arrivalDate => { },
1221            expired => {
1222                ro => 1,
1223                label => l('Expired'),
1224            },
1225            active => {
1226                ro => 1,
1227                label => l('Active'),
1228            },
1229            status => {
1230                ro => 1,
1231                label => l('Statut du compte'),
1232            },
1233            pwdAccountLockedTime => {
1234                auto => 1,
1235                ro => 1,
1236            },
1237            _pwdAccountLockedTime => {
1238                managed => 1,
1239                ro => 1,
1240                hide => 1,
1241                get => sub {
1242                    my ($self) = @_;
1243                    my $obj = $self->object;
1244                    if ($obj->_get_c_field('locked')) {
1245                        return '000001010000Z';
1246                    } else {
1247                        my $sth = $obj->db->prepare_cached(
1248                            sprintf(
1249                                q{select to_char(%s AT TIME ZONE 'Z', 'YYYYMMDDHH24MISSZ') as expire
1250                                from %s where %s = ? and expire < now()},
1251                                $base->config('endCircuitdontExpire') ? 'expire' : 'COALESCE(endcircuit,  expire)',
1252                                $obj->db->quote_identifier($obj->_object_table),
1253                                $obj->db->quote_identifier($obj->_key_field),
1254                            )
1255                        );
1256                        $sth->execute($obj->id);
1257                        my $res = $sth->fetchrow_hashref;
1258                        $sth->finish;
1259                        return $res->{expire}
1260                    }
1261                },
1262            },
1263            userPassword => { readable => 0, },
1264            wWWHomePage => {
1265                label => l('Web Page'),
1266                formopts => { length => 35 },
1267            },
1268            title => { },
1269            snNative => {
1270                label => l('Native name'),
1271            },
1272            givenNameNative => {
1273                label => l('Native first name'),
1274            },
1275            sn => {
1276                label => l('Name'),
1277            },
1278            shadowWarning => { },
1279            shadowMin => { },
1280            shadowMax => { },
1281            shadowLastChange => { },
1282            shadowInactive => { },
1283            shadowFlag => { },
1284            otherTelephone => { },
1285            nickname => {
1286                label => l('Nickname'),
1287            },
1288            mobile => { },
1289            mail => {
1290                label => l('Email'),
1291            },
1292            otherEmail => {
1293                label => l('External mail'),
1294            },
1295            labeledURI => { },
1296            jobType => { },
1297            ipPhone => { },
1298            initials => {
1299                label => l('Initials'),
1300                checkinput => sub {
1301                    $_[0] or return;
1302                    return(length($_[0]) <= 6)
1303                }
1304            },
1305            homePhone => { },
1306            homeDirectory => {
1307                label => l('Home directory'),
1308            },
1309            halReference => {
1310                label => l('HAL id'),
1311            },
1312            grade => { },
1313            givenName => {
1314                label => l('First name'),
1315            },
1316            encryptedPassword => { },
1317            description => {
1318                label => l('Description'),
1319            },
1320            company => {
1321                label => l('Company'),
1322            },
1323            employer => {
1324                label => l('Employer'),
1325            },
1326            comment => {
1327                label => l('Comment'),
1328            },
1329            college => { },
1330            passwordLastSet => {
1331                ro => 1,
1332                label => l('Password set'),
1333            },
1334            oldPassword => {
1335                multiple => 1,
1336            },
1337            bannedPassword => {
1338                multiple => 1,
1339            },
1340            sshPublicKey => {
1341                multiple => 1,
1342                formopts => { length => 45 },
1343            },
1344            sshPublicKeyFilter => {
1345                multiple => 1,
1346                formopts => { length => 45 },
1347            },
1348            delUnknownSshKey => {
1349                formtype => 'CHECKBOX',
1350            },
1351            _authorizedKeys => {
1352                multiple => 1,
1353                ro => 1,
1354                managed => 1,
1355                reference => 'user',
1356                hide => 1,
1357                get => sub {
1358                    my ($attr) = @_;
1359                    my $self = $attr->object;
1360
1361                    my @keys = $self->_get_attributes('sshPublicKey');
1362
1363                    my @filters = $self->_get_attributes('sshPublicKeyFilter');
1364                    my $delUnknownSshKey = $self->_get_attributes('delUnknownSshKey');
1365
1366                    my %users = ( $self->id => 1 );
1367
1368                    if (@filters) {
1369                        foreach my $user ($self->base->search_objects('user', @filters, 'oalias=NULL')) {
1370                            my $ouser = $self->base->get_object('user', $user) or next;
1371                            $users{ $user } and next;
1372                            $users{ $user } = 1;
1373                            push(@keys, $ouser->_get_attributes('sshPublicKey'));
1374                        }
1375                    }
1376
1377                    return @keys
1378                        ? \@keys
1379                        : (@filters || $delUnknownSshKey
1380                            ? [ '# No key (' . DateTime->now->iso8601 . ')' ]
1381                            : undef);
1382                },
1383            },
1384            authorizedKeys => {
1385                multiple => 1,
1386                ro => 1,
1387                reference => 'user',
1388            },
1389            currentEmployment => {
1390                managed => 1,
1391                ro => 1,
1392                reference => 'employment',
1393                get => sub {
1394                    my ($attr) = @_;
1395                    my $self = $attr->object;
1396
1397                    my $now = DateTime->now()->iso8601 . 'Z';
1398
1399                    my $sth = $self->base->db->prepare_cached(
1400                        q{
1401                        select name from employment where firstday <= ?::timestamp and
1402                        (lastday is null or lastday >= ?::timestamp - '1 days'::interval) and "user" = ?
1403                        limit 1
1404                        }
1405                    );
1406                    $sth->execute($now, $now, $self->id);
1407                    my $res = $sth->fetchrow_hashref;
1408                    $sth->finish;
1409                    if ($res) {
1410                        return $res->{name}
1411                    } else {
1412                        return;
1413                    }
1414                },
1415            },
1416            nextEmployment => {
1417                managed => 1,
1418                ro => 1,
1419                reference => 'employment',
1420                get => sub {
1421                    my ($attr) = @_;
1422                    my $self = $attr->object;
1423
1424                    my $now = DateTime->now()->iso8601 . 'Z';
1425
1426                    my $sth = $self->base->db->prepare_cached(
1427                        q{
1428                        select name from employment where firstday > ?::timestamp and "user" = ?
1429                        order by firstday asc
1430                        limit 1
1431                        }
1432                    );
1433                    $sth->execute($now, $self->id);
1434                    my $res = $sth->fetchrow_hashref;
1435                    $sth->finish;
1436                    if ($res) {
1437                        return $res->{name}
1438                    } else {
1439                        return;
1440                    }
1441                }
1442            },
1443            activeEmployment => {
1444                managed => 1,
1445                ro => 1,
1446                reference => 'employment',
1447                multiple => 1,
1448                get => sub {
1449                    my ($attr) = @_;
1450                    my $self = $attr->object;
1451
1452                    my $now = DateTime->now()->iso8601 . 'Z';
1453
1454                    my $sth = $self->base->db->prepare_cached(
1455                        q{
1456                        select name from employment where (lastday > ?::timestamp or lastday IS NULL) and "user" = ?
1457                        order by firstday asc
1458                        limit 1
1459                        }
1460                    );
1461                    $sth->execute($now, $self->id);
1462                    my @Res;
1463                    while (my  $res = $sth->fetchrow_hashref) {
1464                        push(@Res, $res->{name});
1465                    }
1466
1467                    return \@Res;
1468                }
1469            },
1470            prevEmployment => {
1471                managed => 1,
1472                ro => 1,
1473                reference => 'employment',
1474                get => sub {
1475                    my ($attr) = @_;
1476                    my $self = $attr->object;
1477
1478                    my $now = DateTime->now()->iso8601 . 'Z';
1479
1480                    my $sth = $self->base->db->prepare_cached(
1481                        q{
1482                        select name from employment where
1483                        (lastday is not null and lastday <= ?::timestamp - '1 days'::interval) and "user" = ?
1484                        order by firstday desc
1485                        limit 1
1486                        }
1487                    );
1488                    $sth->execute($now, $self->id);
1489                    my $res = $sth->fetchrow_hashref;
1490                    $sth->finish;
1491                    if ($res) {
1492                        return $res->{name}
1493                    } else {
1494                        return;
1495                    }
1496                }
1497            },
1498            appliedEmployement => {
1499                hide => 1,
1500                reference => 'employment',
1501            },
1502            contratTypeHistory => {
1503                reference => 'group',
1504                can_values => sub {
1505                    $base->search_objects('group', 'sutype=contrattype')
1506                },
1507                multiple => 1,
1508            },
1509            employmentHistory => {
1510                reference => 'employment',
1511                multiple => 1,
1512                ro => 1,
1513            },
1514            hosted => {
1515                formtype => 'CHECKBOX',
1516                label => l('Hosted'),
1517            },
1518            assigned => {
1519                formtype => 'CHECKBOX',
1520                label => l('Assigned'),
1521            },
1522            createRequestId => {
1523                label => l('Account Request id')
1524            },
1525            requestId => {
1526                label => l('Request id'),
1527            },
1528            nationality => {
1529                label => l('Nationality'),
1530            },
1531            nativeCountry => {
1532                label => l('Native country'),
1533            },
1534            firstAidTrainingValidity => {
1535                formtype => 'DATE',
1536                label => l('Validité formation SST'),
1537            },
1538    };
1539
1540    my $employmentro = sub {
1541        my $setting = $base->config('employment_lock_user') || 'any';
1542
1543        for ($setting) {
1544            /^always$/ and return 1;
1545            /^never$/ and return 0;
1546
1547            $_[0] or return 0;
1548
1549            /^any$/i and return $_[0]->listEmployment ? 1 : 0;
1550            /^active/i and do {
1551                return $_[0]->_get_c_field('currentEmployment')
1552                ? 1
1553                : $_[0]->_get_c_field('nextEmployment') ? 1 : 0;
1554            };
1555            /(\S+)=(\S+)/ and do {
1556                my $attr = $_[0]->_get_c_field($1);
1557                if (defined($attr)) {
1558                    if ($2 eq '*') {
1559                        return 1;
1560                    } elsif($2 eq $attr) {
1561                        return 1;
1562                    } else {
1563                        return 0;
1564                    }
1565                } else {
1566                    return 0;
1567                }
1568            };
1569        }
1570        return $_[0]->listEmployment ? 1 : 0; # default is any!
1571    };
1572
1573    foreach (_reported_atributes(), qw(department managerContact contratTypeHistory)) {
1574        $attrs->{$_}{ro} = $employmentro;
1575    }
1576
1577    $attrs->{expire}{ro} = sub {
1578        my $expireOn = $base->config('expireOn') || '';
1579        if ($expireOn eq 'never') {
1580            return 0;
1581        }
1582
1583        $employmentro->($_[0]);
1584    };
1585
1586    $class->SUPER::_get_attr_schema($base, $attrs)
1587}
1588
1589sub CreateAlias {
1590    my ($class, $base, $name, $for) = @_;
1591
1592    my $stAddAlias = $base->db->prepare_cached(
1593        q{INSERT INTO "user" (name, uidnumber,            gidnumber, oalias, oaliascache) values
1594                             (?,    -nextval('ikey_seq'), ?,         ?,      ?)}
1595    );
1596
1597    my $ref = $base->_derefObject($class->type, $for);
1598    my $res = $stAddAlias->execute($name, -1, $for, $ref ? $ref->id : undef);
1599    return $res ? 1 : 0;
1600}
1601
1602sub _get_state {
1603    my ($self, $state) = @_;
1604    for ($state) {
1605        /^expired$/ and do {
1606            my $attribute = $self->attribute('expire');
1607            $attribute->check_acl('r') or return;
1608            my $sth = $self->db->prepare_cached(
1609                q{ select coalesce(expire < now(), false) as exp from "user"
1610                where "user".name = ?}
1611            );
1612            $sth->execute($self->id);
1613            my $res = $sth->fetchrow_hashref;
1614            $sth->finish;
1615            return $res->{exp} ? 1 : 0;
1616        };
1617    }
1618}
1619
1620sub set_fields {
1621    my ($self, %data) = @_;
1622
1623    my $old;
1624    if (exists($data{department})) {
1625        $old = $self->_get_attributes('department');
1626    }
1627
1628    if (($data{department} || '') eq ($old || '')) {
1629        # We do not remove the group, there is no change
1630        $old = undef;
1631    }
1632
1633    if ($old) {
1634        # If the department is no longer a department
1635        # we do nothing
1636        my @names = $self->base->search_objects('group', 'name=' . $old, 'sutype=dpmt');
1637        if (! @names) {
1638            $old = undef;
1639        }
1640    }
1641
1642    my $res = $self->SUPER::set_fields(%data) or return;
1643
1644    if ($self->base->config('remove_old_dpmt') && $old) {
1645        $self->base->log(LA_DEBUG,
1646            "Removing %s from group %s (department change to %s)",
1647            $self->id,
1648            $old,
1649            $data{department} || '');
1650        $self->_delAttributeValue('memberOf', $old) or return;
1651        $res++;
1652    }
1653
1654    $res
1655}
1656
1657=head2 listEmployment
1658
1659Return the ordered list of contract
1660
1661=cut
1662
1663sub listEmployment {
1664    my ($self) = @_;
1665
1666    my $sth = $self->base->db->prepare_cached(
1667        q{
1668        select name from employment where "user" = ?
1669        order by lastday desc NULLS first
1670        }
1671    );
1672    $sth->execute($self->id);
1673    my @list = ();
1674    while (my $res = $sth->fetchrow_hashref) {
1675        push(@list, $res->{name});
1676    }
1677
1678    @list
1679}
1680
1681sub _reported_atributes { qw(contratType endcircuit hosted assigned requestId company employer) }
1682
1683=head2 applyCurrentEmployment
1684
1685Search the current employment is any and apply paramter to user
1686
1687=cut
1688
1689sub applyCurrentEmployment {
1690    my ($self) = @_;
1691
1692    # Get current employment name
1693    my $currentempl = $self->get_attributes('currentEmployment') || '';
1694
1695    $self->base->log(
1696        LA_DEBUG,
1697        "Applying Employement %s to user %s",
1698        $currentempl || '(none)',
1699        $self->id
1700    );
1701
1702    if (my $currentemployment = $self->base->get_object('employment', $currentempl)) {
1703
1704        # If an employement apply we set the value to the user object
1705
1706        $self->computeEmploymentDate;
1707
1708        my %attrsets = (
1709            appliedEmployement => $currentemployment->id,
1710        );
1711        foreach my $attr (_reported_atributes(), qw(department managerContact)) {
1712            my $uval = $self->get_attributes($attr) || '';
1713            my $cval = $currentemployment->get_attributes($attr) || '';
1714
1715            if ($attr eq 'managerContact') {
1716                if (!$cval) {
1717                    my $dpmt  = $currentemployment->get_attributes('department') or last;
1718                    my $odmpt = $currentemployment->base->get_object('group', $dpmt) or last;
1719                    $cval = $odmpt->get_attributes('managedBy');
1720                }
1721            }
1722
1723            if ($uval ne $cval) {
1724                my $oattr = $currentemployment->base->attribute('user', $attr);
1725                $attrsets{$oattr->iname} = $cval;
1726            }
1727        }
1728
1729        if (keys %attrsets) {
1730            if (my $res = $self->set_fields(%attrsets)) {
1731                $self->ReportChange('Update', 'Attr %s updated to match Employment %s', join(', ', sort keys %attrsets), $currentemployment->id);
1732                return $res;
1733            }
1734        } else {
1735            return 1;
1736        }
1737    } else {
1738        # No current employment, resetting values:
1739
1740        return $self->_resetEmployment;
1741    }
1742
1743}
1744
1745# Reset attribute value set by employment
1746# except managerContact and expire
1747
1748sub _resetEmployment {
1749    my ($self) = @_;
1750
1751    $self->computeEmploymentDate;
1752
1753    my %changes = (
1754        appliedEmployement => undef,
1755    );
1756
1757    my @attributesToReset = (_reported_atributes(), qw(department));
1758
1759    foreach my $attr (@attributesToReset) {
1760        my $default = $self->base->config("unemployment.$attr") || '';
1761        my $old = $self->_get_attributes($attr) || '';
1762
1763        if ($old ne $default) {
1764            $changes{$attr} = $default || undef;
1765        }
1766    }
1767
1768    if(!$self->get_attributes('managerContact')) {
1769        if (my $next = $self->_get_attributes('nextEmployment')) {
1770            my $onext = $self->base->get_object('employment', $next);
1771            $changes{'manager'} = $onext->_get_attributes('managerContact');
1772        }
1773    }
1774
1775    # Managing expire:
1776    # If we found next status apply, keep it for expiration
1777    # Otherwise we reset to previous end of status
1778
1779    if (my $expire = $self->_computeEndEmployment($self->base->config('employment_delay') || 0)) {
1780        $changes{ 'expire' } = $expire;
1781    } elsif (my $prevEmployment = $self->_get_attributes('prevEmployment')) {
1782        my $oprev = $self->base->get_object('employment', $prevEmployment);
1783        $changes{ 'expire' } = $oprev->_get_attributes('lastday');
1784    } elsif (($self->base->config('unemployed_expire') ||'') ne 'no') {
1785        if (my $def = $self->base->{defattr}{'user.expire'}) {
1786            $changes{ 'expire' } = $def;
1787        }
1788    }
1789
1790
1791    if (%changes) {
1792        if ($self->set_fields(%changes)) {
1793            $self->base->log(LA_NOTICE, "Updating user %s to match unemployment", $self->id);
1794            $self->ReportChange('Update', 'Update %s to match unemployment', join(', ', sort keys %changes));
1795            return 1;
1796        } else {
1797            return 0;
1798        }
1799    } else {
1800        return 1;
1801    }
1802
1803}
1804
1805=head2 computeEmploymentDate
1806
1807Compute and copy to user start and end employment date
1808
1809=cut
1810
1811sub computeEmploymentDate {
1812    my ($self) = @_;
1813
1814    my $currentemployment = $self->get_attributes('currentEmployment') || '';
1815
1816    my $expire = str2time($self->_get_attributes('expire') || '1970-01-01T00:00:00');
1817
1818    my %changes;
1819    my @employmentDate = qw(
1820        endEmployment   endStrictEmployment   endCurrentEmployment   endLastEmployment
1821        startEmployment startStrictEmployment startCurrentEmployment startFirstEmployment
1822        arrival departure
1823    );
1824    foreach (@employmentDate) {
1825        my $old = $self->_get_attributes($_) || '';
1826        my $new = $self->_get_attributes("_$_") || '';
1827        if ($old ne $new) {
1828            $changes{$_} = $new || undef;
1829        }
1830    }
1831
1832    # If there is no current employment we try to find any to not let expire
1833    # unset
1834
1835    my $expireOn = $self->base->config('expireOn') || '';
1836    if (!grep { $_ eq $expireOn } (@employmentDate, '', 'never')) {
1837        $self->base->log(LA_ERR, "expireOn set to invalid parameter %s, using endEmployment instead", $expireOn);
1838        $expireOn = undef;
1839    }
1840    $expireOn ||= 'endEmployment';
1841
1842    # We check if matching start exists to know if using end* for expiration is
1843    # safe, even undef
1844    my %end2start = (
1845        endEmployment        => 'startEmployment',
1846        endStrictEmployment  => 'startStrictEmployment',
1847        endCurrentEmployment => 'startCurrentEmployment',
1848        endLastEmployment    => 'startFirstEmployment',
1849    );
1850
1851    # TODO rework this, working code but bloat
1852    if ($expireOn ne 'never') {
1853        my $endemploy = '';
1854        if ($self->_get_attributes("_$end2start{$expireOn}")) {
1855            $endemploy = $self->_get_attributes("_$expireOn") || '';
1856            $endemploy ||= 'UNCHANGED' unless($currentemployment);
1857        } elsif (($self->base->config('unemployed_expire') ||'') eq 'no') {
1858            $endemploy = '';
1859        } else {
1860            # No expiration date apply, don't touch
1861            $endemploy = 'UNCHANGED';
1862        }
1863
1864        if ($endemploy ne 'UNCHANGED') {
1865            my $nextexpire = str2time( $endemploy || '1970-01-01T00:00:00' );
1866
1867            if ($expire != $nextexpire) {
1868                $changes{expire} = $endemploy;
1869            }
1870        }
1871    }
1872
1873    if (keys %changes) {
1874        $self->base->log(LA_DEBUG, 'Applying employment state to user %s for field %s', $self->id, join(', ', keys %changes));
1875        $self->ReportChange('Update', 'Update %s to match employment', join(', ', sort keys %changes));
1876        if (exists($changes{expire})) {
1877            $self->ReportChange('Update', 'Expire update to %s to match employment', ($changes{expire} || '(none)'));
1878            $self->base->log(LA_DEBUG, 'New expiration is %s for user %s', ($changes{expire} || '(none)'), $self->id);
1879        }
1880        $self->set_fields(%changes);
1881    } else {
1882        $self->base->log(LA_DEBUG, 'No employment change for user %s', $self->id);
1883    }
1884
1885    $self->_computeEmploymentHistory();
1886
1887    return 1;
1888}
1889
1890sub _computeStartEmployment {
1891    my ($self, $delay, $any, $workday) = @_;
1892
1893    $delay ||= 0;
1894    my $start;
1895    my $nstart;
1896
1897    if (my $next = $self->_get_attributes('nextEmployment')) {
1898        my $onext = $self->base->get_object('employment', $next);
1899        $nstart = DateTime->from_epoch(epoch => str2time($onext->_get_attributes('firstday')));
1900        $nstart->set_time_zone( DateTime::TimeZone->new( name => 'local' ) );
1901    }
1902
1903    my $list_empl = $self->base->db->prepare_cached(q{
1904        SELECT *, (lastday is null or lastday >= now() - '1days'::interval) as "current" FROM employment
1905        WHERE "user" = ? and firstday < now()
1906        order by firstday desc
1907        });
1908    $list_empl->execute($self->id);
1909
1910    while (my $res = $list_empl->fetchrow_hashref) {
1911        if ($res->{current}) {
1912        } elsif ($nstart) {
1913            my $prevend = DateTime->from_epoch(epoch => str2time($res->{lastday}));
1914            $prevend->set_time_zone( DateTime::TimeZone->new( name => 'local' ) );
1915            my $tstart = $nstart->clone;
1916            $tstart->subtract(days => $delay + 1);
1917            if ($tstart->ymd gt $prevend->ymd) {
1918                last;
1919            }
1920        } elsif ((!$res->{current}) && (!$any)) {
1921            last;
1922        }
1923        $nstart = DateTime->from_epoch(epoch => str2time($res->{firstday}));
1924        $nstart->set_time_zone( DateTime::TimeZone->new( name => 'local' ) );
1925        $start = $nstart->clone;
1926    }
1927    $list_empl->finish;
1928
1929    $start ||= $nstart if ($any);
1930
1931    if ($start) {
1932        if ($workday) {
1933            my $day_of_week = $start->day_of_week;
1934            $start->add(days =>
1935                $day_of_week == 6 ? 2 :
1936                $day_of_week == 7 ? 1 : 0
1937            );
1938        }
1939    }
1940
1941    return $start ? $start->iso8601 : undef
1942}
1943
1944sub _computeEndEmployment {
1945    my ($self, $delay, $any, $workday) = @_;
1946
1947    $delay ||= 0;
1948    my $end;
1949    my $pend;
1950
1951    if (my $prev = $self->_get_attributes('prevEmployment')) {
1952        my $oprev = $self->base->get_object('employment', $prev);
1953        $pend = DateTime->from_epoch(epoch => str2time($oprev->_get_attributes('lastday')));
1954        $pend->set_time_zone( DateTime::TimeZone->new( name => 'local' ) );
1955        $pend->add(hours => 23, minutes => 59, seconds => 59);
1956    }
1957
1958    my $list_empl = $self->base->db->prepare_cached(q{
1959        SELECT *, firstday <= now() as "current" FROM employment WHERE "user" = ? and
1960        (lastday is null or lastday >= now() - '1 days'::interval)
1961        order by firstday asc
1962        });
1963    $list_empl->execute($self->id);
1964    while (my $res = $list_empl->fetchrow_hashref) {
1965        if (!$res->{lastday}) {
1966            # Ultimate employment.
1967            $list_empl->finish;
1968            return undef;
1969        }
1970        if ($res->{current}) {
1971        } elsif ($end) {
1972            my $nextstart = DateTime->from_epoch(epoch => str2time($res->{firstday}));
1973            $nextstart->set_time_zone( DateTime::TimeZone->new( name => 'local' ) );
1974            my $tend = $end->clone;
1975            $tend->add(days => $delay + 1);
1976            if ($tend->ymd lt $nextstart->ymd) {
1977                last;
1978            }
1979        } elsif ($pend) {
1980            my $nextstart = DateTime->from_epoch(epoch => str2time($res->{firstday}));
1981            $nextstart->set_time_zone( DateTime::TimeZone->new( name => 'local' ) );
1982            my $tend = $pend->clone;
1983            $tend->add(days => $delay + 1);
1984            if ($tend->ymd lt $nextstart->ymd) {
1985                last;
1986            }
1987            $end = $tend
1988        } elsif ((!$res->{current}) && (!$any)) {
1989            last;
1990        }
1991        $end = DateTime->from_epoch(epoch => str2time($res->{lastday}));
1992        $end->set_time_zone( DateTime::TimeZone->new( name => 'local' ) );
1993        $end->add(hours => 23, minutes => 59, seconds => 59);
1994    }
1995    $list_empl->finish;
1996
1997    $end ||= $pend if($any);
1998
1999    if ($end) {
2000        if ($workday) {
2001            my $day_of_week = $end->day_of_week;
2002            $end->subtract(days =>
2003                $day_of_week == 7 ? 2 :
2004                $day_of_week == 6 ? 1 : 0
2005            );
2006        }
2007    }
2008
2009    return $end ? $end->iso8601 : undef
2010}
2011
2012# Compute a summary of employement and store the value
2013# into an attribute
2014
2015sub _computeEmploymentHistory {
2016    my ($self) = @_;
2017
2018    my %changes;
2019
2020    {
2021        my $sth = $self->db->prepare_cached(q{
2022            select employment.name from employment
2023            where "user" = ? and employment.firstday < now()
2024        });
2025        $sth->execute($self->id);
2026        my @values;
2027        while (my $res = $sth->fetchrow_hashref) {
2028            push(@values, $res->{name});
2029        }
2030        $changes{"employmentHistory"} = \@values;
2031    }
2032
2033    foreach my $attribute (qw(contratType)) {
2034        my $sth = $self->db->prepare_cached(q{
2035            select employment_attributes.value from employment
2036            join employment_attributes
2037            on employment.ikey = employment_attributes.okey
2038            where "user" = ? and employment_attributes.attr = ?
2039            and employment.firstday < now()
2040            group by employment_attributes.value
2041            });
2042        $sth->execute($self->id, $attribute);
2043        my @values;
2044        while (my $res = $sth->fetchrow_hashref) {
2045            push(@values, $res->{value});
2046        }
2047        $changes{$attribute . "History"} = \@values;
2048    }
2049
2050    $self->set_fields(%changes);
2051}
2052
2053=head2 storeBannedPassword($epassword)
2054
2055Add an encrypted password to untrust list
2056
2057=cut
2058
2059sub storeBannedPassword {
2060    my ($self, $EncPass) = @_;
2061
2062    my @banned = sort { $b cmp $a } $self->_get_attributes('bannedPassword');
2063    my $now = DateTime->now; 
2064    unshift(@banned, $now->iso8601 . ';' . $EncPass);
2065    $self->set_fields('bannedPassword', [ grep { $_ } @banned ]);
2066
2067}
2068
2069=head2 banCurrentPassword
2070
2071Store the current password as banned
2072
2073=cut
2074
2075sub banCurrentPassword {
2076    my ($self) = @_;
2077   
2078    my $old = $self->get_field('userPassword') or return;
2079    $self->storeBannedPassword($old);
2080}
2081
2082sub check_password {
2083    my ( $self, $password ) = @_;
2084
2085    my $res = $self->SUPER::check_password($password);
2086    if ($res !~ /^ok$/) {
2087        return $res;
2088    }
2089
2090    foreach my $banned ($self->_get_attributes('bannedPassword')) {
2091        my ($date, $oldPassword) = $banned =~ /^([^;]*);(.*)/;
2092        if (crypt($password, $oldPassword) eq $oldPassword) {
2093            return "Banned password, cannot be used anymore";
2094        }
2095    }
2096
2097    return 'ok';
2098}
2099
2100sub _set_password {
2101    my ($self, $clear_pass) = @_;
2102    if (my $attr = $self->base->attribute($self->type, 'userPassword')) {
2103        my $field = $attr->iname;
2104
2105        # Storing as old password
2106        my @olds = sort { $b cmp $a } $self->_get_attributes('oldPassword');
2107        if (my $old = $self->get_field('userPassword')) {
2108            my $now = DateTime->now; 
2109            unshift(@olds, $now->iso8601 . ';' . $old);
2110            $self->set_fields('oldPassword', [ grep { $_ } @olds[0 .. 14] ]);
2111        }
2112
2113        my $res = $self->set_fields($field, $self->base->passCrypt($clear_pass));
2114        if ($res) {
2115            if ($self->base->get_global_value('rsa_public_key')) {
2116                $self->setCryptPassword($clear_pass) or return;
2117            }
2118        }
2119
2120        $self->set_fields('passwordLastSet', DateTime->now->datetime);
2121        $self->base->log(LA_NOTICE,
2122            'Mot de passe changé pour %s',
2123            $self->id
2124        );
2125
2126
2127        return $res;
2128    } else {
2129        $self->log(LA_WARN,
2130            "Cannot set password: userPassword attributes is unsupported");
2131    }
2132}
2133
2134=head2 setCryptPassword($clear_pass)
2135
2136Store password encrypted using RSA encryption.
2137
2138=cut
2139
2140sub setCryptPassword {
2141    my ($self, $clear_pass) = @_;
2142    if (my $serialize = $self->base->get_global_value('rsa_public_key')) {
2143        my $public = Crypt::RSA::Key::Public->new;
2144        $public = $public->deserialize(String => [ $serialize ]);
2145        my $rsa = new Crypt::RSA ES => 'PKCS1v15';
2146        my $rsa_password = $rsa->encrypt (
2147            Message    => $clear_pass,
2148            Key        => $public,
2149            Armour     => 1,
2150        ) || die $rsa->errstr();
2151        if (!$self->_set_c_fields('encryptedPassword', $rsa_password)) {
2152            $self->log(LA_ERR,
2153                "Cannot set 'encryptedPassword' attribute for object %s/%s",
2154                $self->type, $self->id,
2155            );
2156            return;
2157        }
2158    }
2159    $self->ReportChange('Password', 'Password stored using internal key');
2160    return 1;
2161}
2162
2163=head2 _InjectCryptPasswd($cryptpasswd)
2164
2165Inject a password encrypted using standard UNIX method.
2166
2167The passwrod will be used to authenticate user inside the application but it
2168will not be transmit to any other database.
2169
2170=cut
2171
2172sub _InjectCryptPasswd {
2173    my ($self, $cryptpasswd) = @_;
2174
2175    if (my $current = $self->get_field('userPassword')) {
2176        if ($cryptpasswd eq $current) {
2177            return 1;
2178        }
2179    }
2180    my $res = $self->set_fields('userPassword', $cryptpasswd);
2181
2182    if ($res) {
2183        $self->base->log(LA_NOTICE, 'Crypted password injected for %s', $self->id);
2184        return 1;
2185    } else {
2186        $self->base->log(LA_ERR, 'Cannot inject crypted password for %s', $self->id);
2187        return 0;
2188    }
2189}
2190
2191=head2 GenPasswordResetId
2192
2193Return a new id allowing passowrd reset
2194
2195=cut
2196
2197sub GenPasswordResetId {
2198    my ($self) = @_;
2199
2200    my $id = LATMOS::Accounts::Utils::genpassword(length => 32);
2201
2202    my $sth = $self->base->db->prepare_cached(q{
2203        INSERT INTO passwordreset (id, "user") values (?,?)
2204    });
2205
2206    if ($sth->execute($id, $self->id)) {
2207        return $id;
2208    } else {
2209        return;
2210    }
2211}
2212
2213=head2 SendPasswordReset($url)
2214
2215Generate a password reset Id and the to the user.
2216
2217C<$url> is the URL where the password can changed (printf forward, the %s is
2218replaced by the request id)
2219
2220=cut
2221
2222sub SendPasswordReset {
2223    my ($self, $url) = @_;
2224
2225    my $id = $self->GenPasswordResetId;
2226
2227    my $mail = $self->_get_attributes('mail') or do {
2228        $self->base->log(LA_ERR, "Cannot sent reset password mail: no mail found");
2229        return;
2230    };
2231
2232    my $MailSubject = $self->base->la->val('_default_', 'mailSubject', 'LATMOS::Accounts');
2233
2234    my %mail = (
2235        Subject => "$MailSubject: pasword reset",
2236        'X-LATMOS-Reason' => 'Password Reset',
2237        to => $mail,
2238    );
2239
2240    if (my $otherEmail = $self->_get_attributes('otherEmail')) {
2241        $mail{cc} = $otherEmail;
2242    }
2243
2244    my $vars = {
2245        url => sprintf($url, $id),
2246    };
2247    $vars->{id} =  $id;
2248    $vars->{obj} = $self;
2249
2250    my $lamail = LATMOS::Accounts::Mail->new(
2251        $self->base->la,
2252        'passwordreset.mail',
2253    );
2254
2255    if ($lamail->process(\%mail, $vars)) {
2256        $self->base->log(LA_NOTICE,
2257            "Reset password sent to %s for user %s",
2258            $mail{to},
2259            $self->id,
2260        );
2261        return 1;
2262    } else {
2263        return;
2264    }
2265
2266}
2267
2268=head2 CheckPasswordResetId($id)
2269
2270Return True if the reset password ID can be found and is less than one day old
2271
2272=cut
2273
2274sub CheckPasswordResetId {
2275    my ($self, $id) = @_;
2276
2277    my $sth = $self->base->db->prepare_cached(q{
2278        SELECT * FROM passwordreset WHERE
2279            "user" = ? and
2280            id = ? and
2281            "create" >= now() - '1 days'::interval
2282    });
2283    $sth->execute($self->id, $id);
2284
2285    my $res = $sth->fetchrow_hashref;
2286    $sth->finish;
2287
2288    return $res ? 1 : 0;
2289}
2290
2291=head2 DeletePasswordId($id)
2292
2293Delete password reset C<$id> and all expired request
2294
2295=cut
2296
2297sub DeletePasswordId {
2298    my ($self, $id) = @_;
2299
2300    my $sth = $self->base->db->prepare_cached(q{
2301        DELETE FROM passwordreset WHERE
2302        "user" = ? AND (id = ? or "create" < now() - '1 days'::interval)
2303    });
2304
2305    $sth->execute($self->id, $id);
2306}
2307
23081;
2309
2310__END__
2311
2312=head1 SEE ALSO
2313
2314=head1 AUTHOR
2315
2316Olivier Thauvin, E<lt>olivier.thauvin@latmos.ipsl.frE<gt>
2317
2318=head1 COPYRIGHT AND LICENSE
2319
2320Copyright (C) 2008, 2009 CNRS SA/CETP/LATMOS
2321
2322This library is free software; you can redistribute it and/or modify
2323it under the same terms as Perl itself, either Perl version 5.10.0 or,
2324at your option, any later version of Perl 5 you may have available.
2325
2326=cut
Note: See TracBrowser for help on using the repository browser.