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

# Sympa - SYsteme de Multi-Postage Automatique
#
7
8
# Copyright 2017, 2018, 2019 The Sympa Community. See the AUTHORS.md file at
# the top-level directory of this distribution and at
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# <https://github.com/sympa-community/sympa.git>.
#
# 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.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

package Sympa::Request::Handler::move_list;

use strict;
use warnings;
use File::Copy qw();

use Sympa;
31
use Sympa::Aliases;
32
33
34
35
use Conf;
use Sympa::DatabaseManager;
use Sympa::List;
use Sympa::Log;
IKEDA Soji's avatar
IKEDA Soji committed
36
use Sympa::Spool;
37
38
39
40
41
42
43
44
use Sympa::Spool::Archive;
use Sympa::Spool::Auth;
use Sympa::Spool::Automatic;
use Sympa::Spool::Bounce;
use Sympa::Spool::Digest::Collection;
use Sympa::Spool::Held;
use Sympa::Spool::Incoming;
use Sympa::Spool::Moderation;
45
use Sympa::Spool::Outgoing;
46
use Sympa::Spool::Task;
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
use Sympa::Tools::File;

use base qw(Sympa::Request::Handler);

my $log = Sympa::Log->instance;

use constant _action_regexp   => qr{reject|listmaster|do_it}i;
use constant _action_scenario => 'create_list';

# Old name: Sympa::Admin::rename_list().
sub _twist {
    my $self    = shift;
    my $request = shift;

    my $robot_id     = $request->{context};
    my $current_list = $request->{current_list};
63
    my $listname     = lc($request->{listname} || '');
64
65
66
67
68
69
70
71
    my $mode         = $request->{mode};
    my $pending      = $request->{pending};
    my $notify       = $request->{notify};
    my $sender       = $request->{sender};

    die 'bug in logic. Ask developer'
        unless ref $current_list eq 'Sympa::List';

72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
    # No changes.
    if ($current_list->get_id eq $listname . '@' . $robot_id) {
        $log->syslog('err', 'Cannot rename list: List %s will not be changed',
            $current_list);
        $self->add_stash(
            $request, 'user',
            'unable_to_rename_list',
            {   listname     => $current_list->get_id,
                new_listname => $listname . '@' . $robot_id,
                reason       => 'no_change'
            }
        );
        return undef;
    }

87
88
89
90
91
92
    # If list is included by another list, then it cannot be renamed.
    unless ($mode and $mode eq 'copy') {
        if ($current_list->is_included) {
            $log->syslog('err',
                'List %s is included by other list: cannot rename it',
                $current_list);
93
94
95
96
97
98
99
100
            $self->add_stash(
                $request, 'user',
                'unable_to_rename_list',
                {   listname     => $current_list->get_id,
                    new_listname => $listname . '@' . $robot_id,
                    reason       => 'included'
                }
            );
101
102
103
104
            return undef;
        }
    }

105
106
107
108
    # Check new listname.
    my @stash = Sympa::Aliases::check_new_listname($listname, $robot_id);
    if (@stash) {
        $self->add_stash($request, @stash);
109
110
111
        return undef;
    }

112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
    # Rename or create this list directory itself.
    my $new_dir;
    my $home = $Conf::Conf{'home'};
    my $base = $home . '/' . $robot_id;
    if (-d $base) {
        $new_dir = $base . '/' . $listname;
    } elsif ($robot_id eq $Conf::Conf{'domain'}) {
        # Default robot.
        $new_dir = $home . '/' . $listname;
    } else {
        $log->syslog('err', 'Unknown robot %s', $robot_id);
        $self->add_stash($request, 'user', 'unknown_robot',
            {new_robot => $robot_id});
        return undef;
    }

    if ($mode and $mode eq 'copy') {
        _copy($self, $request, $new_dir) or return undef;
    } else {
        _move($self, $request, $new_dir) or return undef;
    }

    my $list;
    unless ($list =
        Sympa::List->new($listname, $robot_id, {reload_config => 1})) {
        $log->syslog('err', 'Unable to load %s while renaming', $listname);
        $self->add_stash($request, 'intern');
        return undef;
    }

    if ($listname ne $request->{listname}) {
        $self->add_stash($request, 'notice', 'listname_lowercased');
    }

    if ($list->{'admin'}{'status'} eq 'open') {
        # Install new aliases.
148
149
150
151
152
        my $aliases = Sympa::Aliases->new(
            Conf::get_robot_conf($robot_id, 'alias_manager'));
        if ($aliases and $aliases->add($list)) {
            $self->add_stash($request, 'notice', 'auto_aliases');
        }
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
    } elsif ($list->{'admin'}{'status'} eq 'pending') {
        # Notify listmaster that creation list is moderated.
        Sympa::send_notify_to_listmaster(
            $list,
            'request_list_renaming',
            {   'new_listname' => $listname,
                'old_listname' => $current_list->{'name'},
                'email'        => $sender,
                'mode'         => $mode,
            }
        ) if $notify;

        $self->add_stash($request, 'notice', 'pending_list');
    }

    if ($mode and $mode eq 'copy') {
        $log->add_stat(
            robot     => $list->{'domain'},
            list      => $list->{'name'},
            operation => 'copy_list',
            mail      => $sender,
            client    => $self->{scenario_context}->{remote_addr},
        );
    }

    return 1;
}

