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