List.pm 204 KB
Newer Older
1
2
3
4
# -*- indent-tabs-mode: nil; -*-
# vim:ft=perl:et:sw=4
# $Id$

5
# Sympa - SYsteme de Multi-Postage Automatique
6
7
8
9
#
# Copyright (c) 1997, 1998, 1999 Institut Pasteur & Christophe Wolfhugel
# Copyright (c) 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005,
# 2006, 2007, 2008, 2009, 2010, 2011 Comite Reseau des Universites
10
# Copyright (c) 2011, 2012, 2013, 2014, 2015, 2016, 2017 GIP RENATER
11
12
# Copyright 2017, 2018, 2019, 2020, 2021 The Sympa Community. See the
# AUTHORS.md file at the top-level directory of this distribution and at
13
# <https://github.com/sympa-community/sympa.git>.
14
15
16
17
18
19
20
21
22
23
24
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
25
26
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
root's avatar
root committed
27

28
package Sympa::List;
root's avatar
root committed
29
30

use strict;
31
use warnings;
32
use Digest::MD5 qw();
33
use English qw(-no_match_vars);
34
use IO::Scalar;
sikeda's avatar
sikeda committed
35
use POSIX qw();
36
use Storable qw();
37

38
use Sympa;
39
use Conf;
40
use Sympa::ConfDef;
41
use Sympa::Constants;
42
use Sympa::Database;
43
use Sympa::DatabaseDescription;
44
use Sympa::DatabaseManager;
45
use Sympa::Family;
46
use Sympa::Language;
47
use Sympa::List::Config;
48
use Sympa::ListDef;
49
use Sympa::LockedFile;
50
use Sympa::Log;
51
use Sympa::Regexps;
52
use Sympa::Robot;
IKEDA Soji's avatar
IKEDA Soji committed
53
use Sympa::Spindle::ProcessRequest;
54
use Sympa::Spindle::ProcessTemplate;
55
use Sympa::Spool::Auth;
56
use Sympa::Template;
57
use Sympa::Tools::Data;
58
use Sympa::Tools::Domains;
59
use Sympa::Tools::File;
60
use Sympa::Tools::SMIME;
61
use Sympa::Tools::Text;
62
use Sympa::User;
63

64
my @sources_providing_listmembers = qw/
65
66
67
68
69
70
    include_file
    include_ldap_2level_query
    include_ldap_query
    include_remote_file
    include_remote_sympa_list
    include_sql_query
71
    include_sympa_list
72
73
    /;

74
75
# No longer used.
#my @more_data_sources;
76
77

# All non-pluggable sources are in the admin user file
78
# NO LONGER USED.
79
80
my %config_in_admin_user_file = map +($_ => 1),
    @sources_providing_listmembers;
81

82
my $language = Sympa::Language->instance;
83
my $log      = Sympa::Log->instance;
84

root's avatar
root committed
85
## Database and SQL statement handlers
86
my ($sth, @sth_stack);
87

sikeda's avatar
sikeda committed
88
89
90
91
92
93
94
95
96
97
98
99
# DB fields with numeric type.
# We should not do quote() for these while inserting data.
my %db_struct = Sympa::DatabaseDescription::full_db_struct();
my %numeric_field;
foreach my $t (qw(subscriber_table admin_table)) {
    foreach my $k (keys %{$db_struct{$t}->{fields}}) {
        if ($db_struct{$t}->{fields}{$k}{struct} =~ /\A(tiny|small|big)?int/)
        {
            $numeric_field{$k} = 1;
        }
    }
}
root's avatar
root committed
100

101
# This is the generic hash which keeps all lists in memory.
Luc Didry's avatar
Luc Didry committed
102
my %list_of_lists = ();
salaun's avatar
salaun committed
103

104
105
my %all_edit_list = ();

