List.pm 208 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
259
260
261
262
263
264
265

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

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

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

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

295
    # Be sure the list has been loaded.
296
    my $file = "$self->{'dir'}/stats";
297
298
299
300
301
302
303
304
305

    my $count = 0;
    if (open(MSG_COUNT, $file)) {
        while (<MSG_COUNT>) {
            if ($_ =~ /^(\d+)\s+(.*)$/) {
                $count = $1;
            }
        }
        close MSG_COUNT;
306
307
308
309
    }

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

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

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

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

331
332
    return undef if ($latest_date == 0);
    return $latest_date;
333
334
}

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

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

345
346
347
348
349
350
    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;
    }
351

352
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
    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
389
390
}

391
392
393
394
sub _cache_publish_expiry {
    my $self = shift;
    my $type = shift;

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

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

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

    if ($type eq 'member') {
415
416
417
418
        # 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];
419
420
421
422
423
    } 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];
424
425
    } else {
        die 'bug in logic. Ask developer';
426
427
428
429
    }
}

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

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

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

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

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

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

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

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

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

471
472
473
474
    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
475
476
477
478
        $log->syslog(
            'err', 'Failed to save file %s.new: %s',
            $file, Sympa::LockedFile->last_error
        );
479
        return undef;
480
    }
481

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

485
486
487
488
489
        my $user;
        for (
            $user = $self->get_first_list_member();
            $user;
            $user = $self->get_next_list_member()
Luc Didry's avatar
Luc Didry committed
490
        ) {
491
            foreach my $k (sort keys %map_field) {
492
493
494
495
496
497
498
499
500
                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};
                }
501
            }
502

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

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

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

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

525
526
            print $lock_fh "\n";
        }
root's avatar
root committed
527
    }
528

529
530
    $lock_fh->close;

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

    return 1;
root's avatar
root committed
536
537
538
539
540
}

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

543
544
    return undef
        unless ($self);
545
546
547
548

    my $config_file_name = "$self->{'dir'}/config";

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

555
556
    my $name                 = $self->{'name'};
    my $old_serial           = $self->{'admin'}{'serial'};
salaun's avatar
salaun committed
557
    my $old_config_file_name = "$self->{'dir'}/config.$old_serial";
root's avatar
root committed
558
559
560

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

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

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

590
    ## Release the lock
591
    unless ($lock_fh->close()) {
592
        return undef;
593
594
    }

595
    unless ($self->_update_list_db) {
596
        $log->syslog('err', "Unable to update list_table");
597
598
    }

root's avatar
root committed
599
600
601
602
603
    return 1;
}

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

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

612
613
    ## Set of initializations ; only performed when the config is first loaded
    if ($options->{'first_access'}) {
614
615
616
617
618
619
620
        # 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;
621
        }
622

623
624
625
626
627
        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 {
628
            $log->syslog('err', 'No such robot (virtual domain) %s', $robot)
629
630
631
632
633
634
                unless ($options->{'just_try'});
            return undef;
        }

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

635
636
        # default list host is robot domain: Deprecated.
        #XXX$self->{'admin'}{'host'} ||= $self->{'domain'};
637
        $self->{'name'} = $name;
638
    }
639

640
    unless ((-d $self->{'dir'}) && (-f "$self->{'dir'}/config")) {
641
        $log->syslog('debug2', 'Missing directory (%s) or config file for %s',
642
643
644
            $self->{'dir'}, $name)
            unless ($options->{'just_try'});
        return undef;
645
    }
salaun's avatar
salaun committed
646

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

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

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

        ## Load a binary version of the data structure
        ## unless config is more recent than config.bin
677
        eval { $admin = Storable::retrieve("$self->{'dir'}/config.bin") };
678
        if ($@) {
679
            $log->syslog('err',
680
681
682
683
684
685
                'Failed to load the binary config %s, error: %s',
                "$self->{'dir'}/config.bin", $@);
            $lock_fh->close();
            return undef;
        }

686
687
        $config_reloaded  = 1;
        $last_time_config = $time_config_bin;
688
        $lock_fh->close();
689
    } elsif ($self->{'name'} ne $name
690
        or $time_config > $last_time_config
691
        or $options->{'reload_config'}) {
692
        $admin = $self->_load_list_config_file;
693
694
695
696
697

        ## Get a shared lock on config file first
        my $lock_fh =
            Sympa::LockedFile->new($self->{'dir'} . '/config', 5, '+<');
        unless ($lock_fh) {
698
            $log->syslog('err', 'Could not create new lock');
699
700
701
702
703
704
            return undef;
        }

        ## update the binary version of the data structure
        if (Conf::get_robot_conf($self->{'domain'}, 'cache_list_config') eq
            'binary_file') {
705
            eval { Storable::store($admin, "$self->{'dir'}/config.bin") };
706
            if ($@) {
707
                $log->syslog('err',
708
709
710
711
712
713
714
                    'Failed to save the binary config %s. error: %s',
                    "$self->{'dir'}/config.bin", $@);
            }
        }

        $config_reloaded = 1;
        unless (defined $admin) {
715
            $log->syslog(
716
                'err',
sikeda's avatar
sikeda committed
717
718
                'Impossible to load list config file for list %s set in status error_config',
                $self
719
            );
720
            $self->set_status_error_config('load_admin_file_error');
721
722
723
724
            $lock_fh->close();
            return undef;
        }

725
        $last_time_config = $time_config;
726
        $lock_fh->close();
root's avatar
root committed
727
    }