sub _move {
    my $self    = shift;
    my $request = shift;
    my $new_dir = shift;

    my $robot_id     = $request->{context};
IKEDA Soji's avatar
typos.    
IKEDA Soji committed
187
    my $listname     = lc($request->{listname} || '');
188
189
190
191
192
    my $current_list = $request->{current_list};
    my $sender       = $request->{sender};
    my $pending      = $request->{pending};

    # Remove aliases and dump subscribers.
193
194
195
    my $aliases = Sympa::Aliases->new(
        Conf::get_robot_conf($current_list->{'domain'}, 'alias_manager'));
    $aliases->del($current_list) if $aliases;
196
197
198
    $current_list->dump_users('member');
    $current_list->dump_users('owner');
    $current_list->dump_users('editor');
199
200
201
202
203
204
205
206

    # Set list status to pending if creation list is moderated.
    # Save config file for the new() later to reload it.
    $current_list->{'admin'}{'status'} = 'pending'
        if $pending;
    _modify_custom_subject($request, $current_list);
    $current_list->save_config($sender);

207
    # Start moving list.
IKEDA Soji's avatar
IKEDA Soji committed
208
    my $sdm       = Sympa::DatabaseManager->instance;
209
210
211
212
213
214
    my $fake_list = bless {
        name   => $listname,
        domain => $robot_id,
        dir    => $new_dir,
    } => 'Sympa::List';

215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
    # If subscribtion are stored in database rewrite the database.
    unless (
        $sdm
        and $sdm->do_prepared_query(
            q{UPDATE subscriber_table
              SET list_subscriber = ?, robot_subscriber = ?
              WHERE list_subscriber = ? AND robot_subscriber = ?},
            $listname,               $robot_id,
            $current_list->{'name'}, $current_list->{'domain'}
        )
        and $sdm->do_prepared_query(
            q{UPDATE admin_table
              SET list_admin = ?, robot_admin = ?
              WHERE list_admin = ? AND robot_admin = ?},
            $listname,               $robot_id,
            $current_list->{'name'}, $current_list->{'domain'}
        )
        and $sdm->do_prepared_query(
            q{UPDATE list_table
              SET name_list = ?, robot_list = ?
              WHERE name_list = ? AND robot_list = ?},
            $listname,               $robot_id,
            $current_list->{'name'}, $current_list->{'domain'}
        )
239
240
241
242
243
244
245
        and $sdm->do_prepared_query(
            q{UPDATE exclusion_table
              SET list_exclusion = ?, robot_exclusion = ?
              WHERE list_exclusion = ? AND robot_exclusion = ?},
            $listname,               $robot_id,
            $current_list->{'name'}, $current_list->{'domain'}
        )
246
247
248
249
250
251
252
        and $sdm->do_prepared_query(
            q{UPDATE inclusion_table
              SET target_inclusion = ?
              WHERE target_inclusion = ?},
            sprintf('%s@%s', $listname, $robot_id),
            $current_list->get_id
        )
Luc Didry's avatar
Luc Didry committed
253
    ) {
254
255
256
        $log->syslog('err',
            'Unable to rename list %s to %s@%s in the database',
            $current_list, $listname, $robot_id);
257
        $self->add_stash($request, 'intern');
258
259
260
261
        return undef;
    }

    # Move stats.
262
    # Continue even if there are some troubles.
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
    unless (
        $sdm
        and $sdm->do_prepared_query(
            q{UPDATE stat_table
              SET list_stat = ?, robot_stat = ?
              WHERE list_stat = ? AND robot_stat = ?},
            $listname,               $robot_id,
            $current_list->{'name'}, $current_list->{'domain'}
        )
        and $sdm->do_prepared_query(
            q{UPDATE stat_counter_table
              SET list_counter = ?, robot_counter = ?
              WHERE list_counter = ? AND robot_counter = ?},
            $listname,               $robot_id,
            $current_list->{'name'}, $current_list->{'domain'}
        )
Luc Didry's avatar
Luc Didry committed
279
    ) {
280
281
282
283
284
285
        $log->syslog('err',
            'Unable to transfer stats from list %s to list %s@%s',
            $current_list, $listname, $robot_id);
    }

    # Rename files in spools.
286
287
288
289
    # Continue even if there are some troubles.
    foreach my $spool_class (
        qw(Sympa::Spool::Automatic Sympa::Spool::Bounce Sympa::Spool::Incoming
        Sympa::Spool::Auth Sympa::Spool::Held Sympa::Spool::Moderation
IKEDA Soji's avatar
IKEDA Soji committed
290
291
        Sympa::Spool::Archive Sympa::Spool::Digest::Collection
        Sympa::Spool::Task)
Luc Didry's avatar
Luc Didry committed
292
    ) {
293
294
        my $spool = $spool_class->new(context => $current_list);
        next unless $spool;
295

296
        while (1) {
297
            my ($message, $handle) = $spool->next(no_filter => 1);
298
            last unless $handle;
299
            next
300
                unless $message
Luc Didry's avatar
Luc Didry committed
301
302
                and ref $message->{context} eq 'Sympa::List'
                and $message->{context}->get_id eq $current_list->get_id;
303
304
305
306
307
308

            # Remove old HTML view if any (For moderation spool).
            $spool->html_remove($message) if $spool->can('html_remove');

            # Rename message in spool.
            $message->{context} = $fake_list;
309
            my $marshalled = $spool->marshal($message, keep_keys => 1);
310
311
312
313
314
            unless ($handle->rename($spool->{directory} . '/' . $marshalled))
            {
                $log->syslog('err',
                    'Cannot rename message in %s from %s to %s: %m',
                    $spool, $handle->basename, $marshalled);
315
            }
316
317
318
        }
    }

319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
    my $queue;
    my $dh;

    # Rename files in topic spool.
    # Continue even if there are some troubles.
    #FIXME: Refactor to use Sympa::Spool subclass.
    $queue = $Conf::Conf{'queuetopic'};
    unless (opendir $dh, $queue) {
        $log->syslog('err', 'Unable to open topic spool %s: %m', $queue);
    } else {
        my $current_list_id = $current_list->get_id;
        my $new_list_id     = $fake_list->get_id;

        foreach my $file (sort readdir $dh) {
            next unless 0 == index $file, $current_list_id . '.';

            my $newfile = sprintf '%s.%s', $new_list_id,
                substr($file, length($current_list_id) + 1);
            unless (rename $queue . '/' . $file, $queue . '/' . $newfile) {
338
339
                $log->syslog('err',
                    'Unable to rename file in %s from %s to %s: %m',
340
                    $queue, $file, $newfile);
341
342
343
            }
        }

344
        close $dh;
345
    }
346

347
348
    # Rename files in outgoing spool.
    # Continue even if there are some troubles.
349
    my $spool = Sympa::Spool::Outgoing->new(context => $current_list);
350
351
352
353
354
    while (1) {
        my ($message, $handle) = $spool->next(no_filter => 1);
        last unless $handle;
        next
            unless $message
Luc Didry's avatar
Luc Didry committed
355
356
            and ref $message->{context} eq 'Sympa::List'
            and $message->{context}->get_id eq $current_list->get_id;
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
391

        my $pct_directory =
            $spool->{pct_directory} . '/' . $handle->basename(1);
        my $msg_file = $spool->{msg_directory} . '/' . $handle->basename(1);
        my $pct_file = $pct_directory . '/' . $handle->basename;

        # Rename message in spool.
        $message->{context} = $fake_list;
        my $marshalled = Sympa::Spool::marshal_metadata(
            $message,
            '%s.%s.%d.%f.%s@%s_%s,%ld,%d',
            [   qw(priority packet_priority date time localpart domainpart tag pid rand)
            ]
        );
        my $new_pct_directory = $spool->{pct_directory} . '/' . $marshalled;
        my $new_msg_file      = $spool->{msg_directory} . '/' . $marshalled;
        my $new_pct_file      = $new_pct_directory . '/' . $handle->basename;

        File::Copy::cp($msg_file, $new_msg_file)
            unless -e $new_msg_file;

        mkdir $new_pct_directory unless -d $new_pct_directory;
        unless (-d $new_pct_directory and $handle->rename($new_pct_file)) {
            $log->syslog('err',
                'Cannot rename message in %s from %s to %s: %m',
                $spool, $pct_file, $new_pct_file);
            next;
        }

        if (rmdir $pct_directory) {
            # No more packet.
            unlink $msg_file;
        }
    }

IKEDA Soji's avatar
IKEDA Soji committed
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
    # Try renaming archive.
    # Continue even if there are some troubles.
    my $arc_dir     = $current_list->get_archive_dir;
    my $new_arc_dir = $fake_list->get_archive_dir;
    if (-d $arc_dir and $arc_dir ne $new_arc_dir) {
        unless (File::Copy::move($arc_dir, $new_arc_dir)) {
            $log->syslog('err', 'Unable to rename archive %s to %s: %m',
                $arc_dir, $new_arc_dir);
        }
    }

    # Try renaming bounces and tracking information.
    # Continue even if there are some troubles.
    my $bounce_dir     = $current_list->get_bounce_dir;
    my $new_bounce_dir = $fake_list->get_bounce_dir;
    if (-d $bounce_dir and $bounce_dir ne $new_bounce_dir) {
        unless (File::Copy::move($bounce_dir, $new_bounce_dir)) {
            $log->syslog('err', 'Unable to rename bounces from %s to %s: %m',
                $bounce_dir, $new_bounce_dir);
        }
    }
413
    unless (
IKEDA Soji's avatar
IKEDA Soji committed
414
415
416
417
418
419
420
421
        $sdm
        and $sdm->do_prepared_query(
            q{UPDATE notification_table
              SET list_notification = ?, robot_notification = ?
              WHERE list_notification = ? AND robot_notification = ?},
            $listname,               $robot_id,
            $current_list->{'name'}, $current_list->{'domain'}
        )
Luc Didry's avatar
Luc Didry committed
422
    ) {
IKEDA Soji's avatar
IKEDA Soji committed
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
        $log->syslog(
            'err',
            'Unable to transfer tracking information from list %s to list %s@%s',
            $current_list,
            $listname,
            $robot_id
        );
    }
    # Clear old HTML view.
    Sympa::Tools::File::remove_dir(
        sprintf '%s/%s/%s',
        $Conf::Conf{'viewmail_dir'},
        'bounce', $current_list->get_id
    );

    # End moving list.
    unless (File::Copy::move($current_list->{'dir'}, $new_dir)) {
        $log->syslog(
            'err',
            'Unable to rename %s to %s: %m',
            $current_list->{'dir'}, $new_dir
        );
        $self->add_stash($request, 'intern');
        return undef;
447
448
449
450
451
    }

    return 1;
}