root's avatar
root committed
106
107
## Creates an object.
sub new {
108
109
    my ($pkg, $name, $robot, $options) = @_;
    my $list = {};
110
    $log->syslog('debug3', '(%s, %s, %s)', $name, $robot,
111
        join('/', keys %$options));
112

IKEDA Soji's avatar
IKEDA Soji committed
113
114
115
116
117
    # Lowercase list name.
    $name = lc $name;
    # In case the variable was multiple. FIXME:required?
    $name = $1 if $name =~ /^(\S+)\0/;

118
119
    ## Allow robot in the name
    if ($name =~ /\@/) {
120
121
122
        my @parts = split /\@/, $name;
        $robot ||= $parts[1];
        $name = $parts[0];
123
    }
root's avatar
root committed
124

125
126
127
128
    # Look for the list if no robot was provided.
    if (not $robot or $robot eq '*') {
        #FIXME: Default robot would be used instead of oppotunistic search.
        $robot = search_list_among_robots($name);
129
130
    } else {
        $robot = lc $robot;    #FIXME: More canonicalization.
131
    }
132

133
    unless ($robot) {
134
        $log->syslog('err',
135
136
137
138
            'Missing robot parameter, cannot create list object for %s',
            $name)
            unless ($options->{'just_try'});
        return undef;
139
140
    }

141
142
    $options = {} unless (defined $options);

root's avatar
root committed
143
    ## Only process the list if the name is valid.
144
    #FIXME: Existing lists may be checked with looser rule.
145
    my $listname_regexp = Sympa::Regexps::listname();
146
    unless ($name and ($name =~ /^($listname_regexp)$/io)) {
147
        $log->syslog('err', 'Incorrect listname "%s"', $name)
148
149
            unless ($options->{'just_try'});
        return undef;
root's avatar
root committed
150
151
    }
    ## Lowercase the list name.
152
    $name = $1;
root's avatar
root committed
153
    $name =~ tr/A-Z/a-z/;
154

155
    ## Reject listnames with reserved list suffixes
156
157
158
    my $regx = Conf::get_robot_conf($robot, 'list_check_regexp');
    if ($regx) {
        if ($name =~ /^(\S+)-($regx)$/) {
159
            $log->syslog(
160
161
162
163
164
165
                'err',
                'Incorrect name: listname "%s" matches one of service aliases',
                $name
            ) unless ($options->{'just_try'});
            return undef;
        }
166
167
    }

168
    my $status;
169
    ## If list already in memory and not previously purged by another process
170
171
172
173
174
175
176
177
178
179
180
181
182
    if ($list_of_lists{$robot}{$name}
        and -d $list_of_lists{$robot}{$name}{'dir'}) {
        # use the current list in memory and update it
        $list = $list_of_lists{$robot}{$name};

        $status = $list->load($name, $robot, $options);
    } else {
        # create a new object list
        bless $list, $pkg;

        $options->{'first_access'} = 1;
        $status = $list->load($name, $robot, $options);
    }
183
    unless (defined $status) {
184
        return undef;
185
186
    }

187
    $list->_load_edit_list_conf;
188

189
190
191
    return $list;
}

192
193
194
## When no robot is specified, look for a list among robots
sub search_list_among_robots {
    my $listname = shift;
195

196
    unless ($listname) {
197
        $log->syslog('err', 'Missing list parameter');
198
        return undef;
199
    }
200

201
    ## Search in default robot
202
203
    if (-d $Conf::Conf{'home'} . '/' . $listname) {
        return $Conf::Conf{'domain'};
204
    }
205
206
207
208
209
210
211
212

    foreach my $r (keys %{$Conf::Conf{'robots'}}) {
        if (-d $Conf::Conf{'home'} . '/' . $r . '/' . $listname) {
            return $r;
        }
    }

    return 0;
213
214
}