728

729
730
    ## If config was reloaded...
    if ($admin) {
731
732
733
734
        $self->{'admin'} = $admin;

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

    $self->{'as_x509_cert'} = 1
        if ((-r "$self->{'dir'}/cert.pem")
        || (-r "$self->{'dir'}/cert.pem.enc"));

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

759
    $list_of_lists{$self->{'domain'}}{$name} = $self;
760
    return $config_reloaded;
root's avatar
root committed
761
762
}

763
## Return a list of hash's owners and their param
764
#OBSOLETED.  Use get_admins().
765
#sub get_owners;
766

767
# OBSOLETED: No longer used.
768
#sub get_nb_owners;
769

770
771
## Return a hash of list's editors and their param(empty if there isn't any
## editor)
772
#OBSOLETED. Use get_admins().
773
#sub get_editors;
774

775
## Returns an array of owners' email addresses
776
777
#OBSOLETED: Use get_admins_email('receptive_owner') or
#           get_admins_email('owner').
778
#sub get_owners_email;
779

780
## Returns an array of editors' email addresses
781
#  or owners if there isn't any editors' email addresses
782
783
#OBSOLETED: Use get_admins_email('receptive_editor') or
#           get_admins_email('actual_editor').
784
#sub get_editors_email;
785

786
## Returns an object Sympa::Family if the list belongs to a family or undef
787
788
sub get_family {
    my $self = shift;
789

790
    if (ref $self->{'family'} eq 'Sympa::Family') {
791
        return $self->{'family'};
792
793
    } elsif ($self->{'admin'}{'family_name'}) {
        return $self->{'family'} =
794
795
            Sympa::Family->new($self->{'admin'}{'family_name'},
            $self->{'domain'});
796
    } else {
797
        return undef;
798
799
800
801
    }
}

## return the config_changes hash
802
## Used ONLY with lists belonging to a family.
803
804
sub get_config_changes {
    my $self = shift;
805
    $log->syslog('debug3', '(%s)', $self->{'name'});
806

807
    unless ($self->{'admin'}{'family_name'}) {
808
        $log->syslog('err',
809
810
            '(%s) Is called but there is no family_name for this list',
            $self->{'name'});
811
        return undef;
812
    }
813

814
    ## load config_changes
815
816
    my $time_file =
        Sympa::Tools::File::get_mtime("$self->{'dir'}/config_changes");
817
818
819
820
    unless (defined $self->{'config_changes'}
        && ($self->{'config_changes'}{'mtime'} >= $time_file)) {
        unless ($self->{'config_changes'} =
            $self->_load_config_changes_file()) {
821
            $log->syslog('err',
822
823
824
825
                'Impossible to load file config_changes from list %s',
                $self->{'name'});
            return undef;
        }
826
827
828
829
830
    }
    return $self->{'config_changes'};
}

## update file config_changes if the list belongs to a family by
831
#  writing the $what(file or param) name
832
833
834
835
836
sub update_config_changes {
    my $self = shift;
    my $what = shift;
    # one param or a ref on array of param
    my $name = shift;
837
    $log->syslog('debug2', '(%s, %s)', $self->{'name'}, $what);
838

839
    unless ($self->{'admin'}{'family_name'}) {
840
        $log->syslog(
841
            'err',
842
            '(%s, %s, %s) Is called but there is no family_name for this list',
843
844
845
846
            $self->{'name'},
            $what
        );
        return undef;
847
    }
848
    unless (($what eq 'file') || ($what eq 'param')) {
849
        $log->syslog('err', '(%s, %s) %s is wrong: must be "file" or "param"',
850
            $self->{'name'}, $what);
851
852
853
        return undef;
    }

854
855
    # status parameter isn't updating set in config_changes
    if (($what eq 'param') && ($name eq 'status')) {
856
        return 1;
857
858
859
    }

    ## load config_changes
860
861
    my $time_file =
        Sympa::Tools::File::get_mtime("$self->{'dir'}/config_changes");
862
863
864
865
    unless (defined $self->{'config_changes'}
        && ($self->{'config_changes'}{'mtime'} >= $time_file)) {
        unless ($self->{'config_changes'} =
            $self->_load_config_changes_file()) {
866
            $log->syslog('err',
867
868
869
870
                'Impossible to load file config_changes from list %s',
                $self->{'name'});
            return undef;
        }
871
    }
872
873
874
875
876

    if (ref($name) eq 'ARRAY') {
        foreach my $n (@{$name}) {
            $self->{'config_changes'}{$what}{$n} = 1;
        }
877