IKEDA Soji's avatar
IKEDA Soji committed
452
# Old name: Sympa::Admin::clone_list_as_empty() etc.
453
454
455
456
457
458
sub _copy {
    my $self    = shift;
    my $request = shift;
    my $new_dir = shift;

    my $robot_id     = $request->{context};
IKEDA Soji's avatar
typos.    
IKEDA Soji committed
459
    my $listname     = lc($request->{listname} || '');
460
461
462
463
464
465
    my $current_list = $request->{current_list};
    my $sender       = $request->{sender};
    my $pending      = $request->{pending};

    unless (mkdir $new_dir, 0775) {
        $log->syslog('err', 'Failed to create directory %s: %m', $new_dir);
IKEDA Soji's avatar
IKEDA Soji committed
466
        $self->add_stash($request, 'intern');
467
468
        return undef;
    }
469
470
471
472
473

    # Dump permanent/transitional owners and moderators (Not subscribers).
    $current_list->dump_users('owner');
    $current_list->dump_users('editor');

474
475
476
477
478
479
480
481
    chmod 0775, $new_dir;
    foreach my $subdir ('etc', 'web_tt2', 'mail_tt2', 'data_sources') {
        if (-d $new_dir . '/' . $subdir) {
            unless (
                Sympa::Tools::File::copy_dir(
                    $current_list->{'dir'} . '/' . $subdir,
                    $new_dir . '/' . $subdir
                )
Luc Didry's avatar
Luc Didry committed
482
            ) {
483
484
485
486
487
                $log->syslog(
                    'err',
                    'Failed to copy_directory %s: %m',
                    $new_dir . '/' . $subdir
                );
IKEDA Soji's avatar
IKEDA Soji committed
488
                $self->add_stash($request, 'intern');
489
490
491
492
493
                return undef;
            }
        }
    }
    # copy mandatory files
494
    foreach my $file ('config', 'owner.dump', 'editor.dump') {
495
496
497
498
499
        unless (
            File::Copy::copy(
                $current_list->{'dir'} . '/' . $file,
                $new_dir . '/' . $file
            )
Luc Didry's avatar
Luc Didry committed
500
        ) {
501
502
503
504
505
            $log->syslog(
                'err',
                'Failed to copy %s: %m',
                $new_dir . '/' . $file
            );
IKEDA Soji's avatar
IKEDA Soji committed
506
            $self->add_stash($request, 'intern');
507
508
509
510
            return undef;
        }
    }
    # copy optional files
511
    foreach my $file ('message_header', 'message_footer', 'info', 'homepage')
512
513
514
515
516
517
518
    {
        if (-f $current_list->{'dir'} . '/' . $file) {
            unless (
                File::Copy::copy(
                    $current_list->{'dir'} . '/' . $file,
                    $new_dir . '/' . $file
                )
Luc Didry's avatar
Luc Didry committed
519
            ) {
520
521
522
523
524
                $log->syslog(
                    'err',
                    'Failed to copy %s: %m',
                    $new_dir . '/' . $file
                );
IKEDA Soji's avatar
IKEDA Soji committed
525
                $self->add_stash($request, 'intern');
526
527
528
529
530
531
532
                return undef;
            }
        }
    }

    my $new_list;
    # Now switch List object to new list, update some values.
533
    unless ($new_list = Sympa::List->new($listname, $robot_id)) {
534
        $log->syslog('info', 'Unable to load %s while renamming', $listname);
IKEDA Soji's avatar
IKEDA Soji committed
535
        $self->add_stash($request, 'intern');
536
537
538
539
540
        return undef;
    }
    $new_list->{'admin'}{'serial'} = 0;
    $new_list->{'admin'}{'creation'}{'email'} = $sender if ($sender);
    $new_list->{'admin'}{'creation'}{'date_epoch'} = time;
IKEDA Soji's avatar
IKEDA Soji committed
541
542
543
544
545
546

    # Set list status to pending if creation list is moderated.
    # Save config file for the new() later to reload it.
    $new_list->{'admin'}{'status'} = 'pending'
        if $pending;
    _modify_custom_subject($request, $new_list);
547
    $new_list->save_config($sender);
IKEDA Soji's avatar
IKEDA Soji committed
548

549
550
551
552
    # Store permanent/transitional owners and moderators (Not subscribers).
    $new_list->restore_users('owner');
    $new_list->restore_users('editor');

IKEDA Soji's avatar
IKEDA Soji committed
553
    return 1;
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
}