215
216
## set the list in status error_config and send a notify to listmaster
sub set_status_error_config {
217
    $log->syslog('debug2', '(%s, %s, ...)', @_);
218
    my ($self, $msg, @param) = @_;
219

220
221
    unless ($self->{'admin'}
        and $self->{'admin'}{'status'} eq 'error_config') {
222
223
        $self->{'admin'}{'status'} = 'error_config';

224
225
226
        # No more save config in error...
        # $self->save_config(tools::get_address($self->{'domain'},
        #     'listmaster'));
227
        $log->syslog('err',
228
229
            'The list %s is set in status error_config: %s(%s)',
            $self, $msg, join(', ', @param));
230
        Sympa::send_notify_to_listmaster($self, $msg,
231
            [$self->{'name'}, @param]);
232
233
234
    }
}

235
236
237
238
# Destroy multiton instance. FIXME
sub destroy_multiton {
    my $self = shift;
    delete $list_of_lists{$self->{'domain'}}{$self->{'name'}};
root's avatar
root committed
239
240
}

241
242
243
244
## set the list in status family_closed and send a notify to owners
# Deprecated.  Use Sympa::Request::Handler::close_list handler.
#sub set_status_family_closed;

245
246
247
# Saves the statistics data to disk.
# Deprecated. Use Sympa::List::update_stats().
#sub savestats;
root's avatar
root committed
248

249
## msg count.
250
251
# Old name: increment_msg_count().
sub _increment_msg_count {
252
    $log->syslog('debug2', '(%s)', @_);
253
    my $self = shift;
254

255
    # Be sure the list has been loaded.
256
    my $file = $self->{'dir'} . '/msg_count';
257
258

    my %count;
259
260
    if (open my $ifh, '<', $file) {
        while (<$ifh>) {
261
262
263
264
            if ($_ =~ /^(\d+)\s(\d+)$/) {
                $count{$1} = $2;
            }
        }
265
        close $ifh;
266
267
268
    }
    my $today = int(time / 86400);
    if ($count{$today}) {
269
270
271
        $count{$today}++;
    } else {
        $count{$today} = 1;
272
    }
273

274
275
    my $ofh;
    unless (open $ofh, '>', "$file.$PID") {
276
        $log->syslog('err', 'Unable to create "%s.%s": %m', $file, $PID);
277
        return undef;
278
    }
279
    foreach my $key (sort { $a <=> $b } keys %count) {
280
        printf $ofh "%d\t%d\n", $key, $count{$key};
281
    }
282
    close $ofh;
283

284
    unless (rename("$file.$PID", $file)) {
285
        $log->syslog('err', 'Unable to write "%s": %m', $file);
286
        return undef;
287
288
289
290
    }
    return 1;
}

291
292
# Returns the number of messages sent to the list
sub get_msg_count {
293
    $log->syslog('debug2', '(%s)', @_);
294
295
    my $self = shift;

296
    # Be sure the list has been loaded.
297
    my $file = $self->{'dir'} . '/stats';
298
299

    my $count = 0;
300
301
    if (open my $ifh, '<', $file) {
        while (<$ifh>) {
302
303
304
305
            if ($_ =~ /^(\d+)\s+(.*)$/) {
                $count = $1;
            }
        }
306
        close $ifh;
307
308
309
310
    }

    return $count;
}
311
312
## last date of distribution message .
sub get_latest_distribution_date {
313
    $log->syslog('debug2', '(%s)', @_);
314
    my $self = shift;
315

316
    # Be sure the list has been loaded.
317
    my $file = $self->{'dir'} . '/msg_count';
318
319

    my $latest_date = 0;
320
321
    my $ifh;
    unless (open $ifh, '<', $file) {
322
        $log->syslog('debug2', 'Unable to open %s', $file);
323
        return undef;
salaun's avatar
salaun committed
324
    }
325

326
    while (<$ifh>) {
327
328
329
        if ($_ =~ /^(\d+)\s(\d+)$/) {
            $latest_date = $1 if ($1 > $latest_date);
        }
330
    }
331
    close $ifh;
332

333
334
    return undef if ($latest_date == 0);
    return $latest_date;
335
336
}

