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

Last change on this file since 1287 was 1287, checked in by nanardon, 9 years ago

ensure changes on group members are correctly reported

  • Property svn:keywords set to Id Rev
File size: 27.7 KB
Line 
1package LATMOS::Accounts::Bases::Sql::User;
2
3use 5.010000;
4use strict;
5use warnings;
6
7use LATMOS::Accounts::Utils;
8use LATMOS::Accounts::Log;
9use POSIX qw(strftime);
10use base qw(LATMOS::Accounts::Bases::Sql::objects);
11
12our $VERSION = (q$Rev$ =~ /^Rev: (\d+) /)[0];
13
14=head1 NAME
15
16LATMOS::Ad - Perl extension for blah blah blah
17
18=head1 DESCRIPTION
19
20Account base access over standard unix file format.
21
22=head1 FUNCTIONS
23
24=cut
25
26=head2 new(%config)
27
28Create a new LATMOS::Ad object for windows AD $domain.
29
30domain / server: either the Ad domain or directly the server
31
32ldap_args is an optionnal list of arguments to pass to L<Net::LDAP>.
33
34=cut
35
36sub _object_table { 'user' }
37
38sub _key_field { 'name' }
39
40sub _has_extended_attributes { 1 }
41
42sub _get_attr_schema {
43    my ($class, $base) = @_;
44
45    $class->SUPER::_get_attr_schema($base,
46        {
47            uidNumber => {
48                inline => 1,
49                iname => 'uidnumber',
50                uniq => 1,
51                mandatory => 1,
52                formopts => { length => 7 },
53                notify => 1,
54            },
55            uidnumber => { inline => 1, hide => 1, },
56            gidNumber => {
57                notify => 1,
58                inline => 1,
59                iname => 'gidnumber',
60                mandatory => 1,
61                can_values => sub {
62                    map { $base->get_object('group',
63                            $_)->get_attributes('gidNumber') }
64                    $base->list_objects('group')
65                },
66                display => sub {
67                    my ($self, $val) = @_;
68                    my ($gr) = $self->base->search_objects('group', "gidNumber=$val")
69                        or return;
70                    return $gr;
71                },
72                reference => 'group',
73            },
74            loginShell => { mandatory => 1 },
75            gidnumber => { inline => 1, hide => 1,
76                can_values => sub {
77                    map { $_->get_attributes('gidNumber') }
78                    map { $base->get_object('group', $_) }
79                    $base->list_objects('group')
80                },
81                mandatory => 1,
82                reference => 'group',
83            },
84            exported  => {
85                inline => 1,
86                formtype => 'CHECKBOX',
87                notify => 1,
88            },
89            locked    => {
90                formtype => 'CHECKBOX',
91                formopts => { rawvalue => 1, },
92                notify => 1,
93            },
94            expire    => { inline => 1, formtype => 'DATE', notify => 1, },
95            name      => { inline => 1, ro => 1, },
96            cn        => {
97                inline => 1, ro => 1,
98                get => sub {
99                    my ($self) = @_;
100                    return join(' ', grep { $_ } 
101                        (
102                            $self->object->_get_c_field('givenName'),
103                            $self->object->_get_c_field('sn')
104                        )
105                    )
106                    || $self->object->_get_c_field('description')
107                    || $self->object->id;
108                },
109            },
110            create    => { inline => 1, ro => 1, },
111            date      => { inline => 1, ro => 1, },
112            memberOf  => {
113                multiple => 1, delayed => 1,
114                get => sub {
115                    my ($self) = @_;
116                    my $obj = $self->object;
117                    my $sth = $obj->db->prepare_cached(
118                        q{
119                        select name from "group" join
120                        group_attributes on group_attributes.okey = "group".ikey
121                        where value = ? and attr = ?
122                        }
123                    );
124                    $sth->execute($obj->id, 'memberUID');
125                    my @res;
126                    while (my $res = $sth->fetchrow_hashref) {
127                        push(@res, $res->{name});
128                    }
129                    return \@res;
130                }
131            },
132            forward   => {},
133            aliases   => {
134                reference => 'aliases',
135                formtype => 'TEXT',
136                multiple => 1,
137            },
138            revaliases => {
139                formtype => 'TEXT',
140            },
141            manager => {
142                delayed => 1,
143                can_values => sub {
144                    my %uniq = map { $_ => 1 } grep { $_ }
145                    ($_[1] ? $_[1]->get_attributes('manager') : ()),
146                    $base->search_objects('user', 'active=*');
147                    sort keys %uniq;
148                },
149                reference => 'user',
150                notify => 1,
151            },
152            department => {
153                reference => 'group',
154                can_values => sub {
155                    $base->search_objects('group', 'sutype=dpmt')
156                },
157                notify => 1,
158            },
159            contratType => {
160                reference => 'group',
161                can_values => sub {
162                    $base->search_objects('group', 'sutype=contrattype')
163                },
164                notify => 1,
165            },
166            site => {
167                reference => 'site',
168                can_values => sub {
169                    $base->search_objects('site')
170                }
171            },
172            co => { },
173            l => { },
174            postalCode => { },
175            streetAddress => { formtype => 'TEXTAREA', },
176            postOfficeBox => { },
177            st => { },
178            facsimileTelephoneNumber => { },
179            o => { iname => 'company', ro => 1 },
180            ou => { iname => 'department', ro => 1 },
181            telephoneNumber => { },
182            physicalDeliveryOfficeName => { },
183            uid => { iname => 'name', ro => 1 },
184            cn =>  { iname => 'name', ro => 1 },
185            gecos => {
186                ro => 1,
187                get => sub {
188                    my ($self) = @_;
189                    my $obj = $self->object;
190                    my $gecos = sprintf("%s,%s,%s,%s",
191                        join(' ', grep { $_ }
192                                ($obj->_get_c_field('givenName'),
193                                ($obj->_get_c_field('sn'))))
194                            || $obj->_get_c_field('description') || '',
195                        join(' - ', grep { $_ } (($obj->_get_c_field('site') ||
196                                    $obj->_get_c_field('l')),
197                            $obj->_get_c_field('physicalDeliveryOfficeName'))) || '',
198                        $obj->_get_c_field('telephoneNumber') || '',
199                        $obj->_get_c_field('expireText') || '',
200                    );
201                    $gecos =~ s/:/ /g;
202                    return to_ascii($gecos);
203                },
204            },
205            displayName  => {
206                ro => 1, managed => 1,
207                get => sub {
208                    my ($self) = @_;
209                    return join(' ', grep { $_ } 
210                        (
211                            $self->object->_get_c_field('givenName'),
212                            $self->object->_get_c_field('sn')
213                        )
214                    )
215                    || $self->object->_get_c_field('description')
216                    || $self->object->id;
217                },
218            },
219            sAMAccountName  => { ro => 1, managed => 1  },
220            accountExpires => {
221                ro => 1,
222                managed => 1,
223                get => sub {
224                    my ($self) = @_;
225                    my $obj = $self->object;
226                    my $sth = $obj->db->prepare_cached(
227                        sprintf(
228                            q{select extract(epoch from expire) + 11644474161 as expire
229                            from %s where %s = ?},
230                            $obj->db->quote_identifier($obj->_object_table),
231                            $obj->db->quote_identifier($obj->_key_field),
232                        )
233                    );
234                    $sth->execute($obj->id);
235                    my $res = $sth->fetchrow_hashref;
236                    $sth->finish;
237                    return $res->{expire} ? sprintf("%.f", $res->{expire} * 1E7) : '9223372036854775807';
238                }
239            },
240            shadowExpire => {
241                ro => 1,
242                managed => 1,
243                get => sub {
244                    my ($self) = @_;
245                    my $obj = $self->object;
246                    my $sth = $obj->db->prepare_cached(
247                        sprintf(
248                            q{select justify_hours(expire - '1/1/1970'::timestamp) as expire
249                            from %s where %s = ?},
250                            $obj->db->quote_identifier($obj->_object_table),
251                            $obj->db->quote_identifier($obj->_key_field),
252                        )
253                    );
254                    $sth->execute($obj->id);
255                    my $res = $sth->fetchrow_hashref;
256                    $sth->finish;
257                    return -1 unless($res->{expire});
258                    $res->{expire} =~ /(\d+) days\s*(\w)?/;
259                    return $1 + ($2 ? 1 : 0);
260                }
261            },
262            directReports => {
263                reference => 'user',
264                ro => 1,
265                delayed => 1,
266                get => sub {
267                    my ($self) = @_;
268                    my $obj = $self->object;
269                    my $sth = $obj->db->prepare_cached(
270                        q{
271                        select name from "user" join
272                        user_attributes on user_attributes.okey = "user".ikey
273                        where value = ? and attr = ?
274                        }
275                    );
276                    $sth->execute($obj->id, 'manager');
277                    my @res;
278                    while (my $res = $sth->fetchrow_hashref) {
279                        push(@res, $res->{name});
280                    }
281                    return \@res;
282                },
283            },
284            managedObjects => { ro => 1, reference => 'group', },
285            otheraddress => { ro => 1, reference => 'address', },
286            mainaddress => { ro => 1, reference => 'address', },
287            postalAddress => { ro => 1, },
288            facsimileTelephoneNumber => { ro => 1, },
289            allsite   => {
290                ro => 1,
291                reference => 'site',
292            },
293            managerContact => {
294                ro => 1,
295                reference => 'user',
296                get => sub {
297                    my ($self) = @_;
298                    if (my $manager = $self->object->_get_c_field('manager')) {
299                        return $manager;
300                    } elsif (my $department = $self->object->_get_c_field('department')) {
301                        my $obj = $self->base->get_object('group', $department);
302                        return $obj->_get_c_field('managedBy');
303                    } else {
304                        return;
305                    }
306                },
307            },
308            expireText => {
309                ro => 1,
310                get => sub {
311                    my ($self) = @_;
312                    my $obj = $self->object;
313                    my $sth = $obj->db->prepare_cached(
314                        sprintf(
315                            q{select to_char(expire, 'YYYY/MM/DD') as expire
316                            from %s where %s = ?},
317                            $obj->db->quote_identifier($obj->_object_table),
318                            $obj->db->quote_identifier($obj->_key_field),
319                        )
320                    );
321                    $sth->execute($obj->id);
322                    my $res = $sth->fetchrow_hashref;
323                    $sth->finish;
324                    return $res->{expire}
325                },
326            },
327            krb5ValidEnd => { ro => 1, },
328            cells  => {
329                ro => 1,
330                reference => 'group',
331            },
332            departments => {
333                reference => 'group',
334                delayed => 1,
335                ro => 1,
336            },
337            arrivalDate => { },
338            expired => { ro => 1 },
339            active => { ro => 1 },
340            pwdAccountLockedTime => {
341                managed => 1,
342                ro => 1,
343                get => sub {
344                    my ($self) = @_;
345                    my $obj = $self->object;
346                    if ($obj->_get_c_field('locked')) {
347                        return '000001010000Z';
348                    } else {
349                        my $sth = $obj->db->prepare_cached(
350                            sprintf(
351                                q{select to_char(expire AT TIME ZONE 'Z', 'YYYYMMDDHH24MISSZ') as expire
352                                from %s where %s = ? and expire < now()},
353                                $obj->db->quote_identifier($obj->_object_table),
354                                $obj->db->quote_identifier($obj->_key_field),
355                            )
356                        );
357                        $sth->execute($obj->id);
358                        my $res = $sth->fetchrow_hashref;
359                        $sth->finish;
360                        return $res->{expire}
361                    }
362                },
363            },
364            userPassword => { readable => 0, },
365        }
366    )
367}
368
369sub get_field {
370    my ($self, $field) = @_;
371    if ($field eq 'sAMAccountName') {
372        return $self->id;
373    } elsif ($field eq 'krb5ValidEnd') {
374        my $sth = $self->db->prepare_cached(
375            sprintf(
376                q{select date_part('epoch', expire)::int as expire
377                from %s where %s = ?},
378                $self->db->quote_identifier($self->_object_table),
379                $self->db->quote_identifier($self->_key_field),
380            )
381        );
382        $sth->execute($self->id);
383        my $res = $sth->fetchrow_hashref;
384        $sth->finish;
385        return $res->{expire}
386    } elsif ($field eq 'pwdAccountLockedTime') {
387    } elsif ($field eq 'otheraddress') {
388        my $sth = $self->db->prepare_cached(q{
389            select name from address left join address_attributes
390            on address.ikey = address_attributes.okey and
391            address_attributes.attr = 'isMainAddress'
392            where "user" = ?
393            order by address_attributes.attr
394        });
395        $sth->execute($self->id);
396        my @values;
397        while (my $res = $sth->fetchrow_hashref) {
398            push(@values, $res->{name});
399        }
400        return \@values;
401    } elsif ($field eq 'mainaddress') {
402        my $sth = $self->db->prepare_cached(q{
403            select name from address join address_attributes on ikey = okey
404            where "user" = ? and attr = 'isMainAddress'
405            });
406        $sth->execute($self->id);
407        my $res = $sth->fetchrow_hashref;
408        $sth->finish;
409        return $res->{name};
410    } elsif (grep { $field eq $_ } qw(postalAddress
411            co l postalCode streetAddress
412            postOfficeBox st
413            facsimileTelephoneNumber
414            o telephoneNumber
415            physicalDeliveryOfficeName
416            site
417        )) {
418        if (my $fmainaddress = $self->_get_c_field('mainaddress')) {
419            my $address = $self->base->get_object('address', $fmainaddress);
420            if ($address) {
421                return $address->_get_c_field($field);
422            } else { # can't happend
423                return;
424            }
425        } else {
426            return $self->SUPER::get_field($field);
427        }
428    } elsif ($field eq 'aliases') {
429        my $sth = $self->db->prepare(q{
430            select name from aliases where array[lower($1)] =
431                string_to_array(lower(array_to_string("forward", ',')), ',')
432        } . ($self->base->{wexported} ? '' : 'and exported = true'));
433        $sth->execute($self->id);
434        my @values;
435        while (my $res = $sth->fetchrow_hashref) {
436            push(@values, $res->{name});
437        }
438        return \@values;
439    } elsif ($field eq 'forward') {
440        my $sth = $self->db->prepare(q{
441            select forward from aliases where name = ?
442        } . ($self->base->{wexported} ? '' : ' and exported = true'));
443        $sth->execute($self->id);
444        my $res = $sth->fetchrow_hashref;
445        $sth->finish;
446        return $res->{forward}
447    } elsif ($field eq 'revaliases') {
448        my $sth = $self->db->prepare(q{
449            select "as" from revaliases where name = ?
450        } . ($self->base->{wexported} ? '' : ' and exported = true'));
451        $sth->execute($self->id);
452        my $res = $sth->fetchrow_hashref;
453        $sth->finish;
454        return $res->{as}
455    } elsif ($field eq 'managerContact') {
456    } else {
457        return $self->SUPER::get_field($field);
458    }
459}
460
461sub _get_state {
462    my ($self, $state) = @_;
463    for ($state) {
464        /^expired$/ and do {
465            my $attribute = $self->attribute('expire');
466            $attribute->check_acl('r') or return;
467            my $sth = $self->db->prepare_cached(
468                q{ select coalesce(expire < now(), false) as exp from "user"
469                where "user".name = ?}
470            );
471            $sth->execute($self->id);
472            my $res = $sth->fetchrow_hashref;
473            $sth->finish;
474            return $res->{exp} ? 1 : 0;
475        };
476    }
477}
478
479sub set_fields {
480    my ($self, %data) = @_;
481    my %fdata;
482    my $res = 0;
483    foreach my $attr (keys %data) {
484        $attr =~ /^(un)?exported$/ and do {
485            if (my $obj = $self->base->
486                get_object('revaliases', $self->id)) {
487                my $ares = $obj->set_c_fields(
488                    ($attr eq 'exported' ? 'exported' : 'unexported') => $data{$attr}
489                );
490                if (defined($ares)) {
491                    $res+=$ares;
492                } else {
493                    $self->base->log(LA_ERR,
494                        'Cannot set revaliases exported attribute for user %s',
495                        $self->id);
496                }
497            }
498            my $must_expire = $attr eq 'exported'
499                ? ($data{$attr} ? 0 : 1 )
500                : ($data{$attr} ? 1 : 0 );
501
502            foreach my $al ($self->get_attributes('aliases')) {
503                my $obj = $self->base->get_object('aliases', $al) or next;
504                $obj->_set_c_fields(
505                    expire => $must_expire
506                        ? strftime(
507                            "%Y-%m-%d %H:%M:%S",
508                            localtime(time + 3600 * 24 * 365)
509                          )
510                        : undef,
511                );
512            }
513        };
514        $attr eq 'gidnumber' && $data{$attr} !~ /^\d+$/ and do {
515            my $group = $self->base->get_object('group', $data{$attr}) or do {
516                $self->base->log(LA_ERROR,
517                    "Can't set gidNumber to %s: no such group", $data{$attr});
518                return;
519            };
520            $data{$attr} = $group->get_attributes('gidNumber');
521        };
522        $attr =~ /^memberOf$/ and do {
523            my %members;
524            my $memberof = $self->get_field('memberOf');
525            foreach (ref $memberof
526                ? @{ $memberof }
527                : $memberof || ()) {
528                $members{$_}{c} = 1;
529            }
530            foreach (grep { $_ } ref $data{$attr} ? @{ $data{$attr} || []} : $data{$attr}) {
531                $members{$_}{n} = 1;
532            }
533
534            foreach my $member (keys %members) {
535                $members{$member}{c} && $members{$member}{n} and next; # no change !
536                my $group = $self->base->get_object('group', $member) or do {
537                    la_log(LA_WARN, "Cannot get group %s to set members", $member); 
538                    next;
539                };
540                ($group->_get_c_field('sutype') || '') =~ /^(jobtype|contrattype)$/ and next;
541                if ($members{$member}{n}) {
542                    my @newmembers = $group->get_attributes('memberUID');
543                    $res += $group->_set_c_fields('memberUID', [ $self->id, @newmembers ]);
544                } elsif ($members{$member}{c}) {
545                    if (($self->_get_c_field('department') || '') eq $group->id) {
546                        $self->base->log(LA_WARN,
547                            "Don't removing user %s from group %s: is it's department",
548                            $self->id, $group->id);
549                        next;
550                    }
551                    my @newmembers = grep { $_ ne $self->id } $group->get_attributes('memberUID');
552                    $res += $group->_set_c_fields('memberUID', [ @newmembers ]);
553                } # else {} # can't happend
554            }
555            next;
556        };
557        $attr =~ /^forward$/ and do {
558            if ($data{$attr}) {
559                if (my $f = $self->base->get_object('aliases', $self->id)) {
560                    $res += $f->_set_c_fields(forward => $data{$attr});
561                } else {
562                    if ($self->base->_create_c_object(
563                            'aliases', $self->id,
564                            forward => $data{$attr},
565                            description => 'automatically created for ' . $self->id,
566                        )) {
567                        $res++;
568                    } else {
569                        $self->base->log(LA_ERR, "Cannot add forward for %s",
570                            $self->id);
571                    }
572                }
573            } else {
574                if ($self->base->_delete_object('aliases', $self->id)) {
575                    $res++;
576                } else {
577                    $self->base->log(LA_ERR, "Cannot remove forward for %s",
578                        $self->id);
579                }
580            }
581            next;
582        };
583        $attr =~ /^aliases$/ and do {
584            my %aliases = map { $_ => 1 } grep { $_ } (ref $data{$attr} ? @{$data{$attr}} :
585                $data{$attr});
586            foreach ($self->_get_attributes('aliases')) {
587                $aliases{$_} ||= 0;
588                $aliases{$_} +=2;
589            }
590            foreach (keys %aliases) {
591                if ($aliases{$_} == 2) {
592                    if ($self->base->_delete_object('aliases', $_)) {
593                        $res++
594                    } else {
595                        $self->base->log(LA_ERR,
596                            "Cannot remove aliases %s from user %s", $_,
597                            $self->id);
598                    }
599                } elsif ($aliases{$_} == 1) {
600                    if ($self->base->_create_c_object(
601                            'aliases', $_,
602                            forward => [ $self->id ],
603                            description => 'automatically created for ' . $self->id,
604                        )) {
605                        $res++
606                    } else {
607                        $self->base->log(LA_ERR, 'Cannot set forward %s to user %s',
608                            $_, $self->id);
609                        return
610                    }
611                } # 3 no change
612            }
613            next;
614        };
615        $attr =~ /^revaliases$/ and do {
616            if ($data{$attr}) {
617                if (my $obj = $self->base->
618                        get_object('revaliases', $self->id)) {
619                    my $ares = $obj->set_c_fields(
620                        'as' => $data{$attr},
621                        'exported' => ($self->get_attributes('exported') || 0),
622                    );
623                    if (defined($ares)) {
624                        $res+=$ares;
625                    } else {
626                        $self->base->log(LA_ERR, 'Cannot set revaliases for user %s',
627                            $self->id);
628                    }
629                } else {
630                    if ($self->base->_create_c_object(
631                        'revaliases',
632                        $self->id, as => $data{$attr},
633                        'exported' => ($self->get_attributes('exported') || 0),
634                        description => 'automatically created for ' . $self->id,
635                    )) {
636                        $res++;
637                    } else {
638                        $self->base->log(LA_ERR, 'Cannot set revaliases for user %s',
639                            $self->id);
640                    }
641                }
642            } else {
643                $self->base->_delete_object('revaliases', $self->id);
644                $res++;
645            }
646            next;
647        };
648        $attr =~ /^department$/ and do {
649            if ($data{$attr}) {
650                my $dpmt = $self->base->get_object('group', $data{$attr}) or do {
651                    $self->base->log(LA_ERR, 
652                        "Group %s does not exists",
653                        $data{$attr});
654                    return;
655                };
656                if ((($dpmt->_get_c_field('sutype') || '') ne 'dpmt')) {
657                    $self->base->log(LA_ERR, "Group %s is not a department",
658                        $data{$attr});
659                    return;
660                }
661            }
662        };         
663        $attr =~ /^jobType$/ and do {
664            if ($data{$attr}) {
665                my $dpmt = $self->base->get_object('group', $data{$attr}) or do {
666                    $self->base->log(LA_ERR, 
667                        "Group %s does not exists",
668                        $data{$attr});
669                    return;
670                };
671                if ((($dpmt->_get_c_field('sutype') || '') ne 'jobtype')) {
672                    $self->base->log(LA_ERR, "Group %s is not a jobtype",
673                        $data{$attr});
674                    return;
675                }
676            }
677        };         
678        $attr =~ /^contratType$/ and do {
679            if ($data{$attr}) {
680                my $dpmt = $self->base->get_object('group', $data{$attr}) or do {
681                    $self->base->log(LA_ERR, 
682                        "Group %s does not exists",
683                        $data{$attr});
684                    return;
685                };
686                if ((($dpmt->_get_c_field('sutype') || '') ne 'contrattype')) {
687                    $self->base->log(LA_ERR, "Group %s is not a contrattype",
688                        $data{$attr});
689                    return;
690                }
691            }
692        };         
693        grep { $attr eq $_ } (qw(co l postalCode streetAddress
694            postOfficeBox st facsimileTelephoneNumber
695            o telephoneNumber physicalDeliveryOfficeName site)) and do {
696            my $fmainaddress = $self->_get_c_field('mainaddress');
697            # set address attribute => create address object on the fly
698            # except if attr is empty !
699            if (!$fmainaddress && $data{$attr}) {
700                $fmainaddress = $self->id . '-' . join('', map { ('a'..'z')[rand(26)] }
701                (0..4));
702                $self->base->_create_c_object(
703                    'address', $fmainaddress,
704                    user => $self->id,
705                    isMainAddress => 1, ) or do {
706                    $self->base->log(LA_ERR,
707                        "Cannot create main address for user %s", $self->id);
708                    return;
709                };
710            }
711            if ($fmainaddress && 
712                (my $address = $self->base->get_object('address', $fmainaddress))) {
713                if ($address->attribute($attr) &&
714                    !$address->attribute($attr)->ro) {
715                    $res += $address->set_c_fields($attr => $data{$attr}) ||0;
716                }
717            }
718            next;
719        };
720        $fdata{$attr} = $data{$attr} || undef;
721    }
722    if (keys %fdata) {
723        if (defined(my $res2 = $self->SUPER::set_fields(%fdata))) {
724           return $res2 + $res;
725       } else {
726           return;
727       }
728    } else { return $res; }
729}
730
731
7321;
733
734__END__
735
736=head1 SEE ALSO
737
738=head1 AUTHOR
739
740Olivier Thauvin, E<lt>olivier.thauvin@latmos.ipsl.frE<gt>
741
742=head1 COPYRIGHT AND LICENSE
743
744Copyright (C) 2008, 2009 CNRS SA/CETP/LATMOS
745
746This library is free software; you can redistribute it and/or modify
747it under the same terms as Perl itself, either Perl version 5.10.0 or,
748at your option, any later version of Perl 5 you may have available.
749
750=cut
Note: See TracBrowser for help on using the repository browser.