source: branches/4.0/LATMOS-Accounts/lib/LATMOS/Accounts/Bases/Sql/User.pm @ 1299

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

backport fix

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