337
## Update the stats struct
root's avatar
root committed
338
339
## Input  : num of bytes of msg
## Output : num of msgs sent
340
# Old name: List::update_stats().
341
342
# No longer used. Use Sympa::List::update_stats(1);
#sub get_next_sequence;
343

344
sub get_stats {
Luc Didry's avatar
Luc Didry committed
345
    my $self = shift;
346

347
348
349
350
351
352
    my @stats;
    my $lock_fh = Sympa::LockedFile->new($self->{'dir'} . '/stats', 2, '<');
    if ($lock_fh) {
        @stats = split /\s+/, do { my $line = <$lock_fh>; $line };
        $lock_fh->close;
    }
353

354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
    foreach my $i ((0 .. 3)) {
        $stats[$i] = 0 unless $stats[$i];
    }
    return @stats[0 .. 3];
}

sub update_stats {
    $log->syslog('debug2', '(%s, %s, %s, %s, %s)', @_);
    my $self  = shift;
    my @diffs = @_;

    my $lock_fh = Sympa::LockedFile->new($self->{'dir'} . '/stats', 2, '+>>');
    unless ($lock_fh) {
        $log->syslog('err', 'Could not create new lock');
        return;
    }

    # Update stats file.
    # Note: The last three fields total, last_sync and last_sync_admin_user
    # were deprecated.
    seek $lock_fh, 0, 0;
    my @stats = split /\s+/, do { my $line = <$lock_fh>; $line };
    foreach my $i ((0 .. 3)) {
        $stats[$i] ||= 0;
        $stats[$i] += $diffs[$i] if $diffs[$i];
    }
    seek $lock_fh, 0, 0;
    truncate $lock_fh, 0;
    printf $lock_fh "%d %.0f %.0f %.0f\n", @stats;

    return unless $lock_fh->close;

    if ($diffs[0]) {
        $self->_increment_msg_count;
    }

    return @stats;
root's avatar
root committed
391
392
}

393
394
395
396
sub _cache_publish_expiry {
    my $self = shift;
    my $type = shift;

397
    my $stat_file;
398
    if ($type eq 'member') {
399
        $stat_file = $self->{'dir'} . '/.last_change.member';
400
401
    } elsif ($type eq 'admin_user') {
        $stat_file = $self->{'dir'} . '/.last_change.admin';
402
403
    } else {
        die 'bug in logic. Ask developer';
404
    }
405
406
407
408

    # Touch status file.
    my $fh;
    open $fh, '>', $stat_file and close $fh;
409
    utime undef, undef, $stat_file;    # required for such as NFS.
410
411
412
413
414
415
416
}

sub _cache_read_expiry {
    my $self = shift;
    my $type = shift;

    if ($type eq 'member') {
417
418
419
420
        # If changes have never been done, just now is assumed.
        my $stat_file = $self->{'dir'} . '/.last_change.member';
        $self->_cache_publish_expiry('member') unless -e $stat_file;
        return [stat $stat_file]->[9];
421
422
423
424
425
    } elsif ($type eq 'admin_user') {
        # If changes have never been done, just now is assumed.
        my $stat_file = $self->{'dir'} . '/.last_change.admin';
        $self->_cache_publish_expiry('admin_user') unless -e $stat_file;
        return [stat $stat_file]->[9];
426
427
    } else {
        die 'bug in logic. Ask developer';
428
429
430
431
    }
}

sub _cache_get {
Luc Didry's avatar
Luc Didry committed
432
433
    my $self = shift;
    my $type = shift;
434
435
436

    my $lasttime = $self->{_mtime}{$type};
    my $mtime;
437
    if ($type eq 'total' or $type eq 'is_list_member') {
438
439
        $mtime = $self->_cache_read_expiry('member');
    } else {
440
        $mtime = $self->_cache_read_expiry($type);
441
442
443
    }
    $self->{_mtime}{$type} = $mtime;

444
    return undef unless defined $lasttime and defined $mtime;
445
    return undef if $lasttime <= $mtime;
446
    return $self->{_cached}{$type};
447
448
449
450
451
452
453
454
455
456
}