sub _modify_custom_subject {
    my $request  = shift;
    my $new_list = shift;

    return unless defined $new_list->{'admin'}{'custom_subject'};

    # Check custom_subject.
    my $custom_subject  = $new_list->{'admin'}{'custom_subject'};
    my $old_listname_re = $request->{current_list}->{'name'};
    $old_listname_re =~ s/([^\s\w\x80-\xFF])/\\$1/g;    # excape metachars
    my $listname = $request->{listname};

    $custom_subject =~ s/\b$old_listname_re\b/$listname/g;
    $new_list->{'admin'}{'custom_subject'} = $custom_subject;
}

1;
__END__

=encoding utf-8

=head1 NAME

Sympa::Request::Handler::move_list - move_list request handler

=head1 DESCRIPTION

583
Renames a list or move a list to possibly beyond another virtual host.
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621

On copy mode, Clone a list config including customization, templates,
scenario config but without archives, subscribers and shared.

=head2 Attributes

See also L<Sympa::Request/"Attributes">.

=over

=item {context}

Context of request.  The robot the new list will belong to.

=item {current_list}

Source of moving or copying.  An instance of L<Sympa::List>.

=item {listname}

The name of the new list.

=item {mode}

I<Optional>.
If it is set and its value is C<'copy'>,
won't erase source list.

=back

=head1 SEE ALSO

L<Sympa::Request::Collection>,
L<Sympa::Request::Handler>,
L<Sympa::Spindle::ProcessRequest>.

=head1 HISTORY

622
L<Sympa::Request::Handler::move_list> appeared on Sympa 6.2.23b.
623
624

=cut