source: trunk/LATMOS-Accounts/lib/LATMOS/Accounts/Bases/Objects.pm @ 2045

Last change on this file since 2045 was 2041, checked in by nanardon, 7 years ago

Allow to choose crypt algorythm per configuration

  • Property svn:keywords set to Id Rev
File size: 20.1 KB
RevLine 
[2]1package LATMOS::Accounts::Bases::Objects;
2
3use 5.010000;
4use strict;
5use warnings;
[1329]6
7use overload '""' => 'stringify';
8
[298]9use LATMOS::Accounts::Log;
[852]10use LATMOS::Accounts::Bases::Attributes;
[584]11use Crypt::Cracklib;
[2]12
[3]13our $VERSION = (q$Rev$ =~ /^Rev: (\d+) /)[0];
[2]14
[3]15=head1 NAME
16
17LATMOS::Accounts::Bases::Objects - Base class for account objects
18
19=head1 SYNOPSIS
20
21  use LATMOS::Accounts::Bases::Objects;
22  LATMOS::Accounts::Bases::Objects->new($base, $type, $id);
23
24=head1 DESCRIPTION
25
26=head1 FUNCTIONS
27
28=cut
29
[1023]30=head2 is_supported
31
32If exists, must return true or false if the object is supported or not
33
34=cut
35
[28]36=head2 list($base)
37
38List object supported by this module existing in base $base
39
[42]40Must be provide by object class
41
42    sub list {
[63]43        my ($class, $base) = @_;
[42]44    }
45
[28]46=cut
47
[1865]48=head2 listReal($base)
49
50List object supported by this module existing in base $base
51
52Can be override by base driver. The result must exclude specials object such alias.
53
54=cut
55
56sub listReal {
57    my ($class, $base) = @_;
58    $class->list($base);
59}
60
[1014]61=head2 list_from_rev($base, $rev)
62
63List objects create or modified after base revision C<$rev>.
64
65=cut
66
[27]67=head2 new($base, $id)
[3]68
[27]69Create a new object having $id as uid.
[3]70
71=cut
72
[28]73sub new {
74    my ($class, $base, $id, @args) = @_;
75    # So can be call as $class->SUPER::new()
76    bless {
77        _base => $base,
[389]78        _type => lc(($class =~ m/::([^:]*)$/)[0]),
79        _id => $id,
[28]80    }, $class;
81}
82
[42]83=head2 _create($class, $base, $id, %data)
84
85Must create a new object in database.
86
87Is called if underling base does not override create_object
88
89    sub _create(
90        my ($class, $base, $id, %data)
91    }
92
93=cut
94
[28]95=head2 type
96
97Return the type of the object
98
99=cut
100
[12]101sub type {
102    my ($self) = @_;
[71]103    if (ref $self) {
104        return $self->{_type}
105    } else {
106        return lc(($self =~ /::([^:]+)$/)[0]);
107    }
[12]108}
109
[3]110=head2 base
[2]111
[3]112Return the base handle for this object.
113
114=cut
115
116sub base {
117    return $_[0]->{_base}
118}
119
[81]120=head2 id
121
122Must return the unique identifier for this object
123
124=cut
125
126sub id {
127    my ($self) = @_;
128    $self->{_id}
129}
130
[1286]131=head2 Iid
132
133Return internal id if different from Id
134
135=cut
136
137sub Iid {
138    my ($self) = @_;
139    $self->id
140}
141
[1351]142=head2 stringify
143
144Display object as a string
145
146=cut
147
[1329]148sub stringify {
149    my ($self) = @_;
150
151    return $self->id
152}
153
[64]154=head2 list_canonical_fields($for)
[42]155
156Object shortcut to get the list of field supported by the object.
157
158=cut
159
160sub list_canonical_fields {
[64]161    my ($self, $for) = @_;
[861]162    $for ||= 'rw';
163    $self->_canonical_fields($for);
[42]164}
165
[1023]166=head2 attribute ($attribute)
167
168Return L<LATMOS::Accounts::Bases::Attributes> object for C<$attribute>
169
170=cut
171
[852]172sub attribute {
173    my ($self, $attribute) = @_;
[1002]174
175    my $attrinfo;
176    if (! ref $attribute) {
[1023]177        $attrinfo = $self->_get_attr_schema(
178            $self->base)->{$attribute}
179        or return;
[1002]180        $attrinfo->{name} = $attribute;
181    } else {
182        $attrinfo = $attribute;
183    }
184
[852]185    return LATMOS::Accounts::Bases::Attributes->new(
[1002]186        $attrinfo,
[852]187        $self,
188    );
189}   
190
[861]191sub _canonical_fields {
192    my ($class, $base, $for) = @_;
193    $for ||= 'rw';
194    my $info = $base->_get_attr_schema($class->type);
195    my @attrs = map { $base->attribute($class->type, $_) } keys %{$info || {}};
196    @attrs = grep { ! $_->ro } @attrs if($for =~ /w/);
[933]197    @attrs = grep { $_->readable } @attrs if($for =~ /r/);
[861]198    map { $_->name } grep { !$_->hidden }  @attrs;
[64]199}
200
[1992]201=head2 GetOtypeDef
202
203This function is called to provide sub object definition. Must be overwritten
204per object class when need.
205
206Must return a hashref listing each sub object type and their related key atribute:
207
208    return {
209        addresses => 'user',
210    }
211
212=cut
213
214sub GetOtypeDef {
215    my ($class) = @_;
216
217    return;
218}
219
[11]220=head2 get_field($field)
221
222Return the value for $field, must be provide by data base.
223
[42]224    sub get_field {
225        my ($self, $field)
226    }
227
[11]228=cut
229
[311]230=head2 get_c_field($cfield)
[11]231
[42]232Return the value for canonical field $cfield.
[11]233
[861]234Call driver specific get_field()
[42]235
[11]236=cut
237
238sub get_c_field {
239    my ($self, $cfield) = @_;
[805]240    $self->base->check_acl($self, $cfield, 'r') or do {
241        $self->base->log(LA_ERR, "Permission denied to get %s/%s",
242            $self->id, $cfield
243        );
244        return;
[331]245    };
[805]246    return $self->_get_c_field($cfield);
[317]247}
248
[375]249=head2 get_attributes($attr)
250
251Like get_c_field but always return an array
252
253=cut
254
255sub get_attributes {
256    my ($self, $cfield) = @_;
257    my $res = $self->get_c_field($cfield);
[1567]258    if ($res) {
259        return(ref $res ? @{$res} : $res);
260    } else {
261        return;
262    }
[375]263}
264
[468]265sub _get_attributes {
266    my ($self, $cfield) = @_;
267    my $res = $self->_get_c_field($cfield);
[1567]268    if ($res) {
269        return(ref $res ? @{$res} : ($res));
270    } else {
271        return;
272    }
[468]273}
274
[317]275sub _get_c_field {
276    my ($self, $cfield) = @_;
[861]277    my $attribute = $self->attribute($cfield) or do {
[805]278        $self->base->log(LA_WARN, "Unknow attribute $cfield");
279        return;
[331]280    };
[933]281    $attribute->readable or do {
282        $self->base->log(LA_WARN, "Attribute $cfield is not readable");
283        return;
284    };
[936]285    return $attribute->get; 
[11]286}
287
[1865]288=head2 GetAttributeValue($cfield)
289
290Return the value to exposed to other base
291
292=cut
293
294sub GetAttributeValue {
295    my ($self, $cfield) = @_;
296
297    return $self->get_c_field($cfield);
298}
299
300
[1023]301=head2 queryformat ($fmt)
302
303Return formated string according C<$fmt>
304
305=cut
306
[440]307sub queryformat {
308    my ($self, $fmt) = @_;
309    $fmt =~ s/\\n/\n/g;
[1536]310    $fmt =~ s/\\t/\t/g;
[440]311    $fmt =~ s!
[1364]312        (?:%\{([^:}]*)(?::([^}]+))?\})
[440]313        !
[441]314        my $val = $self->get_c_field($1);
[1832]315        sprintf('%' . ($2 || 's'), ref $val ? join(',', @$val) : (defined($val) ? $val : ''))
[440]316        !egx;
317    $fmt;
318}
319
[8]320=head2 set_fields(%data)
321
322Set values for this object. %data is a list or peer field => values.
323
[42]324    sub set_fields {
325        my ($self, %data) = @_;
326    }
327
[8]328=cut
329
[1500]330=head2 checkValues ($base, $obj, %attributes)
331
332Allow to pre-check values when object are modified or created
333
334C<$obj> is either the new id at object creation or the object itself on modification.
335
336=cut
337
338sub checkValues {
339    my ($class, $base, $obj, %attributes) = @_;
340
341    return 1;
342}
343
[1023]344=head2 check_allowed_values ($attr, $values)
345
346Check if value C<$values> is allowed for attributes C<$attr>
347
348=cut
349
[683]350sub check_allowed_values {
351    my ($self, $attr, $values) = @_;
352    $self->base->check_allowed_values($self->type, $attr, $values);
353}
354
[1023]355=head2 attr_allow_values ($attr)
356
357Return allowed for attribute C<$attr>
358
359=cut
360
[699]361sub attr_allow_values {
362    my ($self, $attr) = @_;
363    return $self->base->obj_attr_allowed_values(
364        $self->type,
365        $attr,
366    );
367}
368
[42]369=head2 set_c_fields(%data)
[8]370
371Set values for this object. %data is a list or peer
372canonical field => values. Fields names are translated.
373
374=cut
375
376sub set_c_fields {
377    my ($self, %cdata) = @_;
[805]378    foreach my $cfield (keys %cdata) {
379        $self->base->check_acl($self, $cfield, 'w') or do { 
380            $self->base->log(LA_ERR, "Cannot modified %s/%s: %s",
381                $self->type, $self->id, "permission denied");
382            return;
383        };
384    }
[683]385
[805]386    foreach my $cfield (keys %cdata) {
387        $self->check_allowed_values($cfield, $cdata{$cfield}) or do {
388            $self->base->log(LA_ERR, "Cannot modified %s/%s: %s",
389                $self->type, $self->id, "non authorized value");
390            return;
391        };
[683]392    }
[1500]393
[317]394    $self->_set_c_fields(%cdata);
395}
396
397sub _set_c_fields {
398    my ($self, %cdata) = @_;
399    my %data;
[936]400    my $res = 0;
[316]401    foreach my $cfield (keys %cdata) {
[861]402        my $attribute = $self->attribute($cfield) or do {
[354]403            $self->base->log(LA_ERR,
404                "Cannot set unsupported attribute %s to %s (%s)",
405                $cfield, $self->id, $self->type
406            );
407            return;
408        };
[861]409        $attribute->ro and do {
410            $self->base->log(LA_ERR,
411                "Cannot set read-only attribute %s to %s (%s)",
412                $cfield, $self->id, $self->type
413            );
414            return;
415        };
[1315]416
417        if (!$attribute->checkinput($cdata{$cfield})) {
[861]418            $self->base->log(LA_ERR,
[1315]419                "Value for attribute %s to %s (%s) does not match requirements",
420                $cfield, $self->id, $self->type
[861]421            );
[1315]422            return;
[861]423        };
[1286]424    }
[936]425
[1500]426    if (!$self->checkValues($self->base, $self, %cdata)) {
[1545]427        my $last = LATMOS::Accounts::Log::lastmessage(LA_ERR);
[1500]428        $self->base->log(LA_ERR,
[1545]429            "Cannot update %s (%s): wrong value%s",
430            $self->id, $self->type,
431            ($last ? ": $last" : $last)
[1500]432        );
433        return;
434    }
435
[1286]436    my %updated = ();
437    foreach my $cfield (keys %cdata) {
438        my $attribute = $self->attribute($cfield) or do {
439            $self->base->log(LA_ERR,
440                "Cannot set unsupported attribute %s to %s (%s)",
441                $cfield, $self->id, $self->type
442            );
443            return;
444        };
445        if ($attribute->set($cdata{$cfield})) {
[1297]446            $updated{$cfield} = $attribute->monitored;
[1286]447        }
[8]448    }
[1286]449   
450    if (keys %updated) {
[1595]451        $self->ReportChange('Update', 'Attributes %s have been updated', join(', ', sort keys %updated));
[1286]452        foreach (sort keys %updated) {
453            $self->ReportChange('Attributes', '%s set to %s', $_, 
454                (ref $cdata{$_}
[1840]455                    ? join(', ', sort @{ $cdata{$_} })
[1286]456                    : $cdata{$_}) || '(none)')
457                if ($updated{$_});
458        }
459    }
460    return scalar(keys %updated);
[8]461}
462
[1306]463=head2 addAttributeValue($attribute, $value)
464
465Add a value to a multivalue attributes
466
467=cut
468
469sub _addAttributeValue {
470    my ($self, $attribute, @values) = @_;
471
[1316]472    my @oldvalues = grep { $_ } $self->_get_attributes($attribute);
[1306]473    $self->_set_c_fields($attribute => [ @oldvalues, @values ]);
474}
475
476sub addAttributeValue {
477    my ($self, $attribute, @values) = @_;
478
[1316]479    my @oldvalues = grep { $_ } $self->_get_attributes($attribute);
[1306]480    $self->set_c_fields($attribute => [ @oldvalues, @values ]);
481}
482
483=head2 delAttributeValue($attribute, $value)
484
485Remove a value to a multivalue attributes
486
487=cut
488
489sub _delAttributeValue {
490    my ($self, $attribute, @values) = @_;
491
492    my @oldvalues = grep { $_ } $self->_get_attributes($attribute);
493
494    foreach my $value (@values) {
495        @oldvalues = grep { $_ ne $value } @oldvalues;
496    }
497
498    $self->_set_c_fields($attribute => @oldvalues ? [ @oldvalues, ] : undef );
499}
500
501sub delAttributeValue {
502    my ($self, $attribute, @values) = @_;
503
504    my @oldvalues = grep { $_ } $self->_get_attributes($attribute);
505
506    foreach my $value (@values) {
507        @oldvalues = grep { $_ ne $value } @oldvalues;
508    }
509
510    $self->set_c_fields($attribute => @oldvalues ? [ @oldvalues, ] : undef );
511}
512
[55]513=head2 set_password($password)
514
515Set the password into the database, $password is the clear version
516of the password.
517
518This function store it into userPassword canonical field if supported
519using crypt unix and md5 algorythm (crypt md5), the salt is 8 random
520caracters.
521
522The base driver should override it if another encryption is need.
523
524=cut
525
526sub set_password {
527    my ($self, $clear_pass) = @_;
[504]528    if ($self->base->check_acl($self, 'userPassword', 'w')) {
[1286]529        if ($self->_set_password($clear_pass)) {
530             $self->ReportChange('Password', 'user password has changed');
531             return 1;
532        } else {
533            return;
534        }
[504]535    } else {
[584]536        $self->base->log(LA_ERROR, "Permission denied for %s to change its password",
[504]537            $self->id);
538        return;
539    }
540}
541
542sub _set_password {
543    my ($self, $clear_pass) = @_;
[861]544    if (my $attribute = $self->base->attribute($self->type, 'userPassword')) {
[2041]545        my $res = $self->set_fields($attribute->iname, $self->base->passCrypt($clear_pass));
[861]546        $self->base->log(LA_NOTICE, 'Mot de passe changé pour %s', $self->id)
547            if($res);
[774]548        return $res;
[298]549    } else {
[1386]550        $self->base->log(LA_WARN,
[298]551            "Cannot set password: userPassword attributes is unsupported");
[55]552    }
553}
554
[1023]555=head2 check_password ($password)
556
557Check given password is secure using L<Crypt::Cracklib>
558
559=cut
560
[584]561sub check_password {
562    my ( $self, $password ) = @_;
563    my $dictionary;
564
[1278]565    if ($password !~ /^[[:ascii:]]*$/) {
566       return "the password must contains ascii characters only";
567    }
568
[584]569    return fascist_check($password, $dictionary);
570}
571
[1935]572=head2 InjectCryptPasswd($cryptpasswd)
573
574Inject a password encrypted using standard UNIX method.
575
576=cut
577
578sub InjectCryptPasswd {
579    my ($self, $cryptpasswd) = @_;
580
581    if ($self->can('_InjectCryptPasswd')) {
582        return $self->_InjectCryptPasswd($cryptpasswd);
583    } else {
[2005]584        $self->base->log(LA_ERR, 'Injecting unix crypt password is not supported');
[1935]585        return;
586    }
587}
588
[1023]589=head2 search ($base, @filter)
590
591Search object matching C<@filter>
592
593=cut
594
[122]595sub search {
[256]596    my ($class, $base, @filter) = @_;
[122]597    my @results;
[719]598    my %parsed_filter;
[256]599    while (my $item = shift(@filter)) {
600        # attr=foo => no extra white space !
601        # \W is false, it is possible to have two char
602        my ($attr, $mode, $val) = $item =~ /^(\w+)(?:(\W)(.+))?$/ or next;
603        if (!$mode) {
604            $mode = '~';
605            $val = shift(@filter);
606        }
607        push(
[719]608            @{$parsed_filter{$attr}},
[256]609            {
610                attr => $attr,
611                mode => $mode,
612                val  => $val,
613            }
614        );
615    }
[122]616    foreach my $id ($base->list_objects($class->type)) {
617        my $obj = $base->get_object($class->type, $id);
618        my $match = 1;
[719]619        foreach my $field (keys %parsed_filter) {
[861]620            $base->attribute($class->type, $field) or
[1698]621                la_log(LA_WARN, "Unsupported attribute %s", $field);
[719]622            my $tmatch = 0;
623            foreach (@{$parsed_filter{$field}}) {
624                my $value = $_->{val};
625                my $fval = $obj->_get_c_field($field) || '';
626                if ($value eq '*') {
627                    if ($fval ne '') {
628                        $tmatch = 1;
629                        last;
630                    }
631                } elsif ($value eq '!') {
632                    if ($fval eq '') {
633                        $match = 1;
634                        last;
635                    }
636                } elsif ($_->{mode} eq '=') {
637                    if ($fval eq $value) {
638                        $tmatch = 1;
639                        last;
640                    }
641                } elsif($_->{mode} eq '~') {
642                    if ($fval =~ m/\Q$value\E/i) {
643                        $tmatch = 1;
644                        last;
645                    }
[164]646                }
[122]647            }
[719]648            $match = 0 unless($tmatch);
[122]649        }
650        push(@results, $id) if($match);
651    }
652    @results;
653}
654
[1023]655=head2 attributes_summary ($base, $attribute)
[257]656
[1023]657Return list of values existing in base for C<$attribute>
658
659=cut
660
[257]661sub attributes_summary {
662    my ($class, $base, $attribute) = @_;
[1569]663    my $attr = $base->attribute($class->type, $attribute) or do {
664        $base->log(LA_ERR, "Cannot instantiate %s attribute", $attribute);
665        return;
666    };
667    if (!$attr->readable) {
668        $base->log(LA_WARN, l('Attribute %s is not readable', $attribute));
669        return;
670    }
671    if (!$base->check_acl($class->type, $attribute, 'r')) {
672        $base->log(LA_WARN, l('Permission denied to read attribute %s', $attribute));
673        return;
674    }
[257]675    my %values;
676    foreach my $id ($base->list_objects($class->type)) {
677        my $obj = $base->get_object($class->type, $id);
[317]678        my $value = $obj->_get_c_field($attribute);
[257]679        if ($value) {
680            if (ref $value) {
681                foreach (@$value) {
682                    $values{$_} = 1;
683                }
684            } else {
685                $values{$value} = 1;
686            }
687        }
688    }
689    return sort(keys %values);
690}
691
[1453]692=head2 attributes_summary_by_object ($base, $attribute)
693
694Return list of peer object <=> values
695
696=cut
697
698sub attributes_summary_by_object {
699    my ($class, $base, $attribute) = @_;
[1569]700    my $attr = $base->attribute($class->type, $attribute) or do {
701        $base->log(LA_ERR, "Cannot instantiate %s attribute", $attribute);
702        return;
703    };
704    if (!$attr->readable) {
705        $base->log(LA_WARN, l('Attribute %s is not readable', $attribute));
706        return;
707    }
708    if (!$base->check_acl($class->type, $attribute, 'r')) {
709        $base->log(LA_WARN, l('Permission denied to read attribute %s', $attribute));
710        return;
711    }
[1453]712    my %values;
713    foreach my $id ($base->list_objects($class->type)) {
714        my $obj = $base->get_object($class->type, $id);
715        my $value = $obj->_get_c_field($attribute);
716        if ($value) {
717            if (ref $value) {
718                foreach (@$value) {
719                    push(@{ $values{ $id } }, $_);
720                }
721            } else {
722                push(@{ $values{ $id } }, $value);
723            }
724        }
725    }
726    return %values;
727}
728
[1023]729=head2 find_next_numeric_id ($base, $field, $min, $max)
730
731Find next free uniq id for attribute C<$field>
732
733=cut
734
[137]735sub find_next_numeric_id {
736    my ($class, $base, $field, $min, $max) = @_;
[861]737    $base->attribute($class->type, $field) or return;
[137]738    $min ||= 
739        $field eq 'uidNumber' ? 500 :
740        $field eq 'gidNumber' ? 500 :
741        1;
742    $max ||= 65635;
[298]743    $base->log(LA_DEBUG, "Trying to find %s in range %d - %d",
744        $field, $min, $max);
[137]745    my %existsid;
[1182]746    $base->temp_switch_unexported(sub {
747        foreach ($base->list_objects($class->type)) {
748            my $obj = $base->get_object($class->type, $_) or next;
749            my $id = $obj->_get_c_field($field) or next;
750            $existsid{$id + 0} = 1;
751        }
752    }, 1);
[628]753    $min += 0;
754    $max += 0;
[137]755    for(my $i = $min; $i <= $max; $i++) {
[628]756        $existsid{$i + 0} or do {
757            $base->log(LA_DEBUG, "Next %s found: %d", $field, $i);
758            return $i;
759        };
[137]760    }
761    return;
762}
763
[1071]764=head2 text_dump ($handle, $config, $base)
[1023]765
766Dump object into C<$handle>
767
768=cut
769
[333]770sub text_dump {
[1071]771    my ($self, $handle, $config, $base) = @_;
772    print $handle $self->dump($config, $base);
[675]773    return 1;
774}
[333]775
[1023]776=head2 dump
777
778Return dump for tihs object
779
780=cut
781
[675]782sub dump {
[1071]783    my ($self, $config, $base) = @_;
[675]784
[333]785    my $otype = $self->type;
786    $base ||= $self->base;
[675]787    my $dump;
[405]788    if (ref $self) {
[675]789        $dump .= sprintf "# base %s: object %s/%s\n",
[405]790            $base->label, $self->type, $self->id;
791    }
[675]792    $dump .= sprintf "# %s\n", scalar(localtime);
[333]793
794    foreach my $attr (sort { $a cmp $b } $base->list_canonical_fields($otype,
[1071]795        $config->{only_rw} ? 'rw' : 'r')) {
[1134]796        my $oattr = ref $self ? $self->attribute($attr) : $base->attribute($otype, $attr);
[1565]797        if ($oattr->hidden) { next; }
[333]798        if (ref $self) {
799            my $val = $self->get_c_field($attr);
[1071]800            if ($val || $config->{empty_attr}) {
[704]801                if (my @allowed = $base->obj_attr_allowed_values($otype, $attr)) {
[861]802                    $dump .= sprintf("# %s must be%s: %s\n",
[704]803                        $attr,
[861]804                        ($oattr->mandatory ? '' : ' empty or either'),
[704]805                        join(', ', @allowed)
806                    );
807                }
808                my @vals = ref $val ? @{ $val } : $val;
[333]809                foreach (@vals) {
[404]810                    $_ ||= '';
[400]811                    s/\r?\n/\\n/g;
[675]812                    $dump .= sprintf("%s%s:%s\n", 
[861]813                        $oattr->ro ? '# (ro) ' : '',
[598]814                        $attr, $_ ? " $_" : '');
[333]815                }
816            }
[339]817        } else {
[704]818            if (my @allowed = $base->obj_attr_allowed_values($otype, $attr)) {
819                $dump .= sprintf("# %s must be empty or either: %s\n",
820                    $attr,
821                    join(', ', @allowed)
822                );
823            }
[675]824            $dump .= sprintf("%s%s: %s\n", 
[861]825                $oattr->ro ? '# (ro) ' : '',
[404]826                $attr, '');
[333]827        }
828    }
[675]829    return $dump;
[333]830}
831
[1286]832=head2 ReportChange($changetype, $message, @args)
833
834Possible per database way to log changes
835
836=cut
837
838sub ReportChange {
839    my ($self, $changetype, $message, @args) = @_;
840
841    $self->base->ReportChange(
842        $self->type,
843        $self->id,
844        $self->Iid,
845        $changetype, $message, @args
846    )
847}
848
[2]8491;
850
851__END__
852
[6]853
[2]854=head1 SEE ALSO
855
[1023]856L<LATMOS::Accounts::Bases>
[2]857
858=head1 AUTHOR
859
[3]860Thauvin Olivier, E<lt>olivier.thauvin.ipsl.fr@localdomainE<gt>
[2]861
862=head1 COPYRIGHT AND LICENSE
863
864Copyright (C) 2009 by Thauvin Olivier
865
866This library is free software; you can redistribute it and/or modify
867it under the same terms as Perl itself, either Perl version 5.10.0 or,
868at your option, any later version of Perl 5 you may have available.
869
870=cut
Note: See TracBrowser for help on using the repository browser.