sub _cache_put {
    my $self  = shift;
    my $type  = shift;
    my $value = shift;

    return $self->{_cached}{$type} = $value;
}

457
# Old name: List::extract_verp_rcpt().
458
459
# Moved to: Sympa::Spindle::DistributeMessage::_extract_verp_rcpt().
#sub _extract_verp_rcpt;
460

461
462
# Dumps a copy of list users to disk, in text format.
# Old name: Sympa::List::dump() which dumped only members.
463
sub dump_users {
464
    $log->syslog('debug2', '(%s, %s)', @_);
465
    my $self = shift;
466
    my $role = shift;
467

468
    die 'bug in logic. Ask developer'
Luc Didry's avatar
Luc Didry committed
469
        unless grep { $role eq $_ } qw(member owner editor);
470

471
    my $file = $self->{'dir'} . '/' . $role . '.dump';
472

473
474
475
476
    unlink $file . '.old' if -e $file . '.old';
    rename $file, $file . '.old' if -e $file;
    my $lock_fh = Sympa::LockedFile->new($file, 5, '>');
    unless ($lock_fh) {
Luc Didry's avatar
Luc Didry committed
477
478
479
480
        $log->syslog(
            'err', 'Failed to save file %s.new: %s',
            $file, Sympa::LockedFile->last_error
        );
481
        return undef;
482
    }
483

484
    if ($role eq 'member') {
485
        my %map_field = _map_list_member_cols();
486

487
488
489
490
491
        my $user;
        for (
            $user = $self->get_first_list_member();
            $user;
            $user = $self->get_next_list_member()
Luc Didry's avatar
Luc Didry committed
492
        ) {
493
            foreach my $k (sort keys %map_field) {
494
495
496
497
498
499
500
501
502
                if ($k eq 'custom_attribute') {
                    next unless ref $user->{$k} eq 'HASH' and %{$user->{$k}};
                    my $encoded = Sympa::Tools::Data::encode_custom_attribute(
                        $user->{$k});
                    printf $lock_fh "%s %s\n", $k, $encoded;
                } else {
                    next unless defined $user->{$k} and length $user->{$k};
                    printf $lock_fh "%s %s\n", $k, $user->{$k};
                }
503
            }
504

505
            # Compat.<=6.2.44
506
507
508
509
            # This is needed for earlier version of Sympa on e.g. remote host.
            print $lock_fh "included 1\n"
                if defined $user->{inclusion};

510
511
512
            print $lock_fh "\n";
        }
    } else {
IKEDA Soji's avatar
IKEDA Soji committed
513
514
        my %map_field = _map_list_admin_cols();

515
        foreach my $user (@{$self->get_current_admins || []}) {
516
            next unless $user->{role} eq $role;
IKEDA Soji's avatar
IKEDA Soji committed
517
            foreach my $k (sort keys %map_field) {
518
519
520
                printf $lock_fh "%s %s\n", $k, $user->{$k}
                    if defined $user->{$k} and length $user->{$k};
            }
521

522
            # Compat.<=6.2.44
523
524
525
526
            # This is needed for earlier version of Sympa on e.g. remote host.
            print $lock_fh "included 1\n"
                if defined $user->{inclusion};

527
528
            print $lock_fh "\n";
        }
root's avatar
root committed
529
    }
530

531
532
    $lock_fh->close;

533
534
535
    # FIXME:Are these lines required?
    $self->{'_mtime'}{'config'} =
        Sympa::Tools::File::get_mtime($self->{'dir'} . '/config');
536
537

    return 1;
root's avatar
root committed
538
539
540
541
542
}

