1# -*- indent-tabs-mode: nil; -*-
2# vim:ft=perl:et:sw=4
3# $Id$
4
5# Sympa - SYsteme de Multi-Postage Automatique
6#
7# Copyright (c) 1997, 1998, 1999 Institut Pasteur & Christophe Wolfhugel
8# Copyright (c) 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005,
9# 2006, 2007, 2008, 2009, 2010, 2011 Comite Reseau des Universites
10# Copyright (c) 2011, 2012, 2013, 2014, 2015, 2016, 2017 GIP RENATER
11# Copyright 2017, 2020 The Sympa Community. See the AUTHORS.md
12# file at the top-level directory of this distribution and at
13# <https://github.com/sympa-community/sympa.git>.
14#
15# This program is free software; you can redistribute it and/or modify
16# it under the terms of the GNU General Public License as published by
17# the Free Software Foundation; either version 2 of the License, or
18# (at your option) any later version.
19#
20# This program is distributed in the hope that it will be useful,
21# but WITHOUT ANY WARRANTY; without even the implied warranty of
22# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
23# GNU General Public License for more details.
24#
25# You should have received a copy of the GNU General Public License
26# along with this program.  If not, see <http://www.gnu.org/licenses/>.
27
28package Sympa::Robot;
29
30use strict;
31use warnings;
32use Encode qw();
33
34use Sympa;
35use Conf;
36use Sympa::DatabaseManager;
37use Sympa::Language;
38use Sympa::ListDef;
39use Sympa::Log;
40use Sympa::Tools::Data;
41use Sympa::Tools::File;
42
43my $language = Sympa::Language->instance;
44my $log      = Sympa::Log->instance;
45
46## Database and SQL statement handlers
47my ($sth, @sth_stack);
48
49our %list_of_topics = ();
50## Last modification times
51our %mtime;
52
53our %listmaster_messages_stack;
54
55# MOVED: Use Sympa::send_file(), or Sympa::Message::Template::new() with
56# Sympa::Mailer::send_message().
57# sub send_global_file($tpl, $who, $robot, $context, $options);
58
59# MOVED: Use Sympa::send_notify_to_listmaster() or
60# Sympa::Spool::Listmaster::flush().
61# sub send_notify_to_listmaster($operation, $robot, $data, $checkstack, $purge);
62
63## Is the user listmaster
64# MOVED: Use Sympa::is_listmaster().
65#sub is_listmaster;
66
67## get idp xref to locally validated email address
68sub get_netidtoemail_db {
69    my $robot   = shift;
70    my $netid   = shift;
71    my $idpname = shift;
72    $log->syslog('debug', '(%s, %s)', $netid, $idpname);
73
74    my ($l, %which, $email);
75
76    push @sth_stack, $sth;
77
78    my $sdm = Sympa::DatabaseManager->instance;
79    unless (
80        $sdm
81        and $sth = $sdm->do_prepared_query(
82            q{SELECT email_netidmap
83              FROM netidmap_table
84              WHERE netid_netidmap = ? and serviceid_netidmap = ? and
85                    robot_netidmap = ?},
86            $netid, $idpname,
87            $robot
88        )
89    ) {
90        $log->syslog(
91            'err',
92            'Unable to get email address from netidmap_table for id %s, service %s, robot %s',
93            $netid,
94            $idpname,
95            $robot
96        );
97        return undef;
98    }
99
100    $email = $sth->fetchrow;
101
102    $sth->finish();
103
104    $sth = pop @sth_stack;
105
106    return $email;
107}
108
109## set idp xref to locally validated email address
110sub set_netidtoemail_db {
111    my $robot   = shift;
112    my $netid   = shift;
113    my $idpname = shift;
114    my $email   = shift;
115    $log->syslog('debug', '(%s, %s, %s)', $netid, $idpname, $email);
116
117    my ($l, %which);
118
119    my $sdm = Sympa::DatabaseManager->instance;
120    unless (
121        $sdm
122        and $sdm->do_prepared_query(
123            q{INSERT INTO netidmap_table
124              (netid_netidmap, serviceid_netidmap, email_netidmap,
125               robot_netidmap)
126              VALUES (?, ?, ?, ?)},
127            $netid, $idpname, $email, $robot
128        )
129    ) {
130        $log->syslog(
131            'err',
132            'Unable to set email address %s in netidmap_table for id %s, service %s, robot %s',
133            $email,
134            $netid,
135            $idpname,
136            $robot
137        );
138        return undef;
139    }
140
141    return 1;
142}
143
144## Update netidmap table when user email address changes
145sub update_email_netidmap_db {
146    my ($robot, $old_email, $new_email) = @_;
147
148    unless (defined $robot
149        && defined $old_email
150        && defined $new_email) {
151        $log->syslog('err', 'Missing parameter');
152        return undef;
153    }
154
155    my $sdm = Sympa::DatabaseManager->instance;
156    unless (
157        $sdm
158        and $sdm->do_prepared_query(
159            q{UPDATE netidmap_table
160              SET email_netidmap = ?
161              WHERE email_netidmap = ? AND robot_netidmap = ?},
162            $new_email,
163            $old_email, $robot
164        )
165    ) {
166        $log->syslog(
167            'err',
168            'Unable to set new email address %s in netidmap_table to replace old address %s for robot %s',
169            $new_email,
170            $old_email,
171            $robot
172        );
173        return undef;
174    }
175
176    return 1;
177}
178
179my $default_topics_visibility = 'noconceal';
180
181# Loads the list of topics if updated.
182# The topic names "others" and "topicsless" are reserved words therefore
183# ignored.  Note: "other" is not reserved and may be used.
184sub load_topics {
185    my $robot = shift;
186    $log->syslog('debug2', '(%s)', $robot);
187
188    my $conf_file = Sympa::search_fullpath($robot, 'topics.conf');
189
190    unless ($conf_file) {
191        $log->syslog('err', 'No topics.conf defined');
192        return;
193    }
194
195    my $topics = {};
196
197    ## Load if not loaded or changed on disk
198    if (!$list_of_topics{$robot}
199        or Sympa::Tools::File::get_mtime($conf_file) >
200        $mtime{'topics'}{$robot}) {
201
202        ## delete previous list of topics
203        %list_of_topics = ();
204
205        unless (-r $conf_file) {
206            $log->syslog('err', 'Unable to read %s', $conf_file);
207            return;
208        }
209
210        my $config_content = Sympa::Tools::Text::slurp($conf_file);
211        unless (defined $config_content) {
212            $log->syslog('err', 'Unable to open config file %s', $conf_file);
213            return;
214        }
215
216        ## Rough parsing
217        my $index = 0;
218        my (@rough_data, $topic);
219        foreach my $line (split /(?<=\n)(?=\n|.)/, $config_content) {
220            if ($line =~ /\A(others|topicsless)\s*\z/i) {
221                # "others" and "topicsless" are reserved words. Ignore.
222                next;
223            } elsif ($line =~ /^([\-\w\/]+)\s*$/) {
224                $index++;
225                $topic = {
226                    'name'  => lc($1),
227                    'order' => $index
228                };
229            } elsif ($line =~ /^([\w\.]+)\s+(.+\S)\s*$/) {
230                next unless defined $topic->{'name'};
231
232                $topic->{$1} = $2;
233            } elsif ($line =~ /^\s*$/) {
234                next unless defined $topic->{'name'};
235
236                push @rough_data, $topic;
237                $topic = {};
238            }
239        }
240
241        ## Last topic
242        if (defined $topic->{'name'}) {
243            push @rough_data, $topic;
244            $topic = {};
245        }
246
247        $mtime{'topics'}{$robot} = Sympa::Tools::File::get_mtime($conf_file);
248
249        unless ($#rough_data > -1) {
250            $log->syslog('notice', 'No topic defined in %s', $conf_file);
251            return;
252        }
253
254        ## Analysis
255        foreach my $topic (@rough_data) {
256            my @tree = split '/', $topic->{'name'};
257
258            if ($#tree == 0) {
259                my $title = _load_topics_get_title($topic);
260                $list_of_topics{$robot}{$tree[0]}{'title'} = $title;
261                $list_of_topics{$robot}{$tree[0]}{'visibility'} =
262                    $topic->{'visibility'} || $default_topics_visibility;
263                $list_of_topics{$robot}{$tree[0]}{'order'} =
264                    $topic->{'order'};
265            } else {
266                my $subtopic = join('/', @tree[1 .. $#tree]);
267                my $title = _load_topics_get_title($topic);
268                my $visibility =
269                    $topic->{'visibility'} || $default_topics_visibility;
270                $list_of_topics{$robot}{$tree[0]}{'sub'}{$subtopic} =
271                    _add_topic($subtopic, $title, $visibility);
272            }
273        }
274
275        ## Set undefined Topic (defined via subtopic)
276        foreach my $t (keys %{$list_of_topics{$robot}}) {
277            unless (defined $list_of_topics{$robot}{$t}{'title'}) {
278                $list_of_topics{$robot}{$t}{'title'} = {'default' => $t};
279            }
280        }
281    }
282
283    ## Set the title in the current language
284    my $lang = $language->get_lang;
285    foreach my $top (keys %{$list_of_topics{$robot}}) {
286        my $topic = $list_of_topics{$robot}{$top};
287        foreach my $l (Sympa::Language::implicated_langs($lang)) {
288            if (exists $topic->{'title'}{$l}) {
289                $topic->{'current_title'} = $topic->{'title'}{$l};
290            }
291        }
292        unless (exists $topic->{'current_title'}) {
293            if (exists $topic->{'title'}{'gettext'}) {
294                $topic->{'current_title'} =
295                    $language->gettext($topic->{'title'}{'gettext'});
296            } else {
297                $topic->{'current_title'} = $topic->{'title'}{'default'}
298                    || $top;
299            }
300        }
301
302        foreach my $subtop (keys %{$topic->{'sub'}}) {
303            foreach my $l (Sympa::Language::implicated_langs($lang)) {
304                if (exists $topic->{'sub'}{$subtop}{'title'}{$l}) {
305                    $topic->{'sub'}{$subtop}{'current_title'} =
306                        $topic->{'sub'}{$subtop}{'title'}{$l};
307                }
308            }
309            unless (exists $topic->{'sub'}{$subtop}{'current_title'}) {
310                if (exists $topic->{'sub'}{$subtop}{'title'}{'gettext'}) {
311                    $topic->{'sub'}{$subtop}{'current_title'} =
312                        $language->gettext(
313                        $topic->{'sub'}{$subtop}{'title'}{'gettext'});
314                } else {
315                    $topic->{'sub'}{$subtop}{'current_title'} =
316                           $topic->{'sub'}{$subtop}{'title'}{'default'}
317                        || $subtop;
318                }
319            }
320        }
321    }
322
323    return %{$list_of_topics{$robot}};
324}
325
326# Old name: _get_topic_titles().
327sub _load_topics_get_title {
328    my $topic = shift;
329
330    my $title;
331    foreach my $key (%{$topic}) {
332        if ($key =~ /^title\.gettext$/i) {
333            $title->{'gettext'} = $topic->{$key};
334        } elsif ($key =~ /^title\.(\S+)$/i) {
335            my $lang = $1;
336            # canonicalize lang if possible.
337            $lang = Sympa::Language::canonic_lang($lang) || $lang;
338            $title->{$lang} = $topic->{$key};
339        } elsif ($key =~ /^title$/i) {
340            $title->{'default'} = $topic->{$key};
341        }
342    }
343
344    return $title;
345}
346
347## Inner sub used by load_topics()
348sub _add_topic {
349    my ($name, $title, $visibility) = @_;
350    my $topic = {};
351
352    my @tree = split '/', $name;
353    if ($#tree == 0) {
354        return {'title' => $title, 'visibility' => $visibility};
355    } else {
356        $topic->{'sub'}{$name} =
357            _add_topic(join('/', @tree[1 .. $#tree]), $title, $visibility);
358        return $topic;
359    }
360}
361
362sub topic_keys {
363    my $robot_id = shift;
364
365    my %topics = Sympa::Robot::load_topics($robot_id);
366    return map {
367        my $topic = $_;
368        if ($topics{$topic}->{sub}) {
369            (   $topic,
370                map { $topic . '/' . $_ } sort keys %{$topics{$topic}->{sub}}
371            );
372        } else {
373            ($topic);
374        }
375    } sort keys %topics;
376}
377
378sub topic_get_title {
379    my $robot_id = shift;
380    my $topic    = shift;
381
382    my $tinfo = {Sympa::Robot::load_topics($robot_id)};
383    return unless %$tinfo;
384
385    my @ttitles;
386    my @tpaths = split '/', $topic;
387
388    while (1) {
389        my $t = shift @tpaths;
390        unless (exists $tinfo->{$t}) {
391            @ttitles = ();
392            last;
393        } elsif (not @tpaths) {
394            push @ttitles, (_topic_get_title($tinfo->{$t}) || $t);
395            last;
396        } elsif (not $tinfo->{$t}->{sub}) {
397            @ttitles = ();
398            last;
399        } else {
400            push @ttitles, (_topic_get_title($tinfo->{$t}) || $t);
401            $tinfo = $tinfo->{$t}->{sub};
402        }
403    }
404
405    return @ttitles if wantarray;
406    return join ' / ', @ttitles;
407}
408
409sub _topic_get_title {
410    my $titem = shift;
411
412    return undef unless $titem and exists $titem->{title};
413
414    foreach my $lang (Sympa::Language::implicated_langs($language->get_lang))
415    {
416        return $titem->{title}->{$lang}
417            if $titem->{title}->{$lang};
418    }
419    if ($titem->{title}->{gettext}) {
420        return $language->gettext($titem->{title}->{gettext});
421    } elsif ($titem->{title}->{default}) {
422        return $titem->{title}->{default};
423    } else {
424        return undef;
425    }
426}
427
428=over 4
429
430=item list_params
431
432I<Getter>.
433Returns hashref to list parameter information.
434
435=back
436
437=cut
438
439# Old name: tools::get_list_params().
440sub list_params {
441    my $robot_id = shift;
442
443    my $pinfo = Sympa::Tools::Data::clone_var(\%Sympa::ListDef::pinfo);
444    $pinfo->{lang}{format} = [Sympa::get_supported_languages($robot_id)];
445
446    my @topics = Sympa::Robot::topic_keys($robot_id);
447    $pinfo->{topics}{format} = [@topics];
448    # Compat.
449    $pinfo->{topics}{file_format} = sprintf '(%s)(,(%s))*',
450        join('|', @topics),
451        join('|', @topics);
452
453    return $pinfo;
454}
455
4561;
457