## Saves the configuration file to disk
sub save_config {
    my ($self, $email) = @_;
543
    $log->syslog('debug3', '(%s, %s)', $self->{'name'}, $email);
root's avatar
root committed
544

545
546
    return undef
        unless ($self);
547

548
    my $config_file_name = $self->{'dir'} . '/config';
549
550

    ## Lock file
551
552
    my $lock_fh = Sympa::LockedFile->new($config_file_name, 5, '+<');
    unless ($lock_fh) {
553
        $log->syslog('err', 'Could not create new lock');
554
        return undef;
555
556
    }

557
558
    my $name                 = $self->{'name'};
    my $old_serial           = $self->{'admin'}{'serial'};
559
    my $old_config_file_name = $config_file_name . '.' . $old_serial;
root's avatar
root committed
560
561
562

    ## Update management info
    $self->{'admin'}{'serial'}++;
563
    $self->{'admin'}{'update'} = {
564
565
        'email'      => $email,
        'date_epoch' => time,
566
    };
567

568
569
570
571
    unless (
        $self->_save_list_config_file(
            $config_file_name, $old_config_file_name
        )
Luc Didry's avatar
Luc Didry committed
572
    ) {
573
        $log->syslog('info', 'Unable to save config file %s',
574
575
576
            $config_file_name);
        $lock_fh->close();
        return undef;
root's avatar
root committed
577
    }
578

579
    ## Also update the binary version of the data structure
580
581
582
    if (Conf::get_robot_conf($self->{'domain'}, 'cache_list_config') eq
        'binary_file') {
        eval {
583
            Storable::store($self->{'admin'}, $self->{'dir'} . '/config.bin');
584
        };
585
586
587
        if ($EVAL_ERROR) {
            $log->syslog(
                'err',
588
                'Failed to save the binary config %s. error: %s',
589
590
                $self->{'dir'} . '/config.bin', $EVAL_ERROR
            );
591
        }
592
593
    }

594
    ## Release the lock
595
    unless ($lock_fh->close()) {
596
        return undef;
597
598
    }

599
    unless ($self->_update_list_db) {
600
        $log->syslog('err', "Unable to update list_table");
601
602
    }

root's avatar
root committed
603
604
605
606
607
    return 1;
}

## Loads the administrative data for a list
sub load {
608
    $log->syslog('debug3', '(%s, %s, %s, ...)', @_);
609
610
611
612
613
614
    my $self    = shift;
    my $name    = shift;
    my $robot   = shift;
    my $options = shift;

    die 'bug in logic. Ask developer' unless $robot;
615

616
617
    ## Set of initializations ; only performed when the config is first loaded
    if ($options->{'first_access'}) {
618
619
620
621
622
623
624
        # Create parent of list directory if not exist yet e.g. when list to
        # be created manually.
        # Note: For compatibility, directory with primary domain is omitted.
        if (    $robot
            and $robot ne $Conf::Conf{'domain'}
            and not -d "$Conf::Conf{'home'}/$robot") {
            mkdir "$Conf::Conf{'home'}/$robot", 0775;
625
        }
626

627
628
629
630
631
        if ($robot && (-d "$Conf::Conf{'home'}/$robot")) {
            $self->{'dir'} = "$Conf::Conf{'home'}/$robot/$name";
        } elsif (lc($robot) eq lc($Conf::Conf{'domain'})) {
            $self->{'dir'} = "$Conf::Conf{'home'}/$name";
        } else {
632
            $log->syslog('err', 'No such robot (virtual domain) %s', $robot)
633
634
635
636
637
638
                unless ($options->{'just_try'});
            return undef;
        }

        $self->{'domain'} = $robot;

639
640
        # default list host is robot domain: Deprecated.
        #XXX$self->{'admin'}{'host'} ||= $self->{'domain'};
641
        $self->{'name'} = $name;
642
    }
643

644
    unless (-d $self->{'dir'} and -f ($self->{'dir'} . '/config')) {
645
        $log->syslog('debug2', 'Missing directory (%s) or config file for %s',
646
647
648
            $self->{'dir'}, $name)
            unless ($options->{'just_try'});
        return undef;
649
    }
salaun's avatar
salaun committed
650

651
652
    # Last modification of list config ($last_time_config) on memory cache.
    # Note: "subscribers" file was deprecated. No need to load "stats" file.
653
654
    my $last_time_config = $self->{'_mtime'}{'config'};
    $last_time_config = POSIX::INT_MIN() unless defined $last_time_config;
655

656
657
    my $time_config =
        Sympa::Tools::File::get_mtime($self->{'dir'} . '/config');
658
    my $time_config_bin =
659
        Sympa::Tools::File::get_mtime($self->{'dir'} . '/config.bin');
660
661
662
    my $main_config_time =
        Sympa::Tools::File::get_mtime(Sympa::Constants::CONFIG);
    # my $web_config_time  = Sympa::Tools::File::get_mtime(Sympa::Constants::WWSCONFIG);
663
    my $config_reloaded = 0;
root's avatar
root committed
664
    my $admin;
665
666

    if (Conf::get_robot_conf($self->{'domain'}, 'cache_list_config') eq
667
668
            'binary_file'
        and !$options->{'reload_config'}
669
        and $time_config_bin > $last_time_config
670
671
        and $time_config_bin >= $time_config
        and $time_config_bin >= $main_config_time) {
672
673
674
675
        ## Get a shared lock on config file first
        my $lock_fh =
            Sympa::LockedFile->new($self->{'dir'} . '/config', 5, '<');
        unless ($lock_fh) {
676
            $log->syslog('err', 'Could not create new lock');
677
678
679
680
681
            return undef;
        }

        ## Load a binary version of the data structure
        ## unless config is more recent than config.bin
682
683
684
685
        eval { $admin = Storable::retrieve($self->{'dir'} . '/config.bin') };
        if ($EVAL_ERROR) {
            $log->syslog(
                'err',
686
                'Failed to load the binary config %s, error: %s',
687
688
                $self->{'dir'} . '/config.bin', $EVAL_ERROR
            );
689
690
691
692
            $lock_fh->close();
            return undef;
        }

693
694
        $config_reloaded  = 1;
        $last_time_config = $time_config_bin;
695
        $lock_fh->close();
696
    } elsif ($self->{'name'} ne $name
697
        or $time_config > $last_time_config
698
        or $options->{'reload_config'}) {
699
        $admin = $self->_load_list_config_file;
700
701
702
703
704

        ## Get a shared lock on config file first
        my $lock_fh =
            Sympa::LockedFile->new($self->{'dir'} . '/config', 5, '+<');
        unless ($lock_fh) {
705
            $log->syslog('err', 'Could not create new lock');
706
707
708
709
710
711
            return undef;
        }

        ## update the binary version of the data structure
        if (Conf::get_robot_conf($self->{'domain'}, 'cache_list_config') eq
            'binary_file') {
712
713
714
715
            eval { Storable::store($admin, $self->{'dir'} . '/config.bin') };
            if ($EVAL_ERROR) {
                $log->syslog(
                    'err',
716
                    'Failed to save the binary config %s. error: %s',
717
718
                    $self->{'dir'} . '/config.bin', $EVAL_ERROR
                );
719
720
721
722
723
            }
        }

        $config_reloaded = 1;
        unless (defined $admin) {
724
            $log->syslog(
725
                'err',
sikeda's avatar
sikeda committed
726
727
                'Impossible to load list config file for list %s set in status error_config',
                $self
728
            );
729
            $self->set_status_error_config('load_admin_file_error');
730
731
732
733
            $lock_fh->close();
            return undef;
        }

734
        $last_time_config = $time_config;
735
        $lock_fh->close();
root's avatar
root committed
736
    }
737

738
739
    ## If config was reloaded...
    if ($admin) {
740
741
742
743
        $self->{'admin'} = $admin;

        ## check param_constraint.conf if belongs to a family and the config
        ## has been loaded
744
745
746
        if (    not $options->{'no_check_family'}
            and defined $admin->{'family_name'}
            and $admin->{'status'} ne 'error_config') {
747
748
            my $family;
            unless ($family = $self->get_family()) {
749
                $log->syslog(
750
                    'err',
751
                    'Impossible to get list %s family: %s. The list is set in status error_config',
752
                    $self,
753
754
755
                    $self->{'admin'}{'family_name'}
                );
                $self->set_status_error_config('no_list_family',
756
                    $self->{'admin'}{'family_name'});
757
758
759
760
761
762
                return undef;
            }
        }
    }

    $self->{'as_x509_cert'} = 1
763
764
        if -r ($self->{'dir'} . '/cert.pem')
        or -r ($self->{'dir'} . '/cert.pem.enc');
765

766
    $self->{'_mtime'}{'config'} = $last_time_config;
root's avatar
root committed
767

768
    $list_of_lists{$self->{'domain'}}{$name} = $self;
769
    return $config_reloaded;
root's avatar
root committed
770
771
}

772
## Return a list of hash's owners and their param
773
#OBSOLETED.  Use get_admins().
774
#sub get_owners;
775

776
# OBSOLETED: No longer used.
777
#sub get_nb_owners;
778

779
780
## Return a hash of list's editors and their param(empty if there isn't any
## editor)
781
#OBSOLETED. Use get_admins().
782
#sub get_editors;
783

784
## Returns an array of owners' email addresses
785
786
#OBSOLETED: Use get_admins_email('receptive_owner') or
#           get_admins_email('owner').
787
#sub get_owners_email;
788

789
## Returns an array of editors' email addresses
790
#  or owners if there isn't any editors' email addresses
791
792
#OBSOLETED: Use get_admins_email('receptive_editor') or
#           get_admins_email('actual_editor').
793
#sub get_editors_email;
794

795
## Returns an object Sympa::Family if the list belongs to a family or undef
796
797
sub get_family {
    my $self = shift;
798

799
    if (ref $self->{'family'} eq 'Sympa::Family') {
800
        return $self->{'family'};
801
802
    } elsif ($self->{'admin'}{'family_name'}) {
        return $self->{'family'} =
803
804
            Sympa::Family->new($self->{'admin'}{'family_name'},
            $self->{'domain'});
805
    } else {
806
        return undef;
807
808
809
810
    }
}

## return the config_changes hash
811
## Used ONLY with lists belonging to a family.
812
813
sub get_config_changes {
    my $self = shift;
814
    $log->syslog('debug3', '(%s)', $self->{'name'});
815

816
    unless ($self->{'admin'}{'family_name'}) {
817
        $log->syslog('err',
818
819
            '(%s) Is called but there is no family_name for this list',
            $self->{'name'});
820
        return undef;
821
    }
822

823
    ## load config_changes
824
    my $time_file =
825
        Sympa::Tools::File::get_mtime($self->{'dir'} . '/config_changes');
826
827
828
829
    unless (defined $self->{'config_changes'}
        && ($self->{'config_changes'}{'mtime'} >= $time_file)) {
        unless ($self->{'config_changes'} =
            $self->_load_config_changes_file()) {
830
            $log->syslog('err',
831
832
833
834
                'Impossible to load file config_changes from list %s',
                $self->{'name'});
            return undef;
        }
835
836
837
838
839
    }
    return $self->{'config_changes'};
}

## update file config_changes if the list belongs to a family by
840
#  writing the $what(file or param) name
841
842
843
844
845
sub update_config_changes {
    my $self = shift;
    my $what = shift;
    # one param or a ref on array of param
    my $name = shift;
846
    $log->syslog('debug2', '(%s, %s)', $self->{'name'}, $what);
847

848
    unless ($self->{'admin'}{'family_name'}) {
849
        $log->syslog(
850
            'err',
851
            '(%s, %s, %s) Is called but there is no family_name for this list',
852
853
854
</