#!/usr/bin/perl
# -*- indent-tabs-mode: nil; -*-
# vim:ft=perl:et:sw=4
# $Id: wwsympa.fcgi.in 12833 2016-06-08 08:30:27Z sikeda $

# Sympa - SYsteme de Multi-Postage Automatique
#
# 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
# Copyright (c) 2011, 2012, 2013, 2014, 2015, 2016 GIP RENATER
#
# 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/>.

## Copyright 1999 Comité Réseaux des Universités
## web interface to Sympa mailing lists manager
## Sympa: http://www.sympa.org/
## Authors :
##           Serge Aumont <sa AT cru.fr>
##           Olivier Salaün <os AT cru.fr>

use strict;
##use warnings;
use lib split(/:/, $ENV{SYMPALIB} || ''), '/usr/local/libdata/perl5/site_perl';

use Archive::Zip qw();
use DateTime;
use DateTime::Format::Mail;
use Digest::MD5;
use Encode qw();
use English qw(-no_match_vars);
use MIME::EncWords;
use MIME::Lite::HTML;
use POSIX qw();
use Time::Local qw();
use URI;
use Data::Dumper;    # tentative
BEGIN { eval 'use Crypt::OpenSSL::X509'; }

use Sympa;
use Sympa::Admin;
use Sympa::Alarm;
use Sympa::Archive;
use Sympa::Auth;
use Sympa::Bulk;
use Conf;
use Sympa::ConfDef;
use Sympa::Constants;
use Sympa::Crash Hook => \&_crash_handler;    # Show traceback.
use Sympa::Database;
use Sympa::DatabaseManager;
use Sympa::Family;
use Sympa::HTMLSanitizer;
use Sympa::Language;
use Sympa::List;
use Sympa::ListOpt;
use Sympa::Log;
use Sympa::Marc::Search;
use Sympa::Message;
use Sympa::Regexps;
use Sympa::Report;
use Sympa::Request;
use Sympa::Robot;
use Sympa::Scenario;
use Sympa::Session;
use Sympa::SharedDocument;
use Sympa::Spindle::ResendArchive;
use Sympa::Spool::Archive;
use Sympa::Spool::Auth;
use Sympa::Spool::Held;
use Sympa::Spool::Incoming;
use Sympa::Spool::Moderation;
use Sympa::Template;
use Sympa::Ticket;
use Sympa::Tools::Data;
use Sympa::Tools::File;
use Sympa::Tools::Password;
use Sympa::Tools::Text;
use Sympa::Tools::WWW;
use Sympa::Topic;
use Sympa::Tracking;
use Sympa::User;

## WWSympa librairies
my %options;

my $sympa_conf_file = Sympa::Constants::CONFIG;

# Used via the Sympa::Plugin interface
our $list;
our $param = {};
our $robot_id;
our $session;
our $plugins;

my $loop = 0;
my ($robot, $robot_object);
my $pinfo;
my $ip;
my $rss;
my $ajax;

my $allow_absolute_path;    #FIXME: to be removed in the future.
my @other_include_path;     #FIXME: ditto.

## Load sympa config
unless (Conf::load()) {
    printf STDERR
        "Unable to load sympa configuration, file %s or one of the vhost robot.conf files contain errors. Exiting.\n",
        Conf::get_sympa_conf();
    exit 1;
}

# Open log
my $log = Sympa::Log->instance;
$log->{level} = $Conf::Conf{'log_level'};
$log->openlog($Conf::Conf{'log_facility'} || $Conf::Conf{'syslog'},
    $Conf::Conf{'log_socket_type'});

## Start plugins
if (eval "require Sympa::Plugin::Manager") {
    $plugins = Sympa::Plugin::Manager->new(application => 'website');
}

Sympa::Alarm->instance->{use_bulk} = 1;

if ($Conf::Conf{'use_fast_cgi'}) {
    require CGI::Fast;
} else {
    require CGI;
}

# hash of all the description files already loaded
# format :
#     $desc_files{pathfile}{'date'} : date of the last load
#     $desc_files{pathfile}{'desc_hash'} : hash which describes
#                         the description file

#%desc_files_map; NOT USED ANYMORE

## Shared directory and description file

#$shared = 'shared';
#$desc = '.desc';

## subroutines
our %comm = (
    'home'                   => 'do_home',
    'logout'                 => 'do_logout',
    'loginrequest'           => 'do_loginrequest',
    'login'                  => 'do_login',
    'sso_login'              => 'do_sso_login',
    'sso_login_succeeded'    => 'do_sso_login_succeeded',
    'subscribe'              => 'do_subscribe',
    'multiple_subscribe'     => 'do_multiple_subscribe',
    'subrequest'             => 'do_subrequest',
    'subindex'               => 'do_subindex',
    'suboptions'             => 'do_suboptions',
    'signoff'                => 'do_signoff',
    'auto_signoff'           => 'do_auto_signoff',
    'family_signoff'         => 'do_family_signoff',
    'family_signoff_request' => 'do_family_signoff_request',
    #XXX'multiple_signoff' => 'do_multiple_signoff',
    'sigrequest'       => 'do_sigrequest',
    'sigindex'         => 'do_sigindex',
    'ignoresub'        => 'do_ignoresub',
    'ignoresig'        => 'do_ignoresig',
    'my'               => 'do_my',
    'which'            => 'do_which',
    'lists'            => 'do_lists',
    'lists_categories' => 'do_lists_categories',
    'latest_lists'     => 'do_latest_lists',
    'active_lists'     => 'do_active_lists',
    'including_lists'  => 'do_including_lists',
    'info'             => 'do_info',
    'subscriber_count' => 'do_subscriber_count',
    'review'           => 'do_review',
    'search'           => 'do_search',
    'pref',            => 'do_pref',
    'setpref'          => 'do_setpref',
    'setpasswd'        => 'do_setpasswd',
    'renewpasswd'      => 'do_renewpasswd',
    'firstpasswd'      => 'do_firstpasswd',
    'requestpasswd'    => 'do_requestpasswd',
    'choosepasswd'     => 'do_choosepasswd',
    'viewfile'         => 'do_viewfile',
    'set'              => 'do_set',
    'admin'            => 'do_admin',
    'add_request'      => 'do_add_request',
    'add'              => 'do_add',
    'add_fromsub'      => 'do_add_fromsub',
    'del'              => 'do_del',
    'del_fromsig'      => 'do_del_fromsig',
    'modindex'         => 'do_modindex',
    'docindex'         => 'do_docindex',
    'reject'           => 'do_reject',
    #XXX'reject_notify' => 'do_reject_notify',
    'distribute'      => 'do_distribute',
    'add_frommod'     => 'do_add_frommod',
    'viewmod'         => 'do_viewmod',
    'd_reject_shared' => 'do_d_reject_shared',
    #XXX'reject_notify_shared' => 'do_reject_notify_shared',
    'd_install_shared'         => 'do_d_install_shared',
    'editfile'                 => 'do_editfile',
    'savefile'                 => 'do_savefile',
    'arc'                      => 'do_arc',
    'latest_arc'               => 'do_latest_arc',
    'latest_d_read'            => 'do_latest_d_read',
    'arc_manage'               => 'do_arc_manage',
    'remove_arc'               => 'do_remove_arc',
    'send_me'                  => 'do_send_me',
    'view_source'              => 'do_view_source',
    'tracking'                 => 'do_tracking',
    'arcsearch_form'           => 'do_arcsearch_form',
    'arcsearch_id'             => 'do_arcsearch_id',
    'arcsearch'                => 'do_arcsearch',
    'rebuildarc'               => 'do_rebuildarc',
    'rebuildallarc'            => 'do_rebuildallarc',
    'arc_download'             => 'do_arc_download',
    'arc_delete'               => 'do_arc_delete',
    'serveradmin'              => 'do_serveradmin',
    'set_loglevel'             => 'do_set_loglevel',
    'set_dumpvars'             => 'do_set_dumpvars',
    'show_sessions'            => 'do_show_sessions',
    'unset_dumpvars'           => 'do_unset_dumpvars',
    'set_session_email'        => 'do_set_session_email',
    'restore_email'            => 'do_restore_email',
    'skinsedit'                => 'do_skinsedit',
    'css'                      => 'do_css',
    'help'                     => 'do_help',
    'edit_list_request'        => 'do_edit_list_request',
    'edit_list'                => 'do_edit_list',
    'create_list_request'      => 'do_create_list_request',
    'create_list'              => 'do_create_list',
    'get_pending_lists'        => 'do_get_pending_lists',
    'get_closed_lists'         => 'do_get_closed_lists',
    'get_latest_lists'         => 'do_get_latest_lists',
    'get_inactive_lists'       => 'do_get_inactive_lists',
    'get_biggest_lists'        => 'do_get_biggest_lists',
    'set_pending_list_request' => 'do_set_pending_list_request',
    'install_pending_list'     => 'do_install_pending_list',
    'edit_config'              => 'do_edit_config',
    #XXX'submit_list' => 'do_submit_list',
    'editsubscriber'       => 'do_editsubscriber',
    'viewbounce'           => 'do_viewbounce',
    'redirect'             => 'do_redirect',
    'rename_list_request'  => 'do_rename_list_request',
    'rename_list'          => 'do_rename_list',
    'copy_list'            => 'do_copy_list',
    'reviewbouncing'       => 'do_reviewbouncing',
    'resetbounce'          => 'do_resetbounce',
    'scenario_test'        => 'do_scenario_test',
    'search_list'          => 'do_search_list',
    'search_list_request'  => 'do_search_list_request',
    'show_cert'            => 'do_show_cert',
    'close_list_request'   => 'do_close_list_request',
    'close_list'           => 'do_close_list',
    'purge_list'           => 'do_purge_list',
    'restore_list'         => 'do_restore_list',
    'upload_pictures'      => 'do_upload_pictures',
    'delete_pictures'      => 'do_delete_pictures',
    'd_read'               => 'do_d_read',
    'd_create_dir'         => 'do_d_create_dir',
    'd_upload'             => 'do_d_upload',
    'd_unzip'              => 'do_d_unzip',
    'd_editfile'           => 'do_d_editfile',
    'd_properties'         => 'do_d_properties',
    'd_overwrite'          => 'do_d_overwrite',
    'd_savefile'           => 'do_d_savefile',
    'd_describe'           => 'do_d_describe',
    'd_delete'             => 'do_d_delete',
    'd_rename'             => 'do_d_rename',
    'd_control'            => 'do_d_control',
    'd_change_access'      => 'do_d_change_access',
    'd_set_owner'          => 'do_d_set_owner',
    'd_admin'              => 'do_d_admin',
    'dump_scenario'        => 'do_dump_scenario',
    'dump'                 => 'do_dump',
    'arc_protect'          => 'do_arc_protect',
    'remind'               => 'do_remind',
    'change_email'         => 'do_change_email',
    'change_email_request' => 'do_change_email_request',
    'load_cert'            => 'do_load_cert',
    'compose_mail'         => 'do_compose_mail',
    'send_mail'            => 'do_send_mail',
    'request_topic'        => 'do_request_topic',
    'tag_topic_by_sender'  => 'do_tag_topic_by_sender',
    'search_user'          => 'do_search_user',
    'set_lang'             => 'do_set_lang',
    'attach'               => 'do_attach',
    'stats'                => 'do_stats',
    'viewlogs'             => 'do_viewlogs',
    'wsdl'                 => 'do_wsdl',
    'sync_include'         => 'do_sync_include',
    'review_family'        => 'do_review_family',
    'ls_templates'         => 'do_ls_templates',
    'remove_template'      => 'do_remove_template',
    'copy_template'        => 'do_copy_template',
    'view_template'        => 'do_view_template',
    'edit_template'        => 'do_edit_template',
    #'rss' => 'do_rss', #FIXME:Currently processed in differenct way.
    'rss_request'     => 'do_rss_request',
    'maintenance'     => 'do_maintenance',
    'blacklist'       => 'do_blacklist',
    'edit_attributes' => 'do_edit_attributes',
    'ticket'          => 'do_ticket',
    'manage_template' => 'do_manage_template',
    #XXX'send_newsletter' => 'do_send_newsletter',
    'suspend'                => 'do_suspend',
    'suspend_request'        => 'do_suspend_request',
    'suspend_request_action' => 'do_suspend_request_action',
    'show_exclude'           => 'do_show_exclude',
    # 'ca' stands for 'custom_action'. I used a short name to make it discrete
    # in a URL.
    'ca' => 'do_ca',
    # 'lca' stands for 'list_custom_action'. I used a short name to make it
    # discrete in a URL.
    'lca' => 'do_lca',
    'automatic_lists_management_request' =>
        'do_automatic_lists_management_request',
    'automatic_lists_management' => 'do_automatic_lists_management',
    'automatic_lists_request'    => 'do_automatic_lists_request',
    'automatic_lists'            => 'do_automatic_lists',
);

my %auth_action = (
    'logout'              => 1,
    'loginrequest'        => 1,
    'login'               => 1,
    'sso_login'           => 1,
    'sso_login_succeeded' => 1,
    'renewpasswd'         => 1,
    'firstpasswd'         => 1,
    'choosepasswd'        => 1,
    'sendssopasswd'       => 1,    #FIXME: currently not used
    'ticket'              => 1,
);

## Arguments awaited in the PATH_INFO, depending on the action
our %action_args = (
    'default'       => ['list'],
    'editfile'      => ['list', 'file', 'previous_action'],
    'requestpasswd' => ['email'],
    'choosepasswd'    => ['email', 'passwd'],
    'lists'           => ['topic', 'subtopic'],
    'latest_lists'    => ['topic', 'subtopic'],
    'active_lists'    => ['topic', 'subtopic'],
    'including_lists' => ['list'],
    'login' => ['email', 'passwd', 'previous_action', 'previous_list'],
    'sso_login' => ['auth_service_name', 'subaction', 'email', 'ticket'],
    'sso_login_succeeded' =>
        ['auth_service_name', 'previous_action', 'previous_list'],
    'loginrequest'     => ['previous_action', 'previous_list'],
    'logout'           => ['previous_action', 'previous_list'],
    'renewpasswd'      => ['previous_action', 'previous_list'],
    'firstpasswd'      => ['previous_action', 'previous_list'],
    'css'              => ['file'],
    'pref'             => ['previous_action', 'previous_list'],
    'reject'           => ['list',            'id'],
    'distribute'       => ['list',            'id'],
    'add_frommod'      => ['list',            'id'],
    'dump_scenario'    => ['list',            'pname'],
    'd_reject_shared'  => ['list',            'id'],
    'd_install_shared' => ['list',            'id'],
    'modindex'         => ['list'],
    'docindex'         => ['list'],
    'viewmod'     => ['list', 'id', '@file'],
    'viewfile'    => ['list', 'file'],
    'add'         => ['list', 'email'],
    'add_request' => ['list'],
    'del' => ['list', 'email'],
    'editsubscriber' =>
        ['list', 'email', 'previous_action', 'custom_attribute'],
#		'editsubscriber' => ['list','email','previous_action'],
    'viewbounce'  => ['list', 'email', '@file'],
    'resetbounce' => ['list', 'email'],
    'review'         => ['list', 'page',  'size', 'sortby'],
    'reviewbouncing' => ['list', 'page',  'size'],
    'arc'            => ['list', 'month', '@arc_file'],
    'latest_arc'     => ['list'],
    'arc_manage'     => ['list'],
    'arcsearch_form' => ['list', 'archive_name'],
    'arcsearch_id'   => ['list', 'archive_name', '@msgid'],
    'rebuildarc'     => ['list', 'month'],
    'rebuildallarc' => [],
    'arc_download'  => ['list'],
    'arc_delete'    => ['list', 'zip'],
    'home'          => [],
    'help'          => ['help_topic'],
    'show_cert'     => [],
    'subscribe'     => ['list', 'email', 'passwd'],
    #'subrequest' => ['list','email'],
    'subrequest' => ['list'],
    'subindex'   => ['list'],
    'ignoresub'  => ['list', '@email', '@gecos'],
    'signoff'    => ['list', 'email', 'passwd'],
    'auto_signoff'           => ['list',   'email'],
    'family_signoff'         => ['family', 'email'],
    'family_signoff_request' => ['family', 'email'],
    'sigrequest'             => ['list',   'email'],
    'sigindex'               => ['list'],
    'ignoresig'              => ['list',   '@email'],
    'set'                => ['list', 'email', 'reception', 'gecos'],
    'serveradmin'        => ['subaction'],
    'set_session_email'  => ['email'],
    'skinsedit'          => [],
    'get_pending_lists'  => [],
    'get_closed_lists'   => [],
    'get_latest_lists'   => [],
    'get_inactive_lists' => [],
    'get_biggest_lists'  => [],
    'search_list'        => ['filter_list'],
    'shared'          => ['list', '@path'],     #FIXME: no such function.
    'd_read'          => ['list', '@path'],
    'latest_d_read'   => ['list'],
    'd_admin'         => ['list', 'd_admin'],
    'd_delete'        => ['list', '@path'],
    'd_rename'        => ['list', '@path'],
    'd_create_dir'    => ['list', '@path'],
    'd_overwrite'     => ['list', '@path'],
    'd_savefile'      => ['list', '@path'],
    'd_describe'      => ['list', '@path'],
    'd_editfile'      => ['list', '@path'],
    'd_properties'    => ['list', '@path'],
    'd_control'       => ['list', '@path'],
    'd_change_access' => ['list', '@path'],
    'd_set_owner'     => ['list', '@path'],
    'dump'            => ['list', 'format'],
    'search'          => ['list', 'filter'],
    'search_user'     => ['email'],
    'set_lang'        => ['lang'],
    'attach' => ['list', 'dir', 'file'],
    'stats'  => ['list'],
    'edit_list_request' => ['list', 'group'],
    'rename_list'       => ['list', 'new_list', 'new_robot'],
    'copy_list'         => ['list', 'new_list', 'new_robot'],
    'redirect'        => [],
    'viewlogs'        => ['list', 'page', 'size', 'sortby'],
    'wsdl'            => [],
    'sync_include'    => ['list'],
    'review_family'   => ['family_name'],
    'ls_templates'    => ['list'],
    'view_template'   => [],
    'remove_template' => [],
    'copy_template'   => ['list'],
    'edit_template'   => ['list'],
    'rss_request'     => ['list'],
    'request_topic'       => ['list', 'authkey'],
    'tag_topic_by_sender' => ['list'],
    'multiple_subscribe'  => ['lists'],
    'multiple_signoff'    => ['lists'],
    'ticket'              => ['ticket'],
    'change_email'        => ['email'],
    'manage_template' => ['subaction', 'list', 'message_template'],
    'send_newsletter' => [],
    'compose_mail'    => ['list',          'subaction'],
    'suspend'         => ['list'],
    'suspend_request' => ['subaction'],
    'show_exclude'    => ['list'],
    'ca'              => ['custom_action', '@cap'],
    'lca'                                => ['custom_action', 'list', '@cap'],
    'automatic_lists_management_request' => [],
    'automatic_lists_management'         => [],
    'automatic_lists_request'            => ['family'],
    'automatic_lists'                    => [],
);

## Define the required parameters for each action
## Parameter names refer to the %in structure of to $param if mentionned as
## 'param.x'
## This structure is used to determine if any parameter is missing
## The list of parameters is not ordered
## Some keywords are reserved: param.list and param.user.email
## Alternate parameters can be defined with the '|' character
## Limits of this structure: it does not define optional parameters (a or b)
## Limit: it does not allow to have a specific error message and redirect to a
## given page if the parameter is missing
our %required_args = (
    'active_lists'            => ['for|count'],
    'admin'                   => ['param.list', 'param.user.email'],
    'add'                     => ['param.list', 'param.user.email'],
    'add_request'             => ['param.list', 'param.user.email'],
    'arc'                     => ['param.list'],
    'arc_delete'              => ['param.user.email', 'param.list'],
    'arc_download'            => ['param.user.email', 'param.list'],
    'arc_manage'              => ['param.list'],
    'arc_protect'             => ['param.list'],
    'arcsearch'               => ['param.list'],
    'arcsearch_form'          => ['param.list'],
    'arcsearch_id'            => ['param.list'],
    'automatic_lists_request' => ['family'],
    'automatic_lists'         => ['family'],
    'attach'                  => ['param.list'],
    'blacklist'               => ['param.list'],
    'change_email'            => ['param.user.email'],
    'change_email_request'    => ['param.user.email', 'new_email'],
    'close_list'              => ['param.user.email', 'param.list'],
    'close_list_request'      => ['param.user.email', 'param.list'],
    'compose_mail'            => ['param.user.email', 'param.list'],
    'copy_template'           => ['webormail'],
    ## other required parameters are checked in the subroutine
    'create_list'         => ['param.user.email'],
    'create_list_request' => ['param.user.email'],
    'css'                 => [],
    'd_admin'             => ['param.list', 'param.user.email'],
    'd_change_access'     => ['param.list', 'param.user.email'],
    'd_control'           => ['param.list', 'param.user.email'],
    'd_create_dir'        => ['param.list', 'param.user.email', 'name_doc'],
    'd_delete'         => ['param.list', 'param.user.email'],
    'd_describe'       => ['param.list', 'param.user.email', 'content'],
    'd_editfile'       => ['param.list', 'param.user.email'],
    'd_install_shared' => ['param.list', 'param.user.email', 'id'],
    'd_overwrite'      => ['param.list', 'param.user.email'],
    'd_properties'     => ['param.list', 'param.user.email'],
    'd_read'          => ['param.list'],
    'd_reject_shared' => ['param.list', 'param.user.email', 'id'],
    'd_rename'        => ['param.list', 'param.user.email', 'new_name'],
    'd_savefile'      => ['param.list', 'param.user.email', 'content|url'],
    'd_set_owner'     => ['param.list', 'param.user.email'],
    'd_unzip'         => ['param.list', 'param.user.email'],
    'd_upload'        => ['param.list', 'param.user.email'],
    'del'             => ['param.list', 'param.user.email', 'email'],
    'delete_pictures' => ['param.list', 'param.user.email'],
    'distribute'      => ['param.list', 'param.user.email', 'id|idspam'],
    'add_frommod'     => ['param.list', 'param.user.email', 'id'],
    'dump'               => ['param.list'],
    'dump_scenario'      => ['param.list', 'pname'],
    'edit_list'          => ['param.user.email', 'param.list'],
    'edit_list_request'  => ['param.user.email', 'param.list'],
    'edit_template'      => ['webormail'],
    'editfile'           => ['param.user.email'],
    'editsubscriber'     => ['param.list', 'param.user.email', 'email'],
    'get_closed_lists'   => ['param.user.email'],
    'get_inactive_lists' => ['param.user.email'],
    'get_latest_lists'   => ['param.user.email'],
    'get_biggest_lists'  => ['param.user.email'],
    'get_pending_lists'  => ['param.user.email'],
    'ignoresig'            => ['param.list', 'param.user.email'],
    'ignoresub'            => ['param.list', 'param.user.email'],
    'including_lists'      => ['param.list', 'param.user.email'],
    'info'                 => ['param.list'],
    'install_pending_list' => ['param.user.email'],
    'edit_config'          => ['param.user.email'],
    'latest_arc'           => ['param.list', 'for|count'],
    'latest_d_read' => ['param.list', 'for', 'count'],
    'latest_lists'  => ['for|count'],
    'load_cert'     => ['param.list'],
    'logout'        => ['param.user.email'],
    'manage_template'    => ['param.list',       'param.user.email'],
    'modindex'           => ['param.list',       'param.user.email'],
    'docindex'           => ['param.list',       'param.user.email'],
    'multiple_subscribe' => ['param.list'],
    'pref'               => ['param.user.email'],
    'purge_list'         => ['param.user.email', 'selected_lists'],
    'rebuildallarc'      => ['param.user.email'],
    'rebuildarc'         => ['param.user.email', 'param.list'],
    'reject'          => ['param.list', 'param.user.email', 'id|idspam'],
    'remind'          => ['param.list', 'param.user.email'],
    'remove_arc'      => ['param.list'],
    'remove_template' => ['webormail'],
    'rename_list' =>
        ['param.user.email', 'param.list', 'new_listname', 'new_robot'],
    'copy_list' =>
        ['param.user.email', 'param.list', 'new_listname', 'new_robot'],
    'rename_list_request' => ['param.user.email', 'param.list'],
    'request_topic'       => ['param.list',       'authkey'],
    'resetbounce'  => ['param.list',       'param.user.email', 'email'],
    'restore_list' => ['param.user.email', 'param.list'],
    'review'          => ['param.list'],
    'review_family'   => ['param.user.email', 'family_name'],
    'reviewbouncing'  => ['param.list'],
    'rss_request'     => [],
    'savefile'        => ['param.user.email', 'file'],
    'search'          => ['param.list', 'filter'],
    'search_user'     => ['param.user.email', 'email'],
    'send_mail'       => ['param.user.email'],
    'send_newsletter' => ['param.list', 'param.user.email', 'url'],
    'send_me'         => ['param.list'],
    'view_source'     => ['param.list'],
    'tracking'        => ['param.list'],
    'requestpasswd'   => ['email'],
    'serveradmin'     => ['param.user.email'],
    'set'      => ['param.list', 'reception|visibility'],
    'set_lang' => [],
    'set_pending_list_request' => ['param.user.email'],
    'setpasswd' => ['param.user.email', 'newpasswd1', 'newpasswd2'],
    'setpref'   => ['param.user.email'],
    'sigindex'               => ['param.list',       'param.user.email'],
    'signoff'                => ['param.list'],
    'sigrequest'             => ['param.list'],
    'skinsedit'              => ['param.user.email'],
    'sso_login'              => ['auth_service_name'],
    'stats'                  => ['param.list'],
    'subindex'               => ['param.list',       'param.user.email'],
    'suboptions'             => ['param.list',       'param.user.email'],
    'subrequest'             => ['param.list'],
    'subscribe'              => ['param.list'],
    'subscriber_count'       => ['param.list'],
    'suspend'                => ['param.list',       'param.user.email'],
    'suspend_request'        => [],
    'suspend_request_action' => [],
    'show_exclude'           => ['param.list'],
    'sync_include'           => ['param.list',       'param.user.email'],
    'tag_topic_by_sender'    => ['param.list'],
    'upload_pictures'        => ['param.user.email', 'param.list'],
    'view_template'          => ['webormail'],
    'viewbounce'             => ['param.list',       'email'],
    'viewfile'               => ['file',             'param.list'],
    'viewlogs'               => ['param.list'],
    'viewmod' => ['param.list', 'param.user.email', 'id|idspam'],
    'wsdl'    => [],
    'which'   => ['param.user.email'],
);

## Defines the required privileges to access privileged actions
## You can define a set ofequiivalent privileges in the ARRAYREF
our %required_privileges = (
    'admin'              => ['owner',  'editor'],
    'arc_delete'         => ['owner'],
    'arc_download'       => ['owner'],
    'arc_manage'         => ['owner'],
    'blacklist'          => ['owner',  'editor'],
    'close_list'         => ['privileged_owner'],
    'close_list_request' => ['privileged_owner'],
    'copy_template'      => ['listmaster'],
    'd_install_shared'   => ['editor', 'owner'],
    'd_reject_shared'    => ['editor', 'owner'],
    'distribute'        => ['editor', 'owner', 'listmaster'],
    'add_frommod'       => ['editor', 'owner'],
    'dump_scenario'     => ['listmaster'],
    'edit_list'         => ['owner'],
    'edit_list_request' => ['owner'],
    'edit_template'     => ['listmaster'],
    'editsubscriber'       => ['owner', 'editor'],
    'get_closed_lists'     => ['listmaster'],
    'get_inactive_lists'   => ['listmaster'],
    'get_latest_lists'     => ['listmaster'],
    'get_biggest_lists'    => ['listmaster'],
    'get_pending_lists'    => ['listmaster'],
    'ignoresig'            => ['owner', 'editor'],
    'ignoresub'            => ['owner', 'editor'],
    'including_lists'      => ['owner', 'listmaster'],
    'install_pending_list' => ['listmaster'],
    'edit_config'          => ['listmaster'],
    'ls_templates'         => ['listmaster'],
    'manage_template'      => ['owner'],
    'modindex'        => ['editor',           'owner', 'listmaster'],
    'docindex'        => ['editor',           'owner', 'listmaster'],
    'purge_list'      => ['privileged_owner', 'listmaster'],
    'rebuildallarc'   => ['listmaster'],
    'rebuildarc'      => ['listmaster'],
    'reject'          => ['editor',           'owner', 'listmaster'],
    'remove_template' => ['listmaster'],
    'rename_list'     => ['privileged_owner'],
    'copy_list'                => ['owner', 'listmaster'],
    'rename_list_request'      => ['privileged_owner'],
    'resetbounce'              => ['owner', 'editor'],
    'restore_list'             => ['listmaster'],
    'review_family'            => ['listmaster'],
    'reviewbouncing'           => ['owner', 'editor'],
    'search_user'              => ['listmaster'],
    'serveradmin'              => ['listmaster'],
    'set_dumpvars'             => ['listmaster'],
    'set_loglevel'             => ['listmaster'],
    'set_pending_list_request' => ['listmaster'],
    'set_session_email'        => ['listmaster'],
    'show_sessions'            => ['listmaster'],
    'sigindex'                 => ['owner', 'editor'],
    'stats'                    => ['owner'],
    'subindex'                 => ['owner', 'editor'],
    'sync_include'             => ['owner', 'editor'],
    'skinsedit'                => ['listmaster'],
    'view_template'            => ['listmaster'],
    'viewbounce'               => ['owner', 'editor'],
    'viewlogs'                 => ['owner', 'editor'],
    'viewmod' => ['editor', 'owner', 'listmaster'],
    'automatic_lists_management_request' => ['listmaster'],
    'automatic_lists_management'         => ['listmaster'],
);

# this definition is used to choose the left side menu type (admin ->
# listowner admin menu | serveradmin -> server_admin menu | none list or
# your_list menu)
my %action_type = (
    'review'      => 'admin',
    'search'      => 'admin',
    'viewfile'    => 'admin',
    'admin'       => 'admin',
    'add_request' => 'admin',
    'add'         => 'admin',
    'del'         => 'admin',
    # 'modindex' =>'admin',
    'reject'             => 'admin',
    'reject_notify'      => 'admin',
    'distribute'         => 'admin',
    'add_frommod'        => 'admin',
    'viewmod'            => 'admin',
    'savefile'           => 'admin',
    'rebuildallarc'      => 'admin',    #FIXME: serveradmin?
    'reviewbouncing'     => 'admin',
    'edit_list_request'  => 'admin',
    'edit_list'          => 'admin',
    'editsubscriber'     => 'admin',
    'viewbounce'         => 'admin',
    'resetbounce'        => 'admin',
    'scenario_test'      => 'admin',
    'close_list_request' => 'admin',
    'close_list'         => 'admin',
    'restore_list'       => 'admin',
    'd_admin'            => 'admin',
    'd_reject_shared'    => 'admin',
    'd_install_shared'   => 'admin',
    'dump_scenario'      => 'admin',
    'dump'               => 'admin',
    'remind'             => 'admin',
    #'subindex' => 'admin',
    'stats'                              => 'admin',
    'ignoresig'                          => 'admin',
    'ignoresub'                          => 'admin',
    'rename_list'                        => 'admin',
    'copy_list'                          => 'admin',
    'rename_list_request'                => 'admin',
    'arc_manage'                         => 'admin',
    'sync_include'                       => 'admin',
    'view_template'                      => 'admin',
    'remove_template'                    => 'admin',
    'copy_template'                      => 'admin',
    'edit_template'                      => 'admin',
    'blacklist'                          => 'admin',
    'viewlogs'                           => 'admin',
    'serveradmin'                        => 'serveradmin',
    'get_pending_lists'                  => 'serveradmin',
    'get_closed_lists'                   => 'serveradmin',
    'get_inactive_lists'                 => 'serveradmin',
    'get_latest_lists'                   => 'serveradmin',
    'get_biggest_lists'                  => 'serveradmin',
    'ls_templates'                       => 'serveradmin',
    'skinsedit'                          => 'serveradmin',
    'review_family'                      => 'serveradmin',
    'search_user'                        => 'serveradmin',
    'show_sessions'                      => 'serveradmin',
    'show_exclude'                       => 'admin',
    'rebuildarc'                         => 'serveradmin',
    'set_session_email'                  => 'serveradmin',
    'set_loglevel'                       => 'serveradmin',
    'editfile'                           => 'serveradmin',    #FIXME: admin?
    'unset_dumpvars'                     => 'serveradmin',
    'set_dumpvars'                       => 'serveradmin',
    'automatic_lists_management_request' => 'serveradmin',
    'automatic_lists_management'         => 'serveradmin',
);

## actions tthat are not used in return of login,
my %temporary_actions = (
    'logout'              => 1,
    'loginrequest'        => 1,
    'login'               => 1,
    'sso_login'           => 1,
    'sso_login_succeeded' => 1,
    'ticket'              => 1,
    'css'                 => 1,
    'rss'                 => 1,    # FIXME:currently not used.
    'ajax'                => 1,
    'wsdl'                => 1,
    'redirect'            => 1,
);

## Regexp applied on incoming parameters (%in)
## The aim is not a strict definition of parameter format
## but rather a security check
our %in_regexp = (
    ## Default regexp
    '*' => '[\w\-\.]+',

    ## List config parameters
    'single_param'   => '.+',
    'multiple_param' => '.+',

    ## Textarea content
    'template_content'     => '.+',
    'content'              => '.+',
    'body'                 => '.+',
    'info'                 => '.+',
    'new_scenario_content' => '.+',
    'blacklist'            => '.*',

    ## Integer
    'page' => '\d+',
    'size' => '\d+',

    ## Free data
    'subject'          => '.*',
    'gecos'            => '[^<>\\\*\$\n]+',
    'fromname'         => '[^<>\\\*\$\n]+',
    'additional_field' => '[^<>\\\*\$\n]+',
    'dump'             => '[^<>\\\*\$]+',     # contents email + gecos

    ## Search
    'filter'      => '.*',                    # search subscriber
    'filter_list' => '.*',                    # search list
    'key_word'    => '.*',
    'format'      => '[^<>\\\$\n]+',          # dump format/filter string

    ## File names
    'file'          => '[^<>\*\$\n]+',
    'template_path' => '[\w\-\.\/_]+',
    'arc_file'      => '[^<>\\\*\$\n]+',
    'path'          => '[^<>\\\*\$\n]+',
    'uploaded_file' =>
        '(.*[\/\\\\])?[^<>\*\$\n]+',          # Could be precised (use of "'")
    'unzipped_file'     => '(.*[\/\\\\])?[^<>\*\$\n]+',
    'dir'               => '[^<>\\\*\$\n]+',
    'name_doc'          => '[^<>\\\*\$\[\]\/\n]+',
    'shortname'         => '[^<>\\\*\$\n]+',
    'new_name'          => '[^<>\\\*\$\n]+',
    'id'                => '[^<>\\\*\$\n]+',
    'template_name'     => Sympa::Regexps::template_name(),
    'new_template_name' => Sympa::Regexps::template_name(),
    'message_template'  => Sympa::Regexps::template_name(),
    'new_default'       => Sympa::Regexps::template_name(),

    ## Archives
    ## format is yyyy-mm for 'arc' and mm for 'send_me'
    'month' => '\d{2}|\d{4}\-\d{2}',

    ## URL
    'referer'         => '[^\\\$\*\"\'\`\^\|\<\>\n]+',
    'failure_referer' => '[^\\\$\*\"\'\`\^\|\<\>\n]+',
    'url'             => '[^\\\$\*\"\'\`\^\|\<\>\n]+',

    ## Msg ID
    'msgid'       => '[^\\\*\"\'\`\^\|\n]+',
    'in_reply_to' => '[^\\\*\"\'\`\^\|\n]+',
    'message_id'  => '[^\\\*\"\'\`\^\|\n]+',

    ## Password
    'passwd'       => '.+',
    'password'     => '.+',
    'newpasswd1'   => '.+',
    'newpasswd2'   => '.+',
    'new_password' => '.+',

    ## Topics
    'topic'    => '\@?[\-\w\/]+',
    'topics'   => '[\-\w\/]+',
    'subtopic' => '[\-\w\/]+',

    ## List names
    'list' => '[\w\-\.\+]*',    ## Sympa::Regexps::listname() + uppercase
    'previous_list'  => '[\w\-\.\+]*',
    'new_list'       => '[\w\-\.\+]*',
    'listname'       => '[\w\-\.\+]*',
    'new_listname'   => '[\w\-\.\+]*',
    'selected_lists' => '[\w\-\.\+]*',

    ## Family names
    'family_name' => Sympa::Regexps::family_name(),
    'family'      => Sympa::Regexps::family_name(),

    # Email addresses
    'email'      => Sympa::Regexps::email() . '|' . Sympa::Regexps::uid(),
    'init_email' => Sympa::Regexps::email(),
    'old_email'  => Sympa::Regexps::email(),
    'new_email'  => Sympa::Regexps::email(),
    'sender'     => Sympa::Regexps::email(),
    'fromaddr'   => Sympa::Regexps::email(),
    'to' => '(([\w\-\_\.\/\+\=\']+|\".*\")\s[\w\-]+(\.[\w\-]+)+(,?))*',
    'automatic_list_part_*' => '[\w\-\.\+]*',

    ## Host
    'new_robot'   => Sympa::Regexps::host(),
    'remote_host' => Sympa::Regexps::host(),
    'remote_addr' => Sympa::Regexps::host(),

    ## Scenario name
    'scenario'    => Sympa::Regexps::scenario(),
    'read_access' => Sympa::Regexps::scenario(),
    'edit_access' => Sympa::Regexps::scenario(),
    ## RSS URL or blank
    'active_lists'  => '.*',
    'latest_lists'  => '.*',
    'latest_arc'    => '.*',
    'latest_d_read' => '.*',

    ##Logs
    'target_type' => '[\w\-\.\:]*',
    'target'      => Sympa::Regexps::email(),
    'date_from'   => '[\d\/\-]+',
    'date_to'     => '[\d\/\-]+',
    'ip'          => Sympa::Regexps::host(),

    ## colors
    'subaction_test'      => '.*',
    'subaction_reset'     => '.*',
    'subaction_install'   => '.*',
    'custom_color_value'  => '\#[0-9a-fA-F]+',
    'custom_color_number' => 'color_\w+',

    ## Custom attribute
    'custom_attribute' => '.*',

    ## Templates
    'scope' => 'distrib|robot|family|list|site',

    ## Custom Inputs from create_list_request.tt2
    'custom_input' => '.*',

    ## conf parameters
    'conf_new_value' => '.*',

    ## custom actions
    'cap'  => '.*',
    'lcap' => '.*',

    'plugin' => '.*',

    ## Envelope ID
    'envid' => '\w+',

    ## Authentication/moderation key
    'authkey' => '\w+',
);

## Regexp applied on incoming parameters (%in)
## This regular expression defines forbidden expressions applied on all
## incoming parameters
## Note that you can use the ^ and $ expressions to match beginning and ending
## of expressions
our %in_negative_regexp = ('arc_file' => '^(arctxt|\.)');

## List some required filtering of incoming parameters, depending on current
## action
## Paramater can be '*' or 'param*'
## Like Q-encoding
my %filtering = (
    'd_reject_shared'  => {'id'       => 'qencode'},
    'd_install_shared' => {'id'       => 'qencode'},
    'd_read'           => {'path'     => 'qencode'},
    'd_create_dir'     => {'name_doc' => 'qencode', 'path' => 'qencode'},
    'd_upload'         => {'path'     => 'qencode'},
    'd_unzip'          => {'path'     => 'qencode'},
    'd_editfile'       => {'path'     => 'qencode'},
    'd_properties'     => {'path'     => 'qencode'},
    'd_overwrite'      => {'path'     => 'qencode'},
    'd_savefile'       => {'path'     => 'qencode', 'name_doc' => 'qencode'},
    'd_describe'       => {'path'     => 'qencode'},
    'd_delete'         => {'path'     => 'qencode'},
    'd_rename'         => {'path'     => 'qencode', 'new_name' => 'qencode'},
    'd_control'        => {'path'     => 'qencode'},
    'd_change_access'  => {'path'     => 'qencode'},
    'd_set_owner'      => {'path'     => 'qencode'},
    'requestpasswd'    => {'email'    => 'fix_escape_uri'},
    'viewbounce'       => {'email'    => 'fix_escape_uri'},
    'editsubscriber'   => {'email'    => 'fix_escape_uri'},
    ## Required because outgoing parameters have been html-escaped in
    ## edit_list_request
    'edit_list' => {'*param*' => 'unescape_html'},
    ## Remove leading/trailing white spaces and lowercase
    'change_email' => {'*email' => 'normalize'},
);

## Set locale configuration
my $language = Sympa::Language->instance;
$language->set_lang($Conf::Conf{'lang'}, 'en');

# Important to leave this there because it defined defaults for
# user_data_source
#FIXME: Is it really required?
Sympa::DatabaseManager->instance;

# Now, load all components which are implemented as plugins.  Those will
# add fields to above configuration and may do database upgrades.
$plugins->start if $plugins;

## Check that the data structure is uptodate
## If not, set the web interface to maintenance mode
my $maintenance_mode;
unless (Conf::data_structure_uptodate()) {
    $maintenance_mode = 1;
    $log->syslog('err',
        'WWSympa set to maintenance mode; you should run sympa.pl --upgrade');
} elsif (Conf::cookie_changed()) {
    $maintenance_mode = 1;
    $log->syslog('err',
        'WWSympa set to maintenance mode; sympa.conf/cookie parameter has changed.'
    );
}

# Update main CSS if required.
Sympa::Tools::WWW::update_css();

our %changed_params;

our %in;
my $query;

my $birthday = time;

my $bulk = Sympa::Bulk->new;

$log->syslog('info', 'WWSympa started, process %d', $PID);

# Now internal encoding is same as input/output.
#XXX## Set output encoding
#XXX## All outgoing strings will be recoded transparently using this charset
#XXXbinmode STDOUT, ":utf8";

#XXX## Incoming data is utf8-encoded
#XXXbinmode STDIN, ":utf8";

## Main loop
my $loop_count;
my $start_time = time;
while ($query = new_loop()) {

    undef %::changed_params;

    undef $param;
    undef $list;
    undef $robot;
    undef $robot_object;
    undef $pinfo;
    undef $ip;
    undef $rss;
    undef $ajax;
    undef $session;

    $log->{level} = $Conf::Conf{'log_level'};
    $language->set_lang(Sympa::best_language('*'));

    ## Empty cache of the List.pm module
    Sympa::List::init_list_cache();

    # Process grouped notifications
    Sympa::Alarm->instance->flush;

    ## Check effective ID
    unless ($EUID eq (getpwnam(Sympa::Constants::USER))[2]) {
        $maintenance_mode = 1;
        Sympa::Report::reject_report_web('intern_quiet',
            'incorrect_server_config', {}, '', '');
        wwslog(
            'err',
            'Config error: WWSympa should run with UID %s (instead of %s). *** Switching to maintenance mode. ***',
            (getpwnam(Sympa::Constants::USER))[2],
            $EUID
        );
    }

    ## We set the real UID with the effective UID value
    ## It is useful to allow execution of scripts like alias_manager
    ## that otherwise might loose the benefit of SetUID
    $UID = $EUID;    ## UID
    $GID = $EGID;    ## GID

    unless (Sympa::DatabaseManager->instance) {
        Sympa::Report::reject_report_web('system_quiet', 'no_database', {},
            '', '');
        $log->syslog('info', 'WWSympa requires a RDBMS to run');
    }

    ## If in maintenance mode, check if the data structure is now uptodate
    if (    $maintenance_mode
        and Conf::data_structure_uptodate()
        and not Conf::cookie_changed()
        and ($EUID eq (getpwnam(Sympa::Constants::USER))[2])) {
        $maintenance_mode = undef;
        $log->syslog('notice',
            "Data structure seem updated, setting OFF maintenance mode");
    }

    ## Generate traceback if crashed.
    ## Though I don't know why, __DIE__ handler is cleared after INIT.
    Sympa::Crash::register_handler();

    ## Get params in a hash
    %in = $query->Vars;

    foreach my $k (keys %::changed_params) {
        $log->syslog('debug3', 'Changed Param: %s', $k);
    }

    ## Parse CGI parameters
    #    &CGI::ReadParse();

    # Determin robot.
    # N.B. As of 6.2.15, the http_host parameter will match with the host name
    # and path locally detected by server.  If remotely detected host name
    # and / or path should be differ, the proxy must adjust them.
    my $server_name = Sympa::Tools::WWW::get_server_name() || '';
    if ($server_name
        and my $hostmap = $Conf::Conf{'robot_by_http_host'}->{$server_name}) {
        my $selected_robot;
        my $selected_path = '';

        my $request_uri = $ENV{'REQUEST_URI'} || '';
        while (my ($path, $v) = each %$hostmap) {
            if (0 == index $request_uri, $path) {    #FIXME:percent-decode
                # Longer path wins.
                if (length $selected_path < length $path) {
                    ($selected_robot, $selected_path) = ($v, $path);
                }
            }
        }
        $robot = $selected_robot;
    }
    $robot = $Conf::Conf{'host'} unless $robot;      #FIXME:Use domain.

    # Not yet implemented
    ### Create Robot object
    #$robot_object = new Robot $robot;
    $pinfo = Sympa::Robot::list_params($robot);

    ## Default robot
    if ($robot eq $Conf::Conf{'host'}) {             #FIXME:Use domain.
        $param->{'default_robot'} = 1;
    }

    $ip = $ENV{'REMOTE_HOST'} || $ENV{'REMOTE_ADDR'} || 'undef';

    # Determin cookie domain.
    # In case HTTP_HOST does not match cookie_domain, use former.
    # N.B. As of 6.2.15, the cookie domain will match with the host name
    # locally detected by server.  If remotely detected name should be differ,
    # the proxy must adjust it.
    my $cookie_domain = Conf::get_robot_conf($robot, 'cookie_domain');
    my $http_host = Sympa::Tools::WWW::get_http_host() || '';
    $http_host =~ s/:\d+$//;    # Suppress port.
    unless (substr($http_host, -length($cookie_domain)) eq lc $cookie_domain
        or $cookie_domain eq 'localhost') {
        wwslog('notice',
            '(%s) Does NOT match HTTP_HOST; setting cookie_domain to %s',
            $cookie_domain, $http_host);
        $cookie_domain = $http_host;
    }
    $param->{'cookie_domain'} = $cookie_domain;

    $log->{level} = Conf::get_robot_conf($robot, 'log_level');

    ## Sympa parameters in $param->{'conf'}
    $param->{'conf'} = {};
    foreach my $p (
        'email',
        'host',
        'sympa',
        'request',
        'soap_url',
        'wwsympa_url',
        'listmaster_email',
        'logo_html_definition',
        'favicon_url',
        'main_menu_custom_button_1_url',
        'main_menu_custom_button_1_title',
        'main_menu_custom_button_1_target',
        'main_menu_custom_button_2_url',
        'main_menu_custom_button_2_title',
        'main_menu_custom_button_2_target',
        'main_menu_custom_button_3_url',
        'main_menu_custom_button_3_title',
        'main_menu_custom_button_3_target',
        'static_content_url',
        'dark_color',
        'light_color',
        'text_color',
        'bg_color',
        'error_color',
        'use_blacklist',
        'antispam_feature',
        'custom_robot_parameter',
        'selected_color',
        'shaded_color',
        'color_0',
        'color_1',
        'color_2',
        'color_3',
        'color_4',
        'color_5',
        'color_6',
        'color_7',
        'color_8',
        'color_9',
        'color_10',
        'color_11',
        'color_12',
        'color_13',
        'color_14',
        'color_15',
        'reporting_spam_script_path',
        'automatic_list_families',
        'spam_protection',
        ) {

        $param->{'conf'}{$p} = Conf::get_robot_conf($robot, $p);
        $param->{$p} = Conf::get_robot_conf($robot, $p)
            if $p =~ /_color\z/
                or $p =~ /\Acolor_/
                or $p =~ /_url\z/;
    }

    foreach my $auth (keys %{$Conf::Conf{'cas_id'}{$robot}}) {
        $log->syslog('debug2', 'CAS authentication service %s', $auth);
        $param->{'sso'}{$auth} =
            $Conf::Conf{'cas_id'}{$robot}{$auth}
            {'auth_service_friendly_name'};
    }

    foreach my $auth (keys %{$Conf::Conf{'generic_sso_id'}{$robot}}) {
        $log->syslog('debug', 'Generic SSO authentication service %s', $auth);
        $param->{'sso'}{$auth} =
            $Conf::Conf{'auth_services'}{$robot}
            [$Conf::Conf{'generic_sso_id'}{$robot}{$auth}]{'service_name'};
    }

    $param->{'sso_number'} =
        $Conf::Conf{'cas_number'}{$robot} +
        $Conf::Conf{'generic_sso_number'}{$robot};
    $param->{'use_passwd'} = $Conf::Conf{'use_passwd'}{$robot};
    $param->{'use_sso'} = 1 if ($param->{'sso_number'});
    $param->{'authentication_info_url'} =
        $Conf::Conf{'authentication_info_url'}{$robot};
    $param->{'wwsconf'} = Conf::_load_wwsconf;    #FXIME: no longer used?

    $param->{'version'} = Sympa::Constants::VERSION;
    $param->{'date'} =
        $language->gettext_strftime("%d %b %Y at %H:%M:%S", localtime time);
    $param->{'time'} =
        $language->gettext_strftime("%H:%M:%S", localtime time);

    ## Hash defining the parameters where no control is performed (because
    ## they are supposed to contain html and/or javascript).
    $param->{'htmlAllowedParam'} = {
        #'hidden_head'          => 1,
        #'hidden_end'           => 1,
        #'hidden_at'            => 1,
        'selected'             => 1,
        'logo_html_definition' => 1,
        'template_content'     => 1,
        'html_dumpvars'        => 1,
        'html_editor_init'     => 1,
        'html_content'         => 1,
    };
    ## Hash defining the parameters where HTML must be filtered.
    $param->{'htmlToFilter'} = {
        'homepage_content' => 1,
        'info_content'     => 1,
    };

    ## Change to list root
    unless (chdir $Conf::Conf{'home'}) {
        Sympa::Report::reject_report_web('intern', 'chdir_error', {}, '', '',
            '', $robot);
        wwslog('info', 'Unable to change directory');
        exit -1;
    }

    ## Sets the UMASK
    umask(oct($Conf::Conf{'umask'}));

    ## Authentication
    ## use https client certificate information if define.

    ## Default auth method (for scenarios)
    $param->{'auth_method'} = 'md5';

    Sympa::Report::init_report_web();

    ## Get PATH_INFO parameters
    get_parameters($robot);

    # Propagate plugins parameters
    $param->{'plugin'} = $in{'plugin'};

    # # Static content
    # $param->{'static_content_url'} =
    #     Conf::get_robot_conf($robot, 'static_content_url');

    ## CSS related
    $param->{'css_path'} = Conf::get_robot_conf($robot, 'css_path');
    $param->{'css_url'}  = Conf::get_robot_conf($robot, 'css_url');
    ## If CSS file not found, let Sympa do the job...
    unless ($param->{'css_path'} and -f $param->{'css_path'} . '/style.css') {
        wwslog(
            'err',
            'Could not find CSS file %s, using default CSS',
            $param->{'css_path'} . '/style.css'
        ) if $param->{'css_path'};    # Notice only if path was defined.
        $param->{'css_url'} =
            Sympa::get_url($robot, 'css', authority => 'omit');
    }

    wwslog(
        'info',
        'Parameter css_url "%s" seems strange, it must be the url of a directory not a css file',
        $param->{'css_url'}
    ) if $param->{'css_url'} =~ /\.css$/;

    $session = Sympa::Session->new(
        $robot,
        {   'cookie' =>
                Sympa::Session::get_session_cookie($ENV{'HTTP_COOKIE'}),
            'action' => $in{'action'},
            'rss'    => $rss,
            'ajax'   => $ajax
        }
    );

    # Getting rid of the environment variable to make sure it won't be
    # affected to another anonymous session.
    undef $ENV{'HTTP_COOKIE'};
    unless (defined $session) {
        Sympa::send_notify_to_listmaster($robot,
            'failed_to_create_web_session', {});
        wwslog('info', 'Failed to create session');
        $session = Sympa::Session->new($robot, {});
    }

    # Retreive oauth_token from session if it exists and if we have a (newly)
    # identified user
    my $oauth_token = $session->{'oauth_token'} || '';
    $in{'oauth_token'} = delete $session->{'oauth_token'}
        if $oauth_token ne '' && $session->{'email'} ne 'nobody';

    $session->{'is_family_owner'} = undef;
    my $automatic_list_families =
        Conf::get_robot_conf($robot, 'automatic_list_families');
    if (defined $automatic_list_families) {
        foreach my $key (keys %{$automatic_list_families}) {
            my $family;
            if ($family = Sympa::Family->new($key, $robot)) {
                if ($family->is_allowed_to_create_automatic_lists(
                        (   'auth_level' => 'md5',
                            'sender'     => $session->{'email'},
                            'message'    => undef,
                            'listname'   => ''
                        )
                    )
                    ) {
                    $session->{'is_family_owner'}{$key} = 1;
                } else {
                    $session->{'is_family_owner'}{$key} = undef;
                }
            }
        }
    }

    $param->{'session'} = $session->as_hashref();

    $log->{level} = $session->{'log_level'} if ($session->{'log_level'});
    $param->{'restore_email'}         = $session->{'restore_email'};
    $param->{'dumpvars'}              = $session->{'dumpvars'};
    $param->{'unauthenticated_email'} = $session->{'unauthenticated_email'};

    ## testing custom CSS
    if ($session->{'custom_css'}) {
        foreach my $i (0 .. 15) {
            $param->{'color_' . $i} = $session->{'color_' . $i}
                if $session->{'color_' . $i};
        }
    }

    ## RSS does not require user authentication
    unless ($rss) {
        if (    $ENV{'SSL_CLIENT_VERIFY'} eq 'SUCCESS'
            and $in{'action'} ne 'sso_login') {
            # Do not check client certificate automatically if in sso_login

            $log->syslog(
                'debug2',
                'SSL verified, S_EMAIL = %s, " . " S_DN_Email = %s',
                $ENV{'SSL_CLIENT_S_EMAIL'},
                $ENV{'SSL_CLIENT_S_DN_Email'}
            );
            if (($ENV{'SSL_CLIENT_S_EMAIL'})) {
                # this is the X509v3 SubjectAlternativeName, and requires
                # a patch to mod_ssl -- cm@coretec.at
                $param->{'user'}{'email'} = lc($ENV{'SSL_CLIENT_S_EMAIL'});
            } elsif ($ENV{SSL_CLIENT_S_DN_Email}) {
                $param->{'user'}{'email'} = lc($ENV{'SSL_CLIENT_S_DN_Email'});
            } elsif ($ENV{'SSL_CLIENT_S_DN'} =~ /\+MAIL=([^\+\/]+)$/) {
                ## Compatibility issue with old a-sign.at certs
                $param->{'user'}{'email'} = lc($1);
            } elsif ($Crypt::OpenSSL::X509::VERSION
                and exists($ENV{SSL_CLIENT_CERT})) {
                # this is the X509v3 SubjectAlternativeName, and does only
                # require "SSLOptions +ExportCertData" without patching
                # mod_ssl -- massar@unix-ag.uni-kl.de
                $param->{'user'}{'email'} = lc(
                    Crypt::OpenSSL::X509->new_from_string(
                        $ENV{SSL_CLIENT_CERT}
                        )->email()
                );
            }

            if ($param->{user}{email}) {
                $session->{'email'}          = $param->{user}{email};
                $param->{'auth_method'}      = 'smime';
                $session->{'auth'}           = 'x509';
                $param->{'ssl_client_s_dn'}  = $ENV{'SSL_CLIENT_S_DN'};
                $param->{'ssl_client_v_end'} = $ENV{'SSL_CLIENT_V_END'};
                $param->{'ssl_client_i_dn'}  = $ENV{'SSL_CLIENT_I_DN'};
                $param->{'ssl_cipher_usekeysize'} =
                    $ENV{'SSL_CIPHER_USEKEYSIZE'};
            }

        } elsif (($session->{'email'}) && ($session->{'email'} ne 'nobody')) {
            $param->{'user'}{'email'} = $session->{'email'};
        } elsif ($in{'ticket'} =~ /(S|P)T\-/) {
            # the request contain a CAS named ticket that use CAS ticket format
            #reset do_not_use_cas because this client probably use CAS
            delete $session->{'do_not_use_cas'};

            # select the cas server that redirect the user to sympa and check
            # the ticket
            $log->syslog('notice',
                "CAS ticket is detected. in{'ticket'}=$in{'ticket'} checked_cas=$session->{'checked_cas'}"
            );

            my $cas_id = '';
            if ($in{'checked_cas'} =~ /^(\d+)\,?/) {
                $cas_id = $1;
            } elsif ($session->{'checked_cas'} =~ /^(\d+)\,?/) {
                $cas_id = $1;
            }
            if ($cas_id ne '') {

                my $ticket = $in{'ticket'};
                my $cas_server =
                    $Conf::Conf{'auth_services'}{$robot}[$cas_id]
                    {'cas_server'};

                my $service_url = Sympa::Tools::WWW::get_my_url($robot);
                $service_url =~ s/[&;?]ticket=.+\z//;

                my $net_id = $cas_server->validateST($service_url, $ticket);

                if (defined $net_id) {    # the ticket is valid net-id
                    $log->syslog('notice', 'Login CAS OK server netid=%s',
                        $net_id);
                    $param->{'user'}{'email'} = lc(
                        Sympa::Auth::get_email_by_net_id(
                            $robot, $cas_id, {'uid' => $net_id}
                        )
                    );
                    $session->{'auth'}  = 'cas';
                    $session->{'email'} = $param->{user}{email};

                    $session->{'cas_server'} = $cas_id;

                } else {
                    $log->syslog('err', 'CAS ticket validation failed: %s',
                        AuthCAS::get_errors());
                }
            } else {
                $log->syslog('notice',
                    "Internal error while receiving a CAS ticket $session->{'checked_cas'} "
                );
            }
        } elsif ($Conf::Conf{'cas_number'}{$robot} > 0
            and $in{'action'} !~ /^(login|sso_login|wsdl)$/) {
            # some cas server are defined but no CAS ticket detected
            unless ($session->{'do_not_use_cas'}) {
                # user not taggued as not using cas
                foreach
                    my $auth_service (@{$Conf::Conf{'auth_services'}{$robot}})
                {
                    # skip auth services not related to cas
                    next
                        unless ($auth_service->{'auth_type'} eq 'cas');
                    next
                        unless (
                        $auth_service->{'non_blocking_redirection'} eq 'on');

                    ## skip cas server where client as been already redirect
                    ## to the list of cas servers already checked is stored in
                    ## the session
                    ## the check below works fine as long as we
                    ## don't have more then 10 CAS servers (because we don't
                    ## properly split the list of values)
                    $log->syslog('debug',
                        "check_cas checker_cas : $session->{'checked_cas'} current cas_id $Conf::Conf{'cas_id'}{$robot}{$auth_service->{'auth_service_name'}}{'casnum'}"
                    );
                    next
                        if ($session->{'checked_cas'} =~
                        /$Conf::Conf{'cas_id'}{$robot}{$auth_service->{'auth_service_name'}}{'casnum'}/
                        );

                    # before redirect update the list of already checked cas
                    # server to prevent loop
                    my $cas_server = $auth_service->{'cas_server'};
                    my $return_url = Sympa::Tools::WWW::get_my_url($robot);

                    ## Append the current CAS server ID to the list of checked
                    ## CAS servers
                    $session->{'checked_cas'} .=
                        $Conf::Conf{'cas_id'}{$robot}
                        {$auth_service->{'auth_service_name'}}{'casnum'};

                    my $redirect_url =
                        $cas_server->getServerLoginGatewayURL($return_url);

                    if ($redirect_url =~ /http(s)+\:\//i) {
                        $in{'action'} = 'redirect';
                        $param->{'redirect_to'} = $redirect_url;

                        last;
                    } elsif ($redirect_url == -1) {    # CAS server auth error
                        $log->syslog('notice',
                            "CAS server auth error $auth_service->{'auth_service_name'}"
                        );
                    } else {
                        $log->syslog('notice',
                            "Strange CAS ticket detected and validated check sympa code !"
                        );
                    }
                }
                # set do_not_use_cas because all cas servers have been checked
                # without success
                $session->{'do_not_use_cas'} = 1
                    unless ($param->{'redirect_to'} =~ /http(s)+\:\//i);
            }
        }

        ##Cookie extern : sympa_altemails
        ## !!
        $param->{'alt_emails'} =
            Sympa::Session::check_cookie_extern($ENV{'HTTP_COOKIE'},
            $Conf::Conf{'cookie'}, $param->{'user'}{'email'});

        if ($param->{'user'}{'email'}) {
            #$param->{'auth'} =
            #    $param->{'alt_emails'}{$param->{'user'}{'email'}}
            #    || 'classic';

            if (Sympa::User::is_global_user($param->{'user'}{'email'})) {
                $param->{'user'} =
                    Sympa::User::get_global_user($param->{'user'}{'email'});
            }

            ## For the parser to display an empty field instead of [xxx]
            $param->{'user'}{'gecos'} ||= '';
            unless (defined $param->{'user'}{'cookie_delay'}) {
                $param->{'user'}{'cookie_delay'} =
                    $Conf::Conf{'cookie_expire'};
            }
        }
    }    # END unless ($rss)

    ## Action
    my $action = $in{'action'};

    # Store current action in the session in order to redirect after a login
    # or other temporary actions.
    # - We should not memorize URLs that are transitory actions.
    # - POST is not handled.
    # - A lot of other methods where used in the past (before session was
    #   introduced in Sympa). We must clean all.
    # N.B.: Location to where redirect should respect local authority.
    unless ($temporary_actions{$action} or $ENV{'REQUEST_METHOD'} ne 'GET') {
        my $redirect_url =
            Sympa::Tools::WWW::get_my_url($robot, authority => 'local');
        $redirect_url =~ s/[?].*\z//;
        $session->{'redirect_url'} = $redirect_url;
    }

    $action ||= Conf::get_robot_conf($robot, 'default_home');
    $param->{'remote_addr'}     = $ENV{'REMOTE_ADDR'};
    $param->{'remote_host'}     = $ENV{'REMOTE_HOST'};
    $param->{'http_user_agent'} = $ENV{'HTTP_USER_AGENT'};
    $param->{'htmlarea_url'}    = $Conf::Conf{'htmlarea_url'};
    # if ($Conf::Conf{'export_topics'} =~ /all/i);

    if ($in{'action'} eq 'css') {
        do_css();
        $param->{'action'} = 'css';
    } elsif ($maintenance_mode) {
        do_maintenance();
        $param->{'action'} = 'maintenance';
    } else {
        ## Session loop
        while ($action) {
            unless (check_param_in()) {
                Sympa::Report::reject_report_web('user', 'wrong_param', {},
                    $action, $list);
                wwslog('info', 'Wrong parameters');
                last;
            }

            $param->{'host'} = $list->{'admin'}{'host'}
                if (ref($list) eq 'Sympa::List');
            $param->{'host'} ||= $robot;
            $param->{'domain'} = $list->{'domain'}
                if (ref($list) eq 'Sympa::List');

            # Set best content language
            my $user_lang = $param->{'user'}{'lang'} if $param->{'user'};
            my $lang_context = (ref $list eq 'Sympa::List') ? $list : $robot;
            $param->{'lang'} =
                $language->set_lang($session->{'lang'}, $user_lang,
                Sympa::best_language($lang_context));
            # compatibility concern: old-style locale.
            $param->{'locale'} =
                Sympa::Language::lang2oldlocale($param->{'lang'});
            # compatibility concern: for 6.1.
            $param->{'lang_tag'} = $param->{'lang'};

            export_topics($robot);

            unless ($comm{$action}) {
                if (my $list = Sympa::List->new($action, $robot)) {
                    do_redirect(
                        Sympa::get_url(
                            $list, 'info',
                            nomenu    => $param->{'nomenu'},
                            authority => 'local',
                        )
                    );
                    last;
                }
                Sympa::Report::reject_report_web('user', 'unknown_action', {},
                    $action, $list);
                wwslog('info', 'Unknown action %s', $action);
                unless ($action = prevent_visibility_bypass()) {
                    last;
                }
            }

            $param->{'action'} = $action;

            my $old_action    = $action;
            my $old_subaction = $in{'subaction'};

            ## Check required action parameters
            my $check_output = check_action_parameters($action);

            if (!defined $check_output) {
                wwslog('err', 'Missing required parameters for action "%s"',
                    $action);
                delete($param->{'action'});
                last;

            } elsif ($check_output != 1) {
                ## The output of the check may indicate another action to run
                ## first
                ## Example : running loginrequest if user is not authenticated
                $action = $param->{'action'} = $check_output;
            }

            ## Execute the action ##
            if (defined $action) {
                no strict 'refs';
                $action = $comm{$action}->();
            }

            unless (defined $action) {
                unless ($action = prevent_visibility_bypass()) {
                    delete($param->{'action'});
                    last;
                } else {
                    Sympa::Report::reject_report_web('user',
                        'authorization_reject', {}, $param->{'action'}, '');
                }
            }

            # after redirect do not send anything, it will crash fcgi lib
            last
                if ($action =~ /redirect/);

            if ($action eq $old_action) {
                # if a subaction is define and change, then it is not a loop
                if (!defined($in{'subaction'})
                    || ($in{'subaction'} eq $old_subaction)) {
                    wwslog('info', 'Stopping loop with %s action', $action);
                    #undef $action;
                    $action = 'home';
                }
            }

            undef $action if ($action == 1);
        }
    }

    ## Prepare outgoing params
    check_param_out();

    ## Params
    $param->{'refparam'}    = ref($param);
    $param->{'action_type'} = $action_type{$param->{'action'}};

    $param->{'action_type'} = 'none'
        unless (($param->{'is_priv'})
        || ($param->{'action_type'} eq 'serveradmin'));

    #FIXME: is this block neccessary?
    unless ($param->{'lang'}) {
        my $user_lang = $param->{'user'}{'lang'} if $param->{'user'};
        $param->{'lang'} =
            $language->set_lang($user_lang, Sympa::best_language($robot));
        # compatibility: 6.1.
        $param->{'lang_tag'} = $param->{'lang'};
    }

    if ($param->{'list'}) {
        $param->{'list_title'}      = $list->{'admin'}{'subject'};
        $param->{'title'}           = Sympa::get_address($list);
        $param->{'title_clear_txt'} = "$param->{'list'}";

        if ($param->{'subtitle'}) {
            $param->{'main_title'} =
                "$param->{'list'} - $param->{'subtitle'}";
        }
    } else {
        $param->{'main_title'} = $param->{'title'} =
            Conf::get_robot_conf($robot, 'title');
        $param->{'title_clear_txt'} = $param->{'title'};
    }

    # Store oauth_token in session if present and no user so that it
    # will not be lost during login process
    my $oauth_token = $in{'oauth_token'} || '';
    $session->{'oauth_token'} = $oauth_token
        if $oauth_token ne '' && $session->{'email'} eq 'nobody';

    ## store in session table this session contexte
    $session->store();

    ## Do not manage cookies at this level if content was already sent
    unless ($param->{'bypass'} eq 'extreme'
        || $param->{'action'} eq 'css'
        || $maintenance_mode
        || $rss
        || $ajax) {

        my $delay = $param->{'user'}{'cookie_delay'};
        unless (defined $delay) {
            $delay = $Conf::Conf{'cookie_expire'};
        }

        if ($delay == 0) {
            $delay = 'session';
        }
        $session->renew() unless $param->{'use_ssl'};

        unless (
            $session->set_cookie(
                $param->{'cookie_domain'},
                $delay, $param->{'use_ssl'}
            )
            ) {
            wwslog('notice', 'Could not set HTTP cookie');
        }

        $param->{'is_user_allowed_to'} = sub {
            my $permission = shift;
            my $list       = shift;

            return 0 unless $permission and $list;

            $list = Sympa::List->new($list, $robot)
                unless ref $list eq 'Sympa::List';

            my $result = Sympa::Scenario::request_action(
                $list,
                $permission,
                $param->{'auth_method'},
                {   'sender'      => $param->{'user'}{'email'},
                    'remote_host' => $param->{'remote_host'},
                    'remote_addr' => $param->{'remote_addr'}
                }
            );

            return 0
                unless ref($result) eq 'HASH'
                    and $result->{'action'} ne 'reject';

            return 0
                if $permission eq 'subscribe'
                    and $list->is_list_member($param->{'user'}{'email'});

            return 0
                if $permission eq 'archive.web_access'
                    and not defined $list->{'admin'}{'archive'};

            return 1;
        };

        # Set cookies unless client use https authentication
        if ($param->{'user'}{'email'}) {
            if ($param->{'user'}{'email'} ne 'x509') {    #FIXME FIXME FIXME
                $session->{'auth'} ||= 'classic';
                $param->{'cookie_set'} = 1;

                ###Cookie extern : sympa_altemails
                my $number = 0;
                foreach my $element (keys %{$param->{'alt_emails'}}) {
                    $number++ if ($element);
                }

                unless ($number == 0) {
                    unless (
                        Sympa::Session::set_cookie_extern(
                            $Conf::Conf{'cookie'},
                            $param->{'cookie_domain'},
                            %{$param->{'alt_emails'}}
                        )
                        ) {
                        wwslog('notice',
                            'Could not set HTTP cookie for external_auth');
                    }
                }
            }
        }
        # elsif ($ENV{'HTTP_COOKIE'} =~ /sympauser\=/) {
        #     cookielib::set_cookie('unknown', $Conf::Conf{'cookie'},
        #         $param->{'cookie_domain'}, 'now');
        # }
    }

    ## Available languages
    $param->{'languages'} = {};
    $language->push_lang();
    foreach my $lang (Sympa::get_supported_languages($robot)) {
        next unless $lang = $language->set_lang($lang);
        $param->{'languages'}{$lang}{'complete'} = $language->native_name
            || $lang;
        # compatibility: 6.1.
        $param->{'languages'}{$lang}{'lang_tag'} = $lang;
    }
    if (my $lang = $language->set_lang($param->{'lang'})) {    #current lang
        $param->{'languages'}{$lang}{'complete'} = $language->native_name
            || $lang;
        # compatibility: 6.1.
        $param->{'languages'}{$lang}{'lang_tag'} = $lang;

        $param->{'languages'}{$lang}{'selected'} = 'selected="selected"';
    }
    $language->pop_lang;

    $param->{'html_dumpvars'} = Sympa::Tools::Data::dump_html_var($param)
        if $session->{'dumpvars'};

    # if bypass is defined select the content-type from various vars
    if ($param->{'bypass'}) {

        ## if bypass = 'extreme' leave the action send the content-type and
        ## the content itself
        unless ($param->{'bypass'} eq 'extreme') {

            ## if bypass = 'asis', file content-type is in the file itself as is define by the action in $param->{'content_type'};
            unless ($param->{'bypass'} eq 'asis') {
                my $type =
                       $param->{'content_type'}
                    || Conf::get_mime_type($param->{'file_extension'})
                    || 'application/octet-stream';
                printf "Content-Type: %s\n\n", $type;
            }

            #  $param->{'file'} or $param->{'error'} must be define in this case.

            if (open(FILE, $param->{'file'})) {
                print <FILE>;
                close FILE;
            } elsif (Sympa::Report::is_there_any_reject_report_web()) {
                ## for compatibility : it could be better
                my $intern = Sympa::Report::get_intern_error_web();
                my $system = Sympa::Report::get_system_error_web();
                my $user   = Sympa::Report::get_user_error_web();
                my $auth   = Sympa::Report::get_auth_reject_web();

                if (ref($intern) eq 'ARRAY') {
                    print "INTERNAL SERVER ERROR\n";
                }
                if (ref($system) eq 'ARRAY') {
                    print "SYSTEM ERROR\n";
                }
                if (ref($user) eq 'ARRAY') {
                    foreach my $err (@$user) {
                        printf "ERROR : %s\n", $err;
                    }
                }
                if (ref($auth) eq 'ARRAY') {
                    foreach my $err (@$auth) {
                        printf "AUTHORIZATION FAILED : %s\n", $err;
                    }
                }

            } else {
                print "Internal error content-type nor file defined\n";
                $log->syslog('err',
                    'Internal error content-type nor file defined');
            }
        }

    } elsif ($rss) {
        ## Send RSS
        print "Cache-control: no-cache\n";
        print "Content-Type: application/rss+xml; charset=utf-8\n\n";

        ## Icons
        $param->{'icons_url'} =
            Conf::get_robot_conf($robot, 'static_content_url') . '/icons';

        ## Retro compatibility concerns
        $param->{'active'} = 1;

        if (defined $list) {
            $param->{'list_conf'} = $list->{'admin'};
        }

        my $template = Sympa::Template->new(
            $list || $robot,
            plugins      => $plugins,
            subdir       => 'web_tt2',
            lang         => $param->{'lang'},
            include_path => [@other_include_path]
        );
        unless ($template->parse($param, 'rss.tt2', \*STDOUT)) {
            my $error = $template->{last_error};
            $error = $error->as_string if ref $error;
            $param->{'tt2_error'} = $error;

            Sympa::send_notify_to_listmaster($robot, 'web_tt2_error',
                [$error]);
            wwslog('err', '/rss: error: %s', $error);
            printf STDOUT "\n<!-- %s -->\n",
                Sympa::Tools::Text::encode_html($error);
        }
    } elsif ($ajax) {
        print "Cache-control: no-cache\n";
        print "Content-Type: text/html; charset=utf-8\n\n";

        ## Icons
        $param->{'icons_url'} =
            Conf::get_robot_conf($robot, 'static_content_url') . '/icons';

        ## Retro compatibility concerns
        $param->{'active'} = 1;

        if (defined $list) {
            $param->{'list_conf'} = $list->{'admin'};
        }

        my $template = Sympa::Template->new(
            $list || $robot,
            plugins      => $plugins,
            subdir       => 'web_tt2',
            lang         => $param->{'lang'},
            include_path => [@other_include_path]
        );
        # Reset additional settings.
        undef $allow_absolute_path;
        @other_include_path = ();

        unless ($template->parse($param, 'ajax.tt2', \*STDOUT)) {
            my $error = $template->{last_error};
            $error = $error->as_string if ref $error;
            $param->{'tt2_error'} = $error;

            Sympa::send_notify_to_listmaster($robot, 'web_tt2_error',
                [$error]);
            wwslog('err', '/ajax/%s: error: %s', $param->{'action'}, $error);
            printf "\n<!-- %s -->\n", Sympa::Tools::Text::encode_html($error);
        }
# 	 close FILE;
    } elsif ($param->{'redirect_to'}) {
        $log->syslog('notice', 'Redirecting to %s', $param->{'redirect_to'});
        print "Location: $param->{'redirect_to'}\n\n";
    } else {
        prepare_report_user();
        send_html('main.tt2');
    }

    # exit if wwsympa.fcgi itself has changed
    if ($ENV{'SCRIPT_FILENAME'}
        and Sympa::Tools::File::get_mtime($ENV{'SCRIPT_FILENAME'}) >
        $birthday) {
        $log->syslog('notice',
            'Exiting because %s has changed since fastcgi server started',
            $ENV{'SCRIPT_FILENAME'});
        exit(0);
    }

}

# Purge grouped notifications
Sympa::Alarm->instance->flush(purge => 1);

##############################################################
#-#\#|#/#-#\#|#/#-#\#|#/#-#\#|#/#-#\#|#/#-#\#|#/#-#\#|#/#-#\#|#/
##############################################################

## Write to log
sub wwslog {
    my $facility = shift;

    my $msg    = shift;
    my $remote = $ENV{'REMOTE_HOST'} || $ENV{'REMOTE_ADDR'};
    my $wwsmsg = '';

    $wwsmsg = "[list $param->{'list'}] " . $wwsmsg
        if $param->{'list'};

    if ($param->{'alt_emails'}) {
        my @alts;
        foreach my $alt (keys %{$param->{'alt_emails'}}) {
            push @alts, $alt
                unless ($alt eq $param->{'user'}{'email'});
        }

        if ($#alts >= 0) {
            my $alt_list = join ',', @alts;
            $wwsmsg = "[alt $alt_list] " . $wwsmsg;
        }
    }

    $wwsmsg = "[user $param->{'user'}{'email'}] " . $wwsmsg
        if $param->{'user'}{'email'};

    $wwsmsg = "[rss] " . $wwsmsg
        if $rss;

    $wwsmsg = "[client $remote] " . $wwsmsg
        if $remote;

    $wwsmsg = "[session $session->{'id_session'}] " . $wwsmsg
        if $session;

    $wwsmsg = "[robot $robot] " . $wwsmsg;

    push @_, $wwsmsg;
    if ($msg =~ /^([(][^)]*[)])\s*(.*)/s) {
        $msg = sprintf '%s %%%d$s%s', $1, scalar(@_), $2;
    } else {
        $msg = sprintf '%%%d$s%s', scalar(@_), $msg;
    }

    # Don't push caller stack.  Note that goto statement requires "&" prefix!
    unshift @_, $log, $facility, $msg;
    goto &Sympa::Log::syslog;
}

sub web_db_log {
    my $data = shift;

    my %options = %{$data || {}};

    $options{'client'} = $param->{'remote_addr'};
    $options{'daemon'} = 'wwsympa';
    $options{'robot'}      ||= $robot;
    $options{'list'}       ||= $list->{'name'} if ref $list eq 'Sympa::List';
    $options{'action'}     ||= $param->{'action'};
    $options{'user_email'} ||= $param->{'user'}{'email'}
        if defined $param->{'user'};
    # Default email is the user email
    $options{'target_email'} ||= $options{'user_email'};

    unless ($log->db_log(%options)) {
        wwslog('err', 'Failed to log in database');
        return undef;
    }

    return 1;
}

###################################
# log in stat_table via web interface
sub web_db_stat_log {
    my %options = @_;

    $options{'mail'} ||= $param->{'user'}{'email'}
        if defined $param->{'user'};
    $options{'operation'} ||= $param->{'action'};
    $options{'list'} ||= $list->{'name'} if ref $list eq 'Sympa::List';
    $options{'daemon'} = 'wwsympa';
    $options{'client'} = $param->{'remote_addr'};
    $options{'robot'} ||= $robot;

    unless ($log->add_stat(%options)) {
        wwslog('err', 'Failed to log in database');
        return undef;
    }
    return 1;
}

####################################
sub _crash_handler {
    my ($mess, $longmess) = @_;

    $param->{'traceback'}     = $longmess;
    $param->{'error_message'} = $mess;
    $param->{'main_title'} ||= Conf::get_robot_conf($robot, 'title');
    $param->{'last_action'} = $param->{'action'};
    $param->{'action'}      = 'crash';
    eval { send_html('crash.tt2'); };
    print "\n\n";    # when tt2 failed to parse
    exit 0;
}

sub new_loop {
    $loop++;
    my $query;

    if ($Conf::Conf{'use_fast_cgi'}) {
        $query = CGI::Fast->new;
        $loop_count++;
    } else {
        return undef if ($loop > 1);

        $query = CGI->new;
    }

    return $query;
}

# OBSOLETED.  Use Sympa::Tools::WWW::get_server_name() or
# Sympa::Tools::WWW::get_http_host().
sub get_header_field {
    my $field = shift;

    ## HTTP_X_ header fields set when using a proxy
    if ($field eq 'SERVER_NAME') {
        return $ENV{'HTTP_X_FORWARDED_SERVER'} || $ENV{'SERVER_NAME'};
    } elsif ($field eq 'HTTP_HOST') {
        return $ENV{'HTTP_X_FORWARDED_HOST'} || $ENV{'HTTP_HOST'};
    } else {
        return $ENV{$field};
    }
}

# _split_params is used by get_parameters to split path info in the
# appropriate parameters list.
# It is used also by action ticket to prepare the context stored in the
# one_time_ticket table in string like path_info
# input ENV{'PATH_INFO'} like string, output in the global $param hash
sub _split_params {
    my $args_string = shift;

    $log->syslog('debug', "PATH_INFO: %s", $ENV{'PATH_INFO'});

    $args_string =~ s+^/++;

    my $ending_slash = 0;
    if ($args_string =~ /\/$/) {
        $ending_slash = 1;
    }

    my @params = split /\//, $args_string;

    if ($params[0] eq 'nomenu') {
        $param->{'nomenu'} = 1;
        shift @params;
    }

    ## debug mode
    if ($params[0] =~ /debug(\d)?/) {
        shift @params;
        if ($1) {
            $main::options{'debug_level'} = $1 if ($1);
        } else {
            $main::options{'debug_level'} = 1;
        }
    } else {
        $main::options{'debug_level'} = 0;
    }
    $log->syslog('debug2', 'Debug level %s', $main::options{'debug_level'});

    ## rss mode
    if ($params[0] eq 'rss') {
        shift @params;
        $rss = 1;
    }

    ## ajax mode
    if ($params[0] eq 'ajax') {
        shift @params;
        $ajax = 1;
    }

    if ($#params >= 0) {
        $in{'action'} = $params[0];
        my $args;
        if (defined $action_args{$in{'action'}}) {
            $args = $action_args{$in{'action'}};
        } else {
            $args = $action_args{'default'};
        }

        my $i = 1;
        foreach my $p (@$args) {
            my $pname;
            ## More than 1 param
            if ($p =~ /^\@(\w+)$/) {
                $pname = $1;
                $in{$pname} = join '/', @params[$i .. $#params];
                $in{$pname} .= '/' if $ending_slash;
                last;
            } else {
                $pname = $p;
                $in{$pname} = $params[$i];
            }
            wwslog('debug', 'Incoming parameter: %s=%s', $pname, $in{$pname});
            $i++;
        }
    }
}

sub get_parameters {
    my $robot = shift;

    $param->{'path_info'} = $ENV{'PATH_INFO'};
    # Useful to skip previous_action when using POST.
    $param->{'http_method'} = $ENV{'REQUEST_METHOD'};

    if ($ENV{'REQUEST_METHOD'} eq 'GET') {
        _split_params($ENV{'PATH_INFO'});
    } elsif ($ENV{'REQUEST_METHOD'} eq 'POST') {
        ## POST

        if ($in{'javascript_action'}) {
            ## because of incompatibility javascript
            $in{'action'} = $in{'javascript_action'};
        }
        foreach my $p (keys %in) {
            $log->syslog('debug2', 'POST key %s value %s', $p, $in{$p})
                unless ($p =~ /passwd/);
            if ($p =~ /^((\w*)action)_(\w+)((\.\w+)*)$/) {

                $in{$1} = $3;
                if ($4) {
                    foreach my $v (split /\./, $4) {
                        $v =~ s/^\.?(\w+)\.?/$1/;
                        $in{$v} = 1;
                    }
                }
                undef $in{$p};
            }
        }
        $param->{'nomenu'} = $in{'nomenu'};
    }

    # From CGI URL get {base_url} and {path_cgi} parameters.
    # Note that other links should keep the nomenu attribute.
    # NOTE: The base_url is kept for compatibility to Sympa < 6.2.15.  The
    # path_cgi is still used in archives, help etc.
    my $uri =
        URI->new(Sympa::get_url($robot, undef, nomenu => $param->{'nomenu'}));
    $param->{'base_url'} = $uri->scheme . '://' . $uri->authority
        if $uri->authority;
    $param->{'path_cgi'} = $uri->path;

    # mod_ssl sets SSL_PROTOCOL; Apache-SSL sets SSL_PROTOCOL_VERSION.
    $param->{'use_ssl'} = ($ENV{HTTPS} && $ENV{HTTPS} eq 'on');

    ## Lowercase email addresses
    $in{'email'} = lc($in{'email'});

    ## Don't get multiple listnames
    if ($in{'list'}) {
        my @lists = split /\0/, $in{'list'};
        $in{'list'} = $lists[0];
    }

    my $custom_attribute;
    my $custom_input;
    my $plugin = {};

    ## Check parameters format
    foreach my $p (keys %in) {

        ## Skip empty parameters
        next if ($in{$p} =~ /^$/);

        ## Remove DOS linefeeds (^M) that cause problems with Outlook 98, AOL,
        ## and EIMS:
        $in{$p} =~ s/\r\n|\r/\n/g;

        #XXX## Convert from the web encoding to unicode string
        #XXX$in{$p} = Encode::decode('utf8', $in{$p});

        my @tokens = split(/\./, $p);
        my $pname = $tokens[0];

        ## Regular expressions applied on parameters

        my $regexp;
        if ($pname =~ /^additional_field/) {
            $regexp = $in_regexp{'additional_field'};
        } elsif ($pname =~ /^custom_attribute(.*)$/) {
            my $key = $tokens[1];
            $regexp = $in_regexp{'custom_attribute'};
            # $log->syslog('debug2', '() (%s)(%s) %s %s %s', $p, $key, $name,
            #     $in{$p}, $Conf::Conf{$key}->{type});
            $custom_attribute->{$key} = {value => $in{$p}};
            undef $in{$p};
        } elsif ($pname eq 'plugin' and $#tokens >= 2) {
            my $plugin_name = $tokens[1];
            my $param_name  = $tokens[2];
            $regexp = $in_regexp{'plugin'};
            $plugin->{$plugin_name} = {}
                unless defined $plugin->{$plugin_name};
            $plugin->{$plugin_name}{$param_name} = $in{$p};
            undef $in{$p};
        } elsif ($pname =~ /^custom_input(.*)$/) {
            my $key = $tokens[1];
            $regexp = $in_regexp{'custom_input'};
            $log->syslog('debug2',
                "get_parameters (custom_input) : ($p)($key) $pname $in{$p} $Conf::Conf{$key}{type}"
            );
            $custom_input->{$key} = $in{$p};
            undef $in{$p};
        } elsif ($in_regexp{$pname}) {
            $regexp = $in_regexp{$pname};
        } else {
            $regexp = $in_regexp{'*'};
        }

        my $negative_regexp;
        if ($pname =~ /^additional_field/) {
            $negative_regexp = $in_negative_regexp{'additional_field'};
        } elsif ($in_negative_regexp{$pname}) {
            $negative_regexp = $in_negative_regexp{$pname};
        }

        # If we are editing an HTML file in the shared, allow HTML but prevent
        # XSS.
        if (   $pname eq 'content'
            && $in{'action'} eq 'd_savefile'
            && $in{'path'} =~ $list->{'dir'} . '/shared'
            && lc($in{'path'}) =~ /\.html?/) {
            my $tmpparam = $in{$p};
            $tmpparam =
                Sympa::HTMLSanitizer->new($robot)->sanitize_html($in{$p});
            if (defined $tmpparam) {
                $in{$p} = $tmpparam;
            } else {
                $log->syslog('err', 'Unable to sanitize parameter %s',
                    $pname);
            }
        }
        foreach my $one_p (split /\0/, $in{$p}) {
            if ($one_p !~ /^$regexp$/s
                || (defined $negative_regexp && $one_p =~ /$negative_regexp/s)
                ) {
                ## Dump parameters in a tmp file for later analysis
                my $dump_file =
                      Conf::get_robot_conf($robot, 'tmpdir')
                    . '/sympa_dump.'
                    . time . '.'
                    . $PID;
                unless (open DUMP, ">$dump_file") {
                    wwslog('err', 'Failed to create %s: %s',
                        $dump_file, $ERRNO);
                }
                Sympa::Tools::Data::dump_var(\%in, 0, \*DUMP);
                close DUMP;

                Sympa::Report::reject_report_web('user', 'syntax_errors',
                    {'params' => $p},
                    '', '');
                wwslog(
                    'err',
                    'Syntax error for parameter %s value "%s" not conform to regexp:%s; dumped vars in %s',
                    $pname,
                    $one_p,
                    $regexp,
                    $dump_file
                );
                $in{$p} = '';
                next;
            }
        }
    }

    $in{custom_attribute} = $custom_attribute;
    $in{custom_input}     = $custom_input;
    $in{plugin}           = $plugin;

    ## For shared-related actions, Q-encode filenames
    ## This required for filenames that include non ascii characters
    if (defined $filtering{$in{'action'}}) {

        my %apply_to;    ## Build list of parameters filters apply to
        foreach my $p (keys %{$filtering{$in{'action'}}}) {
            if ($p =~ /\*/) {    ## use of wildcar
                my $p_regexp = $p;
                $p_regexp =~ s/\*/\.\*/g;    ## Turn wildcar into a regexp
                foreach my $in_key (keys %in) {
                    if ($in_key =~ /^$p_regexp$/) {
                        $apply_to{$in_key} = $filtering{$in{'action'}}{$p};
                    }
                }
            } else {
                $apply_to{$p} = $filtering{$in{'action'}}{$p};
            }
        }

        foreach my $p (keys %apply_to) {
            my $filtering_action = $apply_to{$p};
            if ($filtering_action eq 'qencode') {
                ## Q-encode file path
                my @tokens = split /\//, $in{$p};
                foreach my $i (0 .. $#tokens) {
                    $tokens[$i] =
                        Sympa::Tools::Text::qencode_filename($tokens[$i]);
                }
                $in{$p} = join '/', @tokens;
            } elsif ($filtering_action eq 'unescape_html') {
                # Sympa's URI escaping subroutine
                # (Sympa::Tools::Text::escape_chars()) replaces '/' with
                # %A5 ('¥' character).  This should be transformed into a
                # '/' again.
                $in{$p} = Sympa::Tools::Text::unescape_chars($in{$p});
            } elsif ($filtering_action eq 'fix_escape_uri') {
                $in{$p} =~ s/\xa5/\//g;

            } elsif ($filtering_action eq 'normalize') {
                $in{$p} =~ s/^\$+//;    ## remove leading \s
                $in{$p} =~ s/\$+$//;    ## remove trailing \s
                $in{$p} = lc($in{$p});  ## lowercase
            }
        }
    }

    return 1;
}

# NO LONGER USED.
#sub get_parameters_old;

## Check required parameters for an action
## It compares incoming parameter to those declared as required in
## %required_args
## Also check required privileges to perform each action
sub check_action_parameters {
    my $action = shift;

    if (defined $required_args{$action}) {
        foreach my $arg_name (@{$required_args{$action}}) {

            ## Missing list parameter
            if ($arg_name eq 'param.list') {
                unless (defined $list) {
                    Sympa::Report::reject_report_web('user', 'missing_arg',
                        {'argument' => 'list'}, $action);
                    wwslog('info', 'Missing list parameter');
                    web_db_log(
                        {   'status'     => 'error',
                            'error_type' => 'no_list'
                        }
                    );

                    return undef;
                }

                ## User is not authenticated
            } elsif ($arg_name eq 'param.user.email') {
                unless (defined $param->{'user'} && $param->{'user'}{'email'})
                {
                    if (prevent_visibility_bypass()) {
                        Sympa::Report::reject_report_web('user',
                            'authorization_reject', {}, $param->{'action'},
                            '');
                    }
                    Sympa::Report::reject_report_web('user', 'no_user', {},
                        $action);
                    wwslog('err', 'User not logged in');
                    web_db_log(
                        {   'status'     => 'error',
                            'error_type' => "not_logged_in"
                        }
                    );

                    ## User is redirected to the login request form
                    $param->{'previous_action'} = $action;
                    $param->{'previous_list'}   = $param->{'list'}
                        if (defined $param->{'list'});
                    return 'loginrequest';

                }
                ## Other incoming parameters
            } else {
                ## There may be alternate parameters
                ## Then at least one of them MUST be set
                my @req_parameters = split(/\|/, $arg_name);
                my $ok = 0;
                foreach my $req_param (@req_parameters) {
                    $ok = 1 if ($in{$req_param});
                }
                unless ($ok) {
                    ## Replace \0 and '|' with ',' before logging
                    $in{$arg_name} =~ s/\0/,/g;
                    $in{$arg_name} =~ s/\|/,/g;

                    if (prevent_visibility_bypass()) {
                        Sympa::Report::reject_report_web('user',
                            'authorization_reject', {'list' => $in{'list'}},
                            $param->{'action'}, '');
                    }
                    Sympa::Report::reject_report_web('user', 'missing_arg',
                        {'argument' => $arg_name}, $action);
                    wwslog('info', 'Missing parameter "%s"', $arg_name);
                    web_db_log(
                        {   'status'     => 'error',
                            'error_type' => 'missing_parameter'
                        }
                    );
                    delete $param->{'list'};
                    return undef;
                }
            }
        }
    }

    ## Check required privileges
    if (defined $required_privileges{$action}) {
        ## There may be alternate privileges
        ## Then at least one of them MUST verified
        my $ok = 0;
        my $missing_priv;
        foreach my $req_priv (@{$required_privileges{$action}}) {
            $ok = 1 if ($param->{'is_' . $req_priv});
            $missing_priv = $req_priv;
        }
        unless ($ok) {
            Sympa::Report::reject_report_web('auth',
                'action_' . $missing_priv,
                {}, $param->{'action'}, $list);
            wwslog('info', 'Authorization failed, insufficient privileges');
            web_db_log(
                {   'status'     => 'error',
                    'error_type' => 'authorization'
                }
            );
            delete $param->{'list'};
            return undef;
        }
    }

    return 1;
}

## Send HTML output
sub send_html {
    my $tt2_file = shift;

    ## Send HTML headers
    if ($param->{'date'}) {
        printf "Date: %s\n",
            DateTime->now->strftime("%a, %{day} %b %Y %H:%M:%S GMT");
    }
    ## If we set the header indicating the last time the file to send was
    ## modified, add an HTTP header (limitate web harvesting).
    if ($param->{'header_date'}) {
        printf "Last-Modified: %s\n",
            DateTime->from_epoch(epoch => $param->{'header_date'})
            ->strftime("%a, %{day} %b %Y %H:%M:%S GMT");
    }
    print "Cache-control: max-age=0\n" unless $param->{'action'} eq 'arc';
    print "Content-Type: text/html; charset=utf-8\n";
    ## Workaround for Internet Explorer 8 or later.
    print "X-UA-Compatible: IE=100\n";

    ## Notify crash to client.
    if ($param->{'action'} eq 'crash') {
        print "Status: 503 Service Unavailable\n";
        print "Retry-After: 300\n";
    }

    ## Icons
    $param->{'icons_url'} =
        Conf::get_robot_conf($robot, 'static_content_url') . '/icons';

    ## Retro compatibility concerns
    $param->{'active'} = 1;

    ## undefined $list has been initialized to be hashref.
    if (ref $list eq 'HASH') {
        $log->syslog('notice',
            'Someone tried to access inside of List object directly.  Fix the codes'
        );
        local $Data::Dumper::Varname = 'list';
        local $Data::Dumper::Indent  = 0;
        $log->syslog('notice', '%s', Dumper($list));
        undef $list;
    }

    if (ref $list eq 'Sympa::List') {
        $param->{'list_conf'} = $list->{'admin'};
    }

    ## Trying to use custom_vars
    if (ref $list eq 'Sympa::List'
        and @{$list->{'admin'}{'custom_vars'} || []}) {
        foreach my $var (@{$list->{'admin'}{'custom_vars'}}) {
            $param->{'custom_vars'}{$var->{'name'}} = $var->{'value'};
        }
    }

    # XSS escaping applied to all outgoing parameters.
    ## Escape parameters on a copy to avoid altering useful data.
    my $param_copy = Sympa::Tools::Data::dup_var($param);
    if (defined $param_copy) {
        unless (
            Sympa::HTMLSanitizer->new($robot)->sanitize_var(
                $param_copy,
                'htmlAllowedParam' => $param_copy->{'htmlAllowedParam'},
                'htmlToFilter'     => $param_copy->{'htmlToFilter'},
            )
            ) {
            $log->syslog('err', 'Failed to sanitize $param in host %s',
                $robot);
        }
    }

    ## Available plugins, to switch on template fragments
    $param_copy->{plugins} =
        $plugins ? sub { $plugins->tt2Fragments(@_) } : sub { () };

    ## Custom CSSes.  They are stored to $param_copy so that they won't be
    ## escaped.

    # testing custom CSS.
    if ($session->{'custom_css'}) {
        # Do not include locale paths (lang parameter).
        # The css.tt2 by each locales will override styles in main CSS.
        my $css;
        my $css_template = Sympa::Template->new(
            $list || $robot,
            plugins => $plugins,
            subdir  => 'web_tt2'
        );
        unless ($css_template->parse($param, 'css.tt2', \$css)) {
            wwslog('info', 'Error while parsing custom CSS');
            delete $param_copy->{'custom_css'};
        } else {
            $param_copy->{'custom_css'} = $css;
        }
    }

    # per-locale CSS.
    if (Sympa::search_fullpath(
            $list || $robot, 'css.tt2',
            subdir    => 'web_tt2',
            lang      => $param->{'lang'},
            lang_only => 1
        )
        ) {
        # Conversely, include only locale paths.
        my $css;
        my $css_template = Sympa::Template->new(
            $list || $robot,
            subdir    => 'web_tt2',
            lang      => $param->{'lang'},
            lang_only => 1
        );
        unless ($css_template->parse($param, 'css.tt2', \$css)) {
            wwslog('info', 'Error while parsing CSS css.tt2 for lang %s',
                $param->{'lang'});
        } else {
            $param_copy->{'locale_css'} = $css;
        }
    }

    # Now include locale paths (lang parameter).
    my $template = Sympa::Template->new(
        $list || $robot,
        allow_absolute => $allow_absolute_path,
        plugins        => $plugins,
        subdir         => 'web_tt2',
        lang           => $param->{'lang'},
        include_path   => [@other_include_path]
    );
    # Reset additional settings.
    undef $allow_absolute_path;
    @other_include_path = ();

    # Then output the content.
    unless (
        $template->parse($param_copy, $tt2_file, \*STDOUT, has_header => 1)) {
        my $error = $template->{last_error};

        if (    $param->{'action'} eq 'help'
            and ref $error
            and $error->type eq 'file') {
            # "Not Found" response for random help page.
            print "Status: 404 Not Found\n";

            $error = $error->as_string;
        } else {
            $error = $error->as_string if ref $error;

            Sympa::send_notify_to_listmaster($robot, 'web_tt2_error',
                [$error]);
            wwslog('err', '/%s: error: %s', $param->{'action'}, $error);
        }

        my $error_escaped = Sympa::Tools::Text::encode_html($error);
        $param->{'tt2_error'}      = $error_escaped;
        $param_copy->{'tt2_error'} = $error_escaped;
        $template->parse($param_copy, 'tt2_error.tt2', \*STDOUT,
            has_header => 1);
        print STDOUT "\n\n";    # when tt2 failed to parse
    }
}

sub prepare_report_user {
    $param->{'intern_errors'} = Sympa::Report::get_intern_error_web();
    $param->{'system_errors'} = Sympa::Report::get_system_error_web();
    $param->{'user_errors'}   = Sympa::Report::get_user_error_web();
    $param->{'auth_rejects'}  = Sympa::Report::get_auth_reject_web();
    $param->{'notices'}       = Sympa::Report::get_notice_web();
    $param->{'errors'} = Sympa::Report::is_there_any_reject_report_web();
}

#=head2 sub check_param_in
#
#Checks parameters contained in the global variable $in. It is the process used to analyze the incoming parameters.
#Use it to create a List object and initialize output parameters.
#
#=head3 Arguments
#
#=over
#
#=item * I<None>
#
#=back
#
#=head3 Return
#
#=over
#
#=item * I<undef> if the process encounters problems.
#
#=item * I<1> if everything goes well
#
#=back
#
#=cut

## Analysis of incoming parameters
sub check_param_in {
    wwslog('debug2', '');

    ## Lowercase list name
    $in{'list'} =~ tr/A-Z/a-z/;

    ## In case the variable was multiple
    if ($in{'list'} =~ /^(\S+)\0/) {
        $in{'list'} = $1;

        ## Create a new List instance.
        unless ($list = Sympa::List->new($in{'list'}, $robot)) {
            Sympa::Report::reject_report_web('user', 'unknown_list',
                {'list' => $in{'list'}},
                $param->{'action'}, '');
            wwslog('info', 'Unknown list %s', $in{'list'});
            return undef;
        }

        ## Set lang to list lang
        $language->set_lang($list->{'admin'}{'lang'});
    }

    ## listmaster has owner and editor privileges for the list
    if (Sympa::is_listmaster($robot, $param->{'user'}{'email'})) {
        $param->{'is_listmaster'} = 1;
    }

    if ($in{'list'}) {
        ## Create a new Sympa::List instance.
        unless ($list = Sympa::List->new($in{'list'}, $robot, {})) {
            Sympa::Report::reject_report_web('user', 'unknown_list',
                {'list' => $in{'list'}},
                $param->{'action'}, '');
            wwslog('info', 'Unknown list %s', $in{'list'});
            return undef;
        }

        # Gather list configuration information for further output.
        $param->{'list'}      = $in{'list'};
        $param->{'subtitle'}  = $list->{'admin'}{'subject'};
        $param->{'subscribe'} = $list->{'admin'}{'subscribe'}{'name'};
        #FIXME: Use Sympa::Scenario::get_current_title().
        $param->{'send'} =
            $list->{'admin'}{'send'}{'title'}{$param->{'lang'}};

        # Pictures are not available unless it is configured for the list and
        # the robot
        if ($list->{'admin'}{'pictures_feature'} eq 'off') {
            $param->{'pictures_display'} = undef;
        } else {
            $param->{'pictures_display'} = 'on';
        }

        ## Get the total number of subscribers to the list.
        if (defined $param->{'total'}) {
            $param->{'total'} = $list->get_total();
        } else {
            $param->{'total'} = $list->get_total('nocache');
        }

        ## Check if the current list has a public key X.509 certificate.
        $param->{'list_as_x509_cert'} = $list->{'as_x509_cert'};

        ## Stores to output the whole list's admin configuration.
        $param->{'listconf'} = $list->{'admin'};

        ## If an user is logged in, checks this user's privileges.
        if ($param->{'user'}{'email'}) {
            $param->{'is_subscriber'} =
                $list->is_list_member($param->{'user'}{'email'});
            $param->{'subscriber'} =
                $list->get_list_member($param->{'user'}{'email'})
                if $param->{'is_subscriber'};
            $param->{'is_privileged_owner'} =
                $list->is_admin('privileged_owner', $param->{'user'}{'email'})
                || Sympa::is_listmaster($list, $param->{'user'}{'email'});
            $param->{'is_owner'} =
                $list->is_admin('owner', $param->{'user'}{'email'})
                || Sympa::is_listmaster($list, $param->{'user'}{'email'});
            $param->{'is_editor'} =
                $list->is_admin('actual_editor', $param->{'user'}{'email'});
            $param->{'is_priv'} = $param->{'is_owner'}
                || $param->{'is_editor'};
            $param->{'pictures_url'} =
                $list->find_picture_url($param->{'user'}{'email'});

            ## Checks if the user can post in this list.
            my $result = Sympa::Scenario::request_action(
                $list, 'send',
                $param->{'auth_method'},
                {   'sender'      => $param->{'user'}{'email'},
                    'remote_host' => $param->{'remote_host'},
                    'remote_addr' => $param->{'remote_addr'}
                }
            );
            my $r_action;
            $r_action = $result->{'action'} if (ref($result) eq 'HASH');
            $param->{'may_post'} = 1 if ($r_action !~ /reject/);
        } else {
            # If no user logged in, the output can ask for authentication.
            $param->{'user'}{'email'} = undef;
            $param->{'need_login'} = 1;

        }

        ## Check if this list's messages must be moderated.
        $param->{'is_moderated'} = $list->is_moderated();

        # If the user logged in is a privileged user, gather information
        # relative to administration tasks.
        if ($param->{'is_priv'}) {
            $param->{'mod_message'} =
                Sympa::Spool::Moderation->new(context => $list)->size;
            $param->{'mod_subscription'} = Sympa::Spool::Auth->new(
                context => $list,
                action  => 'add'
            )->size;
            $param->{'mod_signoff'} = Sympa::Spool::Auth->new(
                context => $list,
                action  => 'del'
            )->size;

            $param->{'doc_mod_list'}     = $list->get_shared_moderated();
            $param->{'mod_total_shared'} = $#{$param->{'doc_mod_list'}} + 1;

            if ($param->{'total'} > 0) {
                $param->{'bounce_total'} = $list->get_total_bouncing();
                $param->{'bounce_rate'} =
                    $param->{'bounce_total'} * 100 / $param->{'total'};
                $param->{'bounce_rate'} =
                    int($param->{'bounce_rate'} * 10) / 10;
            } else {
                $param->{'bounce_rate'} = 0;
            }
            $param->{'mod_total'} =
                $param->{'mod_total_shared'} +
                $param->{'mod_message'} +
                $param->{'mod_subscription'};
        }

        ## Check unsubscription authorization for the current user and list.
        my $result = Sympa::Scenario::request_action(
            $list,
            'unsubscribe',
            $param->{'auth_method'},
            {   'sender'      => $param->{'user'}{'email'},
                'remote_host' => $param->{'remote_host'},
                'remote_addr' => $param->{'remote_addr'}
            }
        );
        $main::action = $result->{'action'} if (ref($result) eq 'HASH');

        if (!$param->{'user'}{'email'}) {
            $param->{'may_signoff'} = 1
                if ($main::action =~ /do_it|owner|request_auth/);

        } elsif ($param->{'is_subscriber'}) {
            $param->{'may_signoff'} = 1
                if ($main::action =~ /do_it|owner|request_auth/);
            $param->{'may_suboptions'} = 1;
        }

        ## Check subscription authorization for the current user and list.
        $result = Sympa::Scenario::request_action(
            $list,
            'subscribe',
            $param->{'auth_method'},
            {   'sender'      => $param->{'user'}{'email'},
                'remote_host' => $param->{'remote_host'},
                'remote_addr' => $param->{'remote_addr'}
            }
        );
        $main::action = $result->{'action'} if (ref($result) eq 'HASH');

        $param->{'may_subscribe'} = 1
            if ($main::action =~ /do_it|owner|request_auth/);

        ## Check if the current user can read the shared documents.
        my %mode;
        $mode{'read'} = 1;
        my %access = d_access_control(\%mode, "");
        $param->{'may_d_read'} = $access{'may'}{'read'};

        ## Check the status (exists, deleted, doesn't exist) of the shared
        ## directory
        $param->{'shared'} = $list->get_shared_status();
    }

    ## Check if the current user can create a list.
    my $result = Sympa::Scenario::request_action(
        $robot,
        'create_list',
        $param->{'auth_method'},
        {   'sender'      => $param->{'user'}{'email'},
            'remote_host' => $param->{'remote_host'},
            'remote_addr' => $param->{'remote_addr'}
        }
    );
    my $r_action;
    my $reason;
    if (ref($result) eq 'HASH') {
        $r_action = $result->{'action'};
        $reason   = $result->{'reason'};
    }
    $param->{'create_list_reason'} = $reason;

    if ($param->{'user'}{'email'}
        && (($param->{'create_list'} = $r_action) =~ /do_it|listmaster/)) {
        $param->{'may_create_list'} = 1;
    } else {
        undef($param->{'may_create_list'});
    }

    return 1;

}

## Prepare outgoing params
sub check_param_out {
    wwslog('debug2', '');

    $param->{'loop_count'} = $loop_count;
    $param->{'start_time'} =
        $language->gettext_strftime("%d %b %Y at %H:%M:%S",
        localtime $start_time);
    $param->{'process_id'} = $PID;

    ## listmaster has owner and editor privileges for the list
    if (Sympa::is_listmaster($robot, $param->{'user'}{'email'})) {
        $param->{'is_listmaster'} = 1;
    } else {
        undef $param->{'is_listmaster'};
    }

    ## Reset $list variable if it is not expected for the current action
    ## To prevent the list panel from being printed in a non list context
    ## Only check if the corresponding entry exists in %action_args
    if (   defined $param->{'action'}
        && defined $action_args{$param->{'action'}}) {
        unless (grep /^list$/, @{$action_args{$param->{'action'}}}) {
            $param->{'list'} = undef;
            $list = undef;
        }
    }

    # Compat: 6.2.13 and earlier generated HTML archive etc. using these
    # parameters for email addresses protection.
    $param->{'hidden_head'} = '';
    $param->{'hidden_at'}   = '@';
    $param->{'hidden_end'}  = '';

    if (ref $list eq 'Sympa::List' and $list->{'name'}) {
        wwslog('debug2', 'List-name %s', $list->{'name'});

        # Owners and editors
        foreach my $role (qw(owner editor)) {
            foreach my $u ($list->get_admins($role)) {
                next unless $u->{'email'};

                my ($local, $domain) = split /\@/, $u->{'email'};

                $param->{$role}{$u->{'email'}} = {
                    gecos      => $u->{gecos},
                    visibility => $u->{visibility},
                    local      => $local,
                    domain     => $domain,
                };
            }
        }

        ## Environment variables
        foreach my $k (keys %ENV) {
            $param->{'env'}{$k} = $ENV{$k};
        }
        ## privileges
        if ($param->{'user'}{'email'}) {
            $param->{'is_subscriber'} =
                $list->is_list_member($param->{'user'}{'email'});
            $param->{'subscriber'} =
                $list->get_list_member($param->{'user'}{'email'})
                if $param->{'is_subscriber'};
            $param->{'is_privileged_owner'} =
                $list->is_admin('privileged_owner', $param->{'user'}{'email'})
                || Sympa::is_listmaster($list, $param->{'user'}{'email'});
            $param->{'is_owner'} =
                $list->is_admin('owner', $param->{'user'}{'email'})
                || Sympa::is_listmaster($list, $param->{'user'}{'email'});
            $param->{'is_editor'} =
                $list->is_admin('actual_editor', $param->{'user'}{'email'});
            $param->{'is_priv'} = $param->{'is_owner'}
                || $param->{'is_editor'};

            #May post:
            my $result = Sympa::Scenario::request_action(
                $list, 'send',
                $param->{'auth_method'},
                {   'sender'      => $param->{'user'}{'email'},
                    'remote_host' => $param->{'remote_host'},
                    'remote_addr' => $param->{'remote_addr'}
                }
            );

            my $r_action;
            my $reason;
            if (ref($result) eq 'HASH') {
                $r_action = $result->{'action'};
                $reason   = $result->{'reason'};
            }

            if ($r_action =~ /do_it/) {
                $param->{'may_post'} = 1;
            } else {
                $param->{'may_post_reason'} = $reason;
            }

            if (   $list->has_include_data_sources()
                && $param->{'is_owner'}) {
                $param->{'may_sync'} = 1;
            }
        } else {
            ## If user not logged in && GET method && not an authN-related
            ## action
            ## Keep track of the 'referer' parameter
            if ($ENV{'REQUEST_METHOD'} eq 'GET'
                and not $auth_action{$in{'action'}}) {
                $param->{'referer'} =
                    Sympa::Tools::Text::escape_chars(
                    Sympa::Tools::WWW::get_my_url($robot));
            } else {
                ## Keep the previous value of the referer
                $param->{'referer'} = $in{'referer'};
            }
        }

        ## Should Not be used anymore ##
        $param->{'may_subunsub'} = 1
            if ($param->{'may_signoff'} || $param->{'may_subscribe'});

        ## May review
        my $result = Sympa::Scenario::request_action(
            $list, 'review',
            $param->{'auth_method'},
            {   'sender'      => $param->{'user'}{'email'},
                'remote_host' => $param->{'remote_host'},
                'remote_addr' => $param->{'remote_addr'}
            }
        );
        my $r_action;
        $r_action = $result->{'action'} if (ref($result) eq 'HASH');

        $param->{'may_suboptions'} = 1;
        $param->{'total'}          = $list->get_total();
        $param->{'may_review'}     = 1 if ($r_action =~ /do_it/);
        $param->{'list_status'}    = $list->{'admin'}{'status'};

        ## May signoff
        $result = Sympa::Scenario::request_action(
            $list,
            'unsubscribe',
            $param->{'auth_method'},
            {   'sender'      => $param->{'user'}{'email'},
                'remote_host' => $param->{'remote_host'},
                'remote_addr' => $param->{'remote_addr'}
            }
        );
        $main::action = $result->{'action'} if (ref($result) eq 'HASH');

        if (!$param->{'user'}{'email'}) {
            $param->{'may_signoff'} = 1
                if ($main::action =~ /do_it|owner|request_auth/);

        } elsif ($param->{'is_subscriber'}
            && ($param->{'subscriber'}{'subscribed'} == 1)) {
            $param->{'may_signoff'} = 1
                if ($main::action =~ /do_it|owner|request_auth/);
            $param->{'may_suboptions'} = 1;
        }

        ## May Subscribe
        $result = Sympa::Scenario::request_action(
            $list,
            'subscribe',
            $param->{'auth_method'},
            {   'sender'      => $param->{'user'}{'email'},
                'remote_host' => $param->{'remote_host'},
                'remote_addr' => $param->{'remote_addr'}
            }
        );
        $main::action = $result->{'action'} if (ref($result) eq 'HASH');

        $param->{'may_subscribe'} = 1
            if ($main::action =~ /do_it|owner|request_auth/);

# SJS START
        ## May Add or del subscribers
        my $result = Sympa::Scenario::request_action(
            $list, 'add',
            $param->{'auth_method'},
            {   'sender'      => $param->{'user'}{'email'},
                'remote_host' => $param->{'remote_host'},
                'remote_addr' => $param->{'remote_addr'}
            }
        );
        $main::action = $result->{'action'} if (ref($result) eq 'HASH');
        $param->{'may_add'} = 1 if ($main::action =~ /do_it/);
        my $result = Sympa::Scenario::request_action(
            $list, 'del',
            $param->{'auth_method'},
            {   'sender'      => $param->{'user'}{'email'},
                'remote_host' => $param->{'remote_host'},
                'remote_addr' => $param->{'remote_addr'}
            }
        );
        $main::action = $result->{'action'} if (ref($result) eq 'HASH');
        $param->{'may_del'} = 1 if ($main::action =~ /do_it/);
# SJS END

        ## Archives Access control
        if (defined $list->{'admin'}{'archive'}) {
            $param->{'is_archived'} = 1;

            ## Check if the current user may access web archives
            my $result = Sympa::Scenario::request_action(
                $list,
                'archive.web_access',
                $param->{'auth_method'},
                {   'sender'      => $param->{'user'}{'email'},
                    'remote_host' => $param->{'remote_host'},
                    'remote_addr' => $param->{'remote_addr'}
                }
            );
            my $r_action;
            $r_action = $result->{'action'} if (ref($result) eq 'HASH');

            if ($r_action =~ /do_it/i) {
                $param->{'arc_access'} = 1;
            } else {
                undef($param->{'arc_access'});
            }

            ## Check if web archive is publically accessible (useful
            ## information for RSS)
            $result = Sympa::Scenario::request_action(
                $list, 'archive.web_access',
                $param->{'auth_method'},
                {'sender' => 'nobody'}
            );
            $r_action = $result->{'action'} if (ref($result) eq 'HASH');

            if ($r_action =~ /do_it/i) {
                $param->{'arc_public_access'} = 1;
            }
        }

        ## Shared documents access control
        if ($list->get_shared_status() eq 'exist') {
            ## Check if shared is publically accessible (useful information
            ## for RSS)
            my $result = Sympa::Scenario::request_action(
                $list, 'shared_doc.d_read',
                $param->{'auth_method'},
                {'sender' => 'nobody'}
            );
            my $r_action;
            if (ref($result) eq 'HASH') {
                $r_action = $result->{'action'};
            }

            if ($r_action =~ /do_it/i) {
                $param->{'shared_public_access'} = 1;
            }

        }

        # List included in other list may not be closed nor renamed.
        $param->{'is_included'} = 1 if $list->is_included;
    }

    $param->{'robot'} = $robot;

    # If parameter has the Unicode Perl flag, then switch to utf-8.
    # This switch is applied recursively.
    Sympa::Tools::Data::recursive_transformation(
        $param,
        sub {
            my $s = shift;
            return Encode::encode_utf8($s) if Encode::is_utf8($s);
            return $s;
        }
    );
}

## ticket : this action is used if someone submits a one time ticket
sub do_ticket {
    wwslog('info', '(%s)', $in{'ticket'});

    $param->{'ticket_context'} =
        Sympa::Ticket::load($robot, $in{'ticket'}, $ip);
    $param->{'ticket_context'}{'printable_date'} =
        $language->gettext_strftime("%d %b %Y at %H:%M:%S",
        localtime($param->{'ticket_context'}{'date'}));

    return 1
        unless ($param->{'ticket_context'}{'result'} eq 'success'
        or $param->{'ticket_context'}{'result'} eq 'closed');

    # if the ticket is related to someone which is not logged in, the system
    # performs the same operation as for a login
    my $email_regexp = Sympa::Regexps::email();
    if (($param->{'ticket_context'}{'result'} eq 'success')
        || # a valid ticket or a closed or expired ticket but with a valid pre-existing session
        (   (      ($param->{'ticket_context'}{'result'} eq 'expired')
                || ($param->{'ticket_context'}{'result'} eq 'closed')
            )
            && (lc($param->{'ticket_context'}{'email'}) eq
                $session->{'email'})
        )
        ) {
        $session->{'email'} = lc($param->{'ticket_context'}{'email'});
        $param->{'user'} = Sympa::User::get_global_user($session->{'email'});
        $param->{'user'}{'email'} = $session->{'email'};
        $param->{'last_login _host'} = $param->{'user'}{'last_login_host'};
        $param->{'last_login_date'} =
            $language->gettext_strftime("%d %b %Y at %H:%M:%S",
            localtime($param->{'user'}{'last_login_date'}))
            if ($param->{'user'}{'last_login_date'});
        Sympa::User::update_global_user($param->{'user'}{'email'},
            {last_login_date => time(), last_login_host => $ip});
    } elsif ($param->{'ticket_context'}{'result'} eq 'closed') {
        wwslog(
            'info',
            '(%s) Refusing to perform login because the ticket has been used before',
            $in{'ticket'}
        );
        return 1;
    } else {
        wwslog('err',
            '(%s) Unable to evaluate the ticket validity (status: %s)',
            $in{'ticket'}, $param->{'ticket_context'}{'result'});
        return 1;
    }
    _split_params($param->{'ticket_context'}{'data'});
    return $in{'action'};

}

## Login WWSympa
sub do_login {
    wwslog('info', '(%s)', $in{'email'});
    my $user;
    my $next_action;

    if ($in{'referer'}) {
        $param->{'redirect_to'} =
            Sympa::Tools::Text::unescape_chars($in{'referer'});
    } elsif ($in{'previous_action'}
        && $in{'previous_action'} !~ /^(login|logout|loginrequest)$/) {
        $next_action = $in{'previous_action'};
        $in{'list'} = $in{'previous_list'};
    } else {
        $next_action = Conf::get_robot_conf($robot, 'default_home');
    }
    # never return to login or logout when login.
    $next_action = Conf::get_robot_conf($robot, 'default_home')
        if $in{'next_action'} =~ /^(login|logout)$/;

    if ($param->{'user'}{'email'}) {
        Sympa::Report::reject_report_web('user', 'already_login',
            {'email' => $param->{'user'}{'email'}},
            $param->{'action'}, '');
        wwslog('info', 'User %s already logged in',
            $param->{'user'}{'email'});
        web_db_log(
            {   'parameters'   => $in{'email'},
                'target_email' => $in{'email'},
                'status'       => 'error',
                'error_type'   => 'already_login'
            }
        );
        if ($param->{'nomenu'}) {
            $param->{'back_to_mom'} = 1;
            return 1;
        } else {
            return $next_action;
        }
    }

    unless ($in{'email'}) {
        Sympa::Report::reject_report_web('user', 'no_email', {},
            $param->{'action'}, '');
        wwslog('info', 'No email');
        web_db_log(
            {   'parameters'   => $in{'email'},
                'target_email' => $in{'email'},
                'status'       => 'error',
                'error_type'   => "no_email"
            }
        );
        return $in{'previous_action'}
            || Conf::get_robot_conf($robot, 'default_home');
    }

    $session->{'unauthenticated_email'} = $param->{'unauthenticated_email'} =
        $in{'email'};

    unless ($in{'passwd'}) {
        my $url_redirect;
        #Does the email belongs to an ldap directory?
        if ($url_redirect = is_ldap_user($in{'email'})) {
            $param->{'redirect_to'} = $url_redirect
                if $url_redirect ne 'none';
        } elsif ($in{'failure_referer'}) {
            $param->{'redirect_to'} = $in{'failure_referer'};
        } else {
            $in{'init_email'} = $in{'email'};
            $param->{'init_email'} = $in{'email'};
            $param->{'escaped_init_email'} =
                Sympa::Tools::Text::escape_chars($in{'email'});

            Sympa::Report::reject_report_web('user', 'missing_arg',
                {'argument' => 'passwd'},
                $param->{'action'}, '');
            wwslog('info', 'Missing parameter passwd');
            web_db_log(
                {   'parameters'   => $in{'email'},
                    'target_email' => $in{'email'},
                    'status'       => 'error',
                    'error_type'   => "missing_parameter"
                }
            );
            $param->{'login_error'} = 'missing_password';
            return $in{'previous_action'} || 'renewpasswd';
        }
    }

    my $data;

    unless ($ENV{'REQUEST_METHOD'} eq 'POST') {
        $log->syslog(
            'notice',
            "Authentication failed, because do not use HTTP method POST but %s",
            $ENV{'REQUEST_METHOD'}
        );
        web_db_log(
            {   'parameters'   => $in{'email'},
                'target_email' => $in{'email'},
                'status'       => 'error',
                'error_type'   => 'not_using_post'
            }
        );
        return 'loginrequest';
    }

    unless ($data =
        Sympa::Auth::check_auth($robot, $in{'email'}, $in{'passwd'})) {
        $log->syslog('notice', 'Authentication failed');
        web_db_log(
            {   'parameters'   => $in{'email'},
                'target_email' => $in{'email'},
                'status'       => 'error',
                'error_type'   => 'authentication'
            }
        );
        my $unauthenticated_user = Sympa::User::get_global_user($in{'email'});
        if ($unauthenticated_user->{'wrong_login_count'} >
            Conf::get_robot_conf($robot, 'max_wrong_password')) {
            $param->{'login_error'} = 'password_reset';
        } else {
            $param->{'login_error'} = 'wrong_password';
        }
        if ($in{'previous_action'}) {
            delete $in{'passwd'};
            $in{'list'} = $in{'previous_list'};
            return $in{'previous_action'};
        } elsif ($in{'failure_referer'}) {
            $param->{'redirect_to'} = $in{'failure_referer'};
        } else {
            return 'renewpasswd';
        }
    }
    $param->{'user'}            = $data->{'user'};
    $param->{'last_login_host'} = $data->{'user'}{'last_login_host'};
    $param->{'last_login_date'} =
        $language->gettext_strftime("%d %b %Y at %H:%M:%S",
        localtime($data->{'user'}{'last_login_date'}))
        if ($data->{'user'}{'last_login_date'});
    $session->{'auth'} = $data->{'auth'};
    my $email = lc($param->{'user'}{'email'});
    $session->{'email'}                 = $email;
    $session->{'unauthenticated_email'} = '';

    Sympa::User::update_global_user(
        $param->{'user'}{'email'},
        {   last_login_date   => time(),
            last_login_host   => $ip,
            wrong_login_count => 0
        }
    );

    ## Set alt_email
    if ($data->{'alt_emails'}) {
        foreach my $k (keys %{$data->{'alt_emails'}}) {
            $param->{'alt_emails'}{$k} = $data->{'alt_emails'}{$k};
        }
    }

    unless ($param->{'alt_emails'}{$email}) {
        unless (
            Sympa::Session::set_cookie_extern(
                $Conf::Conf{'cookie'}, $param->{'cookie_domain'},
                %{$param->{'alt_emails'}}
            )
            ) {
            wwslog('notice', 'Could not set HTTP cookie for external_auth');
            web_db_log(
                {   'parameters' => $param->{'cookie_domain'} . ','
                        . join(',',
                        map {"$_ => $param->{'alt_emails'}{$_}"}
                            keys %{$param->{'alt_emails'} || {}}),
                    'target_email' => $in{'email'},
                    'status'       => 'error',
                    'error_type'   => 'cookie'
                }
            );
            return undef;
        }
    }

    ## Current authentication mode
    #$param->{'auth'} = $param->{'alt_emails'}{$param->{'user'}{'email'}} || 'classic';

    if ($session->{'lang'}) {
        # user did choose a specific language before being logged.  Apply it
        # as a user pref.
        # FIXME: Should users' language preference be changed?
        Sympa::User::update_global_user($param->{'user'}{'email'},
            {lang => $session->{'lang'}});
        $param->{'lang'} = $session->{'lang'};
    } else {
        # user did not choose a specific language, apply user pref for this
        # session.
        my $lang_context = (ref $list eq 'Sympa::List') ? $list : $robot;
        $param->{'lang'} =
            $language->set_lang($user->{'lang'},
            Sympa::best_language($lang_context));
        $session->{'lang'} = $param->{'lang'};
    }
    # compatibility: old-style locale.
    $param->{'locale'} = Sympa::Language::lang2oldlocale($param->{'lang'});
    # compatibility: 6.1.
    $param->{'lang_tag'} = $param->{'lang'};

    if ($session->{'review_page_size'}) {
        # user did choose a specific page size upgrade prefs
        Sympa::User::update_global_user(
            $param->{'user'}{'email'},
            {   data => Sympa::Tools::Data::hash_2_string(
                    $param->{'user'}{'prefs'}
                )
            }
        );
    }

    if ($session->{'shared_mode'}) {
        # user did choose a shared expert/standard mode
        Sympa::User::update_global_user(
            $param->{'user'}{'email'},
            {   data => Sympa::Tools::Data::hash_2_string(
                    $param->{'user'}{'prefs'}
                )
            }
        );
    }

    if ($in{'newpasswd1'} && $in{'newpasswd2'}) {
        my $old_action = $param->{'action'};
        $param->{'action'} = 'setpasswd';
        do_setpasswd();
        $param->{'action'} = $old_action;
    }

    if ($param->{'nomenu'}) {
        $param->{'back_to_mom'} = 1;
        return 1;
    }
    web_db_log(
        {   'parameters'   => $in{'email'},
            'target_email' => $in{'email'},
            'status'       => 'success'
        }
    );

    web_db_stat_log();

    do_redirect($session->{'redirect_url'});
    return;

}

## Login WWSympa
## The sso_login action is made of 4 subactions that make a complete workflow.
## Note that this comlexe workflow is only used if the SSO server does not
## provide
## the user email address or if this email address is not trusted and
## therefore
## needs to be checked.
## The workflow:
##  1) init: determine if email address needs to be collected/checked
##  2) requestemail: collect the user email address in a web form. Note that
##  form may be initialized with
##     one email address provided by the SSO server
##  3) validateemail: a challenge is sent to the email address to validate it
##  4) confirmemail: user confirms his email address with the challenge
sub do_sso_login {
    wwslog('info', '(%s)', $in{'auth_service_name'});

    delete $session->{'do_not_use_cas'
        };    #when user require CAS login, reset do_not_use_cas cookie
    my $next_action;

    if ($param->{'user'}{'email'}) {
        Sympa::Report::reject_report_web('user', 'already_login',
            {'email' => $param->{'user'}{'email'}},
            $param->{'action'}, '');
        wwslog('err', 'User %s already logged in', $param->{'user'}{'email'});
        web_db_log(
            {   'parameters' => $in{'auth_service_name'},
                'status'     => 'error',
                'error_type' => "already_login"
            }
        );
        return Conf::get_robot_conf($robot, 'default_home');
    }

    ## This is a CAS service
    if (defined(
            my $cas_id =
                $Conf::Conf{'cas_id'}{$robot}{$in{'auth_service_name'}}
                {'casnum'}
        )
        ) {
        my $cas_server =
            $Conf::Conf{'auth_services'}{$robot}[$cas_id]{'cas_server'};

        $session->{'checked_cas'} = $cas_id;
        my $service = Sympa::get_url(
            $robot, 'sso_login_succeeded',
            nomenu => $param->{'nomenu'},
            paths  => [$in{'auth_service_name'}],
        );

        my $redirect_url = $cas_server->getServerLoginURL($service);
        wwslog('info', '(%s)', $redirect_url);
        if ($redirect_url =~ /http(s)+\:\//i) {
            $in{'action'}           = 'redirect';
            $param->{'redirect_to'} = $redirect_url;
            $param->{'bypass'}      = 'extreme';
            $session->set_cookie($param->{'cookie_domain'}, 'session');
            #$session->set_cookie('localhost','session');
            print "Location: $param->{'redirect_to'}\n\n";
        }

    } elsif (
        defined(
            my $sso_id =
                $Conf::Conf{'generic_sso_id'}{$robot}
                {$in{'auth_service_name'}}
        )
        ) {
        ## Generic SSO

        ## If contacted via POST, then redirect the user to the URL for the
        ## access control to apply
        if ($ENV{'REQUEST_METHOD'} eq 'POST') {
            my @paths;
            my $service;

            if ($param->{'nomenu'}) {
                push @paths, 'nomenu';    #FIXME:Is it required?
            }

            wwslog('info', 'POST request processing');

            if ($in{'subaction'} eq 'validateemail') {
                push @paths, 'validateemail', $in{'email'};
            } elsif ($in{'subaction'} eq 'confirmemail') {
                push @paths, 'confirmemail', $in{'email'}, $in{'ticket'};
            } else {
                push @paths, 'init';
            }

            $service = Sympa::get_url(
                $robot, 'sso_login',
                nomenu    => $param->{'nomenu'},
                paths     => [$in{'auth_service_name'}, @paths],
                authority => 'local'
            );

            wwslog('info', 'Redirect user to %s', $service);
            $in{'action'}           = 'redirect';
            $param->{'redirect_to'} = $service;
            $param->{'bypass'}      = 'extreme';
            print "Location: $param->{'redirect_to'}\n\n";

            return 1;
        }

        my $email;
        ## We need to collect/verify the user's email address
        if (defined $Conf::Conf{'auth_services'}{$robot}[$sso_id]
            {'force_email_verify'}) {
            my $email_is_trusted = 0;

            ## the subactions order is : init, requestemail, validateemail,
            ## sendssopasswd, confirmemail

            ## get email from NetiD table
            if (defined $Conf::Conf{'auth_services'}{$robot}[$sso_id]
                {'internal_email_by_netid'}) {
                wwslog('debug', 'Lookup email internal: %s', $sso_id);
                if ($email =
                    Sympa::Auth::get_email_by_net_id($robot, $sso_id, \%ENV))
                {
                    $email_is_trusted = 1;
                }
            }

            ## get email from authN module
            if (defined $Conf::Conf{'auth_services'}{$robot}[$sso_id]
                {'email_http_header'} && !$email_is_trusted) {
                my @email_list = split(
                    /$Conf::Conf{'auth_services'}{$robot}[$sso_id]{'http_header_value_separator'}/,
                    lc( $ENV{
                            $Conf::Conf{'auth_services'}{$robot}[$sso_id]
                                {'email_http_header'}
                            }
                    )
                );
                ## Only get the first occurrence if multi-valued
                $email = $email_list[0];
            }

            ## Start the email validation process
            if ($in{'subaction'} eq 'init'
                && ($email_is_trusted == 0 || !$email)) {
                wwslog('info', 'Return request email');
                $session->{'auth'}        = 'generic_sso';
                $param->{'server'}{'key'} = $in{'auth_service_name'};
                $param->{'subaction'}     = 'requestemail';
                $param->{'init_email'}    = $email;
                return 1;
            }

            if (defined($in{'email'}) and !($in{'subaction'} eq 'init')) {
                $email = $in{'email'};
            }

            ## Send a confirmation email and request it on the web interface
            if ($in{'subaction'} eq 'validateemail') {
                $session->{'auth'}        = 'generic_sso';
                $param->{'server'}{'key'} = $in{'auth_service_name'};
                $param->{'init_email'}    = $email;

                ## Replace sendpassword with one time ticket
                $param->{'one_time_ticket'} = Sympa::Ticket::create(
                    $in{'email'},
                    $robot,
                    'sso_login/confirmemail?auth_service_name='
                        . $in{'auth_service_name'},
                    $ip
                );

                unless (sendssopasswd($email)) {
                    Sympa::Report::reject_report_web('user',
                        'incorrect_email', {'email' => $email},
                        $param->{'action'});
                    $param->{'subaction'} = 'requestemail';
                    return 1;
                }

                $param->{'subaction'} = 'validateemail';
                return 1;
            }

            if ($in{'subaction'} eq 'confirmemail') {
                $session->{'auth'}        = 'generic_sso';
                $param->{'server'}{'key'} = $in{'auth_service_name'};
                $param->{'init_email'}    = $email;
                $in{'email'}              = $email;

                #
                # Check input parameters and verify ticket for email, stolen
                # from do_login()
                #
                unless ($in{'email'}) {
                    Sympa::Report::reject_report_web('user', 'no_email', {},
                        $param->{'action'});
                    wwslog('info', 'No email');
                    web_db_log(
                        {   'parameters'   => $in{'auth_service_name'},
                            'target_email' => $in{'email'},
                            'status'       => 'error',
                            'error_type'   => 'no_email'
                        }
                    );
                    $param->{'subaction'} = 'validateemail';
                    return 1;
                }

                unless ($in{'ticket'}) {
                    $in{'init_email'} = $in{'email'};
                    $param->{'init_email'} = $in{'email'};
                    $param->{'escaped_init_email'} =
                        Sympa::Tools::Text::escape_chars($in{'email'});

                    Sympa::Report::reject_report_web('user', 'missing_arg',
                        {'argument' => 'ticket'},
                        $param->{'action'});
                    wwslog('info', 'Confirmemail: missing parameter ticket');
                    web_db_log(
                        {   'parameters'   => $in{'auth_service_name'},
                            'target_email' => $in{'email'},
                            'status'       => 'error',
                            'error_type'   => 'missing_parameter'
                        }
                    );
                    $param->{'subaction'} = 'validateemail';
                    return 1;
                }

                ## Validate the ticket
                my $ticket_output =
                    Sympa::Ticket::load($robot, $in{'ticket'}, $ip);
                unless ($ticket_output->{'result'} eq 'success') {
                    Sympa::Report::reject_report_web('user', 'auth_failed',
                        {}, $param->{'action'});
                    web_db_log(
                        {   'parameters'   => $in{'auth_service_name'},
                            'target_email' => $in{'email'},
                            'status'       => 'error',
                            'error_type'   => 'authentication'
                        }
                    );
                    wwslog('err', 'Authentication failed');

                    $param->{'subaction'} = 'validateemail';
                    return 1;
                }

                wwslog('info', 'Confirmemail: email validation succeeded');
                # need to create netid to email map entry
                $email = $in{'email'};

                # everything is ok to proceed to with possible sympa account
                # created and traddional sso login

                ## TODO : netidmap_table should also be used when no
                ## confirmation is performed
                if (defined $Conf::Conf{'auth_services'}{$robot}[$sso_id]
                    {'internal_email_by_netid'}) {

                    my $netid =
                        $ENV{$Conf::Conf{'auth_services'}{$robot}[$sso_id]
                            {'netid_http_header'}};
                    my $idpname =
                        $Conf::Conf{'auth_services'}{$robot}[$sso_id]
                        {'service_id'};

                    unless (
                        Sympa::Robot::set_netidtoemail_db(
                            $robot, $netid, $idpname, $in{'email'}
                        )
                        ) {
                        Sympa::Report::reject_report_web('intern',
                            'db_update_failed', {}, $param->{'action'}, '',
                            $param->{'user'}{'email'}, $robot);
                        wwslog('err', 'Error update netid map');
                        web_db_log(
                            {   'parameters'   => $in{'auth_service_name'},
                                'target_email' => $in{'email'},
                                'status'       => 'error',
                                'error_type'   => 'internal'
                            }
                        );
                        return Conf::get_robot_conf($robot, 'default_home');
                    }

                } else {
                    wwslog('info', 'Confirmemail: validation failed');

                    $param->{'subaction'} = 'validateemail';
                    return 1;
                }
            }

        } else {
            ##
            if (defined $Conf::Conf{'auth_services'}{$robot}[$sso_id]
                {'email_http_header'}) {
                my @email_list = split(
                    $Conf::Conf{'auth_services'}{$robot}[$sso_id]
                        {'http_header_value_separator'},
                    lc( $ENV{
                            $Conf::Conf{'auth_services'}{$robot}[$sso_id]
                                {'email_http_header'}
                            }
                    )
                );
                ## Only get the first occurrence if multi-valued
                $email = $email_list[0];

            } else {
                unless (
                    defined $Conf::Conf{'auth_services'}{$robot}[$sso_id]
                    {'host'}
                    && defined $Conf::Conf{'auth_services'}{$robot}[$sso_id]
                    {'get_email_by_uid_filter'}) {
                    Sympa::Report::reject_report_web('intern',
                        'auth_conf_no_identified_user',
                        {}, $param->{'action'}, '', '', $robot);
                    wwslog('err',
                        'auth.conf error: Either email_http_header or host/get_email_by_uid_filter entries should be defined'
                    );
                    web_db_log(
                        {   'parameters'   => $in{'auth_service_name'},
                            'target_email' => $in{'email'},
                            'status'       => 'error',
                            'error_type'   => 'internal'
                        }
                    );
                    return 'home';
                }

                $email =
                    Sympa::Auth::get_email_by_net_id($robot, $sso_id, \%ENV);
            }
        }

        unless ($email) {
            Sympa::Report::reject_report_web('intern', 'no_identified_user',
                {}, $param->{'action'}, '', '', $robot);
            wwslog(
                'err',
                'User could not be identified, no %s HTTP header set',
                $Conf::Conf{'auth_services'}{$robot}[$sso_id]
                    {'email_http_header'}
            );
            web_db_log(
                {   'parameters' => $in{'auth_service_name'},

                    'status'     => 'error',
                    'error_type' => 'no_email'
                }
            );
            return 'home';
        }

        $param->{'user'}{'email'} = $email;
        $session->{'email'}       = $email;
        $session->{'auth'}        = 'generic_sso';

        wwslog('notice', 'User identified as %s', $email);

        ## There are two ways to list the attributes that Sympa will cache for
        ## the user
        ## Either with a defined header prefix (http_header_prefix)
        ## Or with an explicit list of header fields (http_header_list)
        my @sso_attr;
        if ($Conf::Conf{'auth_services'}{$robot}[$sso_id]{'http_header_list'})
        {
            my $list_of_headers =
                $Conf::Conf{'auth_services'}{$robot}[$sso_id]
                {'http_header_list'};

            foreach my $field (split(/,/, $list_of_headers)) {
                if (defined $ENV{$field}) {
                    push @sso_attr, $field . '__PAIRS_SEP__' . $ENV{$field};
                }
            }

        } elsif ($Conf::Conf{'auth_services'}{$robot}[$sso_id]
            {'http_header_prefix'}) {

            my $prefix =
                $Conf::Conf{'auth_services'}{$robot}[$sso_id]
                {'http_header_prefix'};
            foreach my $k (keys %ENV) {
                if ($k =~ /^$prefix/) {
                    push @sso_attr, $k . '__PAIRS_SEP__' . $ENV{$k};
                }
            }
        }

        my $all_sso_attr = join '__ATT_SEP__', @sso_attr;

        ## Create user entry if required
        unless (Sympa::User::is_global_user($email)) {
            unless (Sympa::User::add_global_user({'email' => $email})) {
                Sympa::Report::reject_report_web('intern',
                    'add_user_db_failed', {'email' => $email},
                    $param->{'action'}, '', $email, $robot);
                wwslog('info', 'Add failed');
                web_db_log(
                    {   'parameters'   => $in{'auth_service_name'},
                        'target_email' => $in{'email'},
                        'status'       => 'error',
                        'error_type'   => 'internal'
                    }
                );
                return undef;
            }
        }

        unless (
            Sympa::User::update_global_user(
                $email, {'attributes' => $all_sso_attr}
            )
            ) {
            Sympa::Report::reject_report_web('intern',
                'update_user_db_failed', {'user' => Sympa::User->new($email)},
                $param->{'action'}, '', $email, $robot);
            wwslog('info', 'Update failed');
            web_db_log(
                {   'parameters'   => $in{'auth_service_name'},
                    'target_email' => $in{'email'},
                    'status'       => 'error',
                    'error_type'   => 'internal'
                }
            );
            return undef;
        }

        Sympa::Report::notice_report_web('you_have_been_authenticated', {},
            $param->{'action'});

        ## Keep track of the SSO used to login
        ## Required to provide logout feature if available
        $session->{'sso_id'} = $in{'auth_service_name'};

        do_redirect($session->{'redirect_url'});
        return;
    } else {
        ## Unknown SSO service
        Sympa::Report::reject_report_web(
            'intern',
            'unknown_authentication_service',
            {'name' => $in{'auth_service_name'}},
            $param->{'action'}, '', '', $robot
        );
        wwslog(
            'err',
            'Unknown authentication service %s',
            $in{'auth_service_name'}
        );
        web_db_log(
            {   'parameters'   => $in{'auth_service_name'},
                'target_email' => $in{'email'},
                'status'       => 'error',
                'error_type'   => 'internal'
            }
        );
        return 'home';
    }
    web_db_log(
        {   'parameters'   => $in{'auth_service_name'},
            'target_email' => $in{'email'},
            'status'       => 'success'
        }
    );
    return 1;
}

sub do_sso_login_succeeded {
    wwslog('info', '(%s)', $in{'auth_service_name'});

    if (defined $param->{'user'} && $param->{'user'}{'email'}) {
        Sympa::Report::notice_report_web('you_have_been_authenticated', {},
            $param->{'action'});
        web_db_log(
            {   'parameters' => $in{'auth_service_name'},
                'status'     => 'success'
            }
        );

    } else {
        Sympa::Report::reject_report_web('user', 'auth_failed', {},
            $param->{'action'});
        web_db_log(
            {   'parameters' => $in{'auth_service_name'},
                'status'     => 'error',
                'error_type' => 'authentication'
            }
        );
    }

    ## We should refresh the main window
    if ($param->{'nomenu'}) {
        $param->{'back_to_mom'} = 1;
        return 1;
    } else {
        do_redirect($session->{'redirect_url'});
        return;
    }
}

sub is_ldap_user {
    my $auth = shift;    ## User email or UID
    wwslog('debug2', '(%s)', $auth);

    unless (Sympa::search_fullpath($robot, 'auth.conf')) {
        return undef;
    }

    # List all LDAP servers first
    my @ldap_servers;
    foreach my $ldap (@{$Conf::Conf{'auth_services'}{$robot}}) {
        next unless ($ldap->{'auth_type'} eq 'ldap');

        push @ldap_servers, $ldap;
    }

    unless (@ldap_servers) {
        return undef;
    }

    my $filter;

    foreach my $ldap (@ldap_servers) {
        # skip ldap auth service if the user id or email do not match regexp
        # auth service parameter
        next unless $auth =~ /$ldap->{'regexp'}/i;

        my $db = Sympa::Database->new('LDAP', %$ldap);
        unless ($db and $db->connect) {
            $log->syslog('err', 'Unable to connect to the LDAP server "%s"',
                $ldap->{'host'});
            next;
        }

        my @alternative_conf =
            split(/,/, $ldap->{'alternative_email_attribute'});
        my $attrs = $ldap->{'email_attribute'};

        if (Sympa::Tools::Text::valid_email($auth)) {
            $filter = $ldap->{'get_dn_by_email_filter'};
        } else {
            $filter = $ldap->{'get_dn_by_uid_filter'};
        }
        $filter =~ s/\[sender\]/$auth/ig;

        ## !! une fonction get_dn_by_email/uid

        my $mesg = $db->do_operation(
            'search',
            base    => $ldap->{'suffix'},
            filter  => "$filter",
            scope   => $ldap->{'scope'},
            timeout => $ldap->{'timeout'}
        );

        unless ($mesg and $mesg->count()) {
            wwslog('notice',
                'No entry in the LDAP Directory Tree of %s for %s',
                $ldap->{'host'}, $auth);
            $db->disconnect();
            last;
        }

        $db->disconnect();
        return $ldap->{'authentication_info_url'} || 'none';
    }

    return undef;
}

## send back login form
sub do_loginrequest {
    wwslog('info', '');

    if ($param->{'user'}{'email'}) {
        Sympa::Report::reject_report_web('user', 'already_login',
            {'email' => $param->{'user'}{'email'}},
            $param->{'action'});
        wwslog('info', 'Already logged in as %s', $param->{'user'}{'email'});
        return undef;
    }

    if ($in{'init_email'}) {
        $param->{'init_email'} = $in{'init_email'};
    }

    if ($in{'previous_action'} eq 'referer') {
        $param->{'referer'} =
            Sympa::Tools::Text::escape_chars($ENV{'HTTP_REFERER'});
    } elsif (!$param->{'previous_action'}) {
        $param->{'previous_action'} = 'loginrequest';
    }

    $param->{'title'} = 'Login'
        if ($param->{'nomenu'});

    return 1;
}

## Help / about WWSympa
sub do_help {
    wwslog('info', '(%s)', $in{'help_topic'});

    ## Contextual help
    if ($in{'help_topic'}) {
        if ($in{'help_topic'} eq 'editlist') {
            foreach my $pname (sort Sympa::List::by_order keys %{$pinfo}) {
                next if grep { $pname eq $_ } qw(comment defaults);
                # Skip obsoleted parameters and alias names.
                next if $pinfo->{$pname}{'obsolete'};

                if ($pinfo->{$pname}{'gettext_id'}) {
                    $param->{'param'}{$pname}{'title'} =
                        $language->gettext($pinfo->{$pname}{'gettext_id'});
                } else {
                    $param->{'param'}{$pname}{'title'} = $pname;
                }
                if ($pinfo->{$pname}{'gettext_comment'}) {
                    $param->{'param'}{$pname}{'comment'} =
                        $language->gettext(
                        $pinfo->{$pname}{'gettext_comment'});
                }
            }
        }

        $param->{'help_topic'} = $in{'help_topic'};
    }

    return 1;
}

# update session cookie and redirect the client to redirect_to parameter or
# glob var;
sub do_redirect {

    my $redirect_to = shift;
    wwslog('info', '(%s)', $redirect_to);

    $redirect_to ||= $param->{'redirect_to'};
    # Because of some bug Sympa did redirection to an empty URL.  Lines below
    # should prevent it.
    $redirect_to ||= Sympa::get_url(
        $robot, undef,
        nomenu    => $param->{'nomenu'},
        authority => 'local'
    );

    #$session->set_cookie('localhost','session');
    $session->set_cookie($param->{'cookie_domain'}, 'session');
    print "Location: $redirect_to\n\n";
    $param->{'bypass'} = 'extreme';
    return 1;
}

## Logout from WWSympa
sub do_logout {
    wwslog('info', '(%s)', $param->{'user'}{'email'});

    delete $param->{'user'};
    $session->{'email'} = 'nobody';

    # no reason to alter the lang because user perform logout
    # $param->{'lang'} = $param->{'cookie_lang'} =
    #     cookielib::check_lang_cookie($ENV{'HTTP_COOKIE'})
    #     || $list->{'admin'}{'lang'}
    #     || Conf::get_robot_conf($robot, 'lang');

    if (defined $session->{'cas_server'}
        && (defined $Conf::Conf{'auth_services'}{$robot}
            [$session->{'cas_server'}])
        ) {
        # this user was logged using CAS
        my $cas_server =
            $Conf::Conf{'auth_services'}{$robot}[$session->{'cas_server'}]
            {'cas_server'};

        $in{'action'} = 'redirect';
        my $return_url = Sympa::Tools::WWW::get_my_url($robot);
        $return_url =~ s{/logout\b}{};

        $param->{'redirect_to'} =
            $cas_server->getServerLogoutURL($return_url);

        delete $session->{'cas_server'};
        return 'redirect';
    } elsif (defined $session->{'sso_id'}) {
        # this user was logged using a generic_sso

        ## Check if logout_url is known for this SSO
        my $sso;
        unless (
            $sso = Conf::get_sso_by_id(
                robot      => $robot,
                service_id => $session->{'sso_id'}
            )
            ) {

            wwslog('err', 'Unknown SSO service_id "%s"',
                $session->{'sso_id'});
            return undef;
        }

        ## Remove sso_id
        delete $session->{'sso_id'};

        if ($sso->{'logout_url'}) {

            $in{'action'} = 'redirect';
            $param->{'redirect_to'} = $sso->{'logout_url'};

            return 'redirect';
        }
    }

    wwslog('info', 'Logout performed');
    web_db_log(
        {   'parameters'   => $param->{'user'}{'email'},
            'target_email' => $in{'email'},
            'status'       => 'success'
        }
    );

    web_db_stat_log();

    if ($in{'previous_action'} eq 'referer') {
        $param->{'referer'} =
            Sympa::Tools::Text::escape_chars($in{'previous_list'});
    }

    return Conf::get_robot_conf($robot, 'default_home');
}

sub sendssopasswd {
    my $email = shift;
    $log->syslog('info', '(%s)', $email);

    my ($passwd, $user);

    unless ($email) {
        Sympa::Report::reject_report_web('user', 'no_email', {},
            $param->{'action'});
        wwslog('info', 'No email');
        web_db_log(
            {   'parameters'   => $email,
                'target_email' => $email,
                'status'       => 'error',
                'error_type'   => "no_email"
            }
        );
        return 'requestemail';
    }

    unless (Sympa::Tools::Text::valid_email($email)) {
        Sympa::Report::reject_report_web('user', 'incorrect_email',
            {'email' => $email},
            $param->{'action'});
        wwslog('info', 'Incorrect email %s', $email);
        web_db_log(
            {   'parameters'   => $email,
                'target_email' => $email,
                'status'       => 'error',
                'error_type'   => "incorrect_email"
            }
        );

        return 'requestemail';
    }

    my $url_redirect;

    if ($param->{'newuser'} = Sympa::User::get_global_user($email)) {

        ## Create a password if none
        unless ($param->{'newuser'}{'password'}) {
            unless (
                Sympa::User::update_global_user(
                    $email,
                    {   'password' =>
                            Sympa::Tools::Password::tmp_passwd($email)
                    }
                )
                ) {
                Sympa::Report::reject_report_web('intern', 'db_update_failed',
                    {}, $param->{'action'}, '', $param->{'user'}{'email'},
                    $robot);
                wwslog('info', 'Update failed');
                web_db_log(
                    {   'parameters'   => $email,
                        'target_email' => $email,
                        'status'       => 'error',
                        'error_type'   => "internal"
                    }
                );
                return undef;
            }
            $param->{'newuser'}{'password'} =
                Sympa::Tools::Password::tmp_passwd($email);
        }

        $param->{'newuser'}{'escaped_email'} =
            Sympa::Tools::Text::escape_chars($param->{'newuser'}{'email'});

    } else {

        $param->{'newuser'} = {
            'email'         => $email,
            'escaped_email' => Sympa::Tools::Text::escape_chars($email),
            'password'      => Sympa::Tools::Password::tmp_passwd($email)
        };

    }

    $param->{'init_passwd'} = 1
        if ($param->{'user'}{'password'} =~ /^init/);

    #FIXME: check error
    Sympa::send_file($robot, 'sendssopasswd', $email, $param);

    $param->{'email'} = $email;
    web_db_log(
        {   'parameters'   => $email,
            'target_email' => $email,
            'status'       => 'success'
        }
    );

    return 'validateemail';
}

sub do_firstpasswd {
    wwslog('info', '(%s)', $in{'email'});
    $param->{'requestpasswd_context'} = 'firstpasswd';
    return 'renewpasswd';
}
## send a ticket for choosing a new password
sub do_renewpasswd {
    wwslog('info', '(%s)', $in{'email'});

    my $url_redirect;
    if ($in{'email'}) {
        if ($url_redirect = is_ldap_user($in{'email'})) {
            $param->{'redirect_to'} = $url_redirect
                if $url_redirect ne 'none';
        } elsif (!Sympa::Tools::Text::valid_email($in{'email'})) {
            Sympa::Report::reject_report_web('user', 'incorrect_email',
                {'email' => $in{'email'}},
                $param->{'action'});
            wwslog('info', 'Incorrect email "%s"', $in{'email'});
            web_db_log(
                {   'parameters'   => $in{'email'},
                    'target_email' => $in{'email'},
                    'status'       => 'error',
                    'error_type'   => 'incorrect_email'
                }
            );
            return undef;
        }
    }

    $param->{'email'} = $in{'email'};
    web_db_log(
        {   'parameters'   => $in{'email'},
            'target_email' => $in{'email'},
            'status'       => 'success',
        }
    );

    if ($in{'previous_action'} eq 'referer') {
        $param->{'referer'} =
            Sympa::Tools::Text::escape_chars($in{'previous_list'});
    }
    return 1;
}

####################################################
# do_requestpasswd
####################################################
#  Sends a message to the user containing user password.
#
# IN : -
#
# OUT : 'renewpasswd' |  1 | 'loginrequest' | undef
#
####################################################
sub do_requestpasswd {
    wwslog('info', '(%s)', $in{'email'});
    my ($passwd, $user);

    $param->{'account_creation'} = 1;

    my $url_redirect;
    if ($url_redirect = is_ldap_user($in{'email'})) {
        ## There might be no authentication_info_url URL defined in auth.conf
        if ($url_redirect eq 'none') {
            Sympa::Report::reject_report_web('user', 'ldap_user', {},
                $param->{'action'});
            wwslog('info', 'LDAP user %s, cannot remind password',
                $in{'email'});
            web_db_log(
                {   'parameters'   => $in{'email'},
                    'target_email' => $in{'email'},
                    'status'       => 'error',
                    'error_type'   => 'internal'
                }
            );
            return 'home';
        } else {
            $param->{'redirect_to'} = $url_redirect;
            return 1;
        }
    }

    ## Check auth.conf before creating/sending a password
    unless (Sympa::Auth::may_use_sympa_native_auth($robot, $in{'email'})) {
        ## TODO: Error handling
        Sympa::Report::reject_report_web('user',
            'passwd_reminder_not_allowed', {}, $param->{'action'});
        return undef;
    }
    wwslog('debug', 'Sending one time ticket for %s', $in{'email'});
    $param->{'one_time_ticket'} =
        Sympa::Ticket::create($in{'email'}, $robot, 'choosepasswd', $ip);
    $param->{'request_from_host'} = $ip;
    unless ($param->{'newuser'} = Sympa::User::get_global_user($in{'email'}))
    {
        $param->{'newuser'} =
            {'email' => Sympa::Tools::Text::canonic_email($in{'email'})};
        $param->{'newuser'}{'escaped_email'} =
            Sympa::Tools::Text::escape_chars($param->{'newuser'}{'email'});
    }
    if ($param->{'one_time_ticket'}) {
        $param->{'login_error'} = 'ticket_sent';
        unless (Sympa::send_file($robot, 'sendpasswd', $in{'email'}, $param))
        {
            wwslog('notice', 'Unable to send template "sendpasswd" to %s',
                $in{'email'});
            $param->{'login_error'} = 'unable_to_send_ticket';
        }
    } else {
        wwslog('notice', "Unable to create_one_time_ticket");
        Sympa::Report::reject_report_web('user', 'passwd_reminder_error', {},
            $param->{'action'});
        $param->{'login_error'} = 'unable_to_create_ticket';
    }

    return 1 unless ($param->{'previous_action'});
    return $param->{'previous_action'};
}

sub do_my {
    wwslog('info', '');

    # Sets the date of the field "start date" to "today"
    $param->{'d_day'} = POSIX::strftime('%d-%m-%Y', localtime time);
    _set_my_lists_info();
    return 1;
}

## Which list the user is subscribed to
## TODO (pour listmaster, toutes les listes)
sub do_which {
    my $which = {};

    wwslog('info', '');

    $param->{'get_which'} = undef;
    $param->{'which'}     = undef;

    foreach my $role ('member', 'owner', 'editor') {

        foreach my $list (
            Sympa::List::get_which($param->{'user'}{'email'}, $robot, $role))
        {
            my $l = $list->{'name'};

            my $result = Sympa::Scenario::request_action(
                $list,
                'visibility',
                $param->{'auth_method'},
                {   'sender'      => $param->{'user'}{'email'},
                    'remote_host' => $param->{'remote_host'},
                    'remote_addr' => $param->{'remote_addr'}
                }
            );

            my $r_action;
            $r_action = $result->{'action'} if (ref($result) eq 'HASH');

            next unless ($r_action =~ /do_it/);

            $param->{'which'}{$l}{'subject'} = $list->{'admin'}{'subject'};
            $param->{'which'}{$l}{'host'}    = $list->{'admin'}{'host'};

            if ($role eq 'member') {
                push @{$param->{'get_which'}}, $list;
            }

            if ($role eq 'owner' || $role eq 'editor') {
                $param->{'which'}{$l}{'admin'} = 1;
            }

            ## For compatibility concerns (3.0)
            ## To be deleted one of these day
            $param->{$role}{$l}{'subject'} = $list->{'admin'}{'subject'};
            $param->{$role}{$l}{'host'}    = $list->{'admin'}{'host'};

        }

    }
    return 1;
}

## The list of list
sub do_lists {
    my @lists;
    wwslog('info', '(%s, %s)', $in{'topic'}, $in{'subtopic'});

    my $all_lists = [];
    if ($in{'topic'} and $in{'topic'} eq '@which') {
        my %lists = ();
        foreach my $role ('member', 'owner', 'editor') {
            foreach my $list (
                Sympa::List::get_which(
                    $param->{'user'}{'email'},
                    $robot, $role
                )
                ) {
                $lists{$list->{'name'}} = $list;
            }
        }
        $all_lists = [map { $lists{$_} } sort keys %lists];
        $param->{'subtitle'} = $language->gettext('Your lists');
    } elsif ($in{'topic'}) {
        my %topics = Sympa::Robot::load_topics($robot);
        $param->{'topic'} = $in{'topic'};
        if ($in{'subtopic'}) {
            $param->{'subtopic'} = $in{'subtopic'};
            $param->{'subtitle'} = $language->gettext_sprintf(
                '%s / %s',
                $topics{$in{'topic'}}{'current_title'},
                $topics{$in{'topic'}}{'sub'}{$in{'subtopic'}}{'current_title'}
                    || $in{'subtopic'}
            );
        } elsif ($topics{$in{'topic'}}{'current_title'}) {
            $param->{'subtitle'} = $topics{$in{'topic'}}{'current_title'};
        } elsif ($in{'topic'} eq 'others' or $in{'topic'} eq 'topicsless') {
            $param->{'subtitle'} = $language->gettext('Others');
        } else {
            $param->{'subtitle'} = $in{'topic'};
        }

        ## Filter lists by topic.
        ## topic argument 'topicsless' or 'other' means 'lists with topic
        ## "other" or without topics'.
        ## no topic argument; List all lists
        my $options = {};
        if ($in{'topic'}) {
            if ($in{'subtopic'}) {
                $options->{'filter'} =
                    ['topics' => "$in{'topic'}/$in{'subtopic'}"];
            } else {
                $options->{'filter'} = ['topics' => $in{'topic'}];
            }
        }
        $all_lists = Sympa::List::get_lists($robot, %$options);
    } else {
        $all_lists = Sympa::List::get_lists($robot);
    }

    foreach my $list (@$all_lists) {
        my $sender = $param->{'user'}{'email'} || 'nobody';

        my $result = Sympa::Scenario::request_action(
            $list,
            'visibility',
            $param->{'auth_method'},
            {   'sender'      => $sender,
                'remote_host' => $param->{'remote_host'},
                'remote_addr' => $param->{'remote_addr'},
                'options'     => {'dont_reload_scenario' => 1}
            }
        );

        my $r_action;
        $r_action = $result->{'action'} if (ref($result) eq 'HASH');

        next unless ($r_action eq 'do_it');

        my $list_info = {};
        $list_info->{'subject'} = $list->{'admin'}{'subject'};
        $list_info->{'host'}    = $list->{'admin'}{'host'};
        $list_info->{'date_epoch'} =
            $list->{'admin'}{'creation'}{'date_epoch'};
        $list_info->{'date'}   = $list->{'admin'}{'creation'}{'date'};
        $list_info->{'topics'} = $list->{'admin'}{'topics'};

        if (    $param->{'user'}{'email'}
            and $list->is_admin('privileged_owner', $param->{'user'}{'email'})
            ) {
            $list_info->{'is_privileged_owner'} = 1;
            $list_info->{'is_owner'}            = 1;
            # Compat. < 6.2b.2.
            $list_info->{'admin'} = 1;
        }
        if (    $param->{'user'}{'email'}
            and $list->is_admin('owner', $param->{'user'}{'email'})) {
            $list_info->{'is_owner'} = 1;
            # Compat. < 6.2b.2.
            $list_info->{'admin'} = 1;
        }
        if (    $param->{'user'}{'email'}
            and $list->is_admin('actual_editor', $param->{'user'}{'email'})) {
            $list_info->{'is_editor'} = 1;
            # Compat. < 6.2b.2.
            $list_info->{'admin'} = 1;
        }
        if (    $param->{'user'}{'email'}
            and $list->is_list_member($param->{'user'}{'email'})) {
            $list_info->{'is_subscriber'} = 1;
        }

        my $listname = $list->{'name'};

        $param->{'which'} ||= {};
        $param->{'which'}{$listname} = $list_info;
        if ($listname =~ /^([a-z])/) {
            push @{$param->{'orderedlist'}{$1}}, $listname;
        } else {
            push @{$param->{'orderedlist'}{'others'}}, $listname;
        }
    }
    return 1;
}

sub do_lists_categories {
    wwslog('info', '');
    return 1;
}

## The list of latest created lists
sub do_latest_lists {
    wwslog('info', '(for=%s, count=%s, topic=%s, subtopic=%s)',
        $in{'for'}, $in{'count'}, $in{'topic'}, $in{'subtopic'});

    unless (do_lists()) {
        wwslog('err', 'Error while calling do_lists');
        return undef;
    }

    my $today = time;

    my $oldest_day;
    if (defined $in{'for'}) {
        $oldest_day = $today - (3600 * 24 * ($in{'for'}));
        $param->{'for'} = $in{'for'};
        unless ($oldest_day >= 0) {
            Sympa::Report::reject_report_web('user', 'nb_days_to_much',
                {'nb_days' => $in{'for'}},
                $param->{'action'});
            wwslog('err', 'Parameter "for" is too big"');
        }
    }

    my $nb_lists = 0;
    my @date_lists;
    foreach my $listname (keys(%{$param->{'which'}})) {
        if ($param->{'which'}{$listname}{'date_epoch'} < $oldest_day) {
            delete $param->{'which'}{$listname};
            next;
        }
        $nb_lists++;
    }

    if (defined $in{'count'}) {
        $param->{'count'} = $in{'count'};

        unless ($in{'count'}) {
            $param->{'which'} = undef;
        }
    }

    my $count_lists = 0;
    foreach my $l (
        sort {
            $param->{'which'}{$b}{'date_epoch'} <=> $param->{'which'}{$a}
                {'date_epoch'}
        } (keys(%{$param->{'which'}}))
        ) {

        $count_lists++;

        if ($in{'count'}) {
            if ($count_lists > $in{'count'}) {
                last;
            }
        }

        $param->{'which'}{$l}{'name'} = $l;
        push @{$param->{'latest_lists'}}, $param->{'which'}{$l};
    }

    $param->{'which'} = undef;

    return 1;
}

## The list of the most active lists
sub do_active_lists {
    wwslog('info', '(for=%s, count=%s, topic=%s, subtopic=%s)',
        $in{'for'}, $in{'count'}, $in{'topic'}, $in{'subtopic'});

    unless (do_lists()) {
        wwslog('err', 'Error while calling do_lists');
        return undef;
    }

    ## oldest interesting day
    my $oldest_day = 0;

    if (defined $in{'for'}) {
        $oldest_day = int(time / 86400) - $in{'for'};
        unless ($oldest_day >= 0) {
            Sympa::Report::reject_report_web('user', 'nb_days_to_much',
                {'nb_days' => $in{'for'}},
                $param->{'action'});
            wwslog('err', 'Parameter "for" is too big"');
            return undef;
        }
    }

    ## get msg count for each list
    foreach my $l (keys(%{$param->{'which'}})) {
        my $list = Sympa::List->new($l, $robot);
        my $file = "$list->{'dir'}/msg_count";

        my %count;

        if (open(MSG_COUNT, $file)) {
            while (<MSG_COUNT>) {
                if ($_ =~ /^(\d+)\s(\d+)$/) {
                    $count{$1} = $2;
                }
            }
            close MSG_COUNT;

            $param->{'which'}{$l}{'msg_count'} =
                count_total_msg_since($oldest_day, \%count);

            if ($in{'for'}) {
                my $average =
                    $param->{'which'}{$l}{'msg_count'} /
                    $in{'for'};    ## nb msg by day
                $average = int($average * 10);
                $param->{'which'}{$l}{'average'} = $average / 10; ## one digit
            }
        } else {
            $param->{'which'}{$l}{'msg_count'} = 0;
        }
    }

    my $nb_lists = 0;

    ## get "count" lists
    foreach my $l (
        sort {
            $param->{'which'}{$b}{'msg_count'} <=> $param->{'which'}{$a}
                {'msg_count'}
        } (keys(%{$param->{'which'}}))
        ) {
        if (defined $in{'count'}) {
            $nb_lists++;
            if ($nb_lists > $in{'count'}) {
                last;
            }
        }

        $param->{'which'}{$l}{'name'} = $l;
        push @{$param->{'active_lists'}}, $param->{'which'}{$l};

    }

    if (defined $in{'count'}) {
        $param->{'count'} = $in{'count'};
    }
    if (defined $in{'for'}) {
        $param->{'for'} = $in{'for'};
    }

    $param->{'which'} = undef;

    return 1;
}

sub do_including_lists {
    my %which;

    foreach my $role (qw(member owner editor)) {
        foreach my $l (@{$list->get_including_lists($role) || []}) {
            unless (exists $which{$l->get_id}) {
                # Check visibility.
                my $result = Sympa::Scenario::request_action(
                    $l,
                    'visibility',
                    $param->{'auth_method'},
                    {   'sender'      => $param->{'user'}{'email'},
                        'remote_host' => $param->{'remote_host'},
                        'remote_addr' => $param->{'remote_addr'},
                        'options'     => {'dont_reload_scenario' => 1}
                    }
                );
                my $action = $result->{'action'} if ref $result eq 'HASH';
                next unless $action;

                $which{$l->get_id} = {
                    host    => $l->{'admin'}{'host'},
                    name    => $l->{'name'},
                    robot   => $l->{'domain'},
                    subject => ($l->{'admin'}{'subject'} || $l->{'name'}),
                    url_abs => Sympa::get_url($l, 'info'),
                    url_rel =>
                        Sympa::get_url($l, 'info', authority => 'omit'),
                    visible => ($action =~ /\Ado_it\b/i),
                };
            }
            $which{$l->get_id}->{$role . '_include'} = 1;
        }
    }

    $param->{which} = {%which};

    return 1;
}

sub count_total_msg_since {
    my $oldest_day = shift;
    my $count      = shift;

    my $total = 0;
    foreach my $d (sort { $b <=> $a } (keys %$count)) {
        if ($d < $oldest_day) {
            last;
        }
        $total = $total + $count->{$d};
    }
    return $total;
}

## List information page
sub do_info {
    wwslog('info', '');

    ## Access control
    unless (defined check_authz('do_info', 'info')) {
        delete $param->{'list'};
        return undef;
    }

    ## Get List Description
    if (-r $list->{'dir'} . '/homepage') {
        my $file_path = $list->{'dir'} . '/homepage';
        unless (open FILE, "<", $file_path) {
            Sympa::Report::reject_report_web('intern', 'cannot_open_file',
                {'file' => $file_path},
                $param->{'action'}, $list, $param->{'user'}{'email'}, $robot);
            wwslog('err', 'Failed to open file %s: %s', $file_path, $ERRNO);
            web_db_log(
                {   'parameters' => $file_path,
                    'status'     => 'error',
                    'error_type' => 'internal'
                }
            );
            return undef;
        }
        while (<FILE>) {
            Encode::from_to($_, $Conf::Conf{'filesystem_encoding'}, 'utf8');
            $param->{'homepage_content'} .= $_;
        }
        close FILE;

        ## Used by previous templates
        $param->{'homepage'} = 1;
    } elsif (-r $list->{'dir'} . '/info') {
        my $file_path = $list->{'dir'} . '/info';
        unless (open FILE, "<", $file_path) {
            Sympa::Report::reject_report_web('intern', 'cannot_open_file',
                {'file' => $file_path},
                $param->{'action'}, $list, $param->{'user'}{'email'}, $robot);
            wwslog('err', 'Failed to open file %s: %s', $file_path, $ERRNO);
            web_db_log(
                {   'parameters' => $file_path,
                    'status'     => 'error',
                    'error_type' => 'internal'
                }
            );
            return undef;
        }
        while (<FILE>) {
            Encode::from_to($_, $Conf::Conf{'filesystem_encoding'}, 'utf8');
            $param->{'info_content'} .= $_;
        }
        close FILE;
        $param->{'info_content'} =~ s/\n/\<br\/\>/g;
    }

    push @other_include_path, $list->{'dir'};

    return 1;
}

## List subcriber count page
sub do_subscriber_count {
    wwslog('info', '');

    unless (do_info()) {
        wwslog('info', 'Error while calling do_info');
        return undef;
    }

    print "Content-type: text/plain\n\n";
    print $list->get_total() . "\n";

    $param->{'bypass'} = 'extreme';

    return 1;
}

## Subscribers' list
sub do_review {
    wwslog('info', '(%s)', $in{'page'});
    my $record;
    my @users;
    my $size;
    my $sortby = lc($in{'sortby'} || 'email');
    my %sources;

    ## Access control
    return undef unless (defined check_authz('do_review', 'review'));

    if ($in{'size'}) {
        $size = $in{'size'};
        $session->{'review_page_size'} = $in{'size'};
        if ($param->{'user'}{'prefs'}{'review_page_size'} ne $in{'size'}) {
            # update user pref  as soon as connected user change page size
            $param->{'user'}{'prefs'}{'review_page_size'} = $in{'size'};
            Sympa::User::update_global_user(
                $param->{'user'}{'email'},
                {   data => Sympa::Tools::Data::hash_2_string(
                        $param->{'user'}{'prefs'}
                    )
                }
            );
        }
    } else {
        $size =
               $param->{'user'}{'prefs'}{'review_page_size'}
            || $session->{'review_page_size'}
            || $Conf::Conf{'review_page_size'};
    }
    $param->{'review_page_size'} = $size;

    unless ($param->{'total'}) {
        wwslog('info', 'No subscriber');

        return 1;
    }

    ## Owner
    $param->{'page'} = $in{'page'} || 1;
    $param->{'total_page'} = int($param->{'total'} / $size);
    $param->{'total_page'}++
        if ($param->{'total'} % $size);

    if ($param->{'total_page'} > 0
        and ($param->{'page'} > $param->{'total_page'})) {
        Sympa::Report::reject_report_web('user', 'no_page',
            {'page' => $param->{'page'}},
            $param->{'action'}, $list);
        web_db_log({'status' => 'error', 'error_type' => 'out of pages'});
        wwslog('info', 'No page %d', $param->{'page'});
        return undef;
    }

    my $offset;
    if ($param->{'page'} > 1) {
        $offset = (($param->{'page'} - 1) * $size);
    } else {
        $offset = 0;
    }

    ## Additional DB fields
    my @additional_fields = split ',',
        $Conf::Conf{'db_additional_subscriber_fields'};

    ## Members list synchronization if list has included data sources.
    if ($list->has_include_data_sources()) {
        my $sync_result = $list->on_the_fly_sync_include(use_ttl => 1);
        unless (defined $sync_result) {
            Sympa::Report::reject_report_web('intern', 'sync_include_failed',
                {}, $param->{'action'}, $list, $param->{'user'}{'email'},
                $robot);
        } elsif ($sync_result) {
            Sympa::Report::notice_report_web('subscribers_updated', {},
                $param->{'action'});
        }
    }

    # Members list
    # Some review pages may be empty while viewed by subscribers.
    my @members = $list->get_members(
        ($param->{'is_priv'} ? 'member' : 'unconcealed_member'),
        (     ($sortby eq 'domain')
            ? (order => 'email')
            : (offset => $offset, order => $sortby, limit => $size)
        )
    );
    # Special treatment of key "domain".
    if ($sortby eq 'domain') {
        # Sort
        foreach my $u (@members) {
            $u ||= {};
            my ($local, $dom) = split /\@/, ($u->{email} || '');
            $u->{_dom} = join '.', reverse split(/[.]/, $dom);
        }
        @members = sort { $a->{_dom} cmp $b->{_dom} } @members;
        # Offset
        splice @members, 0, $offset if $offset and @members;
        # Size
        @members = splice @members, 0, $size if $size and @members;
    }
    foreach my $i (@members) {
        # Add user
        _prepare_subscriber($i, \@additional_fields, \%sources);
        push @{$param->{'members'}}, $i;
    }

    if ($param->{'page'} > 1) {
        $param->{'prev_page'} = $param->{'page'} - 1;
    }

    unless (($offset + $size) >= $param->{'total'}) {
        $param->{'next_page'} = $param->{'page'} + 1;
    }

    $param->{'size'}   = $size;
    $param->{'sortby'} = $sortby;

    ######################
    if ($in{'exclude'} eq '1') {
        $param->{'exclude_opt'} = 0;
    } else {
        $param->{'exclude_opt'} = 1;
    }
    #######################

    ## additional DB fields
    $param->{'additional_fields'} =
        $Conf::Conf{'db_additional_subscriber_fields'};
    web_db_log({'status' => 'success'});

    ## msg_topics
    if ($list->is_there_msg_topic()) {
        foreach my $top (@{$list->{'admin'}{'msg_topic'}}) {
            if (defined $top->{'name'}) {
                push(@{$param->{'available_topics'}}, $top);
            }
        }
    }

    return 1;
}

## Show the table of exclude
sub do_show_exclude {

    wwslog('info', '');
    # Get the emails of the exclude about a list and the date of their
    # insertion
    my $data_exclu = $list->get_exclusion();

    my $excluded;
    my $key = 0;
    while (($data_exclu->{emails}->[$key]) && ($data_exclu->{date}->[$key])) {
        my $email = $data_exclu->{'emails'}->[$key];
        my $date =
            $language->gettext_strftime("%d %b %Y",
            localtime($data_exclu->{'date'}->[$key]));

        $excluded = {
            'email' => $email,
            'since' => $date
        };
        push @{$param->{'exclude_users'}}, $excluded;
        $key = $key + 1;
    }
    return 1;
}

## Search in subscribers and in exclude
sub do_search {
    wwslog('info', '(%s)', $in{'filter'});

    my %sources;
    my %emails;

    ## Additional DB fields
    my @additional_fields = split ',',
        $Conf::Conf{'db_additional_subscriber_fields'};
    ## Access control
    return undef unless (defined check_authz('do_search', 'review'));
    ## Search key
    $param->{'filter'} = $in{'filter'};
    my $searchkey = undef;
    $searchkey = Sympa::Tools::Text::foldcase($param->{'filter'})
        if defined $param->{'filter'} and length $param->{'filter'};

    my $record = 0;
    ## Maximum size of selection
    my $max_select = 50;

    ## Members list
    for (
        my $i = $list->get_first_list_member({'sortby' => 'email'});
        $i;
        $i = $list->get_next_list_member()
        ) {

        ## Search filter
        next if $i->{'visibility'} eq 'conceal' and !$param->{'is_owner'};

        if (defined $searchkey) {
            my $gecos = undef;
            $gecos = Sympa::Tools::Text::foldcase($i->{'gecos'})
                if defined $i->{'gecos'};
            next
                unless index($i->{'email'}, $searchkey) >= 0
                    or (defined $gecos and index($gecos, $searchkey) >= 0);
        }

        ## Add user
        _prepare_subscriber($i, \@additional_fields, \%sources);

        $record++;
        push @{$param->{'members'}}, $i;
        $emails{$i->{'email'}} = 1;
    }

    my $data_exclu = $list->get_exclusion();
    my $key        = 0;
    ## Exclude users are searched too
    while (($data_exclu->{emails}->[$key]) && ($data_exclu->{date}->[$key])) {
        my $email = $data_exclu->{'emails'}->[$key];
        my $date =
            $language->gettext_strftime("%d %b %Y",
            localtime($data_exclu->{'date'}->[$key]));
        $key = $key + 1;

        ## Search filter
        next unless $param->{'is_owner'};

        if (defined $searchkey) {
            next unless index($email, $searchkey) >= 0;
        }

        my $excluded = {
            'email' => $email,
            'since' => $date
        };

        push @{$param->{'exclude_users'}}, $excluded;
        $record++;
    }

    if ($record > $max_select && $param->{'filter'} !~ /^\@[\w-]+\./) {
        undef $param->{'members'};
        $param->{'too_many_select'} = 1;
    }

    $param->{'similar_subscribers_occurrence'} = 0;
    if ($param->{'filter'} !~ /^\@[\w-]+\./) {
        foreach my $user (
            $list->get_resembling_members(
                ($param->{'is_owner'} ? 'member' : 'unconcealed_member'),
                $in{'filter'}
            )
            ) {
            next unless $user and $user->{email};

            next if $emails{$user->{email}};
            push @{$param->{'similar_subscribers'}}, $user;
            last if ($#{$param->{'similar_subscribers'}} + 1 > $max_select);
        }
        $param->{'similar_subscribers_occurrence'} =
            $#{$param->{'similar_subscribers'}} + 1;
    }
    # for misspelling in 6.2a or earlier.
    $param->{'similar_subscribers_occurence'} =
        $param->{'similar_subscribers_occurrence'};

    $param->{'occurrence'} = $record;
    return 1;
}

## Access to user preferences
sub do_pref {
    wwslog('info', '');

    ## Find nearest expiration period
    my $selected = 0;
    foreach my $p (sort { $b <=> $a } keys %Sympa::Tools::WWW::cookie_period)
    {
        my $entry = {'value' => $p};

        ## Set description from NLS
        $entry->{'desc'} =
            $language->gettext(
            $Sympa::Tools::WWW::cookie_period{$p}{'gettext_id'});

        ## Choose nearest delay
        if ((!$selected) && $param->{'user'}{'cookie_delay'} >= $p) {
            $entry->{'selected'} = 'selected="selected"';
            $selected = 1;
        }

        unshift @{$param->{'cookie_periods'}}, $entry;
    }

    $param->{'previous_list'}   = $in{'previous_list'};
    $param->{'previous_action'} = $in{'previous_action'};

    return 1;
}

## Set the initial password
sub do_choosepasswd {
    wwslog('info', '');

    if ($session->{'auth'} eq 'ldap') {
        Sympa::Report::reject_report_web('auth', '',
            {'login' => $param->{'need_login'}},
            $param->{'action'});
        wwslog('notice', 'User not authorized');
        web_db_log(
            {   'parameters'   => $in{'email'},
                'target_email' => $in{'email'},
                'status'       => 'error',
                'error_type'   => 'authorization'
            }
        );
    }

    unless ($param->{'user'}{'email'}) {
        unless ($in{'email'} && $in{'passwd'}) {
            Sympa::Report::reject_report_web('user', 'no_user', {},
                $param->{'action'});
            wwslog('info', 'No user');
            web_db_log(
                {   'parameters'   => $in{'email'},
                    'target_email' => $in{'email'},
                    'status'       => 'error',
                    'error_type'   => 'no_user'
                }
            );
            $param->{'previous_action'} = 'choosepasswd';
            return 'loginrequest';
        }

        $in{'previous_action'} = 'choosepasswd';
        return 'login';
    }
    web_db_log(
        {   'parameters'   => "$in{'email'}",
            'target_email' => $in{'email'} || $param->{'user'}{'email'},
            'status'       => 'success',
        }
    );
    $param->{'init_passwd'} = 1 if ($param->{'user'}{'password'} =~ /^INIT/i);

    return 1;
}

####################################################
# do_set
####################################################
# Changes subscription parameter (reception or visibility)
#
# IN : -
#
# OUT :'loginrequest'|'info' | undef

sub do_set {
    wwslog('info', '(%s, %s)', $in{'reception'}, $in{'visibility'});

    my ($reception, $visibility) = ($in{'reception'}, $in{'visibility'});
    my $email;

    if ($in{custom_attribute}) {
        return undef
            unless _check_custom_attribute($list, $param->{action},
            $in{custom_attribute});
    }

    if ($in{'email'}) {
        unless ($param->{'is_owner'}) {
            Sympa::Report::reject_report_web('auth', 'action_owner', {},
                $param->{'action'}, $list);
            wwslog('info', 'Not owner');
            web_db_log(
                {   'parameters' => "$in{'reception'},$in{'visibility'}",
                    'status'     => 'error',
                    'error_type' => 'authorization'
                }
            );
            return undef;
        }

        $email = Sympa::Tools::Text::unescape_chars($in{'email'});
    } else {
        unless ($param->{'user'}{'email'}) {
            Sympa::Report::reject_report_web('user', 'no_user', {},
                $param->{'action'});
            wwslog('info', 'No user');
            web_db_log(
                {   'parameters' => "$in{'reception'},$in{'visibility'}",
                    'status'     => 'error',
                    'error_type' => 'no_user'
                }
            );
            return 'loginrequest';
        }
        $email = $param->{'user'}{'email'};
    }

    unless ($list->is_list_member($email)) {
        Sympa::Report::reject_report_web('user', 'not_subscriber',
            {'list' => $param->{'list'}},
            $param->{'action'}, $list);
        wwslog('info', '%s not subscriber of list %s',
            $email, $param->{'list'});
        web_db_log(
            {   'parameters' => "$in{'reception'},$in{'visibility'}",
                'status'     => 'error',
                'error_type' => 'not_subscriber'
            }
        );
        return undef;
    }

    # Verify that the mode is allowed
    if (!$list->is_available_reception_mode($reception)) {
        Sympa::Report::reject_report_web(
            'user',
            'not_available_reception_mode',
            {'recpetion_mode' => $reception},
            $param->{'action'}, $list
        );
        return undef;
    }

    $reception  = '' if $reception  eq 'mail';
    $visibility = '' if $visibility eq 'noconceal';

    my $update = {
        'reception'   => $reception,
        'visibility'  => $visibility,
        'update_date' => time
    };

    ## Lower-case new email address
    $in{'new_email'} = lc($in{'new_email'});

    if ($in{'new_email'} and $in{'email'} ne $in{'new_email'}) {
        unless ($in{'new_email'}
            and Sympa::Tools::Text::valid_email($in{'new_email'})) {
            wwslog('notice', 'Incorrect email %s', $in{'new_email'});
            Sympa::Report::reject_report_web('user', 'incorrect_email',
                {'email' => $in{'new_email'}},
                $param->{'action'});
            web_db_log(
                {   'parameters' => "$in{'reception'},$in{'visibility'}",
                    'status'     => 'error',
                    'error_type' => 'incorrect_email'
                }
            );
            return undef;
        }

        ## Check if new email is already subscribed
        if ($list->is_list_member($in{'new_email'})) {
            Sympa::Report::reject_report_web('user', 'already_subscriber',
                {'list' => $list->{'name'}},
                $param->{'action'}, $list);
            wwslog('info', '%s already subscriber', $in{'new_email'});
            web_db_log(
                {   'parameters' => $in{'new_email'},
                    'status'     => 'error',
                    'error_type' => 'already subscriber'
                }
            );
            return undef;
        }

        ## Duplicate entry in user_table
        unless (Sympa::User::is_global_user($in{'new_email'})) {

            my $user_pref = Sympa::User::get_global_user($in{'email'});
            $user_pref->{'email'} = $in{'new_email'};
            Sympa::User::add_global_user($user_pref);
        }

        $update->{'email'} = $in{'new_email'};
    }

    ## message topic subscription
    if ($list->is_there_msg_topic()) {
        my @user_topics;

        if ($in{'no_topic'}) {
            $update->{'topics'} = undef;

        } else {
            foreach my $msg_topic (@{$list->{'admin'}{'msg_topic'}}) {
                my $var_name = "topic_" . "$msg_topic->{'name'}";
                if ($in{"$var_name"}) {
                    push @user_topics, $msg_topic->{'name'};
                }
            }

            if ($in{"topic_other"}) {
                push @user_topics, 'other';
            }

            $update->{'topics'} = join(',', @user_topics);
        }
    }

    if ($reception =~ /^(digest|digestplain|nomail|summary)$/i) {
        $update->{'topics'} = '';
    }

    ## Get additional DB fields
    foreach my $v (keys %in) {
        if ($v =~ /^additional_field_(\w+)$/) {
            $update->{$1} = $in{$v};
        }
    }

    if ($in{'gecos'}) {
        $update->{'gecos'} = $in{'gecos'};
    } else {
        $update->{'gecos'} = undef;
    }
    $update->{'custom_attribute'} = $in{custom_attribute}
        if $in{custom_attribute};

    unless ($list->update_list_member($email, $update)) {
        Sympa::Report::reject_report_web('intern',
            'update_subscriber_db_failed', {'sub' => $email},
            $param->{'action'}, $list, $param->{'user'}{'email'}, $robot);
        wwslog('info', 'Set failed');
        web_db_log(
            {   'parameters' => "$email,$update",
                'status'     => 'error',
                'error_type' => 'internal'
            }
        );
        return undef;
    }

    Sympa::Report::notice_report_web('performed', {}, $param->{'action'});
    web_db_log(
        {   'parameters' => "$in{'reception'},$in{'visibility'}",
            'status'     => 'success',
        }
    );

    return $in{'previous_action'} || 'info';
}

## checks if each element of the custom attribute is conform to the list's
## definition
# Old name: check_custom_attribute() in wwsympa.fcgi.
# TODO: This would be moved to a method of appropriate class.
sub _check_custom_attribute {
    my $list             = shift;
    my $action           = shift;
    my $custom_attribute = shift;

    my @custom_attributes = @{$list->{'admin'}{'custom_attribute'}};
    my $isOK              = 1;

    foreach my $ca (@custom_attributes) {
        my $value = $custom_attribute->{$ca->{id}}{value};
        if (    $ca->{optional}
            and $ca->{optional} eq 'required'
            and not(defined $value and length $value)) {
            Sympa::Report::reject_report_web('user', 'missing_arg',
                {'argument' => $ca->{name}}, $action);
            wwslog('info', 'Missing parameter "%s"', $ca->{id});
            web_db_log(
                {   'parameters' => $ca->{id},
                    'status'     => 'error',
                    'error_type' => 'missing_parameter'
                }
            );
            $isOK = undef;
            next;
        }

        # No further checking if attribute is empty.
        next unless defined $value and length $value;

        my @values = split /,/, $ca->{enum_values}
            if defined $ca->{enum_values};

        ## Check that the parameter has the correct format
        unless (($ca->{type} eq 'enum' and grep { $value eq $_ } @values)
            or ($ca->{type} eq 'integer' and $value =~ /\A\d+\z/)
            or ($ca->{type} eq 'string'  and $value =~ /\A.+\z/)
            or ($ca->{type} eq 'text'    and length $value)) {
            Sympa::Report::reject_report_web('user', 'syntax_errors',
                {'params' => $ca->{name}}, $action);
            wwslog('info', 'Syntax error in parameter "%s"', $ca->{id});
            web_db_log(
                {   'parameters' => $ca->{id},
                    'status'     => 'error',
                    'error_type' => 'missing_parameter'
                }
            );
            $isOK = undef;
            next;
        }
    }
    return $isOK;
}

## Update of user preferences
sub do_setpref {
    wwslog('info', '');
    my $changes = {};

    # Set session language and user language to new value
    # At first check if it is available lang.
    my $lang;
    if ($in{'lang'} and $lang = $language->set_lang($in{'lang'})) {
        $session->{'lang'} = $lang;
        $param->{'lang'}   = $lang;
        # compatibility: 6.1.
        $param->{'lang_tag'} = $lang;

        $changes->{'lang'} = $lang;
    }
    # other prefs.
    foreach my $p ('gecos', 'cookie_delay') {
        $changes->{$p} = $in{$p} if defined $in{$p};
    }

    if (Sympa::User::is_global_user($param->{'user'}{'email'})) {

        unless (
            Sympa::User::update_global_user(
                $param->{'user'}{'email'}, $changes
            )
            ) {
            Sympa::Report::reject_report_web(
                'intern', 'update_user_db_failed',
                {'user' => $param->{'user'}}, $param->{'action'},
                '', $param->{'user'}{'email'},
                $robot
            );
            wwslog('info', 'Update failed');
            web_db_log(
                {   'parameters' =>
                        "$in{'gecos'},$in{'lang'},$in{'cookie_delay'}",
                    'status'     => 'error',
                    'error_type' => 'internal'
                }
            );
            return undef;
        }
    } else {
        $changes->{'email'} = $param->{'user'}{'email'};
        unless (Sympa::User::add_global_user($changes)) {
            Sympa::Report::reject_report_web('intern', 'add_user_db_failed',
                {'user' => $param->{'user'}},
                $param->{'action'}, '', $param->{'user'}{'email'}, $robot);
            wwslog('info', 'Add failed');
            web_db_log(
                {   'parameters' =>
                        "$in{'gecos'},$in{'lang'},$in{'cookie_delay'}",
                    'status'     => 'error',
                    'error_type' => 'internal'
                }
            );
            return undef;
        }
    }

    $param->{'user'} =
        Sympa::User::get_global_user($param->{'user'}{'email'});

    web_db_log(
        {   'parameters' => "$in{'gecos'},$in{'lang'},$in{'cookie_delay'}",
            'status'     => 'success',
        }
    );
    if ($in{'previous_action'}) {
        $in{'list'} = $in{'previous_list'};
        return $in{'previous_action'};
    } else {
        return 'pref';
    }
}

## Prendre en compte les défauts
sub do_viewfile {
    wwslog('info', '');

    unless (defined $Sympa::Tools::WWW::filenames{$in{'file'}}) {
        Sympa::Report::reject_report_web('user', 'file_not_editable',
            {'file' => $in{'file'}},
            $param->{'action'});
        wwslog('info', 'File %s not editable', $in{'file'});
        return undef;
    }

    $param->{'file'} = $in{'file'};

    $param->{'filepath'} = $list->{'dir'} . '/' . $in{'file'};

    if ((-e $param->{'filepath'}) and (!-r $param->{'filepath'})) {
        Sympa::Report::reject_report_web('intern', 'cannot_read',
            {'filepath' => $param->{'filepath'}},
            $param->{'action'}, '', '', $robot);
        wwslog('info', 'Cannot read %s', $param->{'filepath'});
        return undef;
    }

    return 1;
}

####################################################
# do_subscribe
####################################################
# Subscribes a user to the list
#
# IN : -
#
# OUT :'subrequest'|'login'|'info'|$in{'previous_action'}
#     | undef
####################################################
## TOTO: accepter nouveaux users
sub do_subscribe {
    wwslog('info', '(%s)', $in{'email'});

    return undef if purely_closed('subscribe');    #FIXME: mv this to Scenario
    if ($param->{'user'} and $param->{'user'}{'email'}) {
        if ($list->{'admin'}{'custom_attribute'}) {
            # This variable is set in the subrequest form.
            # If not set, it means that the user has not been prompted to
            # provide custom_attributes.
            unless ($in{'via_subrequest'}) {
                wwslog('notice', 'Returning subrequest form');
                return "subrequest";
            }
            unless (
                _check_custom_attribute(
                    $list, $param->{action}, $in{custom_attribute}
                )
                ) {
                wwslog('notice', "Missing required custom attributes");
                return 'subrequest';
            }
        }

        if ($param->{'is_subscriber'}) {
            Sympa::Report::reject_report_web('user', 'already_subscriber',
                {'list' => $list->{'name'}},
                $param->{'action'}, $list);
            wwslog('info', '%s already subscriber',
                $param->{'user'}{'email'});
            web_db_log(
                {   'parameters' => $in{'email'},
                    'status'     => 'error',
                    'error_type' => 'already_subscriber'
                }
            );
            return undef;
        }
        my $result = Sympa::Scenario::request_action(
            $list,
            'subscribe',
            $param->{'auth_method'},
            {   'sender'      => $param->{'user'}{'email'},
                'remote_host' => $param->{'remote_host'},
                'remote_addr' => $param->{'remote_addr'}
            }
        );
        my $sub_is;
        my $reason;
        if (ref($result) eq 'HASH') {
            $sub_is = $result->{'action'};
            $reason = $result->{'reason'};
        }
        if ($sub_is =~ /reject/) {
            Sympa::Report::reject_report_web('auth', $reason, {},
                $param->{'action'}, $list);
            wwslog('info', 'Subscribe closed');
            web_db_log(
                {   'parameters' => $in{'email'},
                    'status'     => 'error',
                    'error_type' => 'authorization'
                }
            );
            return undef;
        }

        $param->{'may_subscribe'} = 1;

        if ($sub_is =~ /owner/) {
            my $spool_req = Sympa::Spool::Auth->new;
            my $request   = Sympa::Request->new(
                context          => $list,
                action           => 'add',
                custom_attribute => $in{custom_attribute},
                email            => $param->{'user'}{'email'},
                gecos            => $param->{'user'}{'gecos'},
                sender           => $param->{'user'}{'email'},
            );
            my $keyauth = $spool_req->store($request);

            $list->send_notify_to_owner(
                'subrequest',
                {   'who'              => $param->{'user'}{'email'},
                    'keyauth'          => $keyauth,
                    'replyto'          => Sympa::get_address($list, 'sympa'),
                    'custom_attribute' => $in{custom_attribute},
                    'gecos'            => $param->{'user'}{'gecos'},
                    'ip'               => $ip
                }
            );

            Sympa::Report::notice_report_web('sent_to_owner', {},
                $param->{'action'});
            wwslog('info', 'Subscribe sent to owners');

            return 'info';
        } elsif ($sub_is =~ /do_it/) {
            if ($param->{'is_subscriber'}) {
                unless (
                    $list->update_list_member(
                        $param->{'user'}{'email'},
                        subscribed  => 1,
                        update_date => time
                    )
                    ) {
                    Sympa::Report::reject_report_web(
                        'intern',
                        'update_subscriber_db_failed',
                        {'sub' => $param->{'user'}{'email'}},
                        $param->{'action'},
                        $list,
                        $param->{'user'}{'email'},
                        $robot
                    );
                    wwslog('info', 'Update failed');
                    web_db_log(
                        {   'parameters' => $in{'email'},
                            'status'     => 'error',
                            'error_type' => 'internal'
                        }
                    );
                    return undef;
                }
            } else {
                my $defaults = $list->get_default_user_options();
                my $u;
                %{$u} = %{$defaults};
                $u->{'email'}    = $param->{'user'}{'email'};
                $u->{'gecos'}    = $param->{'user'}{'gecos'} || $in{'gecos'};
                $u->{'date'}     = $u->{'update_date'} = time;
                $u->{'password'} = $param->{'user'}{'password'};
                if (my $reason = Sympa::Tools::Password::password_validation(
                        $u->{'password'}
                    )
                    ) {
                    Sympa::Report::reject_report_web('user',
                        'passwd_validation', {'reason' => $reason},
                        $param->{'action'});
                    wwslog('info', 'Password validation');
                    web_db_log(
                        {   'status'     => 'error',
                            'error_type' => 'bad_parameter'
                        }
                    );
                    return undef;
                }
                $u->{custom_attribute} = $in{custom_attribute}
                    if $in{custom_attribute};
                $u->{'lang'} = $param->{'user'}{'lang'} || $param->{'lang'};
                $u->{'parameter'} = 'manual subscription';

                $list->add_list_member($u);
                if (defined $list->{'add_outcome'}{'errors'}) {
                    if (defined $list->{'add_outcome'}{'errors'}
                        {'max_list_members_exceeded'}) {
                        Sympa::Report::reject_report_web(
                            'user',
                            'max_list_members_exceeded',
                            {   max_list_members =>
                                    $list->{'admin'}{'max_list_members'},
                                list  => $list->{'name'},
                                'sub' => $param->{'user'}{'email'}
                            },
                            $param->{'action'},
                            $list,
                            $param->{'user'}{'email'},
                            $robot
                        );
                    } else {
                        my $error = $language->gettext_sprintf(
                            'Unable to add user %s in list %s : %s',
                            $u->{'email'},
                            $list->{'name'},
                            $list->{'add_outcome'}{'errors'}{'error_message'}
                        );
                        Sympa::Report::reject_report_web(
                            'intern',
                            $error,
                            {'sub' => $param->{'user'}{'email'}},
                            $param->{'action'},
                            $list,
                            $param->{'user'}{'email'},
                            $robot
                        );
                    }
                    wwslog(
                        'info',
                        'Subscribe failed: %s',
                        $list->{'add_outcome'}{'errors'}{'error_message'}
                    );
                    web_db_log(
                        {   'parameters' => $in{'email'},
                            'status'     => 'error',
                            'error_type' => 'internal'
                        }
                    );
                    return "info";
                }
            }

            unless ($sub_is =~ /quiet/i) {
                unless (
                    $list->send_probe_to_user(
                        'welcome', $param->{'user'}{'email'}
                    )
                    ) {
                    wwslog(
                        'notice',
                        'Unable to send "welcome" probe to %s',
                        $param->{'user'}{'email'}
                    );
                }
            }

            if ($sub_is =~ /notify/) {
                $list->send_notify_to_owner(
                    'notice',
                    {   'who'     => $param->{'user'}{'email'},
                        'gecos'   => $param->{'user'}{'gecos'},
                        'command' => 'subscribe'
                    }
                );
            }

        }
    } else {    # user is not autenticated

        if ($in{'email'} && $in{'passwd'}) {
            $in{'previous_action'} = 'subscribe';
            $in{'previous_list'}   = $param->{'list'};
            return 'login';
        } else {
            return 'subrequest';
        }
    }

    Sympa::Report::notice_report_web('performed', {}, $param->{'action'});
    web_db_log({'parameters' => $in{'email'}, 'status' => 'success'});

    if ($in{'previous_action'}) {
        return $in{'previous_action'};
    }

#    return 'suboptions';
    return 'info';
}

####################################################
# do_multiple_subscribe
####################################################
# Subscribes a user to each lists
#
# IN : lists a array of lists
#
# OUT :'subrequest'|'login'|'info'|$in{'previous_action'}
#     | undef
####################################################
sub do_multiple_subscribe {
    wwslog('info', '(%s)', $in{'email'});

    ## Not authenticated
    unless (defined $param->{'user'} && $param->{'user'}{'email'}) {
        ## no email
        unless ($in{'email'}) {
            return 'lists';
        }
    }

    my @lists = split /\0/, $in{'lists'};
    my $total;
    my %results;

    foreach my $requested_list (@lists) {
        my $param->{'list'} = Sympa::List->new($requested_list, $robot);
        $results{'requested_list'} = do_subscribe();
    }
}

## Subscription request (user not authenticated)
sub do_suboptions {
    wwslog('info', '');

    unless ($param->{'is_subscriber'}) {
        Sympa::Report::reject_report_web('user', 'not_subscriber',
            {'list' => $list->{'name'}},
            $param->{'action'}, $list);
        wwslog(
            'info',
            '%s not subscribed to %s',
            $param->{'user'}{'email'},
            $param->{'list'}
        );
        return undef;
    }

    my ($s, $m);

    unless ($s = $list->get_list_member($param->{'user'}{'email'})) {
        Sympa::Report::reject_report_web(
            'intern',
            'subscriber_not_found',
            {'email' => $param->{'user'}{'email'}},
            $param->{'action'},
            $list,
            $param->{'user'}{'email'},
            $robot
        );
        wwslog('info', 'Subscriber %s not found', $param->{'user'}{'email'});
        return undef;
    }

    $s->{'reception'}  ||= 'mail';
    $s->{'visibility'} ||= 'noconceal';
    $s->{'date'} =
        $language->gettext_strftime("%d %b %Y", localtime($s->{'date'}));
    $s->{'update_date'} =
        $language->gettext_strftime("%d %b %Y",
        localtime($s->{'update_date'}));

    foreach $m ($list->available_reception_mode) {
        $param->{'reception'}{$m}{'description'} =
            Sympa::ListOpt::get_title($m, 'reception');
        if ($s->{'reception'} eq $m) {
            $param->{'reception'}{$m}{'selected'} = ' selected';
            if ($m =~ /^(mail|notice|not_me|txt|html|urlize)$/i) {
                $param->{'possible_topic'} = 1;
            }
        } else {
            $param->{'reception'}{$m}{'selected'} = '';
        }
    }

    foreach $m (qw(conceal noconceal)) {
        $param->{'visibility'}{$m}{'description'} =
            Sympa::ListOpt::get_title($m, 'visibility');
        if ($s->{'visibility'} eq $m) {
            $param->{'visibility'}{$m}{'selected'} = ' selected';
        } else {
            $param->{'visibility'}{$m}{'selected'} = '';
        }
    }

    $param->{'subscriber'} = $s;

    #msg_topic
    $param->{'sub_user_topic'} = 0;
    foreach my $user_topic (split(/,/, $s->{'topics'})) {
        $param->{'topic_checked'}{$user_topic} = 1;
        $param->{'sub_user_topic'}++;
    }

    if ($list->is_there_msg_topic()) {
        foreach my $top (@{$list->{'admin'}{'msg_topic'}}) {
            if (defined $top->{'name'}) {
                push(@{$param->{'available_topics'}}, $top);
            }
        }
    }

    return 1;
}

## Subscription request (user not authenticated)
sub do_subrequest {
    wwslog('info', '(%s, %s)', $in{'email'}, $in{custom_attribute});

    return undef if (purely_closed('subscribe'));
    $param->{'custom_attribute'} = $in{custom_attribute}
        if $in{custom_attribute};

    # Auth ?
    if ($param->{'user'}{'email'}) {
        # Subscriber ?
        if ($param->{'is_subscriber'}) {
            $param->{'status'} = 'auth_subscribed';
        } else {
            $param->{'status'} = 'auth';
        }
        return 1;
    }
    # Provided email parameter ?
    unless ($in{'email'}) {
        $param->{'status'} = 'notauth_noemail';
        return 1;
    }
    # Valid email address?
    unless (Sympa::Tools::Text::valid_email($in{'email'})) {
        Sympa::Report::reject_report_web('user', 'incorrect_email',
            {'email' => $in{'email'}},
            $param->{'action'}, $list);
        wwslog('info', 'Incorrect email %s', $in{'email'});
        web_db_log(
            {   'target_email' => $in{'email'},
                'status'       => 'error',
                'error_type'   => 'incorrect_email'
            }
        );
        $param->{'status'} = 'notauth_noemail';
        return 1;
    }

    # Subscriber ?
    if ($list->is_list_member($in{'email'})) {
        # To prevent sniffing users, fake "You requested a subscription"
        # notice.
        $param->{'login_error'} = 'ticket_sent';
        $param->{'status'}      = 'notauth_passwordsent';
        return 1;
    }

    $param->{'login_error'}       = 'ticket_sent';
    $param->{'request_from_host'} = $ip;
    $param->{'newuser'}           = Sympa::User::get_global_user($in{'email'})
        if Sympa::User::is_global_user($in{'email'});
    unless ($param->{'newuser'}) {
        $param->{'newuser'} =
            {'email' => Sympa::Tools::Text::canonic_email($in{'email'})};
        $param->{'newuser'}{'escaped_email'} =
            Sympa::Tools::Text::escape_chars($param->{'newuser'}{'email'});
    }
    # Need to send a password by email
    $param->{'one_time_ticket'} = Sympa::Ticket::create(
        $in{'email'}, $robot,
        'subscribe/' . $list->{'name'},
        $param->{'request_from_host'}
    );
    unless (Sympa::send_file($robot, 'sendpasswd', $in{'email'}, $param)) {
        wwslog('notice', 'Unable to send template "sendpasswd" to %s',
            $in{'email'});
        $param->{'login_error'} = 'unable_to_send_ticket';
    }
    # do_requestpasswd();
    $param->{'status'} = 'notauth_passwordsent';

    return 1;
}

sub do_auto_signoff {
    wwslog('info', '');
    ## If the URL isn't valid, then go to home page. No need to guide the
    ## user: this function is supposed to be used by clicking on autocreated
    ## URL only.
    return Conf::get_robot_conf($robot, 'default_home') unless $in{'email'};

    ## If unsubscribe is forbidden, reject the request. Other

    my $result = Sympa::Scenario::request_action(
        $list,
        'unsubscribe',
        $param->{'auth_method'},
        {   'sender'      => $in{'email'},
            'remote_host' => $param->{'remote_host'},
            'remote_addr' => $param->{'remote_addr'}
        }
    );
    my $sig_is;
    my $reason;
    if (ref($result) eq 'HASH') {
        $sig_is = $result->{'action'};
        $reason = $result->{'reason'};
    }

    if ($sig_is =~ /reject/) {
        Sympa::Report::reject_report_web('auth', $reason, {},
            $param->{'action'}, $list);
        wwslog('info', '%s may not signoff from %s',
            $in{'email'}, $param->{'list'});
        web_db_log(
            {   'status'     => 'error',
                'error_type' => 'authorization'
            }
        );
        return undef;
    }

    ## Send the confirmation email to the user.

    if ($list->is_list_member($in{'email'})) {
        Sympa::send_notify_to_user($list, 'ticket_to_signoff', $in{'email'},
            {context => 'auto_signoff', ip => $ip})
            or return undef;
    } else {
        return Conf::get_robot_conf($robot, 'default_home');
    }
    $param->{'signing_off_email'} = $in{'email'};
    # If OK, return the page displaying the information to the user.
    return 1;

}

sub do_family_signoff_request {
    wwslog('info', '');
    # If the URL isn't valid, then go to home page. No need to guide the
    # user: this function is supposed to be used by clicking on autocreated
    # URL only.
    return Conf::get_robot_conf($robot, 'default_home') unless $in{'email'};
    my $family = Sympa::Family->new($in{'family'}, $robot);
    return Conf::get_robot_conf($robot, 'default_home') unless $family;

    Sympa::send_notify_to_user(
        $robot,
        'ticket_to_family_signoff',
        $in{'email'},
        {context => 'family_signoff', family => $family->{'name'}, ip => $ip}
    ) or return undef;

    $param->{'signing_off_email'} = $in{'email'};
    $param->{'family'}            = $in{'family'};
    # If OK, return the page displaying the information to the user.
    return 1;
}

sub do_family_signoff {
    wwslog('info', '');
    $param->{'signing_off_email'} = $in{'email'};
    $param->{'family'}            = $in{'family'};

    unless ($in{'email'} eq $session->{'email'}) {
        Sympa::Report::reject_report_web('user', 'cannot_do_signoff');
        wwslog('err',
            'User %s tried to unsubscribe address %s from family %s',
            $session->{'email'}, $in{'email'}, $in{'family'});
        return undef;
    }

    my $family = Sympa::Family->new($param->{'family'}, $robot);
    unless ($family
        and $family->insert_delete_exclusion($in{'email'}, 'insert')) {
        Sympa::Report::reject_report_web('user', 'cannot_do_signoff');
        wwslog('err', 'Unsubscription of address %s from family %s failed',
            $in{'email'}, $in{'family'});
        return undef;
    }
    return 1;
}
####################################################
# do_signoff
####################################################
# Unsubcribes a user from a list
#
# IN : -
#
# OUT : 'sigrequest' | 'login' | 'info'
#
####################################################
## Unsubscribe from list
sub do_signoff {
    wwslog('info', '');

    return undef if (purely_closed('unsubscribe'));
    my $authenticated_email_address = $param->{'user'}{'email'};
    unless ($authenticated_email_address) {
        unless ($in{'email'}) {
            return 'sigrequest';
        }

        ## Perform login first
        if ($in{'passwd'}) {
            $in{'previous_action'} = 'signoff';
            $in{'previous_list'}   = $param->{'list'};
            return 'login';
        }

        if (Sympa::User::is_global_user($in{'email'})) {
            Sympa::Report::reject_report_web('user', 'no_user', {},
                $param->{'action'});
            wwslog('info', 'Need auth for user %s', $in{'email'});
            web_db_log(
                {   'target_email' => $in{'email'},
                    'status'       => 'error',
                    'error_type'   => 'authentication'
                }
            );
            return undef;
        }

        ## No passwd
        Sympa::Tools::WWW::init_passwd($robot, $in{'email'},
            {'lang' => $param->{'lang'}});

        $param->{'user'}{'email'} = $in{'email'};
    }

    my %result = unsubscribe($authenticated_email_address, $list);
    if ($result{'success'} == 1) {
        Sympa::Report::notice_report_web($result{'details'}, {},
            $param->{'action'});
        $param->{'is_subscriber'} = 0;
        $param->{'may_signoff'}   = 0;
    } else {
        Sympa::Report::reject_report_web(
            $result{'category_error'},
            $result{'reason_error'}, {'list' => $list->{'name'}},
            $param->{'action'}, $list
        );
    }
    return Conf::get_robot_conf($robot, 'default_home');
}

## Unsubscribe current user from a list.
sub unsubscribe {
    my $authenticated_email_address = shift;
    my $list                        = shift;
    my %report                      = ('success', 1, 'details', '');

    unless ($list->is_list_member($authenticated_email_address)) {
        wwslog(
            'info',
            '%s not subscribed to %s',
            $param->{'user'}{'email'},
            $param->{'list'}
        );
        web_db_log(
            {   'status'     => 'error',
                'error_type' => 'not_subscriber'
            }
        );
        $report{'success'}        = 0;
        $report{'category_error'} = 'user';
        $report{'reason_error'}   = 'not_subscribed';
        $report{'details_error'}  = {};
        return %report;
    }

    my $result = Sympa::Scenario::request_action(
        $list,
        'unsubscribe',
        $param->{'auth_method'},
        {   'sender'      => $param->{'user'}{'email'},
            'remote_host' => $param->{'remote_host'},
            'remote_addr' => $param->{'remote_addr'}
        }
    );
    my $sig_is;
    my $reason;
    if (ref($result) eq 'HASH') {
        $sig_is = $result->{'action'};
        $reason = $result->{'reason'};
    }

    $param->{'may_signoff'} = 1 if ($sig_is =~ /do_it|owner/);

    if ($sig_is =~ /reject/) {
        wwslog(
            'info',
            '%s may not signoff from %s',
            $param->{'user'}{'email'},
            $param->{'list'}
        );
        web_db_log(
            {   'status'     => 'error',
                'error_type' => 'authorization'
            }
        );
        $report{'success'}        = 0;
        $report{'category_error'} = 'auth';
        $report{'reason_error'}   = $reason;
        $report{'details_error'}  = {};
        return %report;
    } elsif ($sig_is =~ /owner/) {
        my $spool_req = Sympa::Spool::Auth->new;
        my $request   = Sympa::Request->new(
            context => $list,
            action  => 'del',
            email   => $param->{'user'}{'email'},
            sender  => $param->{'user'}{'email'},
        );
        my $keyauth = $spool_req->store($request);

        $list->send_notify_to_owner(
            'sigrequest',
            {   'who'     => $param->{'user'}{'email'},
                'keyauth' => $keyauth,
            }
        );

        wwslog('info', 'Signoff sent to owner');
        $report{'success'} = 1;
        $report{'details'} = 'sent_to_owner';
        return %report;
    } else {
        unless (
            $list->delete_list_member(
                'users'     => [$authenticated_email_address],
                'exclude'   => ' 1',
                'operation' => 'signoff',
            )
            ) {
            wwslog('info', 'Signoff failed');
            web_db_log(
                {   'status'     => 'error',
                    'error_type' => 'internal'
                }
            );
            $report{'success'}        = 0;
            $report{'category_error'} = 'intern';
            $report{'reason_error'}   = 'delete_subscriber_db_failed';
            $report{'details_error'} =
                {'sub' => $authenticated_email_address};
            return %report;
        }

        if ($sig_is =~ /notify/) {
            $list->send_notify_to_owner(
                'notice',
                {   'who'     => $authenticated_email_address,
                    'gecos'   => '',
                    'command' => 'signoff'
                }
            );
        }

        unless (
            Sympa::send_file($list, 'bye', $authenticated_email_address, {}))
        {
            wwslog('notice',
                "Unable to send template 'bye' to $authenticated_email_address"
            );
        }
    }
    web_db_log({'status' => 'success'});
    $report{'success'} = 1;
    $report{'details'} = 'performed';
    return %report;
}

## Unsubscription request (user not authenticated)
sub do_sigrequest {
    wwslog('info', '(%s)', $in{'email'});

    return undef if (purely_closed('unsubscribe'));
    ## If user is authenticated then redirect him to the signoff action but
    ## get a confirmation (via the sigrequest web page) first
    if ($param->{'user'}{'email'}) {
        return 1;
    }

    ## Not auth & no email => return the sigrequest web form to get the user
    ## email
    unless ($in{'email'}) {
        return 1;
    }
    ## valid email address?
    unless (Sympa::Tools::Text::valid_email($in{'email'})) {
        Sympa::Report::reject_report_web('user', 'incorrect_email',
            {'email' => $in{'email'}},
            $param->{'action'}, $list);
        wwslog('info', 'Incorrect email %s', $in{'email'});
        web_db_log(
            {   'target_email' => $in{'email'},
                'status'       => 'error',
                'error_type'   => 'incorrect_email'
            }
        );
        return 1;
    }
    if ($list->is_list_member($in{'email'})) {
        Sympa::send_notify_to_user($list, 'ticket_to_signoff', $in{'email'},
            {ip => $ip})
            or return undef;
    } else {
        $param->{'not_subscriber'} = 1;
    }

    $param->{'email'} = $in{'email'};

    return 1;
}

## Update of password
sub do_setpasswd {
    wwslog('info', '');
    my $user;

    if ($in{'newpasswd1'} =~ /^\s+$/) {
        Sympa::Report::reject_report_web('user', 'no_passwd', {},
            $param->{'action'});
        wwslog('info', 'No newpasswd1');
        web_db_log(
            {   'status'     => 'error',
                'error_type' => 'missing_parameter'
            }
        );
        return undef;
    }

    unless ($in{'newpasswd1'} eq $in{'newpasswd2'}) {
        Sympa::Report::reject_report_web('user', 'diff_passwd', {},
            $param->{'action'});
        wwslog('info', 'Different newpasswds');
        web_db_log(
            {   'status'     => 'error',
                'error_type' => 'bad_parameter'
            }
        );
        return undef;
    }

    if (my $reason =
        Sympa::Tools::Password::password_validation($in{'newpasswd1'})) {
        Sympa::Report::reject_report_web('user', 'passwd_validation',
            {'reason' => $reason},
            $param->{'action'});
        wwslog('info', 'Password validation');
        web_db_log({'status' => 'error', 'error_type' => 'bad_parameter'});
        return undef;
    }

    if (Sympa::User::is_global_user($param->{'user'}{'email'})) {

        unless (
            Sympa::User::update_global_user(
                $param->{'user'}{'email'},
                {'password' => $in{'newpasswd1'}, 'wrong_login_count' => 0}
            )
            ) {
            Sympa::Report::reject_report_web(
                'intern', 'update_user_db_failed',
                {'user' => $param->{'user'}}, $param->{'action'},
                '', $param->{'user'}{'email'},
                $robot
            );
            wwslog('info', 'Update failed');
            web_db_log(
                {   'status'     => 'error',
                    'error_type' => 'internal'
                }
            );
            return undef;
        }
    } else {

        unless (
            Sympa::User::add_global_user(
                {   'email'             => $param->{'user'}{'email'},
                    'password'          => $in{'newpasswd1'},
                    'wrong_login_count' => 0
                }
            )
            ) {
            Sympa::Report::reject_report_web('intern', 'add_user_db_failed',
                {'user' => $param->{'user'}},
                $param->{'action'}, '', $param->{'user'}{'email'}, $robot);
            wwslog('info', 'Update failed');
            web_db_log(
                {   'status'     => 'error',
                    'error_type' => 'internal'
                }
            );
            return undef;
        }
    }

    $param->{'user'}{'password'} = $in{'newpasswd1'};

    Sympa::Report::notice_report_web('performed', {}, $param->{'action'});
    web_db_log({'status' => 'success'});

    if ($in{'previous_action'}) {
        $in{'list'} = $in{'previous_list'};
        return $in{'previous_action'};
    } else {
        return 'pref';
    }
}

## List admin page
sub do_admin {
    wwslog('info', '');

    ## Messages edition
    foreach my $f (
        'info',           'homepage',
        'welcome.tt2',    'bye.tt2',
        'removed.tt2',    'message.footer',
        'message.header', 'remind.tt2',
        'invite.tt2',     'reject.tt2'
        ) {
        next
            unless (
            $list->may_edit($f, $param->{'user'}{'email'}) eq 'write');
        if ($Sympa::Tools::WWW::filenames{$f}{'gettext_id'}) {
            $param->{'files'}{$f}{'complete'} =
                $language->gettext(
                $Sympa::Tools::WWW::filenames{$f}{'gettext_id'});
        } else {
            $param->{'files'}{$f}{'complete'} = $f;
        }
        $param->{'files'}{$f}{'selected'} = '';
    }
    $param->{'files'}{'info'}{'selected'} = 'selected="selected"';

    #    my %mode;
    #    $mode{'edit'} = 1;
    #    my %access = d_access_control(\%mode,$path);

    return 1;
}

## Server admin page
sub do_serveradmin {
    wwslog('info', '');

    my $f;

    ## Lists Default files
    foreach my $f (
        'welcome.tt2',    'bye.tt2',
        'removed.tt2',    'message.footer',
        'message.header', 'remind.tt2',
        'invite.tt2',     'reject.tt2',
        'your_infected_msg.tt2'
        ) {
        if ($Sympa::Tools::WWW::filenames{$f}{'gettext_id'}) {
            $param->{'lists_default_files'}{$f}{'complete'} =
                $language->gettext(
                $Sympa::Tools::WWW::filenames{$f}{'gettext_id'});
        } else {
            $param->{'lists_default_files'}{$f}{'complete'} = $f;
        }
        $param->{'lists_default_files'}{$f}{'selected'} = '';
    }

    ## Checking families and other virtual hosts.
    get_server_details();

    ## Server files
    foreach my $f (
        'helpfile.tt2',            'lists.tt2',
        'global_remind.tt2',       'summary.tt2',
        'create_list_request.tt2', 'list_created.tt2',
        'list_aliases.tt2'
        ) {
        $param->{'server_files'}{$f}{'complete'} =
            $language->gettext(
            $Sympa::Tools::WWW::filenames{$f}{'gettext_id'});
        $param->{'server_files'}{$f}{'selected'} = '';
    }
    $param->{'server_files'}{'helpfile.tt2'}{'selected'} =
        'selected="selected"';
    $param->{'log_level'} = $session->{'log_level'};
    $param->{'subaction'} = $in{'subaction'};
    return 1;
}

sub do_edit_config {
    my $editable_params =
        Sympa::Tools::Data::dup_var(\@Sympa::ConfDef::params);

    get_server_details();

    unless ($param->{'main_robot'}) {
        Sympa::Report::reject_report_web('auth',
            'super lismaster feature only',
            {}, $param->{'action'});
        wwslog(
            'info',
            'Access denied in edit_config for %s because not super listmaster',
            $param->{'user'}{'email'}
        );
    }

    for my $p (@$editable_params) {
        if ($p->{'name'}) {
            my $name = $p->{'name'};
            my $v = Conf::get_robot_conf($robot || '*', $name);
            if (ref $v eq 'ARRAY') {
                $p->{'current_value'} = join ',', @$v;
            } else {
                $p->{'current_value'} = $v;
            }
            $p->{'query'} = $language->gettext($p->{'gettext_id'})
                if $p->{'gettext_id'};
            $p->{'advice'} = $language->gettext($p->{'gettext_comment'})
                if $p->{'gettext_comment'};
        } elsif ($p->{'gettext_id'}) {
            $p->{'title'} = $language->gettext($p->{'gettext_id'});
            unless ($p->{'group'}) {
                my $g = $p->{'gettext_id'};
                $g =~ s/([^-\w])/sprintf '.%02X', ord $1/eg;
                $p->{'group'} = $g;
            }
        }
    }

    if ($in{'conf_new_value'}) {
        my $editable;
        my $i;
        foreach my $p (@$editable_params) {
            next unless $p->{'name'};

            # if the parameter is editable and if the is a change
            next unless $p->{'name'} eq $in{'conf_parameter_name'};
            unless ($p->{'edit'} and $p->{'edit'} eq '1') {
                $log->syslog(
                    'err',
                    'Ignoring change of parameter %s (value %s) because not editable',
                    $in{'conf_parameter_name'},
                    $in{'conf_new_value'}
                );
                last;
            }
            if ($in{'conf_new_value'} eq $p->{'current_value'}) {
                $log->syslog(
                    'notice',
                    'Ignoring change of parameter %s (value %s) because inchanged',
                    $in{'conf_parameter_name'},
                    $in{'conf_new_value'}
                );
                last;
            } else {
                $p->{'current_value'} = $in{'conf_new_value'};
                Conf::set_robot_conf($robot, $in{'conf_parameter_name'},
                    $in{'conf_new_value'});
                $log->syslog(
                    'notice',
                    'Setting parameter %s to value %s',
                    $in{'conf_parameter_name'},
                    $in{'conf_new_value'}
                );
                last;
            }
        }
    }

    $param->{'editable_params'} = $editable_params;
    return 1;

}

## Change log_level for the current session
sub do_set_loglevel {
    wwslog('info', '');

    $session->{'log_level'} = $in{'log_level'};
    return 'serveradmin';
}

## activate dump var feature
sub do_set_dumpvars {
    wwslog('info', '');

    $session->{'dumpvars'}  = 'true';
    $param->{'dumpavars'}   = $session->{'dumpvars'};
    $param->{'redirect_to'} = Sympa::get_url(
        $robot, 'serveradmin',
        nomenu    => $param->{'nomenu'},
        authority => 'local'
    );
    return '1';
}
## un-activate dump var feature
sub do_unset_dumpvars {
    wwslog('info', '');

    $session->{'dumpvars'}  = '';
    $param->{'dumpavars'}   = '';
    $param->{'redirect_to'} = Sympa::get_url(
        $robot, 'serveradmin',
        nomenu    => $param->{'nomenu'},
        authority => 'local'
    );
    return '1';
}
## un-activate dump var feature
sub do_show_sessions {
    wwslog('info', '');

    $in{'session_delay'} = 10 unless ($in{'session_delay'});
    my $delay = 60 * $in{'session_delay'};
    my $sessions =
        Sympa::Session::list_sessions($delay, $robot, $in{'connected_only'});
    foreach my $session (@$sessions) {
        $session->{'date'} =
            $language->gettext_strftime("%d %b %Y at %H:%M:%S",
            localtime($session->{'date_epoch'}));
        $session->{'start_date'} =
            $language->gettext_strftime("%d %b %Y at %H:%M:%S",
            localtime($session->{'start_date_epoch'}));
        # Compatibility for misspelling.
        $session->{'formated_date'}       = $session->{'date'};
        $session->{'formated_start_date'} = $session->{'start_date'};
    }
    $param->{'sessions'} = $sessions;
    return '1';
}

## Change user email
sub do_set_session_email {
    wwslog('info', '');

    my $email_regexp = Sympa::Regexps::email();
    unless ($in{'email'} =~ /^\s*$email_regexp\s*$/) {
        Sympa::Report::reject_report_web('user', 'Invalid email provided.',
            {}, $param->{'action'}, $list);
        return 'serveradmin';
    }

    # Prevent getting privilege of super-listmaster.
    if (Sympa::is_listmaster('*', $in{'email'})) {
        Sympa::Report::reject_report_web('user',
            'You are not allowed to get the privilege of this user.',
            {}, $param->{'action'}, $list);
        return 'serveradmin';
    }

    if ($session) {
        $session->{'restore_email'} ||= $param->{'user'}{'email'};
        $session->{'email'}     = $in{'email'};
        $param->{'redirect_to'} = Sympa::get_url(
            $robot, undef,
            nomenu    => $param->{'nomenu'},
            authority => 'local'
        );
        return '1';
    } else {
        Sympa::Report::reject_report_web('user', 'No active session',
            {}, $param->{'action'}, $list);
        return 'serveradmin';
    }
}

## Change user email
sub do_restore_email {
    wwslog('info', '');
    wwslog('debug2', 'From %s to %s',
        $session->{'email'}, $session->{'restore_email'});

    if ($param->{'restore_email'}) {
        $session->{'email'}       = $session->{'restore_email'};
        $param->{'restore_email'} = $session->{'restore_email'} = '';
        $param->{'redirect_to'}   = Sympa::get_url(
            $robot, undef,
            nomenu    => $param->{'nomenu'},
            authority => 'local'
        );
    } else {
        wwslog(
            'info',
            'From %s no restore_email attached to current session',
            $param->{'user'}{'email'}
        );
        Sympa::Report::reject_report_web('user', 'wrong_param', {},
            $param->{'action'}, $list);
    }
    return 'home';
}

## list available templates
sub do_ls_templates {
    wwslog('info', '');

    $in{'webormail'} ||= 'web';

    $param->{'templates'} =
        Sympa::Tools::WWW::get_templates_list($list || $robot,
        $in{'webormail'});

    ## List of lang per type
    foreach my $level ('site', 'robot', 'list') {
        $param->{'lang_per_level'}{$level}{'default'} = 1;
    }

    foreach my $file (keys %{$param->{'templates'}}) {
        foreach my $level (keys %{$param->{'templates'}{$file}}) {
            $language->push_lang;
            foreach my $subdir (keys %{$param->{'templates'}{$file}{$level}})
            {
                my $lang = $language->set_lang($subdir); # allow unknown lang.
                $param->{'lang_per_level'}{$level}{$subdir} = {
                    'title' => ($lang ? $language->native_name : ''),
                    'lang' => $lang,
                };
            }
            $language->pop_lang;
        }
    }

    ## Colspan per level
    foreach my $level (keys %{$param->{'lang_per_level'}}) {
        foreach my $subdir (keys %{$param->{'lang_per_level'}{$level}}) {
            $param->{'colspan_per_level'}{$level}++;
            foreach my $file (keys %{$param->{'templates'}}) {
                $param->{'templates'}{$file}{$level}{$subdir} ||= '';
            }
        }
    }

    $param->{'webormail'} = $in{'webormail'};

    return 1;
}

# show a template, used by copy_template and edit_emplate
sub do_remove_template {

    wwslog('info', '');

    my $template_path;

    if ($in{'scope'} eq 'list' and ref $list ne 'Sympa::List') {
        Sympa::Report::reject_report_web('user', 'missing_arg',
            {'argument' => 'list'},
            $param->{'action'});
        wwslog('err', 'Missing parameter list');
        web_db_log(
            {   'parameters' => $in{'webormail'},
                'status'     => 'error',
                'error_type' => 'missing_parameter'
            }
        );
        return 1;
    }
    $template_path = Sympa::Tools::WWW::get_template_path(
        $list || $robot, $in{'webormail'}, $in{'scope'},
        $in{'template_name'}, $in{'tpl_lang'}
    );

    my $template_old_path =
        Sympa::Tools::File::shift_file($template_path, 10);
    unless ($template_old_path) {
        Sympa::Report::reject_report_web('intern', 'remove_failed',
            {'path' => $template_path},
            $param->{'action'}, $list, $param->{'user'}{'email'}, $robot);
        wwslog('info', 'Could not remove %s', $template_path);
        web_db_log(
            {   'parameters' => $in{'webormail'},
                'status'     => 'error',
                'error_type' => 'internal'
            }
        );
        return undef;
    }

    Sympa::Report::notice_report_web('file_renamed',
        {'orig_file' => $template_path, 'new_file' => $template_old_path},
        $param->{'action'});
    web_db_log(
        {   'parameters' => $in{'webormail'},
            'status'     => 'status'
        }
    );
    $param->{'webormail'}     = $in{'webormail'};
    $param->{'scope'}         = $in{'scope'};
    $param->{'template_name'} = $in{'template_name'};
    $param->{'tpl_lang'}      = $in{'tpl_lang'};

    return 'ls_templates';
}

# show a template, used by copy_template and edit_emplate
sub do_view_template {
    wwslog(
        'info',
        '(type=%s, template-name=%s, listname=%s, path=%s, scope=%s, lang=%s)',
        $in{'webormail'},
        $in{'template_name'},
        $in{'list'},
        $in{'template_path'},
        $in{'scope'},
        $in{'tpl_lang'}
    );

    my $template_path;

    if ($in{'scope'} eq 'list' and ref $list ne 'Sympa::List') {
        Sympa::Report::reject_report_web('user', 'missing_arg',
            {'argument' => 'list'},
            $param->{'action'});
        wwslog('err', 'Missing parameter webormail');
        web_db_log(
            {   'parameters' => $in{'webormail'},
                'status'     => 'error',
                'error_type' => 'missing_parameter'
            }
        );
        return 1;
    }
    $template_path = Sympa::Tools::WWW::get_template_path(
        $list || $robot, $in{'webormail'}, $in{'scope'},
        $in{'template_name'}, $in{'tpl_lang'}
    );

    unless ($template_path and open(TPL, $template_path)) {
        Sympa::Report::reject_report_web('intern', 'cannot_open_file',
            {'path' => $in{'template_path'}},
            $param->{'action'}, '', $param->{'user'}{'email'}, $robot);
        wwslog('err', 'Can\'t open file %s', $template_path);
        return undef;
    }

    $param->{'rows'}             = 5;     # minimum size of 5 rows;
    $param->{'template_content'} = '';    # init content
    while (<TPL>) { $param->{'template_content'} .= $_; $param->{'rows'}++; }
    $param->{'template_content'} =
        Sympa::Tools::WWW::escape_html_minimum($param->{'template_content'});
    close TPL;

    $param->{'webormail'}     = $in{'webormail'};
    $param->{'template_name'} = $in{'template_name'};
    $param->{'template_path'} = $template_path;
    $param->{'scope'}         = $in{'scope'};

    my $tpl_lang = $in{'tpl_lang'} || 'default';
    $param->{'tpl_lang'} = $tpl_lang;
    unless ($tpl_lang eq 'default') {
        $language->push_lang;

        if (my $lang = $language->set_lang($tpl_lang)) { # allow unknown lang.
            $param->{'tpl_lang_title'} = $language->native_name || $tpl_lang;
            $param->{'tpl_lang_lang'} = $lang;
        } else {
            $param->{'tpl_lang_title'} = $tpl_lang;
            $param->{'tpl_lang_lang'} =
                Sympa::Language::canonic_lang($tpl_lang);
        }

        $language->pop_lang;
    }

    return 1;
}

##  template copy
sub do_copy_template {
    wwslog('info', '');

    ## Load original template
    do_view_template();

    ## Return form
    unless ($in{'scope_out'}) {
        return 1;
    }

    # one of these parameters is commit from the form submission
    if ($in{'scope_out'} eq 'list') {
        if ($in{'list_out'}) {
            my $list_out;
            unless ($list_out =
                Sympa::List->new($in{'list_out'}, $robot, {just_try => 1})) {
                Sympa::Report::reject_report_web('user', 'unknown_list',
                    {'list' => $in{'list_out'}},
                    $param->{'action'}, '');
                wwslog('info', 'Unknown list %s', $in{'list_out'});
                web_db_log(
                    {   'parameters' => $in{'list_out'},
                        'status'     => 'error',
                        'error_type' => 'unknown_list'
                    }
                );
                return undef;
            }
            $param->{'template_path_out'} =
                Sympa::Tools::WWW::get_template_path($list_out,
                $in{'webormail'}, 'list', $in{'template_name_out'},
                $in{'tpl_lang_out'});
        } else {
            Sympa::Report::reject_report_web('user', 'missing_arg',
                {'argument' => 'list'},
                $param->{'action'});
            wwslog('err', 'Missing parameter webormail');
            web_db_log(
                {   'parameters' => $in{'webormail'},
                    'status'     => 'error',
                    'error_type' => 'missing_parameter'
                }
            );
            return 1;
        }
    } else {
        $param->{'template_path_out'} =
            Sympa::Tools::WWW::get_template_path($robot, $in{'webormail'},
            $in{'scope_out'}, $in{'template_name_out'},
            $in{'tpl_lang_out'});
    }

    unless ($param->{'template_path_out'}
        and Sympa::Tools::File::mk_parent_dir($param->{'template_path_out'}))
    {
        Sympa::Report::reject_report_web(
            'intern',
            'cannot_open_file',
            {'path' => $param->{'template_path_out'}},
            $param->{'action'},
            '',
            $param->{'user'}{'email'},
            $robot
        );
        wwslog(
            'err',
            'Can\'t create parent directory for %s: %s',
            $param->{'template_path_out'}, $ERRNO
        );
        web_db_log(
            {   'parameters' => $param->{'template_name_out'},
                'status'     => 'error',
                'error_type' => 'internal'
            }
        );
        return undef;
    }

    unless (open(TPLOUT, '>', $param->{'template_path_out'})) {
        Sympa::Report::reject_report_web(
            'intern',
            'cannot_open_file',
            {'path' => $param->{'template_path_out'}},
            $param->{'action'},
            '',
            $param->{'user'}{'email'},
            $robot
        );
        wwslog(
            'err',
            'Can\'t open file %s: %s',
            $param->{'template_path_out'}, $ERRNO
        );
        web_db_log(
            {   'parameters' => $param->{'template_name_out'},
                'status'     => 'error',
                'error_type' => 'internal'
            }
        );
        return undef;
    }
    print TPLOUT Sympa::Tools::WWW::unescape_html_minimum(
        $param->{'template_content'});
    close TPLOUT;

    if ($in{'list_out'}) { $param->{'list'} = $in{'list'} = $in{'list_out'}; }

    $param->{'webormail'} = $in{'webormail'};

    my $tpl_lang = $in{'tpl_lang_out'} || 'default';
    $param->{'tpl_lang'} = $in{'tpl_lang'} = $tpl_lang;
    unless ($tpl_lang eq 'default') {
        $language->push_lang;

        if (my $lang = $language->set_lang($tpl_lang)) { # allow unknown lang.
            $param->{'tpl_lang_title'} = $language->native_name || $tpl_lang;
            $param->{'tpl_lang_lang'} = $lang;
        } else {
            $param->{'tpl_lang_title'} = $tpl_lang;
            $param->{'tpl_lang_lang'} =
                Sympa::Language::caonic_lang($tpl_lang);
        }

        $language->pop_lang;
    }

    $param->{'scope'} = $in{'scope'} = $in{'scope_out'};
    $param->{'template_path'} = $in{'template_path'} =
        $param->{'template_path_out'};
    $param->{'template_name'} = $in{'template_name'} =
        $in{'template_name_out'};
    web_db_log(
        {   'parameters' => $param->{'template_name_out'},
            'status'     => 'success'
        }
    );
    return ('edit_template');
}

## manage the rejection templates
sub do_manage_template {
    wwslog('info', '(%s, %s)', $in{'subaction'}, $in{'message_template'});

    my $file;

    $in{'message_template'} =~ s/^reject_//;

    if ($in{'message_template'}) {
        my $escaped_template_path = $in{'message_template'};
        $escaped_template_path =~ s/\s/_/g;
        $param->{'template_path'} =
            Sympa::Tools::WWW::get_template_path($list || $robot,
            'mail', 'list', 'reject_' . $escaped_template_path . '.tt2');
    }
    my $default_file = Sympa::search_fullpath($list || $robot,
        'reject.tt2', subdir => 'mail_tt2');

    if ($in{'subaction'} eq 'save') {
        ## create the parent directory if it doesn't already exist
        unless (Sympa::Tools::File::mk_parent_dir($param->{'template_path'}))
        {
            Sympa::Report::reject_report_web(
                'intern',
                'cannot_open_file',
                {'path' => $param->{'template_path'}},
                $param->{'action'},
                '',
                $param->{'user'}{'email'},
                $robot
            );
            wwslog(
                'err',
                'Can\'t create parent directory for %s: %s',
                $param->{'template_path'}, $ERRNO
            );
            web_db_log(
                {   'parameters' => $param->{'template_name'},
                    'status'     => 'error',
                    'error_type' => 'internal'
                }
            );
            return undef;
        }
        ## open the template
        unless (open(TPLOUT, '>', $param->{'template_path'})) {
            Sympa::Report::reject_report_web(
                'intern',
                'cannot_open_file',
                {'path' => $param->{'template_path'}},
                $param->{'action'},
                '',
                $param->{'user'}{'email'},
                $robot
            );
            wwslog(
                'err',
                'Can\'t open file %s: %s',
                $param->{'template_path'}, $ERRNO
            );
            web_db_log(
                {   'parameters' => $in{'template_name'},
                    'status'     => 'error',
                    'error_type' => 'internal'
                }
            );
            return undef;
        }
        ##  save template contents
        print TPLOUT $in{'template_content'};
        close TPLOUT;
        Sympa::Report::notice_report_web('performed', {}, $in{'subaction'});
    } elsif ($in{'subaction'} eq 'create_new') {
        $in{'template_new'} = $in{'new_template_name'};

        unless ($in{'new_template_name'}) {
            Sympa::Report::reject_report_web(
                'user',
                'missing template name',
                {'path' => ''},
                $param->{'action'}, '', $param->{'user'}{'email'}, $robot
            );
            return undef;
        }
        my $escaped_template_path = $in{'new_template_name'};
        $escaped_template_path =~ s/\s/_/g;
        my $new_template_path =
            Sympa::Tools::WWW::get_template_path($list || $robot,
            'mail', 'list', 'reject_' . $escaped_template_path . '.tt2');

        if (-f $new_template_path) {
            Sympa::Report::reject_report_web(
                'intern', 'template already exist',
                {'path' => $new_template_path}, $param->{'action'},
                '', $param->{'user'}{'email'},
                $robot
            );
            return undef;
        }
        ## create the parent directory if it doesn't already exist
        unless (Sympa::Tools::File::mk_parent_dir($new_template_path)) {
            Sympa::Report::reject_report_web(
                'intern',
                'cannot_open_file',
                {'path' => $param->{'template_path'}},
                $param->{'action'},
                '',
                $param->{'user'}{'email'},
                $robot
            );
            wwslog(
                'err',
                'Can\'t create parent directory for %s: %s',
                $param->{'template_path'}, $ERRNO
            );
            web_db_log(
                {   'parameters' => $param->{'template_name'},
                    'status'     => 'error',
                    'error_type' => 'internal'
                }
            );
            return undef;
        }

        my $default_file = Sympa::search_fullpath($list || $robot,
            'reject.tt2', subdir => 'mail_tt2');

        unless (open(DEFAULT, $default_file)) {
            Sympa::Report::reject_report_web('intern', 'cannot_open_file',
                {'path' => $default_file},
                $param->{'action'}, '', $param->{'user'}{'email'}, $robot);
            wwslog('err', 'Can\'t open file %s: %s', $default_file, $ERRNO);
            return undef;
        }

        unless (open(TPL, '> ' . $new_template_path)) {
            Sympa::Report::reject_report_web('intern', 'cannot_open_file',
                {'path' => $new_template_path},
                $param->{'action'}, '', $param->{'user'}{'email'}, $robot);
            wwslog('err', 'Can\'t open file %s: %s',
                $new_template_path, $ERRNO);
            return undef;
        }

        while (<DEFAULT>) {
            print TPL $_;
        }
        close DEFAULT;
        close TPL;
        $in{'subaction'}        = 'modify';
        $in{'message_template'} = $in{'new_template_name'};
        return 'manage_template';

    } elsif ($in{'subaction'} eq 'modify') {

        unless (open(FILE, $param->{'template_path'})) {
            Sympa::Report::reject_report_web(
                'intern',
                'cannot_open_file',
                {'path' => $param->{'template_path'}},
                $param->{'action'},
                '',
                $param->{'user'}{'email'},
                $robot
            );
            wwslog(
                'err',
                'Can\'t open file MODIFY %s: %s',
                $param->{'template_path'}, $ERRNO
            );
            web_db_log(
                {   'parameters' => $param->{'template_path'},
                    'status'     => 'error',
                    'error_type' => 'internal'
                }
            );
            return undef;
        }
        while (<FILE>) {
            $param->{'content'} .= $_;
        }
        $param->{'content'} =
            Sympa::Tools::WWW::escape_html_minimum($param->{'content'});
        close FILE;
        $param->{'message_template'} = $in{'message_template'};

    } elsif ($in{'subaction'} eq 'setdefault') {
        # replace existing reject.tt2 file by a symlink to reject_default.tt2
        # for compatibility with version older than 6.0
        my $base = $list->{'dir'} . '/mail_tt2/';
        $in{'new_default'} =~ s/\s/_/g;
        my $absolute_file = $base . 'reject_' . $in{'new_default'} . '.tt2';

        $log->syslog(
            'info',
            'Change default by linking %s 2 %s',
            $base . 'reject.tt2',
            $absolute_file
        );
        if (-l $base . 'reject.tt2') {
            unless (unlink($base . 'reject.tt2')) {
                wwslog('err', 'Could not unlink %s', $base . 'reject.tt2');
            }
        }
        unless (symlink($absolute_file, $base . 'reject.tt2')) {
            wwslog('err', 'Could not symlink %s, %s',
                $absolute_file, $base . 'reject.tt2');
        }

    } elsif ($in{'subaction'} eq 'delete') {

        unless (unlink $param->{'template_path'}) {
            Sympa::Report::reject_report_web('intern', 'cannot_delete',
                {'file_del' => $param->{'template_path'}},
                '', '', '', $robot);
            wwslog(
                'err',
                'Can\'t open file %s: %s',
                $param->{'template_path'}, $ERRNO
            );
            web_db_log(
                {   'parameters' => $param->{'template_path'},
                    'status'     => 'error',
                    'error_type' => 'internal'
                }
            );
            return undef;
        }
        Sympa::Report::notice_report_web('performed', {}, $in{'subaction'});
    }
    ## Build the list of available templates
    my $available_files = Sympa::Tools::WWW::get_templates_list($list, 'mail',
        ignore_global => 1);
    foreach $file (keys %$available_files) {
        if ($file eq 'reject.tt2') {
            my $base          = $list->{'dir'} . '/mail_tt2/';
            my $absolute_file = $base . 'reject.tt2';
            if (-l $absolute_file) {
                my $default = readlink($absolute_file);
                if ((-f $default) || (-f $base . $default)) {

                    $default =~ s/^.*reject_//;
                    $default =~ s/.tt2$//;
                    $default =~ s/_/ /g;
                    $param->{'default_reject_template'} = $default;
                } else {
                    # link to no existing file. remove link
                    wwslog(
                        'err',
                        'Link %s point to un no existing file (%s)',
                        $base . 'reject.tt2', $default
                    );
                    unless (unlink($absolute_file)) {
                        wwslog(
                            'err',
                            'Could not unlink %s',
                            $base . 'reject.tt2'
                        );
                    }
                }
            } elsif (-f $absolute_file) {
                # replace existing reject.tt2 file by a symlink to
                # reject_default.tt2 for compatibility with version older than
                # 6.0
                unless (rename($absolute_file, $base . 'reject_default.tt2'))
                {
                    wwslog(
                        'err',
                        'Could not rename %, %s',
                        $base . 'reject.tt2',
                        $base . 'reject_default.tt2'
                    );
                }
                unless (symlink($base . 'reject_default.tt2', $absolute_file))
                {
                    wwslog(
                        'err',
                        'Could not symlink %s, %s',
                        $base . 'reject_default.tt2',
                        $absolute_file
                    );
                }

                $param->{'default_reject_template'} = 'default';
                push(@{$param->{'available_files'}}, 'default');
            }
        } else {
            next unless ($file =~ /^reject_/);
            $file =~ s/^reject_//;
            $file =~ s/.tt2$//;
            $file =~ s/_/ /g;
            push(@{$param->{'available_files'}}, $file);
        }
    }

    return 1;
}

## online template edition
sub do_edit_template {

    $in{'subdir'} ||= 'default';

    wwslog(
        'info',
        '(type=%s, template-name=%s, listname=%s, path=%s, scope=%s, lang=%s)',
        $in{'webormail'},
        $in{'template_name'},
        $in{'list'},
        $in{'template_path'},
        $in{'scope'},
        $in{'tpl_lang'}
    );

    ## Load original template
    do_view_template();

    unless ($in{'content'}) {
        return 1;
    }
    if ($in{'scope'} eq 'list' and ref $list ne 'Sympa::List') {
        Sympa::Report::reject_report_web('user', 'listname_needed', {},
            $param->{'action'});
        wwslog('info', 'No output lisname while output scope is list');
        web_db_log(
            {   'parameters' => $in{'template_name'},
                'status'     => 'error',
                'error_type' => 'no_list'
            }
        );
        return undef;
    }
    $param->{'template_path'} = Sympa::Tools::WWW::get_template_path(
        $list || $robot, $in{'webormail'}, $in{'scope'},
        $in{'template_name'}, $in{'tpl_lang'}
    );

    unless ($param->{'template_path'}
        and open(TPLOUT, '>', $param->{'template_path'})) {
        Sympa::Report::reject_report_web('intern', 'cannot_open_file',
            {'path' => $param->{'template_path'}},
            $param->{'action'}, '', $param->{'user'}{'email'}, $robot);
        wwslog('err', 'Can\'t open file %s', $param->{'template_path'});
        web_db_log(
            {   'parameters' => $in{'template_name'},
                'status'     => 'error',
                'error_type' => 'internal'
            }
        );
        return undef;
    }
    print TPLOUT Sympa::Tools::WWW::unescape_html_minimum($in{'content'});
    close TPLOUT;

    $param->{'saved'}            = 1;
    $param->{'template_content'} = $in{'content'};
    $param->{'webormail'}        = $in{'webormail'};
    $param->{'template_name'}    = $in{'template_name'};
    $param->{'list'}             = $in{'list'};
    $param->{'scope'}            = $in{'scope'};
    $param->{'template_path'}    = $in{'template_path'};
    $param->{'tpl_lang'}         = $in{'tpl_lang'};

    web_db_log(
        {   'parameters' => $in{'template_name'},
            'status'     => 'success'
        }
    );

    return 'ls_templates';

}

## Server show colors, and install static css in future edit colors etc

## Server show colors, and install static css in futur edit colors etc
sub do_skinsedit {
    wwslog('info', '');
    my $f;

    my $dir     = Conf::get_robot_conf($robot, 'css_path');
    my $css_url = Conf::get_robot_conf($robot, 'css_url');

    ## Checking families and other virtual hosts.
    get_server_details();

    $param->{'css_warning'} =
        "parameter css_url seems strange, it must be the url of a directory not a css file"
        if ($css_url =~ /\.css$/);

    if (($in{'editcolors'}) && ($in{'subaction'} eq 'reset')) {
        delete $session->{'custom_css'};
        delete $param->{'session'}{'custom_css'};
        delete $param->{'custom_css'};

        foreach my $colornumber (0 .. 15) {
            delete $session->{'color_' . $colornumber};
            delete $param->{'session'}{'color_' . $colornumber};
        }
    }

    if ($in{'editcolors'} and $in{'subaction'} eq 'test') {
        return unless $in{'custom_color_number'} =~ /color_/;
        $param->{'custom_color_number'} = $in{'custom_color_number'};
        $param->{'custom_color_value'}  = $in{'custom_color_value'};
        $param->{'custom_css'} =
            $css_url . '/' . $param->{'user'}{'email'} . '.style.css';
        $session->{'custom_css'} = $param->{'custom_css'};

        $session->{$in{'custom_color_number'}} = $in{'custom_color_value'};

        $param->{$in{'custom_color_number'}} = $in{'custom_color_value'};
        foreach my $colornumber (0 .. 15) {
            if ($session->{'color_' . $colornumber}) {
                $param->{'color_' . $colornumber} =
                    $session->{'color_' . $colornumber};
                $param->{'session'}{'color_' . $colornumber} =
                    $session->{'color_' . $colornumber};
            }
        }
    }

    if ($in{'subaction'} eq 'install' or $in{'installcss'}) {
        # Do not include locale subdirectories (lang parameter).
        # The css.tt2 by each locale will override styles in main CSS.
        my $css_template = Sympa::Template->new($robot, subdir => 'web_tt2');

        my $date = time;
        my $style_file;

        # update config
        foreach my $colornumber (0 .. 15) {
            Conf::set_robot_conf(
                $robot,
                'color_' . $colornumber,
                $session->{'color_' . $colornumber}
            ) if ($session->{'color_' . $colornumber});
        }
        $param->{'conf'} = $Conf::Conf;

        foreach my $css (qw(style.css)) {
            $param->{'css'} = $css;
            my $css_file;
            # if user use editcolor form we must generate a static CSS that
            # used custom colors.
            if ($in{'subaction_test'}) {
                $css_file = "$dir/$param->{'user'}{'email'}.$css";
            } else {
                $css_file = "$dir/$css";
            }
            unless (-d $dir) {
                unless (mkdir $dir, 0775) {
                    Sympa::Report::reject_report_web('intern', "mkdir_failed",
                        {'path' => $dir},
                        $param->{'action'}, '', $param->{'user'}{'email'},
                        $robot);
                    wwslog('err', 'Failed to create directory %s: %s',
                        $dir, $ERRNO);
                    return undef;
                }
                chmod 0775, $dir;
                wwslog('notice', 'Created missing directory %s', $dir);
            }

            ## Keep a copy of the previous CSS (only if this is not a custom
            ## css).
            if ((-f "$css_file") && !($in{'editcolors'})) {
                unless (rename "$css_file", "$css_file.$date") {
                    Sympa::Report::reject_report_web(
                        'intern',
                        'cannot_rename_file',
                        {'path' => "$css_file.$date"},
                        $param->{'action'},
                        '',
                        $param->{'user'}{'email'},
                        $robot
                    );
                    wwslog('err', 'Can\'t open file %s.%s', $css_file, $date);
                    return undef;
                }
            }

            if ($in{'subaction_install'}) {
                foreach my $colornumber (0 .. 15) {
                    $param->{'color_' . $colornumber} =
                        $session->{'color_' . $colornumber}
                        if ($session->{'color_' . $colornumber});
                }
            }

            unless (open(CSS, ">$css_file")) {
                Sympa::Report::reject_report_web(
                    'intern', 'cannot_open_file',
                    {'path' => "$css_file"}, $param->{'action'},
                    '', $param->{'user'}{'email'},
                    $robot
                );
                wwslog('err', 'Can\'t open file (write) %s', $css_file);
                return undef;
            }
            unless ($css_template->parse($param, 'css.tt2', \*CSS)) {
                my $error = $css_template->{last_error};
                $error = $error->as_string if ref $error;
                $param->{'tt2_error'} = $error;

                Sympa::send_notify_to_listmaster($robot, 'web_tt2_error',
                    [$error]);
                wwslog('info', 'Error while installing %s: %s',
                    $css_file, $error);
            }
            close(CSS);

            ## Make the CSS readable to anyone
            chmod 0775, "$css_file";

        }

        $param->{'css_result'} = 1;
    }
    return 1;
}

## Multiple add
sub do_add_request {
    wwslog('info', '(%s)', $in{'email'});

    ## Access control
    return undef unless (defined check_authz('do_add_request', 'add'));

    return 1;
}

####################################################
#  do_add
####################################################
#  Adds a user to a list (requested by an other user)
#
# IN : -
#
# OUT : 'loginrequest'
#      | ($in{'previous_action'} || 'review')
#      | undef
####################################################
## TODO: vérifier validité email
sub do_add {
    wwslog('info', '(%s)', $in{'email'});

    my %user;

    ## If a list is not 'open' and allow_subscribe_if_pending has been set to
    ## 'off' returns undef.
    unless ($list->{'admin'}{'status'} eq 'open'
        or Conf::get_robot_conf($robot, 'allow_subscribe_if_pending') eq 'on')
    {
        Sympa::Report::reject_report_web('user', 'list_not_open',
            {'status' => $list->{'admin'}{'status'}},
            $param->{'action'});
        wwslog('info', 'List not open');
        web_db_log(
            {   'target_email' => $in{'email'},
                'status'       => 'error',
                'error_type'   => 'list_not_open'
            }
        );
        return undef;
    }

    my $email_regexp = Sympa::Regexps::email();
    if ($in{'dump'}) {
        foreach (split /\n/, $in{'dump'}) {
            if (/^\s*($email_regexp)(\s+(.*))?\s*$/) {
                $user{Sympa::Tools::Text::canonic_email($1)} = $5;
            }
        }
    } elsif ($in{'email'} =~ /,/) {
        foreach my $pair (split /\0/, $in{'email'}) {
            if ($pair =~ /^($email_regexp)(,(.*))?\s*$/) {
                $user{Sympa::Tools::Text::canonic_email($1)} = $5;
            }
        }
    } elsif ($in{'email'}) {
        foreach my $email (split /\0/, $in{'email'}) {
            $user{Sympa::Tools::Text::canonic_email($email)} = $in{'gecos'};
        }
    } else {
        Sympa::Report::reject_report_web('user', 'no_email', {},
            $param->{'action'});
        wwslog('info', 'No email');
        web_db_log(
            {   'target_email' => $in{'email'},
                'status'       => 'error',
                'error_type'   => 'no_email'
            }
        );
        return undef;
    }

    my ($total, @new_users, @added_users);
    my $comma_emails;
    foreach my $email (keys %user) {
        my $result = Sympa::Scenario::request_action(
            $list, 'add',
            $param->{'auth_method'},
            {   'sender'      => $param->{'user'}{'email'},
                'email'       => $in{'email'},
                'remote_host' => $param->{'remote_host'},
                'remote_addr' => $param->{'remote_addr'}
            }
        );
        my $add_is;
        my $reason;
        if (ref($result) eq 'HASH') {
            $add_is = $result->{'action'};
            $reason = $result->{'reason'};
        }

        unless ($add_is =~ /do_it/) {
            Sympa::Report::reject_report_web('auth', $reason, {},
                $param->{'action'}, $list);
            wwslog('info', '%s may not add', $param->{'user'}{'email'});
            web_db_log(
                {   'target_email' => $in{'email'},
                    'status'       => 'error',
                    'error_type'   => 'internal'
                }
            );
            next;
        }

        unless (Sympa::Tools::Text::valid_email($email)) {
            Sympa::Report::reject_report_web('user', 'incorrect_email',
                {'email' => $email},
                $param->{'action'}, $list);
            wwslog('info', 'Incorrect email %s', $email);
            web_db_log(
                {   'target_email' => $in{'email'},
                    'status'       => 'error',
                    'error_type'   => 'incorrect_email'
                }
            );
            next;
        }

        my $user_entry = $list->get_list_member($email);

        if (defined($user_entry)) {
            Sympa::Report::reject_report_web('user',
                'user_already_subscriber',
                {'list' => $list->{'name'}, 'email' => $email},
                $param->{'action'}, $list);
            wwslog('info', '%s already subscriber', $email);
            web_db_log(
                {   'target_email' => $in{'email'},
                    'status'       => 'error',
                    'error_type'   => 'already_subscriber'
                }
            );
            next;
        }

        my $u2       = Sympa::User::get_global_user($email);
        my $defaults = $list->get_default_user_options();
        my $u;
        %{$u} = %{$defaults};
        $u->{'email'}    = $email;
        $u->{'gecos'}    = $user{$email} || $u2->{'gecos'};
        $u->{'date'}     = $u->{'update_date'} = time;
        $u->{'password'} = $u2->{'password'}
            || Sympa::Tools::Password::tmp_passwd($email);

        ##if (my $reason =
        ##    Sympa::Tools::Password::password_validation($u->{'password'})) {
        ##    Sympa::Report::reject_report_web('user', 'passwd_validation',
        ##        {'reason' => $reason},
        ##        $param->{'action'});
        ##    wwslog('info', 'Password validation');
        ##    web_db_log(
        ##        {   'status'     => 'error',
        ##            'error_type' => 'bad_parameter'
        ##        }
        ##    );
        ##    return undef;
        ##}
        ##
        $u->{'lang'} = $u2->{'lang'} || $list->{'admin'}{'lang'};
        if ($comma_emails) {
            $comma_emails = $comma_emails . ',' . $email;
        } else {
            $comma_emails = $email;
        }

        ##
        push @new_users, $u;
        ## List only email addresses ; used later to remove pending
        ## subrequests
        push @added_users, $email;

        unless ($in{'quiet'} || $add_is =~ /quiet/i) {
            unless ($list->send_probe_to_user('welcome', $email)) {
                wwslog('err', 'Unable to send "welcome" probe to %s', $email);
            }
        }
    }

    $list->add_list_member(@new_users);
    $total = $list->{'add_outcome'}{'added_members'};
    if (defined $list->{'add_outcome'}{'errors'}) {
        if (defined $list->{'add_outcome'}{'errors'}
            {'max_list_members_exceeded'}) {
            Sympa::Report::reject_report_web(
                'user',
                'max_list_members_exceeded',
                {   max_list_members => $list->{'admin'}{'max_list_members'},
                    list             => $list->{'name'},
                    'sub'            => $param->{'user'}{'email'}
                },
                $param->{'action'},
                $list,
                $param->{'user'}{'email'},
                $robot
            );
        } else {
            my $error = $language->gettext_sprintf(
                "Unable to add users in list %s : %s",
                $list->{'name'},
                $list->{'add_outcome'}{'errors'}{'error_message'});
            Sympa::Report::reject_report_web('intern', $error,
                {'sub' => $param->{'user'}{'email'}},
                $param->{'action'}, $list, $param->{'user'}{'email'}, $robot);
        }
        wwslog(
            'info',
            'Subscribe failed: %s',
            $list->{'add_outcome'}{'errors'}{'error_message'}
        );
        web_db_log(
            {   'parameters' => $in{'email'},
                'status'     => 'error',
                'error_type' => 'internal'
            }
        );
        return 'info';
    }

    Sympa::Report::notice_report_web('add_performed', {'total' => $total},
        $param->{'action'});
    web_db_log(
        {   'target_email' => $in{'email'},
            'status'       => 'success'
        }
    );

    foreach my $email (@added_users) {
        web_db_log(
            {   'target_email' => $email,
                'status'       => 'success'
            }
        );
    }

    $in{'list'} = $in{'previous_list'} if ($in{'previous_list'});
    return $in{'previous_action'} || 'review';
}

####################################################
#  do_add_fromsub
####################################################
#  add user from subscription request index
#
####################################################
sub do_add_fromsub {
    wwslog('info', '(%s)', $in{'id'});

    # If a list is not 'open' and allow_subscribe_if_pending has been set to
    # 'off', returns undef.
    unless ($list->{'admin'}{'status'} eq 'open'
        or Conf::get_robot_conf($robot, 'allow_subscribe_if_pending') eq 'on')
    {
        Sympa::Report::reject_report_web('user', 'list_not_open',
            {status => $list->{'admin'}{'status'}},
            $param->{'action'});
        wwslog('info', 'List not open');
        web_db_log(
            {   target_email => $in{'email'},
                status       => 'error',
                error_type   => 'list_not_open'
            }
        );
        return undef;
    }

    my $total = 0;
    foreach my $id (split /\0/, $in{'id'}) {
        next unless $id and $id =~ /\A\w+\z/;

        my $spool_req = Sympa::Spool::Auth->new(
            context => $list,
            keyauth => $id,
            action  => 'add'
        );
        my ($request, $handle);
        while (1) {
            ($request, $handle) = $spool_req->next;
            last unless $handle;
            last if $request;
        }
        unless ($request) {
            Sympa::Report::reject_report_web('user', 'already_added', {},
                $param->{'action'});
            wwslog('err',
                'No request with authkey %s.  It may be already subscribed',
                $id);
            web_db_log(
                {   parameters => $id,
                    status     => 'error',
                    error_type => 'internal'
                }
            );
            next;
        }
        next unless defined $request->{email} and $request->{email} =~ /\S/;

        $list->add_list_member(
            {   email            => $request->{email},
                gecos            => $request->{gecos},
                custom_attribute => $request->{custom_attribute},
            }
        );
        if ($list->{'add_outcome'}{'errors'}) {
            wwslog(
                'info',
                'Subscribe failed: %s',
                $list->{'add_outcome'}{'errors'}{'error_message'}
            );
            web_db_log(
                {   parameters => $in{'email'},
                    status     => 'error',
                    error_type => 'user'
                }
            );
            if ($list->{'add_outcome'}{'errors'}{'max_list_members_exceeded'})
            {
                Sympa::Report::reject_report_web(
                    'user',
                    'max_list_members_exceeded',
                    {   max_list_members =>
                            $list->{'admin'}{'max_list_members'},
                        list => $list->{'name'},
                        sub  => $param->{'user'}{'email'}
                    },
                    $param->{'action'},
                    $list,
                    $param->{'user'}{'email'},
                    $robot
                );
                last;
            } else {
                my $error = $language->gettext_sprintf(
                    "Unable to add users in list %s : %s",
                    $list->{'name'},
                    $list->{'add_outcome'}{'errors'}{'error_message'});
                Sympa::Report::reject_report_web(
                    'intern',
                    $error,
                    {sub => $param->{'user'}{'email'}},
                    $param->{'action'},
                    $list,
                    $param->{'user'}{'email'},
                    $robot
                );
            }
        } else {
            $spool_req->remove($handle);
            web_db_log({target => $request->{email}, status => 'success'});
            $total++;
        }
    }
    if ($total) {
        Sympa::Report::notice_report_web('add_performed', {total => $total},
            $param->{'action'});
    }

    return 'subindex';
}

####################################################
#  do_del
####################################################
#  Deletes a user from a list (requested by an other user)
#
# IN : -
#
# OUT : 'loginrequest'
#      | ($in{'previous_action'} || 'review') | undef
#
####################################################
## TODO: vérifier validité email
sub do_del {
    wwslog('info', '');

    $in{'email'} = Sympa::Tools::Text::unescape_chars($in{'email'});

    my $result = Sympa::Scenario::request_action(
        $list, 'del',
        $param->{'auth_method'},
        {   'sender'      => $param->{'user'}{'email'},
            'email'       => $in{'email'},
            'remote_host' => $param->{'remote_host'},
            'remote_addr' => $param->{'remote_addr'}
        }
    );
    my $del_is;
    my $reason;
    if (ref($result) eq 'HASH') {
        $del_is = $result->{'action'};
        $reason = $result->{'reason'};
    }

    unless ($del_is =~ /do_it/) {
        Sympa::Report::reject_report_web('auth', $reason, {},
            $param->{'action'}, $list);

        wwslog('info', '%s may not del', $param->{'user'}{'email'});
        web_db_log(
            {   'target_email' => $in{'email'},
                'status'       => 'error',
                'error_type'   => 'internal'
            }
        );
        return undef;
    }

    my @emails = split /\0/, $in{'email'};

    my ($total, @removed_users);

    foreach my $email (@emails) {

        my $escaped_email = Sympa::Tools::Text::escape_chars($email);

        my $user_entry = $list->get_list_member($email);

        unless (defined($user_entry)) {
            Sympa::Report::reject_report_web('user', 'user_not_subscriber',
                {'email' => $email},
                $param->{'action'}, $list);

            wwslog('info', '%s not subscribed', $email);
            web_db_log(
                {   'target_email' => $in{'email'},
                    'status'       => 'error',
                    'error_type'   => 'not_subscriber'
                }
            );
            next;
        }

        push @removed_users, $email;

        my $bounce_dir = $list->get_bounce_dir();

        if (-f $bounce_dir . '/' . $escaped_email) {
            unless (unlink $bounce_dir . '/' . $escaped_email) {
                wwslog(
                    'info',
                    'Failed deleting %s',
                    $bounce_dir . '/' . $escaped_email
                );
                web_db_log(
                    {   'target_email' => $in{'email'},
                        'status'       => 'error',
                        'error_type'   => 'internal'
                    }
                );
                next;
            }
        }

        wwslog('info', 'Subscriber %s deleted from list %s',
            $email, $param->{'list'});

        unless ($in{'quiet'}) {
            unless (Sympa::send_file($list, 'removed', $email, {})) {
                wwslog('notice', 'Unable to send template "removed" to %s',
                    $email);
            }
        }
        web_db_log(
            {   'target_email' => $email,
                'status'       => 'success'
            }
        );
    }

    $total = $list->delete_list_member(
        'users'     => \@removed_users,
        'exclude'   => '1',
        'operation' => 'del',
    );

    unless (defined $total) {
        Sympa::Report::reject_report_web('intern',
            'delete_subscriber_db_failed', {}, $param->{'action'}, $list,
            $param->{'user'}{'email'}, $robot);
        wwslog('info', 'Failed');
        web_db_log(
            {   'target_email' => $in{'email'},
                'status'       => 'error',
                'error_type'   => 'internal'
            }
        );
        return undef;
    }

    Sympa::Report::notice_report_web('del_performed', {'total' => $total},
        $param->{'action'});

    $param->{'is_subscriber'} = 1;
    $param->{'may_signoff'}   = 1;

    ## Skip search because we don't have the expression anymore
    delete $in{'previous_action'} if ($in{'previous_action'} eq 'search');

    return $in{'previous_action'} || 'review';
}

####################################################
#  do_del_fromsig
####################################################
#  delete user in signoff request index
#
####################################################
sub do_del_fromsig {
    wwslog('info', '(%s)', $in{'id'});

    # If a list is not 'open' and allow_subscribe_if_pending has been set to
    # 'off', returns undef.
    unless ($list->{'admin'}{'status'} eq 'open'
        or Conf::get_robot_conf($robot, 'allow_subscribe_if_pending') eq 'on')
    {
        Sympa::Report::reject_report_web('user', 'list_not_open',
            {status => $list->{'admin'}{'status'}},
            $param->{'action'});
        wwslog('info', 'List not open');
        web_db_log(
            {   target_email => $in{'email'},
                status       => 'error',
                error_type   => 'list_not_open'
            }
        );
        return undef;
    }

    my $total = 0;
    foreach my $id (split /\0/, $in{'id'}) {
        next unless $id and $id =~ /\A\w+\z/;

        my $spool_req = Sympa::Spool::Auth->new(
            context => $list,
            keyauth => $id,
            action  => 'del'
        );
        my ($request, $handle);
        while (1) {
            ($request, $handle) = $spool_req->next;
            last unless $handle;
            last if $request;
        }
        unless ($request) {
            Sympa::Report::reject_report_web('user', 'already_deleted', {},
                $param->{'action'});
            wwslog('err',
                'No request with authkey %s.  It may be already deleted',
                $id);
            web_db_log(
                {   parameters => $id,
                    status     => 'error',
                    error_type => 'internal'
                }
            );
            next;
        }
        next unless defined $request->{email} and $request->{email} =~ /\S/;

        my $status = $list->delete_list_member(
            users     => [$request->{email}],
            exclude   => '1',
            operation => 'del',
        );
        unless ($status) {
            wwslog('info', 'Deletion failed: %s');
            web_db_log(
                {   parameters => $request->{email},
                    status     => 'error',
                    error_type => 'user'
                }
            );
            Sympa::Report::reject_report_web('intern',
                {sig => $request->{email}},
                $param->{'action'}, $list, $param->{'user'}{'email'}, $robot);
        } else {
            $spool_req->remove($handle);
            web_db_log({target => $request->{email}, status => 'success'});
            $total++;
        }
    }
    if ($total) {
        Sympa::Report::notice_report_web('del_performed', {total => $total},
            $param->{'action'});
    }

    return 'sigindex';
}

####################################################
#  do_modindex
####################################################
#  Web page for an editor to moderate documents and
#  and/or to tag message in message topic context
#
# IN : -
#
# OUT : 'loginrequest' | 'admin' | '1' | undef
#
#######################################################
sub do_modindex {
    wwslog('info', '');

    # Load message list.
    $param->{'spool'} = [];
    my $spool_mod = Sympa::Spool::Moderation->new(context => $list);
    while (1) {
        my ($message, $handle) = $spool_mod->next(no_lock => 1);
        last unless $handle;
        next unless $message and not $message->{validated};

        my $id = $message->{authkey};

        my $date = $message->get_header('Date') || undef;
        my ($date_epoch, $date_str);
        if ($date) {
            $date_epoch = eval {
                DateTime::Format::Mail->new->loose->parse_datetime($date)
                    ->epoch;
            };
            if (defined $date_epoch) {
                $date_str =
                    $language->gettext_strftime('%a, %d %b %Y %H:%M:%S',
                    localtime $date_epoch);
            }
        }

        my $entry = {
            size          => int($message->{size} / 1024 + 0.5),
            subject       => ($message->{decoded_subject} || 'no_subject'),
            date_smtp     => $date,
            date_epoch    => $date_epoch,
            date          => $date_str,
            from          => $message->{sender},
            gecos         => $message->{gecos},
            spam_status   => $message->{spam_status},
            is_subscriber => $list->is_list_member($message->{sender}),
        };

        #FIXME: Are these required?
        foreach my $field (qw(subject date from)) {
            $entry->{$field} =~ s/&/&amp;/g;
            $entry->{$field} =~ s/</&lt;/g;
            $entry->{$field} =~ s/>/&gt;/g;
        }

        push @{$param->{'spool'}}, {key => $id, value => $entry};
    }

    if ($list->is_there_msg_topic()) {
        $param->{'request_topic'} = 1;

        foreach my $top (@{$list->{'admin'}{'msg_topic'}}) {
            if ($top->{'name'}) {
                push(@{$param->{'available_topics'}}, $top);
            }
        }
        $param->{'topic_required'} = $list->is_msg_topic_tagging_required();
    }

    my $available_files = Sympa::Tools::WWW::get_templates_list($list, 'mail',
        ignore_global => 1);
    foreach my $file (keys %$available_files) {

        if ($file eq 'reject.tt2') {

            my $base          = $list->{'dir'} . '/mail_tt2/';
            my $absolute_file = $base . 'reject.tt2';
            if (-l $absolute_file) {

                my $default = readlink($absolute_file);
                if ((-f $default) || (-f $base . $default)) {
                    $default =~ s/^.*reject_//;
                    $default =~ s/.tt2$//;
                    $param->{'default_reject_template'} = $default;
                } else {
                    # link to no existing file. remove link
                    wwslog(
                        'err',
                        'Link %s point to un no existing file (%s)',
                        $base . 'reject.tt2', $default
                    );
                    unless (unlink($absolute_file)) {
                        wwslog(
                            'err',
                            'Could not unlink %s',
                            $base . 'reject.tt2'
                        );
                    }
                }
            } elsif (-f $absolute_file) {
                # replace existing reject.tt2 file by a symlink to
                # reject_default.tt2 for compatibility with version older than
                # 6.0
                unless (rename($absolute_file, $base . 'reject_default.tt2'))
                {
                    wwslog(
                        'err',
                        'Could not rename %, %s',
                        $base . 'reject.tt2',
                        $base . 'reject_default.tt2'
                    );
                }
                unless (symlink($base . 'reject_default.tt2', $absolute_file))
                {
                    wwslog(
                        'err',
                        'Could not symlink %s, %s',
                        $base . 'reject_default.tt2',
                        $absolute_file
                    );
                }

                $param->{'default_reject_template'} = 'default';
                push(@{$param->{'available_files'}}, 'default');
            }
        } else {
            next unless ($file =~ /^reject_/);
            $file =~ s/^reject_//;
            $file =~ s/.tt2$//;
            push(@{$param->{'available_files'}}, $file);
        }
    }

    return 1;
}

sub do_docindex {
    wwslog('info', '');

    # Shared documents awaiting moderation
    foreach my $d (@{$param->{'doc_mod_list'}}) {

        $d =~ /^(([^\/]*\/)*)([^\/]+)(\/?)$/;

        my $long_path = $1;           # path without the filename
        my $fname     = $3;           # the filename with .moderate
        my $path      = $long_path;
        # The path for the user, without the filename
        $path =~ s/^.*\/shared//;
        # The filename without .moderate
        my $visible_fname = Sympa::Tools::WWW::make_visible_path($fname);
        my $visible_path  = $path;
        $visible_path = Sympa::Tools::WWW::make_visible_path($visible_path);

        my %desc_hash;
        if ($d && (-e "$long_path.desc.$fname")) {
            %desc_hash =
                Sympa::Tools::WWW::get_desc_file("$long_path.desc.$fname");
        }

        my $doc = {};
        $doc->{'visible_path'}  = $visible_path;
        $doc->{'visible_fname'} = $visible_fname;
        $doc->{'escaped_fname'} =
            Sympa::SharedDocument::escape_docname($fname, '/');
        $doc->{'escaped_path'} =
            Sympa::SharedDocument::escape_docname($path, '/');
        $doc->{'fname'} = $fname;
        $doc->{'size'}  = (-s $d) / 1000;
        $doc->{'date'} =
            $language->gettext_strftime("%d %b %Y",
            localtime Sympa::Tools::File::get_mtime($d));
        $doc->{'author'} = $desc_hash{'email'};
        $doc->{'path'}   = $path;

        push(@{$param->{'info_doc_mod'}}, $doc);
    }

    return 1;
}

### installation of moderated documents of shared
sub do_d_install_shared {
    wwslog('info', '(%s)', $in{'id'});

    if ($in{'mode_cancel'}) {
        return 'docindex';
    }

    my $shareddir = $list->{'dir'} . '/shared';
    my $file;
    my $slash_path;
    my $fname;
    my $visible_fname;
    # list of file already existing
    my @list_file_exist;

    unless ($in{'mode_confirm'} || $in{'mode_cancel'}) {

        # file already exists ?
        foreach my $id (split /\0/, $in{'id'}) {

            $file = "$shareddir$id";
            $id =~ /^(([^\/]*\/)*)([^\/]+)(\/?)$/;
            $slash_path    = $1;
            $fname         = $3;
            $visible_fname = Sympa::Tools::WWW::make_visible_path($fname);

            if (-e "$file") {
                if (-e "$shareddir$slash_path$visible_fname") {
                    push(@list_file_exist, "$slash_path$visible_fname");
                }
            }
        }

        if (@list_file_exist) {

            $param->{'list_file'} = \@list_file_exist;
            my @id = split(/\0/, $in{'id'});
            $param->{'id'} = \@id;

            return 1;
        }
    }

    # install the file(s) selected
    foreach my $id (split /\0/, $in{'id'}) {

        $file = "$shareddir$id";
        $id =~ /^(([^\/]*\/)*)([^\/]+)(\/?)$/;
        $slash_path = $1;
        $fname      = $3;
        my $new_fname;    ## new filename without the .moderate extension
        if ($fname =~ /^\.(.+)\.moderate$/) {
            $new_fname = $1;
        }
        my $visible_path = Sympa::Tools::WWW::make_visible_path($slash_path);
        $visible_fname = Sympa::Tools::WWW::make_visible_path($fname);

        if (-e "$file") {

            # rename the old file in .old if exists
            if (-e "$shareddir$slash_path$new_fname") {
                unless (
                    rename "$shareddir$slash_path$new_fname",
                    "$shareddir$slash_path$new_fname.old"
                    ) {
                    Sympa::Report::reject_report_web(
                        'intern',
                        'rename_file',
                        {   'old' => "$shareddir$slash_path$new_fname",
                            'new' => "$shareddir$slash_path$new_fname.old"
                        },
                        $param->{'action'},
                        $list,
                        $param->{'user'}{'email'},
                        $robot
                    );
                    wwslog('err', 'Failed to rename %s%s%s to .old: %s',
                        $shareddir, $slash_path, $new_fname, $ERRNO);
                    web_db_log(
                        {   'status'     => 'error',
                            'error_type' => 'internal'
                        }
                    );
                    return undef;
                }
                unless (
                    rename "$shareddir$slash_path.desc.$new_fname",
                    "$shareddir$slash_path.desc.$new_fname.old"
                    ) {
                    Sympa::Report::reject_report_web(
                        'intern',
                        'rename_file',
                        {   'old' => "$shareddir$slash_path.desc.$new_fname",
                            'new' =>
                                "$shareddir$slash_path.desc.$new_fname.old"
                        },
                        $param->{'action'},
                        $list,
                        $param->{'user'}{'email'},
                        $robot
                    );
                    wwslog('err', 'Failed to rename %s%s.desc.%s to .old: %s',
                        $shareddir, $slash_path, $new_fname, $ERRNO);
                    web_db_log(
                        {   'status'     => 'error',
                            'error_type' => 'internal'
                        }
                    );
                    return undef;
                }

            }

            unless (
                rename("$shareddir$id", "$shareddir$slash_path$new_fname")) {
                Sympa::Report::reject_report_web(
                    'intern',
                    'rename_file',
                    {   'old' => "$shareddir$id",
                        'new' => "$shareddir$slash_path$new_fname"
                    },
                    $param->{'action'},
                    $list,
                    $param->{'user'}{'email'},
                    $robot
                );
                wwslog('err', 'Failed to rename %s to %s%s%s: %s',
                    $file, $shareddir, $slash_path, $new_fname, $ERRNO);
                web_db_log(
                    {   'status'     => 'error',
                        'error_type' => 'internal'
                    }
                );
                return undef;
            }
            unless (
                rename(
                    "$shareddir$slash_path.desc.$fname",
                    "$shareddir$slash_path.desc.$new_fname"
                )
                ) {
                Sympa::Report::reject_report_web(
                    'intern',
                    'rename_file',
                    {   'old' => "$shareddir$slash_path.desc.$fname",
                        'new' => "$shareddir$slash_path.desc.$new_fname"
                    },
                    $param->{'action'},
                    $list,
                    $param->{'user'}{'email'},
                    $robot
                );
                wwslog('err', 'Failed to rename %s to %s%s%s: %s',
                    $file, $shareddir, $slash_path, $new_fname, $ERRNO);
                web_db_log(
                    {   'status'     => 'error',
                        'error_type' => 'internal'
                    }
                );
                return undef;
            }

            # send a message to the author
            my %context;
            $context{'installed_by'} = $param->{'user'}{'email'};
            $context{'filename'}     = "$visible_path$visible_fname";

            my %desc_hash;
            if ($id && (-e "$shareddir$slash_path.desc.$visible_fname")) {
                %desc_hash = Sympa::Tools::WWW::get_desc_file(
                    "$shareddir$slash_path.desc.$visible_fname");
            }

            my $sender = $desc_hash{'email'};
            unless (
                Sympa::send_file(
                    $list, 'd_install_shared', $sender, \%context
                )
                ) {
                wwslog('notice',
                    'Unable to send template "d_install_shared" to %s',
                    $sender);
            }
        }
    }

    Sympa::Report::notice_report_web('performed', {}, $param->{'action'});
    web_db_log({'status' => 'success'});
    return 'docindex';
}

### reject moderated documents of shared
sub do_d_reject_shared {
    wwslog('info', '(%s)', $in{'id'});

    my $shareddir = $list->{'dir'} . '/shared';
    my $file;
    my $slash_path;
    my $fname;
    my $visible_fname;

    foreach my $id (split /\0/, $in{'id'}) {

        $file = "$shareddir$id";
        $id =~ /^(([^\/]*\/)*)([^\/]+)(\/?)$/;
        $slash_path    = $1;
        $fname         = $3;
        $visible_fname = Sympa::Tools::WWW::make_visible_path($fname);
        my $visible_path = Sympa::Tools::WWW::make_visible_path($slash_path);

        unless ($in{'quiet'}) {

            my %context;
            my $sender;
            $context{'rejected_by'} = $param->{'user'}{'email'};
            $context{'filename'}    = "$visible_path$visible_fname";

            my %desc_hash;
            if ($id && (-e "$shareddir$slash_path.desc.$fname")) {
                %desc_hash = Sympa::Tools::WWW::get_desc_file(
                    "$shareddir$slash_path.desc.$fname");
            }
            $sender = $desc_hash{'email'};

            unless (
                Sympa::send_file(
                    $list, 'd_reject_shared', $sender, \%context
                )
                ) {
                wwslog('notice',
                    'Unable to send template "d_reject_shared" to %s',
                    $sender);
            }
        }

        unless (unlink($file)) {
            Sympa::Report::reject_report_web('intern', 'erase_file',
                {'file' => $file},
                $param->{'action'}, $list, $param->{'user'}{'email'}, $robot);
            wwslog('err', 'Failed to erase %s', $file);
            web_db_log(
                {   'parameters' => $in{'id'},
                    'status'     => 'error',
                    'error_type' => 'internal'
                }
            );
            return undef;
        }

        unless (unlink("$shareddir$slash_path.desc.$fname")) {
            Sympa::Report::reject_report_web(
                'intern',
                'erase_file',
                {'file' => "$shareddir$slash_path.desc.$fname"},
                $param->{'action'},
                $list,
                $param->{'user'}{'email'},
                $robot
            );
            wwslog('err',
                "do_d_reject_shared: failed to erase $shareddir$slash_path.desc.$fname"
            );
            web_db_log(
                {   'parameters' => $in{'id'},
                    'status'     => 'error',
                    'error_type' => 'internal'
                }
            );
            return undef;
        }
    }

    Sympa::Report::notice_report_web('performed', {}, $param->{'action'});
    web_db_log(
        {   'parameters' => $in{'id'},
            'status'     => 'success'
        }
    );
    return 'docindex';
}

####################################################
#  do_reject
####################################################
#  Moderation of messages : rejects messages and notifies
#  their senders. If in{'blacklist'} add sender to list blacklist
#
# IN : -
#
# OUT : 'loginrequest' | 'modindex' | undef
#
####################################################
sub do_reject {

    # toggle selection javascript have a distinction of spam and ham base on
    # the checkbox name . It is not useful here so join id list and idspam
    # list.
    $in{'id'} .= ',' . $in{'idspam'} if ($in{'idspam'});
    $in{'id'} =~ s/^,//;
    $in{'id'} =~ s/\0/,/g;

    ## The quiet information might either be provided by the 'quiet' variable
    ## or by the 'quiet' value of the 'message_template' variable
    if ($in{'message_template'} eq 'quiet') {
        $in{'quiet'} = 1;
        delete $in{'message_template'};
    }
    if ($in{'blacklist'}) {
        $in{'quiet'} = 1;
    }

    wwslog('info', '(%s)', $in{'id'});
    my $file;

    $param->{'blacklist_added'}   = 0;
    $param->{'blacklist_ignored'} = 0;
    foreach my $id (split(/,/, $in{'id'})) {
        next unless $id and $id =~ /\A\w+\z/;

        my $spool_mod =
            Sympa::Spool::Moderation->new(context => $list, authkey => $id);
        my ($message, $handle);
        while (1) {
            ($message, $handle) = $spool_mod->next;
            last unless $handle;
            last if $message and not $message->{validated};
        }

        unless ($message) {
            Sympa::Report::reject_report_web('user', 'already_moderated', {},
                $param->{'action'});
            wwslog('err', 'Unable to get message with <%s> for list %s',
                $id, $list);
            web_db_log(
                {   'parameters' => $id,
                    'status'     => 'error',
                    'error_type' => 'internal'
                }
            );
            next;
        }

        #  extract sender address is needed to report reject to sender and in
        #  case the sender is to be added to the blacklist
        if (($in{'quiet'} ne '1') || ($in{'blacklist'})) {
            my $rejected_sender = $message->{'sender'};
            if ($rejected_sender) {
                unless ($in{'message_template'} eq 'reject_quiet') {
                    my %context;
                    $context{'subject'}       = $message->{'decoded_subject'};
                    $context{'rejected_by'}   = $param->{'user'}{'email'};
                    $context{'template_used'} = $in{'message_template'};
                    unless (
                        Sympa::send_file(
                            $list,            $in{'message_template'},  #FIXME
                            $rejected_sender, \%context
                        )
                        ) {
                        wwslog('notice',
                            "Unable to send template $in{'message_template'} to $rejected_sender"
                        );
                    }
                }
                if ($in{'blacklist'}) {
                    if (_add_in_blacklist($rejected_sender, $robot, $list)) {
                        $param->{'blacklist_added'} += 1;
                        wwslog('info',
                            "added $rejected_sender to $list->{'name'} blacklist"
                        );
                    } else {
                        wwslog('notice',
                            "Unable to add $rejected_sender to $list->{'name'} blacklist"
                        );
                        $param->{'blacklist_ignored'} += 0;
                    }
                }
            } else {
                $log->syslog(
                    'err',
                    'No sender found for message %s.  Unable to use her address to add to blacklist or send notification',
                    $message
                );
            }
        }

        if (   ($in{'signal_spam'})
            && ($Conf::Conf{'reporting_spam_script_path'} ne '')) {
            if (-x $Conf::Conf{'reporting_spam_script_path'}) {
                unless (
                    open(SCRIPT, "|$Conf::Conf{'reporting_spam_script_path'}"
                    )
                    ) {
                    $log->syslog('err',
                        "could not execute $Conf::Conf{'reporting_spam_script_path'}"
                    );
                }
                # Sending encrypted form in case a crypted message would be
                # sent by error.
                print SCRIPT $message->as_string;

                if (close(SCRIPT)) {
                    $log->syslog('info',
                        "message $file reported as spam by $param->{'user'}{'email'}"
                    );
                } else {
                    $log->syslog('err',
                        "could not report message $file as spam (close failed)"
                    );
                }
            } else {
                $log->syslog('err',
                    "ignoring parameter reporting_spam_script_path, value $Conf::Conf{'reporting_spam_script_path'} because not an executable script"
                );
            }
        }

        $spool_mod->remove($handle) and $spool_mod->html_remove($message);

    }
    web_db_log(
        {   'parameters' => $in{'id'},
            'status'     => 'success'
        }
    );

    web_db_stat_log();

    Sympa::Report::notice_report_web('performed', {}, $param->{'action'});

    return 'modindex';
}

####################################################
#  do_distribute
####################################################
#  Moderation of messages : distributes moderated
#  messages and tag it in message moderation context
#
# IN : - id of message to distribute. This value can also be in idspam
# parameter
#
# OUT : 'loginrequest' | 'modindex' | undef
#
######################################################
sub do_distribute {
    $in{'id'} .= ',' . $in{'idspam'} if ($in{'idspam'});
    $in{'id'} =~ s/^,//;
    $in{'id'} =~ s/\0/,/g;

    wwslog('info', '(%s)', $in{'id'});
    my ($msg, $file);

    my @mail_command = ();

    ## msg topics
    my @msg_topics;
    foreach my $msg_topic (@{$list->{'admin'}{'msg_topic'} || []}) {
        my $var_name = "topic_" . "$msg_topic->{'name'}";
        if ($in{"$var_name"}) {
            push @msg_topics, $msg_topic->{'name'};
        }
    }
    my $list_topics = join(',', @msg_topics);

    if (!$list_topics && $list->is_msg_topic_tagging_required()) {
        Sympa::Report::reject_report_web('user', 'msg_topic_missing', {},
            $param->{'action'});
        wwslog('info', 'Message(s) without topic but in a required list');
        web_db_log(
            {   'parameters' => $in{'id'},
                'status'     => 'error',
                'error_type' => 'no_topic'
            }
        );
        return undef;
    }

    # Load message list.
    foreach my $id (split(/,/, $in{'id'})) {    # QUIET DISTRIBUTE
        next unless $id and $id =~ /\A\w+\z/;

        my $spool_mod =
            Sympa::Spool::Moderation->new(context => $list, authkey => $id);
        my ($message, $handle);
        while (1) {
            ($message, $handle) = $spool_mod->next;
            last unless $handle;
            last if $message and not $message->{validated};
        }

        unless ($message) {
            Sympa::Report::reject_report_web('user', 'already_moderated', {},
                $param->{'action'});
            wwslog('err', 'Unable to find message with <%s> for list %s',
                $id, $list);
            web_db_log(
                {   'parameters' => $id,
                    'status'     => 'error',
                    'error_type' => 'internal'
                }
            );
            next;
        }
        push @mail_command,
            sprintf('QUIET DISTRIBUTE %s %s', $list->{'name'}, $id);

        # TAG
        if ($list_topics) {
            Sympa::Topic->new(topic => $list_topics, method => 'editor')
                ->store($message);
        }

        $spool_mod->remove($handle, action => 'distribute');
    }

    # Commands are injected into incoming spool directly with "md5"
    # authentication level.
    my $cmd_message = Sympa::Message->new(
        sprintf("\n\n%s\n", join("\n", @mail_command)),
        context         => $robot,
        envelope_sender => Conf::get_robot_conf($robot, 'request'),
        sender          => $param->{'user'}{'email'},
        md5_check       => 1,
        message_id      => Sympa::unique_message_id($robot)
    );
    $cmd_message->add_header('Content-Type', 'text/plain; Charset=utf-8');

    unless (Sympa::Spool::Incoming->new->store($cmd_message)) {
        Sympa::Report::reject_report_web(
            'intern',
            'cannot_send_distribute',
            {   'from'     => $param->{'user'}{'email'},
                'listname' => $list->{'name'}
            },
            $param->{'action'},
            $list,
            $param->{'user'}{'email'},
            $robot
        );
        wwslog('err', 'Failed to send message for list %s, id %s',
            $list, $in{'id'});
        web_db_log(
            {   'parameters' => $in{'id'},
                'status'     => 'error',
                'error_type' => 'internal'
            }
        );
        return undef;
    }

    web_db_log(
        {   'parameters' => $in{'id'},
            'status'     => 'success'
        }
    );

    Sympa::Report::notice_report_web('performed_soon', {},
        $param->{'action'});

    return 'modindex';
}

####################################################
#  do_add_frommod
####################################################
#  add user from moderation index
#
####################################################
sub do_add_frommod {
    $in{'id'} =~ s/^,//;
    $in{'id'} =~ s/\0/,/g;

    wwslog('info', '(%s)', $in{'id'});

    # Load message list.
    $in{'dump'} = '';
    foreach my $id (split /,/, $in{'id'}) {
        next unless $id and $id =~ /\A\w+\z/;

        my $spool_mod =
            Sympa::Spool::Moderation->new(context => $list, authkey => $id);
        my ($message, $handle);
        while (1) {
            ($message, $handle) = $spool_mod->next(no_lock => 1);
            last unless $handle;
            last if $message;    # Won't check {validated} metadata.
        }
        unless ($message) {
            Sympa::Report::reject_report_web('user', 'already_moderated', {},
                $param->{'action'});
            wwslog('err',
                'No message with authkey %s.  It may be already moderated',
                $id);
            web_db_log(
                {   'parameters' => $id,
                    'status'     => 'error',
                    'error_type' => 'internal'
                }
            );
            next;
        }
        my $email = $message->{sender};
        next unless defined $email and $email =~ /\S/;
        my $fullname = $message->{gecos};

        if (defined $fullname and $fullname =~ /\S/) {
            $in{'dump'} .= "$email $fullname\n";
        } else {
            $in{'dump'} .= "$email\n";
        }
    }

    if ($in{'dump'}) {
        delete $in{'email'};
        $in{'previous_list'} = $list->{'name'};
        do_add();
    }

    return 'modindex';
}

####################################################
#  do_viewmod
####################################################
#  Web page for an editor to moderate a mail and/or
#  to tag it in message topic context
#
# IN : -
#
# OUT : 'login,request' | '1' | undef
#
####################################################
sub do_viewmod {
    wwslog('info', '(%s, %s)', $in{'id'}, $in{'file'});

    # Prevent directory traversal.
    if ($in{'file'}) {
        my $subpath = $in{'file'};
        $subpath =~ s{\Amsg00000/}{};
        delete $in{'file'} if $subpath =~ m{/};
    }

    my $msg;
    my $tmp_dir;

    my $available_files = Sympa::Tools::WWW::get_templates_list($list, 'mail',
        ignore_global => 1);
    foreach my $file (keys %$available_files) {
        next unless ($file =~ /^reject_/);
        $file =~ s/^reject_//;
        $file =~ s/.tt2$//;
        push(@{$param->{'available_files'}}, $file);
    }

    my $html_dir =
          $Conf::Conf{'viewmail_dir'} . '/mod/'
        . $list->get_list_id() . '/'
        . $in{'id'};

    unless (-d $html_dir) {
        Sympa::Report::reject_report_web('intern',
            'no_html_message_available', {'dir' => $html_dir},
            $param->{'action'});
        wwslog('err', 'No HTML version of the message available in %s',
            $html_dir);
        return undef;
    }

    if (    $in{'file'}
        and $in{'file'} ne 'msg00000.html'
        and -f $html_dir . '/' . $in{'file'}
        and -r $html_dir . '/' . $in{'file'}) {
        $in{'file'} =~ /\.(\w+)$/;
        $param->{'file_extension'} = $1;
        $param->{'file'}           = $html_dir . '/' . $in{'file'};
        $param->{'bypass'}         = 1;
    } else {
        if (open my $fh, '<', $html_dir . '/msg00000.html') {
            $param->{'html_content'} = do { local $RS; <$fh> };
            close $fh;
        }

        #FIXME: Is this required?
        push @other_include_path, $html_dir;
    }

    $param->{'id'} = $in{'id'};

    if ($list->is_there_msg_topic()) {
        $param->{'request_topic'} = 1;

        foreach my $top (@{$list->{'admin'}{'msg_topic'} || []}) {
            if ($top->{'name'}) {
                push(@{$param->{'available_topics'}}, $top);
            }
        }
        $param->{'topic_required'} = $list->is_msg_topic_tagging_required();
    }

    return 1;
}

## Edition of list/sympa files
## No list -> sympa files (helpfile,...)
## TODO : upload
## TODO : edit family file ???
sub do_editfile {
    wwslog('info', '(%s)', $in{'file'});

    $param->{'subtitle'} = sprintf $param->{'subtitle'}, $in{'file'};

    my %files = (
        description_templates => ['info', 'homepage'],
        message_templates     => [
            'welcome.tt2',    'bye.tt2',
            'removed.tt2',    'message.footer',
            'message.header', 'remind.tt2',
            'invite.tt2',     'reject.tt2',
            'your_infected_msg.tt2'
        ],
        all_templates => [
            'info',           'homepage',
            'welcome.tt2',    'bye.tt2',
            'removed.tt2',    'message.footer',
            'message.header', 'remind.tt2',
            'invite.tt2',     'reject.tt2',
            'your_infected_msg.tt2'
        ]
    );

    $in{'file'} = 'all_templates' unless ($in{'file'});
    $param->{'selected_file'} = $in{'file'};
    $param->{'previous_action'} = $in{'previous_action'} || '';

    if (defined $files{$in{'file'}}) {
        foreach my $f (@{$files{$in{'file'}}}) {
            my $filename_for_auth = $f;
            $filename_for_auth = 'info.file'
                if ($filename_for_auth eq 'info');
            next
                unless (
                $list->may_edit(
                    $filename_for_auth, $param->{'user'}{'email'}
                ) eq 'write'
                );
            if ($Sympa::Tools::WWW::filenames{$f}{'gettext_id'}) {
                $param->{'files'}{$f}{'complete'} =
                    $language->gettext(
                    $Sympa::Tools::WWW::filenames{$f}{'gettext_id'});
            } else {
                $param->{'files'}{$f}{'complete'} = $f;
            }
            $param->{'files'}{$f}{'selected'} = '';
        }
        return 1;
    }

    unless (defined $Sympa::Tools::WWW::filenames{$in{'file'}}) {
        Sympa::Report::reject_report_web('user', 'file_not_editable',
            {'file' => $in{'file'}},
            $param->{'action'});
        wwslog('err', 'File %s not editable', $in{'file'});
        web_db_log(
            {   'parameters' => $in{'file'},
                'status'     => 'error',
                'error_type' => 'internal'
            }
        );
        return undef;
    }

    $param->{'file'} = $in{'file'};
    $param->{'complete'} =
        $language->gettext(
        $Sympa::Tools::WWW::filenames{$in{'file'}}{'gettext_id'});

    my $subdir = '';
    if ($in{'file'} =~ /\.tt2$/) {
        $subdir = 'mail_tt2/';
    }

    if ($param->{'list'}) {
        my $filename_for_auth = $in{'file'};
        $filename_for_auth = 'info.file' if ($filename_for_auth eq 'info');
        my ($role, $right) =
            $list->may_edit($filename_for_auth, $param->{'user'}{'email'});

        unless ($right eq 'write') {
            Sympa::Report::reject_report_web('auth', 'edit_right',
                {'role' => $role, 'right' => $right},
                $param->{'action'}, $list);
            wwslog('err', 'Not allowed');
            web_db_log(
                {   'parameters' => $in{'file'},
                    'status'     => 'error',
                    'error_type' => 'authorization'
                }
            );
            return undef;
        }

        ## Add list lang to tpl filename
        my $file = $in{'file'};
        #$file =~ s/\.tpl$/\.$list->{'admin'}{'lang'}\.tpl/;

        ## Look for the template
        $param->{'filepath'} =
            Sympa::search_fullpath($list || $robot, $file, subdir => $subdir);

        ## There might be no matching file if default template not provided
        ## with Sympa
        if (defined $param->{'filepath'}) {
            ## open file and provide filecontent to the parser
            ## It allows to us the correct file encoding
            my $fh;
            unless (open $fh, '<', $param->{'filepath'}) {
                Sympa::Report::reject_report_web(
                    'intern',
                    'cannot_open_file',
                    {'file' => $param->{'filepath'}},
                    $param->{'action'},
                    $list,
                    $param->{'user'}{'email'},
                    $robot
                );
                wwslog('err', 'Failed to open file %s: %s',
                    $param->{'filepath'}, $ERRNO);
                web_db_log(
                    {   'parameters' => $in{'file'},
                        'status'     => 'error',
                        'error_type' => 'internal'
                    }
                );
                return undef;
            }

            my $file_content = do { local $RS; <$fh> };
            close $fh;
            Encode::from_to($file_content, $Conf::Conf{'filesystem_encoding'},
                'utf8');
            $param->{'filecontent'} = $file_content;
        } else {
            $param->{'filepath'} = $list->{'dir'} . '/' . $subdir . $file;
        }

        ## Default for 'homepage' is 'info'
        if (($in{'file'} eq 'homepage')
            && !$param->{'filepath'}) {
            $param->{'filepath'} = Sympa::search_fullpath($list || $robot,
                'info', subdir => $subdir);
        }
    } else {
        unless (Sympa::is_listmaster($robot, $param->{'user'}{'email'})) {
            Sympa::Report::reject_report_web('user', 'missing_arg',
                {'argument' => 'list'},
                $param->{'action'});
            wwslog('err', 'No list');
            web_db_log(
                {   'parameters' => $in{'file'},
                    'status'     => 'error',
                    'error_type' => 'no_list'
                }
            );
            return undef;
        }

        my $file = $in{'file'};

        ## Look for the template
        if ($file eq 'list_aliases.tt2') {
            $param->{'filepath'} =
                Sympa::search_fullpath($list || $robot, $file);
        } else {
            #my $lang = Conf::get_robot_conf($robot, 'lang');
            #$file =~ s/\.tpl$/\.$lang\.tpl/;

            $param->{'filepath'} = Sympa::search_fullpath($list || $robot,
                $file, subdir => $subdir);
        }
    }

    if (-f $param->{'filepath'} && (!-r $param->{'filepath'})) {
        Sympa::Report::reject_report_web('intern', 'cannot_read',
            {'filepath' => $param->{'filepath'}},
            $param->{'action'}, '', $param->{'user'}{'email'}, $robot);
        wwslog('err', 'Cannot read %s', $param->{'filepath'});
        web_db_log(
            {   'parameters' => $in{'file'},
                'status'     => 'error',
                'error_type' => 'internal'
            }
        );
        return undef;
    }
    web_db_log(
        {   'parameters' => $in{'file'},
            'status'     => 'success'
        }
    );

    #FIXME: Required?
    $allow_absolute_path = 1;

    return 1;
}

##############################################################################

## Saving of list files
sub do_savefile {
    wwslog('info', '(%s)', $in{'file'});

    $param->{'subtitle'} = sprintf $param->{'subtitle'}, $in{'file'};

    if ($param->{'list'}) {
        unless ($list->is_admin('owner', $param->{'user'}{'email'})
            or Sympa::is_listmaster($list, $param->{'user'}->{'email'})) {
            Sympa::Report::reject_report_web('auth', 'action_owner', {},
                $param->{'action'}, $list);
            wwslog('err', 'Not allowed');
            web_db_log(
                {   'parameters' => $in{'file'},
                    'status'     => 'error',
                    'error_type' => 'authorization'
                }
            );
            return undef;
        }

        if ($in{'file'} =~ /\.tt2$/) {
            $param->{'filepath'} =
                $list->{'dir'} . '/mail_tt2/' . $in{'file'};
        } else {
            $param->{'filepath'} = $list->{'dir'} . '/' . $in{'file'};

            if (defined $list->{'admin'}{'family_name'}) {
                unless ($list->update_config_changes('file', $in{'file'})) {
                    Sympa::Report::reject_report_web('intern',
                        'update_config_changes', {}, $param->{'action'},
                        $list, $param->{'user'}{'email'}, $robot);
                    wwslog('info',
                        'Cannot write in config_changes for file %s',
                        $param->{'filepath'});
                    web_db_log(
                        {   'parameters' => $in{'file'},
                            'status'     => 'error',
                            'error_type' => 'internal'
                        }
                    );
                    return undef;
                }
            }

        }
    } else {
        unless (Sympa::is_listmaster($robot, $param->{'user'}{'email'})) {
            Sympa::Report::reject_report_web('user', 'missing_arg',
                {'argument' => 'list'},
                $param->{'action'});
            wwslog('err', 'No list');
            web_db_log(
                {   'parameters' => $in{'file'},
                    'status'     => 'error',
                    'error_type' => 'no_list'
                }
            );
            return undef;
        }

        if ($robot ne $Conf::Conf{'domain'}) {
            if ($in{'file'} eq 'list_aliases.tt2') {
                $param->{'filepath'} =
                    "$Conf::Conf{'etc'}/$robot/$in{'file'}";
            } else {
                $param->{'filepath'} =
                    "$Conf::Conf{'etc'}/$robot/mail_tt2/$in{'file'}";
            }
        } else {
            if ($in{'file'} eq 'list_aliases.tt2') {
                $param->{'filepath'} = "$Conf::Conf{'etc'}/$in{'file'}";
            } else {
                $param->{'filepath'} =
                    "$Conf::Conf{'etc'}/mail_tt2/$in{'file'}";
            }
        }
    }

    unless ((!-e $param->{'filepath'}) or (-w $param->{'filepath'})) {
        Sympa::Report::reject_report_web('intern', 'cannot_write',
            {'filepath' => $param->{'filepath'}},
            $param->{'action'}, $list, $param->{'user'}{'email'}, $robot);
        wwslog('err', 'Cannot write %s', $param->{'filepath'});
        web_db_log(
            {   'parameters' => $in{'file'},
                'status'     => 'error',
                'error_type' => 'internal'
            }
        );
        return undef;
    }

    ## Keep the old file
    if (-e $param->{'filepath'}) {
        rename($param->{'filepath'}, "$param->{'filepath'}.orig");
    }

    ## Not empty
    if ($in{'content'} && ($in{'content'} !~ /^\s*$/)) {

        ## Remove DOS linefeeds (^M) that cause problems with Outlook 98, AOL,
        ## and EIMS:
        $in{'content'} =~ s/\r\n|\r/\n/g;

        ## Create directory if required
        my $dir = $param->{'filepath'};
        $dir =~ s/\/[^\/]+$//;
        unless (-d $dir) {
            unless (mkdir $dir, 0777) {
                Sympa::Report::reject_report_web('intern', 'cannot_mkdir',
                    {'dir' => $dir},
                    $param->{'action'}, $list, $param->{'user'}{'email'},
                    $robot);
                wwslog('err', 'Failed to create directory %s: %s',
                    $dir, $ERRNO);
                web_db_log(
                    {   'parameters' => $in{'file'},
                        'status'     => 'error',
                        'error_type' => 'internal'
                    }
                );
                return undef;
            }
        }

        ## Save new file
        unless (open FILE, ">", $param->{'filepath'}) {
            Sympa::Report::reject_report_web(
                'intern', 'cannot_open_file',
                {'file' => $param->{'filepath'}}, $param->{'action'},
                $list, $param->{'user'}{'email'},
                $robot
            );
            wwslog('err', 'Failed to save file %s: %s',
                $param->{'filepath'}, $ERRNO);
            web_db_log(
                {   'parameters' => $in{'file'},
                    'status'     => 'error',
                    'error_type' => 'internal'
                }
            );
            return undef;
        }
        my $e = $in{'content'};
        Encode::from_to($e, 'utf8', $Conf::Conf{'filesystem_encoding'});
        print FILE $e;
        close FILE;
    } elsif (-f $param->{'filepath'}) {
        wwslog('info', 'Deleting %s', $param->{'filepath'});
        unlink $param->{'filepath'};
    }
    web_db_log(
        {   'parameters' => $in{'file'},
            'status'     => 'success'
        }
    );

    Sympa::Report::notice_report_web('performed', {}, $param->{'action'});

    #    undef $in{'file'};
    #    undef $param->{'file'};
    my $pa = 'editfile';
    $pa = $in{'previous_action'} if ($in{'previous_action'});
    return $pa;
}

## Access to web archives
sub do_arc {
    wwslog('info', '(%s, %s)', $in{'month'}, $in{'arc_file'});
    my $latest;

    my $index = $session->{'arc_mode'}
        || $Conf::Conf{'archive_default_index'};
    $index = 'thrd' unless $index and $index =~ /^(thrd|mail)$/;

    ## Clean arc_file
    if ($in{'arc_file'} eq '/') {
        delete $in{'arc_file'};
    }

    ## Access control
    unless (defined check_authz('do_arc', 'archive.web_access')) {
        $param->{'previous_action'} = 'arc';
        $param->{'previous_list'}   = $list->{'name'};
        return undef;
    }

    # Check authorization for tracking.
    my $result = Sympa::Scenario::request_action(
        $list,
        'tracking.tracking',
        $param->{'auth_method'},
        {   'sender'      => $param->{'user'}{'email'},
            'remote_host' => $param->{'remote_host'},
            'remote_addr' => $param->{'remote_addr'}
        }
    );
    my $r_action;
    if (ref($result) eq 'HASH') {
        $r_action = $result->{'action'};
    }

    if ($r_action =~ /do_it/i) {
        $param->{'may_tracking'} = 1;
    } else {
        $param->{'may_tracking'} = 0;
    }

    $session->{'archive_sniffer'} = 'false'
        if ($param->{'user'}{'email'} or $in{'not_a_sniffer'});

    if ($list->{'admin'}{'web_archive_spam_protection'} eq 'cookie') {
        return 'arc_protect'
            unless ($session->{'archive_sniffer'} eq 'false');
    }

    my $archive = Sympa::Archive->new(context => $list);
    # Calendar
    my @arcs = $archive->get_archives;
    unless (@arcs) {
        Sympa::Report::reject_report_web('user', 'empty_archives', {},
            $param->{'action'}, $list);
        wwslog('err', 'Empty archive %s', $archive);
        return undef;
    }
    foreach my $arc (@arcs) {
        my $info;
        if (    $info = $archive->select_archive($arc, count => 1)
            and $info->{count}) {
            my ($yyyy, $mm) = split /-/, $arc;
            $param->{'calendar'}{$yyyy}{$mm} = $info->{count};
            $latest = $arc;
        }
    }

    # Given partial URI, redirect to base.
    unless ($in{'month'}) {
        $param->{'redirect_to'} = Sympa::get_url(
            $list, 'arc',
            nomenu    => $param->{'nomenu'},
            paths     => [$latest, ''],        # Ends with '/'.
            authority => 'local'
        );
        return 1;
    }
    unless ($in{'arc_file'} or $ENV{REQUEST_URI} =~ /\/\z/) {
        $param->{'redirect_to'} = Sympa::get_url(
            $list, 'arc',
            nomenu    => $param->{'nomenu'},
            paths     => [$in{'month'}, ''],    # Ends with '/'.
            authority => 'local'
        );
        return 1;
    }

    # Read HTML file
    unless ($archive->select_archive($in{'month'})) {
        wwslog('err', 'Unable to find month "%s" in %s',
            $in{'month'}, $archive);
        Sympa::Report::reject_report_web(
            'user',
            'month_not_found',
            {   'month'    => $in{'month'},
                'listname' => $param->{'list'}
            },
            $param->{'action'},
            $list,
            $param->{'user'}{'email'},
            $robot
        );

        $archive->select_archive($latest);
    }

    # File exists?
    my $html_metadata;
    unless ($in{'arc_file'}) {
        while ($html_metadata = $archive->html_next(reverse => 1)) {
            next unless %$html_metadata;
            next unless $html_metadata->{filename} =~ /\A$index(\d+)\.html\z/;
            last;
        }
        $in{'arc_file'} = $html_metadata->{filename} if $html_metadata;
    } else {
        $html_metadata = $archive->html_fetch(file => $in{'arc_file'});
    }
    unless ($html_metadata) {
        wwslog('err', 'Unable to read HTML message <%s>', $in{'arc_file'});
        Sympa::Report::reject_report_web(
            'user',
            'arc_not_found',    #FIXME: Not implemented.
            {   'arc_file' => $in{'arc_file'},
                'month'    => $in{'month'},
                'listname' => $param->{'list'}
            },
            $param->{'action'},
            $list,
            $param->{'user'}{'email'},
            $robot
        );
        return undef;
    }

    ## File type
    if ($in{'arc_file'} =~ /^(mail\d+|msg\d+|thrd\d+)\.html$/) {
        if ($in{'arc_file'} =~ /^(thrd|mail)\d+\.html/) {
            $session->{'arc_mode'} = $1;
        }
        if ($param->{'user'}{'email'}) {
            if ($param->{'user'}{'prefs'}{'arc_mode'} ne
                $session->{'arc_mode'}) {
                # update user pref  as soon as connected user change the way
                # he consult archives
                $param->{'user'}{'prefs'}{'arc_mode'} =
                    $session->{'arc_mode'};
                Sympa::User::update_global_user(
                    $param->{'user'}{'email'},
                    {   data => Sympa::Tools::Data::hash_2_string(
                            $param->{'user'}{'prefs'}
                        )
                    }
                );
            }
        }

        if ($in{'arc_file'} =~ /^(msg\d+)\.html$/) {
            # If the file is a message, load the metadata to find out who is
            # the author of the message.
            $param->{'include_picture'} =
                $list->find_picture_url($html_metadata->{'X-From'});
            $param->{'subtitle'} = $html_metadata->{'X-Subject'};
        }

        # Provide a file content to the TT2 parser (instead of a filename
        # previously).
        $param->{'html_content'} = $html_metadata->{html_content};

        #FIXME: Is this required?
        push @other_include_path, $archive->{arc_directory};
    } else {
        if ($in{'arc_file'} =~ /\.(\w+)$/) {
            $param->{'file_extension'} = $1;
        }

        $param->{'bypass'} = 1;
        $param->{'file'} = $archive->{arc_directory} . '/' . $in{'arc_file'};
    }

    $param->{'date'} = Sympa::Tools::File::get_mtime(
        $archive->{arc_directory} . '/' . $in{'arc_file'});
    # send page as static if client is a bot. That's prevent crawling all
    # archices every weeks by google, yahoo and others bots
    if ($session->{'is_a_crawler'}) {
        $param->{'header_date'} = $param->{'date'};
    }
    $param->{'archive_name'} = $in{'month'};

    #test pour différentier les action d'un robot et d'un simple abonné

    web_db_stat_log();

    return 1;
}

## Access to latest web archives
sub do_latest_arc {
    wwslog('info', '(%s, %s, %s)', $in{'list'}, $in{'for'}, $in{'count'});

    ## Access control
    return undef
        unless (defined check_authz('do_latest_arc', 'archive.web_access'));

    ## parameters of the query
    my $today = time;

    my $oldest_day;
    if (defined $in{'for'}) {
        $oldest_day = $today - (86400 * ($in{'for'}));
        $param->{'for'} = $in{'for'};
        unless ($oldest_day >= 0) {
            Sympa::Report::reject_report_web('user', 'nb_days_to_much',
                {'nb_days' => $in{'for'}},
                $param->{'action'}, $list);
            wwslog('err', 'Parameter "for" is too big"');
        }
    }

    my $nb_arc;
    my $NB_ARC_MAX = 100;
    if (defined $in{'count'}) {
        if ($in{'count'} > $NB_ARC_MAX) {
            $in{'count'} = $NB_ARC_MAX;
        }
        $param->{'count'} = $in{'count'};
        $nb_arc = $in{'count'};
    } else {
        $nb_arc = $NB_ARC_MAX;
    }

    my $archive = Sympa::Archive->new(context => $list);
    my @arcs = reverse $archive->get_archives;
    my $stop_search;
    my @archives;

    # year-month directory
    foreach my $arc (@arcs) {
        if ($nb_arc <= 0) {
            last;
        }

        last if $stop_search;

        unless ($archive->select_archive($arc)) {
            Sympa::Report::reject_report_web(
                'intern',
                'inaccessible_archive',
                {   'year_month' => $arc,
                    'listname'   => $list->{'name'}
                },
                $param->{'action'},
                $list,
                $param->{'user'}{'email'},
                $robot
            );
            wwslog('err', 'Unable to open directory %s in %s', $arc,
                $archive);
            next;
        }

        # Messages in the year-month directory
        while (1) {
            my ($message, $handle) = $archive->next(reverse => 1);
            last unless $handle;
            next unless $message;

            last if $nb_arc <= 0;

            my %msg_info;

            foreach my $field ('message-id', 'subject', 'from') {

                my $var = $field;
                $var =~ s/-/_/g;

                $msg_info{$var} = $message->get_header($field);

                # Hide full email address
                if ($field eq 'from') {
                    if ($msg_info{$var} =~ /(.+)\<.+\>/) {
                        $msg_info{$var} = $1;
                    } else {
                        my @email = split /\@/, $msg_info{$var};
                        $msg_info{$var} = $email[0];
                    }
                }

                if ($field eq 'message-id') {
                    $msg_info{$var} = Sympa::Tools::Text::canonic_message_id(
                        $msg_info{'message_id'});
                    #FIXME: Required?
                    $msg_info{$var} =
                        Sympa::Tools::Text::escape_chars($msg_info{$var});

                    $msg_info{'year_month'} = $arc;
                } else {
                    $msg_info{$var} =
                        MIME::EncWords::decode_mimewords($msg_info{$var},
                        Charset => 'utf8');
                    #FIXME: Required?
                    $msg_info{$var} = Sympa::Tools::WWW::escape_html_minimum(
                        $msg_info{$var});
                }
            }

            my $date = $message->get_header('Date');

            unless (defined $date) {
                wwslog('err', 'No date found in message %s', $message);
                next;
            }

            $msg_info{'date_smtp'} = $date;
            my $date_epoch = eval {
                DateTime::Format::Mail->new->loose->parse_datetime($date)
                    ->epoch;
            };
            if (defined $date_epoch) {
                $msg_info{'date_epoch'} = $date_epoch;
                $msg_info{'date'} = $language->gettext_strftime("%d %b %Y",
                    localtime $date_epoch);

                if ($date_epoch < $oldest_day) {
                    $stop_search = 1;
                    last;
                }
            }
            foreach my $key (keys %msg_info) {
                chomp($msg_info{$key});
            }

            push @archives, \%msg_info;
            $nb_arc--;
        }
    }

    @{$param->{'archives'}} =
        sort ({$b->{'date_epoch'} <=> $a->{'date_epoch'}} @archives);

    return 1;
}

sub get_timelocal_from_date {
    my ($mday, $mon, $yr, $hr, $min, $sec, $zone) = @_;
    my ($time) = 0;

    $yr -= 1900 if $yr >= 1900;    # if given full 4 digit year
    $yr += 100 if $yr <= 37;       # in case of 2 digit years
    if (($yr < 70) || ($yr > 137)) {
        warn "Warning: Bad year (", $yr + 1900, ") using current\n";
        $yr = (localtime(time))[5];
    }

    $time = Time::Local::timelocal($sec, $min, $hr, $mday, $mon, $yr);
    return $time

}

####################################################
#  do_remove_arc
####################################################
#
#  request by list owner or message sender to remove message from archive
#  Create in the outgoing spool a file containing the message-id of mesage to
#  be removed
#
# IN : list@host yyyy month and a tab of msgid
#
# OUT :  1 | undef
#
####################################################

sub do_remove_arc {
    wwslog('info', 'List %s, yyyy %s, mm %s, #message %s',
        $in{'list'}, $in{'yyyy'}, $in{'month'});

    # $in{'msgid'} = Sympa::Tools::Text::unescape_chars($in{'msgid'});
    my @msgids = split /\0/, $in{'msgid'};

    unless (@msgids) {
        Sympa::Report::reject_report_web('user', 'may_not_remove_arc', {},
            $param->{'action'}, $list, $param->{'user'}{'email'}, $robot);
        wwslog('err', 'No message id found');
        web_db_log(
            {   'parameters' => $in{'msgid'},
                'msg_id'     => $in{'msgid'},
                'status'     => 'error',
                'error_type' => 'no_msgid'
            }
        );
        $param->{'status'} = 'no_msgid';
        return undef;
    }

    my $msg_string = "\n\n";
    my $tracking = Sympa::Tracking->new(context => $list);
    foreach my $msgid (@msgids) {
        chomp $msgid;
        $msg_string .= sprintf "remove_arc %s %s-%s %s\n", $list->{'name'},
            $in{'yyyy'}, $in{'month'}, $msgid;

        #FIXME: Removing tracking should be done by archived.
        $tracking->remove_message_by_id($msgid);
    }
    my $arc_message = Sympa::Message->new(
        $msg_string,
        context => $robot,
        sender  => $param->{'user'}{'email'},
        date    => time
    );
    my $marshalled = Sympa::Spool::Archive->new->store($arc_message);
    unless ($marshalled) {
        Sympa::Report::reject_report_web('intern', 'cannot_store_command',
            {'command' => 'remove'},
            $param->{'action'}, $list, $param->{'user'}{'email'}, $robot);
        wwslog('info',
            'Cannot store command to remove archive %s-%s of list %s',
            $in{'yyyy'}, $in{'month'}, $list);
        web_db_log(
            {   'parameters' => $in{'msgid'},
                'msg_id'     => $in{'msgid'},
                'status'     => 'error',
                'error_type' => 'internal'
            }
        );
        return undef;
    }

    wwslog(
        'info',
        '%d messages marked to be removed by archived',
        scalar @msgids
    );
    web_db_log(
        {   'parameters' => $in{'msgid'},
            'msg_id'     => $in{'msgid'},
            'status'     => 'success'
        }
    );

    #web_db_stat_log();

    $param->{'status'} = 'done';

    return 1;
}

####################################################
#  do_send_me
####################################################
#  Sends a web archive message to a
#  requesting user
#
# IN : -
#
# OUT : 'arc' | 1 | undef
#
####################################################
sub do_send_me {
    wwslog('info', '(%s, %s, %s, %s)',
        $in{'list'}, $in{'yyyy'}, $in{'month'}, $in{'msgid'});

    my $message_id = Sympa::Tools::Text::canonic_message_id($in{'msgid'});
    unless ($message_id
        and $message_id !~ /NO-ID-FOUND\.mhonarc\.org/) {
        Sympa::Report::reject_report_web('intern', 'may_not_send_me', {},
            $param->{'action'}, $list, $param->{'user'}{'email'}, $robot);
        wwslog('info', 'No message id found');
        $param->{'status'} = 'no_msgid';
        return undef;
    }

    my $spindle = Sympa::Spindle::ResendArchive->new(
        resent_by  => $param->{'user'}{'email'},
        context    => $list,
        arc        => "$in{'yyyy'}-$in{'month'}",
        message_id => $message_id,
        quiet      => 1
    );

    unless ($spindle and $spindle->spin) {
        wwslog('info', 'No file match msgid');
        $param->{'status'} = 'not_found';
        return undef;
    } elsif ($spindle->{finish} and $spindle->{finish} eq 'success') {
        wwslog(
            'info',      'Message %s spooled for %s',
            $message_id, $param->{'user'}{'email'}
        );
        Sympa::Report::notice_report_web('performed', {}, $param->{'action'});
        $in{'month'} = $in{'yyyy'} . "-" . $in{'month'};
        return 'arc';
    } else {
        $param->{'status'} = 'message_err';
        wwslog(
            'err',
            'Impossible to send archive file to %s',
            $param->{'user'}{'email'}
        );
        return undef;
    }

    return 1;
}

####################################################
#  do_view_source
####################################################
#  Display message as text/plain in archives
#
# IN : -
#
# OUT : 'arc' | 1 | undef
#
####################################################
sub do_view_source {
    wwslog('info', '(%s, %s, %s, %s)',
        $in{'list'}, $in{'yyyy'}, $in{'month'}, $in{'msgid'});

    ## Access control
    unless (defined check_authz('do_arc', 'archive.web_access')) {
        $param->{'previous_action'} = 'arc';
        $param->{'previous_list'}   = $list->{'name'};
        return undef;
    }

    unless ($in{'msgid'}
        and $in{'msgid'} !~ /NO-ID-FOUND\.mhonarc\.org/) {
        Sympa::Report::reject_report_web('intern', 'may_not_view_source', {},
            $param->{'action'}, $list, $param->{'user'}{'email'}, $robot);
        wwslog('info', 'No message id found');
        $param->{'status'} = 'no_msgid';
        return undef;
    }

    my $archive = Sympa::Archive->new(context => $list);
    my ($message, $handle);
    if ($archive->select_archive("$in{'yyyy'}-$in{'month'}")) {
        ($message, $handle) = $archive->fetch(message_id => $in{'msgid'});
    }
    if ($message) {
        $param->{'bypass'} = 'extreme';
        print "Content-Type: text/plain\n\n";
        print $message->as_string;
    } else {
        wwslog('info', 'No file match msgid');
        $param->{'status'} = 'not_found';
        return undef;
    }

    return 1;
}

####################################################
#  do_tracking
####################################################
#  Display notifications status when a recipient is not usually delivered
#
# IN : -
#
# OUT : 'arc' | 1 | undef
#
####################################################
sub do_tracking {
    wwslog('info', '(%s, %s, %s, %s)',
        $in{'list'}, $in{'yyyy'}, $in{'month'}, $in{'msgid'});

    if (    $in{'yyyy'}
        and $in{'yyyy'} =~ /\A\d\d\d\d\z/
        and $in{'month'}
        and $in{'month'} =~ /\A\d\d?\z/) {
        $param->{'archive_name'} = sprintf '%d-%02d', $in{'yyyy'},
            $in{'month'};
    }

    ## Access control
    my $result = Sympa::Scenario::request_action(
        $list,
        'tracking.tracking',
        $param->{'auth_method'},
        {   'sender'      => $param->{'user'}{'email'},
            'remote_host' => $param->{'remote_host'},
            'remote_addr' => $param->{'remote_addr'}
        }
    );
    my $r_action;
    my $reason;
    if (ref($result) eq 'HASH') {
        $r_action = $result->{'action'};
        $reason   = $result->{'reason'};
    }

    unless ($r_action =~ /do_it/i) {
        $param->{'previous_action'} = 'arc';
        $param->{'previous_list'}   = $list->{'name'};
        Sympa::Report::reject_report_web('auth', $reason, {},
            $param->{'action'}, $list);
        wwslog('info', 'Access denied for %s', $param->{'user'}{'email'});
        return undef;
    }

    # is tracking configured for this list ?
    unless (
        ($list->{admin}{tracking}{delivery_status_notification} eq 'on')
        || ($list->{admin}{tracking}{message_disposition_notification} eq
            'on')
        || ($list->{admin}{tracking}{message_disposition_notification} eq
            'on_demand')
        ) {
        wwslog('err', 'List not configured for tracking');
        Sympa::Report::reject_report_web('intern',
            'list_not_configured_for_tracking');
        $param->{'previous_action'} = 'arc';
        $param->{'previous_list'}   = $list->{'name'};
        return undef;
    }
    if (  !$in{'msgid'}
        || $in{'msgid'} =~ /NO-ID-FOUND\.mhonarc\.org/) {
        Sympa::Report::reject_report_web('user', 'no_msgid', {},
            $param->{'action'}, $list, $param->{'user'}{'email'}, $robot);
        wwslog('err', 'No message id found');
        $param->{'status'} = 'no_msgid';
        return undef;
    }
    ##

    $param->{'subject'}  = $in{'subject'};
    $param->{'fromname'} = $in{'fromname'};
    $param->{'fromaddr'} = $in{'fromaddr'};
    $param->{'msgid'}    = $in{'msgid'};
    $param->{'listname'} = $in{'list'};

    my $tracking_info =
        Sympa::Tracking::get_recipients_status($in{'msgid'}, $in{'list'},
        $robot);
    unless ($tracking_info) {
        Sympa::Report::reject_report_web('user',
            'could_not_get_tracking_info', {}, $param->{'action'}, $list,
            $param->{'user'}{'email'}, $robot);
        wwslog('err',
            "could not get tracking info for message_id $in{'msgid'} and list $in{'list'}"
        );
        delete $param->{'tracking_info'};
        $param->{'status'} = 'could_not_get_tracking_info';
        return undef;
    }

    # Arrival-Date would be reformatted as local time and current language.
    foreach my $info (@$tracking_info) {
        $info->{'arrival_date'} =
            $language->gettext_strftime('%d %b %Y at %H:%M:%S',
            localtime $info->{'arrival_epoch'})
            if defined $info->{'arrival_epoch'};
    }

    $param->{'tracking_info'} = $tracking_info;
    return 1;
}

## Output an initial form to search in web archives
sub do_arcsearch_form {
    wwslog('info', '(%s)', $param->{'list'});

    ## Access control
    return undef
        unless (
        defined check_authz('do_arcsearch_form', 'archive.web_access'));

    my $archive = Sympa::Archive->new(context => $list);
    $param->{'yyyymm'}       = [reverse $archive->get_archives];
    $param->{'key_word'}     = $in{'key_word'};
    $param->{'archive_name'} = $in{'archive_name'};

    return 1;
}

## Search in web archives
sub do_arcsearch {
    wwslog('info', '(%s)', $param->{'list'});

    # Access control
    return undef
        unless defined check_authz('do_arcsearch', 'archive.web_access');

    my $search = Sympa::Marc::Search->new;
    my $archive = Sympa::Archive->new(context => $list);
    $search->search_base($archive->{base_directory});
    $search->base_href(Sympa::get_url($list, 'arc'));
    $search->archive_name($in{'archive_name'});

    unless (defined($in{'directories'})) {
        # by default search in current month and in the previous non-empty one
        my $archive_name = $in{'archive_name'} || '';
        $archive_name = POSIX::strftime('%Y-%m', localtime time)
            unless $archive_name =~ /^\d{4}-\d{2}$/;
        my @directories = ();
        foreach my $arc (reverse $archive->get_archives) {
            if ($archive_name) {
                push @directories, $arc if $arc le $archive_name;
                $archive_name = '' if $arc lt $archive_name;
            }
            push @{$param->{'yyyymm'}}, $arc;
        }
        $in{'directories'} = join "\0", @directories;
    }

    if (defined($in{'directories'})) {
        $search->directories($in{'directories'});
        foreach my $dir (split /\0/, $in{'directories'}) {
            push @{$param->{'directories'}}, $dir;
        }
    }

    if (defined $in{'previous'}) {
        $search->body_count($in{'body_count'});
        $search->date_count($in{'date_count'});
        $search->from_count($in{'from_count'});
        $search->subj_count($in{'subj_count'});
        $search->previous($in{'previous'});
    }

    ## User didn't enter any search terms
    if ($in{'key_word'} =~ /^\s*$/) {
        Sympa::Report::reject_report_web('user', 'missing_arg',
            {'argument' => 'key_word'},
            $param->{'action'});
        wwslog('info', 'No search term');
        return undef;
    }

    $param->{'key_word'} = $in{'key_word'};

    $search->limit($in{'limit'});

    $search->age(1)
        if ($in{'age'} eq 'new');

    $search->match(1)
        if (($in{'match'} eq 'partial') or ($in{'match'} eq '1'));

    $search->clean_words($in{'key_word'});
    my @clean_words = split(/\s+/, $in{'key_word'});
    my @words = @clean_words;
    foreach my $w (@words) {
        $w =~ s/([^\x00-\x1F\s\w\x7F-\xFF])/\\$1/g;    # Escape non-words.
        $w = '\b' . $w . '\b'
            if $in{'match'} eq 'exact';
    }
    $search->words(\@words);

    $search->key_word(join('|', @words));

    if ($in{'case'} eq 'off') {
        $search->case(1);
        $search->key_word('(?i)' . $search->key_word);
    }
    if ($in{'how'} eq 'any') {
        $search->function2($search->match_any(@words));
        $search->how('any');
    } elsif ($in{'how'} eq 'all') {
        $search->function1($search->body_match_all(@clean_words, @words));
        $search->function2($search->match_all(@words));
        $search->how('all');
    } else {
        $search->function2($search->match_this(@words));
        $search->how('phrase');
    }

    $search->subj(defined($in{'subj'}));
    $search->from(defined($in{'from'}));
    $search->date(defined($in{'date'}));
    $search->body(defined($in{'body'}));

    $search->body(1)
        if (not($search->subj)
        and not($search->from)
        and not($search->body)
        and not($search->date));

    my $searched = $search->search;

    if (defined($search->error)) {
        wwslog('info', '%s', $search->error);
    }

    $search->searched($searched);

    if ($searched < $search->file_count) {
        $param->{'continue'} = 1;
    }

    foreach my $field (
        'list',  'archive_name', 'age',  'body',
        'case',  'date',         'from', 'how',
        'limit', 'match',        'subj'
        ) {
        $param->{$field} = $in{$field};
    }

    $param->{'body_count'}  = $search->body_count;
    $param->{'clean_words'} = $search->clean_words;
    $param->{'date_count'}  = $search->date_count;
    $param->{'from_count'}  = $search->from_count;
    $param->{'subj_count'}  = $search->subj_count;

    $param->{'num'}      = $search->file_count + 1;
    $param->{'searched'} = $search->searched;

    $param->{'res'} = $search->res;

    return 1;
}

## Search message-id in web archives
sub do_arcsearch_id {
    wwslog('info', '(%s, %s, %s)', $param->{'list'}, $in{'archive_name'},
        $in{'msgid'});

    # Access control
    return undef
        unless defined check_authz('do_arcsearch_id', 'archive.web_access');

    my $search = Sympa::Marc::Search->new;
    my $archive = Sympa::Archive->new(context => $list);
    $search->search_base($archive->{base_directory});
    $search->base_href(Sympa::get_url($list, 'arc'));

    $search->archive_name($in{'archive_name'});

    # search in current month and in the previous none empty one
    my $search_base = $search->search_base;
    my $previous_active_dir;
    foreach my $arc (reverse $archive->get_archives) {
        if ($arc =~ /^(\d{4})-(\d{2})$/ and $arc lt $search->archive_name) {
            $previous_active_dir = $arc;
            last;
        }
    }
    $in{'archive_name'} = $search->archive_name . "\0" . $previous_active_dir;

    $search->directories($in{'archive_name'});
    #    $search->directories ($search->archive_name);

    ## User didn't enter any search terms
    if ($in{'msgid'} =~ /^\s*$/) {
        Sympa::Report::reject_report_web('user', 'missing_arg',
            {'argument' => 'msgid'},
            $param->{'action'});
        wwslog('info', 'No search term');
        return undef;
    }

    $in{'msgid'} = Sympa::Tools::Text::unescape_chars($in{'msgid'});
    $param->{'msgid'} = $in{'msgid'};

    $search->limit(1);

    $search->clean_words($in{'msgid'});
    my @words = split(/\s+/, $in{'msgid'});
    foreach my $w (@words) {
        $w =~ s/([^\x00-\x1F\s\w\x7F-\xFF])/\\$1/g;    # Escape non-words.
    }
    $search->words(\@words);

    $search->key_word(join('|', @words));

    $search->function2($search->match_this(@words));

    $search->id(1);

    my $searched = $search->search;

    if (defined($search->error)) {
        wwslog('info', '%s', $search->error);
    }

    $search->searched($searched);

    $param->{'res'} = $search->res;

    unless ($#{$param->{'res'}} >= 0) {
        Sympa::Report::reject_report_web('intern_quiet', 'archive_not_found',
            {'msgid' => $in{'msgid'}},
            $param->{'action'}, $list, $param->{'user'}{'email'}, $robot);
        wwslog('err', 'No message found in archives matching message ID %s',
            $in{'msgid'});
        return 'arc';
    }

    $param->{'redirect_to'} = $param->{'res'}[0]{'file'};

    return 1;
}

# get pendings lists
sub do_get_pending_lists {

    wwslog('info', '');

    ## Checking families and other virtual hosts.
    get_server_details();

    my $all_lists =
        Sympa::List::get_lists($robot, 'filter' => ['status' => 'pending']);
    foreach my $list (@$all_lists) {
        $param->{'pending'}{$list->{'name'}}{'subject'} =
            $list->{'admin'}{'subject'};
        $param->{'pending'}{$list->{'name'}}{'by'} =
            $list->{'admin'}{'creation'}{'email'};
        $param->{'pending'}{$list->{'name'}}{'date'} =
            $language->gettext_strftime("%d %b %y  %H:%M",
            localtime($list->{'admin'}{'creation'}{'date_epoch'}));
    }

    return 1;
}

# get closed lists
sub do_get_closed_lists {

    wwslog('info', '');

    ## Checking families and other virtual hosts.
    get_server_details();

    my $all_lists =
        Sympa::List::get_lists($robot,
        'filter' => ['status' => 'closed|family_closed']);
    foreach my $list (@$all_lists) {
        $param->{'closed'}{$list->{'name'}}{'subject'} =
            $list->{'admin'}{'subject'};
        $param->{'closed'}{$list->{'name'}}{'by'} =
            $list->{'admin'}{'creation'}{'email'};
    }

    return 1;
}

# get ordered latest lists
sub do_get_latest_lists {

    wwslog('info', '');

    ## Checking families and other virtual hosts.
    get_server_details();

    my @unordered_lists;
    my $all_lists = Sympa::List::get_lists($robot);
    foreach my $list (@$all_lists) {

        push @unordered_lists,
            {
            'name'          => $list->{'name'},
            'subject'       => $list->{'admin'}{'subject'},
            'creation_date' => $list->{'admin'}{'creation'}{'date_epoch'}
            };
    }

    foreach my $l (sort { $b->{'creation_date'} <=> $a->{'creation_date'} }
        @unordered_lists) {
        push @{$param->{'latest_lists'}}, $l;
        $l->{'creation_date'} =
            $language->gettext_strftime("%d %b %Y",
            localtime($l->{'creation_date'}));
    }

    return 1;
}

# get inactive lists
sub do_get_inactive_lists {
    wwslog('info', '');

    ## Checking families and other virtual hosts.
    get_server_details();

    my @unordered_lists;
    my $all_lists =
        Sympa::List::get_lists($robot,
        filter => ['! status' => 'closed|family_closed']);
    foreach my $list (@$all_lists) {
        my $last_message = 0;

        if (open COUNT, $list->{'dir'} . '/msg_count') {
            while (<COUNT>) {
                $last_message = $1 if (/^(\d+)\s/ && ($1 > $last_message));
            }
            close COUNT;

        } else {
            wwslog(
                'info',
                'Could not open file %s',
                $list->{'dir'} . '/msg_count'
            );
        }

        push @unordered_lists,
            {
            'name'          => $list->{'name'},
            'creator'       => $list->{'admin'}{'creation'}{'email'},
            'send_scenario' => $list->{'admin'}{'send'}{'name'},
            'owners'        => join(
                ", ", map { $_->{'email'} } @{$list->{'admin'}{'owner'}}
            ),
            'editors' => join(", ",
                map { $_->{'email'} } @{$list->{'admin'}{'editor'}}),
            'subscribers_count'  => $list->get_total('nocache'),
            'subject'            => $list->{'admin'}{'subject'},
            'msg_count'          => $list->get_msg_count(),
            'last_message_epoch' => $last_message,
            'last_message_date'  => $language->gettext_strftime(
                "%d %b %Y", localtime($last_message * 86400)
            ),
            'creation_date_epoch' =>
                $list->{'admin'}{'creation'}{'date_epoch'},
            'creation_date' => $language->gettext_strftime(
                "%d %b %Y",
                localtime($list->{'admin'}{'creation'}{'date_epoch'})
            ),
            };
    }

    foreach my $l (
        sort { $a->{'last_message_epoch'} <=> $b->{'last_message_epoch'} }
        @unordered_lists) {
        push @{$param->{'inactive_lists'}}, $l;
    }

    return 1;
}

# get ordered biggest lists
sub do_get_biggest_lists {
    wwslog('info', '');

    ## Checking families and other virtual hosts.
    get_server_details();

    my @unordered_lists;
    my $all_lists = Sympa::List::get_lists($robot);
    foreach my $list (@$all_lists) {
        push @unordered_lists,
            {
            'name'          => $list->{'name'},
            'subject'       => $list->{'admin'}{'subject'},
            'creation_date' => $list->{'admin'}{'creation'}{'date_epoch'},
            'subscribers'   => $list->get_total
            };
    }

    foreach my $l (sort { $b->{'subscribers'} <=> $a->{'subscribers'} }
        @unordered_lists) {
        $l->{'creation_date'} =
            $language->gettext_strftime("%d %b %Y",
            localtime($l->{'creation_date'}));
        push @{$param->{'biggest_lists'}}, $l;
    }

## Not yet implemented.
##	my $all_lists = Sympa::List::get_lists($robot, 'order' => ['-total']);
##	$param->{'biggest_lists'} = [
##	    map { {
##		'name' => $_->{'name'},
##		'subject' => $_->{'admin'}{'subject'},
##		'creation_date' =>
##                  $language->gettext_strftime("%d %b %Y",
##                  localtime $_->creation->{'date_epoch'}),
##		'subscribers' => $_->total
##	    }; } @{$all_lists || []}
##	];

    return 1;
}

## show a list parameters
sub do_set_pending_list_request {
    wwslog('info', '(%s)', $in{'list'});

    my $list_dir = $list->{'dir'};

    $param->{'list_config'} = $list_dir . '/config';
    if (-f $list_dir . '/info') {
        $param->{'list_info_file_exists'} = 1;
    }
    $param->{'list_info'}         = $list_dir . '/info';
    $param->{'list_subject'}      = $list->{'admin'}{'subject'};
    $param->{'list_request_by'}   = $list->{'admin'}{'creation'}{'email'};
    $param->{'list_request_date'} = $list->{'admin'}{'creation'}{'date'};
    $param->{'list_serial'}       = $list->{'admin'}{'serial'};
    $param->{'list_status'}       = $list->{'admin'}{'status'};

    if (open my $fh, '<', $list_dir . '/config') {
        $param->{'list_config_content'} = do { local $RS; <$fh> };
        close $fh;
    }
    if (open my $fh, '<', $list_dir . '/info') {
        $param->{'list_info_content'} = do { local $RS; <$fh> };
        close $fh;
    }

    return 1;
}

## show a list parameters
sub do_install_pending_list {
    wwslog('info', '(%s, %s, %s)', $in{'list'}, $in{'status'}, $in{'notify'});

    unless ($in{'status'}
        && (($in{'status'} eq 'open') || ($in{'status'} eq 'closed'))) {
        Sympa::Report::reject_report_web('user', 'missing_arg',
            {'argument' => 'status'},
            $param->{'action'});
        wwslog('info', 'Missing status parameter',);
        web_db_log(
            {   'parameters' => "$in{'status'},$in{'notify'}",
                'status'     => 'error',
                'error_type' => 'missing_parameter'
            }
        );
        return undef;
    }

    if ($list->{'admin'}{'status'} eq $in{'status'}) {
        Sympa::Report::reject_report_web('user', 'didnt_change_anything', {},
            $param->{'action'});
        wwslog('info', 'Didn\'t change really the status, nothing to do');
        web_db_log(
            {   'parameters' => "$in{'status'},$in{'notify'}",
                'status'     => 'error',
                'error_type' => 'didnt_change_anything'
            }
        );
        return undef;
    }

    $list->{'admin'}{'status'} = $in{'status'};

    #    open TMP, ">/tmp/dump1";
    #    Sympa::Tools::Data::dump_var ($list->{'admin'}, 0, \*TMP);
    #    close TMP;

    unless ($list->save_config($param->{'user'}{'email'})) {
        Sympa::Report::reject_report_web('intern', 'cannot_save_config',
            {'listname' => $list->{'name'}},
            $param->{'action'}, $list, $param->{'user'}{'email'}, $robot);
        wwslog('info', 'Cannot save config file');
        web_db_log(
            {   'parameters' => "$in{'status'},$in{'notify'}",
                'status'     => 'error',
                'error_type' => 'internal'
            }
        );
        return undef;
    }

    #    open TMP, ">/tmp/dump2";
    #    Sympa::Tools::Data::dump_var ($list->{'admin'}, 0, \*TMP);
    #    close TMP;

    ## create the aliases
    if ($in{'status'} eq 'open') {
        my $aliases = Sympa::Admin::install_aliases($list);
        if ($aliases == 1) {
            $param->{'auto_aliases'} = 1;
        } else {
            Sympa::Report::reject_report_web(
                'intern',
                'failed_to_install_aliases',
                {'listname' => $list->{'name'}},
                $param->{'action'},
                $list,
                $param->{'user'}{'email'},
                $robot
            );
            wwslog('err', 'Failed to install list aliases');
        }

    }

    # Notify listmasters and (optionally) owners
    if ($in{'status'} eq 'open') {
        Sympa::send_notify_to_listmaster($list, 'list_created',
            [$list->{'name'}]);
        $list->send_notify_to_owner('list_created', [$list->{'name'}])
            if $in{'notify'};
    } elsif ($in{'status'} eq 'closed') {
        Sympa::send_notify_to_listmaster($list, 'list_rejected',
            [$list->{'name'}]);
        $list->send_notify_to_owner('list_rejected', [$list->{'name'}])
            if $in{'notify'};
    }

    $param->{'status'} = $in{'status'};

    if ($in{'status'} eq 'closed') {
        web_db_stat_log(operation => 'list_rejected');
    }

    $list = $param->{'list'} = $in{'list'} = undef;
    return 'get_pending_lists';
    web_db_log(
        {   'parameters' => "$in{'status'},$in{'notify'}",
            'status'     => 'success'
        }
    );
    return 1;
}

#=head2 sub do_create_list
#
#Creates a list using a list template
#
#=head3 Arguments
#
#=over
#
#=item * I<None>
#
#=back
#
#=head3 Return
#
#=over
#
#=item * I<1>, if no problem is encountered
#
#=item * I<undef>, if anything goes wrong
#
#=item * I<'loginrequest'> if no user is logged in at the time the function is called.
#
#=back
#
#=cut

# create a list using a list template.
sub do_create_list {
    wwslog(
        'info', '(%s, %s, %s)', $in{'listname'}, $in{'subject'},
        $in{'template'}
    );

    ## Check that all the needed arguments are present.
    ## This is checked here because it requires to return the incomplete form
    ## to the user
    foreach my $arg ('listname', 'subject', 'template', 'info', 'topics') {
        unless ($in{$arg}) {
            Sympa::Report::reject_report_web('user', 'missing_arg',
                {'argument' => $arg},
                $param->{'action'});
            wwslog('info', 'Missing param %s', $arg);
            web_db_log(
                {   'parameters' => $in{'listname'},
                    'list'       => $in{'listname'},
                    'status'     => 'error',
                    'error_type' => 'missing_parameter'
                }
            );
            return 'create_list_request';
        }
    }

    # Check length.
    if (Sympa::Constants::LIST_LEN() < length($in{'listname'})) {
        Sympa::Report::reject_report_web('user', 'wrong_value',
            {'argument' => 'listname'},
            $param->{'action'});
        wwslog('info', 'Too long listname');
        web_db_log(
            {   'parameters' => $in{'listname'},
                'status'     => 'error',
                'error_type' => 'wrong_value',     #FIXME
            }
        );
        return 'create_list_request';
    }

    ## Lowercase listname if required
    if ($in{'listname'} =~ /[A-Z]/) {
        $in{'listname'} = lc($in{'listname'});
        Sympa::Report::notice_report_web('listname_lowercased', {},
            $param->{'action'});
    }

    my $result = Sympa::Scenario::request_action(
        $robot,
        'create_list',
        $param->{'auth_method'},
        {   'sender'             => $param->{'user'}{'email'},
            'candidate_listname' => $in{'listname'},
            'candidate_subject'  => $in{'subject'},
            'candidate_template' => $in{'template'},
            'candidate_info'     => $in{'info'},
            'candidate_topics'   => $in{'topics'},
            'remote_host'        => $param->{'remote_host'},
            'remote_addr'        => $param->{'remote_addr'}
        }
    );
    if (ref($result) eq 'HASH') {
        $param->{'create_action'} = $result->{'action'};
        $param->{'reason'}        = $result->{'reason'};
    }

    wwslog('info', ', get action: %s', $param->{'create_action'});

    ## If the action is forbidden, stop here.
    if ($param->{'create_action'} =~ /reject/) {
        Sympa::Report::reject_report_web('auth', $param->{'reason'}, {},
            $param->{'action'}, $list);
        wwslog('info', 'Not allowed');
        web_db_log(
            {   'parameters' => $in{'listname'},
                'list'       => $in{'listname'},
                'status'     => 'error',
                'error_type' => 'authorization'
            }
        );
        return 'home';

        ## If the action is reserved to listmaster, note that it will have to
        ## be moderated
    } elsif ($param->{'create_action'} =~ /listmaster/i) {
        $param->{'status'} = 'pending';

        ## If the action is plainly authorized, note that it will be executed.
    } elsif ($param->{'create_action'} =~ /do_it/i) {
        $param->{'status'} = 'open';

        ## If the action hasn't an authorization status, stop here.
    } else {
        Sympa::Report::reject_report_web('intern',
            'internal_scenario_error_create_list',
            {}, $param->{'action'}, '', $param->{'user'}{'email'}, $robot);
        wwslog('info', 'Internal error in scenario create_list');
        web_db_log(
            {   'parameters' => $in{'listname'},
                'list'       => $in{'listname'},
                'status'     => 'error',
                'error_type' => 'internal'
            }
        );
        return 'create_list_request';
    }

    ## 'other' topic means no topic
    $in{'topics'} = undef if ($in{'topics'} eq 'other');

    ## Store creation parameters.
    my %owner;
    $owner{'email'} = $param->{'user'}{'email'};
    $owner{'gecos'} = $param->{'user'}{'gecos'};

    my $parameters;
    push @{$parameters->{'owner'}}, \%owner;
    $parameters->{'listname'}       = $in{'listname'};
    $parameters->{'subject'}        = $in{'subject'};
    $parameters->{'creation_email'} = $param->{'user'}{'email'};
    $parameters->{'lang'}           = $param->{'lang'};
    $parameters->{'status'}         = $param->{'status'};
    $parameters->{'topics'}         = $in{'topics'};
    $parameters->{'description'}    = $in{'info'};
    $parameters->{'custom_input'}   = $in{'custom_input'};

    # Create list
    if (my $testlist = Sympa::List->new($in{'listname'}, $robot)) {
        Sympa::Report::reject_report_web('user', 'list_already_exists',
            {'new_listname' => $in{'listname'}},
            $param->{'action'}, '', $param->{'user'}{'email'}, $robot);
        wwslog('info', 'Requested list %s already exist (from %s)',
            $in{'listname'}, $param->{'user'}{'email'});
        web_db_log(
            {   'parameters' => $in{'listname'},
                'list'       => $in{'listname'},
                'status'     => 'error',
                'error_type' => 'user'
            }
        );
        return 'create_list_request';
    }
    my $resul =
        Sympa::Admin::create_list_old($parameters, $in{'template'}, $robot,
        'web', $param->{'user'}{'email'});
    unless (defined $resul) {
        Sympa::Report::reject_report_web('intern', 'create_list', {},
            $param->{'action'}, '', $param->{'user'}{'email'}, $robot);
        wwslog('info', 'Unable to create list %s for %s',
            $in{'listname'}, $param->{'user'}{'email'});
        web_db_log(
            {   'parameters' => $in{'listname'},
                'list'       => $in{'listname'},
                'status'     => 'error',
                'error_type' => 'internal'
            }
        );
        return 'create_list_request';
    }

    ## Create list object
    $in{'list'} = $in{'listname'};
    check_param_in();

    if ($param->{'create_action'} =~ /do_it/i) {
        if ($resul->{'aliases'} == 1) {
            $param->{'auto_aliases'} = 1;
        } else {
            Sympa::Report::reject_report_web(
                'intern',
                'failed_to_install_aliases',
                {'listname' => $in{'listname'}},
                $param->{'action'},
                $list,
                $param->{'user'}{'email'},
                $robot
            );
            wwslog('err', 'Failed to install list aliases');
        }
    }

    ## notify listmaster
    my $list = Sympa::List->new($in{'listname'}, $robot);
    unless (defined $list) {
        wwslog('info', 'Failed to create list object for list "%s"',
            $in{'listname'});
        Sympa::Report::reject_report_web('intern', 'create_list', {},
            $param->{'action'}, '', $param->{'user'}{'email'}, $robot);
        return 'create_list_request';
    }

    if ($param->{'create_action'} =~ /notify/) {
        wwslog('info', 'Notify listmaster');

        Sympa::send_notify_to_listmaster($list, 'request_list_creation',
            {'email' => $param->{'user'}{'email'}});
    }

    web_db_log(
        {   'parameters' => $in{'listname'},
            'list'       => $in{'listname'},
            'status'     => 'success'
        }
    );

    $in{'list'} = $resul->{'list'}{'name'};
    check_param_in();

    $param->{'listname'} = $resul->{'list'}{'name'};
    return 1;
}

#=head2 sub do_create_list_request
#
#Sends back the list creation edition form.
#
#=head3 Arguments
#
#=over
#
#=item * I<None>
#
#=back
#
#=head3 Return
#
#=over
#
#=item * I<1>, if no problem is encountered
#
#=item * I<undef>, if anything goes wrong
#
#=item * I<'loginrequest'> if no user is logged in at the time the function is called.
#
#=back
#
#=cut

## Return the creation form
sub do_create_list_request {
    wwslog('info', '');

    my $result = Sympa::Scenario::request_action(
        $robot,
        'create_list',
        $param->{'auth_method'},
        {   'sender'      => $param->{'user'}{'email'},
            'remote_host' => $param->{'remote_host'},
            'remote_addr' => $param->{'remote_addr'}
        }
    );

    my $r_action;
    my $reason;
    if (ref($result) eq 'HASH') {
        $r_action = $result->{'action'};
        $reason   = $result->{'reason'};
    }

    $param->{'create_action'} = $r_action;
    ## Initialize the form
    ## When returning to the form
    foreach my $p ('listname', 'template', 'subject', 'topics', 'info') {
        $param->{'saved'}{$p} = $in{$p};
    }

    if ($param->{'create_action'} =~ /reject/) {
        Sympa::Report::reject_report_web('auth', $reason, {},
            $param->{'action'}, $list);
        wwslog('info', 'Not allowed');
        return undef;
    }

    # load lists the user is administoring
    #XXX# Slow on the host with large number of lists.
    #XXXif ($param->{'is_listmaster'}) {
    #XXX    $param->{'all_lists'} = Sympa::List::get_lists($robot) || [];
    #XXX} else {
    $param->{'all_lists'} =
        Sympa::List::get_lists($robot,
        filter => ['owner' => $param->{'user'}{'email'}])
        || [];
    #XXX}

    my %topics;
    unless (%topics = Sympa::Robot::load_topics($robot)) {
        Sympa::Report::reject_report_web('intern',
            'unable_to_load_list_of_topics',
            {}, $param->{'action'}, '', $param->{'user'}{'email'}, $robot);
    }
    $param->{'list_of_topics'} = \%topics;

    if ($in{'topics'}) {
        my ($topic, $subtopic) = split('/', $in{'topics'});
        if ($subtopic) {
            $param->{'list_of_topics'}{$topic}{'sub'}{$subtopic}{'selected'} =
                1;
        } else {
            $param->{'list_of_topics'}{$topic}{'selected'} = 1;
        }
    }

    unless ($param->{'list_list_tpl'} =
        Sympa::Tools::WWW::get_list_list_tpl($robot)) {
        Sympa::Report::reject_report_web('intern',
            'unable_to_load_create_list_templates',
            {}, $param->{'action'}, '', $param->{'user'}{'email'}, $robot);
    }

    $param->{'tpl_count'} = scalar keys %{$param->{'list_list_tpl'} || {}};

    $param->{'list_list_tpl'}{$in{'template'}}{'selected'} = 1
        if $in{'template'};

    return 1;

}

## WWSympa Home-Page
sub do_home {
    wwslog('info', '');

    unless ($session->is_anonymous()) {
        my @which_owner =
            Sympa::List::get_which($param->{'user'}{'email'}, $robot,
            'owner');
        my @which_editor = Sympa::List::get_which($param->{'user'}{'email'},
            $robot, 'editor');
        my @which_member = Sympa::List::get_which($param->{'user'}{'email'},
            $robot, 'member');

        ## Build the admin_summary variable that tells foreach list
        ## how many messages/subscriptions are awaiting moderation
        foreach my $one_list (@which_owner, @which_editor) {
            # skip already treated ones
            next
                if $param->{'admin_summary'}{$one_list->{'name'}};

            my $mod_message =
                Sympa::Spool::Moderation->new(context => $one_list)->size;
            $param->{'admin_summary'}{$one_list->{'name'}}{'mod_message'} =
                $mod_message
                if $mod_message;

            my $mod_subscription = Sympa::Spool::Auth->new(
                context => $one_list,
                action  => 'add'
            )->size;
            $param->{'admin_summary'}{$one_list->{'name'}}
                {'mod_subscription'} = $mod_subscription
                if $mod_subscription;
            my $mod_signoff = Sympa::Spool::Auth->new(
                context => $one_list,
                action  => 'del'
            )->size;
            $param->{'admin_summary'}{$one_list->{'name'}}{'mod_signoff'} =
                $mod_signoff
                if $mod_signoff;

            my $doc_mod_list     = $one_list->get_shared_moderated();
            my $mod_shared_total = $#{$doc_mod_list} + 1;
            $param->{'admin_summary'}{$one_list->{'name'}}
                {'mod_shared_total'} = $mod_shared_total
                if $mod_shared_total;

        }

        ## Build the errors_summary variable, tells for a suscriber
        ## how many sending messages are in errors

        foreach my $the_list (@which_member) {
            # review all the lists

            next if $param->{'errors_summary'}{$the_list->{'name'}};

            my $user = $the_list->get_list_member($param->{'user'}{'email'});
            $the_list->parse_list_member_bounce($user);
            # if there is bounces put it in errors_summary
            if ($user->{bounce_count}) {
                $param->{'errors_summary'}{$the_list->{'name'}} = {
                    count => $user->{bounce_count},
                    first => $language->gettext_strftime(
                        "%d %b %Y", localtime($user->{first_bounce})
                    ),
                    last => $language->gettext_strftime(
                        "%d %b %Y", localtime($user->{last_bounce})
                    ),
                    type => $user->{bounce_class},
                };
            }
        }
    }

    return 1;

}

sub do_editsubscriber {
    wwslog('info', '(%s)', $in{'email'});

    my $subscriber;

    $in{'email'} = Sympa::Tools::Text::unescape_chars($in{'email'});

    unless ($subscriber = $list->get_list_member($in{'email'})) {
        Sympa::Report::reject_report_web('intern', 'subscriber_not_found',
            {'email' => $in{'email'}},
            $param->{'action'}, $list, $param->{'user'}{'email'}, $robot);
        wwslog('info', 'Subscriber %s not found', $in{'email'});
        return undef;
    }

    $param->{'current_subscriber'} = $subscriber;
    $param->{'current_subscriber'}{'escaped_email'} =
        Sympa::Tools::WWW::escape_html_minimum(
        $param->{'current_subscriber'}{'email'});
    $param->{'current_subscriber'}{'escaped_bounce_address'} =
        Sympa::Tools::WWW::escape_html_minimum(
        $param->{'current_subscriber'}{'bounce_address'});
    $param->{'current_subscriber'}{'date'} =
        $language->gettext_strftime("%d %b %Y",
        localtime($subscriber->{'date'}));
    $param->{'current_subscriber'}{'update_date'} =
        $language->gettext_strftime("%d %b %Y",
        localtime($subscriber->{'update_date'}));
    $param->{'current_subscriber'}{'pictures_url'} =
        $list->find_picture_url($subscriber->{'email'});

    ## Prefs
    $param->{'current_subscriber'}{'reception'}  ||= 'mail';
    $param->{'current_subscriber'}{'visibility'} ||= 'noconceal';

    ## Get language from user_table
    my $user = Sympa::User::get_global_user($in{'email'});
    $language->push_lang($user->{'lang'});
    $param->{'current_subscriber'}{'lang'} = $language->native_name;
    $language->pop_lang;

    foreach my $m ($list->available_reception_mode) {
        $param->{'reception'}{$m}{'description'} =
            Sympa::ListOpt::get_title($m, 'reception');
        if ($param->{'current_subscriber'}{'reception'} eq $m) {
            $param->{'reception'}{$m}{'selected'} = ' selected';
        } else {
            $param->{'reception'}{$m}{'selected'} = '';
        }
    }

    foreach my $m (qw(conceal noconceal)) {
        $param->{'visibility'}{$m}{'description'} =
            Sympa::ListOpt::get_title($m, 'visibility');
        if ($param->{'current_subscriber'}{'visibility'} eq $m) {
            $param->{'visibility'}{$m}{'selected'} = ' selected';
        } else {
            $param->{'visibility'}{$m}{'selected'} = '';
        }
    }

    ## Bounces
    if ($subscriber->{'bounce'} =~ /^(\d+)\s+(\d+)\s+(\d+)(\s+(.*))?$/) {
        my @bounce = ($1, $2, $3, $5);
        $param->{'current_subscriber'}{'first_bounce'} =
            $language->gettext_strftime("%d %b %Y", localtime($bounce[0]));
        $param->{'current_subscriber'}{'last_bounce'} =
            $language->gettext_strftime("%d %b %Y", localtime($bounce[1]));
        $param->{'current_subscriber'}{'bounce_count'} = $bounce[2];
        if ($bounce[3] and $bounce[3] =~ /^(\d+\.(\d+\.\d+))$/) {
            $subscriber->{'bounce_code'} = $1;
            $subscriber->{'bounce_status'} =
                $Sympa::Tools::WWW::bounce_status{$2};
        } else {
            $subscriber->{'bounce_status'} = $bounce[3];
        }

        $param->{'previous_action'} = $in{'previous_action'};
    }

    ## Additional DB fields
    if ($Conf::Conf{'db_additional_subscriber_fields'}) {
        my @additional_fields = split ',',
            $Conf::Conf{'db_additional_subscriber_fields'};

        my %data;

        my $sdm = Sympa::DatabaseManager->instance;
        foreach my $field (@additional_fields) {
            # Is the Database defined
            unless ($sdm) {
                wwslog('err', 'Unavailable database connection');
                return undef;
            }

            # Check field type (enum or not).
            #FIXME FIXME: ENUM data type is not supported by at least SQLite;
            # types might be better to be defined by configuration.
            my $field_type;
            if ($sdm->can('get_fields')) {
                my $fields = $sdm->get_fields({table => 'subscriber_table'});
                $field_type = ($fields || {})->{$field};
            }
            if ($field_type and $field_type =~ /^enum[(](.+)[)]$/) {
                my @enum = split /\s*,\s*/, $1;
                foreach my $e (@enum) {
                    $e =~ s/^\'([^\']+)\'$/$1/;
                    $data{$field}{'enum'}{$e} = '';
                }
                $data{$field}{'type'} = 'enum';

                $data{$field}{'enum'}{$subscriber->{$field}} =
                    'selected="selected"'
                    if (defined $subscriber->{$field});
            } else {
                $data{$field}{'type'}  = 'string';
                $data{$field}{'value'} = $subscriber->{$field};
            }
        }
        $param->{'additional_fields'} = \%data;
    }

    $param->{'previous_action'} = $in{'previous_action'};

    return 1;
}

sub do_viewbounce {
    wwslog('info', '(%s, %s, envid=%s)',
        $in{'email'}, $in{'file'}, $in{'envid'});

    # Prevent directory traversal.
    if ($in{'file'}) {
        my $subpath = $in{'file'};
        $subpath =~ s{\Amsg00000/}{};
        delete $in{'file'} if $subpath =~ m{/};
    }

    my $escaped_email = Sympa::Tools::Text::escape_chars($in{'email'});

    my $bounce_path =
        $in{'envid'}
        ? sprintf('%s/%s_%08s',
        $list->get_bounce_dir(), $escaped_email, $in{'envid'})
        : $list->get_bounce_dir() . '/' . $escaped_email;

    unless (-r $bounce_path) {
        Sympa::Report::reject_report_web('user', 'no_bounce_user',
            {'email' => $in{'email'}},
            $param->{'action'}, $list);
        wwslog('info', 'No bounce %s', $param->{'lastbounce_path'});
        return undef;
    }

    my $html_relpath =
        $in{'envid'}
        ? sprintf('%s_%08s', $escaped_email, $in{'envid'})
        : $escaped_email;
    my $html_dir =
          $Conf::Conf{'viewmail_dir'}
        . '/bounce/'
        . $list->get_id . '/'
        . $html_relpath;

    unless (-d $html_dir) {
        my $bounce_message =
            Sympa::Message->new_from_file($bounce_path,
            context => ($list || $robot));
        Sympa::Archive::html_format(
            $bounce_message,
            'destination_dir' => $html_dir,
            'attachment_url' =>
                sprintf('viewbounce/%s/%s', $list->{'name'}, $html_relpath),
        ) if $bounce_message;
    }

    unless (-d $html_dir) {
        Sympa::Report::reject_report_web('intern',
            'no_html_message_available', {'dir' => $html_dir},
            $param->{'action'});
        wwslog('err', 'No HTML version of the message available in %s',
            $html_dir);
        return undef;
    }

    if (    $in{'file'}
        and $in{'file'} ne 'msg00000.html'
        and -f $html_dir . '/' . $in{'file'}
        and -r $html_dir . '/' . $in{'file'}) {
        $in{'file'} =~ /\.(\w+)$/;
        $param->{'file_extension'} = $1;
        $param->{'file'}           = $html_dir . '/' . $in{'file'};
        $param->{'bypass'}         = 1;
    } else {
        if (open my $fh, '<', $html_dir . '/msg00000.html') {
            $param->{'html_content'} = do { local $RS; <$fh> };
            close $fh;
        }

        #FIXME: Is this required?
        push @other_include_path, $html_dir;
    }

    return 1;
}

## some help for listmaster and developpers
sub do_scenario_test {
    wwslog('info', '');

    ## List available scenarii
    unless (opendir SCENARI, Sympa::Constants::DEFAULTDIR . '/scenari/') {
        Sympa::Report::reject_report_web(
            'intern',
            'cannot_open_dir',
            {'dir' => Sympa::Constants::DEFAULTDIR . '/scenari/'},
            $param->{'action'},
            $list,
            $param->{'user'}{'email'},
            $robot
        );
        wwslog('info', 'Unable to open %s/scenari',
            Sympa::Constants::DEFAULTDIR);
        return undef;
    }

    foreach my $scfile (readdir SCENARI) {
        if ($scfile =~ /^(\w+)\.(\w+)/) {
            $param->{'scenario'}{$1}{'defined'} = 1;
        }
    }
    closedir SCENARI;
    my $all_lists = Sympa::List::get_lists('*');
    foreach my $list (@$all_lists) {
        $param->{'listname'}{$list->{'name'}}{'defined'} = 1;
    }
    foreach my $a ('smtp', 'md5', 'smime') {
        #$param->{'auth_method'}{$a}{'define'}=1 ;
        $param->{'authmethod'}{$a}{'defined'} = 1;
    }

    $param->{'scenario'}{$in{'scenario'}}{'selected'} = 'selected="selected"'
        if $in{'scenario'};

    $param->{'listname'}{$in{'listname'}}{'selected'} = 'selected="selected"'
        if $in{'listname'};

    $param->{'authmethod'}{$in{'auth_method'}}{'selected'} =
        'selected="selected"'
        if $in{'auth_method'};

    $param->{'email'} = $in{'email'};

    if ($in{'scenario'}) {
        my $operation = $in{'scenario'};
        wwslog('debug3', 'Perform scenario_test');

        my $result = Sympa::Scenario::request_action(
            $robot,
            $operation,
            $in{'auth_method'},
            {   'listname'    => $in{'listname'},
                'sender'      => $in{'sender'},
                'email'       => $in{'email'},
                'remote_host' => $in{'remote_host'},
                'remote_addr' => $in{'remote_addr'}
            },
            'debug'
        );
        if (ref($result) eq 'HASH') {
            $param->{'scenario_action'}      = $result->{'action'};
            $param->{'scenario_condition'}   = $result->{'condition'};
            $param->{'scenario_auth_method'} = $result->{'auth_method'};
            $param->{'scenario_reason'}      = $result->{'reason'};
        }
    }
    return 1;
}

## Bouncing addresses review
sub do_reviewbouncing {
    wwslog('info', '(%s)', $in{'page'});
    my $size = $in{'size'} || $Conf::Conf{'review_page_size'};

    ## Owner
    $param->{'page'} = $in{'page'} || 1;
    if ($size eq 'all') {
        $param->{'total_page'} = $param->{'bounce_total'};
    } else {
        $param->{'total_page'} = int($param->{'bounce_total'} / $size);
        $param->{'total_page'}++
            if ($param->{'bounce_total'} % $size);
    }

    if ($param->{'total_page'} > 0
        and ($param->{'page'} > $param->{'total_page'})) {
        Sympa::Report::reject_report_web('user', 'no_page',
            {'page' => $param->{'page'}},
            $param->{'action'});
        wwslog('info', 'No page %d', $param->{'page'});
        return 'admin';
    }

    my @users;
    ## Members list
    for (
        my $i = $list->get_first_bouncing_list_member();
        $i;
        $i = $list->get_next_bouncing_list_member()
        ) {
        $list->parse_list_member_bounce($i);
        push @users, $i;
    }

    my $record;
    foreach my $i (
        sort {
                   ($b->{'bounce_score'} <=> $a->{'bounce_score'})
                || ($b->{'last_bounce'} <=> $a->{'last_bounce'})
                || ($b->{'bounce_class'} <=> $a->{'bounce_class'})
        } @users
        ) {
        $record++;

        if (($size ne 'all') && ($record > ($size * ($param->{'page'})))) {
            $param->{'next_page'} = $param->{'page'} + 1;
            last;
        }

        next
            if (($size ne 'all')
            && ($record <= (($param->{'page'} - 1) * $size)));

        $i->{'first_bounce'} =
            $language->gettext_strftime("%d %b %Y",
            localtime($i->{'first_bounce'}));
        $i->{'last_bounce'} =
            $language->gettext_strftime("%d %b %Y",
            localtime($i->{'last_bounce'}));

        ## Escape some weird chars
        $i->{'escaped_email'} =
            Sympa::Tools::Text::escape_chars($i->{'email'});

        push @{$param->{'members'}}, $i;
    }

    if ($param->{'page'} > 1) {
        $param->{'prev_page'} = $param->{'page'} - 1;
    }

    $param->{'size'} = $size;

    return 1;
}

sub do_resetbounce {
    wwslog('info', '');

    $in{'email'} = Sympa::Tools::Text::unescape_chars($in{'email'});

    my @emails = split /\0/, $in{'email'};

    foreach my $email (@emails) {

        my $escaped_email = Sympa::Tools::Text::escape_chars($email);

        unless ($list->is_list_member($email)) {
            Sympa::Report::reject_report_web('user', 'user_not_subscriber',
                {'email' => $email},
                $param->{'action'}, $list);
            wwslog('info', '%s not subscribed', $email);
            web_db_log(
                {   'status'     => 'error',
                    'error_type' => 'not_subscriber'
                }
            );
            return undef;
        }

        unless (
            $list->update_list_member(
                $email,
                bounce       => undef,
                update_date  => time,
                bounce_score => 0
            )
            ) {
            Sympa::Report::reject_report_web(
                'intern', 'update_subscriber_db_failed',
                {'sub' => $email}, $param->{'action'},
                $list, $param->{'user'}{'email'},
                $robot
            );
            wwslog('info', 'Failed update database for %s', $email);
            web_db_log(
                {   'status'     => 'error',
                    'error_type' => 'internal'
                }
            );
            return undef;
        }

        my $bounce_dir = $list->get_bounce_dir();

        unless (unlink $bounce_dir . '/' . $escaped_email) {
            wwslog(
                'info',
                'Failed deleting %s',
                $bounce_dir . '/' . $escaped_email
            );
            web_db_log(
                {   'status'     => 'error',
                    'error_type' => 'internal'
                }
            );
        }

        wwslog('info', 'Bounces for %s reset', $email);
        web_db_log({'status' => 'success'});

    }

    return $in{'previous_action'} || 'review';
}

## Rebuild an archive using arctxt/
sub do_rebuildarc {
    wwslog('info', '(%s, %s)', $param->{'list'}, $in{'month'});

    unless (_rebuildarc($list)) {
        return undef;
    }

    Sympa::Report::notice_report_web('performed_soon', {},
        $param->{'action'});
    web_db_log(
        {   'parameters' => $in{'month'},
            'status'     => 'success'
        }
    );
    return 'admin';
}

sub _rebuildarc {
    my $that = shift;

    my $listname;
    if (ref $list eq 'Sympa::List') {
        $listname = $list->{'name'};
    } else {
        $listname = '*';
    }

    my $arc_message = Sympa::Message->new(
        sprintf("\nrebuildarc %s *\n\n", $listname),
        context => $robot,
        sender  => $param->{'user'}{'email'},
        date    => time
    );
    my $marshalled = Sympa::Spool::Archive->new->store($arc_message);
    unless ($marshalled) {
        Sympa::Report::reject_report_web('intern', 'cannot_open_file',
            {'command' => 'rebuild'},
            $param->{'action'}, $list, $param->{'user'}{'email'}, $robot);
        wwslog('info', 'Cannot store command to rebuild archive of list %s',
            $list);
        web_db_log(
            {   'parameters' => $in{'month'},
                'status'     => 'error',
                'error_type' => 'internal'
            }
        );
        return undef;
    }

    return 1;
}

# Rebuild all archives using arctxt/
sub do_rebuildallarc {
    wwslog('info', '');

    unless (_rebuildarc($robot)) {
        return undef;
    }
    Sympa::Report::notice_report_web('performed_soon', {},
        $param->{'action'});
    web_db_log({'status' => 'success'});
    return 'serveradmin';
}

## Search among lists
sub do_edit_attributes {
    wwslog('info', '(%s)', $in{'filter'});

    return 1;
}

## list search form
sub do_search_list_request {
    wwslog('info', '');

    return 1;
}

## Search among lists
sub do_search_list {
    wwslog('info', '(%s)', $in{'filter_list'});

    unless (defined $in{'filter_list'} and length $in{'filter_list'}) {
        wwslog('info', 'No filter');
        return 'search_list_request';
    }

    ## Search key
    $param->{'filter_list'} = $in{'filter_list'};

    ## Members list
    my $record = 0;
    my $all_lists =
        Sympa::List::get_lists($robot,
        'filter' => ['%name%|%subject%' => $param->{'filter_list'}]);
    foreach my $list (@$all_lists) {
        my $is_admin = 0;
        my $result   = Sympa::Scenario::request_action(
            $list,
            'visibility',
            $param->{'auth_method'},
            {   'sender'      => $param->{'user'}{'email'},
                'remote_host' => $param->{'remote_host'},
                'remote_addr' => $param->{'remote_addr'}
            }
        );
        my $r_action;
        $r_action = $result->{'action'} if (ref($result) eq 'HASH');
        next unless ($r_action eq 'do_it');

        if ($param->{'user'}{'email'}
            and (  $list->is_admin('owner', $param->{'user'}{'email'})
                or $list->is_admin('editor', $param->{'user'}{'email'}))
            ) {
            $is_admin = 1;
        }

        $record++;
        $param->{'which'}{$list->{'name'}} = {
            'host'    => $list->{'admin'}{'host'},
            'subject' => $list->{'admin'}{'subject'},
            'admin'   => $is_admin,
            'export'  => 'no'
        };
    }
    $param->{'occurrence'} = $record;
    foreach my $listname (sort keys %{$param->{'which'}}) {
        if ($listname =~ /^([a-z])/) {
            push @{$param->{'orderedlist'}{$1}}, $listname;
        } else {
            push @{$param->{'orderedlist'}{'others'}}, $listname;
        }
    }

    return 1;
}

sub do_edit_list {
    wwslog('info', '');

    ## Check if the list belong to a family.
    my $family;
    if (defined $list->{'admin'}{'family_name'}) {
        unless ($family = $list->get_family()) {
            Sympa::Report::reject_report_web('intern', 'unable_get_family',
                {}, $param->{'action'}, $list, $param->{'user'}{'email'},
                $robot);
            wwslog('info', 'Impossible to get list %s\'s family',
                $list->{'name'});
            web_db_log(
                {   'status'     => 'error',
                    'error_type' => 'internal'
                }
            );
            return undef;
        }
    }

    ## This hash will contain all the data gathered from the edit list form.
    ## The keys are the parameter names.
    ## The values are either the parameter value or an array containing this
    ## value if this is a multiple values parameter.
    ## The value can be a scalar or a hash.
    my $new_admin = {};

    ## This hash contains the names of all the parameters sent by the form to
    ## the FCGI.
    ## The keys are the parameters name, the value is always 1.
    ## Used only to parse the data.
    my $edited_param = {};

    ## Parse all the data sent from the web interface to the FCGI.
    ## Fills the $new_admin and $edited_param hashes.
    foreach my $key (sort keys %in) {
        next unless ($key =~ /^(single_param|multiple_param)\.(\S+)$/);

        $key =~ /^(single_param|multiple_param)\.(\S+)$/;
        my ($type, $name) = ($1, $2);

        ## Tag parameter as present in the form
        if (   $name =~ /^([^\.]+)(\.)/
            || $name =~ /^([^\.]+)$/) {
            $edited_param->{$1} = 1;
        }

        ## Parameter value
        my $value = $in{$key};
        next if ($value =~ /^\s*$/);

        ## If the parameter is a multiple values parameter, store the values
        ## into an array.
        if ($type eq 'multiple_param') {
            my @values = split /\0/, $value;
            $value = \@values;
        }

        my @token = split(/\./, $name);

        ## make it an entry in $new_admin
        my $var = _shift_var(0, $new_admin, @token);
        $$var = $value;
    }

    ## Check that the serial number sent by the form is the same as the one we
    ## expect.
    ## Avoid modifying a list previously modified by another way.
    unless ($list->{'admin'}{'serial'} == $in{'serial'}) {
        Sympa::Report::reject_report_web('user', 'config_changed',
            {'email' => $list->{'admin'}{'update'}{'email'}},
            $param->{'action'}, $list);
        wwslog(
            'info',
            'Config file has been modified(%d => %d) by %s. Cannot apply changes',
            $in{'single_param.serial'},
            $list->{'admin'}{'serial'},
            $list->{'admin'}{'update'}{'email'}
        );
        web_db_log(
            {   'status'     => 'error',
                'error_type' => 'internal'
            }
        );
        return undef;
    }

    ## Check changes & check syntax
    ## %changed stores the names of the parameters whose values differs from
    ## the value in the config file.
    ## %stores the name of parameter for which values have been deleted. The
    ## keys are the parameter name, the values are the index of the deleted
    ## value.
    ## @syntax_error stores the list of parameters for which syntax errors wre
    ## founs while evaluating the data sent by the form.
    my (%changed, %delete);
    my @syntax_error;

    ## Check family constraints.
    ## %check_family is a hash whose keys are a parameter name and whose
    ## values are the constraints
    ## defined for this parameter.
    my %check_family;

    ## Getting changes about owners or editors
    ## If changes occurred in the owner or editor definition, these scalars
    ## are set to 1.
    my $owner_update  = 0;
    my $editor_update = 0;

    ######################################################################
    ## Start of the loop parsing the data sent by the edition form. ##
    ######################################################################

    foreach my $pname (sort Sympa::List::by_order keys %{$edited_param}) {

        ## $p will contain the values of the current parameter in the previous
        ## list config
        ## $new_p  will contain the values sent by the form for the current
        ## parameter.
        my ($p, $new_p);

        ## Check privileges first
        next
            unless (
            $list->may_edit($pname, $param->{'user'}{'email'}) eq 'write');

        # If the list belongs to a family, gather all the constraints for
        # each edited parameter.
        if (ref $family eq 'Sympa::Family') {
            unless (ref $pinfo->{$pname}{'format'} eq 'HASH' or ref $pname) {
                # Simple parameter.
                my $constraint = $family->get_param_constraint($pname);

                if (ref $constraint eq 'HASH') {
                    # Controlled parameter.
                    $check_family{$pname} = $constraint;

                } elsif ($constraint ne '0') {
                    # Fixed parameter (free : no control).
                    next;
                }
            }
        }

        # Skip the obsolete parameters and alias names.
        next if $pinfo->{$pname}{'obsolete'};

        ## $to_index value will correspond to the number of not empty
        ## parameters sent by the form.
        my $to_index;

        ####### Validation, step 1: remove empty entries ###########

        ## If the parameter can have multiple values...
        if ($pinfo->{$pname}{'occurrence'} =~ /n$/) {

            ## They were either entries removed by the user or empty entries
            ## added by wwsympa
            ## The loop is going backward so we can remove empty entries
            my @all = 0 .. $#{$new_admin->{$pname}};
            foreach my $i (reverse @all) {
                ## If the parameter has a complex structure
                if (ref($pinfo->{$pname}{'format'}) eq 'HASH') {
                    ## Check each component of the complex parameter.
                    foreach my $key (keys %{$pinfo->{$pname}{'format'}}) {
                        # Skip the obsolete component and the alias.
                        next if $pinfo->{$pname}{'format'}{$key}{'obsolete'};

                        ## As soon as a required component is found missing,
                        ## the whole parameter instance is removed.
                        if ($pinfo->{$pname}{'format'}{$key}{'occurrence'} =~
                            /^1/
                            && $new_admin->{$pname}[$i]{$key} =~ /^\s*$/) {
                            splice(@{$new_admin->{$pname}}, $i, 1);
                            last;
                        }
                    }
                    ## Else if the parameter has only a scalar value
                } else {
                    ## Remove if empty
                    if ($new_admin->{$pname}[$i] =~ /^\s*$/) {
                        splice(@{$new_admin->{$pname}}, $i, 1);
                        next;
                    }
                }
            }

            ## Now, %new_admin contains only entries for which all the
            ## mandatory values are accounted for.

            ## $last_index corresponds to the number of remaining instances of
            ## this param sent by the form.
            my $last_index = $#{$new_admin->{$pname}};

            ## If a mandatory parameter is missing, issue an error and stop
            ## here.
            if ($pinfo->{$pname}{'occurrence'} =~ /^1/ && !($last_index >= 0))
            {
                delete $new_admin->{$pname};
                wwslog('err', 'Parameter %s is mandatory', $pname);
                Sympa::Report::reject_report_web('user',
                    'mandatory_parameter', {'p_name' => $pname},
                    $param->{'action'}, $list);
                web_db_log(
                    {   'status'     => 'error',
                        'error_type' => 'syntax_errors'
                    }
                );
                next;
            }

            ## If there are less entries in the config file than were sent by
            ## the form,
            ## $to_index must correspond to the number of entries sent.
            if ($#{$list->{'admin'}{$pname}} < $last_index) {
                $to_index = $last_index;
                ## Otherwise, $to_index must correspond to the number of
                ## entries in the config file.
            } else {
                $to_index = $#{$list->{'admin'}{$pname}};
            }

            $p     = $list->{'admin'}{$pname};
            $new_p = $new_admin->{$pname};

            ## If the parameter can't have multiple values...
        } else {

            ## If the parameter has a complex structure
            if (ref($pinfo->{$pname}{'format'}) eq 'HASH') {
                ## Check each component of the complex parameter.
                foreach my $key (keys %{$pinfo->{$pname}{'format'}}) {
                    # Skip the obsolete component and the alias.
                    next if $pinfo->{$pname}{'format'}{$key}{'obsolete'};

                    ## Remove the full record if a component is emtpy and
                    ## required
                    if ($pinfo->{$pname}{'format'}{$key}{'occurrence'} =~ /^1/
                        && $new_admin->{$pname}{$key} =~ /^\s*$/) {
                        delete $new_admin->{$pname};
                        last;
                    }
                }
                ## If the parameter contains a simple scalar value.
            } else {

                ## Remove if empty
                if ($new_admin->{$pname} =~ /^\s*$/) {
                    delete $new_admin->{$pname};
                    next;    # Go directly to the next parameter.
                }
            }

            $p     = [$list->{'admin'}{$pname}];
            $new_p = [$new_admin->{$pname}];
        }

        ### Validation, step 2: - check if the parameter was modified.
        ### ###
        ###                     - check that the new values have the right
        ###                     syntax. ###
        ### Note: this step is performed for each occurrence of the parameter.
        ### ###

        foreach my $i (0 .. $to_index) {
            unless (defined $new_p->[$i]) {
                push @{$delete{$pname}}, $i;
                $changed{$pname} = 1;
                next;
            }
            ## If the parameter corresponds to a scenario or a task, mark it
            ## as changed if its name was changed.
            ## Example: 'subscribe'
            if (   $pinfo->{$pname}{'scenario'}
                || $pinfo->{$pname}{'task'}) {
                if ($p->[$i]{'name'} ne $new_p->[$i]{'name'}) {
                    $changed{$pname} = 1;
                    next;
                }
                ## If the parameter has a complex structure, we need to check
                ## all its components.
                ## Example: 'owner'
            } elsif (ref($pinfo->{$pname}{'format'}) eq 'HASH') {
                ## Check each parameter component.
                ## Example: 'owner->email'
                foreach my $key (keys %{$pinfo->{$pname}{'format'}}) {
                    # Skip the obsolete component and the alias.
                    next if $pinfo->{$pname}{'format'}{$key}{'obsolete'};

                    ## Check that the user is allowed to edit this parameter
                    ## component.
                    next
                        unless (
                        $list->may_edit("$pname.$key",
                            $param->{'user'}{'email'}) eq 'write'
                        );

                    ## If the list belongs to a family, check the possible
                    ## constraints on this parameter component.
                    if (ref($family) eq 'Sympa::Family') {
                        ## Test constraints only if the parameter component is
                        ## not a complex structure.
                        if (!ref($key)) {
                            my $constraint =
                                $family->get_param_constraint("$pname.$key");
                            if (ref($constraint) eq 'HASH') {
                                # controlled parameter
                                $check_family{$pname}{$key} = $constraint;
                            } elsif ($constraint ne '0') {   # fixed parameter
                                next;    # Go to the next parameter component.
                            }
                        }
                    }

                    ## If the parameter component corresponds to a task or a
                    ## scenario, mark it as changed if its name was changed.
                    if (   $pinfo->{$pname}{'format'}{$key}{'scenario'}
                        || $pinfo->{$pname}{'format'}{$key}{'task'}) {
                        if ($p->[$i]{$key}{'name'} ne
                            $new_p->[$i]{$key}{'name'}) {
                            $changed{$pname} = 1;
                            # Mark as changed and go to the next parameter
                            # component.
                            next;
                        }
                        ## If the parameter component doesn't correspond to a
                        ## task or a scenario, we must check its content.
                    } else {
                        ## Parameter component check, case 1: this parameter
                        ## component can have multiple occurrence.
                        ## Example: 'digest->days'
                        if ($pinfo->{$pname}{'format'}{$key}{'occurrence'} =~
                            /n$/) {
                            ## If the new value differs from the previous
                            ## value, mark as changed and go to the next
                            ## parameter component.
                            if ($#{$p->[$i]{$key}} != $#{$new_p->[$i]{$key}})
                            {
                                $changed{$pname} = 1;
                                next;
                            }

                            ## For each occurrence of this parameter
                            ## component, check value
                            foreach my $index (0 .. $#{$p->[$i]{$key}}) {
                                my $format =
                                    $pinfo->{$pname}{'format'}{$key}
                                    {'format'};

                                ## If the format has a complex structure, it
                                ## is the description of a file format.
                                if (ref($format)) {
                                    $format =
                                        $pinfo->{$pname}{'format'}{$key}
                                        {'file_format'};
                                }
                                ## If this occurrence of the parameter
                                ## component differs from the corresponding
                                ## one in the config
                                ## check the syntax and mark as changed.
                                if ($p->[$i]{$key}[$index] ne
                                    $new_p->[$i]{$key}[$index]) {

                                    if (defined($new_p->[$i]{$key}[$index])
                                        && $new_p->[$i]{$key}[$index] !~
                                        /^$format$/i) {
                                        wwslog('err',
                                            "Syntax error : $pname/$i/$key/$index = $new_p->[$i]{$key}[$index]"
                                        );
                                        push @syntax_error, $pname;
                                    }
                                    $changed{$pname} = 1;
                                    # Mark as changed and go to the next
                                    # parameter component.
                                    next;
                                }
                            }

                            ## Parameter component check, case 2: this
                            ## component is limited to one occurrence.
                            ## Example: 'owner->email'
                        } else {
                            ## If the parameter component value differs from
                            ## the corresponding one in the config, go on.
                            if ($p->[$i]{$key} ne $new_p->[$i]{$key}) {
                                my $format =
                                    $pinfo->{$pname}{'format'}{$key}
                                    {'format'};

                                ## If the format has a complex structure, it
                                ## is the description of a file format.
                                if (ref($format)) {
                                    $format =
                                        $pinfo->{$pname}{'format'}{$key}
                                        {'file_format'};
                                }

                                ## Check the syntax and mark as changed if the
                                ## syntax is correct.
                                if (defined($new_p->[$i]{$key})
                                    && $new_p->[$i]{$key} !~ /^$format$/i) {
                                    wwslog('err',
                                        "Syntax error : $pname/$i/$key = $new_p->[$i]{$key}"
                                    );
                                    push @syntax_error, $pname;
                                }

                                $changed{$pname} = 1;
                                # Mark as changed and go to the next parameter
                                # component.
                                next;
                            }
                        }
                    }
                }
                ## If the parameter has just a scalar value, just check its
                ## value.
                ## Example: 'max_size'
            } else {
                ## If the value differs from the one in the config file, mark
                ## parameter as changed if the syntax is correct.
                if ($p->[$i] ne $new_p->[$i]) {
                    unless (
                        $new_p->[$i] =~ /^$pinfo->{$pname}{'file_format'}$/) {
                        wwslog('err', 'Syntax error: %s/%s = %s',
                            $pname, $i, $new_p->[$i]);
                        push @syntax_error, $pname;
                    }
                    $changed{$pname} = 1;
                }
            }
        }
    }

    ######################################################################
    ## Validation of the form finished. Start of valid data treatments  ##
    ######################################################################

    ## Error if no parameter was edited
    unless (keys %changed) {
        Sympa::Report::reject_report_web('user', 'no_parameter_edited', {},
            $param->{'action'}, $list);
        wwslog('info', 'No parameter was edited by user');
        return 'edit_list_request';
    }

    ## Syntax errors
    if ($#syntax_error > -1) {
        Sympa::Report::reject_report_web('user', 'syntax_errors',
            {'params' => join(',', @syntax_error)},
            $param->{'action'}, $list);
        wwslog(
            'info',
            'Syntax errors for parameters %s',
            join(',', @syntax_error)
        );
        web_db_log(
            {   'status'     => 'error',
                'error_type' => 'syntax_errors'
            }
        );
        return undef;
    }

    ## Checking no topic named "other"
    foreach my $msg_topic (@{$new_admin->{'msg_topic'}}) {
        if ($msg_topic->{'name'} =~ /^other$/i) {
            $msg_topic->{'name'}  = undef;
            $msg_topic->{'title'} = undef;
            Sympa::Report::reject_report_web('user', 'topic_other', {},
                $param->{'action'}, $list);
            wwslog('notice', 'Topic other is a reserved word');
            web_db_log(
                {   'status'     => 'error',
                    'error_type' => 'syntax_errors'
                }
            );
            return undef;
        }
    }

    ## For changed msg_topic.name
    if (defined $new_admin->{'msg_topic'}
        and _notify_deleted_topic($new_admin->{'msg_topic'})) {
        Sympa::Report::notice_report_web('subscribers_noticed_deleted_topics',
            {}, $param->{'action'});
    }

    ## Checking that list owner address is not set to one of the special
    ## addresses:
    if (exists $changed{'owner'}) {
        my @special = ();
        push @special,
            map { $list->get_list_address($_) }
            qw(owner editor return_path subscribe unsubscribe);
        push @special, map {
            sprintf '%s-%s@%s',
                $list->{'name'}, lc $_, $list->{'admin'}{'host'}
            }
            split /[,\s]+/,
            Conf::get_robot_conf($robot, 'list_check_suffixes');
        my $bounce_email_re = quotemeta($list->get_bounce_address('ANY'));
        $bounce_email_re =~ s/(?<=\\\+).*(?=\\\@)/.*/;

        foreach my $owner (@{$new_admin->{'owner'}}) {
            my $owner_email = lc $owner->{'email'};

            if (grep { $owner_email eq $_ } @special
                or $owner_email =~ /^$bounce_email_re$/) {
                ## generate an error and return
                Sympa::Report::reject_report_web('user', 'incorrect_email',
                    {'email' => $owner->{'email'}},
                    $param->{'action'}, $list);
                wwslog('info', 'Reserved email address %s',
                    $owner->{'email'});
                web_db_log(
                    {   'status'     => 'error',
                        'error_type' => 'incorrect_email',
                        'parameters' => $owner->{'email'}
                    }
                );
                return undef;
            }
        }
    }

    ## Delete selected params
    foreach my $p (keys %delete) {

        if (($p eq 'owner') || ($p eq 'owner_include')) {
            $owner_update = 1;
        }

        if (($p eq 'editor') || ($p eq 'editor_include')) {
            $editor_update = 1;
        }

        ## Delete ALL entries
        unless (ref($delete{$p})) {
            undef $new_admin->{$p};
            next;
        }

        ## Delete selected entries
        foreach my $k (reverse @{$delete{$p}}) {
            splice @{$new_admin->{$p}}, $k, 1;
        }

        if (defined $check_family{$p}) {    # $p is family controlled
            if ($#{$new_admin->{$p}} < 0) {
                Sympa::Report::reject_report_web('user',
                    'p_family_controlled', {'param' => $p},
                    $param->{'action'}, $list);
                wwslog('info',
                    'Parameter %s must have values (family context)', $p);
                web_db_log(
                    {   'status'     => 'error',
                        'error_type' => 'missing_parameter'
                    }
                );
                return undef;
            }
        }
    }

    # updating config_changes for deleted parameters
    if (ref($family)) {
        my @array_delete = keys %delete;
        unless ($list->update_config_changes('param', \@array_delete)) {
            Sympa::Report::reject_report_web('intern',
                'update_config_changes', {}, $param->{'action'}, $list,
                $param->{'user'}{'email'}, $robot);
            wwslog(
                'info',
                'Cannot write in config_changes for deleted parameters from list %s',
                $list->{'name'}
            );
            web_db_log(
                {   'status'     => 'error',
                    'error_type' => 'internal'
                }
            );
            return undef;
        }
    }

    ## Update config in memory
    my $data_source_updated;
    foreach my $parameter (keys %changed) {

        my $pname;
        if ($parameter =~ /^([\w-]+)\.([\w-]+)$/) {
            $pname = $1;
        } else {
            $pname = $parameter;
        }

        ## If new owners/editors have been added, then notify them
        foreach my $admin_type ('owner', 'editor') {
            my (%previous_emails, %new_emails);

            ## Check previous entries
            foreach my $entry (@{$list->{'admin'}{$admin_type}}) {
                $previous_emails{$entry->{'email'}} = 1;
            }

            ## Compare with new entries
            foreach my $entry (@{$new_admin->{$admin_type}}) {
                unless ($previous_emails{$entry->{'email'}}) {
                    # Notify the new list owner/editor
                    Sympa::send_notify_to_user(
                        $list,
                        'added_as_listadmin',
                        $entry->{'email'},
                        {   admin_type => $admin_type,
                            delegator  => $param->{'user'}{'email'}
                        }
                    );
                    Sympa::Report::notice_report_web('user_notified',
                        {'notified_user' => $entry->{'email'}},
                        $param->{'action'});
                }
            }
        }

        if (defined $check_family{$pname}) {    # $pname is CONTROLLED
            _check_new_values(\%check_family, $pname, $new_admin);
        }

        ## If datasource config changed
        if ($pname =~ /^(include_.*|ttl)$/) {
            $data_source_updated = 1;
        }

        $list->{'admin'}{$pname} = $new_admin->{$pname};
        if (defined $new_admin->{$pname} || $pinfo->{$pname}{'internal'}) {
            delete $list->{'admin'}{'defaults'}{$pname};
        } else {
            $list->{'admin'}{'defaults'}{$pname} = 1;
        }

        if (($pname eq 'owner') || ($pname eq 'owner_include')) {
            $owner_update = 1;
        }

        if (($pname eq 'editor') || ($pname eq 'editor_include')) {
            $editor_update = 1;
        }

        # updating config_changes for changed parameters

        if (ref($family)) {
            my @array_changed = keys %changed;
            unless ($list->update_config_changes('param', \@array_changed)) {
                Sympa::Report::reject_report_web('intern',
                    'update_config_changes', {}, $param->{'action'}, $list,
                    $param->{'user'}{'email'}, $robot);
                wwslog(
                    'info',
                    'Cannot write in config_changes for changed parameters from list %s',
                    $list->{'name'}
                );
                web_db_log(
                    {   'status'     => 'error',
                        'error_type' => 'internal'
                    }
                );
                return undef;
            }
        }
    }

    ## Save config file
    unless ($list->save_config($param->{'user'}{'email'})) {
        Sympa::Report::reject_report_web('intern', 'cannot_save_config', {},
            $param->{'action'}, $list, $param->{'user'}{'email'}, $robot);
        wwslog('info', 'Cannot save config file');
        web_db_log(
            {   'status'     => 'error',
                'error_type' => 'internal'
            }
        );
        return undef;
    }

    ## Reload config to clean some empty entries in $list->{'admin'}
    $list = Sympa::List->new($list->{'name'}, $robot, {'reload_config' => 1});

    unless (defined $list) {
        Sympa::Report::reject_report_web('intern', 'list_reload', {},
            $param->{'action'}, '', $param->{'user'}{'email'}, $robot);
        wwslog('info', 'Error in list reloading');
        web_db_log(
            {   'status'     => 'error',
                'error_type' => 'internal'
            }
        );
        return undef;
    }

    ## If list has included data sources, update them and delete sync_include
    ## task.
    if ($data_source_updated) {
        if ($list->on_the_fly_sync_include('use_ttl' => 0)) {
            Sympa::Report::notice_report_web('subscribers_updated', {},
                $param->{'action'});
        } else {
            Sympa::Report::reject_report_web('intern', 'sync_include_failed',
                {}, $param->{'action'}, $list, $param->{'user'}{'email'},
                $robot);
        }
    }

    ## call sync_include_admin if there are changes about owners or editors
    ## and we're in mode include2
    unless ($list->sync_include_admin()) {
        Sympa::Report::reject_report_web('intern',
            'sync_include_admin_failed', {}, $param->{'action'}, $list,
            $param->{'user'}{'email'}, $robot);
        wwslog('info', '() Failed');
        web_db_log(
            {   'status'     => 'error',
                'error_type' => 'internal'
            }
        );
        return undef;
    }

    ## Tag changed parameters
    foreach my $pname (keys %changed) {
        $::changed_params{$pname} = 1;
    }

    ## Save stats
    $list->savestats();

    #      print "Content-type: text/plain\n\n";
    #    Sympa::Tools::Data::dump_var($list->{'admin'}{'msg_topic'},0);
    #    Sympa::Tools::Data::dump_var($param->{'param'},0);

    Sympa::Report::notice_report_web('list_config_updated', {},
        $param->{'action'});
    web_db_log({'status' => 'success'});
    return 'edit_list_request';
}

## Shift tokens to get a reference to the desired
## entry in $var (recursive)
sub _shift_var {
    my ($i, $var, @tokens) = @_;
    wwslog('debug3', '(%s, %s, %s)', $i, $var, join('.', @tokens));
    my $newvar;

    my $token = shift @tokens;

    if ($token =~ /^\d+$/) {
        return \$var->[$token]
            if ($#tokens == -1);

        if ($tokens[0] =~ /^\d+$/) {
            unless (ref $var->[$token]) {
                $var->[$token] = [];
            }
            $newvar = $var->[$token];
        } else {
            unless (ref $var->[$token]) {
                $var->[$token] = {};
            }
            $newvar = $var->[$token];
        }
    } else {
        return \$var->{$token}
            if ($#tokens == -1);

        if ($tokens[0] =~ /^\d+$/) {
            unless (ref $var->{$token}) {
                $var->{$token} = [];
            }
            $newvar = $var->{$token};
        } else {
            unless (ref $var->{$token}) {
                $var->{$token} = {};
            }
            $newvar = $var->{$token};
        }

    }

    if ($#tokens > -1) {
        $i++;
        return _shift_var($i, $newvar, @tokens);
    }
    return $newvar;
}

# Deletes topics subscriber that does not exist anymore and send a notify to
# concerned subscribers.
# Returns 0 if no subscriber topics have been deleted; 1 if some subscribers
# topics have been deleted.
# Old name: Sympa::List::modifying_msg_topic_for_list_members().
sub _notify_deleted_topic {
    $log->syslog('debug3', '(%s)', @_);
    my $new_msg_topic = shift;

    my $deleted = 0;

    my @old_msg_topic_name;
    foreach my $msg_topic (@{$list->{'admin'}{'msg_topic'}}) {
        push @old_msg_topic_name, $msg_topic->{'name'};
    }

    my @new_msg_topic_name;
    foreach my $msg_topic (@{$new_msg_topic}) {
        push @new_msg_topic_name, $msg_topic->{'name'};
    }

    my $msg_topic_changes =
        Sympa::Tools::Data::diff_on_arrays(\@old_msg_topic_name,
        \@new_msg_topic_name);

    if (@{$msg_topic_changes->{'deleted'}}) {
        for (
            my $subscriber = $list->get_first_list_member();
            $subscriber;
            $subscriber = $list->get_next_list_member()
            ) {
            if ($subscriber->{'reception'} eq 'mail') {
                my $topics = Sympa::Tools::Data::diff_on_arrays(
                    $msg_topic_changes->{'deleted'},
                    Sympa::Tools::Data::get_array_from_splitted_string(
                        $subscriber->{'topics'}
                    )
                );

                if (@{$topics->{'intersection'}}) {
                    Sympa::send_notify_to_user(
                        $list, 'deleted_msg_topics',
                        $subscriber->{'email'},
                        {del_topics => $topics->{'intersection'}}
                    );
                    unless (
                        $list->update_list_member(
                            lc($subscriber->{'email'}),
                            update_date => time,
                            topics      => join(',', @{$topics->{'added'}})
                        )
                        ) {
                        $log->syslog(
                            'err',
                            'Impossible to update user "%s" of list %s',
                            $subscriber->{'email'}, $list
                        );
                    }
                    $deleted = 1;
                }
            }
        }
    }
    return 1 if $deleted;
    return 0;
}

#=head2 sub do_edit_list_request
#
#Sends back the list config edition form.
#
#=head3 Arguments
#
#=over
#
#=item * I<None>
#
#=back
#
#=head3 Return
#
#=over
#
#=item * I<1>, if no problem is encountered
#
#=item * I<undef>, if anything goes wrong
#
#=item * I<'loginrequest'> if no user is logged in at the time the function is called.
#
#=back
#
#=cut

## Send back the list config edition form
sub do_edit_list_request {
    wwslog('info', '(%s)', $in{'group'});

    if ($in{'group'}) {
        $param->{'group'} = $in{'group'};
        _prepare_edit_form($list);
    }

    #    print "Content-type: text/plain\n\n";
    #    Sympa::Tools::Data::dump_var(\%pinfo,0);
    #    Sympa::Tools::Data::dump_var($list->{'admin'},0);
    #    Sympa::Tools::Data::dump_var($param->{'param'},0);

    $param->{'serial'} = $list->{'admin'}{'serial'};

    return 1;
}

sub _check_new_values {
    my $check_family = shift;
    my $pname        = shift;
    my $new_admin    = shift;
    wwslog('debug3', '(%s)', $pname);

    my $fake_list =
        bless {'domain' => $robot, 'admin' => $new_admin} => 'Sympa::List';
    ## TODO: update parameter cache
    my $uncompellable_param = Sympa::Family::get_uncompellable_param();

    if (ref($pinfo->{$pname}{'format'}) eq 'HASH') {    #composed parameter

        foreach my $key (keys %{$check_family->{$pname}}) {

            my $constraint = $check_family->{$pname}{$key};
            my $values     = $fake_list->get_param_value("$pname.$key", 1);
            my $nb_for     = 0;

            # exception for uncompellable param
            foreach my $p (keys %{$uncompellable_param}) {
                if (($pname eq $p) && !($uncompellable_param->{$p})) {
                    return 1;
                }

                if (($pname eq $p) && ($key eq $uncompellable_param->{$p})) {
                    return 1;
                }
            }
            foreach my $p_val (@{$values}) {    #each element value
                $nb_for++;
                if (ref($p_val) eq 'ARRAY') {    # multiple values
                    foreach my $p (@{$p_val}) {
                        if (  !($constraint->{$p})
                            && (($nb_for == 1) || ($p ne ''))) {
                            Sympa::Report::reject_report_web('user',
                                'p_family_wrong',
                                {'param' => $pname, 'val' => $p},
                                $param->{'action'});
                            wwslog(
                                'info',
                                'Parameter %s has got wrong value: %s (family context)',
                                $pname,
                                $p
                            );
                            return undef;
                        }
                    }
                } else {    # single value
                    if (  !($constraint->{$p_val})
                        && (($nb_for == 1) || ($p_val ne ''))) {
                        Sympa::Report::reject_report_web('user',
                            'p_family_wrong',
                            {'param' => $pname, 'val' => $p_val},
                            $param->{'action'});
                        wwslog(
                            'info',
                            'Parameter %s has got wrong value: %s (family context)',
                            $pname,
                            $p_val
                        );
                        return undef;
                    }
                }
            }
        }
    } else {    #simple parameter

        # exception for uncompellable param
        foreach my $p (keys %{$uncompellable_param}) {
            if ($pname eq $p) {
                return 1;
            }
        }

        my $constraint = $check_family->{$pname};
        my $values     = $fake_list->get_param_value($pname, 1);
        my $nb_for     = 0;

        foreach my $p_val (@{$values}) {    #each element value
            $nb_for++;
            if (ref($p_val) eq 'ARRAY') {    # multiple values
                foreach my $p (@{$p_val}) {
                    if (  !($constraint->{$p})
                        && (($nb_for == 1) || ($p ne ''))) {
                        Sympa::Report::reject_report_web('user',
                            'p_family_wrong',
                            {'param' => $pname, 'val' => $p},
                            $param->{'action'});
                        wwslog(
                            'info',
                            'Parameter %s has got wrong value: %s (family context)',
                            $pname,
                            $p
                        );
                        return undef;
                    }
                }
            } else {    # single value
                if (  !($constraint->{$p_val})
                    && (($nb_for == 1) || ($p_val ne ''))) {
                    Sympa::Report::reject_report_web('user', 'p_family_wrong',
                        {'param' => $pname, 'val' => $p_val},
                        $param->{'action'});
                    wwslog(
                        'info',
                        'Parameter %s has got wrong value: %s (family context)',
                        $pname,
                        $p_val
                    );
                    return undef;
                }
            }
        }
    }
}

#=head2 sub _prepare_edit_form(LIST)
#
#Prepares config data to be sent in the edition form. Adds to the parameters array a hash for each parameter to be edited.
#
#=head3 Arguments
#
#=over
#
#=item * I<$list>, a List object
#
#=back
#
#=head3 Return
#
#=over
#
#=item * I<1>, if no problem is encountered
#
#=item * I<undef>, if anything goes wrong
#
#=back
#
#=cut

## Prepare config data to be sent in the
## edition form
sub _prepare_edit_form {
    my $list        = shift;
    my $list_config = Sympa::Tools::Data::dup_var($list->{'admin'});
    my $family;
    my $is_form_editable = '0';

    ## If the list belongs to a family, check if the said family can be
    ## retrieved.
    if (defined $list_config->{'family_name'}) {
        unless ($family = $list->get_family()) {
            Sympa::Report::reject_report_web('intern', 'unable_get_family',
                {}, $param->{'action'}, $list, $param->{'user'}{'email'},
                $robot);
            wwslog('info', 'Impossible to get list %s\'s family',
                $list->{'name'});
            return undef;
        }
    }

    ## For each parameter defined in List.pm, retrieve and prepare for editing
    foreach my $pname (sort Sympa::List::by_order keys %{$pinfo}) {
        # Skip comments and default values.
        next if grep { $pname eq $_ } qw(comment defaults);

        # Skip parameters belonging to another group.
        next if $in{'group'} and $pinfo->{$pname}{'group'} ne $in{'group'};

        # Skip obsolete parameters and alias names.
        next if $pinfo->{$pname}{'obsolete'};

        ## Check whether the parameter can be edited by the logged user.
        my $may_edit = $list->may_edit($pname, $param->{'user'}{'email'});

        ## Valid form global edit status as soon as at least one editable
        ## parameter is found.
        if ($may_edit eq 'write') {
            $is_form_editable = '1';
        }

        # Store in $p a reference to the hash containing the information
        # relative to the parameter editing.
        my $p =
            _prepare_data($pname, $pinfo->{$pname}, $list_config->{$pname},
            $may_edit, $family);
        next unless defined $p;

        ## Store if the parameter is still at its default value or not.
        $p->{'default'} = $list_config->{'defaults'}{$pname};

        ## Store the change state of this parameter, taken from the global
        ## variable %changed_params.
        $p->{'changed'} = $::changed_params{$pname};

        ## Exceptions...too many
        if ($pname eq 'topics') {
            $p->{'type'} = 'enum';

            my @topics;
            foreach my $topic (@{$p->{'value'}}) {
                push @topics, $topic->{'value'};
            }
            undef $p->{'value'};
            my %list_of_topics = Sympa::Robot::load_topics($robot);

            if (defined $p->{'constraint'}) {
                _restrict_values(\%list_of_topics, $p->{'constraint'});
            }

            foreach my $topic (keys %list_of_topics) {
                $p->{'value'}{$topic}{'selected'} = 0;
                $p->{'value'}{$topic}{'title'} =
                    $list_of_topics{$topic}{'current_title'};

                if ($list_of_topics{$topic}{'sub'}) {
                    foreach
                        my $subtopic (keys %{$list_of_topics{$topic}{'sub'}})
                    {
                        $p->{'value'}{"$topic/$subtopic"}{'selected'} = 0;
                        $p->{'value'}{"$topic/$subtopic"}{'title'} =
                            "$list_of_topics{$topic}{'current_title'}/$list_of_topics{$topic}{'sub'}{$subtopic}{'current_title'}";
                    }
                }
            }
            foreach my $selected_topic (@topics) {
                next unless (defined $selected_topic);
                $p->{'value'}{$selected_topic}{'selected'} = 1;
                $p->{'value'}{$selected_topic}{'title'} =
                    $language->gettext_sprintf("Unknown (%s)",
                    $selected_topic)
                    unless (defined $p->{'value'}{$selected_topic}{'title'});
            }
        } elsif ($pname eq 'digest') {
            foreach my $v (@{$p->{'value'}}) {
                next unless ($v->{'name'} eq 'days');
                if (ref($v->{'value'}) eq 'ARRAY') {
                    $log->syslog('debug2',
                        'Empty digest parameter. Putting a dummy value');
                    $v->{'value'} = undef;
                    $v->{'type'}  = 'enum';
                } else {
                    foreach my $day (keys %{$v->{'value'}}) {
                        my $t =
                            $language->gettext_strftime("%A",
                            gmtime(0 + ($day + 3) * (3600 * 24)));
                        $t = sprintf '%s (%s)', $t, $day
                            if Sympa::is_listmaster($list,
                            $param->{'user'}{'email'});
                        $v->{'value'}{$day}{'title'} = $t;
                    }
                }
            }
        } elsif ($pname eq 'lang') {
            $language->push_lang;
            foreach my $lang (keys %{$p->{'value'}}) {
                $language->set_lang($lang);
                my $t = $language->native_name || $lang;
                $t = sprintf '%s (%s)', $t, $lang
                    if Sympa::is_listmaster($list, $param->{'user'}{'email'});
                $p->{'value'}{$lang}{'title'} = $t;
                # compatibility: 6.1.
                $p->{'value'}{$lang}{'lang_tag'} = $lang;
            }
            $language->pop_lang;
        } elsif ($pname eq 'available_user_options'
            or $pname eq 'default_user_options') {
            foreach my $v (@{$p->{'value'}}) {
                if ($v->{'name'} eq 'reception') {
                    foreach my $x (keys %{$v->{'value'}}) {
                        $v->{'value'}{$x}{'title'} =
                            Sympa::ListOpt::get_title(
                            $x,
                            'reception',
                            Sympa::is_listmaster(
                                $list, $param->{'user'}{'email'}
                            )
                            );
                    }
                } elsif ($v->{'name'} eq 'visibility') {
                    foreach my $x (keys %{$v->{'value'}}) {
                        $v->{'value'}{$x}{'title'} =
                            Sympa::ListOpt::get_title(
                            $x,
                            'visibility',
                            Sympa::is_listmaster(
                                $list, $param->{'user'}{'email'}
                            )
                            );
                    }
                }
            }
        } elsif ($pname eq 'status') {
            foreach my $x (keys %{$p->{'value'}}) {
                $p->{'value'}{$x}{'title'} =
                    Sympa::ListOpt::get_title($x, 'status',
                    Sympa::is_listmaster($list, $param->{'user'}{'email'}));
            }
        }

        push @{$param->{'param'}}, $p;
    }

    ## If at least one param was editable, make the update button appear in
    ## the form.
    $param->{'is_form_editable'} = $is_form_editable;
    return 1;
}

#=head2 sub _prepare_data(STRING $name, HASH_Ref $struct, SCALAR $data, STRING $may_edit, FAMILY $family, STRING $main_p)
#
#Returns a reference to a hash containing the data used to edit the parameter (of name $name, corresponding to the structure $struct in pinfo, with the $may_edit editing status) containing the data in the Sympa web interface.
#
#=head3 Arguments
#
#=over
#
#=item * I<$name> (STRING), the name of the parameter processed
#
#=item * I<$struct> (HASH_Ref), a ref to the hash describing this parameter in %Sympa::ListDef::pinfo
#
#=item * I<$data> (), the value(s) taken by this parameter in the current list. Can be a reference to a list or the value of a single parameter.
#
#=item * I<$may_edit> (STRING), the editing status of this parameter in the current context.
#
#=item * I<$family> (FAMILY), the family the list belongs to.
#
#=item * I<$main_p> (STRING), the prefix composing the complete name of the parameter.
#
#=back
#
#=head3 Return
#
#=over
#
#=item * I<$p_glob>, a reference to a hash containing the data used to edit the parameter.
#
#=back
#
#=cut

sub _prepare_data {
    my ($name, $struct, $data, $may_edit, $family, $main_p) = @_;
    #    wwslog('debug2', '(%s, %s)', $name, $data);
    # $family and $main_p (recursive call) are optionnal
    # if $main_p is needed, $family also
    return undef if $struct->{'obsolete'};

    ## Prepare data structure for the parser
    my $p_glob = {'name' => $name};

    ## Check if some family constraint modify the editing rights.
    my $restrict = 0;
    my $constraint;
    if ((ref($family) eq 'Sympa::Family') && ($may_edit eq 'write')) {

        if ($main_p && defined $pinfo->{$main_p}) {
            if (ref($pinfo->{$main_p}{'format'}) eq 'HASH') {
                # composed parameter
                $constraint = $family->get_param_constraint(
                    "$main_p.$p_glob->{'name'}");
            }
        } else {    # simple parameter
            if (ref($pinfo->{$p_glob->{'name'}}{'format'}) ne 'HASH') {
                # simple parameter
                $constraint =
                    $family->get_param_constraint($p_glob->{'name'});
            }
        }
        if ($constraint eq '0') {    # free parameter
            $p_glob->{'may_edit'} = 'write';

        } elsif (ref($constraint) eq 'HASH') {    # controlled parameter
            $p_glob->{'may_edit'} = 'write';
            $restrict = 1;

        } else {                                  # fixed parameter
            $p_glob->{'may_edit'} = 'read';
        }

    } else {
        $p_glob->{'may_edit'} = $may_edit;
    }

    ## Naming the parameter.
    if ($struct->{'gettext_id'}) {
        $p_glob->{'title'} = $language->gettext($struct->{'gettext_id'});
    } else {
        $p_glob->{'title'} = $name;
    }
    if ($struct->{'gettext_comment'}) {
        $p_glob->{'comment'} =
            $language->gettext($struct->{'gettext_comment'});
    }

    ## Occurrences : if the parameter can have multiple occurrences,
    ## its values are transfered into the array pointed by $data2
    ## if they were given in arguments (if not, an empty array is created).
    ## if it is a single occurrence parameter, an array is created with
    ## its single value.

    my $data2;
    if ($struct->{'occurrence'} =~ /n$/) {
        $p_glob->{'occurrence'} = 'multiple';
        $data2 = $data || [];

        if ($may_edit eq 'write') {
            # Add an empty entry.
            unless (grep { $name eq $_ }
                qw(days reception rfc2369_header_fields topics)) {
                my $empty_entry;
                if (ref($struct->{'format'}) eq 'HASH') {
                    # Structured parameter.
                    foreach my $sub_parameter (keys %{$struct->{'format'}}) {
                        # Use default value if defined.
                        if ($struct->{'format'}{$sub_parameter}{'default'}) {
                            $empty_entry->{$sub_parameter} =
                                $struct->{'format'}{$sub_parameter}
                                {'default'};
                        }
                    }
                    $empty_entry->{is_empty} = 1;
                } else {
                    # Simple parameter.
                    $empty_entry = undef;
                }

                push @{$data2}, $empty_entry;
            }
        }
    } else {
        $data2 = [$data];
    }

    my @all_p;

    ## Foreach occurrence of param
    foreach my $d (@{$data2}) {
        my $p = {};

        ## Type of data
        if ($struct->{'scenario'}) {
            $p_glob->{'type'} = 'scenario';
            my $list_of_scenario;

            my $tmp_list_of_scenario =
                $list->load_scenario_list($struct->{'scenario'}, $robot);

            ## Only get required scenario attributes
            foreach my $scenario (keys %{$tmp_list_of_scenario}) {
                $list_of_scenario->{$scenario} = {
                    'name'      => $tmp_list_of_scenario->{$scenario}{'name'},
                    'web_title' => $tmp_list_of_scenario->{$scenario}
                        ->get_current_title()
                };
            }

            $list_of_scenario->{$d->{'name'}}{'selected'} = 1;

            $p->{'value'} = $list_of_scenario;

            if ($restrict) {
                _restrict_values($p->{'value'}, $constraint);
            }

        } elsif ($struct->{'task'}) {
            $p_glob->{'type'} = 'task';
            my $list_of_task =
                $list->load_task_list($struct->{'task'}, $robot);

            $list_of_task->{$d->{'name'}}{'selected'} = 1;

            $p->{'value'} = $list_of_task;

            if ($restrict) {
                _restrict_values($p->{'value'}, $constraint);
            }

        } elsif ($struct->{'datasource'}) {
            $p_glob->{'type'} = 'datasource';
            my $list_of_data_sources = $list->load_data_sources_list($robot);

            $list_of_data_sources->{$d}{'selected'} = 1;

            $p->{'value'} = $list_of_data_sources;

            if ($restrict) {
                _restrict_values($p->{'value'}, $constraint);
            }

        } elsif (ref($struct->{'format'}) eq 'HASH') {
            $p_glob->{'type'} = 'paragraph';
            unless (ref($d) eq 'HASH') {
                $d = {};
            }

            $p->{is_empty} = delete $d->{is_empty};
            foreach my $k (
                sort {
                    $struct->{'format'}{$a}{'order'} <=> $struct->{'format'}
                        {$b}{'order'}
                }
                keys %{$struct->{'format'}}
                ) {
                ## Prepare data recursively
                my $m_e =
                    $list->may_edit("$name.$k", $param->{'user'}{'email'});
                my $v = _prepare_data($k, $struct->{'format'}{$k},
                    $d->{$k}, $m_e, $family, $name);
                next unless defined $v;
                push @{$p->{'value'}}, $v;
            }

        } elsif ((ref($struct->{'format'}) eq 'ARRAY')
            || ($restrict && ($main_p eq 'msg_topic' && $name eq 'keywords')))
        {
            $p_glob->{'type'} = 'enum';

            unless (defined $p_glob->{'value'}) {
                ## Initialize
                foreach my $elt (@{$struct->{'format'}}) {
                    $p_glob->{'value'}{$elt}{'selected'} = 0;
                }

                ## Check obsolete values ; they should not be printed
                if (defined $struct->{'obsolete_values'}) {
                    foreach my $elt (@{$struct->{'obsolete_values'}}) {
                        delete $p_glob->{'value'}{$elt};
                    }
                }
            }
            if (ref($d)) {
                next unless (ref($d) eq 'ARRAY');
                foreach my $v (@{$d}) {
                    $p_glob->{'value'}{$v}{'selected'} = 1;
                }
            } else {
                $p_glob->{'value'}{$d}{'selected'} = 1 if (defined $d);
            }

            if ($restrict) {
                _restrict_values($p_glob->{'value'}, $constraint);
            }

        } else {
            if ($restrict && ($name ne 'topics')) {
                $p_glob->{'type'} = 'enum';

                foreach my $elt (keys %{$constraint}) {
                    $elt =~ s/"/&quot;/;
                    $p->{'value'}{$elt}{'selected'} = 0;
                }

                $d =~ s/"/&quot;/;
                $p->{'value'}{$d}{'selected'} = 1;
                $p->{'length'} = $struct->{'length'};
                $p->{'unit'} = $language->gettext($struct->{'gettext_unit'});

            } else {

                $d =~ s/"/&quot;/;
                $p_glob->{'type'}  = 'scalar';
                $p->{'value'}      = $d;
                $p->{'length'}     = $struct->{'length'};
                $p->{'field_type'} = $struct->{'field_type'};
                my $l = length($p->{'value'});
                $p->{'hidden_field'} = '*' x $l;
                $p->{'unit'} = $language->gettext($struct->{'gettext_unit'});
                if ($restrict) {    # for topics
                    $p_glob->{'constraint'} = $constraint;
                }
            }
        }

        push @all_p, $p;
    }

    if (   ($p_glob->{'occurrence'} eq 'multiple')
        && ($p_glob->{'type'} ne 'enum')) {
        $p_glob->{'value'} = \@all_p;
    } else {
        foreach my $k (keys %{$all_p[0]}) {
            $p_glob->{$k} = $all_p[0]->{$k};
        }
    }

    return $p_glob;
}

## Restrict allowed values in the hash
sub _restrict_values {
    my $values  = shift;    #ref on hash of values
    my $allowed = shift;    #ref on hash of allowed values
    wwslog('debug3', '');

    foreach my $v (keys %{$values}) {
        unless (defined $allowed->{$v}) {
            delete $values->{$v};
        }
    }
}

## NOT USED anymore (expect chinese)
sub do_close_list_request {
    wwslog('info', '');

    if ($list->{'admin'}{'status'} eq 'closed') {
        Sympa::Report::reject_report_web('user', 'already_closed', {},
            $param->{'action'}, $list);
        wwslog('info', 'Already closed');
        return undef;
    }

    return 1;
}

# in order to rename a list you must be list owner and you must be allowed to
# create new list
sub do_rename_list_request {
    wwslog('info', '');

    my $result = Sympa::Scenario::request_action(
        $robot,
        'create_list',
        $param->{'auth_method'},
        {   'sender'      => $param->{'user'}{'email'},
            'remote_host' => $param->{'remote_host'},
            'remote_addr' => $param->{'remote_addr'}
        }
    );
    my $r_action;
    my $reason;
    if (ref($result) eq 'HASH') {
        $r_action = $result->{'action'};
        $reason   = $result->{'reason'};
    }

    unless ($r_action =~ /do_it|listmaster/) {
        Sympa::Report::reject_report_web('auth', $reason, {},
            $param->{'action'}, $list);
        wwslog('info', 'Not owner');
        return undef;
    }

    ## Super listmaster can move a list to another robot
    if (Sympa::is_listmaster('*', $param->{'user'}{'email'})) {
        $param->{'robots'} = {};
        foreach my $r (Sympa::List::get_robots()) {
            if ($r eq $robot) {
                $param->{'robots'}{$r} = 'selected="selected"';
            } else {
                $param->{'robots'}{$r} = '';
            }
        }
    } else {
        delete $param->{'robots'};
    }

    return '1';
}

sub do_copy_list {
    wwslog('info', '(%s, %s)', $in{'new_listname'}, $in{'new_robot'});
    my $success = do_rename_list('copy');
    if ($success == 1) {
        web_db_stat_log(
            list      => $in{'new_listname'},
            operation => 'copy_list',
        );
        return $success;
    }
    return undef;
}

# in order to rename a list you must be list owner and you must be allowed to
# create new list
sub do_rename_list {
    my $mode = shift;

    if ($in{'new_listname'} =~ /[A-Z]/) {
        $in{'new_listname'} = lc($in{'new_listname'});
        Sympa::Report::notice_report_web('listname_lowercased', {},
            $param->{'action'});
    }

    wwslog('info', '(%s, %s, mode = %s)',
        $in{'new_listname'}, $in{'new_robot'}, $mode);

    # Don't rename the list included in other list.
    unless ($mode and $mode eq 'copy') {
        if ($list->is_included) {
            Sympa::Report::reject_report_web('user', 'cannot_rename_list',
                {reason => 'included'},
                $param->{'action'}, $list);
            wwslog('Cannot rename list %s: Included by other list', $list);
            web_db_log(
                {   'status'     => 'error',
                    'error_type' => 'cannot_rename_list',
                }
            );
            return undef;
        }
    }

    my $result = Sympa::Admin::rename_list(
        list         => $list,
        new_listname => $in{'new_listname'},
        new_robot    => $in{'new_robot'},
        mode         => $mode,
        auth_method  => $param->{'auth_method'},
        user_email   => $param->{'user'}{'email'},
        remote_host  => $param->{'remote_host'},
        remote_addr  => $param->{'remote_addr'},
        aliases      => $param->{'aliases'},
        status       => $param->{'status'},
    );

    if ($result eq 'incorrect_listname') {
        Sympa::Report::reject_report_web('user', 'incorrect_listname',
            {'bad_listname' => $in{'new_listname'}},
            $param->{'action'}, $list);
        wwslog('info', 'Incorrect listname %s', $in{'new_listname'});
        web_db_log(
            {   'parameters' => "$in{'new_listname'},$in{'new_robot'}",
                'status'     => 'error',
                'error_type' => 'incorrect_listname'
            }
        );
        return 'rename_list_request';

    } elsif ($result eq 'authorization') {
        Sympa::Report::reject_report_web('auth', 'authorization', {},
            $param->{'action'}, $list);
        wwslog('info', 'Not owner');
        web_db_log(
            {   'parameters' => "$in{'new_listname'},$in{'new_robot'}",
                'status'     => 'error',
                'error_type' => 'authorization'
            }
        );
        return undef;

    } elsif ($result eq 'internal') {
        Sympa::Report::reject_report_web(
            'intern',
            'unable_to_rename_list',
            {'new_listname' => $in{'new_listname'}},
            $param->{'action'},
            $list,
            $param->{'user'}{'email'},
            $robot
        );
        wwslog('err', 'Can\'t rename list %s to %s@%s',
            $list, $in{'new_listname'}, $in{'new_robot'});
        web_db_log(
            {   'parameters' => "$in{'new_listname'},$in{'new_robot'}",
                'status'     => 'error',
                'error_type' => 'internal'
            }
        );
        return undef;

    } elsif ($result eq 'list_already_exists') {
        Sympa::Report::reject_report_web('user', 'list_already_exists',
            {'new_listname' => $in{'new_listname'}},
            $param->{'action'}, $list);
        wwslog(
            'info',
            'Could not rename list %s for %s: new list %s already existing list',
            $in{'listname'},
            $param->{'user'}{'email'},
            $in{'new_listname'}
        );
        web_db_log(
            {   'parameters' => "$in{'new_listname'},$in{'new_robot'}",
                'status'     => 'error',
                'error_type' => 'list_already_exists'
            }
        );
        return undef;

    } elsif ($result eq 'incorrect_listname') {
        Sympa::Report::reject_report_web('user', 'listname_matches_aliases',
            {'new_listname' => $in{'new_listname'}},
            $param->{'action'}, $list);
        wwslog('info', 'Incorrect listname %s', $in{'new_listname'});
        web_db_log(
            {   'parameters' => "$in{'new_listname'},$in{'new_robot'}",
                'status'     => 'error',
                'error_type' => 'incorrect_listname'
            }
        );
        return 'rename_list_request';
    } elsif ($result eq 'unknown_robot') {
        wwslog('info', 'Unknown robot %s', $in{'new_robot'});
        Sympa::Report::reject_report_web('user', 'unknown_robot',
            {'new_robot' => $in{'new_robot'}},
            $param->{'action'}, $list);
        web_db_log(
            {   'parameters' => "$in{'new_listname'},$in{'new_robot'}",
                'status'     => 'error',
                'error_type' => 'unknown_robot'
            }
        );
        return undef;
    }

    ## Were aliases installed?
    if ($param->{'aliases'} == 1) {
        $param->{'auto_aliases'} = 1;
    } else {
        $param->{'auto_aliases'} = 0;
    }

    # set list status to pending if creation list is moderated
    if ($param->{'status'} eq 'pending') {
        Sympa::Report::notice_report_web('pending_list', {},
            $param->{'action'}, $list);
    }

    my $new_list = Sympa::List->new($in{'new_listname'}, $in{'new_robot'});
    if ($in{'new_robot'} eq $robot) {
        $param->{'redirect_to'} = Sympa::get_url(
            $new_list, 'admin',
            nomenu    => $param->{'nomenu'},
            authority => 'local'
        );
    } else {
        $param->{'redirect_to'} =
            Sympa::get_url($new_list, 'admin', nomenu => $param->{'nomenu'});
    }

    $param->{'list'} = $in{'new_listname'};
    web_db_log(
        {   'parameters' => "$in{'new_listname'},$in{'new_robot'}",
            'status'     => 'success'
        }
    );
    $list->save_config($param->{'user'}{'email'});
    return 1;
}

sub do_purge_list {
    wwslog('info', '');

    my @lists = split /\0/, $in{'selected_lists'};

    foreach my $l (@lists) {
        my $list = Sympa::List->new($l, $robot);
        next unless (defined $list);
        $list->purge($param->{'user'}{'email'});
    }

    Sympa::Report::notice_report_web('performed', {}, $param->{'action'});
    web_db_log(
        {   'parameters' => $in{'selected_lists'},
            'status'     => 'success'
        }
    );

    return 'get_closed_lists';
}

sub do_close_list {
    wwslog('info', '(%s)', $list->{'name'});

    if ($list->{'admin'}{'status'} eq 'closed') {
        Sympa::Report::reject_report_web('user', 'already_closed', {},
            $param->{'action'}, $list);
        wwslog('info', 'Already closed');
        web_db_log(
            {   'status'     => 'error',
                'error_type' => 'already_closed'
            }
        );
        return undef;
    } elsif ($list->is_included) {
        Sympa::Report::reject_report_web('user', 'cannot_close_list',
            {reason => 'included'},
            $param->{'action'}, $list);
        wwslog('info', 'Cannot close list %s: Included by other list', $list);
        web_db_log(
            {   'status'     => 'error',
                'error_type' => 'cannot_close_list'
            }
        );
        return 'admin';
    } elsif ($list->{'admin'}{'status'} eq 'pending') {
        wwslog('info', 'Closing a pending list makes it purged');
        $list->purge($param->{'user'}{'email'});
        Sympa::Report::notice_report_web('list_purged', {},
            $param->{'action'});
        web_db_log({'status' => 'success'});

        web_db_stat_log();

        return 'home';
    } else {
        unless ($list->close_list($param->{'user'}{'email'})) {
            Sympa::Report::reject_report_web('intern', 'cannot_close_list',
                {}, $param->{'action'}, $list, $param->{'user'}{'email'},
                $robot);
            wwslog('err', 'Cannot close list %s', $list->get_list_id());
            web_db_log(
                {   'status'     => 'error',
                    'error_type' => 'unknown'
                }
            );
        }

        Sympa::Report::notice_report_web('list_closed', {},
            $param->{'action'});
        web_db_log({'status' => 'success'});

        return 'admin';
    }

}

sub do_restore_list {
    wwslog('info', '');

    unless ($list->{'admin'}{'status'} eq 'closed') {
        Sympa::Report::reject_report_web('user', 'not_closed', {},
            $param->{'action'}, $list);
        wwslog('info', 'List not closed');
        web_db_log(
            {   'status'     => 'error',
                'error_type' => 'not_closed'
            }
        );
        return undef;
    }

    ## Change status & save config
    $list->{'admin'}{'status'} = 'open';
    $list->save_config($param->{'user'}{'email'});

    unless (-f $list->{'dir'} . '/subscribers.closed.dump') {
        wwslog('notice', 'No subscribers to restore');
        web_db_log(
            {   'status'     => 'error',
                'error_type' => 'no_subscribers'
            }
        );
    }
    my @users = Sympa::List::_load_list_members_file(
        $list->{'dir'} . '/subscribers.closed.dump');
    ## Insert users in database
    foreach my $user (@users) {
        $list->add_list_member($user);
    }

    $list->savestats();

    my $aliases = Sympa::Admin::install_aliases($list);
    if ($aliases == 1) {
        $param->{'auto_aliases'} = 1;
    } else {
        $param->{'aliases'}      = $aliases;
        $param->{'auto_aliases'} = 0;
    }

    Sympa::Report::notice_report_web('list_restored', {}, $param->{'action'});
    web_db_log({'status' => 'success'});

    web_db_stat_log();

    return 'admin';
}

# Moved to Sympa::Tools::WWW::get_desc_file().
#sub get_desc_file ($file, $ligne);

sub do_show_cert {
    return 1;
}

## Function synchronize
## Return true if the file in parameter can be overwrited
## false if it has changes since the parameter date_epoch
sub synchronize {
    # args : 'path' , 'date_epoch'
    my $path       = shift;
    my $date_epoch = shift;

    return $date_epoch == Sympa::Tools::File::get_mtime($path);
}

#*******************************************
# Function : d_access_control
# Description : return a hash with privileges
#               in read, edit, control
#               if first parameter require
#               it
#******************************************

## Regulars
#  read(/) = default (config list)
#  edit(/) = default (config list)
#  control(/) = not defined
#  read(A/B)= (read(A) && read(B)) ||
#             (author(A) || author(B))
#  edit = idem read
#  control (A/B) : author(A) || author(B)
#  + (set owner A/B) if (empty directory &&
#                        control A)

sub d_access_control {
    # Arguments:
    # (\%mode,$path)
    # if mode->{'read'} control access only for read
    # if mode->{'edit'} control access only for edit
    # if mode->{'control'} control access only for control

    # return the hash (
    # $result{'may'}{'read'} == $result{'may'}{'edit'} == $result{'may'}{'control'}  if is_author else :
    # $result{'may'}{'read'} = 0 or 1 (right or not)
    # $result{'may'}{'edit'} = 0(not may edit) or 0.5(may edit with moderation) or 1(may edit ) : it is not a boolean anymore
    # $result{'may'}{'control'} = 0 or 1 (right or not)
    # $result{'reason'}{'read'} = string for authorization_reject.tt2 when may_read == 0
    # $result{'reason'}{'edit'} = string for authorization_reject.tt2 when may_edit == 0
    # $result{'scenario'}{'read'} = scenario name for the document
    # $result{'scenario'}{'edit'} = scenario name for the document

    # Result
    my %result;
    $result{'reason'} = {};

    # Control

    # Arguments
    my $mode = shift;
    my $path = shift;

    wwslog('debug', '(%s, %s)', join('/', %$mode), $path);

    my $mode_read    = $mode->{'read'};
    my $mode_edit    = $mode->{'edit'};
    my $mode_control = $mode->{'control'};

    # Useful parameters
    my $list_name = $list->{'name'};
    my $shareddir = $list->{'dir'} . '/shared';

    # document to read
    my $doc;
    if ($path) {
        # the path must have no slash a its end
        $path =~ /^(.*[^\/])?(\/*)$/;
        $path = $1;
        $doc  = $shareddir . '/' . $path;
    } else {
        $doc = $shareddir;
    }

    # Control for editing
    my $may_read     = 1;
    my $why_not_read = '';
    my $may_edit     = 1;
    my $why_not_edit = '';
    my $is_author    = 0;    # <=> $may_control

    ## First check privileges on the root shared directory
    $result{'scenario'}{'read'} =
        $list->{'admin'}{'shared_doc'}{'d_read'}{'name'};
    $result{'scenario'}{'edit'} =
        $list->{'admin'}{'shared_doc'}{'d_edit'}{'name'};

    ## Privileged owner has all privileges
    if ($param->{'is_privileged_owner'}) {
        $result{'may'}{'read'}    = 1;
        $result{'may'}{'edit'}    = 1;
        $result{'may'}{'control'} = 1;
        return %result;
    }

    # if not privileged owner
    if ($mode_read) {
        my $result = Sympa::Scenario::request_action(
            $list,
            'shared_doc.d_read',
            $param->{'auth_method'},
            {   'sender'      => $param->{'user'}{'email'},
                'remote_host' => $param->{'remote_host'},
                'remote_addr' => $param->{'remote_addr'}
            }
        );
        my $action;
        if (ref($result) eq 'HASH') {
            $action       = $result->{'action'};
            $why_not_read = $result->{'reason'};
        }

        $may_read = ($action =~ /do_it/i);
    }

    if ($mode_edit) {
        my $result = Sympa::Scenario::request_action(
            $list,
            'shared_doc.d_edit',
            $param->{'auth_method'},
            {   'sender'      => $param->{'user'}{'email'},
                'remote_host' => $param->{'remote_host'},
                'remote_addr' => $param->{'remote_addr'}
            }
        );
        my $action;
        if (ref($result) eq 'HASH') {
            $action       = $result->{'action'};
            $why_not_edit = $result->{'reason'};
        }

        #edit = 0, 0.5 or 1
        $may_edit = Sympa::Tools::WWW::find_edit_mode($action);
        $why_not_edit = '' if ($may_edit);
    }

    ## Only authenticated users can edit files
    unless ($param->{'user'}{'email'}) {
        $may_edit     = 0;
        $why_not_edit = 'not_authenticated';
    }

#     if ($mode_control) {
#	 $result{'may'}{'control'} = 0;
#     }

    my $current_path = $path;
    my $current_document;
    my %desc_hash;
    my $user = $param->{'user'}{'email'} || 'nobody';

    while ($current_path ne "" && $current_path ne '/') {
        # no description file found yet
        my $def_desc_file = 0;
        my $desc_file;

        $current_path =~ /^(([^\/]*\/)*)([^\/]+)(\/?)$/;
        $current_document = $3;
        my $next_path = $1;

        # opening of the description file appropriated
        if (-d $shareddir . '/' . $current_path) {
            # case directory

            #		unless ($slash) {
            $current_path = $current_path . '/';
            #		}

            if (-e "$shareddir/$current_path.desc") {
                $desc_file     = $shareddir . '/' . $current_path . ".desc";
                $def_desc_file = 1;
            }

        } else {
            # case file
            if (-e "$shareddir/$next_path.desc.$3") {
                $desc_file = $shareddir . '/' . $next_path . ".desc." . $3;
                $def_desc_file = 1;
            }
        }

        if ($def_desc_file) {
            # a description file was found
            # loading of acces information

            %desc_hash = Sympa::Tools::WWW::get_desc_file($desc_file);

            if ($mode_read) {
                my $result = Sympa::Scenario::request_action(
                    $list,
                    'shared_doc.d_read',
                    $param->{'auth_method'},
                    {   'sender'      => $param->{'user'}{'email'},
                        'remote_host' => $param->{'remote_host'},
                        'remote_addr' => $param->{'remote_addr'},
                        'scenario'    => $desc_hash{'read'}
                    }
                );
                my $action;
                if (ref($result) eq 'HASH') {
                    $action       = $result->{'action'};
                    $why_not_read = $result->{'reason'};
                }

                $may_read = $may_read && ($action =~ /do_it/i);
                $why_not_read = '' if ($may_read);
            }

            if ($mode_edit) {
                my $result = Sympa::Scenario::request_action(
                    $list,
                    'shared_doc.d_edit',
                    $param->{'auth_method'},
                    {   'sender'      => $param->{'user'}{'email'},
                        'remote_host' => $param->{'remote_host'},
                        'remote_addr' => $param->{'remote_addr'},
                        'scenario'    => $desc_hash{'edit'}
                    }
                );
                my $action_edit;
                if (ref($result) eq 'HASH') {
                    $action_edit  = $result->{'action'};
                    $why_not_edit = $result->{'reason'};
                }

                # $may_edit = 0, 0.5 or 1
                my $may_action_edit =
                    Sympa::Tools::WWW::find_edit_mode($action_edit);
                $may_edit = Sympa::Tools::WWW::merge_edit($may_edit,
                    $may_action_edit);
                $why_not_edit = '' if ($may_edit);

            }

            ## Only authenticated users can edit files
            unless ($param->{'user'}{'email'}) {
                $may_edit     = 0;
                $why_not_edit = 'not_authenticated';
            }

            $is_author = $is_author || ($user eq $desc_hash{'email'});

            unless (defined $result{'scenario'}{'read'}) {
                $result{'scenario'}{'read'} = $desc_hash{'read'};
                $result{'scenario'}{'edit'} = $desc_hash{'edit'};
            }

            ## Author has all privileges
            if ($is_author) {
                $result{'may'}{'read'}    = 1;
                $result{'may'}{'edit'}    = 1;
                $result{'may'}{'control'} = 1;
                return %result;
            }

        }

        # truncate the path for the while
        $current_path = $next_path;
    }

    if ($mode_read) {
        $result{'may'}{'read'}    = $may_read;
        $result{'reason'}{'read'} = $why_not_read;
    }

    if ($mode_edit) {
        $result{'may'}{'edit'}    = $may_edit;
        $result{'reason'}{'edit'} = $why_not_edit;
    }

#     if ($mode_control) {
#	 $result{'may'}{'control'} = 0;
#     }

    return %result;
}

# create the root shared document
sub do_d_admin {
    wwslog('info', '(%s, %s)', $in{'list'}, $in{'d_admin'});

    my %mode;
    $mode{'edit'} = 1;
    my %access = d_access_control(\%mode, $in{'path'});

    my $dir = $list->{'dir'};

    unless ($access{'may'}{'edit'}) {
        wwslog('info', 'Permission denied for %s', $param->{'user'}{'email'});
        Sympa::Report::reject_report_web('auth', $access{'reason'}{'edit'},
            {}, $param->{'action'}, $list);
        web_db_log(
            {   'parameters' => $in{'path'},
                'status'     => 'error',
                'error_type' => 'authorization'
            }
        );
        return undef;
    }

    if ($in{'d_admin'} eq 'create') {

        unless ($list->create_shared()) {
            wwslog('info', 'Could not create the shared');
            Sympa::Report::reject_report_web('intern', 'create_shared', {},
                $param->{'action'}, $list, $param->{'user'}{'email'}, $robot);
            web_db_log(
                {   'parameters' => $in{'path'},
                    'status'     => 'error',
                    'error_type' => 'internal'
                }
            );
            return undef;
        }

        return 'd_read';

    } elsif ($in{'d_admin'} eq 'restore') {
        unless (-e "$dir/pending.shared") {
            wwslog('info', 'Restore; %s/pending.shared not found', $dir);
            Sympa::Report::reject_report_web('intern', 'restore_shared', {},
                $param->{'action'}, $list, $param->{'user'}{'email'}, $robot);
            web_db_log(
                {   'parameters' => $in{'path'},
                    'status'     => 'error',
                    'error_type' => 'internal'
                }
            );
            return undef;
        }
        if (-e "$dir/shared") {
            wwslog('info', 'Restore; %s/shared already exist', $dir);
            Sympa::Report::reject_report_web('intern', 'restore_shared', {},
                $param->{'action'}, $list, $param->{'user'}{'email'}, $robot);
            web_db_log(
                {   'parameters' => $in{'path'},
                    'status'     => 'error',
                    'error_type' => 'internal'
                }
            );
            return undef;
        }
        unless (rename("$dir/pending.shared", "$dir/shared")) {
            wwslog('info', 'Restore; unable to rename %s/pending.shared',
                $dir);
            Sympa::Report::reject_report_web('intern', 'restore_shared', {},
                $param->{'action'}, $list, $param->{'user'}{'email'}, $robot);
            web_db_log(
                {   'parameters' => $in{'path'},
                    'status'     => 'error',
                    'error_type' => 'internal'
                }
            );
            return undef;
        }

        web_db_log(
            {   'parameters' => $in{'path'},
                'status'     => 'success'
            }
        );
        return 'd_read';
    } elsif ($in{'d_admin'} eq 'delete') {
        unless (-e "$dir/shared") {
            wwslog('info', 'Restore; %s/shared not found', $dir);
            Sympa::Report::reject_report_web('intern', 'delete_shared', {},
                $param->{'action'}, $list, $param->{'user'}{'email'}, $robot);
            web_db_log(
                {   'parameters' => $in{'path'},
                    'status'     => 'error',
                    'error_type' => 'internal'
                }
            );
            return undef;
        }
        if (-e "$dir/pending.shared") {
            wwslog('info', 'Delete; %s/pending.shared already exist', $dir);
            Sympa::Report::reject_report_web('intern', 'delete_shared', {},
                $param->{'action'}, $list, $param->{'user'}{'email'}, $robot);
            web_db_log(
                {   'parameters' => $in{'path'},
                    'status'     => 'error',
                    'error_type' => 'internal'
                }
            );
            return undef;
        }
        unless (rename("$dir/shared", "$dir/pending.shared")) {
            wwslog('info', 'Restore; unable to rename %s/shared', $dir);
            Sympa::Report::reject_report_web('intern', 'delete_shared', {},
                $param->{'action'}, $list, $param->{'user'}{'email'}, $robot);
            web_db_log(
                {   'parameters' => $in{'path'},
                    'status'     => 'error',
                    'error_type' => 'internal'
                }
            );
            return undef;
        }
    }
    web_db_log(
        {   'parameters' => $in{'path'},
            'status'     => 'success'
        }
    );
    return 'admin';
}

# Function which sorts a hash of documents
# Sort by various parameters
sub by_order {
    my $order = shift;
    my $hash  = shift;
    # $order =
    # 'order_by_size'/'order_by_doc'/'order_by_author'/'order_by_date'

    if ($order eq 'order_by_doc') {
        $hash->{$a}{'doc'} cmp $hash->{$b}{'doc'}
            or $hash->{$b}{'date_epoch'} <=> $hash->{$a}{'date_epoch'};
    } elsif ($order eq 'order_by_author') {
        $hash->{$a}{'author'} cmp $hash->{$b}{'author'}
            or $hash->{$b}{'date_epoch'} <=> $hash->{$a}{'date_epoch'};
    } elsif ($order eq 'order_by_size') {
        $hash->{$a}{'size'} <=> $hash->{$b}{'size'}
            or $hash->{$b}{'date_epoch'} <=> $hash->{$a}{'date_epoch'};
    } elsif ($order eq 'order_by_date') {
        $hash->{$b}{'date_epoch'} <=> $hash->{$a}{'date_epoch'} or $a cmp $b;
    }

    else {
        $a cmp $b;
    }
}

#*******************************************
# Function : do_d_read
# Description : reads a file or a directory
#******************************************
##
## Function do_d_read
sub do_d_read {
    wwslog('info', '(%s)', $in{'path'});

    ### Useful variables

    # current list / current shared directory
    my $list_name = $list->{'name'};

    # relative path / directory shared of the document
    my $path = Sympa::Tools::WWW::no_slash_end($in{'path'});

    # moderation
    my $visible_path = Sympa::Tools::WWW::make_visible_path($path);

    # path of the shared directory
    my $shareddir = $list->{'dir'} . '/shared';

    # document to read
    my $doc;
    if ($path) {
        $doc = $shareddir . '/' . $path;
    } else {
        $doc = $shareddir;
    }

    ### is list open ?
    unless ($list->{'admin'}{'status'} eq 'open') {
        Sympa::Report::reject_report_web('user', 'list_not_open',
            {'status' => $list->{'admin'}{'status'}},
            $param->{'action'}, $list);
        wwslog(
            'err',
            'Access denied for %s because list is not open',
            $param->{'user'}{'email'}
        );
        web_db_log(
            {   'parameters' => $in{'path'},
                'status'     => 'error',
                'error_type' => 'authorization'
            }
        );
        return undef;
    }

    ### Document exists ?
    unless (-r "$doc") {
        wwslog('err',
            "do_d_read : unable to read $shareddir/$path : no such file or directory"
        );
        Sympa::Report::reject_report_web('user', 'no_such_document',
            {'path' => $visible_path},
            $param->{'action'}, $list);
        web_db_log(
            {   'parameters' => $in{'path'},
                'status'     => 'error',
                'error_type' => 'internal'
            }
        );
        return undef;
    }

    ### Document has non-size zero?
    unless (-s "$doc") {
        wwslog('err', 'Unable to read %s/%s: empty document',
            $shareddir, $path);
        Sympa::Report::reject_report_web('user', 'empty_document',
            {'path' => $visible_path},
            $param->{'action'}, $list);
        web_db_log(
            {   'parameters' => $in{'path'},
                'status'     => 'error',
                'error_type' => 'internal'
            }
        );
        return undef;
    }

    ### Document isn't a description file
    unless ($path !~ /\.desc/) {
        wwslog('err', '%s/%s: description file', $shareddir, $path);
        Sympa::Report::reject_report_web('user', 'no_such_document',
            {'path' => $visible_path},
            $param->{'action'}, $list);
        web_db_log(
            {   'parameters' => $in{'path'},
                'status'     => 'error',
                'error_type' => 'internal'
            }
        );
        return undef;
    }

    ### Access control
    my %mode;
    $mode{'read'}    = 1;
    $mode{'edit'}    = 1;
    $mode{'control'} = 1;
    my %access = d_access_control(\%mode, $path);
    my $may_read = $access{'may'}{'read'};
    unless ($may_read) {
        Sympa::Report::reject_report_web('auth', $access{'reason'}{'read'},
            {}, $param->{'action'}, $list);
        wwslog('err', 'Access denied for %s', $param->{'user'}{'email'});
        web_db_log(
            {   'parameters' => $in{'path'},
                'status'     => 'error',
                'error_type' => 'authorization'
            }
        );
        return undef;
    }

    my $may_edit    = $access{'may'}{'edit'};
    my $may_control = $access{'may'}{'control'};

    # File or directory?

    if (!(-d $doc)) {
        my @tokens = split /\//, $doc;
        my $filename = $tokens[$#tokens];

        ## Jump to the URL
        if ($filename =~ /^\..*\.(\w+)\.moderate$/) {
            $param->{'file_extension'} = $1;
        } elsif ($filename =~ /^.*\.(\w+)$/) {
            $param->{'file_extension'} = $1;
        }

        if ($param->{'file_extension'} eq 'url') {
            open DOC, $doc;
            my $url = <DOC>;
            close DOC;
            chomp $url;
            $param->{'redirect_to'} = $url;
            return 1;
        } else {
            # parameters for the template file
            # view a file
            $param->{'file'}   = $doc;
            $param->{'bypass'} = 1;
            return 1;
        }
    } else {    # directory
        # verification of the URL (the path must have a slash at its end)
        #if ($ENV{'PATH_INFO'} !~ /\/$/) {
        #    $param->{'redirect_to'} = Sympa::get_url($list, 'd_read',
        #        nomenu => $param->{'nomenu'}, authority => 'local');
        #    return 1;
        #}

        ## parameters of the current directory
        if ($path && (-e "$doc/.desc")) {
            my %desc_hash = Sympa::Tools::WWW::get_desc_file("$doc/.desc");
            $param->{'doc_owner'} = $desc_hash{'email'};
            $param->{'doc_title'} = $desc_hash{'title'};
        }
        $param->{'doc_date'} =
            $language->gettext_strftime("%d %b %Y",
            localtime Sympa::Tools::File::get_mtime($doc));

        # listing of all the shared documents of the directory
        unless (opendir DIR, "$doc") {
            Sympa::Report::reject_report_web('intern', 'cannot_open_dir',
                {'dir' => $doc},
                $param->{'action'}, $list, $param->{'user'}{'email'}, $robot);
            wwslog('err', 'Cannot open %s: %s', $doc, $ERRNO);
            web_db_log(
                {   'parameters' => $in{'path'},
                    'status'     => 'error',
                    'error_type' => 'internal'
                }
            );
            return undef;
        }

        # array of entry of the directory DIR
        my @tmpdir = readdir DIR;
        closedir DIR;

        my $dir =
            Sympa::Tools::WWW::get_directory_content(\@tmpdir,
            $param->{'user'}{'email'},
            $list, $doc);

        # empty directory?
        $param->{'empty'} = ($#{$dir} == -1);

        # subdirectories hash
        my %subdirs;
        # file hash
        my %files;

        ## for the exception of index.html
        # name of the file "index.html" if exists in the directory read
        my $indexhtml;

        # boolean : one of the subdirectories or files inside
        # can be edited -> normal mode of read -> d_read.tt2;
        my $normal_mode;

        my $path_doc;
        my %desc_hash;
        my $may, my $def_desc;
        my $user = $param->{'user'}{'email'} || 'nobody';

        foreach my $d (@{$dir}) {

            # current document
            my $path_doc = "$doc/$d";

            #case subdirectory
            if (-d $path_doc) {
                if (-e "$path_doc/.desc") {

                    # check access permission for reading
                    %desc_hash =
                        Sympa::Tools::WWW::get_desc_file("$path_doc/.desc");

                    my $result = Sympa::Scenario::request_action(
                        $list,
                        'shared_doc.d_read',
                        $param->{'auth_method'},
                        {   'sender'      => $param->{'user'}{'email'},
                            'remote_host' => $param->{'remote_host'},
                            'remote_addr' => $param->{'remote_addr'},
                            'scenario'    => $desc_hash{'read'}
                        }
                    );
                    my $action;
                    $action = $result->{'action'} if (ref($result) eq 'HASH');

                    if (   ($user eq $desc_hash{'email'})
                        || ($may_control)
                        || ($action =~ /do_it/i)) {
                        my $date_epoch =
                            Sympa::Tools::File::get_mtime($path_doc);
                        $subdirs{$d}{'date_epoch'} = $date_epoch;
                        $subdirs{$d}{'date'} =
                            $language->gettext_strftime("%d %b %Y",
                            localtime $date_epoch);

                        # Case read authorized : fill the hash
                        $subdirs{$d}{'icon'} =
                            Sympa::Tools::WWW::get_icon($robot, 'folder');

                        $subdirs{$d}{'doc'} =
                            Sympa::Tools::WWW::make_visible_path($d);
                        $subdirs{$d}{'escaped_doc'} =
                            Sympa::SharedDocument::escape_docname($d, '/');

                        # size of the doc
                        $subdirs{$d}{'size'} = (-s $path_doc) / 1000;

                        # description
                        $subdirs{$d}{'title'} = $desc_hash{'title'};
                        $subdirs{$d}{'escaped_title'} =
                            Sympa::Tools::WWW::escape_html_minimum(
                            $desc_hash{'title'});

                        # Author
                        if ($desc_hash{'email'}) {
                            $subdirs{$d}{'author'} = $desc_hash{'email'};
                            $subdirs{$d}{'author_known'} = 1;
                        }

                        # if the file can be read, check for edit access &
                        # edit description files access
                        ## only authenticated users can edit a file

                        if ($param->{'user'}{'email'}) {
                            my $result = Sympa::Scenario::request_action(
                                $list,
                                'shared_doc.d_edit',
                                $param->{'auth_method'},
                                {   'sender' => $param->{'user'}{'email'},
                                    'remote_host' => $param->{'remote_host'},
                                    'remote_addr' => $param->{'remote_addr'},
                                    'scenario'    => $desc_hash{'edit'}
                                }
                            );
                            my $action_edit;
                            $action_edit = $result->{'action'}
                                if (ref($result) eq 'HASH');
                            #may_action_edit = 0, 0.5 or 1
                            my $may_action_edit =
                                Sympa::Tools::WWW::find_edit_mode(
                                $action_edit);
                            $may_action_edit = Sympa::Tools::WWW::merge_edit(
                                $may_action_edit, $may_edit);

                            if ($may_control
                                || ($user eq $desc_hash{'email'})) {

                                $subdirs{$d}{'edit'} =
                                    1;    # or = $may_action_edit ?
                                # if index.html, must know if something can be
                                # edit in the dir
                                $normal_mode = 1;
                            } elsif ($may_action_edit != 0) {
                                # $may_action_edit = 0.5 or 1
                                $subdirs{$d}{'edit'} = $may_action_edit;
                                # if index.html, must know if something can be
                                # edit in the dir
                                $normal_mode = 1;
                            }
                        }

                        if ($may_control || ($user eq $desc_hash{'email'})) {
                            $subdirs{$d}{'control'} = 1;
                        }

                    }
                } else {
                    # no description file = no need to check access for read
                    # access for edit and control

                    if ($may_control) {
                        $subdirs{$d}{'edit'} = 1;    # or = $may_action_edit ?
                        $normal_mode = 1;
                    } elsif ($may_edit != 0) {
                        # $may_action_edit = 1 or 0.5
                        $subdirs{$d}{'edit'} = $may_edit;
                        $normal_mode = 1;
                    }

                    if ($may_control) { $subdirs{$d}{'control'} = 1; }
                }

            } else {
                # case file
                $may      = 1;
                $def_desc = 0;

                if (-e "$doc/.desc.$d") {
                    # a desc file was found
                    $def_desc = 1;

                    # check access permission
                    %desc_hash =
                        Sympa::Tools::WWW::get_desc_file("$doc/.desc.$d");

                    my $result = Sympa::Scenario::request_action(
                        $list,
                        'shared_doc.d_read',
                        $param->{'auth_method'},
                        {   'sender'      => $param->{'user'}{'email'},
                            'remote_host' => $param->{'remote_host'},
                            'remote_addr' => $param->{'remote_addr'},
                            'scenario'    => $desc_hash{'read'}
                        }
                    );
                    my $action;
                    $action = $result->{'action'} if (ref($result) eq 'HASH');
                    unless (($user eq $desc_hash{'email'})
                        || ($may_control)
                        || ($action =~ /do_it/i)) {
                        $may = 0;
                    }
                }

                # if permission or no description file
                if ($may) {
                    $path_doc =~ /^([^\/]*\/)*([^\/]+)\.([^\/]+)$/;

                    ## Bookmark
                    if (   ($path_doc =~ /\.url$/)
                        || ($path_doc =~ /\.url\.moderate$/)) {
                        open DOC, $path_doc;
                        my $url = <DOC>;
                        close DOC;
                        chomp $url;
                        $files{$d}{'url'} = $url;
                        $files{$d}{'anchor'} =
                            Sympa::Tools::WWW::make_visible_path($d);
                        $files{$d}{'icon'} =
                            Sympa::Tools::WWW::get_icon($robot, 'url');

                        ## MIME - TYPES : icons for template
                    } elsif (my $type = Conf::get_mime_type($3)) {
                        # type of the file and apache icon
                        $type =~ /^([\w\-]+)\/([\w\-]+)$/;
                        my $mimet = $1;
                        my $subt  = $2;
                        if ($subt) {
                            if ($subt =~ /^octet-stream$/) {
                                $mimet = 'octet-stream';
                                $subt  = 'binary';
                            }
                            $files{$d}{'type'} = "$subt file";
                        }
                        $files{$d}{'icon'} =
                               Sympa::Tools::WWW::get_icon($robot, $mimet)
                            || Sympa::Tools::WWW::get_icon($robot, 'unknown');
                    } else {
                        # unknown file type
                        $files{$d}{'icon'} =
                            Sympa::Tools::WWW::get_icon($robot, 'unknown');
                    }

                    ## case html
                    if ($3 =~ /^html?$/i) {
                        $files{$d}{'html'} = 1;
                        $files{$d}{'type'} = 'html file';
                        $files{$d}{'icon'} =
                            Sympa::Tools::WWW::get_icon($robot, 'text');
                    }
                    ## exception of index.html
                    if ($d =~ /^(index\.html?)$/i) {
                        $indexhtml = $1;
                    }

                    ## Access control for edit and control
                    if ($def_desc) {
                        # check access for edit and control the file
                        ## Only authenticated users can edit files

                        if ($param->{'user'}{'email'}) {
                            my $result = Sympa::Scenario::request_action(
                                $list,
                                'shared_doc.d_edit',
                                $param->{'auth_method'},
                                {   'sender' => $param->{'user'}{'email'},
                                    'remote_host' => $param->{'remote_host'},
                                    'remote_addr' => $param->{'remote_addr'},
                                    'scenario'    => $desc_hash{'edit'}
                                }
                            );
                            my $action_edit;
                            $action_edit = $result->{'action'}
                                if (ref($result) eq 'HASH');
                            #may_action_edit = 0, 0.5 or 1
                            my $may_action_edit =
                                Sympa::Tools::WWW::find_edit_mode(
                                $action_edit);
                            $may_action_edit = Sympa::Tools::WWW::merge_edit(
                                $may_action_edit, $may_edit);

                            if ($may_control
                                || ($user eq $desc_hash{'email'})) {
                                $normal_mode = 1;
                                $files{$d}{'edit'} =
                                    1;    # or = $may_action_edit ?
                            } elsif ($may_action_edit != 0) {
                                # $may_action_edit = 1 or 0.5
                                $normal_mode = 1;
                                $files{$d}{'edit'} = $may_action_edit;
                            }

                            if (($user eq $desc_hash{'email'})
                                || $may_control) {
                                $files{$d}{'control'} = 1;
                            }

                            # fill the file hash
                            # description of the file
                            $files{$d}{'title'} = $desc_hash{'title'};
                            $files{$d}{'escaped_title'} =
                                Sympa::Tools::WWW::escape_html_minimum(
                                $desc_hash{'title'});
                            # author
                            if ($desc_hash{'email'}) {
                                $files{$d}{'author'} = $desc_hash{'email'};
                                $files{$d}{'author_known'} = 1;
                            }
                        } else {
                            if ($may_edit != 0) {
                                $files{$d}{'edit'} = $may_edit;
                                $normal_mode = 1;
                            }
                            if ($may_control) { $files{$d}{'control'} = 1; }
                        }

                        # name of the file
                        if ($d =~ /^(\.).*(.moderate)$/) {
                            # file not yet moderated can be seen by its author
                            $files{$d}{'doc'} =
                                Sympa::Tools::WWW::make_visible_path($d);
                            $files{$d}{'moderate'} = 1;
                        } else {
                            $files{$d}{'doc'} =
                                Sympa::Tools::WWW::make_visible_path($d);
                        }
                        $files{$d}{'escaped_doc'} =
                            Sympa::SharedDocument::escape_docname($d, '/');

                        # last update
                        my $date_epoch =
                            Sympa::Tools::File::get_mtime($path_doc);
                        $files{$d}{'date_epoch'} = $date_epoch;
                        $files{$d}{'date'} =
                            $language->gettext_strftime("%d %b %Y",
                            localtime $date_epoch);
                        # size
                        $files{$d}{'size'} = (-s $path_doc) / 1000;
                    }
                }
            }

        }

        ### Exception : index.html
        if ($indexhtml) {
            unless ($normal_mode) {
                $param->{'file_extension'} = 'html';
                $param->{'bypass'}         = 1;
                $param->{'file'}           = "$doc/$indexhtml";
                return 1;
            }
        }

        ## to sort subdirs
        my @sort_subdirs;
        my $order = $in{'order'} || 'order_by_doc';
        $param->{'order_by'} = $order;
        foreach my $k (sort { by_order($order, \%subdirs) } keys %subdirs) {
            push @sort_subdirs, $subdirs{$k};
        }

        ## to sort files
        my @sort_files;
        foreach my $k (sort { by_order($order, \%files) } keys %files) {
            push @sort_files, $files{$k};
        }

        # parameters for the template file
        $param->{'list'} = $list_name;

        $param->{'may_edit'}    = $may_edit;
        $param->{'may_control'} = $may_control;

        if ($path) {
            # building of the parent directory path
            if ($path =~ /^(([^\/]*\/)*)([^\/]+)$/) {
                $param->{'father'} = $1;
            } else {
                $param->{'father'} = '';
            }
            $param->{'escaped_father'} =
                Sympa::SharedDocument::escape_docname($param->{'father'},
                '/');

            # Parameters for the description
            if (-e "$doc/.desc") {
                $param->{'serial_desc'} = (stat "$doc/.desc")[9];
                my %desc_hash =
                    Sympa::Tools::WWW::get_desc_file("$doc/.desc");
                $param->{'description'} = $desc_hash{'title'};
            }

            $param->{'path'}         = $path;
            $param->{'visible_path'} = $visible_path;
            $param->{'escaped_path'} =
                Sympa::SharedDocument::escape_docname($param->{'path'}, '/');
        }
        if (scalar keys %subdirs) {
            $param->{'sort_subdirs'} = \@sort_subdirs;
        }
        if (scalar keys %files) {
            $param->{'sort_files'} = \@sort_files;
        }
    }
    $param->{'father_icon'} = Sympa::Tools::WWW::get_icon($robot, 'father');
    $param->{'sort_icon'}   = Sympa::Tools::WWW::get_icon($robot, 'sort');

    ## Show expert commands / user page

    # for the curent directory
    if ($may_edit == 0 && $may_control == 0) {
        $param->{'has_dir_rights'} = 0;
    } else {
        $param->{'has_dir_rights'} = 1;
        if ($may_edit == 1) {    # (is_author || ! moderated)
            $param->{'total_edit'} = 1;
        }
    }

    # set the page mode
    if ($in{'show_expert_page'} && $param->{'has_dir_rights'}) {
        $session->{'shared_mode'} = 'expert';
        if ($param->{'user'}{'prefs'}{'shared_mode'} ne 'expert') {
            # update user pref  as soon as connected user change shared mode
            $param->{'user'}{'prefs'}{'shared_mode'} = 'expert';
            Sympa::User::update_global_user(
                $param->{'user'}{'email'},
                {   data => Sympa::Tools::Data::hash_2_string(
                        $param->{'user'}{'prefs'}
                    )
                }
            );
        }
        $param->{'expert_page'} = 1;

    } elsif ($in{'show_user_page'}) {
        $session->{'shared_mode'} = 'basic';
        if ($param->{'user'}{'prefs'}{'shared_mode'} ne 'basic') {
            # update user pref  as soon as connected user change shared mode
            $param->{'user'}{'prefs'}{'shared_mode'} = 'basic';
            Sympa::User::update_global_user(
                $param->{'user'}{'email'},
                {   data => Sympa::Tools::Data::hash_2_string(
                        $param->{'user'}{'prefs'}
                    )
                }
            );
        }
        $param->{'expert_page'} = 0;
    } else {
        if (   $session->{'shared_mode'} eq 'expert'
            && $param->{'has_dir_rights'}) {
            $param->{'expert_page'} = 1;
        } else {
            $param->{'expert_page'} = 0;
        }
    }

    #open TMP, ">/tmp/dump1";
    #Sympa::Tools::Data::dump_var($param, 0,\*TMP);
    #close TMP;

    web_db_log(
        {   'parameters' => $in{'path'},
            'status'     => 'success'
        }
    );

    return 1;
}

## Access to latest shared documents
sub do_latest_d_read {
    wwslog('info', '(%s, %s, %s)', $in{'list'}, $in{'for'}, $in{'count'});

    ### is list open ?
    unless ($list->{'admin'}{'status'} eq 'open') {
        Sympa::Report::reject_report_web('user', 'list_not_open',
            {'status' => $list->{'admin'}{'status'}},
            $param->{'action'}, $list);
        wwslog(
            'err',
            'Access denied for %s because list is not open',
            $param->{'user'}{'email'}
        );
        web_db_log(
            {   'parameters' => $in{'path'},
                'status'     => 'error',
                'error_type' => 'authorization'
            }
        );
        return undef;
    }

    ### shared exist ?
    my $shareddir = $list->{'dir'} . '/shared';
    unless (-r "$shareddir") {
        wwslog('err',
            "do_latest_d_read : unable to read $shareddir : no such file or directory"
        );
        Sympa::Report::reject_report_web('user', 'no_shared', {},
            $param->{'action'}, $list);
        return undef;
    }

    ### Document has non-size zero?
    unless (-s "$shareddir") {
        wwslog('err', 'Unable to read %s: empty document', $shareddir);
        Sympa::Report::reject_report_web('user', 'shared_empty', {},
            $param->{'action'}, $list);
        return undef;
    }

    ### Access control
    my %mode;
    $mode{'read'}    = 1;
    $mode{'control'} = 1;

    my %access = d_access_control(\%mode, $shareddir);
    unless ($access{'may'}{'read'}) {
        Sympa::Report::reject_report_web('auth', $access{'reason'}{'read'},
            {}, $param->{'action'}, $list);
        wwslog('err', 'Access denied for %s', $param->{'user'}{'email'});
        return undef;
    }

    ## parameters of the query
    my $today = time;

    my $oldest_day;
    if (defined $in{'for'}) {
        $oldest_day = $today - (86400 * ($in{'for'}));
        $param->{'for'} = $in{'for'};
        unless ($oldest_day >= 0) {
            Sympa::Report::reject_report_web('user', 'nb_days_to_much',
                {'nb_days' => $in{'for'}},
                $param->{'action'}, $list);
            wwslog('err', 'Parameter "for" is too big"');
        }
    }

    my $nb_doc;
    my $NB_DOC_MAX = 100;
    if (defined $in{'count'}) {
        if ($in{'count'} > $NB_DOC_MAX) {
            $in{'count'} = $NB_DOC_MAX;
        }
        $param->{'count'} = $in{'count'};
        $nb_doc = $in{'count'};
    } else {
        $nb_doc = $NB_DOC_MAX;
    }

    my $documents;
    unless ($documents =
        directory_browsing('', $oldest_day, $access{'may'}{'control'})) {
        wwslog('err', '(%s) Impossible to browse shared', $list);
        Sympa::Report::reject_report_web('intern', 'browse_shared', {},
            $param->{'action'}, $list, $param->{'user'}{'email'}, $robot);
        return undef;
    }

    @$documents =
        sort ({$b->{'date_epoch'} <=> $a->{'date_epoch'}} @$documents);

    @{$param->{'documents'}} = splice(@$documents, 0, $nb_doc);

    return 1;
}

##  browse a directory recursively and return documents younger than
##  $oldest_day
sub directory_browsing {
    my ($dir, $oldest_day, $may_control) = @_;
    wwslog('debug2', '(%s, %s)', $dir, $oldest_day);

    my @result;
    my $shareddir = $list->{'dir'} . '/shared';
    my $path_dir  = "$shareddir/$dir";

    ## listing of all the shared documents of the directory
    unless (opendir DIR, "$path_dir") {
        wwslog('err', '(%s) Cannot open the directory: %s', $dir, $ERRNO);
        return undef;
    }

    my @tmpdir = readdir DIR;
    closedir DIR;

    # array of file not hidden
    my @directory = grep !/^\./, @tmpdir;

    my $user = $param->{'user'}{'email'} || 'nobody';

    ## browsing
    foreach my $d (@directory) {
        my $path_d = "$path_dir/$d";

        #case subdirectory
        if (-d $path_d) {
            if (-e "$path_d/.desc") {
                # check access permission for reading
                my %desc_hash =
                    Sympa::Tools::WWW::get_desc_file("$path_d/.desc");

                my $result = Sympa::Scenario::request_action(
                    $list,
                    'shared_doc.d_read',
                    $param->{'auth_method'},
                    {   'sender'      => $param->{'user'}{'email'},
                        'remote_host' => $param->{'remote_host'},
                        'remote_addr' => $param->{'remote_addr'},
                        'scenario'    => $desc_hash{'read'}
                    }
                );
                my $action;
                $action = $result->{'action'} if (ref($result) eq 'HASH');
                if (   ($user eq $desc_hash{'email'})
                    || ($may_control)
                    || ($action =~ /do_it/i)) {
                    my $content_d;
                    unless ($content_d =
                        directory_browsing("$dir/$d", $oldest_day)) {
                        wwslog('err',
                            "directory_browsing($dir) : impossible to browse subdirectory $d"
                        );
                        next;
                    }
                    if (ref($content_d) eq "ARRAY") {
                        push @result, @$content_d;
                    }
                }
            }

            #case file
        } else {

            my %file_info;

            ## last update
            my $date_epoch = Sympa::Tools::File::get_mtime($path_d);
            $file_info{'date_epoch'} = $date_epoch;

            if ($file_info{'date_epoch'} < $oldest_day) {
                next;
            }

            $file_info{'last_update'} =
                $language->gettext_strftime("%d %b %Y",
                localtime $date_epoch);

            ## exception of index.html
            if ($d =~ /^(index\.html?)$/i) {
                next;
            }

            my $may      = 1;
            my $def_desc = 0;
            my %desc_hash;

            if (-e "$path_dir/.desc.$d") {
                # a desc file was found
                $def_desc = 1;

                # check access permission
                %desc_hash =
                    Sympa::Tools::WWW::get_desc_file("$path_dir/.desc.$d");

                my $result = Sympa::Scenario::request_action(
                    $list,
                    'shared_doc.d_read',
                    $param->{'auth_method'},
                    {   'sender'      => $param->{'user'}{'email'},
                        'remote_host' => $param->{'remote_host'},
                        'remote_addr' => $param->{'remote_addr'},
                        'scenario'    => $desc_hash{'read'}
                    }
                );
                my $action;
                $action = $result->{'action'} if (ref($result) eq 'HASH');
                unless (($user eq $desc_hash{'email'})
                    || ($may_control)
                    || ($action =~ /do_it/i)) {
                    $may = 0;
                }
            }

            # if permission or no description file
            if ($may) {
                $path_d =~ /^([^\/]*\/)*([^\/]+)\.([^\/]+)$/;

                ## Bookmark
                if ($path_d =~ /\.url$/) {
                    open DOC, $path_d;
                    my $url = <DOC>;
                    close DOC;
                    chomp $url;
                    $file_info{'url'} = $url;
                    $file_info{'anchor'} =
                        Sympa::Tools::WWW::make_visible_path($d);
                    $file_info{'icon'} =
                        Sympa::Tools::WWW::get_icon($robot, 'url');

                    ## MIME - TYPES : icons for template
                } elsif (my $type = Conf::get_mime_type($3)) {
                    # type of the file and apache icon
                    $type =~ /^([\w\-]+)\/([\w\-]+)$/;
                    my $mimet = $1;
                    my $subt  = $2;
                    if ($subt) {
                        if ($subt =~ /^octet-stream$/) {
                            $mimet = 'octet-stream';
                            $subt  = 'binary';
                        }
                    }
                    $file_info{'icon'} =
                           Sympa::Tools::WWW::get_icon($robot, $mimet)
                        || Sympa::Tools::WWW::get_icon($robot, 'unknown');

                    ## UNKNOWN FILE TYPE
                } else {
                    $file_info{'icon'} =
                        Sympa::Tools::WWW::get_icon($robot, 'unknown');
                }

                ## case html
                if ($3 =~ /^html?$/i) {
                    $file_info{'html'} = 1;
                    $file_info{'icon'} =
                        Sympa::Tools::WWW::get_icon($robot, 'text');
                }

                ## name of the file
                $file_info{'name'} = Sympa::Tools::WWW::make_visible_path($d);
                $file_info{'escaped_name'} =
                    Sympa::SharedDocument::escape_docname($d, '/');

                ## content_directory
                if ($dir) {
                    $file_info{'content_dir'} =
                        Sympa::Tools::WWW::make_visible_path($dir);
                } else {
                    $file_info{'content_dir'} = "/";
                }
                $file_info{'escaped_content_dir'} =
                    Sympa::SharedDocument::escape_docname($dir, '/');

                if ($def_desc) {
                    ## description
                    $file_info{'title'} = $desc_hash{'title'};
                    $file_info{'escaped_title'} =
                        Sympa::Tools::WWW::escape_html_minimum(
                        $desc_hash{'title'});

                    ## author
                    if ($desc_hash{'email'}) {
                        $file_info{'author'} = $desc_hash{'email'};
                    }
                }

                push @result, \%file_info;
            }
        }    # else (file)

    }    # foreach

    return \@result;

}

#*******************************************
# Function : do_d_editfile
# Description : prepares the parameters to
#               edit a file
#*******************************************

sub do_d_editfile {
    wwslog('info', '(%s)', $in{'path'});

    # Variables
    my $path = Sympa::Tools::WWW::no_slash_end($in{'path'});

    my $list_name    = $list->{'name'};
    my $shareddir    = $list->{'dir'} . '/shared';
    my $visible_path = Sympa::Tools::WWW::make_visible_path($path);

    $param->{'directory'} = -d "$shareddir/$path";

    # Control

    unless ($path) {
        Sympa::Report::reject_report_web('user', 'missing_arg',
            {'argument' => 'file name'},
            $param->{'action'});
        wwslog('err', 'No file name');
        web_db_log(
            {   'parameters' => $in{'path'},
                'status'     => 'error',
                'error_type' => 'missing_parameter'
            }
        );
        return undef;
    }

    # Existing document? File?
    unless (-w "$shareddir/$path") {
        Sympa::Report::reject_report_web('user', 'no_such_document',
            {'path' => $visible_path},
            $param->{'action'}, $list);
        wwslog('err',
            "d_editfile : Cannot edit $shareddir/$path : not an existing file"
        );
        web_db_log(
            {   'parameters' => $in{'path'},
                'status'     => 'error',
                'error_type' => 'no_file'
            }
        );
        return undef;
    }

    ### Document isn't a description file?
    unless ($path !~ /\.desc/) {
        wwslog('err', '%s/%s: description file', $shareddir, $path);
        Sympa::Report::reject_report_web('user', 'no_such_document',
            {'path' => $visible_path},
            $param->{'action'}, $list);
        web_db_log(
            {   'parameters' => $in{'path'},
                'status'     => 'error',
                'error_type' => 'internal'
            }
        );
        return undef;
    }

    if (($path =~ /\.url$/) || ($path =~ /^\..+\.url.moderate$/)) {
        ## Get URL of bookmark
        open URL, "$shareddir/$path";
        my $url = <URL>;
        close URL;
        chomp $url;

        $param->{'url'} = $url;
        $visible_path =~ s/\.url$//;
    }

    ### is list open ?
    unless ($list->{'admin'}{'status'} eq 'open') {
        Sympa::Report::reject_report_web('user', 'list_not_open',
            {'status' => $list->{'admin'}{'status'}},
            $param->{'action'}, $list);
        wwslog(
            'err',
            'Access denied for %s because list is not open',
            $param->{'user'}{'email'}
        );
        web_db_log(
            {   'parameters' => $in{'path'},
                'status'     => 'error',
                'error_type' => 'authorization'
            }
        );
        return undef;
    }

    # Access control
    my %mode;
    $mode{'edit'} = 1;
    my %access = d_access_control(\%mode, $path);
    my $may_edit = $access{'may'}{'edit'};

    unless ($may_edit > 0) {
        Sympa::Report::reject_report_web('auth', $access{'reason'}{'edit'},
            {}, $param->{'action'}, $list);
        wwslog('err', 'Access denied for %s', $param->{'user'}{'email'});
        web_db_log(
            {   'parameters' => $in{'path'},
                'status'     => 'error',
                'error_type' => 'authorization'
            }
        );
        return undef;
    }

    ## End of controls

    $param->{'list'}         = $list_name;
    $param->{'path'}         = $path;
    $param->{'visible_path'} = $visible_path;

    # test if it's a text file
    if (-T "$shareddir/$path") {
        $param->{'textfile'} = 1;
        $param->{'filepath'} = "$shareddir/$path";
    } else {
        $param->{'textfile'} = 0;
    }
    $param->{'use_htmlarea'} = '1'
        if $Conf::Conf{'htmlarea_url'}
            and $param->{'textfile'}
            and $path =~ /\.html?/;

    #Current directory
    if ($path =~ /^(([^\/]*\/)*)([^\/]+)(\/?)$/) {
        $param->{'father'} = $1;
    } else {
        $param->{'father'} = '';
    }
    $param->{'escaped_father'} =
        Sympa::SharedDocument::escape_docname($param->{'father'}, '/');

    # Description of the file
    my $descfile;
    if (-d "$shareddir/$path") {
        $descfile = "$shareddir/$1$3/.desc";
    } else {
        $descfile = "$shareddir/$1.desc.$3";
    }

    if (-e $descfile) {
        my %desc_hash = Sympa::Tools::WWW::get_desc_file($descfile);
        $param->{'desc'}      = $desc_hash{'title'};
        $param->{'doc_owner'} = $desc_hash{'email'};
        ## Synchronization
        $param->{'serial_desc'} = (stat $descfile)[9];
    }

    ## Synchronization
    my $date_epoch = Sympa::Tools::File::get_mtime("$shareddir/$path");
    $param->{'serial_file'} = $date_epoch;
    ## parameters of the current directory
    $param->{'doc_date'} =
        $language->gettext_strftime("%d %b %y  %H:%M", localtime $date_epoch);

    #FIXME: Required?
    $allow_absolute_path = 1;

    $param->{'father_icon'} = Sympa::Tools::WWW::get_icon($robot, 'father');

    web_db_log(
        {   'parameters' => $in{'path'},
            'status'     => 'success'
        }
    );

    return 1;
}

#*******************************************
# Function : do_d_properties
# Description : prepares the parameters to
#               change a file properties
#*******************************************

sub do_d_properties {
    wwslog('info', '(%s)', $in{'path'});

    # Variables
    my $path = Sympa::Tools::WWW::no_slash_end($in{'path'});

    my $list_name    = $list->{'name'};
    my $shareddir    = $list->{'dir'} . '/shared';
    my $visible_path = Sympa::Tools::WWW::make_visible_path($path);

    $param->{'directory'} = -d "$shareddir/$path";

    # Control

    unless ($path) {
        Sympa::Report::reject_report_web('user', 'missing_arg',
            {'argument' => 'filename'},
            $param->{'action'});
        wwslog('err', 'No file name');
        web_db_log(
            {   'parameters' => $in{'path'},
                'status'     => 'error',
                'error_type' => 'no_file'
            }
        );
        return undef;
    }

    # Existing document? File?
    unless (-w "$shareddir/$path") {
        Sympa::Report::reject_report_web('user', 'no_such_document',
            {'path' => $visible_path},
            $param->{'action'}, $list);
        wwslog('err',
            "do_d_properties : Cannot edit $shareddir/$path : not an existing file"
        );
        web_db_log(
            {   'parameters' => $in{'path'},
                'status'     => 'error',
                'error_type' => 'no_file'
            }
        );
        return undef;
    }

    ### Document isn't a description file?
    unless ($path !~ /\.desc/) {
        wwslog('err', '%s/%s: description file', $shareddir, $path);
        Sympa::Report::reject_report_web('user', 'no_such_document',
            {'path' => $visible_path},
            $param->{'action'}, $list);
        web_db_log(
            {   'parameters' => $in{'path'},
                'status'     => 'error',
                'error_type' => 'internal'
            }
        );
        return undef;
    }

    if ($path =~ /\.url$/) {
        ## Get URL of bookmark
        open URL, "$shareddir/$path";
        my $url = <URL>;
        close URL;
        chomp $url;

        $param->{'url'} = $url;
    }

    # Access control
    my %mode;
    $mode{'edit'} = 1;
    my %access = d_access_control(\%mode, $path);
    my $may_edit = $access{'may'}{'edit'};

    unless ($may_edit > 0) {
        Sympa::Report::reject_report_web('auth', $access{'reason'}{'edit'},
            {}, $param->{'action'}, $list);
        wwslog('err', 'Access denied for %s', $param->{'user'}{'email'});
        web_db_log(
            {   'parameters' => $in{'path'},
                'status'     => 'error',
                'error_type' => 'authorization'
            }
        );
        return undef;
    }

    ## End of controls

    $param->{'list'}         = $list_name;
    $param->{'path'}         = $path;
    $param->{'visible_path'} = $visible_path;

    # test if it's a text file
    if (-T "$shareddir/$path") {
        $param->{'textfile'} = 1;
        $param->{'filepath'} = "$shareddir/$path";
    } else {
        $param->{'textfile'} = 0;
    }
    $param->{'use_htmlarea'} = '1'
        if $Conf::Conf{'htmlarea_url'}
            and $param->{'textfile'}
            and $path =~ /\.html?/;

    #Current directory
    if ($path =~ /^(([^\/]*\/)*)([^\/]+)(\/?)$/) {
        $param->{'father'} = $1;
    } else {
        $param->{'father'} = '';
    }
    $param->{'escaped_father'} =
        Sympa::SharedDocument::escape_docname($param->{'father'}, '/');

    $param->{'fname'} = Sympa::Tools::WWW::make_visible_path($3);
    # Description of the file
    my $descfile;
    if (-d "$shareddir/$path") {
        $descfile = "$shareddir/$1$3/.desc";
    } else {
        $descfile = "$shareddir/$1.desc.$3";
    }

    if (-e $descfile) {
        my %desc_hash = Sympa::Tools::WWW::get_desc_file($descfile);
        $param->{'desc'}      = $desc_hash{'title'};
        $param->{'doc_owner'} = $desc_hash{'email'};
        ## Synchronization
        $param->{'serial_desc'} = (stat $descfile)[9];
    }

    ## Synchronization
    my $date_epoch = Sympa::Tools::File::get_mtime("$shareddir/$path");
    $param->{'serial_file'} = $date_epoch;
    ## parameters of the current directory
    $param->{'doc_date'} =
        $language->gettext_strftime("%d %b %y  %H:%M", localtime $date_epoch);

    #FIXME: Required?
    $allow_absolute_path = 1;

    $param->{'father_icon'} = Sympa::Tools::WWW::get_icon($robot, 'father');

    web_db_log(
        {   'parameters' => $in{'path'},
            'status'     => 'success'
        }
    );

    return 1;
}

#*******************************************
# Function : do_d_describe
# Description : Saves the description of
#               the file
#******************************************

sub do_d_describe {
    wwslog('info', '(%s)', $in{'path'});

    # Variables
    my $path         = Sympa::Tools::WWW::no_slash_end($in{'path'});
    my $visible_path = Sympa::Tools::WWW::make_visible_path($path);
    my $list_name    = $list->{'name'};
    my $shareddir    = $list->{'dir'} . '/shared';

    ####  Controls

    ### Document isn't a description file?
    unless ($path !~ /\.desc/) {
        wwslog('info', '%s/%s: description file', $shareddir, $path);
        Sympa::Report::reject_report_web('user', 'no_such_document',
            {'path' => $visible_path},
            $param->{'action'}, $list);
        web_db_log(
            {   'parameters' => $in{'path'},
                'status'     => 'error',
                'error_type' => 'internal'
            }
        );
        return undef;
    }

    ## the path must not be empty (the description file of the shared
    ## directory
    #  doesn't exist)
    unless ($path) {
        Sympa::Report::reject_report_web(
            'intern',
            'cannot_describe_shared_directory',
            {'path' => $path},
            $param->{'action'}, $list, $param->{'user'}{'email'}, $robot
        );
        wwslog('info', 'Cannot describe %s: root directory', $shareddir);
        web_db_log(
            {   'parameters' => $in{'path'},
                'status'     => 'error',
                'error_type' => 'internal'
            }
        );
        return undef;
    }

    # the file to describe must already exist
    unless (-e "$shareddir/$path") {
        Sympa::Report::reject_report_web('user', 'no_doc_to_describe',
            {'path' => $visible_path},
            $param->{'action'}, $list);
        wwslog('info',
            "d_describe : Unable to describe $shareddir/$path : not an existing document"
        );
        web_db_log(
            {   'parameters' => $in{'path'},
                'status'     => 'error',
                'error_type' => 'no_file'
            }
        );
        return undef;
        in {'shortname'};
    }

    # Access control
    # Access control
    my %mode;
    $mode{'edit'} = 1;
    my %access = d_access_control(\%mode, $path);

    unless ($access{'may'}{'edit'} > 0) {
        Sympa::Report::reject_report_web('auth', $access{'reason'}{'edit'},
            {}, $param->{'action'}, $list);
        wwslog('info', 'Access denied for %s', $param->{'user'}{'email'});
        web_db_log(
            {   'parameters' => $in{'path'},
                'status'     => 'error',
                'error_type' => 'authorization'
            }
        );
        return undef;
    }

    ## End of controls

    if ($in{'content'} !~ /^\s*$/) {

        # Description file
        $path =~ /^(([^\/]*\/)*)([^\/]+)(\/?)$/;
        my $dir  = $1;
        my $file = $3;

        my $desc_file;
        if (-d "$shareddir/$path") {
            $desc_file = "$shareddir/$dir$file/.desc";
        } else {
            $desc_file = "$shareddir/$dir.desc.$file";
        }

        if (-r "$desc_file") {
            # if description file already exists : open it and modify it
            my %desc_hash = Sympa::Tools::WWW::get_desc_file("$desc_file");

            # Synchronization
            unless (synchronize($desc_file, $in{'serial'})) {
                Sympa::Report::reject_report_web('user', 'synchro_failed', {},
                    $param->{'action'}, $list);
                wwslog('info', 'Synchronization failed for %s', $desc_file);
                web_db_log(
                    {   'parameters' => $in{'path'},
                        'status'     => 'error',
                        'error_type' => 'internal'
                    }
                );
                return undef;
            }

            # fill the description file
            unless (open DESC, ">$desc_file") {
                wwslog('info', 'Cannot open %s: %s', $desc_file, $ERRNO);
                Sympa::Report::reject_report_web(
                    'intern', 'cannot_open_file',
                    {'file' => $desc_file}, $param->{'action'},
                    $list, $param->{'user'}{'email'},
                    $robot
                );
                web_db_log(
                    {   'parameters' => $in{'path'},
                        'status'     => 'error',
                        'error_type' => 'internal'
                    }
                );
                return undef;
            }

            # information modified
            print DESC "title\n  $in{'content'}\n\n";
            # information not modified
            print DESC
                "access\n  read $desc_hash{'read'}\n  edit $desc_hash{'edit'}\n\n";
            print DESC "creation\n";
            # time
            print DESC "  date_epoch $desc_hash{'date'}\n";
            # author
            print DESC "  email $desc_hash{'email'}\n\n";

            close DESC;

        } else {
            # Creation of a description file
            unless (open(DESC, ">$desc_file")) {
                Sympa::Report::reject_report_web(
                    'intern', 'cannot_open_file',
                    {'file' => $desc_file}, $param->{'action'},
                    $list, $param->{'user'}{'email'},
                    $robot
                );
                wwslog('info',
                    "d_describe : Cannot create description file $desc_file : $ERRNO"
                );
                web_db_log(
                    {   'parameters' => $in{'path'},
                        'status'     => 'error',
                        'error_type' => 'internal'
                    }
                );
                return undef;
            }
            # fill
            # description
            print DESC "title\n  $in{'content'}\n\n";
            # date and author
            my @info = stat "$shareddir/$path";
            print DESC "creation\n  date_epoch "
                . $info[10]
                . "\n  email\n\n";
            # access rights
            print DESC "access\n";
            print DESC "  read $access{'scenario'}{'read'}\n";
            print DESC "  edit $access{'scenario'}{'edit'}\n\n";

            close DESC;

        }

        $in{'path'} = Sympa::Tools::WWW::no_slash_end($dir);
    }

    web_db_log(
        {   'parameters' => $in{'path'},
            'status'     => 'success'
        }
    );

    return 'd_read';

}

#*******************************************
# Function : do_d_savefile
# Description : Saves a file edited in a
#               text area
#******************************************

sub do_d_savefile {
    wwslog('info', '(%s)', $in{'path'});

    # Variables
    my $path = Sympa::Tools::WWW::no_slash_end($in{'path'});

    if (   $in{'url'}
        && $in{'previous_action'} eq 'd_read') {
        $path .= '/' . $in{'name_doc'} . '.url';
    }

    my $visible_path = Sympa::Tools::WWW::make_visible_path($path);

    my $moderated;
    if ($visible_path ne $path) {
        $moderated = 1;
    }

    #my $list_name = $in{'list'};
    my $list_name = $list->{'name'};

    my $shareddir = $list->{'dir'} . '/shared';

    ####  Controls

    my $creation = 1 unless (-f "$shareddir/$path");

    ### Document isn't a description file
    unless ($path !~ /\.desc/) {
        wwslog('err', '%s/%s: description file', $shareddir, $path);
        Sympa::Report::reject_report_web('user', 'no_such_document',
            {'path' => $visible_path},
            $param->{'action'}, $list);
        web_db_log(
            {   'parameters' => $in{'path'},
                'status'     => 'error',
                'error_type' => 'internal'
            }
        );
        return undef;
    }

    # Access control
    my %mode;
    $mode{'edit'} = 1;
    my %access = d_access_control(\%mode, $path);

    unless ($access{'may'}{'edit'} > 0) {
        Sympa::Report::reject_report_web('auth', $access{'reason'}{'edit'},
            {}, $param->{'action'}, $list);
        wwslog('err', 'Access denied for %s', $param->{'user'}{'email'});
        web_db_log(
            {   'parameters' => $in{'path'},
                'status'     => 'error',
                'error_type' => 'authorization'
            }
        );
        return undef;
    }
    #### End of controls

    if (($in{'content'} =~ /^\s*$/) && ($in{'url'} =~ /^\s*$/)) {
        Sympa::Report::reject_report_web('user', 'no_content', {},
            $param->{'action'}, $list);
        wwslog('err', 'Cannot save file %s/%s: no content', $shareddir,
            $path);
        web_db_log(
            {   'parameters' => $in{'path'},
                'status'     => 'error',
                'error_type' => 'missing_parameter'
            }
        );
        return undef;
    }

    # Synchronization
    unless ($in{'url'}) {    # only for files
        unless (synchronize("$shareddir/$path", $in{'serial'})) {
            Sympa::Report::reject_report_web('user', 'synchro_failed', {},
                $param->{'action'}, $list);
            wwslog('err',
                "do_d_savefile : Synchronization failed for $shareddir/$path"
            );
            web_db_log(
                {   'parameters' => $in{'path'},
                    'status'     => 'error',
                    'error_type' => 'internal'
                }
            );
            return undef;
        }
    }

    # Renaming of the old file
############""" pas les url ?
    rename("$shareddir/$path", "$shareddir/$path.old")
        unless ($creation);

    my $dir;
    my $file;
    if ($path =~ /^(([^\/]*\/)*)([^\/]+)(\/?)$/) {
        $dir  = $1;
        $file = $3;
    }

    if ($in{'url'}) {
##############
#	 if ($access{'may'}{'edit'} == 0.5) {
#	     open URL, ">$shareddir/$dir.$file.moderate";
#	 }else {
        open URL, ">$shareddir/$path";
#	 }
        print URL "$in{'url'}\n";
        close URL;
    } else {
        # Creation of the shared file
        unless (open FILE, ">$shareddir/$path") {
            rename("$shareddir/$path.old", "$shareddir/$path");
            Sympa::Report::reject_report_web(
                'user',
                'cannot_overwrite',
                {   'reason' => $1,
                    'path'   => $visible_path
                },
                $param->{'action'},
                $list
            );
            wwslog('err',
                "do_d_savefile : Cannot open for replace $shareddir/$path : $ERRNO"
            );
            web_db_log(
                {   'parameters' => $in{'path'},
                    'status'     => 'error',
                    'error_type' => 'internal'
                }
            );
            return undef;
        }
        print FILE $in{'content'};
        close FILE;
    }

    unlink "$shareddir/$path.old";

    # Description file
    if (-e "$shareddir/$dir.desc.$file") {

        # if description file already exists : open it and modify it
        my %desc_hash =
            Sympa::Tools::WWW::get_desc_file("$shareddir/$dir.desc.$file");

        open DESC, ">$shareddir/$dir.desc.$file";

        # information not modified
        print DESC "title\n  $desc_hash{'title'}\n\n";
        print DESC
            "access\n  read $desc_hash{'read'}\n  edit $desc_hash{'edit'}\n\n";
        print DESC "creation\n";
        # date
        print DESC '  date_epoch ' . $desc_hash{'date'} . "\n";

        # information modified
        # author
        print DESC "  email $param->{'user'}{'email'}\n\n";

        close DESC;

    } else {
        # Creation of a description file if author is known

        unless (open(DESC, ">$shareddir/$dir.desc.$file")) {
            wwslog('info',
                "do_d_savefile: cannot create description file $shareddir/$dir.desc.$file"
            );
            web_db_log(
                {   'parameters' => $in{'path'},
                    'status'     => 'error',
                    'error_type' => 'internal'
                }
            );
        }
        # description
        print DESC "title\n \n\n";
        # date of creation and author
        my @info = stat "$shareddir/$path";
        print DESC "creation\n  date_epoch "
            . $info[10]
            . "\n  email $param->{'user'}{'email'}\n\n";
        # Access
        print DESC "access\n";
        print DESC "  read $access{'scenario'}{'read'}\n";
        print DESC "  edit $access{'scenario'}{'edit'}\n\n";

        close DESC;
    }

    # shared_moderated
#######################
    if (($access{'may'}{'edit'} == 0.5) && ($creation)) {

        unless (rename "$shareddir/$path", "$shareddir/$dir.$file.moderate") {
            Sympa::Report::reject_report_web(
                'intern',
                'rename_file',
                {   'old' => "$shareddir/$path",
                    'new' => "$shareddir/$dir.$file.moderate"
                },
                $param->{'action'},
                $list,
                $param->{'user'}{'email'},
                $robot
            );
            wwslog('err',
                "do_d_savefile : Failed to rename  $path to $dir.$file.moderate : $ERRNO"
            );
            web_db_log(
                {   'parameters' => $in{'path'},
                    'status'     => 'error',
                    'error_type' => 'internal'
                }
            );
        }
        unless (
            rename "$shareddir/$dir.desc.$file",
            "$shareddir/$dir.desc..$file.moderate"
            ) {
            Sympa::Report::reject_report_web(
                'intern',
                'rename_file',
                {   'old' => "$shareddir/$dir.desc.$file",
                    'new' => "$shareddir/$dir.desc..$file.moderate"
                },
                $param->{'action'},
                $list,
                $param->{'user'}{'email'},
                $robot
            );
            wwslog('err',
                "do_d_savefile : Failed to rename $dir.desc.$file to $dir.desc..$file.moderate : $ERRNO"
            );
            web_db_log(
                {   'parameters' => $in{'path'},
                    'status'     => 'error',
                    'error_type' => 'internal'
                }
            );
        }

        if (!$in{'url'}) {
            $in{'path'} = $path;
            $param->{'path'} = $path;
        } else {
            $visible_path = $file;
            $visible_path =~ s/\.url$//;
        }

        $list->send_notify_to_editor(
            'shared_moderated',
            {   'filename' => $visible_path,
                'who'      => $param->{'user'}{'email'}
            }
        );

        Sympa::Report::notice_report_web('to_moderate',
            {'path' => $visible_path},
            $param->{'action'});
    }

    Sympa::Report::notice_report_web('save_success',
        {'path' => $visible_path},
        $param->{'action'});
    web_db_log(
        {   'parameters' => $in{'path'},
            'status'     => 'success'
        }
    );
    if ($in{'previous_action'}) {
        return $in{'previous_action'};
    } else {
        $in{'path'} =~ s/([^\/]+)$//;
        $param->{'path'} =~ s/([^\/]+)$//;
        return 'd_read';
    }
}

#*******************************************
# Function : do_d_overwrite
# Description : Overwrites a file with a
#               uploaded file
#******************************************

sub do_d_overwrite {
    wwslog('info', '(%s)', $in{'path'});

    # Variables
    my $path = Sympa::Tools::WWW::no_slash_end($in{'path'});

    my $visible_path = Sympa::Tools::WWW::make_visible_path($path);

    #my $list_name = $in{'list'};
    my $list_name = $list->{'name'};

    # path of the shared directory
    my $shareddir = $list->{'dir'} . '/shared';

    # Parameters of the uploaded file
    my $fh = $query->upload('uploaded_file');
    my $fn = $query->param('uploaded_file');

    # name of the file
    my $fname;
    if ($fn =~ /([^\/\\]+)$/) {
        $fname = $1;
    }

    ### uploaded file must have a name
    unless ($fname) {
        Sympa::Report::reject_report_web('user', 'missing_arg',
            {'argument' => 'file name'},
            $param->{'action'});
        wwslog('info', 'No file specified to overwrite');
        web_db_log(
            {   'parameters' => $in{'path'},
                'status'     => 'error',
                'error_type' => 'no_file'
            }
        );
        return undef;
    }

    ####### Controls

    ### Document isn't a description file?
    unless ($path !~ /\.desc/) {
        wwslog('err', '%s/%s: description file', $shareddir, $path);
        Sympa::Report::reject_report_web('user', 'no_such_document',
            {'path' => $visible_path},
            $param->{'action'}, $list);
        web_db_log(
            {   'parameters' => $in{'path'},
                'status'     => 'error',
                'error_type' => 'internal'
            }
        );
        return undef;
    }

    # the path to replace must already exist
    unless (-e "$shareddir/$path") {
        Sympa::Report::reject_report_web('user', 'no_such_document',
            {'path' => $visible_path},
            $param->{'action'}, $list);
        wwslog('err',
            "do_d_overwrite : Unable to overwrite $shareddir/$path : not an existing file"
        );
        web_db_log(
            {   'parameters' => $in{'path'},
                'status'     => 'error',
                'error_type' => 'no_file'
            }
        );
        return undef;
    }

    # the path must represent a file
    if (-d "$shareddir/$path") {
        Sympa::Report::reject_report_web('user', 'doc_already_a_dir',
            {'path' => $visible_path},
            $param->{'action'}, $list);
        wwslog('err',
            "do_d_overwrite : Unable to create $shareddir/$path : a directory named $path already exists"
        );
        web_db_log(
            {   'parameters' => $in{'path'},
                'status'     => 'error',
                'error_type' => 'already_exists'
            }
        );
        return undef;
    }

    # Access control
    my %mode;
    $mode{'edit'} = 1;
    my %access = d_access_control(\%mode, $path);

    unless ($access{'may'}{'edit'} > 0) {
        Sympa::Report::reject_report_web('auth', $access{'reason'}{'edit'},
            {}, $param->{'action'}, $list);
        wwslog('err', 'Access denied for %s', $param->{'user'}{'email'});
        web_db_log(
            {   'parameters' => $in{'path'},
                'status'     => 'error',
                'error_type' => 'authorization'
            }
        );
        return undef;
    }
    #### End of controls

    # Synchronization
    unless (synchronize("$shareddir/$path", $in{'serial'})) {
        Sympa::Report::reject_report_web('user', 'synchro_failed', {},
            $param->{'action'}, $list);
        wwslog('err', 'Synchronization failed for %s/%s', $shareddir, $path);
        web_db_log(
            {   'parameters' => $in{'path'},
                'status'     => 'error',
                'error_type' => 'internal'
            }
        );
        return undef;
    }

    # Renaming of the old file
    rename("$shareddir/$path", "$shareddir/$path.old");

    # Creation of the shared file
    unless (open FILE, ">:bytes", "$shareddir/$path") {
        Sympa::Report::reject_report_web(
            'user',
            'cannot_overwrite',
            {   'reason' => $ERRNO,
                'path'   => $visible_path
            },
            $param->{'action'},
            $list
        );
        wwslog('err', 'Cannot open for replace %s/%s: %s',
            $shareddir, $path, $ERRNO);
        web_db_log(
            {   'parameters' => $in{'path'},
                'status'     => 'error',
                'error_type' => 'cannot_overwrite'
            }
        );
        return undef;
    }
    while (<$fh>) {
        print FILE;
    }
    close FILE;

    # Description file
    my ($dir, $file);
    if ($path =~ /^(([^\/]*\/)*)([^\/]+)(\/?)$/) {
        $dir  = $1;
        $file = $3;
    }

    if (-e "$shareddir/$dir.desc.$file") {
        # if description file already exists: open it and modify it
        my %desc_hash =
            Sympa::Tools::WWW::get_desc_file("$shareddir/$dir.desc.$file");

        open DESC, ">$shareddir/$dir.desc.$file";

        # information not modified
        print DESC "title\n  $desc_hash{'title'}\n\n";
        print DESC
            "access\n  read $desc_hash{'read'}\n  edit $desc_hash{'edit'}\n\n";
        print DESC "creation\n";
        # time
        print DESC "  date_epoch $desc_hash{'date'}\n";
        # information modified
        # author
        print DESC "  email $param->{'user'}{'email'}\n\n";

        close DESC;
    } else {
        # Creation of a description file
        unless (open(DESC, ">$shareddir/$dir.desc.$file")) {
            wwslog('info',
                "do_d_overwrite : Cannot create description file $shareddir/$dir.desc.$file"
            );
            web_db_log(
                {   'parameters' => $in{'path'},
                    'status'     => 'error',
                    'error_type' => 'internal'
                }
            );
            return undef;
        }
        # description
        print DESC "title\n  \n\n";
        # date of creation and author
        my @info = stat "$shareddir/$path";
        print DESC "creation\n  date_epoch "
            . $info[10]
            . "\n  email $param->{'user'}{'email'}\n\n";
        # access rights
        print DESC "access\n";
        print DESC "  read $access{'scenario'}{'read'}\n";
        print DESC "  edit $access{'scenario'}{'edit'}\n\n";

        close DESC;

    }

    # shared_moderated
    if (($access{'may'}{'edit'} == 0.5) && ($path eq $visible_path)) {
        unless (rename "$shareddir/$path", "$shareddir/$dir.$file.moderate") {
            Sympa::Report::reject_report_web(
                'intern',
                'rename_file',
                {   'old' => "$shareddir/$path",
                    'new' => "$shareddir/$dir.$file.moderate"
                },
                $param->{'action'},
                $list,
                $param->{'user'}{'email'},
                $robot
            );
            wwslog('err',
                "do_d_overwrite : Failed to rename  $path to $dir.$file.moderate : $ERRNO"
            );
            web_db_log(
                {   'parameters' => $in{'path'},
                    'status'     => 'error',
                    'error_type' => 'internal'
                }
            );
            return undef;
        }
        unless (
            rename "$shareddir/$dir.desc.$file",
            "$shareddir/$dir.desc..$file.moderate"
            ) {
            Sympa::Report::reject_report_web(
                'intern',
                'rename_file',
                {   'old' => "$shareddir/$dir.desc.$file",
                    'new' => "$shareddir/$dir.desc..$file.moderate"
                },
                $param->{'action'},
                $list,
                $param->{'user'}{'email'},
                $robot
            );
            wwslog('err',
                "do_d_overwrite : Failed to rename $dir.desc.$file to $dir.desc..$file.moderate : $ERRNO"
            );
            web_db_log(
                {   'parameters' => $in{'path'},
                    'status'     => 'error',
                    'error_type' => 'internal'
                }
            );
        }
        $list->send_notify_to_editor(
            'shared_moderated',
            {   'filename' => $visible_path,
                'who'      => $param->{'user'}{'email'}
            }
        );
        $in{'path'} = "$dir.$file.moderate";
        Sympa::Report::notice_report_web('to_moderate',
            {'path' => $visible_path},
            $param->{'action'});
    }

    # Removing of the old file
    unlink "$shareddir/$path.old";

    $in{'list'} = $list_name;
    #$in{'path'} = $dir;

    # message of success
    Sympa::Report::notice_report_web('upload_success',
        {'path' => $visible_path});
    web_db_log(
        {   'parameters' => $in{'path'},
            'status'     => 'success'
        }
    );
    return 'd_editfile';
}

#*******************************************
# Function : do_d_upload
# Description : Creates a new file with a
#               uploaded file
#******************************************

sub do_d_upload {
    # Parameters of the uploaded file (from d_read.tt2)
    my $fn = $in{'uploaded_file'};

    # name of the file, without path
    my ($fname, $visible_fname);
    if ($fn =~ /([^\/\\]+)$/) {
        $fname         = Sympa::Tools::Text::qencode_filename($1);
        $visible_fname = Sympa::Tools::WWW::make_visible_path($fname);
    }

    # param from d_upload.tt2
    if ($in{'shortname'}) {
        $fname = $in{'shortname'};
    }
    wwslog('info', '(%s/%s)', $in{'path'}, $fname);

    # Variables
    my $path         = Sympa::Tools::WWW::no_slash_end($in{'path'});
    my $visible_path = Sympa::Tools::WWW::make_visible_path($path);

    # path of the shared directory
    my $shareddir = $list->{'dir'} . '/shared';

    # name of the file
    my $longname = "$shareddir/$path/$fname";

    $longname =~ s/\/+/\//g;

    ###########
    #Get the size of uploaded file : used in db_stat_log (parameter)
    my @infos = stat $longname;
    my $size  = $infos[7];
    ###########

#     ## $path must have a slash at its end
#     $path = format_path('with_slash',$path);

    #my $list_name = $in{'list'};
    my $list_name = $list->{'name'};

    ## Controls

    # uploaded file must have a name
    unless ($fname) {
        Sympa::Report::reject_report_web('user', 'no_name', {},
            $param->{'action'}, $list);
        wwslog('err', 'No file specified to upload');
        web_db_log(
            {   'robot'        => $robot,
                'list'         => $list->{'name'},
                'action'       => $param->{'action'},
                'parameters'   => "$visible_path,$visible_fname",
                'target_email' => "",
                'msg_id'       => '',
                'status'       => 'error',
                'error_type'   => 'no_file',
                'user_email'   => $param->{'user'}{'email'},
            }
        );
        return undef;
    }

    ## Check quota
    if ($list->{'admin'}{'shared_doc'}{'quota'}) {
        if ($list->get_shared_size() >=
            $list->{'admin'}{'shared_doc'}{'quota'} * 1024) {
            Sympa::Report::reject_report_web('user', 'shared_full', {},
                $param->{'action'}, $list);
            wwslog('err',
                "do_d_upload : Shared Quota exceeded for list $list->{'name'}"
            );
            web_db_log(
                {   'robot'        => $robot,
                    'list'         => $list->{'name'},
                    'action'       => $param->{'action'},
                    'parameters'   => "$visible_path,$visible_fname",
                    'target_email' => "",
                    'msg_id'       => '',
                    'status'       => 'error',
                    'error_type'   => 'shared_full',
                    'user_email'   => $param->{'user'}{'email'},
                }
            );
            return undef;
        }
    }

    # The name of the file must be correct and musn't not be a description
    # file
    if (   $fname =~ /^\./
        || $fname =~ /\.desc/
        || $fname =~ /[~\#\[\]]$/) {

        #    unless ($fname =~ /^\w/ and
        #	    $fname =~ /\w$/ and
        #	    $fname =~ /^[\w\-\.]+$/ and
        #	    $fname !~ /\.desc/) {
        Sympa::Report::reject_report_web('user', 'incorrect_name',
            {'name' => $fname},
            $param->{'action'}, $list);
        wwslog('err', 'Unable to create file %s: incorrect name', $fname);
        web_db_log(
            {   'robot'        => $robot,
                'list'         => $list->{'name'},
                'action'       => $param->{'action'},
                'parameters'   => "$visible_path,$visible_fname",
                'target_email' => "",
                'msg_id'       => '',
                'status'       => 'error',
                'error_type'   => 'bad_parameter',
                'user_email'   => $param->{'user'}{'email'},
            }
        );
        return undef;
    }

    # the file must be uploaded in a directory existing
    unless (-d "$shareddir/$path") {
        Sympa::Report::reject_report_web('user', 'no_such_document',
            {'path' => $visible_path},
            $param->{'action'}, $list);
        wwslog('err', '%s/%s: not a directory', $shareddir, $path);
        web_db_log(
            {   'robot'        => $robot,
                'list'         => $list->{'name'},
                'action'       => $param->{'action'},
                'parameters'   => "$visible_path,$visible_fname",
                'target_email' => "",
                'msg_id'       => '',
                'status'       => 'error',
                'error_type'   => 'internal',
                'user_email'   => $param->{'user'}{'email'},
            }
        );
        return undef;
    }

    # Access control for the directory where there is the uploading
    my %mode;
    $mode{'edit'}    = 1;
    $mode{'control'} = 1;    # for the exception index.html
    my %access_dir = d_access_control(\%mode, $path);

    if ($access_dir{'may'}{'edit'} == 0) {
        Sympa::Report::reject_report_web('auth',
            $access_dir{'reason'}{'edit'},
            {}, $param->{'action'}, $list);
        wwslog('err', 'Access denied for %s', $param->{'user'}{'email'});
        web_db_log(
            {   'robot'        => $robot,
                'list'         => $list->{'name'},
                'action'       => $param->{'action'},
                'parameters'   => "$visible_path,$visible_fname",
                'target_email' => "",
                'msg_id'       => '',
                'status'       => 'error',
                'error_type'   => 'authorization',
                'user_email'   => $param->{'user'}{'email'},
            }
        );
        return undef;
    }

    # Lowercase for file name
    # $fname = $fname;

    ## when the file already exists :

    # the temporary name of the uploaded file : with .duplicate
    my $tmpname     = "." . "$fname" . ".duplicate";
    my $longtmpname = "$shareddir/$path/$tmpname";
    $longtmpname =~ s/\/+/\//g;

    # the temporary desc of the uploaded file : with .duplicate
    my $tmpdesc     = ".desc." . "$tmpname";
    my $longtmpdesc = "$shareddir/$path/$tmpdesc";
    $longtmpdesc =~ s/\/+/\//g;

    # if we aren't in mode_delete nor in mode_rename nor in mode_cancel and
    # the file already exists
    # then we create of a temporary file
    if (   (-e "$longname")
        && ($in{'mode_delete'} eq undef)
        && ($in{'mode_rename'} eq undef)
        && ($in{'mode_cancel'} eq undef)) {

        #access control for the file already existing
        my %mode;
        $mode{'edit'} = 1;
        my %access_file = d_access_control(\%mode, "$path/$fname");

        unless ($access_file{'may'}{'edit'} > 0) {
            Sympa::Report::reject_report_web('auth',
                $access_file{'reason'}{'edit'},
                {}, $param->{'action'}, $list);
            return undef;
        }

        if (-e "$longtmpname") {
            # if exists a temp file younger than 5 minutes that belongs to
            # another user : upload refused
            my @info    = stat $longtmpname;
            my $timeold = time - $info[10];

            if ($timeold <= 300) {
                my %desc_hash =
                    Sympa::Tools::WWW::get_desc_file($longtmpdesc);

                unless ($desc_hash{'email'} eq $param->{'user'}{'email'}) {
                    Sympa::Report::reject_report_web(
                        'user',
                        'cannot_upload',
                        {   'path' => "$visible_path/$visible_fname",
                            'reason' =>
                                "file being uploaded by $desc_hash{'email'} at this time"
                        },
                        $param->{'action'},
                        $list
                    );
                    wwslog('err',
                        "do_d_upload : Unable to upload $longtmpname : file being uploaded at this time "
                    );
                    web_db_log(
                        {   'robot'        => $robot,
                            'list'         => $list->{'name'},
                            'action'       => $param->{'action'},
                            'parameters'   => "$visible_path,$visible_fname",
                            'target_email' => "",
                            'msg_id'       => '',
                            'status'       => 'error',
                            'error_type'   => 'internal',
                            'user_email'   => $param->{'user'}{'email'},
                        }
                    );
                    return undef;
                }
            }
        }

        creation_shared_file($shareddir, $path, $tmpname);
        creation_desc_file($shareddir, $path, $tmpname, %access_file);

        $param->{'serial_file'} = Sympa::Tools::File::get_mtime($longname);
        $param->{'path'}        = $path;
        $param->{'shortname'}   = $fname;

        return 1;
    }

    # for the moderation
    my $longmodname = "$shareddir/$path/" . "." . "$fname" . ".moderate";
    $longmodname =~ s/\/+/\//g;

    my $longmoddesc =
        "$shareddir/$path/" . ".desc.." . "$fname" . ".moderate";
    $longmoddesc =~ s/\/+/\//g;

    # when a file is already waiting for moderation
    my $file_moderated;

    if (-e "$longmodname") {

        my %desc_hash = Sympa::Tools::WWW::get_desc_file("$longmoddesc");
        $file_moderated = 1;

        unless ($desc_hash{'email'} eq $param->{'user'}{'email'}) {
            Sympa::Report::reject_report_web(
                'user',
                'cannot_upload',
                {   'path'   => "$path/$fname",
                    'reason' => "file already exists but not yet moderated"
                },
                $param->{'action'},
                $list
            );
            wwslog('err',
                "do_d_upload : Unable to create $longname : file already exists but not yet moderated"
            );
            web_db_log(
                {   'robot'        => $robot,
                    'list'         => $list->{'name'},
                    'action'       => $param->{'action'},
                    'parameters'   => "$visible_path,$visible_fname",
                    'target_email' => "",
                    'msg_id'       => '',
                    'status'       => 'error',
                    'error_type'   => 'file_already_exists',
                    'user_email'   => $param->{'user'}{'email'},
                }
            );
            return undef;
        }
    }

    ## Exception index.html
    unless ($fname !~ /^index.html?$/i) {
        unless ($access_dir{'may'}{'control'}) {
            Sympa::Report::reject_report_web(
                'user',
                'index_html',
                {   'dir'    => $path,
                    'reason' => "d_access_control"
                },
                $param->{'action'},
                $list
            );
            wwslog('err',
                "do_d_upload : $param->{'user'}{'email'} not authorized to upload a INDEX.HTML file in $path"
            );
            web_db_log(
                {   'robot'        => $robot,
                    'list'         => $list->{'name'},
                    'action'       => $param->{'action'},
                    'parameters'   => "$visible_path,$visible_fname",
                    'target_email' => "",
                    'msg_id'       => '',
                    'status'       => 'error',
                    'error_type'   => 'authorization',
                    'user_email'   => $param->{'user'}{'email'},
                }
            );
            return undef;
        }
    }

    # if we're in mode_delete or mode_rename or mode_cancel, the temp file and
    # his desc file must exist
    if (   $in{'mode_delete'}
        || $in{'mode_rename'}
        || $in{'mode_cancel'}) {

        unless (-e $longtmpname) {
            Sympa::Report::reject_report_web('user', 'no_uploaded_file', {},
                $param->{'action'}, $list);
            wwslog('err',
                "do_d_upload : there isn't any temp file for the uploaded file $fname"
            );
            web_db_log(
                {   'robot'        => $robot,
                    'list'         => $list->{'name'},
                    'action'       => $param->{'action'},
                    'parameters'   => "$visible_path,$visible_fname",
                    'target_email' => "",
                    'msg_id'       => '',
                    'status'       => 'error',
                    'error_type'   => 'no_file',
                    'user_email'   => $param->{'user'}{'email'},
                }
            );
            return undef;
        }

        unless (-e $longtmpdesc) {
            Sympa::Report::reject_report_web('user', 'no_uploaded_file', {},
                $param->{'action'}, $list);
            wwslog('err',
                "do_d_upload : there isn't any desc temp file for the uploaded file $fname"
            );
            web_db_log(
                {   'robot'        => $robot,
                    'list'         => $list->{'name'},
                    'action'       => $param->{'action'},
                    'parameters'   => "$visible_path,$visible_fname",
                    'target_email' => "",
                    'msg_id'       => '',
                    'status'       => 'error',
                    'error_type'   => 'no_file',
                    'user_email'   => $param->{'user'}{'email'},
                }
            );
            return undef;
        }

    }
    ## End of controls

    # in mode_delete the file is going to be overwritten
    if ($in{'mode_delete'}) {

        # Synchronization
        unless (synchronize("$longname", $in{'serial'})) {
            Sympa::Report::reject_report_web('user', 'synchro_failed', {},
                $param->{'action'}, $list);
            wwslog('err', 'Synchronization failed for %s', $longname);
            web_db_log(
                {   'robot'        => $robot,
                    'list'         => $list->{'name'},
                    'action'       => $param->{'action'},
                    'parameters'   => "$visible_path,$visible_fname",
                    'target_email' => "",
                    'msg_id'       => '',
                    'status'       => 'error',
                    'error_type'   => 'synchro_failed',
                    'user_email'   => $param->{'user'}{'email'},
                }
            );
            return undef;
        }

        # Renaming the tmp file and the desc file

        if ($access_dir{'may'}{'edit'} == 1) {

            # Renaming of the old file
            my $longgoodname = "$shareddir/$path/$fname";
            $longgoodname =~ s/\/+/\//g;
            unless (rename "$longgoodname", "$longgoodname.old") {
                Sympa::Report::reject_report_web(
                    'intern',
                    'rename_file',
                    {   'old' => "$longgoodname",
                        'new' => "$longgoodname.old"
                    },
                    $param->{'action'},
                    $list,
                    $param->{'user'}{'email'},
                    $robot
                );
                wwslog('err', 'Failed to rename %s to .old: %s',
                    $longgoodname, $ERRNO);
                web_db_log(
                    {   'robot'        => $robot,
                        'list'         => $list->{'name'},
                        'action'       => $param->{'action'},
                        'parameters'   => "$visible_path,$visible_fname",
                        'target_email' => "",
                        'msg_id'       => '',
                        'status'       => 'error',
                        'error_type'   => 'internal',
                        'user_email'   => $param->{'user'}{'email'},
                    }
                );
                return undef;
            }

            # Renaming of the old desc
            my $longgooddesc = "$shareddir/$path/" . ".desc." . "$fname";
            $longgooddesc =~ s/\/+/\//g;
            unless (rename "$longgooddesc", "$longgooddesc.old") {
                Sympa::Report::reject_report_web(
                    'intern',
                    'rename_file',
                    {   'old' => "$longgooddesc",
                        'new' => "$longgooddesc.old"
                    },
                    $param->{'action'},
                    $list,
                    $param->{'user'}{'email'},
                    $robot
                );
                wwslog('err', 'Failed to rename %s to .old: %s',
                    $longgooddesc, $ERRNO);
                web_db_log(
                    {   'robot'        => $robot,
                        'list'         => $list->{'name'},
                        'action'       => $param->{'action'},
                        'parameters'   => "$visible_path,$visible_fname",
                        'target_email' => "",
                        'msg_id'       => '',
                        'status'       => 'error',
                        'error_type'   => 'internal',
                        'user_email'   => $param->{'user'}{'email'},
                    }
                );
            }

            # the tmp file
            unless (rename "$longtmpname", "$longgoodname") {
                Sympa::Report::reject_report_web(
                    'intern',
                    'rename_file',
                    {   'old' => "$longtmpname",
                        'new' => "$longgoodname"
                    },
                    $param->{'action'},
                    $list,
                    $param->{'user'}{'email'},
                    $robot
                );
                wwslog('err', 'Failed to rename %s to %s: %s',
                    $longtmpname, $longgoodname, $ERRNO);
                web_db_log(
                    {   'robot'        => $robot,
                        'list'         => $list->{'name'},
                        'action'       => $param->{'action'},
                        'parameters'   => "$visible_path,$visible_fname",
                        'target_email' => "",
                        'msg_id'       => '',
                        'status'       => 'error',
                        'error_type'   => 'internal',
                        'user_email'   => $param->{'user'}{'email'},
                    }
                );
            }

            # the tmp desc file
            unless (rename "$longtmpdesc", "$longgooddesc") {
                Sympa::Report::reject_report_web(
                    'intern',
                    'rename_file',
                    {   'old' => "$longtmpdesc",
                        'new' => "$longgooddesc"
                    },
                    $param->{'action'},
                    $list,
                    $param->{'user'}{'email'},
                    $robot
                );
                wwslog('err', 'Failed to rename %s to %s: %s',
                    $longtmpdesc, $longgooddesc, $ERRNO);
                web_db_log(
                    {   'robot'        => $robot,
                        'list'         => $list->{'name'},
                        'action'       => $param->{'action'},
                        'parameters'   => "$visible_path,$visible_fname",
                        'target_email' => "",
                        'msg_id'       => '',
                        'status'       => 'error',
                        'error_type'   => 'internal',
                        'user_email'   => $param->{'user'}{'email'},
                    }
                );
            }

        } elsif ($access_dir{'may'}{'edit'} == 0.5) {

            unless (rename "$longtmpname", "$longmodname") {
                Sympa::Report::reject_report_web(
                    'intern',
                    'rename_file',
                    {   'old' => "$longtmpname",
                        'new' => "$longmodname"
                    },
                    $param->{'action'},
                    $list,
                    $param->{'user'}{'email'},
                    $robot
                );
                wwslog('err', 'Failed to rename %s to %s: %s',
                    $longtmpname, $longmodname, $ERRNO);
                web_db_log(
                    {   'robot'        => $robot,
                        'list'         => $list->{'name'},
                        'action'       => $param->{'action'},
                        'parameters'   => "$visible_path,$visible_fname",
                        'target_email' => "",
                        'msg_id'       => '',
                        'status'       => 'error',
                        'error_type'   => 'internal',
                        'user_email'   => $param->{'user'}{'email'},
                    }
                );
            }

            unless (rename "$longtmpdesc", "$longmoddesc") {
                Sympa::Report::reject_report_web(
                    'intern',
                    'rename_file',
                    {   'old' => "$longtmpdesc",
                        'new' => "$longmoddesc"
                    },
                    $param->{'action'},
                    $list,
                    $param->{'user'}{'email'},
                    $robot
                );
                wwslog('err', 'Failed to rename %s to %s: %s',
                    $longtmpdesc, $longmoddesc, $ERRNO);
                web_db_log(
                    {   'robot'        => $robot,
                        'list'         => $list->{'name'},
                        'action'       => $param->{'action'},
                        'parameters'   => "$visible_path,$visible_fname",
                        'target_email' => "",
                        'msg_id'       => '',
                        'status'       => 'error',
                        'error_type'   => 'internal',
                        'user_email'   => $param->{'user'}{'email'},
                    }
                );
            }

            $list->send_notify_to_editor(
                'shared_moderated',
                {   'filename' => "$path/$fname",
                    'who'      => $param->{'user'}{'email'}
                }
            );
        } else {
            Sympa::Report::reject_report_web('auth',
                $access_dir{'reason'}{'edit'},
                {}, $param->{'action'}, $list);
            wwslog('err', 'Access denied for %s', $param->{'user'}{'email'});
            web_db_log(
                {   'robot'        => $robot,
                    'list'         => $list->{'name'},
                    'action'       => $param->{'action'},
                    'parameters'   => "$visible_path,$visible_fname",
                    'target_email' => "",
                    'msg_id'       => '',
                    'status'       => 'error',
                    'error_type'   => 'authorization',
                    'user_email'   => $param->{'user'}{'email'},
                }
            );
            return undef;
        }

#	 $in{'list'} = $list_name;

        # message of success
        Sympa::Report::notice_report_web('upload_success', {'path' => $fname},
            $param->{'action'});
        web_db_log(
            {   'robot'        => $robot,
                'list'         => $list->{'name'},
                'action'       => $param->{'action'},
                'parameters'   => "$visible_path,$visible_fname",
                'target_email' => "",
                'msg_id'       => '',
                'status'       => 'success',
                'error_type'   => '',
                'user_email'   => $param->{'user'}{'email'},
            }
        );
        return 'd_read';
    }

    # in mode_rename the file is going to be renamed
    if ($in{'mode_rename'}) {

        my $longnewname = "$shareddir/$path/$in{'new_name'}";
        $longnewname =~ s/\/+/\//g;

        # Control new document name
        unless ($in{'new_name'}) {
            Sympa::Report::reject_report_web('user', 'missing_arg',
                {'argument' => 'new name'},
                $param->{'action'});
            wwslog('err', 'New name missing to rename the uploaded file');
            web_db_log(
                {   'robot'        => $robot,
                    'list'         => $list->{'name'},
                    'action'       => $param->{'action'},
                    'parameters'   => "$visible_path,$visible_fname",
                    'target_email' => "",
                    'msg_id'       => '',
                    'status'       => 'error',
                    'error_type'   => 'missing_parameter',
                    'user_email'   => $param->{'user'}{'email'},
                }
            );
            return undef;
        }
        if (   $in{'new_name'} =~ /^\./
            || $in{'new_name'} =~ /\.desc/
            || $in{'new_name'} =~ /[~\#\[\]\/]$/) {
            Sympa::Report::reject_report_web('user', 'incorrect_name',
                {'name' => $in{'new_name'}},
                $param->{'action'}, $list);
            wwslog('err',
                "do_d_upload : Unable to create file $in{'new_name'} : incorrect name"
            );
            web_db_log(
                {   'robot'        => $robot,
                    'list'         => $list->{'name'},
                    'action'       => $param->{'action'},
                    'parameters'   => "$visible_path,$visible_fname",
                    'target_email' => "",
                    'msg_id'       => '',
                    'status'       => 'error',
                    'error_type'   => 'bad_parameter',
                    'user_email'   => $param->{'user'}{'email'},
                }
            );
            return undef;
        }

        if (($fname =~ /\.url$/) && ($in{'new_name'} !~ /\.url$/)) {
            Sympa::Report::reject_report_web('user', 'incorrect_name',
                {'name' => $in{'new_name'}},
                $param->{'action'}, $list);
            wwslog('err',
                "do_d_upload : New file name $in{'new_name'} does not match URL filenames"
            );
            web_db_log(
                {   'robot'        => $robot,
                    'list'         => $list->{'name'},
                    'action'       => $param->{'action'},
                    'parameters'   => "$visible_path,$visible_fname",
                    'target_email' => "",
                    'msg_id'       => '',
                    'status'       => 'error',
                    'error_type'   => 'bad_parameter',
                    'user_email'   => $param->{'user'}{'email'},
                }
            );
            return undef;
        }

        if (-e $longnewname) {
            Sympa::Report::reject_report_web('user', 'doc_already_exist',
                {'name' => $in{'new_name'}},
                $param->{'action'}, $list);
            wwslog('err', '%s is an existing name', $in{'new_name'});
            web_db_log(
                {   'robot'        => $robot,
                    'list'         => $list->{'name'},
                    'action'       => $param->{'action'},
                    'parameters'   => "$visible_path,$visible_fname",
                    'target_email' => "",
                    'msg_id'       => '',
                    'status'       => 'error',
                    'error_type'   => 'file_already_exists',
                    'user_email'   => $param->{'user'}{'email'},
                }
            );
            return undef;
        }

        # when a file is already waiting for moderation
        if (-e "$shareddir/$path/.$in{'new_name'}.moderate") {
            Sympa::Report::reject_report_web('user', 'doc_already_exist',
                {'name' => $in{'new_name'}},
                $param->{'action'}, $list);
            wwslog('err',
                "do_d_upload : $in{'new_name'} is an existing name for a not yet moderated file"
            );
            web_db_log(
                {   'robot'        => $robot,
                    'list'         => $list->{'name'},
                    'action'       => $param->{'action'},
                    'parameters'   => "$visible_path,$visible_fname",
                    'target_email' => "",
                    'msg_id'       => '',
                    'status'       => 'error',
                    'error_type'   => 'file_already_exists',
                    'user_email'   => $param->{'user'}{'email'},
                }
            );
            return undef;
        }
        # when a file is being uploaded
        if (-e "$shareddir/$path/.$in{'new_name'}.duplicate") {
            Sympa::Report::reject_report_web('user', 'doc_already_exist',
                {'name' => $in{'new_name'}},
                $param->{'action'}, $list);
            wwslog('err',
                "do_d_upload : $in{'new_name'} is an existing name for a file being uploaded "
            );
            web_db_log(
                {   'robot'        => $robot,
                    'list'         => $list->{'name'},
                    'action'       => $param->{'action'},
                    'parameters'   => "$visible_path,$visible_fname",
                    'target_email' => "",
                    'msg_id'       => '',
                    'status'       => 'error',
                    'error_type'   => 'file_already_exists',
                    'user_email'   => $param->{'user'}{'email'},
                }
            );
        }

        # Renaming the tmp file and the desc file

        if ($access_dir{'may'}{'edit'} == 1) {
            unless (rename "$longtmpname", "$longnewname") {
                Sympa::Report::reject_report_web(
                    'intern',
                    'rename_file',
                    {   'old' => "$longtmpname",
                        'new' => "$longnewname"
                    },
                    $param->{'action'},
                    $list,
                    $param->{'user'}{'email'},
                    $robot
                );
                wwslog('err', 'Failed to rename %s to %s: %s',
                    $longtmpname, $longnewname, $ERRNO);
                web_db_log(
                    {   'robot'        => $robot,
                        'list'         => $list->{'name'},
                        'action'       => $param->{'action'},
                        'parameters'   => "$visible_path,$visible_fname",
                        'target_email' => "",
                        'msg_id'       => '',
                        'status'       => 'error',
                        'error_type'   => 'internal',
                        'user_email'   => $param->{'user'}{'email'},
                    }
                );
            }

            my $longnewdesc = "$shareddir/$path/.desc.$in{'new_name'}";
            $longnewdesc =~ s/\/+/\//g;

            unless (rename "$longtmpdesc", "$longnewdesc") {
                Sympa::Report::reject_report_web(
                    'intern',
                    'rename_file',
                    {   'old' => "$longtmpdesc",
                        'new' => "$longnewdesc"
                    },
                    $param->{'action'},
                    $list,
                    $param->{'user'}{'email'},
                    $robot
                );
                wwslog('err', 'Failed to rename %s to %s: %s',
                    $longtmpdesc, $longnewdesc, $ERRNO);
                web_db_log(
                    {   'robot'        => $robot,
                        'list'         => $list->{'name'},
                        'action'       => $param->{'action'},
                        'parameters'   => "$visible_path,$visible_fname",
                        'target_email' => "",
                        'msg_id'       => '',
                        'status'       => 'error',
                        'error_type'   => 'internal',
                        'user_email'   => $param->{'user'}{'email'},
                    }
                );
            }

        } elsif ($access_dir{'may'}{'edit'} == 0.5) {

            unless (rename "$longtmpname",
                "$shareddir/$path/.$in{'new_name'}.moderate") {
                Sympa::Report::reject_report_web(
                    'intern',
                    'rename_file',
                    {   'old' => "$longtmpname",
                        'new' => "$shareddir/$path/.$in{'new_name'}.moderate"
                    },
                    $param->{'action'},
                    $list,
                    $param->{'user'}{'email'},
                    $robot
                );
                wwslog('err',
                    "do_d_upload : Failed to rename $longtmpname to $shareddir/$path/.$in{'new_name'}.moderate : $ERRNO"
                );
                web_db_log(
                    {   'robot'        => $robot,
                        'list'         => $list->{'name'},
                        'action'       => $param->{'action'},
                        'parameters'   => "$visible_path,$visible_fname",
                        'target_email' => "",
                        'msg_id'       => '',
                        'status'       => 'error',
                        'error_type'   => 'internal',
                        'user_email'   => $param->{'user'}{'email'},
                    }
                );
            }

            unless (rename "$longtmpdesc",
                "$shareddir/$path/.desc..$in{'new_name'}.moderate") {
                Sympa::Report::reject_report_web(
                    'intern',
                    'rename_file',
                    {   'old' => "$longtmpdesc",
                        'new' =>
                            "$shareddir/$path/.desc..$in{'new_name'}.moderate"
                    },
                    $param->{'action'},
                    $list,
                    $param->{'user'}{'email'},
                    $robot
                );
                wwslog('err',
                    "do_d_upload : Failed to rename $longtmpdesc to $shareddir/$path/.desc..$in{'new_name'}.moderate: $ERRNO"
                );
                web_db_log(
                    {   'robot'        => $robot,
                        'list'         => $list->{'name'},
                        'action'       => $param->{'action'},
                        'parameters'   => "$visible_path,$visible_fname",
                        'target_email' => "",
                        'msg_id'       => '',
                        'status'       => 'error',
                        'error_type'   => 'internal',
                        'user_email'   => $param->{'user'}{'email'},
                    }
                );
            }

            $list->send_notify_to_editor(
                'shared_moderated',
                {   'filename' => "$path/$in{'new_name'}",
                    'who'      => $param->{'user'}{'email'}
                }
            );
        } else {
            Sympa::Report::reject_report_web('auth',
                $access_dir{'reason'}{'edit'},
                {}, $param->{'action'}, $list);
            wwslog('err', 'Access denied for %s', $param->{'user'}{'email'});
            web_db_log(
                {   'robot'        => $robot,
                    'list'         => $list->{'name'},
                    'action'       => $param->{'action'},
                    'parameters'   => "$visible_path,$visible_fname",
                    'target_email' => "",
                    'msg_id'       => '',
                    'status'       => 'error',
                    'error_type'   => 'authorization',
                    'user_email'   => $param->{'user'}{'email'},
                }
            );
            return undef;
        }

#	 $in{'list'} = $list_name;

        # message of success
        Sympa::Report::notice_report_web('upload_success', {'path' => $fname},
            $param->{'action'});
        web_db_log(
            {   'robot'        => $robot,
                'list'         => $list->{'name'},
                'action'       => $param->{'action'},
                'parameters'   => "$visible_path,$visible_fname",
                'target_email' => "",
                'msg_id'       => '',
                'status'       => 'success',
                'error_type'   => '',
                'user_email'   => $param->{'user'}{'email'},
            }
        );
        return 'd_read';
    }

    # in mode_cancel, we delete the temp file and his desc
    if ($in{'mode_cancel'}) {

        # removing of the temp file
        unless (unlink($longtmpname)) {
            Sympa::Report::reject_report_web('intern', 'erase_file',
                {'file' => $longtmpname},
                $param->{'action'}, $list, $param->{'user'}{'email'}, $robot);
            wwslog('err', 'Failed to erase the temp file %s', $longtmpname);
            web_db_log(
                {   'robot'        => $robot,
                    'list'         => $list->{'name'},
                    'action'       => $param->{'action'},
                    'parameters'   => "$visible_path,$visible_fname",
                    'target_email' => "",
                    'msg_id'       => '',
                    'status'       => 'error',
                    'error_type'   => 'internal',
                    'user_email'   => $param->{'user'}{'email'},
                }
            );
            return undef;
        }

        # removing of the description temp file
        unless (unlink($longtmpdesc)) {
            Sympa::Report::reject_report_web('intern', 'erase_file',
                {'file' => $longtmpdesc},
                $param->{'action'}, $list, $param->{'user'}{'email'}, $robot);
            wwslog('err', 'Failed to erase the desc temp file %s',
                $longtmpdesc);
            web_db_log(
                {   'robot'        => $robot,
                    'list'         => $list->{'name'},
                    'action'       => $param->{'action'},
                    'parameters'   => "$visible_path,$visible_fname",
                    'target_email' => "",
                    'msg_id'       => '',
                    'status'       => 'error',
                    'error_type'   => 'internal',
                    'user_email'   => $param->{'user'}{'email'},
                }
            );
            return undef;
        }

        return 'd_read';
    }

    ## usual case

    # shared_moderated
    if ($access_dir{'may'}{'edit'} == 0.5) {
        my $modname = "." . "$fname" . ".moderate";

        creation_shared_file($shareddir, $path, $modname);
        creation_desc_file($shareddir, $path, $modname, %access_dir);

        unless ($file_moderated) {
            $list->send_notify_to_editor(
                'shared_moderated',
                {   'filename' => "$path/$fname",
                    'who'      => $param->{'user'}{'email'}
                }
            );
        }

        Sympa::Report::notice_report_web('to_moderate', {'path' => $fname},
            $param->{'action'});

    } else {
        creation_shared_file($shareddir, $path, $fname);
        creation_desc_file($shareddir, $path, $fname, %access_dir);
    }

    $in{'list'} = $list_name;

    Sympa::Report::notice_report_web('upload_success',
        {'path' => $visible_fname},
        $param->{'action'});
    web_db_log(
        {   'robot'        => $robot,
            'list'         => $list->{'name'},
            'action'       => $param->{'action'},
            'parameters'   => "$visible_path,$visible_fname",
            'target_email' => "",
            'msg_id'       => '',
            'status'       => 'success',
            'error_type'   => '',
            'user_email'   => $param->{'user'}{'email'},
        }
    );

    web_db_stat_log(parameter => $size);

    return 'd_read';
}

## Creation of a picture file
sub creation_picture_file {
    my $path  = shift;
    my $fname = shift;

    unless (-d $path) {
        wwslog('notice', 'Create dir %s/', $path);

        unless (Sympa::Tools::File::mkdir_all($path, 0755)) {
            wwslog('err', 'Unable to create dir %s/', $path);
            return undef;
        }

        unless (open(FF, '>', $path . '/index.html')) {
            wwslog('err', 'Unable to create dir %s/index.html', $path);
        }
        chmod 0644, $path . '/index.html';
        close FF;
    }

    my $fh = $query->upload('uploaded_file');
    unless (open FILE, '>:bytes', "$path/$fname") {
        Sympa::Report::reject_report_web('intern', 'cannot_upload',
            {'path' => "$path/$fname"},
            $param->{'action'}, $list, $param->{'user'}{'email'}, $robot);
        wwslog('err', 'Cannot open file %s/%s: %s', $path, $fname, $ERRNO);
        return undef;
    }
    while (<$fh>) {
        print FILE;
    }
    close FILE;
    chmod 0644, "$path/$fname";
}

## Creation of a shared file
sub creation_shared_file {
    my ($shareddir, $path, $fname) = @_;

    unless (-d $shareddir . '/' . $path) {
        wwslog('notice', 'Create dir %s/%s/', $shareddir, $path);

        unless (mkdir($shareddir . '/' . $path, 0755)) {
            wwslog('err',
                "creation_shared_file : Unable to create dir $shareddir/$path/"
            );
            return undef;
        }

    }
    unless (
        Sympa::Tools::WWW::upload_file_to_server(
            {   'query'       => $query,
                'file_field'  => 'uploaded_file',
                'destination' => "$shareddir/$path/$fname",
            }
        )
        ) {
        Sympa::Report::reject_report_web('intern', 'cannot_upload',
            {'path' => "$path/$fname"},
            $param->{'action'}, $list, $param->{'user'}{'email'}, $robot);
        web_db_log(
            {   'robot'        => $robot,
                'list'         => $list->{'name'},
                'action'       => $param->{'action'},
                'parameters'   => "$fname",
                'target_email' => "",
                'msg_id'       => '',
                'status'       => 'error',
                'error_type'   => 'internal',
                'user_email'   => $param->{'user'}{'email'},
            }
        );
        return undef;
    }

    ## XSS Protection for HTML files.
    if (lc($fname) =~ /\.html?/) {
        my $sanitized_html =
            Sympa::HTMLSanitizer->new($robot)
            ->sanitize_html_file("$shareddir/$path/$fname");
        if (defined $sanitized_html) {
            open my $fh, '>', "$shareddir/$path/$fname";
            print $fh $sanitized_html;
            close $fh;
        } else {
            $log->syslog('err', 'Unable to sanitize file %s', $fname);
        }
    }

}

## Creation of the description file
sub creation_desc_file {
    my ($shareddir, $path, $fname, %access) = @_;

    unless (open(DESC, ">$shareddir/$path/.desc.$fname")) {
        wwslog('err',
            "creation_desc_file: cannot create description file $shareddir/.desc.$path/$fname"
        );
    }

    print DESC "title\n \n\n";
    print DESC "creation\n  date_epoch " . time
        . "\n  email $param->{'user'}{'email'}\n\n";

    print DESC "access\n";
    print DESC "  read $access{'scenario'}{'read'}\n";
    print DESC "  edit $access{'scenario'}{'edit'}\n";

    close DESC;
}

#*******************************************
# Function : do_d_unzip
# Description : unzip a file or a tree structure
#               from an uploaded zip file
#******************************************

sub do_d_unzip {
    # Parameters of the uploaded file (from d_read.tt2)
    my $fn = $in{'unzipped_file'};

    # name of the file, without path
    my $fname;
    if ($fn =~ /([^\/\\]+)$/) {
        $fname = $1;
    }

    wwslog('info', '(%s/%s)', $in{'path'}, $fname);

    # Variables
    my $path = Sympa::Tools::WWW::no_slash_end($in{'path'});

    # path of the shared directory
    my $shareddir = $list->{'dir'} . '/shared';

    # name of the file
    my $longname = "$shareddir/$path/$fname";
    $longname =~ s/\/+/\//g;

    ## Controls

    my $listname = $list->{'name'};

    # uploaded file must have a name
    unless ($fname) {
        Sympa::Report::reject_report_web('user', 'no_name', {},
            $param->{'action'}, $list);
        wwslog('err', '(%s/%s) No file specified to upload', $path, $fname);
        web_db_log(
            {   'robot'        => $robot,
                'list'         => $list->{'name'},
                'action'       => $param->{'action'},
                'parameters'   => "$in{'path'}",
                'target_email' => "",
                'msg_id'       => '',
                'status'       => 'error',
                'error_type'   => 'no_file',
                'user_email'   => $param->{'user'}{'email'},
            }
        );
        return undef;
    }

    # must have .zip extension
    unless ($fname =~ /^.+\.zip$/) {
        Sympa::Report::reject_report_web(
            'user',
            'incorrect_name',
            {   'name'   => "$fname",
                'reason' => "must have the '.zip' extension"
            },
            $param->{'action'},
            $list
        );
        wwslog('err', '(%s/%s) The file must have ".zip" extension',
            $path, $fname);
        web_db_log(
            {   'robot'        => $robot,
                'list'         => $list->{'name'},
                'action'       => $param->{'action'},
                'parameters'   => "$in{'path'}",
                'target_email' => "",
                'msg_id'       => '',
                'status'       => 'error',
                'error_type'   => 'bad_parameter',
                'user_email'   => $param->{'user'}{'email'},
            }
        );
        return undef;
    }

    ## Check quota
    if ($list->{'admin'}{'shared_doc'}{'quota'}) {
        if ($list->get_shared_size() >=
            $list->{'admin'}{'shared_doc'}{'quota'} * 1024) {
            Sympa::Report::reject_report_web('user', 'shared_full', {},
                $param->{'action'}, $list);
            wwslog('err', '(%s/%s) Shared Quota exceeded for list %s',
                $path, $fname, $list);
            web_db_log(
                {   'robot'        => $robot,
                    'list'         => $list->{'name'},
                    'action'       => $param->{'action'},
                    'parameters'   => "$in{'path'}",
                    'target_email' => "",
                    'msg_id'       => '',
                    'status'       => 'error',
                    'error_type'   => 'shared_full',
                    'user_email'   => $param->{'user'}{'email'},
                }
            );
            return undef;
        }
    }

    # The name of the file must be correct and must not be a description file
    if (   $fname =~ /^\./
        || $fname =~ /\.desc/
        || $fname =~ /[~\#\[\]]$/) {
        Sympa::Report::reject_report_web('user', 'incorrect_name',
            {'name' => "$fname"},
            $param->{'action'}, $list);
        wwslog('err', '(%s/%s) Incorrect name', $path, $fname);
        web_db_log(
            {   'robot'        => $robot,
                'list'         => $list->{'name'},
                'action'       => $param->{'action'},
                'parameters'   => "$in{'path'}",
                'target_email' => "",
                'msg_id'       => '',
                'status'       => 'error',
                'error_type'   => 'bad_parameter',
                'user_email'   => $param->{'user'}{'email'},
            }
        );
        return undef;
    }

    # the file must be uploaded in a directory existing
    unless (-d "$shareddir/$path") {
        Sympa::Report::reject_report_web('user', 'no_such_document',
            {'path' => $path},
            $param->{'action'}, $list);
        wwslog('err', '(%s/%s) %s/%s: Not a directory',
            $path, $fname, $shareddir, $path);
        web_db_log(
            {   'robot'        => $robot,
                'list'         => $list->{'name'},
                'action'       => $param->{'action'},
                'parameters'   => "$in{'path'}",
                'target_email' => "",
                'msg_id'       => '',
                'status'       => 'error',
                'error_type'   => 'internal',
                'user_email'   => $param->{'user'}{'email'},
            }
        );
        return undef;
    }

    # Access control for the directory where there is the uploading
    # only for (is_author || !moderated)
    my %mode;
    $mode{'edit'} = 1;
    my %access_dir = d_access_control(\%mode, $path);

    if ($access_dir{'may'}{'edit'} == 0) {
        Sympa::Report::reject_report_web('auth',
            $access_dir{'reason'}{'edit'},
            {}, $param->{'action'}, $list);
        wwslog('err', '(%s/%s) Access denied for %s',
            $path, $fname, $param->{'user'}{'email'});
        web_db_log(
            {   'robot'        => $robot,
                'list'         => $list->{'name'},
                'action'       => $param->{'action'},
                'parameters'   => "$in{'path'}",
                'target_email' => "",
                'msg_id'       => '',
                'status'       => 'error',
                'error_type'   => 'authorization',
                'user_email'   => $param->{'user'}{'email'},
            }
        );
        return undef;
    }

    if ($access_dir{'may'}{'edit'} == 0.5) {
        Sympa::Report::reject_report_web('auth', 'edit_moderated', {},
            $param->{'action'}, $list);
        wwslog('err', '(%s/%s) Access denied for %s',
            $path, $fname, $param->{'user'}{'email'});
        web_db_log(
            {   'robot'        => $robot,
                'list'         => $list->{'name'},
                'action'       => $param->{'action'},
                'parameters'   => "$in{'path'}",
                'target_email' => "",
                'msg_id'       => '',
                'status'       => 'error',
                'error_type'   => 'authorization',
                'user_email'   => $param->{'user'}{'email'},
            }
        );
        return undef;
    }

    ## End of control

    # directory for the uploaded file
    my $date         = time;
    my $zip_dir_name = $listname . $date . $PID;
    my $zip_abs_dir  = $Conf::Conf{'tmpdir'} . '/' . $zip_dir_name;

    unless (mkdir("$zip_abs_dir", 0777)) {
        Sympa::Report::reject_report_web('intern', 'cannot_mkdir',
            {'dir' => $zip_abs_dir},
            $param->{'action'}, $list, $param->{'user'}{'email'}, $robot);
        wwslog('err', '(%s/%s) Unable to create %s: %s',
            $path, $fname, $zip_abs_dir, $ERRNO);
        web_db_log(
            {   'robot'        => $robot,
                'list'         => $list->{'name'},
                'action'       => $param->{'action'},
                'parameters'   => "$in{'path'}",
                'target_email' => "",
                'msg_id'       => '',
                'status'       => 'error',
                'error_type'   => 'internal',
                'user_email'   => $param->{'user'}{'email'},
            }
        );
        return undef;
    }

    ### directory for unzipped files
    unless (mkdir("$zip_abs_dir" . "/zip", 0777)) {
        Sympa::Report::reject_report_web('intern', 'cannot_mkdir',
            {'dir' => "$zip_abs_dir" . "/zip"},
            $param->{'action'}, $list, $param->{'user'}{'email'}, $robot);
        wwslog('err',
            "do_d_unzip($path/$fname) : Unable to create $zip_abs_dir/zip : $ERRNO"
        );
        web_db_log(
            {   'robot'        => $robot,
                'list'         => $list->{'name'},
                'action'       => $param->{'action'},
                'parameters'   => "$in{'path'}",
                'target_email' => "",
                'msg_id'       => '',
                'status'       => 'error',
                'error_type'   => 'internal',
                'user_email'   => $param->{'user'}{'email'},
            }
        );
        return undef;
    }

    ### uploaded of the file.zip
    my $fh = $query->upload('unzipped_file');
    unless (open FILE, ">:bytes", "$zip_abs_dir/$fname") {
        Sympa::Report::reject_report_web('intern', 'cannot_upload',
            {'path' => "$path/$fname"},
            $param->{'action'}, $list, $param->{'user'}{'email'}, $robot);
        wwslog('err',
            "do_d_unzip($path/$fname) : Cannot open file $zip_abs_dir/$fname : $ERRNO"
        );
        web_db_log(
            {   'robot'        => $robot,
                'list'         => $list->{'name'},
                'action'       => $param->{'action'},
                'parameters'   => "$in{'path'}",
                'target_email' => "",
                'msg_id'       => '',
                'status'       => 'error',
                'error_type'   => 'internal',
                'user_email'   => $param->{'user'}{'email'},
            }
        );
        return undef;
    }
    while (<$fh>) {
        print FILE;
    }
    close FILE;

    ### unzip the file
    my $status = d_unzip_shared_file($zip_abs_dir, $fname, $path);

    unless (defined($status)) {
        Sympa::Report::reject_report_web(
            'intern',
            'cannot_unzip',
            {'path' => "$zip_abs_dir/$fname", 'name' => $fname},
            $param->{'action'},
            $list,
            $param->{'user'}{'email'},
            $robot
        );
        wwslog('err',
            "do_d_unzip($path/$fname) : Unable to unzip the file $zip_abs_dir/$fname"
        );
        web_db_log(
            {   'robot'        => $robot,
                'list'         => $list->{'name'},
                'action'       => $param->{'action'},
                'parameters'   => "$in{'path'}",
                'target_email' => "",
                'msg_id'       => '',
                'status'       => 'error',
                'error_type'   => 'internal',
                'user_email'   => $param->{'user'}{'email'},
            }
        );
        return undef;
    }

    unless ($status) {
        Sympa::Report::reject_report_web('intern', 'cannot_unzip',
            {'name' => "$fname"},
            $param->{'action'}, $list, $param->{'user'}{'email'}, $robot);
    }

    ### install the file hierarchy

    unless (
        d_install_file_hierarchy(
            "$zip_abs_dir/zip", $shareddir, $path, \%access_dir
        )
        ) {
        wwslog('err', '(%s/%s) Unable to install file hierarchy',
            $path, $fname);
        web_db_log(
            {   'robot'        => $robot,
                'list'         => $list->{'name'},
                'action'       => $param->{'action'},
                'parameters'   => "$in{'path'}",
                'target_email' => "",
                'msg_id'       => '',
                'status'       => 'error',
                'error_type'   => 'internal',
                'user_email'   => $param->{'user'}{'email'},
            }
        );
        return undef;
    }

    ## remove tmp directories and files
    #     Sympa::Tools::File::remove_dir($zip_abs_dir);

    $in{'list'} = $listname;

    Sympa::Report::notice_report_web('unzip_success', {'path' => $fname},
        $param->{'action'});
    web_db_log(
        {   'robot'        => $robot,
            'list'         => $list->{'name'},
            'action'       => $param->{'action'},
            'parameters'   => "$in{'path'}",
            'target_email' => "",
            'msg_id'       => '',
            'status'       => 'success',
            'error_type'   => '',
            'user_email'   => $param->{'user'}{'email'},
        }
    );
    return 'd_read';
}

## unzip a shared file in the tmp directory
sub d_unzip_shared_file {
    my ($zip_abs_dir, $fname) = @_;
    wwslog('info', '(%s/%s)', $zip_abs_dir, $fname);

    my $status = 1;

    my $zip = Archive::Zip->new();

    my $az = $zip->read("$zip_abs_dir/$fname");

    unless ($az == Archive::Zip::AZ_OK()) {
        wwslog('err',
            "unzip_shared_file : Unable to read the zip file $zip_abs_dir/$fname : $az"
        );
        web_db_log(
            {   'robot'        => $robot,
                'list'         => $list->{'name'},
                'action'       => $param->{'action'},
                'parameters'   => "$zip_abs_dir,$fname",
                'target_email' => "",
                'msg_id'       => '',
                'status'       => 'error',
                'error_type'   => 'internal',
                'user_email'   => $param->{'user'}{'email'},
            }
        );
        return undef;
    }

    my @memberNames = $zip->memberNames();

    foreach my $name (@memberNames) {
        my $az = $zip->extractMember($name, $zip_abs_dir . '/zip/' . $name);
        unless ($az == Archive::Zip::AZ_OK()) {
            wwslog('err',
                "unzip_shared_file : Unable to extract member $name of the zip file $zip_abs_dir/$fname : $az"
            );
            web_db_log(
                {   'robot'        => $robot,
                    'list'         => $list->{'name'},
                    'action'       => $param->{'action'},
                    'parameters'   => "$zip_abs_dir,$fname",
                    'target_email' => "",
                    'msg_id'       => '',
                    'status'       => 'error',
                    'error_type'   => 'internal',
                    'user_email'   => $param->{'user'}{'email'},
                }
            );
            $status = 0;
        }
    }

    ## Qencode 8bit filenames afterward
    ## The suspected charset is the one that is associated to the user's
    ## language
    Sympa::Tools::File::qencode_hierarchy($zip_abs_dir . '/zip',
        Conf::lang2charset($language->get_lang));

    return $status;
}

## Install file hierarchy from $tmp_dir directory to $shareddir/$path
## directory
sub d_install_file_hierarchy {
    my ($tmp_dir, $shareddir, $path, $access_dir) = @_;
    wwslog('debug2', '(%s, %s)', $tmp_dir, $path);

    $tmp_dir   = Sympa::Tools::WWW::no_slash_end($tmp_dir);
    $shareddir = Sympa::Tools::WWW::no_slash_end($shareddir);
    $path      = Sympa::Tools::WWW::no_slash_end($path);

    my $fatal_error = 0;

    unless (opendir DIR, "$tmp_dir") {
        Sympa::Report::reject_report_web('intern', 'cannot_open_dir',
            {'dir' => $tmp_dir},
            $param->{'action'}, $list, $param->{'user'}{'email'}, $robot);
        wwslog('err', '(%s) Impossible to open %s directory', $path,
            $tmp_dir);
        web_db_log(
            {   'robot'        => $robot,
                'list'         => $list->{'name'},
                'action'       => $param->{'action'},
                'parameters'   => "$tmp_dir,$shareddir,$path,$access_dir",
                'target_email' => "",
                'msg_id'       => '',
                'status'       => 'error',
                'error_type'   => 'internal',
                'user_email'   => $param->{'user'}{'email'},
            }
        );
        return undef;
    }
    my @from_dir = readdir DIR;
    closedir DIR;

    foreach my $doc (@from_dir) {
        next
            if ($doc eq '.' || $doc eq '..');
        if (-d "$tmp_dir/$doc") {
            if ($fatal_error) {
                Sympa::Report::reject_report_web(
                    'user',
                    'directory_no_copied',
                    {   'name'   => "$path/$doc",
                        'reason' => "quota exceeded"
                    },
                    $param->{'action'},
                    $list,
                    $param->{'user'}{'email'},
                    $robot
                );
            } else {
                unless (
                    d_copy_rec_dir(
                        "$tmp_dir", "$path", "$shareddir/$path", $doc
                    )
                    ) {
                    $fatal_error = 1;
                    Sympa::Report::reject_report_web(
                        'user',
                        'directory_no_copied',
                        {   'name'   => "$path/$doc",
                            'reason' => "quota exceeded"
                        },
                        $param->{'action'},
                        $list,
                        $param->{'user'}{'email'},
                        $robot
                    );
                    #		    return undef;
                }
            }
        } else {
            if ($fatal_error) {
                Sympa::Report::reject_report_web(
                    'user',
                    'file_no_copied',
                    {   'name'   => "$path/$doc",
                        'reason' => "quota exceeded"
                    },
                    $param->{'action'},
                    $list,
                    $param->{'user'}{'email'},
                    $robot
                );
            } else {
                unless (
                    d_copy_file(
                        "$tmp_dir",         "$path",
                        "$shareddir/$path", $doc,
                        $access_dir
                    )
                    ) {
                    wwslog('err',
                        "d_install_hierarchy($path) : fatal error from d_copy_file($doc)"
                    );
                    web_db_log(
                        {   'robot'  => $robot,
                            'list'   => $list->{'name'},
                            'action' => $param->{'action'},
                            'parameters' =>
                                "$tmp_dir,$shareddir,$path,$access_dir",
                            'target_email' => "",
                            'msg_id'       => '',
                            'status'       => 'error',
                            'error_type'   => 'internal',
                            'user_email'   => $param->{'user'}{'email'},
                        }
                    );

                    $fatal_error = 1;
                    Sympa::Report::reject_report_web(
                        'user',
                        'file_no_copied',
                        {   'name'   => "$path/$doc",
                            'reason' => "quota exceeded"
                        },
                        $param->{'action'},
                        $list,
                        $param->{'user'}{'email'},
                        $robot
                    );
                }
                #		return undef;
            }
        }
    }

    if ($fatal_error) {
        return undef;
    } else {
        web_db_log(
            {   'robot'        => $robot,
                'list'         => $list->{'name'},
                'action'       => $param->{'action'},
                'parameters'   => "$tmp_dir,$shareddir,$path,$access_dir",
                'target_email' => "",
                'msg_id'       => '',
                'status'       => 'success',
                'error_type'   => '',
                'user_email'   => $param->{'user'}{'email'},
            }
        );
        return 1;
    }
}

## copy $dname from $from to $list->{shared}/$path if rights are ok
sub d_copy_rec_dir {
    my ($from, $path, $dest_dir, $dname) = @_;
    wwslog('debug3', '(%s, %s, %s)', $from, $dest_dir, $dname);

    $from     = Sympa::Tools::WWW::no_slash_end($from);
    $path     = Sympa::Tools::WWW::no_slash_end($path);
    $dest_dir = Sympa::Tools::WWW::no_slash_end($dest_dir);

    my $fatal_error = 0;

    # Access control on the directory $path where there is the copy
    # Copy allowed only for (is_author || !moderate)
    my %mode;
    $mode{'edit'}    = 1;
    $mode{'control'} = 1;
    my %access_dir = d_access_control(\%mode, $path);

    unless ($access_dir{'may'}{'edit'} == 1) {
        Sympa::Report::reject_report_web(
            'user',
            'directory_no_copied',
            {   'name'   => $dname,
                'reason' => "no edition right on father directory"
            },
            $param->{'action'},
            $list,
            $param->{'user'}{'email'},
            $robot
        );
        wwslog(
            'err', '(%s) Access denied for %s',
            $path, $param->{'user'}{'email'}
        );
        web_db_log(
            {   'robot'        => $robot,
                'list'         => $list->{'name'},
                'action'       => $param->{'action'},
                'parameters'   => "$from,$path,$dest_dir,$dname",
                'target_email' => "",
                'msg_id'       => '',
                'status'       => 'error',
                'error_type'   => 'authorization',
                'user_email'   => $param->{'user'}{'email'},
            }
        );
        return 1;
    }

    my $may;
    unless ($may = d_test_existing_and_rights($path, $dname, $dest_dir)) {
        Sympa::Report::reject_report_web('user', 'directory_no_copied',
            {'name' => $dname},
            $param->{'action'}, $list, $param->{'user'}{'email'}, $robot);
        wwslog('err',
            '(%s) Error while calling "test_existing_and_rights(%s/%s)"',
            $dname, $dest_dir, $dname);
        web_db_log(
            {   'robot'        => $robot,
                'list'         => $list->{'name'},
                'action'       => $param->{'action'},
                'parameters'   => "$from,$path,$dest_dir,$dname",
                'target_email' => "",
                'msg_id'       => '',
                'status'       => 'error',
                'error_type'   => 'internal',
                'user_email'   => $param->{'user'}{'email'},
            }
        );
        return 1;
    }

    unless ($may->{'exists'}) {

        # The name of the directory must be correct and musn't not be a
        # description file
        if (   $dname =~ /^\./
            || $dname =~ /\.desc/
            || $dname =~ /[~\#\[\]]$/) {
            Sympa::Report::reject_report_web('user', 'incorrect_name',
                {'name' => "$dname"},
                $param->{'action'}, $list);
            wwslog('err', '%s: incorrect name', $dname);
            web_db_log(
                {   'robot'        => $robot,
                    'list'         => $list->{'name'},
                    'action'       => $param->{'action'},
                    'parameters'   => "$from,$path,$dest_dir,$dname",
                    'target_email' => "",
                    'msg_id'       => '',
                    'status'       => 'error',
                    'error_type'   => 'bad_parameter',
                    'user_email'   => $param->{'user'}{'email'},
                }
            );
            return 1;
        }

        ## Exception index.html
        unless ($dname !~ /^index.html?$/i) {
            Sympa::Report::reject_report_web('user', 'index_html',
                {'dir' => $path},
                $param->{'action'}, $list);
            wwslog('err',
                "d_copy_rec_dir : the directory cannot be called INDEX.HTML "
            );
            web_db_log(
                {   'robot'        => $robot,
                    'list'         => $list->{'name'},
                    'action'       => $param->{'action'},
                    'parameters'   => "$from,$path,$dest_dir,$dname",
                    'target_email' => "",
                    'msg_id'       => '',
                    'status'       => 'error',
                    'error_type'   => 'internal',
                    'user_email'   => $param->{'user'}{'email'},
                }
            );
            return 1;
        }

        ## directory creation
        unless (mkdir("$dest_dir/$dname", 0777)) {
            Sympa::Report::reject_report_web('user', 'directory_no_copied',
                {'name' => "$dname"},
                $param->{'action'}, $list);
            wwslog('err',
                "d_copy_rec_dir : Unable to create directory $dest_dir/$dname : $ERRNO"
            );
            web_db_log(
                {   'robot'        => $robot,
                    'list'         => $list->{'name'},
                    'action'       => $param->{'action'},
                    'parameters'   => "$from,$path,$dest_dir,$dname",
                    'target_email' => "",
                    'msg_id'       => '',
                    'status'       => 'error',
                    'error_type'   => 'internal',
                    'user_email'   => $param->{'user'}{'email'},
                }
            );
            return 1;
        }

        ## desc directory creation
        unless (open(DESC, ">$dest_dir/$dname/.desc")) {
            wwslog('err',
                "d_copy_rec_dir: cannot create description file $dest_dir/$dname/.desc"
            );
            web_db_log(
                {   'robot'        => $robot,
                    'list'         => $list->{'name'},
                    'action'       => $param->{'action'},
                    'parameters'   => "$from,$path,$dest_dir,$dname",
                    'target_email' => "",
                    'msg_id'       => '',
                    'status'       => 'error',
                    'error_type'   => 'internal',
                    'user_email'   => $param->{'user'}{'email'},
                }
            );
        }

        print DESC "title\n \n\n";
        print DESC "creation\n  date_epoch " . time
            . "\n  email $param->{'user'}{'email'}\n\n";

        print DESC "access\n";
        print DESC "  read $access_dir{'scenario'}{'read'}\n";
        print DESC "  edit $access_dir{'scenario'}{'edit'}\n";

        close DESC;
    }

    if ($may->{'rights'} || !($may->{'exists'})) {

        unless (opendir DIR, "$from/$dname") {
            Sympa::Report::reject_report_web('user', 'directory_no_copied',
                {'name' => "$dname"},
                $param->{'action'}, $list, $param->{'user'}{'email'}, $robot);
            wwslog('err', '(%s) Impossible to open %s directory',
                $dname, $from);
            web_db_log(
                {   'robot'        => $robot,
                    'list'         => $list->{'name'},
                    'action'       => $param->{'action'},
                    'parameters'   => "$from,$path,$dest_dir,$dname",
                    'target_email' => "",
                    'msg_id'       => '',
                    'status'       => 'error',
                    'error_type'   => 'internal',
                    'user_email'   => $param->{'user'}{'email'},
                }
            );
            return 1;
        }

        my @from_dir = readdir DIR;
        closedir DIR;

        foreach my $doc (@from_dir) {

            if ($doc eq '.' || $doc eq '..') {
                next;
            }
            if (-d "$from/$dname/$doc") {
                if ($fatal_error) {
                    Sympa::Report::reject_report_web(
                        'user',
                        'directory_no_copied',
                        {   'name'   => "$dname/$doc",
                            'reason' => "quota exceeded"
                        },
                        $param->{'action'},
                        $list,
                        $param->{'user'}{'email'},
                        $robot
                    );

                } else {

                    unless (
                        d_copy_rec_dir(
                            "$from/$dname",     "$path/$dname",
                            "$dest_dir/$dname", $doc
                        )
                        ) {
                        $fatal_error = 1;
                        Sympa::Report::reject_report_web(
                            'user',
                            'directory_no_copied',
                            {   'name'   => "$dname/$doc",
                                'reason' => "quota exceeded"
                            },
                            $param->{'action'},
                            $list,
                            $param->{'user'}{'email'},
                            $robot
                        );
#		    return undef;
                    }
                }

            } else {
                if ($fatal_error) {
                    Sympa::Report::reject_report_web(
                        'user',
                        'file_no_copied',
                        {   'name'   => "$dname/$doc",
                            'reason' => "quota exceeded"
                        },
                        $param->{'action'},
                        $list,
                        $param->{'user'}{'email'},
                        $robot
                    );

                } else {
                    unless (
                        d_copy_file(
                            "$from/$dname",     "$path/$dname",
                            "$dest_dir/$dname", $doc,
                            \%access_dir
                        )
                        ) {
                        wwslog('err',
                            "d_copy_rec_dir($path/$dname) : fatal error from d_copy_file($doc)"
                        );
                        web_db_log(
                            {   'robot'  => $robot,
                                'list'   => $list->{'name'},
                                'action' => $param->{'action'},
                                'parameters' =>
                                    "$from,$path,$dest_dir,$dname",
                                'target_email' => "",
                                'msg_id'       => '',
                                'status'       => 'error',
                                'error_type'   => 'internal',
                                'user_email'   => $param->{'user'}{'email'},
                            }
                        );
                        $fatal_error = 1;
                        Sympa::Report::reject_report_web(
                            'user',
                            'file_no_copied',
                            {   'name'   => "$dname/$doc",
                                'reason' => "quota exceeded"
                            },
                            $param->{'action'},
                            $list,
                            $param->{'user'}{'email'},
                            $robot
                        );
                    }
#		    return undef;
                }
            }
        }

    } else {
        Sympa::Report::reject_report_web(
            'user',
            'directory_no_copied',
            {   'name'   => $dname,
                'reason' => "no edition right on the father directory"
            },
            $param->{'action'},
            $list,
            $param->{'user'}{'email'},
            $robot
        );

        wwslog('err',
            "d_copy_rec_file : impossible to copy content directory $dname, the user doesn't have edit rights on directory $path"
        );
        web_db_log(
            {   'robot'        => $robot,
                'list'         => $list->{'name'},
                'action'       => $param->{'action'},
                'parameters'   => "$from,$path,$dest_dir,$dname",
                'target_email' => "",
                'msg_id'       => '',
                'status'       => 'error',
                'error_type'   => 'internal',
                'user_email'   => $param->{'user'}{'email'},
            }
        );
    }

    if ($fatal_error) {
        return undef;
    } else {
        web_db_log(
            {   'robot'        => $robot,
                'list'         => $list->{'name'},
                'action'       => $param->{'action'},
                'parameters'   => "$from,$path,$dest_dir,$dname",
                'target_email' => "",
                'msg_id'       => '',
                'status'       => 'success',
                'error_type'   => '',
                'user_email'   => $param->{'user'}{'email'},
            }
        );
        return 1;
    }
}

## copy $from/$fname to $list->{shared}/$path if rights are ok
sub d_copy_file {
    my ($from, $path, $dest_dir, $fname, $access_dir) = @_;
    wwslog('debug3', '(%s, %s, %s', $from, $dest_dir, $fname);

    $from     = Sympa::Tools::WWW::no_slash_end($from);
    $path     = Sympa::Tools::WWW::no_slash_end($path);
    $dest_dir = Sympa::Tools::WWW::no_slash_end($dest_dir);

    my $may;
    unless ($may = d_test_existing_and_rights($path, $fname, $dest_dir)) {
        Sympa::Report::reject_report_web(
            'user',
            'file_no_copied',
            {   'name'   => "$fname",
                'reason' => "quota exceeded"
            },
            $param->{'action'},
            $list,
            $param->{'user'}{'email'},
            $robot
        );
        wwslog('err',
            '(%s) Error while calling "test_existing_and_rights(%s/%s)"',
            $fname, $dest_dir, $fname);
        web_db_log(
            {   'robot'        => $robot,
                'list'         => $list->{'name'},
                'action'       => $param->{'action'},
                'parameters'   => "$from,$path,$dest_dir,$fname,$access_dir",
                'target_email' => "",
                'msg_id'       => '',
                'status'       => 'error',
                'error_type'   => 'file_no_copied',
                'user_email'   => $param->{'user'}{'email'},
            }
        );
        return 1;
    }

    if ($may->{'rights'} || !($may->{'exists'})) {

        # The name of the file must be correct and musn't not be a description
        # file
        if (   $fname =~ /^\./
            || $fname =~ /\.desc/
            || $fname =~ /[~\#\[\]]$/) {
            Sympa::Report::reject_report_web('user', 'incorrect_name',
                {'name' => "$fname"},
                $param->{'action'}, $list);
            wwslog('err', '%s: incorrect name', $fname);
            web_db_log(
                {   'robot'  => $robot,
                    'list'   => $list->{'name'},
                    'action' => $param->{'action'},
                    'parameters' =>
                        "$from,$path,$dest_dir,$fname,$access_dir",
                    'target_email' => "",
                    'msg_id'       => '',
                    'status'       => 'error',
                    'error_type'   => 'bad_parameter',
                    'user_email'   => $param->{'user'}{'email'},
                }
            );
            return 1;
        }

        ## Exception index.html
        unless ($fname !~ /^index.html?$/i) {
            unless ($access_dir->{'may'}{'control'}) {
                Sympa::Report::reject_report_web('user', 'index_html',
                    {'dir' => $path},
                    $param->{'action'}, $list);
                wwslog('err',
                    "d_copy_file : the user is not authorized to upload a INDEX.HTML file in $dest_dir"
                );
                web_db_log(
                    {   'robot'  => $robot,
                        'list'   => $list->{'name'},
                        'action' => $param->{'action'},
                        'parameters' =>
                            "$from,$path,$dest_dir,$fname,$access_dir",
                        'target_email' => "",
                        'msg_id'       => '',
                        'status'       => 'error',
                        'error_type'   => 'authorization',
                        'user_email'   => $param->{'user'}{'email'},
                    }
                );
                return 1;
            }
        }

        ## Check quota
        if ($list->{'admin'}{'shared_doc'}{'quota'}) {

            if ($list->get_shared_size() >=
                $list->{'admin'}{'shared_doc'}{'quota'} * 1024) {
                Sympa::Report::reject_report_web('user', 'shared_full', {},
                    $param->{'action'}, $list);
                wwslog('err',
                    "d_copy_file : Shared Quota exceeded for list $list->{'name'} on file $path/$fname"
                );
                web_db_log(
                    {   'robot'  => $robot,
                        'list'   => $list->{'name'},
                        'action' => $param->{'action'},
                        'parameters' =>
                            "$from,$path,$dest_dir,$fname,$access_dir",
                        'target_email' => "",
                        'msg_id'       => '',
                        'status'       => 'error',
                        'error_type'   => 'shared_full',
                        'user_email'   => $param->{'user'}{'email'},
                    }
                );
                return undef;
            }
        }

        ## if already existing :delete it
        unlink("$dest_dir/$fname")
            if (-e "$dest_dir/$fname");
        unlink("$dest_dir/.desc.$fname")
            if (-e "$dest_dir/.desc.$fname");

        ##  # if exists a temp file younger than 5 minutes that belongs to
        ##  # another user : file copy refused
        if (-e "$dest_dir/.$fname.duplicate") {
            my @info    = stat "$dest_dir/.$fname.duplicate";
            my $timeold = time - $info[10];
            if ($timeold <= 300) {
                my %desc_hash = Sympa::Tools::WWW::get_desc_file(
                    "$dest_dir/.desc..$fname.duplicate");
                unless ($desc_hash{'email'} eq $param->{'user'}{'email'}) {
                    Sympa::Report::reject_report_web(
                        'user',
                        'file_no_copied',
                        {   'name' => "$path/$fname",
                            'reason' =>
                                "file being uploading by $desc_hash{'email'} at this time"
                        },
                        $param->{'action'},
                        $list,
                        $param->{'user'}{'email'},
                        $robot
                    );
                    wwslog('err',
                        "d_copy_file : unable to copy $path/$fname : file being uploaded at this time "
                    );
                    web_db_log(
                        {   'robot'  => $robot,
                            'list'   => $list->{'name'},
                            'action' => $param->{'action'},
                            'parameters' =>
                                "$from,$path,$dest_dir,$fname,$access_dir",
                            'target_email' => "",
                            'msg_id'       => '',
                            'status'       => 'error',
                            'error_type'   => 'file_no_copied',
                            'user_email'   => $param->{'user'}{'email'},
                        }
                    );
                    return 1;
                }
            }

            unlink("$dest_dir/.$fname.duplicate");
            unlink("$dest_dir/.desc..$fname.duplicate")
                if (-e "$dest_dir/.desc..$fname.duplicate");
        }

        if (-e "$dest_dir/.$fname.moderate") {
            my %desc_hash = Sympa::Tools::WWW::get_desc_file(
                "$dest_dir/.$fname.moderate");

            unless ($desc_hash{'email'} eq $param->{'user'}{'email'}) {
                Sympa::Report::reject_report_web(
                    'user',
                    'file_no_copied',
                    {   'name' => "$path/$fname",
                        'reason' =>
                            "file awaiting for moderation, uploaded by $desc_hash{'email'}"
                    },
                    $param->{'action'},
                    $list,
                    $param->{'user'}{'email'},
                    $robot
                );
                wwslog('err',
                    "d_copy_file : unable to copy $path/$fname : file awaiting for moderation"
                );
                web_db_log(
                    {   'robot'  => $robot,
                        'list'   => $list->{'name'},
                        'action' => $param->{'action'},
                        'parameters' =>
                            "$from,$path,$dest_dir,$fname,$access_dir",
                        'target_email' => "",
                        'msg_id'       => '',
                        'status'       => 'error',
                        'error_type'   => 'file_no_copied',
                        'user_email'   => $param->{'user'}{'email'},
                    }
                );
                return 1;
            }
            unlink("$dest_dir/.$fname.moderate");

            unlink("$dest_dir/.desc..$fname.moderate")
                if (-e "$dest_dir/.desc..$fname.moderate");
        }

        ## file copy
        unless (open FROM_FILE, "$from/$fname") {
            Sympa::Report::reject_report_web('user', 'file_no_copied',
                {'name' => "$path/$fname"},
                $param->{'action'}, $list, $param->{'user'}{'email'}, $robot);
            wwslog('err', 'Impossible to open %s/%s', $from, $fname);
            web_db_log(
                {   'robot'  => $robot,
                    'list'   => $list->{'name'},
                    'action' => $param->{'action'},
                    'parameters' =>
                        "$from,$path,$dest_dir,$fname,$access_dir",
                    'target_email' => "",
                    'msg_id'       => '',
                    'status'       => 'error',
                    'error_type'   => 'file_no_copied',
                    'user_email'   => $param->{'user'}{'email'},
                }
            );
            return 1;
        }

        my $visible_fname = Sympa::Tools::WWW::make_visible_path($fname);

        unless (open DEST_FILE, ">$dest_dir/$fname") {
            Sympa::Report::reject_report_web(
                'user',
                'file_no_copied',
                {'name' => "$path/$visible_fname"},
                $param->{'action'},
                $list,
                $param->{'user'}{'email'},
                $robot
            );
            wwslog('err', 'Cannot create file %s/%s: %s',
                $dest_dir, $fname, $ERRNO);
            web_db_log(
                {   'robot'  => $robot,
                    'list'   => $list->{'name'},
                    'action' => $param->{'action'},
                    'parameters' =>
                        "$from,$path,$dest_dir,$fname,$access_dir",
                    'target_email' => "",
                    'msg_id'       => '',
                    'status'       => 'error',
                    'error_type'   => 'file_no_copied',
                    'user_email'   => $param->{'user'}{'email'},
                }
            );
            return 1;
        }

        while (<FROM_FILE>) {
            print DEST_FILE;
        }
        close FROM_FILE;
        close DEST_FILE;

        ## XSS Protection for HTML files.
        if (lc($fname) =~ /\.html?/) {
            my $sanitized_html =
                Sympa::HTMLSanitizer->new($robot)
                ->sanitize_html_file("$dest_dir/$fname");
            if (defined $sanitized_html) {
                open my $fh, '>', "$dest_dir/$fname";
                print $fh $sanitized_html;
                close $fh;
            } else {
                $log->syslog('err', 'Unable to sanitize file %s', $fname);
            }
        }

        ## desc file creation
        unless (open(DESC, ">$dest_dir/.desc.$fname")) {
            wwslog('err',
                "d_copy_file: cannot create description file $dest_dir/.desc.$fname"
            );
            web_db_log(
                {   'robot'  => $robot,
                    'list'   => $list->{'name'},
                    'action' => $param->{'action'},
                    'parameters' =>
                        "$from,$path,$dest_dir,$fname,$access_dir",
                    'target_email' => "",
                    'msg_id'       => '',
                    'status'       => 'error',
                    'error_type'   => 'internal',
                    'user_email'   => $param->{'user'}{'email'},
                }
            );
        }

        print DESC "title\n \n\n";
        print DESC "creation\n  date_epoch " . time
            . "\n  email $param->{'user'}{'email'}\n\n";

        print DESC "access\n";
        print DESC "  read $access_dir->{'scenario'}{'read'}\n";
        print DESC "  edit $access_dir->{'scenario'}{'edit'}\n";

        close DESC;

        ## information

        Sympa::Report::notice_report_web('file_erased',
            {'path' => "$path/$visible_fname"},
            $param->{'action'})
            if ($may->{'exists'});
        web_db_log(
            {   'robot'        => $robot,
                'list'         => $list->{'name'},
                'action'       => $param->{'action'},
                'parameters'   => "$from,$path,$dest_dir,$fname,$access_dir",
                'target_email' => "",
                'msg_id'       => '',
                'status'       => 'success',
                'error_type'   => '',
                'user_email'   => $param->{'user'}{'email'},
            }
        );
    } else {
        Sympa::Report::reject_report_web(
            'user',
            'file_no_copied',
            {   'name'   => "$path/$fname",
                'reason' => "you do not have total edit right on the file"
            },
            $param->{'action'},
            $list,
            $param->{'user'}{'email'},
            $robot
        );
        wwslog('err',
            "d_copy_file : impossible to copy file $fname, the user doesn't have total edit rights on the file"
        );
        web_db_log(
            {   'robot'        => $robot,
                'list'         => $list->{'name'},
                'action'       => $param->{'action'},
                'parameters'   => "$from,$path,$dest_dir,$fname,$access_dir",
                'target_email' => "",
                'msg_id'       => '',
                'status'       => 'error',
                'error_type'   => 'file_no_copied',
                'user_email'   => $param->{'user'}{'email'},
            }
        );
    }

    return 1;
}

## return information on file or dir : existing and edit rights for the user
## in $param
sub d_test_existing_and_rights {
    my ($path, $name, $dest_dir) = @_;

    $path     = Sympa::Tools::WWW::no_slash_end($path);
    $name     = Sympa::Tools::WWW::no_slash_end($name);
    $dest_dir = Sympa::Tools::WWW::no_slash_end($dest_dir);

    my $return;

    $return->{'exists'} = 0;
    $return->{'rights'} = 0;

    if (   (-e "$dest_dir/$name")
        || (-e "$dest_dir/.$name.duplicate")
        || (-e "$dest_dir/.$name.moderate")) {

        $return->{'exists'} = 1;

        my %mode;
        $mode{'edit'} = 1;
        my %access = d_access_control(\%mode, "$path/$name");
        $return->{'rights'} = 1
            if $access{'may'}{'edit'} == 1;
    }

    return $return;
}

#*******************************************
# Function : do_d_delete
# Description : Delete an existing document
#               (file or directory)
#******************************************

sub do_d_delete {
    wwslog('info', '(%s)', $in{'path'});

    #useful variables
    my $path = Sympa::Tools::WWW::no_slash_end($in{'path'});

    my $visible_path = Sympa::Tools::WWW::make_visible_path($path);

    #Current directory and document to delete
    $path =~ /^(([^\/]*\/)*)([^\/]+)(\/?)$/;
    my $current_directory = Sympa::Tools::WWW::no_slash_end($1);
    my $document          = $3;

    # path of the shared directory
    #my $list_name = $in{'list'};
    my $list_name = $list->{'name'};
    my $shareddir = $list->{'dir'} . '/shared';

    #### Controls

    ## must be something to delete
    unless ($document) {
        Sympa::Report::reject_report_web('user', 'missing_arg',
            {'argument' => 'doccument'},
            $param->{'action'});
        wwslog('err', 'No document to delete has been specified');
        web_db_log(
            {   'robot'        => $robot,
                'list'         => $list->{'name'},
                'action'       => $param->{'action'},
                'parameters'   => "$in{'path'}",
                'target_email' => "",
                'msg_id'       => '',
                'status'       => 'error',
                'error_type'   => 'missing_parameter',
                'user_email'   => $param->{'user'}{'email'},
            }
        );
        return undef;
    }

    ### Document isn't a description file?
    unless ($document !~ /^\.desc/) {
        wwslog('err', '%s/%s: description file', $shareddir, $path);
        Sympa::Report::reject_report_web('user', 'no_such_document',
            {'path' => $visible_path},
            $param->{'action'}, $list);
        web_db_log(
            {   'robot'        => $robot,
                'list'         => $list->{'name'},
                'action'       => $param->{'action'},
                'parameters'   => "$in{'path'}",
                'target_email' => "",
                'msg_id'       => '',
                'status'       => 'error',
                'error_type'   => 'description_file',
                'user_email'   => $param->{'user'}{'email'},
            }
        );
        return undef;
    }

    ### Document exists?
    unless (-e "$shareddir/$path") {
        wwslog('err', '%s/%s: no such file or directory', $shareddir, $path);
        Sympa::Report::reject_report_web('user', 'no_such_document',
            {'path' => $visible_path},
            $param->{'action'}, $list);
        web_db_log(
            {   'robot'        => $robot,
                'list'         => $list->{'name'},
                'action'       => $param->{'action'},
                'parameters'   => "$in{'path'}",
                'target_email' => "",
                'msg_id'       => '',
                'status'       => 'error',
                'error_type'   => 'internal',
                'user_email'   => $param->{'user'}{'email'},
            }
        );
        return undef;
    }

    # removing of the document
    my $doc = "$shareddir/$path";

    # Access control
    my %mode;
    $mode{'edit'} = 1;
    my %access = d_access_control(\%mode, $path);

    unless ($access{'may'}{'edit'} > 0) {
        Sympa::Report::reject_report_web('auth', $access{'reason'}{'edit'},
            {}, $param->{'action'}, $list);
        wwslog('err', 'Access denied for %s', $param->{'user'}{'email'});
        web_db_log(
            {   'robot'        => $robot,
                'list'         => $list->{'name'},
                'action'       => $param->{'action'},
                'parameters'   => "$in{'path'}",
                'target_email' => "",
                'msg_id'       => '',
                'status'       => 'error',
                'error_type'   => 'authorization',
                'user_email'   => $param->{'user'}{'email'},
            }
        );
        return undef;
    }

    ## Directory
    if (-d "$shareddir/$path") {

        # test of emptiness
        opendir DIR, "$doc";
        my @readdir = readdir DIR;
        close DIR;

        # test for "ordinary" files
        my @test_normal = grep !/^\./, @readdir;
        my @test_hidden = grep !(/^\.desc$/ | /^\.(\.)?$/ | /^[^\.]/),
            @readdir;
        if (($#test_normal != -1) || ($#test_hidden != -1)) {
            Sympa::Report::reject_report_web('user', 'full_directory',
                {'directory' => $path},
                $param->{'action'}, $list);
            wwslog('err', 'Failed to erase %s: directory not empty', $doc);
            web_db_log(
                {   'robot'        => $robot,
                    'list'         => $list->{'name'},
                    'action'       => $param->{'action'},
                    'parameters'   => "$in{'path'}",
                    'target_email' => "",
                    'msg_id'       => '',
                    'status'       => 'error',
                    'error_type'   => 'internal',
                    'user_email'   => $param->{'user'}{'email'},
                }
            );
            return undef;
        }

        # removing of the description file if exists
        if (-e "$doc/\.desc") {
            unless (unlink("$doc/.desc")) {
                Sympa::Report::reject_report_web('intern', 'erase_file',
                    {'file' => "$doc/.desc"},
                    $param->{'action'}, $list, $param->{'user'}{'email'},
                    $robot);
                wwslog('err', 'Failed to erase %s/.desc: %s', $doc, $ERRNO);
                web_db_log(
                    {   'robot'        => $robot,
                        'list'         => $list->{'name'},
                        'action'       => $param->{'action'},
                        'parameters'   => "$in{'path'}",
                        'target_email' => "",
                        'msg_id'       => '',
                        'status'       => 'error',
                        'error_type'   => 'internal',
                        'user_email'   => $param->{'user'}{'email'},
                    }
                );
                return undef;
            }
        }
        # removing of the directory
        rmdir $doc;

        ## File
    } else {

        # removing of the document
        unless (unlink($doc)) {
            Sympa::Report::reject_report_web('intern', 'erase_file',
                {'file' => "$doc"},
                $param->{'action'}, $list, $param->{'user'}{'email'}, $robot);
            wwslog('err', 'Failed to erase %s', $doc);
            web_db_log(
                {   'robot'        => $robot,
                    'list'         => $list->{'name'},
                    'action'       => $param->{'action'},
                    'parameters'   => "$in{'path'}",
                    'target_email' => "",
                    'msg_id'       => '',
                    'status'       => 'error',
                    'error_type'   => 'internal',
                    'user_email'   => $param->{'user'}{'email'},
                }
            );
            return undef;
        }
        # removing of the description file if exists
        if (-e "$shareddir/$current_directory/.desc.$document") {
            unless (unlink("$shareddir/$current_directory/.desc.$document")) {
                wwslog('err',
                    "do_d_delete: failed to erase $shareddir/$current_directory/.desc.$document"
                );
                web_db_log(
                    {   'robot'        => $robot,
                        'list'         => $list->{'name'},
                        'action'       => $param->{'action'},
                        'parameters'   => "$in{'path'}",
                        'target_email' => "",
                        'msg_id'       => '',
                        'status'       => 'error',
                        'error_type'   => 'internal',
                        'user_email'   => $param->{'user'}{'email'},
                    }
                );
            }
        }
    }
    web_db_log(
        {   'robot'        => $robot,
            'list'         => $list->{'name'},
            'action'       => $param->{'action'},
            'parameters'   => "$in{'path'}",
            'target_email' => "",
            'msg_id'       => '',
            'status'       => 'success',
            'error_type'   => '',
            'user_email'   => $param->{'user'}{'email'},
        }
    );

    web_db_stat_log();

    $in{'list'} = $list_name;
    $in{'path'} = $current_directory;
    return 'd_read';
}

#*******************************************
# Function : do_d_rename
# Description : Rename a document
#               (file or directory)
#******************************************

sub do_d_rename {
    wwslog('info', '(%s)', $in{'path'});

    #useful variables
    my $path = Sympa::Tools::WWW::no_slash_end($in{'path'});

    #moderation
    my $visible_path = Sympa::Tools::WWW::make_visible_path($path);
    my $moderate;
    if ($path =~ /\.moderate$/) {
        $moderate = 1;
    }

    #Current directory and document to delete
    my $current_directory;
    if ($path =~ /^(.*)\/([^\/]+)$/) {
        $current_directory = Sympa::Tools::WWW::no_slash_end($1);
    } else {
        $current_directory = '.';
    }
    $path =~ /(^|\/)([^\/]+)$/;
    my $document = $2;

    # path of the shared directory
    my $list_name = $list->{'name'};
    my $shareddir = $list->{'dir'} . '/shared';

    #### Controls

    ## must be something to delete
    unless ($document) {
        Sympa::Report::reject_report_web('user', 'missing_arg',
            {'argument' => 'document'},
            $param->{'action'});
        wwslog('err', 'No document to rename has been specified');
        web_db_log(
            {   'robot'        => $robot,
                'list'         => $list->{'name'},
                'action'       => $param->{'action'},
                'parameters'   => "$in{'path'}",
                'target_email' => "",
                'msg_id'       => '',
                'status'       => 'error',
                'error_type'   => 'no_file',
                'user_email'   => $param->{'user'}{'email'},
            }
        );
        return undef;
    }

    ### Document isn't a description file?
    unless ($document !~ /^\.desc/) {
        wwslog('err', '%s/%s: description file', $shareddir, $path);
        Sympa::Report::reject_report_web('user', 'no_such_document',
            {'path' => $visible_path},
            $param->{'action'}, $list);
        web_db_log(
            {   'robot'        => $robot,
                'list'         => $list->{'name'},
                'action'       => $param->{'action'},
                'parameters'   => "$in{'path'}",
                'target_email' => "",
                'msg_id'       => '',
                'status'       => 'error',
                'error_type'   => 'no_such_document',
                'user_email'   => $param->{'user'}{'email'},
            }
        );
        return undef;
    }

    ### Document exists?
    unless (-e "$shareddir/$path") {
        wwslog('err', '%s/%s: no such file or directory', $shareddir, $path);
        Sympa::Report::reject_report_web('user', 'no_such_document',
            {'path' => $visible_path},
            $param->{'action'}, $list);
        web_db_log(
            {   'robot'        => $robot,
                'list'         => $list->{'name'},
                'action'       => $param->{'action'},
                'parameters'   => "$in{'path'}",
                'target_email' => "",
                'msg_id'       => '',
                'status'       => 'error',
                'error_type'   => 'no_such_document',
                'user_email'   => $param->{'user'}{'email'},
            }
        );
        return undef;
    }

    if (   $in{'new_name'} =~ /^\./
        || $in{'new_name'} =~ /\.desc/
        || $in{'new_name'} =~ /[~\#\[\]\/]$/) {
        Sympa::Report::reject_report_web('user', 'incorrect_name',
            {'name' => $in{'new_name'}},
            $param->{'action'}, $list);
        wwslog('err',
            "do_d_rename : Unable to create file $in{'new_name'} : incorrect name"
        );
        web_db_log(
            {   'robot'        => $robot,
                'list'         => $list->{'name'},
                'action'       => $param->{'action'},
                'parameters'   => "$in{'path'}",
                'target_email' => "",
                'msg_id'       => '',
                'status'       => 'error',
                'error_type'   => 'bad_parameter',
                'user_email'   => $param->{'user'}{'email'},
            }
        );
        return undef;
    }

    if (($document =~ /\.url$/) && ($in{'new_name'} !~ /\.url$/)) {
        Sympa::Report::reject_report_web('user', 'incorrect_name',
            {'name' => $in{'new_name'}},
            $param->{'action'}, $list);
        wwslog('err',
            "do_d_rename : New file name $in{'new_name'} does not match URL filenames"
        );
        web_db_log(
            {   'robot'        => $robot,
                'list'         => $list->{'name'},
                'action'       => $param->{'action'},
                'parameters'   => "$in{'path'}",
                'target_email' => "",
                'msg_id'       => '',
                'status'       => 'error',
                'error_type'   => 'bad_parameter',
                'user_email'   => $param->{'user'}{'email'},
            }
        );
        return undef;
    }

    my $doc = "$shareddir/$path";

    # Access control
    my %mode;
    $mode{'edit'} = 1;
    my %access = d_access_control(\%mode, $path);

    unless ($access{'may'}{'edit'} > 0) {
        Sympa::Report::reject_report_web('auth', $access{'reason'}{'edit'},
            {}, $param->{'action'}, $list);
        wwslog('err', 'Access denied for %s', $param->{'user'}{'email'});
        web_db_log(
            {   'robot'        => $robot,
                'list'         => $list->{'name'},
                'action'       => $param->{'action'},
                'parameters'   => "$in{'path'}",
                'target_email' => "",
                'msg_id'       => '',
                'status'       => 'error',
                'error_type'   => 'authorization',
                'user_email'   => $param->{'user'}{'email'},
            }
        );
        return undef;
    }
    if ($moderate) {
        $log->syslog('notice', '%s, %s/%s/%s', $doc, $shareddir,
            $current_directory, $in{'new_name'});
        unless (rename $doc,
            "$shareddir/$current_directory/.$in{'new_name'}.moderate") {
            Sympa::Report::reject_report_web(
                'intern',
                'rename_file',
                {   'old' => $doc,
                    'new' =>
                        "$shareddir/$current_directory/.$in{'new_name'}.moderate"
                },
                $param->{'action'},
                $list,
                $param->{'user'}{'email'},
                $robot
            );
            wwslog('err', 'Failed to rename %s to %s: %s',
                $doc, "$shareddir/$current_directory/$in{'new_name'}",
                $ERRNO);
            web_db_log(
                {   'robot'        => $robot,
                    'list'         => $list->{'name'},
                    'action'       => $param->{'action'},
                    'parameters'   => "$in{'path'}",
                    'target_email' => "",
                    'msg_id'       => '',
                    'status'       => 'error',
                    'error_type'   => 'internal',
                    'user_email'   => $param->{'user'}{'email'},
                }
            );
            return undef;
        }
    } else {
        $log->syslog('notice', '%s, %s/%s/%s', $doc, $shareddir,
            $current_directory, $in{'new_name'});
        unless (rename $doc, "$shareddir/$current_directory/$in{'new_name'}")
        {
            Sympa::Report::reject_report_web(
                'intern',
                'rename_file',
                {   'old' => $doc,
                    'new' => "$shareddir/$current_directory/$in{'new_name'}"
                },
                $param->{'action'},
                $list,
                $param->{'user'}{'email'},
                $robot
            );
            wwslog('err', 'Failed to rename %s to %s: %s',
                $doc, "$shareddir/$current_directory/$in{'new_name'}",
                $ERRNO);
            web_db_log(
                {   'robot'        => $robot,
                    'list'         => $list->{'name'},
                    'action'       => $param->{'action'},
                    'parameters'   => "$in{'path'}",
                    'target_email' => "",
                    'msg_id'       => '',
                    'status'       => 'error',
                    'error_type'   => 'internal',
                    'user_email'   => $param->{'user'}{'email'},
                }
            );
            return undef;
        }
    }
    ## Rename description file
    my $desc_file     = "$shareddir/$current_directory/.desc.$document";
    my $new_desc_file = $desc_file;

    if (-f $desc_file) {
        if ($moderate) {
            $new_desc_file =~ s/\Q$document/\.$in{'new_name'}\.moderate/;
        } else {
            $new_desc_file =~ s/\Q$document/$in{'new_name'}/;
        }
        unless (rename $desc_file, $new_desc_file) {
            Sympa::Report::reject_report_web(
                'intern',
                'rename_file',
                {   'old' => $desc_file,
                    'new' => $new_desc_file
                },
                $param->{'action'},
                $list,
                $param->{'user'}{'email'},
                $robot
            );
            wwslog('err', 'Failed to rename %s: %s', $desc_file, $ERRNO);
            web_db_log(
                {   'robot'        => $robot,
                    'list'         => $list->{'name'},
                    'action'       => $param->{'action'},
                    'parameters'   => "$in{'path'}",
                    'target_email' => "",
                    'msg_id'       => '',
                    'status'       => 'error',
                    'error_type'   => 'internal',
                    'user_email'   => $param->{'user'}{'email'},
                }
            );
            return undef;
        }
    }
    web_db_log(
        {   'robot'        => $robot,
            'list'         => $list->{'name'},
            'action'       => $param->{'action'},
            'parameters'   => "$in{'path'}",
            'target_email' => "",
            'msg_id'       => '',
            'status'       => 'success',
            'error_type'   => '',
            'user_email'   => $param->{'user'}{'email'},
        }
    );
    $in{'list'} = $list_name;
    if ($current_directory eq '.') {
        $in{'path'} = '';
    } else {
        $in{'path'} = $current_directory . '';
    }
    return 'd_read';
}

#*******************************************
# Function : do_d_create_dir
# Description : Creates a new file / directory
#******************************************
sub do_d_create_dir {
    wwslog('info', '(%s)', $in{'name_doc'});

    #useful variables
    my $path = Sympa::Tools::WWW::no_slash_end($in{'path'});

    #my $list_name = $in{'list'};
    my $list_name = $list->{'name'};
    my $name_doc  = $in{'name_doc'};

    $param->{'list'} = $list_name;
    $param->{'path'} = $path;

    ## Q-decode file path and names
    $param->{'decoded_path'} =
        Sympa::Tools::Text::qdecode_filename($param->{'path'});
    $param->{'decoded_name_doc'} =
        Sympa::Tools::Text::qdecode_filename($name_doc);

    my $type = $in{'type'} || 'directory';
    my $desc_file;

    ### Controls

    # Must be a directory to create (directory name not empty)
    unless ($name_doc) {
        Sympa::Report::reject_report_web('user', 'no_name', {},
            $param->{'action'}, $list);
        wwslog('err', 'Unable to create: no name specified!');
        web_db_log(
            {   'robot'        => $robot,
                'list'         => $list->{'name'},
                'action'       => $param->{'action'},
                'parameters'   => "$in{'name_doc'}",
                'target_email' => "",
                'msg_id'       => '',
                'status'       => 'error',
                'error_type'   => 'missing_parameter',
                'user_email'   => $param->{'user'}{'email'},
            }
        );
        return undef;
    }

    # The name of the directory must be correct
    if (   $name_doc =~ /^\./
        || $name_doc =~ /\.desc/
        || $name_doc =~ /[~\#\[\]\/]$/) {
        Sympa::Report::reject_report_web('user', 'incorrect_name',
            {'name' => $name_doc},
            $param->{'action'}, $list);
        wwslog('err', 'Unable to create directory %s: incorrect name',
            $name_doc);
        web_db_log(
            {   'robot'        => $robot,
                'list'         => $list->{'name'},
                'action'       => $param->{'action'},
                'parameters'   => "$in{'name_doc'}",
                'target_email' => "",
                'msg_id'       => '',
                'status'       => 'error',
                'error_type'   => 'bad_parameter',
                'user_email'   => $param->{'user'}{'email'},
            }
        );
        return undef;
    }

    # Access control
    my %mode;
    $mode{'edit'} = 1;
    my %access = d_access_control(\%mode, $path);

    if ($type eq 'directory') {    ## only when (is_author || !moderated)
        if ($access{'may'}{'edit'} == 0) {
            Sympa::Report::reject_report_web('auth',
                $access{'reason'}{'edit'},
                {}, $param->{'action'}, $list);
            wwslog('err', 'Access denied for %s', $param->{'user'}{'email'});
            web_db_log(
                {   'robot'        => $robot,
                    'list'         => $list->{'name'},
                    'action'       => $param->{'action'},
                    'parameters'   => "$in{'name_doc'}",
                    'target_email' => "",
                    'msg_id'       => '',
                    'status'       => 'error',
                    'error_type'   => 'authorization',
                    'user_email'   => $param->{'user'}{'email'},
                }
            );
            return undef;
        }
        if ($access{'may'}{'edit'} == 0.5) {
            Sympa::Report::reject_report_web('auth', 'dir_edit_moderated', {},
                $param->{'action'}, $list);
            wwslog('err', 'Access denied for %s', $param->{'user'}{'email'});
            web_db_log(
                {   'robot'        => $robot,
                    'list'         => $list->{'name'},
                    'action'       => $param->{'action'},
                    'parameters'   => "$in{'name_doc'}",
                    'target_email' => "",
                    'msg_id'       => '',
                    'status'       => 'error',
                    'error_type'   => 'authorization',
                    'user_email'   => $param->{'user'}{'email'},
                }
            );
            return undef;
        }
    } else {
        if ($access{'may'}{'edit'} == 0) {
            Sympa::Report::reject_report_web('auth',
                $access{'reason'}{'edit'},
                {}, $param->{'action'}, $list);
            wwslog('err', 'Access denied for %s', $param->{'user'}{'email'});
            web_db_log(
                {   'robot'        => $robot,
                    'list'         => $list->{'name'},
                    'action'       => $param->{'action'},
                    'parameters'   => "$in{'name_doc'}",
                    'target_email' => "",
                    'msg_id'       => '',
                    'status'       => 'error',
                    'error_type'   => 'authorization',
                    'user_email'   => $param->{'user'}{'email'},
                }
            );
            return undef;
        }
    }

    # path of the shared directory
    my $shareddir = $list->{'dir'} . '/shared';

    my $document = "$shareddir/$path/$name_doc";

    $param->{'document'} = $document;

    # the file musn't already exists
    if (-e $document) {
        Sympa::Report::reject_report_web('user', 'doc_already_exist',
            {'name' => "$path/$name_doc"},
            $param->{'action'}, $list);
        wwslog('err', 'Can\'t create %s/%s: file already exists',
            $path, $name_doc);
        web_db_log(
            {   'robot'        => $robot,
                'list'         => $list->{'name'},
                'action'       => $param->{'action'},
                'parameters'   => "$in{'name_doc'}",
                'target_email' => "",
                'msg_id'       => '',
                'status'       => 'error',
                'error_type'   => 'file_already_exists',
                'user_email'   => $param->{'user'}{'email'},
            }
        );
        return undef;
    }

    # if the file .moderate exists, only its author can erase it

    my $doc_moderate = "$shareddir/$path/" . "." . "$name_doc" . ".moderate";
    my $file_moderated;

    if (-e "$doc_moderate") {

        $file_moderated = 1;
        my $desc =
            "$shareddir/$path/" . ".desc.." . "$name_doc" . ".moderate";
        $desc =~ s/\/+/\//g;
        my %desc_hash = Sympa::Tools::WWW::get_desc_file("$desc");

        unless ($desc_hash{'email'} eq $param->{'user'}{'email'}) {
            Sympa::Report::reject_report_web(
                'user',
                'cannot_upload',
                {   'path'   => "$path/$name_doc",
                    'reason' => "file already exists but not yet moderated"
                },
                $param->{'action'},
                $list
            );
            wwslog(
                'err',
                'Unable to create %s: file already exists but not yet moderated',
                $doc_moderate
            );
            web_db_log(
                {   'robot'        => $robot,
                    'list'         => $list->{'name'},
                    'action'       => $param->{'action'},
                    'parameters'   => "$in{'name_doc'}",
                    'target_email' => "",
                    'msg_id'       => '',
                    'status'       => 'error',
                    'error_type'   => 'file_already_exists',
                    'user_email'   => $param->{'user'}{'email'},
                }
            );
            return undef;
        }
    }

    ### End of controls

    if ($type eq 'directory') {
        # Creation of the new directory
        unless (mkdir("$document", 0777)) {
            Sympa::Report::reject_report_web('intern', 'cannot_mkdir',
                {'dir' => $document},
                $param->{'action'}, $list, $param->{'user'}{'email'}, $robot);
            wwslog('err', 'Unable to create %s: %s', $document, $ERRNO);
            web_db_log(
                {   'robot'        => $robot,
                    'list'         => $list->{'name'},
                    'action'       => $param->{'action'},
                    'parameters'   => "$in{'name_doc'}",
                    'target_email' => "",
                    'msg_id'       => '',
                    'status'       => 'error',
                    'error_type'   => 'internal',
                    'user_email'   => $param->{'user'}{'email'},
                }
            );
            return undef;
        }

        $desc_file = "$document/.desc";

    } else {
        # Creation of the new file
        unless (open FILE, ">$document") {
            Sympa::Report::reject_report_web(
                'intern', 'cannot_open_file',
                {'file' => "$path/$name_doc"}, $param->{'action'},
                $list, $param->{'user'}{'email'},
                $robot
            );
            wwslog('err', 'Unable to create %s: %s', $document, $ERRNO);
            web_db_log(
                {   'robot'        => $robot,
                    'list'         => $list->{'name'},
                    'action'       => $param->{'action'},
                    'parameters'   => "$in{'name_doc'}",
                    'target_email' => "",
                    'msg_id'       => '',
                    'status'       => 'error',
                    'error_type'   => 'internal',
                    'user_email'   => $param->{'user'}{'email'},
                }
            );
            return undef;
        }
        close FILE;

        $desc_file = "$shareddir/$path/.desc.$name_doc";
    }

    # Creation of a default description file
    unless (open(DESC, ">$desc_file")) {
        Sympa::Report::reject_report_web('intern', 'cannot_open_file',
            {'file' => "$desc_file"},
            $param->{'action'}, $list, $param->{'user'}{'email'}, $robot);
    }

    print DESC "title\n \n\n";
    print DESC "creation\n  date_epoch " . time
        . "\n  email $param->{'user'}{'email'}\n\n";

    print DESC "access\n";
    print DESC "  read $access{'scenario'}{'read'}\n";
    print DESC "  edit $access{'scenario'}{'edit'}\n\n";

    #size of the new file created
    my @infos    = stat $desc_file;
    my $filesize = $infos[7];

    close DESC;

    # moderation
    if ($access{'may'}{'edit'} == 0.5 && ($type ne 'directory')) {
        unless (
            rename "$shareddir/$path/$name_doc",
            "$shareddir/$path/.$name_doc.moderate"
            ) {
            Sympa::Report::reject_report_web(
                'intern',
                'rename_file',
                {   'old' => "$shareddir/$path/$name_doc",
                    'new' => "$shareddir/$path/.$name_doc.moderate"
                },
                $param->{'action'},
                $list,
                $param->{'user'}{'email'},
                $robot
            );
            wwslog('err', 'Failed to rename %s/%s to %s/.%s.moderate: %s',
                $path, $name_doc, $path, $name_doc, $ERRNO);
            web_db_log(
                {   'robot'        => $robot,
                    'list'         => $list->{'name'},
                    'action'       => $param->{'action'},
                    'parameters'   => "$in{'name_doc'}",
                    'target_email' => "",
                    'msg_id'       => '',
                    'status'       => 'error',
                    'error_type'   => 'internal',
                    'user_email'   => $param->{'user'}{'email'},
                }
            );
        }

        unless (rename "$desc_file",
            "$shareddir/$path/.desc..$name_doc.moderate") {
            Sympa::Report::reject_report_web(
                'intern',
                'rename_file',
                {   'old' => $desc_file,
                    'new' => "$shareddir/$path/.desc..$name_doc.moderate"
                },
                $param->{'action'},
                $list,
                $param->{'user'}{'email'},
                $robot
            );
            wwslog('err', 'Failed to rename %s to %s/.desc..%s.moderate : %s',
                $desc_file, $path, $name_doc, $ERRNO);
            web_db_log(
                {   'robot'        => $robot,
                    'list'         => $list->{'name'},
                    'action'       => $param->{'action'},
                    'parameters'   => "$in{'name_doc'}",
                    'target_email' => "",
                    'msg_id'       => '',
                    'status'       => 'error',
                    'error_type'   => 'internal',
                    'user_email'   => $param->{'user'}{'email'},
                }
            );
        }

        unless ($file_moderated) {
            $list->send_notify_to_editor(
                'shared_moderated',
                {   'filename' => $param->{'decoded_path'} . '/'
                        . $param->{'decoded_name_doc'},
                    'who' => $param->{'user'}{'email'}
                }
            );
        }
    }

    web_db_log(
        {   'robot'        => $robot,
            'list'         => $list->{'name'},
            'action'       => $param->{'action'},
            'parameters'   => "$in{'name_doc'}",
            'target_email' => "",
            'msg_id'       => '',
            'status'       => 'success',
            'error_type'   => '',
            'user_email'   => $param->{'user'}{'email'},
        }
    );

    #web_db_stat_log : test before if the creation is a file or a directory
    if ($type eq 'directory') {
        web_db_stat_log(operation => 'd_create_dir');
    } else {
        web_db_stat_log(
            operation => 'd_create_file',
            parameter => $filesize,
        );
    }

    if ($type eq 'directory') {
        return 'd_read';
    }

    if ($access{'may'}{'edit'} == 0.5) {
        $in{'path'} = "$path/.$name_doc.moderate";
    } else {
        $in{'path'} = "$path/$name_doc";
    }

    return 'd_editfile';
}

############## Control

#*******************************************
# Function : do_d_control
# Description : prepares the parameters
#               to edit access for a doc
#*******************************************

sub do_d_control {
    wwslog('info', '%s', $in{'path'});

    # Variables
    my $path = Sympa::Tools::WWW::no_slash_end($in{'path'});
    #my $list_name = $in{'list'};
    my $list_name = $list->{'name'};

    # path of the shared directory
    my $shareddir = $list->{'dir'} . '/shared';

    #moderation
    my $visible_path = Sympa::Tools::WWW::make_visible_path($path);

    unless ($path) {
        Sympa::Report::reject_report_web('user', 'missing_arg',
            {'argument' => 'document'},
            $param->{'action'});
        wwslog('info', 'No document name');
        web_db_log(
            {   'robot'        => $robot,
                'list'         => $list->{'name'},
                'action'       => $param->{'action'},
                'parameters'   => "$in{'path'}",
                'target_email' => "",
                'msg_id'       => '',
                'status'       => 'error',
                'error_type'   => 'missing_parameter',
                'user_email'   => $param->{'user'}{'email'},
            }
        );
        return undef;
    }

    # Existing document?
    unless (-e "$shareddir/$path") {
        Sympa::Report::reject_report_web('user', 'no_such_document',
            {'path' => $visible_path},
            $param->{'action'}, $list);
        wwslog('info',
            "do_d_control : Cannot control $shareddir/$path : not an existing document"
        );
        web_db_log(
            {   'robot'        => $robot,
                'list'         => $list->{'name'},
                'action'       => $param->{'action'},
                'parameters'   => "$in{'path'}",
                'target_email' => "",
                'msg_id'       => '',
                'status'       => 'error',
                'error_type'   => 'no_such_document',
                'user_email'   => $param->{'user'}{'email'},
            }
        );
        return undef;
    }

    ### Document isn't a description file?
    unless ($path !~ /\.desc/) {
        wwslog('info', '%s/%s: description file', $shareddir, $path);
        Sympa::Report::reject_report_web('user', 'no_such_document',
            {'path' => $visible_path},
            $param->{'action'}, $list);
        web_db_log(
            {   'robot'        => $robot,
                'list'         => $list->{'name'},
                'action'       => $param->{'action'},
                'parameters'   => "$in{'path'}",
                'target_email' => "",
                'msg_id'       => '',
                'status'       => 'error',
                'error_type'   => 'no_such_document',
                'user_email'   => $param->{'user'}{'email'},
            }
        );
        return undef;
    }

    # Access control
    my %mode;
    $mode{'control'} = 1;
    my %access = d_access_control(\%mode, $path);
    unless ($access{'may'}{'control'}) {
        Sympa::Report::reject_report_web('auth', $access{'reason'}{'edit'},
            {}, $param->{'action'}, $list);
        wwslog('info', 'Access denied for %s', $param->{'user'}{'email'});
        web_db_log(
            {   'robot'        => $robot,
                'list'         => $list->{'name'},
                'action'       => $param->{'action'},
                'parameters'   => "$in{'path'}",
                'target_email' => "",
                'msg_id'       => '',
                'status'       => 'error',
                'error_type'   => 'authorization',
                'user_email'   => $param->{'user'}{'email'},
            }
        );
        return undef;
    }

    ## End of controls

    #Current directory
    if ($path =~ /^(([^\/]*\/)*)([^\/]+)(\/?)$/) {
        $param->{'father'} = Sympa::Tools::WWW::no_slash_end($1);
    } else {
        $param->{'father'} = '';
    }
    $param->{'escaped_father'} =
        Sympa::SharedDocument::escape_docname($param->{'father'}, '/');

    my $desc_file;
    # path of the description file
    if (-d "$shareddir/$path") {
        $desc_file = "$shareddir/$1$3/.desc";
    } else {
        $desc_file = "$shareddir/$1.desc.$3";
    }

    # Description of the file
    my $read;
    my $edit;

    if (-e $desc_file) {
        ## Synchronization
        $param->{'serial_desc'} = (stat $desc_file)[9];
        my %desc_hash = Sympa::Tools::WWW::get_desc_file("$desc_file");
        # rights for read and edit
        $read = $desc_hash{'read'};
        $edit = $desc_hash{'edit'};
        # owner of the document
        $param->{'doc_owner'} = $desc_hash{'email'};
        $param->{'doc_title'} = $desc_hash{'title'};
    } else {
        $read = $access{'scenario'}{'read'};
        $edit = $access{'scenario'}{'edit'};
    }

    ## other info
    $param->{'doc_date'} = $language->gettext_strftime("%d %b %y  %H:%M",
        localtime Sympa::Tools::File::get_mtime("$shareddir/$path"));

    # template parameters
    $param->{'list'}         = $list_name;
    $param->{'path'}         = $path;
    $param->{'visible_path'} = $visible_path;

    my $lang = $param->{'lang'};

    ## Scenario list for READ

    my $tmp_list_of_scenario = $list->load_scenario_list('d_read', $robot);

    ## Only get required scenario attributes
    foreach my $scenario (keys %{$tmp_list_of_scenario}) {
        $param->{'scenari_read'}{$scenario} = {
            'name' => $tmp_list_of_scenario->{$scenario}{'name'},
            'web_title' =>
                $tmp_list_of_scenario->{$scenario}->get_current_title()
        };
    }

    $param->{'scenari_read'}{$read}{'selected'} = 'selected="selected"';

    ## Scenario list for EDIT
    $tmp_list_of_scenario = $list->load_scenario_list('d_edit', $robot);

    ## Only get required scenario attributes
    foreach my $scenario (keys %{$tmp_list_of_scenario}) {
        $param->{'scenari_edit'}{$scenario} = {
            'name' => $tmp_list_of_scenario->{$scenario}{'name'},
            'web_title' =>
                $tmp_list_of_scenario->{$scenario}->get_current_title()
        };
    }
    $param->{'scenari_edit'}{$edit}{'selected'} = 'selected="selected"';

    ## father directory
    if ($path =~ /^(([^\/]*\/)*)([^\/]+)(\/?)$/) {
        $param->{'father'} = Sympa::Tools::WWW::no_slash_end($1);
    } else {
        $param->{'father'} = '';
    }
    $param->{'escaped_father'} =
        Sympa::SharedDocument::escape_docname($param->{'father'}, '/');

    $param->{'set_owner'} = 1;

    $param->{'father_icon'} = Sympa::Tools::WWW::get_icon($robot, 'father');
    web_db_log(
        {   'robot'        => $robot,
            'list'         => $list->{'name'},
            'action'       => $param->{'action'},
            'parameters'   => "$in{'path'}",
            'target_email' => "",
            'msg_id'       => '',
            'status'       => 'success',
            'error_type'   => '',
            'user_email'   => $param->{'user'}{'email'},
        }
    );
    return 1;
}

#*******************************************
# Function : do_d_change_access
# Description : Saves the description of
#               the file
#******************************************

sub do_d_change_access {
    wwslog('info', '(%s)', $in{'path'});

    # Variables
    my $path = Sympa::Tools::WWW::no_slash_end($in{'path'});

    my $list_name = $list->{'name'};

    # path of the shared directory
    my $shareddir = $list->{'dir'} . '/shared';

    ####  Controls

    ## the path must not be empty (the description file of the shared
    ## directory
    #  doesn't exist)
    unless ($path) {
        Sympa::Report::reject_report_web(
            'intern',
            'cannot_describe_shared_directory',
            {'path' => $path},
            $param->{'action'}, $list, $param->{'user'}{'email'}, $robot
        );
        wwslog('info',
            "do_d_change_access : Cannot change access $shareddir : root directory"
        );
        web_db_log(
            {   'robot'        => $robot,
                'list'         => $list->{'name'},
                'action'       => $param->{'action'},
                'parameters'   => "$in{'path'}",
                'target_email' => "",
                'msg_id'       => '',
                'status'       => 'error',
                'error_type'   => 'internal',
                'user_email'   => $param->{'user'}{'email'},
            }
        );
        return undef;
    }

    # the document to describe must already exist
    unless (-e "$shareddir/$path") {
        Sympa::Report::reject_report_web('user', 'no_doc_to_describe',
            {'path' => $path},
            $param->{'action'}, $list);
        wwslog('info',
            "d_change_access : Unable to change access $shareddir/$path : no such document"
        );
        web_db_log(
            {   'robot'        => $robot,
                'list'         => $list->{'name'},
                'action'       => $param->{'action'},
                'parameters'   => "$in{'path'}",
                'target_email' => "",
                'msg_id'       => '',
                'status'       => 'error',
                'error_type'   => 'no_file',
                'user_email'   => $param->{'user'}{'email'},
            }
        );
        return undef;
    }

    # Access control
    my %mode;
    $mode{'control'} = 1;
    my %access = d_access_control(\%mode, $path);

    unless ($access{'may'}{'control'}) {
        Sympa::Report::reject_report_web('auth',
            'action_listmaster_or_privileged_owner_or_author',
            {}, $param->{'action'}, $list);
        wwslog('info', 'Access denied for %s', $param->{'user'}{'email'});
        web_db_log(
            {   'robot'        => $robot,
                'list'         => $list->{'name'},
                'action'       => $param->{'action'},
                'parameters'   => "$in{'path'}",
                'target_email' => "",
                'msg_id'       => '',
                'status'       => 'error',
                'error_type'   => 'authorization',
                'user_email'   => $param->{'user'}{'email'},
            }
        );
        return undef;
    }

    ## End of controls

    # Description file
    $path =~ /^(([^\/]*\/)*)([^\/]+)(\/?)$/;
    my $dir  = $1;
    my $file = $3;

    my $desc_file;
    if (-d "$shareddir/$path") {
        $desc_file = "$shareddir/$1$3/.desc";
    } else {
        $desc_file = "$shareddir/$1.desc.$3";
    }

    if (-e "$desc_file") {
        # if description file already exists : open it and modify it
        my %desc_hash = Sympa::Tools::WWW::get_desc_file("$desc_file");

        # Synchronization
        unless (synchronize($desc_file, $in{'serial'})) {
            Sympa::Report::reject_report_web('user', 'synchro_failed', {},
                $param->{'action'}, $list);
            wwslog('info', 'Synchronization failed for %s', $desc_file);
            web_db_log(
                {   'robot'        => $robot,
                    'list'         => $list->{'name'},
                    'action'       => $param->{'action'},
                    'parameters'   => "$in{'path'}",
                    'target_email' => "",
                    'msg_id'       => '',
                    'status'       => 'error',
                    'error_type'   => 'synchro_failed',
                    'user_email'   => $param->{'user'}{'email'},
                }
            );
            return undef;
        }

        unless (open DESC, ">$desc_file") {
            wwslog('info', 'Cannot open %s: %s', $desc_file, $ERRNO);
            Sympa::Report::reject_report_web('intern', 'cannot_open_file',
                {'file' => $desc_file},
                $param->{'action'}, $list, $param->{'user'}{'email'}, $robot);
            web_db_log(
                {   'robot'        => $robot,
                    'list'         => $list->{'name'},
                    'action'       => $param->{'action'},
                    'parameters'   => "$in{'path'}",
                    'target_email' => "",
                    'msg_id'       => '',
                    'status'       => 'error',
                    'error_type'   => 'internal',
                    'user_email'   => $param->{'user'}{'email'},
                }
            );
            return undef;
        }

        # information not modified
        print DESC "title\n  $desc_hash{'title'}\n\n";

        # access rights
        print DESC "access\n  read $in{'read_access'}\n";
        print DESC "  edit $in{'edit_access'}\n\n";

        print DESC "creation\n";
        # time
        print DESC "  date_epoch $desc_hash{'date'}\n";
        # author
        print DESC "  email $desc_hash{'email'}\n\n";

        close DESC;

    } else {
        # Creation of a description file
        unless (open(DESC, ">$desc_file")) {
            Sympa::Report::reject_report_web('intern', 'cannot_open_file',
                {'file' => $desc_file},
                $param->{'action'}, $list, $param->{'user'}{'email'}, $robot);
            wwslog('info',
                "d_change_access : Cannot create description file $desc_file : $ERRNO"
            );
            web_db_log(
                {   'robot'        => $robot,
                    'list'         => $list->{'name'},
                    'action'       => $param->{'action'},
                    'parameters'   => "$in{'path'}",
                    'target_email' => "",
                    'msg_id'       => '',
                    'status'       => 'error',
                    'error_type'   => 'internal',
                    'user_email'   => $param->{'user'}{'email'},
                }
            );
            return undef;
        }
        print DESC "title\n \n\n";

        my @info = stat "$shareddir/$path";
        print DESC "creation\n  date_epoch " . $info[10] . "\n  email\n\n";
        print DESC "access\n  read $in{'read_access'}\n";
        print DESC "  edit $in{'edit_access'}\n\n";

        close DESC;

    }

    return 'd_control';

}

sub do_d_set_owner {
    wwslog('info', '(%s)', $in{'path'});

    # Variables
    my $desc_file;

    my $path = Sympa::Tools::WWW::no_slash_end($in{'path'});

    #moderation
    my $visible_path = Sympa::Tools::WWW::make_visible_path($path);

    #my $list_name = $in{'list'};
    my $list_name = $list->{'name'};

    # path of the shared directory
    my $shareddir = $list->{'dir'} . '/shared';

    ####  Controls

    ## the path must not be empty (the description file of the shared
    ## directory
    #  doesn't exist)
    unless ($path) {
        Sympa::Report::reject_report_web(
            'intern',
            'cannot_describe_shared_directory',
            {'path' => $path},
            $param->{'action'}, $list, $param->{'user'}{'email'}, $robot
        );
        wwslog('info',
            "do_d_set_owner : Cannot change access $shareddir : root directory"
        );
        web_db_log(
            {   'robot'        => $robot,
                'list'         => $list->{'name'},
                'action'       => $param->{'action'},
                'parameters'   => "$in{'name_doc'}",
                'target_email' => "",
                'msg_id'       => '',
                'status'       => 'error',
                'error_type'   => 'internal',
                'user_email'   => $param->{'user'}{'email'},
            }
        );
        return undef;
    }

    # the email must look like an email "somebody@somewhere"
    unless (Sympa::Tools::Text::valid_email($in{'content'})) {
        Sympa::Report::reject_report_web('user', 'incorrect_email',
            {'email' => $in{'content'}},
            $param->{'action'}, $list);
        wwslog('info', '%s: incorrect email', $in{'content'});
        web_db_log(
            {   'robot'        => $robot,
                'list'         => $list->{'name'},
                'action'       => $param->{'action'},
                'parameters'   => "$in{'name_doc'}",
                'target_email' => "",
                'msg_id'       => '',
                'status'       => 'error',
                'error_type'   => 'incorrect_email',
                'user_email'   => $param->{'user'}{'email'},
            }
        );
        return undef;
    }

    # Access control
    ## father directory
    $path =~ /^(([^\/]*\/)*)([^\/]+)(\/?)$/;
    my $dir  = $1;
    my $file = $3;
    if (-d "$shareddir/$path") {
        $desc_file = "$shareddir/$dir$file/.desc";
    } else {
        $desc_file = "$shareddir/$dir.desc.$file";
    }

    my %mode;
    $mode{'control'} = 1;
    ## must be authorized to control father directory
    #my %access = d_access_control(\%mode,$1);
    my %access = d_access_control(\%mode, $path);

    unless ($access{'may'}{'control'}) {
        Sympa::Report::reject_report_web('auth',
            'action_listmaster_or_privileged_owner_or_author',
            {}, $param->{'action'}, $list);
        wwslog('info', 'Access denied for %s', $param->{'user'}{'email'});
        web_db_log(
            {   'robot'        => $robot,
                'list'         => $list->{'name'},
                'action'       => $param->{'action'},
                'parameters'   => "$in{'name_doc'}",
                'target_email' => "",
                'msg_id'       => '',
                'status'       => 'error',
                'error_type'   => 'authentication',
                'user_email'   => $param->{'user'}{'email'},
            }
        );
        return undef;
    }

    my $may_set = 1;

    unless ($may_set) {
        Sympa::Report::reject_report_web('user', 'full_directory',
            {'directory' => $visible_path},
            $param->{'action'}, $list);
        wwslog('info', 'Cannot set owner of a full directory');
        web_db_log(
            {   'robot'        => $robot,
                'list'         => $list->{'name'},
                'action'       => $param->{'action'},
                'parameters'   => "$in{'name_doc'}",
                'target_email' => "",
                'msg_id'       => '',
                'status'       => 'error',
                'error_type'   => 'full_directory',
                'user_email'   => $param->{'user'}{'email'},
            }
        );
        return undef;
    }

    ## End of controls

    my %desc_hash;

    if (-e "$desc_file") {
        # if description file already exists : open it and modify it
        %desc_hash = Sympa::Tools::WWW::get_desc_file("$desc_file");

        # Synchronization
        unless (synchronize($desc_file, $in{'serial'})) {

            Sympa::Report::reject_report_web('user', 'synchro_failed', {},
                $param->{'action'}, $list);
            wwslog('info', 'Synchronization failed for %s', $desc_file);
            web_db_log(
                {   'robot'        => $robot,
                    'list'         => $list->{'name'},
                    'action'       => $param->{'action'},
                    'parameters'   => "$in{'name_doc'}",
                    'target_email' => "",
                    'msg_id'       => '',
                    'status'       => 'error',
                    'error_type'   => 'synchro_failed',
                    'user_email'   => $param->{'user'}{'email'},
                }
            );
            return undef;
        }

        unless (open DESC, ">$desc_file") {
            wwslog('info', 'Cannot open %s: %s', $desc_file, $ERRNO);
            Sympa::Report::reject_report_web('intern', 'cannot_open_file',
                {'file' => $desc_file},
                $param->{'action'}, $list, $param->{'user'}{'email'}, $robot);
            web_db_log(
                {   'robot'        => $robot,
                    'list'         => $list->{'name'},
                    'action'       => $param->{'action'},
                    'parameters'   => "$in{'name_doc'}",
                    'target_email' => "",
                    'msg_id'       => '',
                    'status'       => 'error',
                    'error_type'   => 'internal',
                    'user_email'   => $param->{'user'}{'email'},
                }
            );
            return undef;
        }

        # information not modified
        print DESC "title\n  $desc_hash{'title'}\n\n";

        print DESC "access\n  read $desc_hash{'read'}\n";
        print DESC "  edit $desc_hash{'edit'}\n\n";
        print DESC "creation\n";
        # time
        print DESC "  date_epoch $desc_hash{'date'}\n";

        #information modified
        # author
        print DESC "  email $in{'content'}\n\n";

        close DESC;

    } else {
        # Creation of a description file
        unless (open(DESC, ">$desc_file")) {
            Sympa::Report::reject_report_web('intern', 'cannot_open_file',
                {'file' => $desc_file},
                $param->{'action'}, $list, $param->{'user'}{'email'}, $robot);
            wwslog('info',
                "d_set_owner : Cannot create description file $desc_file : $ERRNO"
            );
            web_db_log(
                {   'robot'        => $robot,
                    'list'         => $list->{'name'},
                    'action'       => $param->{'action'},
                    'parameters'   => "$in{'name_doc'}",
                    'target_email' => "",
                    'msg_id'       => '',
                    'status'       => 'error',
                    'error_type'   => 'internal',
                    'user_email'   => $param->{'user'}{'email'},
                }
            );
            return undef;
        }
        print DESC "title\n  $desc_hash{'title'}\n\n";
        my @info = stat "$shareddir/$path";
        print DESC "creation\n  date_epoch "
            . $info[10]
            . "\n  email $in{'content'}\n\n";

        print DESC "access\n  read $access{'scenario'}{'read'}\n";
        print DESC "  edit $access{'scenario'}{'edit'}\n\n";

        close DESC;

    }
    web_db_log(
        {   'robot'        => $robot,
            'list'         => $list->{'name'},
            'action'       => $param->{'action'},
            'parameters'   => "$in{'name_doc'}",
            'target_email' => "",
            'msg_id'       => '',
            'status'       => 'success',
            'error_type'   => '',
            'user_email'   => $param->{'user'}{'email'},
        }
    );
    ## ONLY IF SET_OWNER can be performed even if not control of the father
    ## directory
    $mode{'control'} = 1;
    %access = d_access_control(\%mode, $path);
    unless ($access{'may'}{'control'}) {
        ## father directory
        $path =~ /^(([^\/]*\/)*)([^\/]+)(\/?)$/;
        $in{'path'} = Sympa::Tools::WWW::no_slash_end($1);
        return 'd_read';
    }

    ## ELSE
    return 'd_control';
}

## Protecting archives from Email Sniffers
sub do_arc_protect {
    wwslog('info', '');

    return 1;
}

####################################################
#  do_remind
####################################################
#  Sends a remind command to sympa.pl.
#
# IN : -
#
# OUT : 'loginrequest' | 'admin' | undef
#
#####################################################
sub do_remind {
    wwslog('info', '');

    ## Access control
    return undef unless (defined check_authz('do_remind', 'remind'));

    my $extention = time . "." . int(rand 9999);
    my $mail_command;

    ## Sympa will require a confirmation
    my $result = Sympa::Scenario::request_action(
        $list, 'remind', 'smtp',
        {   'sender'      => $param->{'user'}{'email'},
            'remote_host' => $param->{'remote_host'},
            'remote_addr' => $param->{'remote_addr'}
        }
    );
    my $r_action;
    my $reason;
    if (ref($result) eq 'HASH') {
        $r_action = $result->{'action'};
        $reason   = $result->{'reason'};
    }

    if ($r_action =~ /reject/i) {
        Sympa::Report::reject_report_web('auth', $reason, {},
            $param->{'action'}, $list);
        wwslog('info', 'Access denied for %s', $param->{'user'}{'email'});
        web_db_log(
            {   'robot'        => $robot,
                'list'         => $list->{'name'},
                'action'       => $param->{'action'},
                'parameters'   => "",
                'target_email' => "",
                'msg_id'       => '',
                'status'       => 'error',
                'error_type'   => 'authorization',
                'user_email'   => $param->{'user'}{'email'},
            }
        );
        return undef;

    } else {
        $mail_command = sprintf "REMIND %s", $param->{'list'};
    }

    # Commands are injected into incoming spool directly with "md5"
    # authentication level.
    my $time    = time;
    my $message = Sympa::Message->new(
        sprintf("\n\n%s\n", $mail_command),
        context         => $robot,
        envelope_sender => Conf::get_robot_conf($robot, 'request'),
        sender          => $param->{'user'}{'email'},
        md5_check       => 1,
        message_id      => sprintf('<%s@wwsympa>', $time)
    );
    $message->add_header('Content-Type', 'text/plain; Charset=utf-8');

    unless (Sympa::Spool::Incoming->new->store($message)) {
        Sympa::Report::reject_report_web(
            'intern',
            'cannot_send_remind',
            {   'from'     => $param->{'user'}{'email'},
                'listname' => $list->{'name'}
            },
            $param->{'action'},
            $list,
            $param->{'user'}{'email'},
            $robot
        );
        wwslog('err', 'Failed to send message for command REMIND');
        web_db_log(
            {   'robot'        => $robot,
                'list'         => $list->{'name'},
                'action'       => $param->{'action'},
                'parameters'   => "",
                'target_email' => "",
                'msg_id'       => '',
                'status'       => 'error',
                'error_type'   => 'internal',
                'user_email'   => $param->{'user'}{'email'},
            }
        );
        return undef;
    }

    Sympa::Report::notice_report_web('performed_soon', {},
        $param->{'action'});
    web_db_log(
        {   'robot'        => $robot,
            'list'         => $list->{'name'},
            'action'       => $param->{'action'},
            'parameters'   => "",
            'target_email' => "",
            'msg_id'       => '',
            'status'       => 'success',
            'error_type'   => '',
            'user_email'   => $param->{'user'}{'email'},
        }
    );
    return 'admin';
}

# Load list certificate.
sub do_load_cert {
    wwslog('info', '(%s)', $param->{'list'});

    my $cert = $list->get_cert('der');
    unless ($cert) {
        Sympa::Report::reject_report_web('user', 'missing_cert', {},
            $param->{'action'}, $list);
        wwslog('info', 'No cert for this list');
        return undef;
    }

    # don't you just HATE it when every single browser seems to want a
    # different content-type for certificates? order is important, as
    # everybody calls themselves "mozilla", and opera identifies as
    # IE if told so (but Opera doesn't do S/MIME anyways, it seems)
    my ($ua, $ct) = ($ENV{HTTP_USER_AGENT}, 'application/x-x509-email-cert');
    if ($ua =~ /MSIE/) {
        $ct = 'application/pkix-cert';
    }
    $param->{'bypass'} = 'extreme';
    my $filename = sprintf '%s.cer', $list->get_id();
    printf "Content-Disposition: attachment; filename=\"%s\"\n", $filename;
    printf "Content-Type: %s\n\n%s", $ct, $cert;
    return 1;
}

#*******************************************
# Function : do_upload_pictures
# Description : Creates a new pictures with a
#               uploaded file
#******************************************

sub do_upload_pictures {
    # Parameters of the uploaded file (from suboptions.tt2)
    my $fn = $query->param('uploaded_file');
    wwslog('info', '(%s, %s)', $fn, $param->{'user'}{'email'});

    # name of the file, without path
    my $fname;
    if ($fn =~ /([^\/\\]+)$/) {
        $fname = $1;
    }

    # type of the file
    my $filetype;
    if ($fn =~ /\.(jpg|jpeg|png|gif)$/i) {
        $filetype = lc $1;
    } else {
        $filetype = undef;
    }

    #uploaded file must have a name
    unless ($fname) {
        Sympa::Report::reject_report_web('user', 'no_name', {},
            $param->{'action'});
        wwslog('err', 'No file specified to upload');
        return 'suboptions';
    }

    unless ($filetype) {
        Sympa::Report::reject_report_web(
            'user',
            'cannot_upload',
            {   'path'   => $fname,
                'reason' => "your file does not have an authorized format."
            },
            $param->{'action'}
        );
        wwslog('err', 'Unauthorized format');
        return 'suboptions';
    }

    my $filename     = Digest::MD5::md5_hex($param->{'user'}{'email'});
    my $fullfilename = $filename . '.' . $filetype;

    my @filetmp;
    # check if there is not already a file for the user with a different
    # extension
    foreach my $filetmp ($list->find_picture_paths($param->{'user'}{'email'}))
    {
        rename $filetmp, $filetmp . '.tmp';
        push @filetmp, $filetmp;
    }

    my $picture_path = $list->get_picture_path($fullfilename);
    unless (creation_picture_file($list->get_picture_path, $fullfilename)) {
        Sympa::Report::reject_report_web('user', 'upload_failed',
            {'path' => $fullfilename},
            $param->{'action'});
        wwslog('err', 'Failed to create file %s', $picture_path);
        return 'suboptions';
    }

    my ($size) = (stat $picture_path)[7];
    unless (Conf::get_robot_conf($robot, 'pictures_max_size') > $size) {
        unlink $picture_path;
        foreach my $filetmp (@filetmp) {
            rename $filetmp . '.tmp', $filetmp;
        }
        Sympa::Report::reject_report_web(
            'user',
            'cannot_upload',
            {   'path'   => $fullfilename,
                'reason' => "Your file exceeds the authorized size."
            },
            $param->{'action'}
        );
        wwslog('err', 'Failed to upload pictures');
        return 'suboptions';
    }

    # message of success
    foreach my $filetmp (@filetmp) {
        unlink $filetmp . '.tmp';
    }
    wwslog('info', 'Upload of the pictures succeeded');
    return 'suboptions';
}

## Delete a picture file
sub do_delete_pictures {
    wwslog('info', '(%s, %s, %s)', $param->{'list'}, $robot,
        $param->{'user'}{'email'});

    my $email = $param->{'user'}{'email'};

    #deleted file must exist
    unless ($list->find_picture_filenames($email)) {
        Sympa::Report::reject_report_web('user', 'no_name', {},
            $param->{'action'}, $list);
        wwslog('err', 'No file exists to delete');
        return 'suboptions';
    }

    unless ($list->delete_list_member_picture($email)) {
        Sympa::Report::reject_report_web(
            'intern',
            'erase_file',
            {'file' => $list->find_picture_filenames($email)},
            $param->{'action'},
            $list,
            $param->{'user'}{'email'},
            $robot
        );
        wwslog(
            'err',
            'Failed to erase %s',
            $list->find_picture_filenames($email)
        );
        return undef;
    } else {
        wwslog('notice', 'File deleted successfully');
        return 'suboptions';
    }
}

####################################################
#  do_change_email_request
####################################################
#  Checks a user's new email address and passes it
#  to 'change_email'
#
# IN : -
#
# OUT : '1' | 'change_email'
#
####################################################
## Checks a users new email address by sending a ticket to the new email
## address
## and demanding that they click it to verify. Leads to 'change_email'
sub do_change_email_request {
    wwslog('info', '(%s)', $in{'new_email'});

    $param->{'new_email'} = $in{'new_email'};
    Sympa::send_notify_to_user($robot, 'ticket_to_send', $in{'new_email'},
        {email => $param->{'user'}{'email'}, ip => $ip})
        or return undef;
    return '1';
}

####################################################
#  do_change_email
####################################################
#  Changes a user's email address in Sympa environment
#
# IN : -
#
# OUT : '1' | 'pref' | undef
#
####################################################
## Change a user's email address in Sympa environment
sub do_change_email {
    wwslog('info', '(%s)', $in{'email'});

    my ($old_email, $new_email);
    my $edited_by_listmaster;

    unless ($in{'email'} || ($in{'old_email'} && $in{'new_email'})) {
        Sympa::Report::reject_report_web('user', 'Missing argument',
            {}, $param->{'action'});
        wwslog('err',
            "Lacking parameter : $in{'email'} or $in{'old_email'} or $in{'new_email'} "
        );
        web_db_log(
            {   'parameters' => $in{'email'},
                $in{'old_email'}, $in{'new_email'},
                'status'     => 'error',
                'error_type' => 'user'
            }
        );
    }

    ##  There are two ways to access this function 'change_email'. One from
    ##  the preferences page and one from the serveradmin page
    ## If the process comes from server admin it needs the variables
    ## 'old_email' and 'new_email'.
    if ($in{'old_email'} && $in{'new_email'}) {
        ## if variables old_email and new_email are present
        ## $edited_by_listmaster is set to one
        ## so that at the end of the function we can return to the SympaAdmin
        ## page
        ## instead of the preferences page
        $edited_by_listmaster = 1;
        unless (Sympa::is_listmaster($robot, $param->{'user'}{'email'})) {
            Sympa::Report::reject_report_web('auth', 'User is not Listmaster',
                {}, $param->{'action'});
            wwslog('err', 'Not listmaster');
            web_db_log(
                {   'parameters' => $in{'email'},
                    'status'     => 'error',
                    'error_type' => 'authorization'
                }
            );
            return undef;
        }

        $old_email = $in{'old_email'};
        $new_email = $in{'new_email'};
    } else {
        $old_email = $in{'email'};
        $new_email = $param->{'user'}{'email'};
    }

    my ($password, $newuser);

    if ($newuser = Sympa::User::get_global_user($old_email)) {

        $password = $newuser->{'password'};
    }

    ## Do the change_email
    my ($status, $failed_for) = Sympa::Admin::change_user_email(
        current_email => $old_email,
        new_email     => $new_email,
        robot         => $robot
    );
    unless (defined $status) {
        wwslog('err', 'Failed to change user email address');
        return undef;
    }

    foreach my $failed_list (@$failed_for) {
        Sympa::Report::reject_report_web(
            'user',
            'change_member_email_failed_included',
            {'listname' => $failed_list->{'name'}},
            $param->{'action'},
            $failed_list,
            $old_email,
            $robot
        );
        web_db_log(
            {   'robot'        => $in{'robot'},
                'list'         => $failed_list->{'name'},
                'action'       => $param->{'action'},
                'parameters'   => $new_email,
                'target_email' => $new_email,
                'msg_id'       => '',
                'status'       => 'error',
                'error_type'   => 'internal',
                'user_email'   => $old_email,
            }
        );
    }

    Sympa::Report::notice_report_web('performed', {}, $param->{'action'});

    ## Change email as list OWNER/MODERATOR
    my %updated_lists;
    foreach my $role ('owner', 'editor') {
        foreach my $list (Sympa::List::get_which($old_email, $robot, $role)) {

            ## Check if admin is include via an external datasource
            my ($admin_user) =
                @{$list->get_admins($role, filter => [email => $old_email])};
            if ($admin_user and $admin_user->{'included'}) {
                ## Notify listmaster
                Sympa::send_notify_to_listmaster(
                    $list,
                    'failed_to_change_included_admin',
                    {   'current_email' => $old_email,
                        'new_email'     => $new_email,
                        'datasource' =>
                            $list->get_datasource_name($admin_user->{'id'})
                    }
                );

                Sympa::Report::reject_report_web(
                    'user',
                    'change_admin_email_failed_included',
                    {'listname' => $list->{'name'}},
                    $param->{'action'},
                    $list,
                    $old_email,
                    $robot
                );
                wwslog(
                    'err',
                    'Could not change %s email for list %s because admin is included',
                    $role,
                    $list->{'name'}
                );
                next;
            }

            ## Go through owners/editors of the list
            foreach my $admin (@{$list->{'admin'}{$role}}) {
                next unless (lc($admin->{'email'}) eq lc($old_email));

                ## Update entry with new email address
                $admin->{'email'} = $new_email;
                $updated_lists{$list->{'name'}}++;
            }

            ## Update Db cache for the list
            $list->sync_include_admin();
            $list->save_config($param->{'session'}{'email'});
        }
    }
    ## Notify listmasters that list owners/moderators email have changed
    if (keys %updated_lists) {
        Sympa::send_notify_to_listmaster(
            $robot,
            'listowner_email_changed',
            {   'list'           => $list,
                'previous_email' => $old_email,
                'new_email'      => $new_email,
                'updated_lists'  => keys %updated_lists
            }
        );
    }

    ## Update User_table and remove existing entry first (to avoid duplicate
    ## entries)
    Sympa::User::delete_global_user($new_email,);

    unless (
        Sympa::User::update_global_user(
            $old_email,
            {   'email' => $new_email,

            }
        )
        ) {
        Sympa::Report::reject_report_web(
            'intern',
            'update_user_db_failed',
            {   'user'      => $param->{'user'},
                'old_email' => $old_email
            },
            $param->{'action'},
            '',
            $old_email,
            $robot
        );
        wwslog('info', 'Update failed');
        web_db_log(
            {   'robot'        => $robot,
                'list'         => $list->{'name'},
                'action'       => $param->{'action'},
                'parameters'   => "$new_email",
                'target_email' => "$new_email",
                'msg_id'       => '',
                'status'       => 'error',
                'error_type'   => 'internal',
                'user_email'   => $old_email,
            }
        );
        return undef;
    }

    ## Update netidmap_table
    unless (
        Sympa::Robot::update_email_netidmap_db(
            $robot, $old_email, $new_email
        )
        ) {
        Sympa::Report::reject_report_web(
            'intern',
            'update_netidmap_failed',
            {   'user'      => $param->{'user'},
                'old_email' => $old_email
            },
            $param->{'action'},
            '',
            $param->{'user'}{'email'},
            $robot
        );
        wwslog('info', 'Update failed');
        web_db_log(
            {   'target_email' => $old_email,
                'status'       => 'error',
                'error_type'   => 'internal'
            }
        );
        return undef;
    }

    if ($edited_by_listmaster == 1) {
        return 'serveradmin';
    }

    if ($in{'previous_action'}) {
        $in{'list'} = $in{'previous_list'};
        return $in{'previous_action'};
    } elsif ($edited_by_listmaster == 1) {
        return 'serveradmin';
    }

    return 'pref';

}

sub do_suspend {
    goto &do_suspend_request_action;    # "&" is required.
}

####################################################
#  do_suspend_request
####################################################
#  Suspend a subscription to one or more lists     #
#  for a given period: start date and end date     #
#  (or unlimited). The user may at any time        #
#  stop the suspension.                            #
#                                                  #
#  IN : -                                          #
#  OUT : 'loginrequest'                            #
#      | 'info' | undef                            #
#                                                  #
####################################################
# We display in the table the lists of the subscriber and the state in
# which they are.
# reception : - nomail/digest/mail ||
#             - . suspended  From XX-XX-XXXX To XX-XX-XXXX
sub do_suspend_request {
    wwslog('info', '');

    ## Sets the date of the field "start date" to "today"
    $param->{'d_day'} = POSIX::strftime('%d-%m-%Y', localtime time);
    _set_my_lists_info();

    # Compatibility with Sympa <= 6.1b.1.
    $param->{'which_info'} = $param->{'which'};
    $param->{'suspend_list'} =
        [grep { $_->{'listsuspend'} } values %{$param->{'which'}}];

    return 1;
}

sub _set_my_lists_info {
    my $which = {};

    # Set which_info unless in one list page
    if ($param->{'user'}{'email'} and ref $list ne 'Sympa::List') {
        my %get_which;

        foreach my $role (qw(member owner editor)) {
            $get_which{$role} = Sympa::List::get_lists(
                $robot,
                'filter' => [
                    $role      => $param->{'user'}{'email'},
                    '! status' => 'closed|family_closed'
                ],
            );
        }

        # Add lists information to 'which'
        foreach my $list (@{$get_which{member}}) {
            # Evaluate AuthZ scenario first
            my $result = Sympa::Scenario::request_action(
                $list,
                'visibility',
                $param->{'auth_method'},
                {   'sender'      => $param->{'user'}{'email'},
                    'remote_host' => $param->{'remote_host'},
                    'remote_addr' => $param->{'remote_addr'}
                }
            );
            next
                unless ref $result eq 'HASH'
                    and $result->{'action'} eq 'do_it';

            my $l = $list->{'name'};
            $which->{$l}{'subject'} = $list->{'admin'}{'subject'};
            $which->{$l}{'host'}    = $list->{'admin'}{'host'};
            $which->{$l}{'is_subscriber'} = 1;    # New on 6.2b.2.
            # Compat. < 6.2b.2.
            $which->{$l}{'info'} = 1;

            my $member_info =
                $list->get_list_member($param->{'user'}{'email'});
            my ($final_start_date, $final_end_date);
            if ($member_info->{'suspend'}) {
                if (defined $member_info->{'enddate'}
                    and $member_info->{'enddate'} < time) {
                    # If end date is < time, update the BDD by deleting the
                    # suspending's data
                    # FIXME: Is this required?
                    $list->restore_suspended_subscription(
                        $param->{'user'}{'email'});
                }
                $final_start_date =
                    $language->gettext_strftime("%d %b %Y",
                    localtime $member_info->{'startdate'})
                    if defined $member_info->{'startdate'};
                $final_end_date =
                    $language->gettext_strftime("%d %b %Y",
                    localtime $member_info->{'enddate'})
                    if defined $member_info->{'enddate'};
            }

            $member_info->{'reception'}  ||= 'mail';
            $member_info->{'visibility'} ||= 'noconceal';
            foreach my $mode ($list->available_reception_mode) {
                $param->{'reception'}{$list->{'name'}}{$mode}{'description'} =
                    Sympa::ListOpt::get_title($mode, 'reception');
                if ($member_info->{'reception'} eq $mode) {
                    $param->{'reception'}{$list->{'name'}}{$mode}
                        {'selected'} = ' selected';
                } else {
                    $param->{'reception'}{$list->{'name'}}{$mode}
                        {'selected'} = '';
                }
            }

            $which->{$l}{'listname'}      = $list->{'name'};
            $which->{$l}{'listdomain'}    = $list->{'domain'};
            $which->{$l}{'listreception'} = $member_info->{'reception'};
            $which->{$l}{'listsuspend'}   = $member_info->{'suspend'};
            $which->{$l}{'liststartdate'} = $final_start_date;
            $which->{$l}{'listenddate'}   = $final_end_date;
            $which->{$l}{'visibility'}    = $member_info->{'visibility'};
            $which->{$l}{'reception'} =
                $param->{'reception'}{$list->{'name'}};
            # Compat. < 6.2b.1.
            $which->{$l}{'display'} = $which->{$l}{'listsuspend'};
        }
        foreach my $list (@{$get_which{owner}}) {
            my $l = $list->{'name'};

            $which->{$l}{'subject'} = $list->{'admin'}{'subject'};
            $which->{$l}{'host'}    = $list->{'admin'}{'host'};
            $which->{$l}{'is_owner'} = 1;    # New on 6.2b.2.
            # Compat. < 6.2b.1.
            $which->{$l}{'info'}  = 1;
            $which->{$l}{'admin'} = 1;
        }
        foreach my $list (@{$get_which{editor}}) {
            my $l = $list->{'name'};

            $which->{$l}{'subject'} = $list->{'admin'}{'subject'};
            $which->{$l}{'host'}    = $list->{'admin'}{'host'};
            $which->{$l}{'is_editor'} = 1;    # New on 6.2b.2.
            # Compat. < 6.2b.1.
            $which->{$l}{'info'}  = 1;
            $which->{$l}{'admin'} = 1;
        }
    }

    $param->{'which'} = $which;
}

####################################################
#  do_suspend_request_action
####################################################
#  Suspend a subscription for lists.               #
#  Action from the suspend form.                   #
#                                                  #
#  IN : %in : HASH with the form's values          #
#  OUT : 'pref' : action                           #
#      | 'info' | undef                            #
####################################################
sub do_suspend_request_action {
    wwslog('info', '');

    my $day1;
    my $month1;
    my $year1;
    my $day2;
    my $month2;
    my $year2;
    my @lists;
    my $data;

    my $previous_action = $in{'previous_action'} || 'suspend_request';

    if ($in{'sub_action'} eq 'suspendsave') {

        # to retrieve the selected list
        @lists = split /\0/, $in{'listname'};
        my @list_selected;
        foreach my $list (@lists) {
            unless ($list eq '') {
                push @list_selected, $list;
            }
        }

        if ($list_selected[0] eq '') {
            Sympa::Report::reject_report_web(
                'user',
                'missing_arg',
                {   'argument' =>
                        'must picked one or more list(s) you are subscribed'
                },
                $param->{'action'}
            );
            wwslog('info',
                'Must picked one or more list(s) you are subscribed');
            return $previous_action;
        }

        if ($in{'date_deb'}) {
            ($day1, $month1, $year1) = split(/\-/, $in{'date_deb'});
            $month1 = $month1 - 1;

            if (   ($day1 =~ /([0-9]*)/)
                && ($month1 =~ /([0-9]*)/)
                && ($year1  =~ /([0-9]*)/)) {
                if (   ((1 <= $day1) && ($day1 <= 31))
                    && ((0 <= $month1) && ($month1 <= 11))
                    && (1900 <= $year1)) {
                    ## Return an epoch date
                    $data->{'startdate'} =
                        Time::Local::timelocal(0, 0, 0, $day1, $month1,
                        $year1);
                } else {
                    Sympa::Report::reject_report_web('user', 'missing_arg',
                        {'argument' => 'Start Date doesn\'t exist.'},
                        $param->{'action'});
                    wwslog('info', 'Date doesn\'t exist');
                    return $previous_action;
                }
            } else {
                Sympa::Report::reject_report_web('user', 'missing_arg',
                    {'argument' => 'Start Date doesn\'t exist.'},
                    $param->{'action'});
                wwslog('info', 'Date doesn\'t exist');
                return $previous_action;
            }
            ## Case 1 : Start date & End date (without indefinite)
            if (($in{'date_fin'}) && (!$in{'indefinite'})) {
                ($day2, $month2, $year2) = split(/\-/, $in{'date_fin'});
                $month2 = $month2 - 1;

                if (   ($day2 =~ /([0-9]*)/)
                    && ($month2 =~ /([0-9]*)/)
                    && ($year2  =~ /([0-9]*)/)) {
                    if (   ((1 <= $day2) && ($day2 <= 31))
                        && ((0 <= $month2) && ($month2 <= 11))
                        && (1900 <= $year2)) {
                        ## Return an epoch date
                        $data->{'enddate'} =
                            Time::Local::timelocal(0, 0, 0, $day2, $month2,
                            $year2);
                    } else {
                        Sympa::Report::reject_report_web('user',
                            'missing_arg',
                            {'argument' => 'End Date doesn\'t exist.'},
                            $param->{'action'});
                        wwslog('info', 'Date doesn\'t exist');
                        return $previous_action;
                    }
                } else {
                    Sympa::Report::reject_report_web('user', 'missing_arg',
                        {'argument' => 'End Date doesn\'t exist.'},
                        $param->{'action'});
                    wwslog('info', 'Date doesn\'t exist');
                    return $previous_action;
                }

                unless ($data->{'startdate'} <= $data->{'enddate'}) {
                    Sympa::Report::reject_report_web(
                        'user',
                        'missing_arg',
                        {   'argument' =>
                                'The start date must be less than the end date.'
                        },
                        $param->{'action'}
                    );
                    wwslog('info',
                        'The start date must be less than the end date.');
                    return $previous_action;
                }
                ## Case 2 : Start date & without indefinite (without end date)
            } elsif ((!$in{'date_fin'}) && ($in{'indefinite'})) {
                $data->{'enddate'} = undef;
            } else {
                Sympa::Report::reject_report_web(
                    'user',
                    'missing_arg',
                    {   'argument' =>
                            'Choose end date (dd/mm/yyyy) or indefinite end date'
                    },
                    $param->{'action'}
                );
                wwslog('info',
                    'Missing argument for the end date or syntax error : dd/mm/yyyy or must choose a end date or indefinite end date'
                );
                return $previous_action;
            }
        } else {
            Sympa::Report::reject_report_web('user', 'missing_arg',
                {'argument' => 'Miss start date (dd/mm/yyyy)'},
                $param->{'action'});
            wwslog('info',
                'Missing argument for the start date or syntax error : dd/mm/yyyy'
            );
            return $previous_action;
        }

        ## Suspend subscription
        foreach my $list (@list_selected) {
            unless (
                Sympa::List::suspend_subscription(
                    $param->{'user'}{'email'},
                    $list, $data, $robot
                )
                ) {
                wwslog('info', 'Can\'t do List suspend_subscription');
                return $previous_action;
            }
        }

        Sympa::Report::notice_report_web('performed', {}, $in{'sub_action'});
    }
    ## Restore suspended subscription
    elsif ($in{'sub_action'} eq 'suspendstop') {

        # to renew membership lists selected
        @lists = split /\0/, $in{'listname'};
        foreach my $line (@lists) {
            my $list = Sympa::List->new($line, $robot);
            next unless $list;
            $list->restore_suspended_subscription($param->{'user'}{'email'});
        }

        if ($lists[0] eq '') {
            Sympa::Report::reject_report_web('user', 'missing_arg',
                {'argument' => 'must picked one or more list(s)'},
                $param->{'action'});
            wwslog('info', 'Must picked one or more list(s)');
            return $previous_action;
        }
        Sympa::Report::notice_report_web('performed', {},
            "Resume the subscription for the list(s)");

    }
    ## Unsubscribe from the selected lists
    elsif ($in{'sub_action'} eq 'signoff') {

        # lists selected
        @lists = split /\0/, $in{'listname'};
        my $report = "";
        foreach my $line (@lists) {

            my $unsub_list = Sympa::List->new($line, $robot);
            unless ($unsub_list) {
                wwslog('info', 'List %s unknown', $unsub_list);
                return undef;
            }

            my %result = unsubscribe($param->{'user'}{'email'}, $unsub_list);
            if ($result{'success'} == 1) {
                if ($result{'details'} eq 'sent_to_owner') {
                    $report .= $language->gettext_sprintf(
                        "Your unsubscription request to list %s was sent to the list owner.",
                        $unsub_list->{'name'}
                    );
                } else {
                    $report .= $language->gettext_sprintf(
                        "You were successfully unsubscribed from list %s.",
                        $unsub_list->{'name'});
                }
            } else {
                if ($result{'category_error'} eq 'auth') {
                    $report .= $language->gettext_sprintf(
                        "Unsubscription from list %s denied: Unsubscription from this list is closed.",
                        $unsub_list->{'name'}
                    );
                } else {
                    $report .= $language->gettext_sprintf(
                        "Unsubscription from list %s failed.",
                        $unsub_list->{'name'});
                }
            }
            $report .= "\n";

        }
        if ($lists[0] eq '') {
            Sympa::Report::reject_report_web('user', 'missing_arg',
                {'argument' => 'must picked one or more list(s)'},
                $param->{'action'});
            wwslog('info', 'Must picked one or more list(s)');
            return $previous_action;
        }

        Sympa::Report::notice_report_web($report, {}, '');
    } else {
        Sympa::Report::reject_report_web('user', 'unknown_action', {},
            $in{'sub_action'}, $list);
        wwslog('info', 'Unknown action %s', $in{'sub_action'});
        return undef;
    }

    return $previous_action;
}

####################################################
#  do_compose_mail
####################################################
sub do_compose_mail {
    wwslog('info', '(subaction=%s)', $in{'subaction'});

    unless ($param->{'may_post'}) {
        Sympa::Report::reject_report_web('auth', $param->{'may_post_reason'},
            {}, $param->{'action'}, $list);
        wwslog('info', 'May not send message');
        return undef;
    }

    if (    Conf::get_robot_conf($robot, 'use_html_editor')
        and Conf::get_robot_conf($robot, 'use_html_editor') eq 'on') {
        $param->{'use_html_editor'} =
            Conf::get_robot_conf($robot, 'use_html_editor');
        if (    Conf::get_robot_conf($robot, 'html_editor_url')
            and Conf::get_robot_conf($robot, 'html_editor_url') =~
            /^([-.\w]+:\/\/|\/)/i) {
            $param->{'html_editor_url'} =
                Conf::get_robot_conf($robot, 'html_editor_url');
        } elsif (Conf::get_robot_conf($robot, 'html_editor_url')) {
            $param->{'html_editor_url'} =
                  Conf::get_robot_conf($robot, 'static_content_url') . '/'
                . Conf::get_robot_conf($robot, 'html_editor_url');
        }
        $param->{'html_editor_init'} =
            Conf::get_robot_conf($robot, 'html_editor_init');
    }

    # Set the subaction to html_news_letter or undef
    $param->{'subaction'} = $in{'subaction'};
    if ($in{'to'}) {
        # In archive we hide email replacing @ by ' '. Here we must do the
        # reverse transformation
        $in{'to'} =~ s/ /\@/g;
        $param->{'to'} = $in{'to'};
    } else {
        $param->{'to'} = $list->get_list_address();
    }
    foreach my $recipient (split(',', $param->{'to'})) {
        (   $param->{'recipients'}{$recipient}{'local_to'},
            $param->{'recipients'}{$recipient}{'domain_to'}
        ) = split('@', $recipient);
    }
    # headers will be encoded later.
    #XXX$param->{'subject'}= &MIME::Words::encode_mimewords($in{'subject'});
    $param->{'subject'} = $in{'subject'};
    $param->{'in_reply_to'} =
        Sympa::Tools::Text::canonic_message_id($in{'in_reply_to'});
    $param->{'message_id'} = Sympa::unique_message_id($robot);

    if ($list->is_there_msg_topic()) {

        $param->{'request_topic'} = 1;

        foreach my $top (@{$list->{'admin'}{'msg_topic'}}) {
            if ($top->{'name'}) {
                push(@{$param->{'available_topics'}}, $top);
            }
        }
        $param->{'topic_required'} = $list->is_msg_topic_tagging_required();
    }

    $param->{'merge_feature'} =
        Sympa::Tools::Data::smart_eq($list->{'admin'}{'merge_feature'}, 'on');

    return 1;
}

####################################################
#  do_send_mail
####################################################
#  Sends a message to a list by the Web interface
#  or an html page getting its url.
#
# IN : -
#
# OUT : 'loginrequest'
#      | 'info' | undef
#
####################################################
sub do_send_mail {

    wwslog('info', '');

    my $to;

    # Send the message to the list or to the sender as clicking the send to
    # the list or to me.
    # First if : send to the list
    if ($in{'sub_action'} eq 'sendmailtolist') {
        # In archive we hide email replacing @ by ' '. Here we must do the
        # reverse transformation
        $in{'to'} =~ s/ /\@/g;
        $to = $in{'to'};

        unless ($to) {
            unless ($param->{'list'}) {
                Sympa::Report::reject_report_web('user', 'missing_arg',
                    {'argument' => 'list'},
                    $param->{'action'});
                wwslog('info', 'No list');
                web_db_log(
                    {   'robot'        => $robot,
                        'list'         => $list->{'name'},
                        'action'       => $param->{'action'},
                        'parameters'   => "",
                        'target_email' => "",
                        'msg_id'       => '',
                        'status'       => 'error',
                        'error_type'   => 'no_list',
                        'user_email'   => $param->{'user'}{'email'},
                    }
                );
                return undef;
            }
            $to = $list->get_list_address();
        }
        unless ($param->{'may_post'}) {
            Sympa::Report::reject_report_web('auth',
                $param->{'may_post_reason'},
                {}, $param->{'action'}, $list);
            wwslog('info', 'May not send message');
            web_db_log(
                {   'robot'        => $robot,
                    'list'         => $list->{'name'},
                    'action'       => $param->{'action'},
                    'parameters'   => "",
                    'target_email' => "",
                    'msg_id'       => '',
                    'status'       => 'error',
                    'error_type'   => 'authorization',
                    'user_email'   => $param->{'user'}{'email'},
                }
            );
            return undef;
        }
    }

    # Determine user's character set.
    my $charset = Conf::lang2charset($language->get_lang);

    # Take the sender mail
    my $from = $param->{'user'}{'email'};

    # Send the mail to the sender. To test his message
    # Second if : send to the sender "send to me"
    if ($in{'sub_action'} eq 'sendmailtome') {
        #Set the sender mail to the addressee
        $to = $from;
    }

    if (defined $param->{'subscriber'}) {
        $from =
            Sympa::Tools::Text::addrencode($from,
            $param->{'subscriber'}{'gecos'}, $charset);
    }

    # Encode subject.
    my $encoded_subject = MIME::EncWords::encode_mimewords(
        Encode::decode_utf8($in{'subject'}),
        Charset     => $charset,
        Encoding    => 'A',
        Field       => 'Subject',
        Replacement => 'FALLBACK'
    ) if defined $in{'subject'} and $in{'subject'} =~ /\S/;

    ##--------------- TOPICS --------------------
    my $list_topics;
    if ($list->is_there_msg_topic()) {
        my @msg_topics;

        foreach my $msg_topic (@{$list->{'admin'}{'msg_topic'}}) {
            my $var_name = "topic_" . "$msg_topic->{'name'}";
            if ($in{"$var_name"}) {
                push @msg_topics, $msg_topic->{'name'};
            }
        }

        $list_topics = join(',', @msg_topics);
    }

    if (!$list_topics && $list->is_msg_topic_tagging_required()) {
        Sympa::Report::reject_report_web('user', 'msg_topic_missing', {},
            $param->{'action'});
        wwslog('info', 'Message(s) without topic but in a required list');
        web_db_log(
            {   'robot'        => $robot,
                'list'         => $list->{'name'},
                'action'       => $param->{'action'},
                'parameters'   => "",
                'target_email' => "",
                'msg_id'       => '',
                'status'       => 'error',
                'error_type'   => 'no_topic',
                'user_email'   => $param->{'user'}{'email'},
            }
        );
        return undef;
    }

    # "In-Reply-To:" field, eliminating hostile characters.
    my $in_reply_to =
        Sympa::Tools::Text::canonic_message_id($in{'in_reply_to'});
    undef $in_reply_to if $in_reply_to and $in_reply_to =~ /[\s<>]/;

    ##--------------- send an html page or a message -------------------
    my $message;

    if ($in{'html_news_letter'}) {
        # url and uploaded_file should not be both empty -> missing argument
        unless ($in{'url'} =~ /\S/ or $in{'uploaded_file'} =~ /\S/) {
            Sympa::Report::reject_report_web('user', 'missing_post_source',
                {}, $param->{'action'});
            wwslog('info', 'Missing URL and uploaded file');
            return 'compose_mail';
        }

        # url and uploaded_file should not be both filled: we could not chooe
        # which one to use.
        if ($in{'url'} =~ /\S/ and $in{'uploaded_file'} =~ /\S/) {
            Sympa::Report::reject_report_web('user',
                'two_post_sources_defined', {}, $param->{'action'});
            wwslog(
                'info',
                'User specified both an URL (%s) and a file to upload (%s). Can\'t choose between them',
                $in{'url'},
                $in{'uploaded_file'}
            );
            return 'compose_mail';
        }
        my $page_source;
        if ($in{'uploaded_file'} =~ /\S/) {
            my $fh = $query->upload('uploaded_file');
            unless ($fh) {
                wwslog('err', 'Can\'t upload %s', $in{'uploaded_file'});
                Sympa::Report::reject_report_web(
                    'intern',
                    'cannot_upload',
                    {'path' => $in{'uploaded_file'}},
                    $param->{'action'},
                    $list,
                    $param->{'user'}{'email'},
                    $robot
                );
                web_db_log(
                    {   'robot'        => $robot,
                        'list'         => $list->{'name'},
                        'action'       => $param->{'action'},
                        'parameters'   => $in{'uploaded_file'},
                        'target_email' => "",
                        'msg_id'       => '',
                        'status'       => 'error',
                        'error_type'   => 'internal',
                        'user_email'   => $param->{'user'}{'email'},
                    }
                );
                return undef;
            }

            #FIXME: Check the size!
            $page_source = do { local $RS; <$fh> };
            close $fh;

            # If uploaded content looks like URL, escape it by newline.
            if ($page_source and $page_source =~ m{^[-\w]+://}) {
                $page_source = "\n$page_source";
            }
        } else {
            $page_source = $in{'url'};
        }

        # Generate message from page source.
        # FIXME: Always UTF-8 is assumed: Pages by other charset are broken.
        my $mail_html = MIME::Lite::HTML->new(
            HTMLCharset    => 'utf-8',
            TextCharset    => 'utf-8',
            TextEncoding   => '8bit',
            HTMLEncoding   => '8bit',
            IncludeType    => 'cid',
            remove_jscript => '1',       #delete the scripts in the html
            'From'         => $from,
            'To'           => $to,
            'Message-Id' => $in{'message_id'},
            (   $in_reply_to ? ('In-Reply-To' => '<' . $in_reply_to . '>')
                : ()
            ),
            (     (defined $encoded_subject) ? ('Subject' => $encoded_subject)
                : ()
            ),
        );
        # Restrict protocols of URL entered _and_ URLs embedded in the pages.
        $mail_html->{_AGENT}
            ->protocols_allowed(['http', 'https', 'ftp', 'nntp']);

        # parse return the MIME::Lite part to send
        my $part = eval { $mail_html->parse($page_source) };
        unless ($part) {
            Sympa::Report::reject_report_web('user', 'unable_to_parse', {},
                $param->{'action'});
            wwslog(
                'info',
                'A MIME part could not be created with the supplied data, %s, %s',
                $in{'url'},
                $in{'uploaded_file'}
            );
            return undef;
        }
        $message = Sympa::Message->new($part->as_string, context => $list);
        $message->reformat_utf8_message([], 'utf-8');
    } else {
        ## Message body should not be empty
        if ($in{'body'} =~ /^\s*$/) {
            Sympa::Report::reject_report_web('user', 'missing_arg',
                {'argument' => 'body'},
                $param->{'action'});
            wwslog('info', 'Missing body');
            web_db_log(
                {   'robot'        => $robot,
                    'list'         => $list->{'name'},
                    'action'       => $param->{'action'},
                    'parameters'   => "",
                    'target_email' => "",
                    'msg_id'       => '',
                    'status'       => 'error',
                    'error_type'   => 'no_body',
                    'user_email'   => $param->{'user'}{'email'},
                }
            );
            return undef;
        }

        my $msg_string = sprintf "From: %s\nTo: %s\nMessage-Id: %s\n",
            $from, $to, $in{'message_id'};
        $msg_string .= sprintf "In-Reply-To: <%s>\n", $in_reply_to
            if $in_reply_to;
        $msg_string .= sprintf "Subject: %s\n", $encoded_subject
            if defined $encoded_subject;
        if (Conf::get_robot_conf($robot, 'use_html_editor')) {
            $msg_string .= sprintf "Content-Type: text/html\n\n%s",
                $in{'body'};
        } else {
            $msg_string .= sprintf "Content-Type: text/plain\n\n%s",
                $in{'body'};
        }
        $msg_string =~ s/(?<!\n)\z/\n/;

        $message = Sympa::Message->new($msg_string, context => $list);
        $message->reformat_utf8_message([], $charset);
    }

    ## Roughly check TT2 syntax for merge_feature.
    if (Sympa::Tools::Data::smart_eq($list->{'admin'}{'merge_feature'}, 'on'))
    {
        my $new_message = $message->dup;
        unless (defined $new_message->personalize($list)) {
            # FIXME: Get last_error of template object.
            Sympa::Report::reject_report_web('user', 'merge_failed',
                {'error' => 'Syntax error'},
                $param->{'action'});
            return 'compose_mail';
        }
    }

    # - Message bound for list will be injected into incoming spool directly.
    #   In this case message will have "md5" authentication level.
    # - Message bound for user will be injected into bulk spool.
    #FIXME: Check destinations: they should be list, original sender, user or
    # other_email.
    my @to_list = grep { $_ eq $list->get_list_address } split /\s*,\s*/, $to;
    my @to_user =
        grep {
                $_
            and $_ ne $list->get_list_address
            and $_ ne $param->{'user'}{'email'}
        } split /\s*,\s*/, $to;
    my @to_me =
        grep {
                $_
            and $_ ne $list->get_list_address
            and $_ eq $param->{'user'}{'email'}
        } split /\s*,\s*/, $to;

    if (@to_me) {
        my $u_message = $message->dup;

        # Since some users may send message to themselves to test message
        # decoration and/or personalization, add such processing.
        # - Add footer / header.
        $u_message->prepare_message_according_to_mode('mail', $list);
        # - Shelve personalization.
        $u_message->{shelved}{merge} = 1
            if Sympa::Tools::Data::smart_eq($list->{'admin'}{'merge_feature'},
            'on');

        $u_message->{envelope_sender} =
            Conf::get_robot_conf($robot, 'request');
        $u_message->{priority} =
            Conf::get_robot_conf($robot, 'sympa_priority');

        unless (defined $bulk->store($u_message, [@to_me])) {
            Sympa::Report::reject_report_web(
                'intern',
                'cannot_send_mail',
                {   'from'     => $param->{'user'}{'email'},
                    'listname' => $list->{'name'}
                },
                $param->{'action'},
                $list,
                $param->{'user'}{'email'},
                $robot
            );
            wwslog('err', 'Failed to send message for %s', $to);
            web_db_log(
                {   'robot'        => $robot,
                    'list'         => $list->{'name'},
                    'action'       => $param->{'action'},
                    'parameters'   => $to,
                    'target_email' => "",
                    'msg_id'       => '',
                    'status'       => 'error',
                    'error_type'   => 'internal',
                    'user_email'   => $param->{'user'}{'email'},
                }
            );
            return undef;
        }
    }

    if (@to_user) {
        my $u_message = $message->dup;
        # Set <sympa-request> address as envelope sender.
        $u_message->{envelope_sender} = Sympa::get_address($robot, 'owner');
        $u_message->{priority} =
            Conf::get_robot_conf($robot, 'sympa_priority');

        unless (defined $bulk->store($u_message, [@to_user])) {
            Sympa::Report::reject_report_web(
                'intern',
                'cannot_send_mail',
                {   'from'     => $param->{'user'}{'email'},
                    'listname' => $list->{'name'}
                },
                $param->{'action'},
                $list,
                $param->{'user'}{'email'},
                $robot
            );
            wwslog('err', 'Failed to send message for %s', $to);
            web_db_log(
                {   'robot'        => $robot,
                    'list'         => $list->{'name'},
                    'action'       => $param->{'action'},
                    'parameters'   => $to,
                    'target_email' => "",
                    'msg_id'       => '',
                    'status'       => 'error',
                    'error_type'   => 'internal',
                    'user_email'   => $param->{'user'}{'email'},
                }
            );
            return undef;
        }
    }

    if (@to_list and $in{'sub_action'} eq 'sendmailtolist') {
        # TAG
        if ($list_topics) {
            Sympa::Topic->new(topic => $list_topics, method => 'sender')
                ->store($message);
        }

        my $l_message = $message->dup;
        $l_message->{envelope_sender} = $param->{'user'}{'email'};
        $l_message->{sender}          = $param->{'user'}{'email'};
        $l_message->{md5_check}       = 1;

        unless (Sympa::Spool::Incoming->new->store($l_message)) {
            Sympa::Report::reject_report_web(
                'intern',
                'cannot_send_mail',
                {   'from'     => $param->{'user'}{'email'},
                    'listname' => $list->{'name'}
                },
                $param->{'action'},
                $list,
                $param->{'user'}{'email'},
                $robot
            );
            wwslog('err', 'Failed to send message for list %s', $list);
            web_db_log(
                {   'parameters' => join(',', @to_list),
                    'status'     => 'error',
                    'error_type' => 'internal'
                }
            );
            return undef;
        }
    }

    Sympa::Report::notice_report_web('performed', {}, $param->{'action'});
    web_db_log(
        {   'robot'        => $robot,
            'list'         => $list->{'name'},
            'action'       => $param->{'action'},
            'parameters'   => "",
            'target_email' => "",
            'msg_id'       => '',
            'status'       => 'success',
            'error_type'   => '',
            'user_email'   => $param->{'user'}{'email'},
        }
    );

    if ($in{'sub_action'} eq 'sendmailtome') {
        $param->{'body'} = $in{'body'};
        return 'compose_mail';
    } else {
        return 'info';
    }
}

####################################################
#  do_request_topic
####################################################
#  Web page for a sender to tag his mail in message
#  topic context.
#
# IN : -
#
# OUT : '1' | 'loginrequest' | undef
#
####################################################
sub do_request_topic {
    wwslog('info', '(%s)', $in{'authkey'});

    unless ($list->is_there_msg_topic()) {
        Sympa::Report::reject_report_web('user', 'no_topic', {},
            $param->{'action'}, $list);
        wwslog('info', 'List without topic message');
        return undef;
    }

    foreach my $top (@{$list->{'admin'}{'msg_topic'}}) {
        if ($top->{'name'}) {
            push(@{$param->{'available_topics'}}, $top);
        }
    }

    $param->{'to'}      = $list->get_list_address();
    $param->{'authkey'} = $in{'authkey'};

    my $spool_held =
        Sympa::Spool::Held->new(context => $list, authkey => $in{'authkey'});
    my ($message, $handle);
    while (1) {
        ($message, $handle) = $spool_held->next(no_lock => 1);
        last unless $handle;
        last if $message;
    }
    unless ($message) {
        Sympa::Report::reject_report_web('intern', 'already_confirmed', {},
            $param->{'action'}, $list, $param->{'user'}{'email'}, $robot);
        wwslog('notice', 'Cannot get message with key <%s> for list %s',
            $in{'authkey'}, $list);
        return undef;
    }

    # headers will be encoded later.
    $param->{'subject'} = $message->{'decoded_subject'};
    $param->{'subject'} =
        Sympa::Tools::WWW::escape_html_minimum($param->{'subject'});    #XXX
    $param->{'from'} = $message->get_decoded_header('From');
    $param->{'from'} =
        Sympa::Tools::WWW::escape_html_minimum($param->{'from'});       #XXX
    $param->{'date'} = $message->get_decoded_header('Date');
    $param->{'date'} =
        Sympa::Tools::WWW::escape_html_minimum($param->{'date'});       #XXX
    $param->{'message_id'} = $message->{'message_id'};
    $param->{'body'}       = $message->get_plain_body;                  #FIXME

    $param->{'topic_required'} = $list->is_msg_topic_tagging_required();

    return 1;
}

####################################################
#  do_tag_topic_by_sender
####################################################
#  Tag a mail by its sender : tag the mail and
#  send a command CONFIRM for it
#
# IN : -
#
# OUT : 'loginrequest' | 'info' | undef
#
####################################################
sub do_tag_topic_by_sender {
    wwslog('info', '');

    my $spool_held =
        Sympa::Spool::Held->new(context => $list, authkey => $in{'authkey'});
    my ($message, $handle);
    while (1) {
        ($message, $handle) = $spool_held->next(no_lock => 1);
        last unless $handle;
        last if $message;
    }
    unless ($message) {
        Sympa::Report::reject_report_web('intern', 'already_confirmed', {},
            $param->{'action'}, $list, $param->{'user'}{'email'}, $robot);
        wwslog('info', 'cannot get message with key <%s> for list %s',
            $in{'authkey'}, $list);
        return undef;
    }
    my $sender = $message->{'sender'};

    unless ($list->is_there_msg_topic()) {
        Sympa::Report::reject_report_web('user', 'no_topic', {},
            $param->{'action'}, $list);
        wwslog('info', 'List without topic message');
        return undef;
    }

    my @msg_topics;
    foreach my $msg_topic (@{$list->{'admin'}{'msg_topic'}}) {
        my $var_name = "topic_" . "$msg_topic->{'name'}";
        if ($in{"$var_name"}) {
            push @msg_topics, $msg_topic->{'name'};
        }
    }
    my $list_topics = join(',', @msg_topics);

    if (!$list_topics && $list->is_msg_topic_tagging_required()) {
        Sympa::Report::reject_report_web('user', 'msg_topic_missing', {},
            $param->{'action'}, $list);
        wwslog('info', 'Message without topic but in a required list');
        return undef;
    }

    # TAG
    Sympa::Topic->new(topic => $list_topics, method => 'sender')
        ->store($message);

    ## CONFIRM
    # Commands are injected into incoming spool directly with "md5"
    # authentication level.
    my $time        = time;
    my $cmd_message = Sympa::Message->new(
        sprintf("\n\nQUIET CONFIRM %s\n", $in{'authkey'}),
        context         => $robot,
        envelope_sender => Conf::get_robot_conf($robot, 'request'),
        sender          => $sender,
        md5_check       => 1,
        message_id      => sprintf('<%s@wwsympa>', $time)
    );
    $cmd_message->add_header('Content-Type', 'text/plain; Charset=utf-8');

    unless (Sympa::Spool::Incoming->new->store($cmd_message)) {
        Sympa::Report::reject_report_web(
            'intern',
            'cannot_send_mail',
            {   'from'     => $param->{'user'}{'email'},
                'listname' => $list->{'name'}
            },
            $param->{'action'},
            $list,
            $param->{'user'}{'email'},
            $robot
        );
        wwslog('err', 'Failed to send message to comfirm message %s',
            $message);
        return undef;
    }

    Sympa::Report::notice_report_web('performed_soon', {},
        $param->{'action'});
    return 'info';
}

sub do_search_user {
    wwslog('info', '');

    if ($in{'email'} =~ /[<>\\\*\$]/) {
        Sympa::Report::reject_report_web('user', 'syntax_errors',
            {'params' => 'email'},
            $param->{'action'});
        wwslog('err', 'Syntax error');
        return undef;
    }

    foreach my $role ('member', 'owner', 'editor') {
        foreach my $list (Sympa::List::get_which($in{'email'}, $robot, $role))
        {
            my $l = $list->{'name'};

            next unless (defined $list);
            $param->{'which'}{$l}{'subject'} = $list->{'admin'}{'subject'};
            $param->{'which'}{$l}{'host'}    = $list->{'admin'}{'host'};

            # show the requestor role not the requested one
            if (   $list->is_admin('owner', $param->{'user'}{'email'})
                or $list->is_admin('editor', $param->{'user'}{'email'})
                or Sympa::is_listmaster($list, $param->{'user'}{'email'})) {
                $param->{'which'}{$l}{'admin'} = 1;
            }

            if ($role eq 'member') {
                $param->{'which'}{$l}{'is_member'} = 1;
                $param->{'which'}{$l}{'reception'} =
                    $list->{'user'}{'reception'};
                $param->{'which'}{$l}{'include_source'} =
                    $list->{'user'}{'include_source'};
                $param->{'which'}{$l}{'bounce'} = $list->{'user'}{'bounce'};
                $param->{'which'}{$l}{'topic'}  = $list->{'user'}{'topic'};
                $param->{'which'}{$l}{'included'} =
                    $list->{'user'}{'included'}
                    if ($list->{'user'}{'included'} == 1);
                $param->{'which'}{$l}{'subscribed'} =
                    $list->{'user'}{'subscribed'}
                    if ($list->{'user'}{'subscribed'} == 1);
                my $un = $list->{'user'}{'subscribed'};
#		 $param->{'which'}{$l}{'subscribed'} = 1;

            } elsif ($role eq 'owner') {
                $param->{'which'}{$l}{'is_owner'} = 1;
            } elsif ($role eq 'editor') {
                $param->{'which'}{$l}{'is_editor'} = 1;
            }
        }
    }

    $param->{'email'} = $in{'email'};

    unless (defined $param->{'which'}) {
        Sympa::Report::reject_report_web('user', 'no_entry',
            {'email' => $in{'email'}},
            $param->{'action'});
        wwslog('info', 'No entry for %s', $in{'email'});
        return 'serveradmin';
    }

    return 1;
}

## Set language
sub do_set_lang {
    wwslog('info', '(%s)', $in{'lang'});

    my $lang;
    if ($in{'lang'} and $lang = $language->set_lang($in{'lang'})) {
        $session->{'lang'} = $lang;
        $param->{'lang'}   = $lang;
        # compatibility: old-style locale.
        $param->{'locale'} = Sympa::Language::lang2oldlocale($lang);
        # compatibility: 6.1.
        $param->{'lang_tag'} = $lang;

        #FIXME:Should users' language preferences be changed?
        if ($param->{'user'}{'email'}) {
            if (Sympa::User::is_global_user($param->{'user'}{'email'})) {
                unless (
                    Sympa::User::update_global_user(
                        $param->{'user'}{'email'},
                        {'lang' => $lang}
                    )
                    ) {
                    Sympa::Report::reject_report_web(
                        'intern',
                        'update_user_db_failed',
                        {'user' => $param->{'user'}},
                        $param->{'action'},
                        '',
                        $param->{'user'}{'email'},
                        $robot
                    );
                    wwslog('info', 'Update failed');
                    web_db_log(
                        {   'robot'        => $robot,
                            'list'         => $list->{'name'},
                            'action'       => $param->{'action'},
                            'parameters'   => "$in{'lang'}",
                            'target_email' => "$param->{'user'}{'email'}",
                            'msg_id'       => '',
                            'status'       => 'error',
                            'error_type'   => 'internal',
                            'user_email'   => $param->{'user'}{'email'},
                        }
                    );
                    return undef;
                }
            } else {
                unless (
                    Sympa::User::add_global_user(
                        {   'email' => $param->{'user'}{'email'},
                            'lang'  => $lang
                        }
                    )
                    ) {
                    Sympa::Report::reject_report_web(
                        'intern',
                        'add_user_db_failed',
                        {'user' => $param->{'user'}},
                        $param->{'action'},
                        '',
                        $param->{'user'}{'email'},
                        $robot
                    );
                    wwslog('info', 'Update failed');
                    web_db_log(
                        {   'robot'        => $robot,
                            'list'         => $list->{'name'},
                            'action'       => $param->{'action'},
                            'parameters'   => "$in{'lang'}",
                            'target_email' => "$param->{'user'}{'email'}",
                            'msg_id'       => '',
                            'status'       => 'error',
                            'error_type'   => 'internal',
                            'user_email'   => $param->{'user'}{'email'},
                        }
                    );
                    return undef;
                }
            }
        }
    }

    if ($in{'previous_action'}) {
        ## Some actions don't make sense with GET method, redirecting to other
        ## functions
        if ($in{'previous_action'} eq 'arcsearch') {
            $in{'previous_action'} = 'arc';
        }
        $in{'list'} = $in{'previous_list'};
        return $in{'previous_action'};
    }

    return Conf::get_robot_conf($robot, 'default_home');
}
## Function do_attach
sub do_attach {
    wwslog('info', '(%s, %s)', $in{'dir'}, $in{'file'});

    ### Useful variables

    # current list / current shared directory
    my $list_name = $list->{'name'};

    # path of the urlized directory
    my $urlizeddir = $list->{'dir'} . '/urlized';

    # document to read
    my $doc = $urlizeddir . '/' . $in{'dir'} . '/' . $in{'file'};

    ### Document exist ?
    unless (-e "$doc") {
        wwslog('info', 'Unable to read %s: no such file or directory', $doc);
        Sympa::Report::reject_report_web('user', 'no_such_document',
            {'path' => $in{'dir'} . '/' . $in{'file'}},
            $param->{'action'}, $list);
        web_db_log(
            {   'robot'        => $robot,
                'list'         => $list->{'name'},
                'action'       => $param->{'action'},
                'parameters'   => "$in{'dir'},$in{'file'}",
                'target_email' => "",
                'msg_id'       => '',
                'status'       => 'error',
                'error_type'   => 'no_file',
                'user_email'   => $param->{'user'}{'email'},
            }
        );
        return undef;
    }

    ### Document has non-size zero?
    unless (-s "$doc") {
        wwslog('info', 'Unable to read %s: empty document', $doc);
        Sympa::Report::reject_report_web('user', 'empty_document',
            {'path' => $in{'dir'} . '/' . $in{'file'}},
            $param->{'action'}, $list);
        web_db_log(
            {   'robot'        => $robot,
                'list'         => $list->{'name'},
                'action'       => $param->{'action'},
                'parameters'   => "$in{'dir'},$in{'file'}",
                'target_email' => "",
                'msg_id'       => '',
                'status'       => 'error',
                'error_type'   => 'empty_file',
                'user_email'   => $param->{'user'}{'email'},
            }
        );
        return undef;
    }

    ## Access control
    return undef
        unless (defined check_authz('do_attach', 'archive.web_access'));

    # parameters for the template file
    # view a file
    $param->{'file'}   = $doc;
    $param->{'bypass'} = 'asis';

    ## File type
    if ($in{'file'} =~ /\.(\w+)$/) {
        $param->{'file_extension'} = $1;
    }
    web_db_log(
        {   'robot'        => $robot,
            'list'         => $list->{'name'},
            'action'       => $param->{'action'},
            'parameters'   => "$in{'dir'},$in{'file'}",
            'target_email' => "",
            'msg_id'       => '',
            'status'       => 'success',
            'error_type'   => '',
            'user_email'   => $param->{'user'}{'email'},
        }
    );
    return 1;
}

sub do_subindex {
    wwslog('info', '');

    my $spool_req =
        Sympa::Spool::Auth->new(context => $list, action => 'add');
    my @subscriptions;
    while (1) {
        my ($request, $handle) = $spool_req->next(no_lock => 1);
        last unless $handle;
        next unless $request;

        push @subscriptions,
            {
            key   => $request->{keyauth},
            value => {
                custom_attribute => $request->{custom_attribute},
                date             => $language->gettext_strftime(
                    '%d %b %Y', localtime $request->{date}
                ),
                email => $request->{email},
                epoch => $request->{date},
                gecos => $request->{gecos},
            },
            };
    }
    $param->{'subscriptions'} = [@subscriptions];

    web_db_log(
        {   'robot'        => $robot,
            'list'         => $list->{'name'},
            'action'       => $param->{'action'},
            'parameters'   => "",
            'target_email' => "",
            'msg_id'       => '',
            'status'       => 'success',
            'error_type'   => '',
            'user_email'   => $param->{'user'}{'email'},
        }
    );
    return 1;
}

sub do_ignoresub {
    wwslog('info', '');

    foreach my $id (split /\0/, $in{'id'}) {
        next unless $id and $id =~ /\A\w+\z/;

        my $spool_req = Sympa::Spool::Auth->new(
            context => $list,
            keyauth => $id,
            action  => 'add'
        );
        while (1) {
            my ($request, $handle) = $spool_req->next;
            last unless $handle;
            next unless $request;

            $spool_req->remove($handle);
        }
    }

    web_db_log(
        {   'robot'        => $robot,
            'list'         => $list->{'name'},
            'action'       => $param->{'action'},
            'parameters'   => "",
            'target_email' => "",
            'msg_id'       => '',
            'status'       => 'success',
            'error_type'   => '',
            'user_email'   => $param->{'user'}{'email'},
        }
    );
    return 'subindex';
}

sub do_sigindex {
    wwslog('info', '');

    my $spool_req =
        Sympa::Spool::Auth->new(context => $list, action => 'del');
    my @signoffs;
    while (1) {
        my ($request, $handle) = $spool_req->next(no_lock => 1);
        last unless $handle;
        next unless $request;

        push @signoffs,
            {
            key   => $request->{keyauth},
            value => {
                date => $language->gettext_strftime(
                    '%d %b %Y', localtime $request->{date}
                ),
                email => $request->{email},
                epoch => $request->{date},
            },
            };
    }
    $param->{'signoffs'} = [@signoffs];

    web_db_log(
        {   'robot'        => $robot,
            'list'         => $list->{'name'},
            'action'       => $param->{'action'},
            'parameters'   => "",
            'target_email' => "",
            'msg_id'       => '',
            'status'       => 'success',
            'error_type'   => '',
            'user_email'   => $param->{'user'}{'email'},
        }
    );
    return 1;
}

sub do_ignoresig {
    wwslog('info', '');

    foreach my $id (split /\0/, $in{'id'}) {
        next unless $id and $id =~ /\A\w+\z/;

        my $spool_req = Sympa::Spool::Auth->new(
            context => $list,
            keyauth => $id,
            action  => 'del'
        );
        while (1) {
            my ($request, $handle) = $spool_req->next;
            last unless $handle;
            next unless $request;

            $spool_req->remove($handle);
        }
    }

    web_db_log(
        {   'robot'        => $robot,
            'list'         => $list->{'name'},
            'action'       => $param->{'action'},
            'parameters'   => "",
            'target_email' => "",
            'msg_id'       => '',
            'status'       => 'success',
            'error_type'   => '',
            'user_email'   => $param->{'user'}{'email'},
        }
    );
    return 'sigindex';
}

sub do_stats {
    wwslog('info', '');

    $param->{'shared_size'} = int(($list->get_shared_size + 512) / 1024);
    $param->{'arc_size'}    = int(
        (   $list->get_arc_size(Conf::get_robot_conf($robot, 'arc_path')) +
                512
        ) / 1024
    );

    my $stats = {
        send_mail => {title => $language->gettext("Mail sending")},
        add_or_subscribe =>
            {title => $language->gettext("Subscription additions")},
        signoff => {title => $language->gettext("Unsubscription")},
        del     => {title => $language->gettext("Users deleted by admin")},
        auto_del =>
            {title => $language->gettext("Users deleted automatically")},
        d_upload      => {title => $language->gettext("File uploading")},
        d_create_file => {title => $language->gettext("File creation")},
        d_create_dir  => {title => $language->gettext("Directory creation")},
    };

    foreach my $operation (keys %$stats) {
        my $data = $log->aggregate_daily_data($list, $operation);
        if (%{$data || {}}) {
            $stats->{$operation}{'stats_values'} = '[' . join(
                ',',
                map {
                    my $formatted_date =
                        $language->gettext_strftime('%d %b %Y', localtime $_);
                    $formatted_date =~ s/([\\\'])/\\$1/g;
                    sprintf "['%s',%d]", $formatted_date, $data->{$_}
                    } sort keys %$data
            ) . ']';
        }
    }
    $param->{'stats'} = $stats;

    return 1;
}

## setting the topics list for templates
sub export_topics {

    my $robot = shift;
    wwslog('debug2', '(%s)', $robot);
    my %topics = Sympa::Robot::load_topics($robot);

    unless (%topics) {
        wwslog('err', 'No topics defined');
        return undef;
    }

    ## Remove existing topics
    $param->{'topics'} = undef;

    my $total = 0;
    foreach my $t (
        sort { $topics{$a}{'order'} <=> $topics{$b}{'order'} }
        keys %topics
        ) {
        my $result = Sympa::Scenario::request_action(
            $robot,
            'topics_visibility',
            $param->{'auth_method'},
            {   'topicname'   => $t,
                'sender'      => $param->{'user'}{'email'},
                'remote_host' => $param->{'remote_host'},
                'remote_addr' => $param->{'remote_addr'}
            }
        );
        my $action;
        $action = $result->{'action'} if (ref($result) eq 'HASH');
        next unless ($action =~ /do_it/);

        my $current = $topics{$t};
        $current->{'id'} = $t;

        ## For compatibility reasons
        $current->{'mod'}  = $total % 3;
        $current->{'mod2'} = $total % 2;

        push @{$param->{'topics'}}, $current;

        $total++;
    }

    push @{$param->{'topics'}},
        {
        'id'  => 'topicsless',
        'mod' => $total,
        'sub' => {}
        };

    $param->{'topics'}[int($total / 2)]{'next'} = 1;
}

# manage blacklist
sub do_blacklist {
    wwslog('info', '(%s)', $param->{'list'});

    unless ($param->{'list'}) {
        Sympa::Report::reject_report_web('user', 'missing_arg',
            {'argument' => 'list'},
            $param->{'action'});
        wwslog('info', 'No list');
        web_db_log(
            {   'robot'        => $robot,
                'list'         => $list->{'name'},
                'action'       => $param->{'action'},
                'parameters'   => "$param->{'list'}",
                'target_email' => "",
                'msg_id'       => '',
                'status'       => 'error',
                'error_type'   => 'no_list',
                'user_email'   => $param->{'user'}{'email'},
            }
        );
        return undef;
    }
    unless ($param->{'is_owner'}
        || $param->{'is_editor'}
        || $param->{'is_listmaster'}) {
        wwslog('info', 'Not listmaster or list owner or list editor');
        web_db_log(
            {   'robot'        => $robot,
                'list'         => $list->{'name'},
                'action'       => $param->{'action'},
                'parameters'   => "$param->{'list'}",
                'target_email' => "",
                'msg_id'       => '',
                'status'       => 'error',
                'error_type'   => 'authorization',
                'user_email'   => $param->{'user'}{'email'},
            }
        );
        return undef;
    }
    my $file = $list->{'dir'} . '/search_filters/blacklist.txt';
    $param->{'rows'} = 0;

    if (defined $in{'blacklist'}) {
        wwslog('info', 'Submit blacklist update');
        my $dir = $list->{'dir'} . '/search_filters';
        unless ((-d $dir) || mkdir($dir, 0755)) {
            Sympa::Report::reject_report_web('intern',
                'unable to create dir');
            wwslog('info', 'Unable to create dir %s', $dir);
            web_db_log(
                {   'robot'        => $robot,
                    'list'         => $list->{'name'},
                    'action'       => $param->{'action'},
                    'parameters'   => "$param->{'list'}",
                    'target_email' => "",
                    'msg_id'       => '',
                    'status'       => 'error',
                    'error_type'   => 'internal',
                    'user_email'   => $param->{'user'}{'email'},
                }
            );
        }
        my $file = $dir . '/blacklist.txt';
        unless (open BLACKLIST, "> $file") {
            Sympa::Report::reject_report_web('intern',
                'unable to create file');
            wwslog('info', 'Unable to create file %s', $file);
            web_db_log(
                {   'robot'        => $robot,
                    'list'         => $list->{'name'},
                    'action'       => $param->{'action'},
                    'parameters'   => "$param->{'list'}",
                    'target_email' => "",
                    'msg_id'       => '',
                    'status'       => 'error',
                    'error_type'   => 'internal',
                    'user_email'   => $param->{'user'}{'email'},
                }
            );
        }
        my @lines = split(/\r\n|\r|\n/, $in{'blacklist'});
        $param->{'ignored'} = 0;
        my $count =
            0;    # count utils lines in order to remove empty blacklist file
        foreach my $line (@lines) {
            if ($line =~ /\*.*\*/) {
                $param->{'ignored_linest'} .= $line . "\n";
                $param->{'ignored'} += 1;
            } else {
                print BLACKLIST "$line\n";
                $param->{'blacklist'} .= $line . "\n";
                $param->{'rows'} += 1;
                $count += 1 unless ($line =~ /^\s*$/o || /^[\#\;]/o);
            }
        }
        close BLACKLIST;
        if ($count == 0) {
            unless (unlink $file) {
                Sympa::Report::reject_report_web('intern',
                    'unable to remove empty blacklist file');
                wwslog('info', 'Unable to remove empty blacklist file %s',
                    $file);
                web_db_log(
                    {   'robot'        => $robot,
                        'list'         => $list->{'name'},
                        'action'       => $param->{'action'},
                        'parameters'   => "$param->{'list'}",
                        'target_email' => "",
                        'msg_id'       => '',
                        'status'       => 'error',
                        'error_type'   => 'internal',
                        'user_email'   => $param->{'user'}{'email'},
                    }
                );
            }
            wwslog('info', 'Removed empty blacklist file %s', $file);
        }
    } else {
        if (-f $file) {
            unless (open BLACKLIST, $file) {
                Sympa::Report::reject_report_web(
                    'intern',
                    'unable to open file',
                    {'file' => $file},
                    $robot, $param->{'action'}, '', $param->{'user'}{'email'}
                );
                wwslog('err', 'Unable to read %s', $file);
                web_db_log(
                    {   'robot'        => $robot,
                        'list'         => $list->{'name'},
                        'action'       => $param->{'action'},
                        'parameters'   => "$param->{'list'}",
                        'target_email' => "",
                        'msg_id'       => '',
                        'status'       => 'error',
                        'error_type'   => 'internal',
                        'user_email'   => $param->{'user'}{'email'},
                    }
                );
            }
            while (<BLACKLIST>) {
                $param->{'blacklist'} .= $_;
                $param->{'rows'} += 1;
            }
            close BLACKLIST;
        }
    }

    web_db_log(
        {   'robot'        => $robot,
            'list'         => $list->{'name'},
            'action'       => $param->{'action'},
            'parameters'   => "$param->{'list'}",
            'target_email' => "",
            'msg_id'       => '',
            'status'       => 'success',
            'error_type'   => '',
            'user_email'   => $param->{'user'}{'email'},
        }
    );
    return 1;
}

# output in text/plain format a scenario
sub do_dump_scenario {
    wwslog('info', '(%s, %s)', $param->{'list'}, $in{'pname'});

    my $scenario = Sympa::Scenario->new(
        'function'  => $in{'pname'},
        'robot'     => $robot,
        'name'      => $list->{'admin'}{$in{'pname'}}{'name'},
        'directory' => $list->{'dir'}
    );
    unless (defined $scenario) {
        Sympa::Report::reject_report_web('intern', 'cannot_open_file', {},
            $param->{'action'}, $list);
        wwslog('info', 'Failed to load scenario');
        return undef;
    }
    ($param->{'dumped_scenario'}, $param->{'scenario_path'}) =
        ($scenario->{'data'}, $scenario->{'file_path'});
    $param->{'pname'}         = $in{'pname'};
    $param->{'scenario_name'} = $list->{'admin'}{$in{'pname'}}{'name'};

    if ($in{'new_scenario_name'}) {
        # in this case it's a submit.
        my $scenario_dir = $list->{'dir'} . '/scenari/';
        my $scenario_file =
            $scenario_dir . $in{'pname'} . '.' . $in{'new_scenario_name'};
        if ($param->{'dumped_scenario'} eq $in{'new_scenario_content'}) {
            wwslog('info', 'Scenario unchanged');
            $param->{'result'} = 'unchanged';
            return 1;
        }
        unless (-d $scenario_dir) {
            unless (mkdir($scenario_dir, 0777)) {
                wwslog('err', '%s: %s', $scenario_dir, $ERRNO);
                Sympa::Report::reject_report_web(
                    'intern',
                    'cannot_create_dir',
                    {   'file' => $scenario_dir,
                        $param->{'action'}, '', $param->{'user'}{'email'}
                    },
                    $robot
                );
                return undef;
            }
        }
        unless (open SCENARIO, ">$scenario_file") {
            wwslog('info', '%s', $scenario_file);
            Sympa::Report::reject_report_web(
                'intern',
                'cannot_open_file',
                {   'file' => $scenario_file,
                    $param->{'action'}, '', $param->{'user'}{'email'}
                },
                $robot
            );
            return undef;
        }
        print SCENARIO $in{'new_scenario_content'};
        close SCENARIO;
        # load the new scenario in the list config.
        if ($in{'new_scenario_name'} eq $in{'scenario_name'}) {
            $param->{'result'} = 'success';
        } else {
            $param->{'result'} = 'success_new_name';
        }
    }
    return 1;
}

## Subscribers' list
sub do_dump {
    wwslog('info', '(%s)', $param->{'list'});

    ## Whatever the action return, it must never send a complex html page
    $param->{'bypass'}       = 1;
    $param->{'content_type'} = "text/plain";
    $param->{'file'}         = undef;

    ## Access control
    unless (defined check_authz('do_dump', 'review')) {
        undef $param->{'bypass'};
        return undef;
    }

    $list->dump();
    $param->{'file'} = $list->{'dir'} . '/subscribers.db.dump';

    if ($in{'format'} eq 'light') {
        unless (open(DUMP, $param->{'file'})) {
            Sympa::Report::reject_report_web('intern', 'cannot_open_file',
                {'file' => $param->{'file'}},
                $param->{'action'}, '', $param->{'user'}{'email'}, $robot);
            wwslog('info', 'Unable to open file %s', $param->{'file'});
            return undef;
        }
        unless (open(LIGHTDUMP, ">$param->{'file'}.light")) {
            Sympa::Report::reject_report_web(
                'intern',
                'cannot_open_file',
                {'file' => "$param->{'file'}.light"},
                $param->{'action'},
                '',
                $param->{'user'}{'email'},
                $robot
            );
            wwslog('err', 'Unable to create file %s.light', $param->{'file'});
            return undef;
        }
        while (<DUMP>) {
            next unless ($_ =~ /^email\s(.*)/);
            print LIGHTDUMP "$1\n";
        }
        close LIGHTDUMP;
        close DUMP;
        $param->{'file'} = "$list->{'dir'}/subscribers.db.dump.light";

    } else {
        $param->{'file'} = "$list->{'dir'}/select.dump";
        wwslog('info', 'Opening %s', $param->{'file'});

        unless (open(DUMP, ">$param->{'file'}")) {
            Sympa::Report::reject_report_web('intern', 'file_update_failed',
                {}, $param->{'action'}, '', $param->{'user'}{'email'},
                $robot);
            wwslog('err', 'Unable to create file %s', $param->{'file'});
            return undef;
        }

        if ($in{'format'} eq 'bounce') {
            $in{'size'} = 'all';
            do_reviewbouncing();
            print DUMP "# Exported bouncing subscribers\n";
            print DUMP
                "# Email\t\tName\tBounce score\tBounce count\tFirst bounce\tLast bounce\n";
            foreach my $user (@{$param->{'members'}}) {
                print DUMP
                    "$user->{'email'}\t$user->{'gecos'}\t$user->{'bounce_score'}\t$user->{'bounce_count'}\t$user->{'first_bounce'}\t$user->{'last_bounce'}\n";
            }
        } else {
            $in{'filter'} = $in{'format'};
            do_search();
            print DUMP
                "# Exported subscribers with search filter \"$in{'format'}\"\n";
            foreach my $user (@{$param->{'members'}}) {
                print DUMP "$user->{'email'}\t$user->{'gecos'}\n";
            }
        }
        close DUMP;
    }
    return 1;
}

## returns a mailto according to list spam protection parameter
# No longer used.
#sub mailto;

## Returns a spam-protected form of email address
# DEPRECATED.  Use [%|obfuscate()%] in template.
#sub get_protected_email_address;

## view logs stored in RDBMS
## this function as been writen in order to allow list owner and listmater to
## views logs
## of there robot or there is real problems with privacy policy and law in
## such services.
##
sub do_viewlogs {
    wwslog('info', '(%s)', $in{'page'});

    $param->{'page'} = int($in{'page'}) || 1;
    $param->{'size'} = int($in{'size'}) || $Conf::Conf{'viewlogs_page_size'};

    # Unknown sort key as 'date'.
    my $sortby = $in{'sortby'};
    unless (
        $sortby
        and grep { $sortby eq $_ }
        qw(date robot list action parameters target_email msg_id
        status error_type user_email client daemon)
        ) {
        $sortby = 'date';
    }
    $param->{'sortby'} = $sortby;

    $param->{'total_results'} = 0;

    my @date = $log->get_log_date();
    $param->{'date_from_formated'} =
        $language->gettext_strftime("%Y-%m-%d-%H-%M-%S", localtime($date[0]));
    $param->{'date_to_formated'} =
        $language->gettext_strftime("%Y-%m-%d-%H-%M-%S", localtime($date[1]));

    #display and search parameters preparation
    my $select = {};

    $select->{'robot'} = $robot;
    $select->{'list'}  = $param->{'list'};

    foreach
        my $p ('target_type', 'target', 'date_from', 'date_to', 'type', 'ip')
    {
        $param->{$p}  = $in{$p};
        $select->{$p} = $in{$p};
    }

    if ($in{'target_type'} or $in{'page'} or $in{'size'}) {
        #sending of search parameters for the query
        my $line = $log->get_first_db_log($select);
        while (defined $line->{'date'}) {
            $line->{'date'} = $language->gettext_strftime("%d %b %Y %H:%M:%S",
                localtime($line->{'date'}));
            # can be wrapped
            $line->{'parameters'} =~ s/,(?!\s)/, /g
                if $line->{'parameters'};
            push @{$param->{'log_entries'}}, $line;
            $line = $log->get_next_db_log();
        }

        #display the number of rows of the query.
        $param->{'total_results'} = scalar @{$param->{'log_entries'} || []};

        unless ($param->{'total_results'}) {
            #Sympa::Report::reject_report_web('user', 'no_logs', {},
            #    $param->{'action'});
            wwslog('info', 'No results');
            return 1;
        }

        $param->{'total_page'} =
            int($param->{'total_results'} / $param->{'size'});
        $param->{'total_page'}++
            if ($param->{'total_results'} % $param->{'size'});

        if ($param->{'page'} > $param->{'total_page'}) {
            Sympa::Report::reject_report_web('user', 'no_page',
                {'page' => $param->{'page'}},
                $param->{'action'});
            # $log->db_log('wwsympa', $param->{'user'}{'email'},
            #     $param->{'auth_method'}, $ip, 'review', $param->{'list'},
            #     $robot,'','out of pages');
            wwslog('info', 'No page %d', $param->{'page'});
            return undef;
        }

        my $offset = 0;
        if ($param->{'page'} > 1) {
            $offset = (($param->{'page'} - 1) * $param->{'size'});
            $param->{'prev_page'} = $param->{'page'} - 1;
        }

        unless (($offset + $param->{'size'}) >= $param->{'total_results'}) {
            $param->{'next_page'} = $param->{'page'} + 1;
        }

        @{$param->{'log_entries'}} = sort {
            lc $a->{$param->{'sortby'}} cmp lc $b->{$param->{'sortby'}}
        } @{$param->{'log_entries'}};

        my $last = $offset + $param->{'size'};
        $last = $param->{'total_results'} - 1
            if ($last >= $param->{'total_results'});
        @{$param->{'log_entries'}} =
            @{$param->{'log_entries'}}[$offset .. $last];
    }

    return 1;
}

sub do_arc_manage {
    wwslog('info', '(%s)', $in{'list'});

    # Access control
    return undef
        unless defined check_authz('do_arc', 'archive.web_access');

    my $archive = Sympa::Archive->new(context => $list);
    $param->{'yyyymm'} = [reverse $archive->get_archives];

    return 1;
}

## create a zip file with archives from (list,month)
sub do_arc_download {

    wwslog('info', '(%s)', $in{'list'});

    ## Access control
    unless (defined check_authz('do_arc', 'archive.web_access')) {
        return undef;
    }

    ##zip file name:listname_archives.zip
    my $zip_file_name = $in{'list'} . '_archives.zip';
    my $zip_abs_file  = $Conf::Conf{'tmpdir'} . '/' . $zip_file_name;
    my $zip           = Archive::Zip->new();

    #Search for months to put in zip
    unless (defined($in{'directories'})) {
        Sympa::Report::reject_report_web('user', 'select_month', {},
            $param->{'action'});
        wwslog('info', 'No archives specified');
        web_db_log(
            {   'robot'        => $robot,
                'list'         => $list->{'name'},
                'action'       => $param->{'action'},
                'parameters'   => "$in{'list'}",
                'target_email' => "",
                'msg_id'       => '',
                'status'       => 'error',
                'error_type'   => 'select_month',
                'user_email'   => $param->{'user'}{'email'},
            }
        );
        return 'arc_manage';
    }

    my $archive = Sympa::Archive->new(context => $list);

    # For each selected month
    foreach my $arc (split /\0/, $in{'directories'}) {
        # Check arc directory
        unless ($archive->select_archive($arc)) {
            Sympa::Report::reject_report_web(
                'intern',
                'arc_not_found',    #FIXME: Not implemented.
                {   'month'    => $arc,
                    'listname' => $in{'list'},
                },
                $param->{'action'},
                '',
                $param->{'user'}{'email'},
                $robot
            );
            wwslog('info', 'Archive %s not found', $arc);
            web_db_log(
                {   'robot'        => $robot,
                    'list'         => $list->{'name'},
                    'action'       => $param->{'action'},
                    'parameters'   => "$in{'list'}",
                    'target_email' => "",
                    'msg_id'       => '',
                    'status'       => 'error',
                    'error_type'   => 'internal',
                    'user_email'   => $param->{'user'}{'email'},
                }
            );
            next;
        }

        $zip->addDirectory($archive->{directory}, $in{'list'} . '_' . $arc);

        while (1) {
            my ($message, $handle) = $archive->next;
            last unless $handle;
            next unless $message;

            unless (
                $zip->addString(
                    $message->as_string,
                    $in{'list'} . '_' . $arc . '/' . $handle->basename
                )
                ) {
                Sympa::Report::reject_report_web(
                    'intern',
                    'add_file_zip',
                    {'file' => $arc . '/' . $handle->basename},
                    $param->{'action'},
                    '',
                    $param->{'user'}{'email'},
                    $robot
                );
                wwslog('info', 'Failed to add %s file in %s to archive',
                    $handle->basename, $archive);
                web_db_log(
                    {   'robot'        => $robot,
                        'list'         => $list->{'name'},
                        'action'       => $param->{'action'},
                        'parameters'   => "$in{'list'}",
                        'target_email' => "",
                        'msg_id'       => '',
                        'status'       => 'error',
                        'error_type'   => 'internal',
                        'user_email'   => $param->{'user'}{'email'},
                    }
                );
                return undef;
            }
        }

        ## create and fill a new folder in zip
        #$zip->addTree ($abs_dir, $in{'list'}.'_'.$dir);
    }

    ## check if zip isn't empty
    if ($zip->numberOfMembers() == 0) {
        Sympa::Report::reject_report_web('intern', 'inaccessible_archive',
            {'listname' => $in{'list'}},
            $param->{'action'}, '', $param->{'user'}{'email'}, $robot);
        wwslog('info', 'Empty directories');
        web_db_log(
            {   'robot'        => $robot,
                'list'         => $list->{'name'},
                'action'       => $param->{'action'},
                'parameters'   => "$in{'list'}",
                'target_email' => "",
                'msg_id'       => '',
                'status'       => 'error',
                'error_type'   => 'internal',
                'user_email'   => $param->{'user'}{'email'},
            }
        );
        return undef;
    }
    ##writing zip file
    unless ($zip->writeToFileNamed($zip_abs_file) == Archive::Zip::AZ_OK()) {
        Sympa::Report::reject_report_web('intern', 'write_file_zip',
            {'zipfile' => $zip_abs_file},
            $param->{'action'}, '', $param->{'user'}{'email'}, $robot);
        wwslog('info', 'Error while writing ZIP File %s', $zip_file_name);
        web_db_log(
            {   'robot'        => $robot,
                'list'         => $list->{'name'},
                'action'       => $param->{'action'},
                'parameters'   => "$in{'list'}",
                'target_email' => "",
                'msg_id'       => '',
                'status'       => 'error',
                'error_type'   => 'internal',
                'user_email'   => $param->{'user'}{'email'},
            }
        );
        return undef;
    }

    ##Sending Zip to browser
    $param->{'bypass'} = 'extreme';
    printf(
        "Content-Type: application/zip;\nContent-disposition: attachment; filename=\"%s\";\n\n",
        $zip_file_name);
    ##MIME Header
    unless (open(ZIP, $zip_abs_file)) {
        Sympa::Report::reject_report_web('intern', 'cannot_open_file',
            {'file' => $zip_abs_file},
            $param->{'action'}, $list, $param->{'user'}{'email'}, $robot);
        wwslog('info', 'Error while reading ZIP File %s', $zip_abs_file);
        web_db_log(
            {   'robot'        => $robot,
                'list'         => $list->{'name'},
                'action'       => $param->{'action'},
                'parameters'   => "$in{'list'}",
                'target_email' => "",
                'msg_id'       => '',
                'status'       => 'error',
                'error_type'   => 'internal',
                'user_email'   => $param->{'user'}{'email'},
            }
        );
        return undef;
    }
    print <ZIP>;
    close ZIP;

    ## remove zip file from server disk
    unless (unlink($zip_abs_file)) {
        Sympa::Report::reject_report_web('intern', 'erase_file',
            {'file' => $zip_abs_file},
            $param->{'action'}, $list, $param->{'user'}{'email'}, $robot);
        wwslog('info', 'Error while unlinking File %s', $zip_abs_file);
        web_db_log(
            {   'robot'        => $robot,
                'list'         => $list->{'name'},
                'action'       => $param->{'action'},
                'parameters'   => "$in{'list'}",
                'target_email' => "",
                'msg_id'       => '',
                'status'       => 'error',
                'error_type'   => 'internal',
                'user_email'   => $param->{'user'}{'email'},
            }
        );
    }
    web_db_log(
        {   'robot'        => $robot,
            'list'         => $list->{'name'},
            'action'       => $param->{'action'},
            'parameters'   => "$in{'list'}",
            'target_email' => "",
            'msg_id'       => '',
            'status'       => 'success',
            'error_type'   => '',
            'user_email'   => $param->{'user'}{'email'},
        }
    );
    return 1;
}

sub do_arc_delete {
    wwslog('info', '(%s)', $in{'list'});

    # Access control
    return undef
        unless defined check_authz('do_arc', 'archive.web_access');

    unless (defined $in{'directories'}) {
        Sympa::Report::reject_report_web('user', 'select_month', {},
            $param->{'action'});
        wwslog('info', 'No Archives months selected');
        web_db_log(
            {   'robot'        => $robot,
                'list'         => $list->{'name'},
                'action'       => $param->{'action'},
                'parameters'   => "$in{'list'}",
                'target_email' => "",
                'msg_id'       => '',
                'status'       => 'error',
                'error_type'   => 'select_month',
                'user_email'   => $param->{'user'}{'email'},
            }
        );
        return 'arc_manage';
    }

    ## if user want to download archives before delete
    wwslog('notice', 'ZIP: %s', $in{'zip'});
    if ($in{'zip'} == 1) {
        do_arc_download();
    }

    my $archive = Sympa::Archive->new(context => $list);
    foreach my $arc (split /\0/, $in{'directories'}) {
        unless ($archive->purge_archive($arc)) {
            wwslog('info', 'Error while purging archive %s in %s',
                $arc, $archive);
        }
    }

    Sympa::Report::notice_report_web('performed', {}, $param->{'action'});
    web_db_log(
        {   'robot'        => $robot,
            'list'         => $list->{'name'},
            'action'       => $param->{'action'},
            'parameters'   => "$in{'list'}",
            'target_email' => "",
            'msg_id'       => '',
            'status'       => 'success',
            'error_type'   => '',
            'user_email'   => $param->{'user'}{'email'},
        }
    );
    return 'arc_manage';
}

sub do_css {
    wwslog('debug', '(%s)', $in{'file'});
    $param->{'bypass'} = 'extreme';
    print "Content-type: text/css; charset=utf-8\n\n";
    $param->{'css'} = $in{'file'};

    # Do not include locale subdirectories (lang parameter).
    # The css.tt2 by each locale will override styles in main CSS.
    my $css_template = Sympa::Template->new($robot, subdir => 'web_tt2');
    unless ($css_template->parse($param, 'css.tt2', \*STDOUT)) {
        my $error = $css_template->{last_error};
        $error = $error->as_string if ref $error;
        $param->{'tt2_error'} = $error;

        Sympa::send_notify_to_listmaster($robot, 'web_tt2_error', [$error]);
        wwslog('info', '/%s: error: %s', $in{'file'}, $error);
        my $error_escaped = $error;
        $error_escaped =~ s/&/&amp;/g;
        $error_escaped =~ s/\//&#47;/g;
        printf STDOUT "\n/* %s */\n", $error_escaped;
    }

    return;
}

sub do_rss_request {
    wwslog('info', '');

    if (ref $list eq 'Sympa::List') {
        my $result = Sympa::Scenario::request_action(
            $list,
            'visibility',
            $param->{'auth_method'},
            {   'sender'      => $param->{'user'}{'email'},
                'remote_host' => $param->{'remote_host'},
                'remote_addr' => $param->{'remote_addr'}
            }
        );
        my $sub_is;
        my $reason;
        if (ref $result eq 'HASH') {
            $sub_is = $result->{'action'};
            $reason = $result->{'reason'};
        }
        if ($sub_is =~ /\Areject\b/i) {
            wwslog(
                'info',
                'RSS not accessible because list %s is not visible to user %s',
                $list->get_id,
                $param->{'user'}{'email'}
            );
            web_db_log(
                {   'parameters' => $param->{'user'}{'email'},
                    'status'     => 'error',
                    'error_type' => 'authorization'
                }
            );
            return undef;
        }
    }

    my %args;
    $in{'count'} ||= 20;
    $in{'for'}   ||= 10;

    $args{count} = $in{'count'} if $in{'count'};
    $args{for}   = $in{'for'}   if $in{'for'};

    if (ref $list eq 'Sympa::List') {
        $param->{'latest_arc_url'} =
            Sympa::get_url($list, 'rss/latest_arc', query => {%args});
        $param->{'latest_d_read_url'} =
            Sympa::get_url($list, 'rss/latest_d_read', query => {%args});
    }
    $param->{'active_lists_url'} =
        Sympa::get_url($robot, 'rss/active_lists', query => {%args});
    $param->{'latest_lists_url'} =
        Sympa::get_url($robot, 'rss/latest_lists', query => {%args});

    $param->{'output'} = 1;
    return 1;
}

sub do_wsdl {
    wwslog('info', '');

    my $sympawsdl;
    unless ($sympawsdl = Sympa::search_fullpath($robot, 'sympa.wsdl')
        and -r $sympawsdl) {
        Sympa::Report::reject_report_web('intern', 'err_404', {},
            $param->{'action'});
        wwslog('err', 'Could not find sympa.wsdl');
        return undef;
    }

    my $soap_url = Conf::get_robot_conf($robot, 'soap_url');
    unless (defined $soap_url) {
        Sympa::Report::reject_report_web('user', 'no_soap_service', {},
            $param->{'action'});
        wwslog('err',
            'No SOAP service was defined in sympa.conf (soap_url parameter)');
        return undef;
    }

    $param->{'bypass'} = 'extreme';
    print "Content-type: text/xml\n\n";

    $param->{'conf'}{'soap_url'} = $soap_url;

    my $template = Sympa::Template->new($robot);
    $template->parse($param, 'sympa.wsdl', \*STDOUT);

    return 1;
}

## Synchronize list members with data sources
sub do_sync_include {
    wwslog('info', '(%s)', $in{'list'});

    unless ($list->sync_include()) {
        Sympa::Report::reject_report_web('intern', 'sync_include_failed', {},
            $param->{'action'}, $list, $param->{'user'}{'email'}, $robot);
        return undef;
    }
    Sympa::Report::notice_report_web('subscribers_updated', {},
        $param->{'action'});
    return 'review';
}

## Review lists from a family
sub do_review_family {
    wwslog('info', '');

    my $family = Sympa::Family->new($in{'family_name'}, $robot);
    unless (defined $family) {
        Sympa::Report::reject_report_web('user', 'unknown_family',
            {'family' => $in{'family_name'}},
            $param->{'action'}, '', $param->{'user'}{'email'}, $robot);
        wwslog('err', 'Incorrect family %s', $in{'family_name'});
        return undef;
    }

    my $all_lists = Sympa::List::get_lists($family);
    foreach my $flist (@{$all_lists || []}) {
        unless ($flist) {
            wwslog('err', 'Incorrect list');
            next;
        }

        push @{$param->{'family_lists'}},
            {
            'name'               => $flist->{'name'},
            'status'             => $flist->{'admin'}{'status'},
            'instantiation_date' => $language->gettext_strftime(
                "%d %b %Y at %H:%M:%S",
                localtime $flist->{'admin'}{'latest_instantiation'}
                    {'date_epoch'}
            ),
            'subject' => $flist->{'admin'}{'subject'},
            };
    }

    return 1;
}

# Get custom action file
sub _ca_get_file {
    my $custom_action = shift;
    my $robot         = shift;
    my $list          = shift;

    my $file = Sympa::search_fullpath($list || $robot,
        $custom_action, subdir => 'custom_actions');
    return undef unless ($file);

    _ca_add_file_path_to_tt2_include_path($file);

    return $file;
}

# Adds custom action path to tt2 path
sub _ca_add_file_path_to_tt2_include_path {
    my $file = shift;
    $file =~ s/\/[^\/]+$//;
    push @other_include_path, $file;
}

# Process custom action
sub _ca_process {
    my $custom_action = shift;
    my $cap           = shift;
    my $robot         = shift;
    my $list          = shift;

    my $file = _ca_get_file($custom_action . '.pm', $robot, $list);
    return undef unless ($file);

    eval { require "$file"; };
    if ($EVAL_ERROR) {
        $log->syslog('err', 'Error requiring %s: %s (%s)',
            $custom_action, "$EVAL_ERROR", ref($EVAL_ERROR));
        return undef;
    }

    unshift @{$cap}, $list if ($list);
    my $res;
    eval "\$res = " . $custom_action . "_plugin::process(\@{\$cap});";
    if ($EVAL_ERROR) {
        $log->syslog('err', 'Error evaluating %s: %s (%s)',
            $custom_action, "$EVAL_ERROR", ref($EVAL_ERROR));
        return undef;
    }

    return $res;
}

################################################################
## do_ca : executes a custom action
##
## IN:
##    - 'custom_action': ther name of the custom action (and subsequent tt2
##    file to use, see below)
##    - '@cap': an array of parameters.
##
## Custom actions are used to run specific code and/or display user defined
## templates.
## You can create a <your_action>.pm module under etc/custom_actions or etc/
## <robot>/custom_actions (<your_action>_plugin package) with a "process" sub
## to add custom processing.
## You can also create a <your_action>.tt2 file at the same place to display
## your template. You don't need the <head/> section or the <body/> tag.
## The HTML code in '<your_action>.tt2' can make use of the parameters this
## way: [% cap.1 %] for param1, [% cap.2 %] for param, and so on.
## If the module is not defined the template is displayed.
##
## You can even have a robot-common <your_action>.pm module with a specific
## <your_action>.tt2 for each robot as the file (.pm or .tt2) is conducted in
## this order :
##   - expl/<robot>/<list>/custom_actions (if list context and robot support)
##   - expl/<list>/custom_actions (if list context and no robot support)
##   - etc/<robot>/custom_actions (if robot support)
##   - etc/custom_actions
##
## Your custom action is reachable using URL:
## http://your-sympa-server-root-url/ca/your_action/param2/param2/param3/...
##
## The module process sub receive @cap entries as arguments
##
## The module process sub return value can be either :
## 	1 to parse and display the custom action related tt2
## 	<a global action name> to display its template
## 	ca:<other_custom_action> to parse and display another custom action
## 	related tt2
## 	a hash which entries will override $param ones, in case
## 	"custom_action" or "next_action" are present they act as described above.
##
###############################################################
sub do_ca {
    wwslog('info',
        'Custom action: %s (robot %s) with params: (%s, %s, %s, %s, %s)',
        $in{'custom_action'}, $robot, $in{'cap'});

    my $custom_action = $in{'custom_action'};
    my $cap = [split '/', $in{'cap'}];
    $param->{'custom_action'} = $custom_action;
    $param->{'cap'}           = $cap;

    my $res = _ca_process($custom_action, $cap, $robot);

    if ($res) {
        my $next_action = 1;
        if (ref $res eq 'HASH') {
            for my $k (keys %$res) {
                $param->{$k} = $res->{$k};
            }
            $next_action = $res->{'custom_action'}
                if ($res->{'custom_action'});
            $next_action = $res->{'next_action'} if ($res->{'next_action'});
        } else {
            $next_action = scalar($res);
        }

        return 1 if ($next_action =~ /^1$/);    # self tt2

        if ($next_action =~ /^l?ca:(.+)$/) {    # other custom action tt2
            $param->{'custom_action'} = $1;
            _ca_get_file($1 . '.tt2', $robot);
            return 1;
        }

        return $next_action;                    # global action
    }

    my $file = _ca_get_file($custom_action . '.tt2', $robot);
    return 1 if ($file);

    $log->syslog('err', 'Plugin not found: %s', $custom_action);
    return undef;
}

################################################################
## do_ca : executes a custom action in list context
##
## IN:
##    - 'custom_action': ther name of the custom action (and subsequent tt2
##    file to use, see below)
##    - 'list': the nalme of the list (without the '@robot' part) in the
##    context of which the action is executed.
##    - '@lcap': an array of parameters.
##
## Custom actions are used to run specific code and/or display user defined
## templates.
## You can create a <your_action>.pm module under etc/custom_actions or etc/
## <robot>/custom_actions or expl(/<robot>)?/<list>/custom_actions
## (<your_action>_plugin package) with a "process" sub to add custom
## processing.
## You can also create a <your_action>.tt2 file at the same place to display
## your template. You don't need the <head/> section or the <body/> tag.
## The HTML code in '<your_action>.tt2' can make use of the parameters this
## way: [% cap.1 %] for param1, [% cap.2 %] for param, and so on.
## If the module is not defined the template is displayed.
##
## You can even have a robot-common <your_action>.pm module with a specific
## <your_action>.tt2 for each robot as the file (.pm or .tt2) is conducted in
## this order :
##   - expl/<robot>/<list>/custom_actions (if list context and robot support)
##   - expl/<list>/custom_actions (if list context and no robot support)
##   - etc/<robot>/custom_actions (if robot support)
##   - etc/custom_actions
##
## Your custom action is reachable using URL:
## http://your-sympa-server-root-url/lca/your_action/listname/param2/param2/param3/...
##
## The module process sub receive the List object and @cap entries as
## arguments
##
## The module process sub return value can be either :
## 	1 to parse and display the custom action related tt2
## 	<a global action name> to display its template
## 	ca:<other_custom_action> to parse and display another custom action
## 	related tt2
## 	a hash which entries will override $param ones, in case
## 	"custom_action" or "next_action" are present they act as described above.
##
###############################################################
sub do_lca {
    wwslog(
        'info',
        'List custom action: %s for list %s (robot %s) with params: (%s, %s, %s, %s, %s)',
        $in{'custom_action'},
        $in{'list'},
        $robot,
        $in{'lcap'}
    );

    my $custom_action = $in{'custom_action'};
    my $cap = [split '/', $in{'cap'}];
    $param->{'custom_action'} = $custom_action;
    $param->{'cap'}           = $cap;

    my $res = _ca_process($custom_action, $cap, $robot, $list);

    if ($res) {
        my $next_action = 1;
        if (ref $res eq 'HASH') {
            for my $k (keys %$res) {
                $param->{$k} = $res->{$k};
            }
            $next_action = $res->{'custom_action'}
                if ($res->{'custom_action'});
            $next_action = $res->{'next_action'} if ($res->{'next_action'});
        } else {
            $next_action = scalar($res);
        }

        return 1 if ($next_action =~ /^1$/);    # self tt2

        if ($next_action =~ /^l?ca:(.+)$/) {    # other custom action tt2
            $param->{'custom_action'} = $1;
            _ca_get_file($1 . '.tt2', $robot, $list);
            return 1;
        }

        return $next_action;                    # global action
    }

    my $file = _ca_get_file($custom_action . '.tt2', $robot, $list);
    return 1 if ($file);

    $log->syslog('err', 'Plugin not found: %s', $custom_action);
    return undef;
}

## Prepare subscriber data to be prompted on the web interface
## Used by review, search,...
sub _prepare_subscriber {
    my $user              = shift;
    my $additional_fields = shift;
    my $sources           = shift;

    ## Add user
    $user->{'date'} =
        $language->gettext_strftime("%d %b %Y", localtime($user->{'date'}));
    $user->{'update_date'} =
        $language->gettext_strftime("%d %b %Y",
        localtime($user->{'update_date'}));

    ## Reception mode and topics
    $user->{'reception'} ||= 'mail';
    if (($user->{'reception'} eq 'mail') && $user->{'topics'}) {
        $user->{'reception'} =
            $language->gettext_sprintf("topic (%s)", $user->{'topics'});
    }

    $user->{'email'} =~ /\@(.+)$/;
    $user->{'domain'}       = $1;
    $user->{'pictures_url'} = $list->find_picture_url($user->{'email'});

    ## Escape some weird chars
    $user->{'escaped_email'} =
        Sympa::Tools::Text::escape_chars($user->{'email'});

    ## Check data sources
    $user->{'sources'} = $list->get_datasource_name($user->{'id'})
        if ($user->{'id'});

    if (@{$additional_fields}) {
        my @fields;
        foreach my $f (@{$additional_fields}) {
            push @fields, $user->{$f};
        }
        $user->{'additional'} = join ',', @fields;
    }

    return 1;
}

## New d_read function using SharedDocument module
## The following features should be tested :
##      * inheritance on privileges
##      X moderation
##      * escaping special chars
sub new_d_read {
    wwslog('info', '(%s)', $in{'path'});

    ### action relative to a list ?
    unless ($param->{'list'}) {
        Sympa::Report::reject_report_web('user', 'missing_arg',
            {'argument' => 'list'},
            $param->{'action'});
        wwslog('err', 'No list');
        return undef;
    }

    # current list / current shared directory
    my $list_name = $list->{'name'};

    my $document = Sympa::SharedDocument->new($list, $in{'path'}, $param);

    unless (defined $document) {
        Sympa::Report::reject_report_web('intern', 'new_document_failed',
            {'path' => $in{'path'}},
            $param->{'action'}, $list, $param->{'user'}{'email'}, $robot);
        wwslog(
            'err',
            'Cannot open %s: %s',
            $document->{'absolute_path'}, $ERRNO
        );
        web_db_log(
            {   'robot'        => $robot,
                'list'         => $list->{'name'},
                'action'       => $param->{'action'},
                'parameters'   => "$in{'path'}",
                'target_email' => "",
                'msg_id'       => '',
                'status'       => 'error',
                'error_type'   => 'internal',
                'user_email'   => $param->{'user'}{'email'},
            }
        );
        return undef;
    }

    my $path         = $document->{'path'};
    my $visible_path = $document->{'visible_path'};
    my $shareddir    = $document->{'shared_dir'};
    my $doc          = $document->{'absolute_path'};
    my $ref_access   = $document->{'access'};
    my %access       = %{$ref_access};
    $param->{'doc_owner'} = $document->{'owner'};
    $param->{'doc_title'} = $document->{'title'};
    $param->{'doc_date'} =
        $language->gettext_strftime("%d %b %Y",
        localtime $document->{'date_epoch'});

    ### Access control
    unless ($access{'may'}{'read'}) {
        Sympa::Report::reject_report_web('auth', $access{'reason'}{'read'},
            {}, $param->{'action'}, $list);
        wwslog('err', 'Access denied for %s', $param->{'user'}{'email'});
        web_db_log(
            {   'robot'        => $robot,
                'list'         => $list->{'name'},
                'action'       => $param->{'action'},
                'parameters'   => "$in{'path'}",
                'target_email' => "",
                'msg_id'       => '',
                'status'       => 'error',
                'error_type'   => 'authorization',
                'user_email'   => $param->{'user'}{'email'},
            }
        );
        return undef;
    }

    my $may_edit    = $access{'may'}{'edit'};
    my $may_control = $access{'may'}{'control'};
    $param->{'may_edit'}    = $may_edit;
    $param->{'may_control'} = $may_control;

    ### File or directory ?
    if ($document->{'type'} eq 'url') {
        $param->{'file_extension'} = $document->{'file_extension'};
        $param->{'redirect_to'}    = $document->{'url'};
        return 1;

    } elsif ($document->{'type'} eq 'file') {
        $param->{'file'}   = $document->{'absolute_path'};
        $param->{'bypass'} = 1;
        return 1;

    } else {    # directory

        $param->{'empty'} = $#{$document->{'subdir'}} == -1;

        # subdirectories hash
        my %subdirs;
        # file hash
        my %files;

        ## for the exception of index.html
        # name of the file "index.html" if exists in the directory read
        my $indexhtml;

        # boolean : one of the subdirectories or files inside
        # can be edited -> normal mode of read -> d_read.tt2;
        my $normal_mode;

        my $path_doc;
        my %desc_hash;
        my $may, my $def_desc;

        foreach my $subdocument (@{$document->{'subdir'}}) {

            my $d        = $subdocument->{'filename'};
            my $path_doc = $subdocument->{'path'};

            ## Subdir
            if ($subdocument->{'type'} eq 'directory') {

                if ($subdocument->{'access'}{'may'}{'read'}) {

                    $subdirs{$d} = $subdocument->dup();
                    $subdirs{$d}{'doc'} = $subdocument->{'visible_filename'};
                    $subdirs{$d}{'escaped_doc'} =
                        $subdocument->{'escaped_filename'};

                    if ($param->{'user'}{'email'}) {
                        if ($subdocument->{'access'}{'may'}{'control'} == 1) {

                            $subdirs{$d}{'edit'} =
                                1;    # or = $may_action_edit ?
                            # if index.html, must know if something can be
                            # edit in the dir
                            $normal_mode = 1;
                        } elsif ($subdocument->{'access'}{'may'}{'edit'} != 0)
                        {
                            # $may_action_edit = 0.5 or 1
                            $subdirs{$d}{'edit'} =
                                $subdocument->{'access'}{'may'}{'edit'};
                            # if index.html, must know if something can be
                            # edit in the dir
                            $normal_mode = 1;
                        }

                        if ($subdocument->{'access'}{'may'}{'control'}) {
                            $subdirs{$d}{'control'} = 1;
                        }
                    }
                }
            } else {
                # case file

                if ($subdocument->{'access'}{'may'}{'read'}) {

                    $files{$d} = $subdocument->dup();

                    $files{$d}{'doc'} = $subdocument->{'visible_filename'};
                    $files{$d}{'escaped_doc'} =
                        $subdocument->{'escaped_filename'};

                    ## exception of index.html
                    if ($d =~ /^(index\.html?)$/i) {
                        $indexhtml = $1;
                    }

                    if ($param->{'user'}{'email'}) {
                        if ($subdocument->{'access'}{'may'}{'edit'} == 1) {
                            $normal_mode = 1;
                            $files{$d}{'edit'} = 1;    # or = $may_action_edit ?
                        } elsif ($subdocument->{'access'}{'may'}{'edit'} != 0)
                        {
                            # $may_action_edit = 1 or 0.5
                            $normal_mode = 1;
                            $files{$d}{'edit'} =
                                $subdocument->{'access'}{'may'}{'edit'};
                        }

                        if ($subdocument->{'access'}{'may'}{'control'}) {
                            $files{$d}{'control'} = 1;
                        }
                    }
                }
            }
        }

        ### Exception : index.html
        if ($indexhtml) {
            unless ($normal_mode) {
                $param->{'file_extension'} = 'html';
                $param->{'bypass'}         = 1;
                $param->{'file'}           = $document->{'absolute_path'};
                return 1;
            }
        }

        ## to sort subdirs
        my @sort_subdirs;
        my $order = $in{'order'} || 'order_by_doc';
        $param->{'order_by'} = $order;
        foreach my $k (sort { by_order($order, \%subdirs) } keys %subdirs) {
            push @sort_subdirs, $subdirs{$k};
        }

        ## to sort files
        my @sort_files;
        foreach my $k (sort { by_order($order, \%files) } keys %files) {
            push @sort_files, $files{$k};
        }

        # parameters for the template file
        $param->{'list'} = $list_name;

        $param->{'father'}         = $document->{'father_path'};
        $param->{'escaped_father'} = $document->{'escaped_father_path'};
        $param->{'description'}    = $document->{'title'};
        $param->{'serial_desc'}    = $document->{'serial_desc'};
        $param->{'path'}           = $document->{'path'};
        $param->{'visible_path'}   = $document->{'visible_path'};
        $param->{'escaped_path'}   = $document->{'escaped_path'};

        if (scalar keys %subdirs) {
            $param->{'sort_subdirs'} = \@sort_subdirs;
        }
        if (scalar keys %files) {
            $param->{'sort_files'} = \@sort_files;
        }
    }
    $param->{'father_icon'} = Sympa::Tools::WWW::get_icon($robot, 'father');
    $param->{'sort_icon'}   = Sympa::Tools::WWW::get_icon($robot, 'sort');

    ## Show expert commands / user page

    # for the curent directory
    if ($may_edit == 0 && $may_control == 0) {
        $param->{'has_dir_rights'} = 0;
    } else {
        $param->{'has_dir_rights'} = 1;
        if ($may_edit == 1) {    # (is_author || ! moderated)
            $param->{'total_edit'} = 1;
        }
    }

    # set the page mode
    if ($in{'show_expert_page'} && $param->{'has_dir_rights'}) {
        $session->{'shared_mode'} = 'expert';
        if ($param->{'user'}{'prefs'}{'shared_mode'} ne 'expert') {
            # update user pref  as soon as connected user change shared mode
            $param->{'user'}{'prefs'}{'shared_mode'} = 'expert';
            Sympa::User::update_global_user(
                $param->{'user'}{'email'},
                {   data => Sympa::Tools::Data::hash_2_string(
                        $param->{'user'}{'prefs'}
                    )
                }
            );
        }
        $param->{'expert_page'} = 1;

    } elsif ($in{'show_user_page'}) {
        $session->{'shared_mode'} = 'basic';
        if ($param->{'user'}{'prefs'}{'shared_mode'} ne 'basic') {
            # update user pref  as soon as connected user change shared mode
            $param->{'user'}{'prefs'}{'shared_mode'} = 'basic';
            Sympa::User::update_global_user(
                $param->{'user'}{'email'},
                {   data => Sympa::Tools::Data::hash_2_string(
                        $param->{'user'}{'prefs'}
                    )
                }
            );
        }
        $param->{'expert_page'} = 0;
    } else {
        if (   $session->{'shared_mode'} eq 'expert'
            && $param->{'has_dir_rights'}) {
            $param->{'expert_page'} = 1;
        } else {
            $param->{'expert_page'} = 0;
        }
    }

    web_db_log(
        {   'robot'        => $robot,
            'list'         => $list->{'name'},
            'action'       => $param->{'action'},
            'parameters'   => "$in{'path'}",
            'target_email' => "",
            'msg_id'       => '',
            'status'       => 'success',
            'error_type'   => '',
            'user_email'   => $param->{'user'}{'email'},
        }
    );
    return 1;
}

## Check authorizations to the current action
## used in common cases where actions fails unless result is 'do_it'
## It does not apply to actions that can be moderated
sub check_authz {
    my ($subname, $action) = @_;

    my $result = Sympa::Scenario::request_action(
        $list, $action,
        $param->{'auth_method'},
        {   'sender' => $param->{'user'}{'email'} || 'nobody',
            'remote_host' => $param->{'remote_host'},
            'remote_addr' => $param->{'remote_addr'}
        }
    );
    my $r_action;
    my $reason;
    if (ref($result) eq 'HASH') {
        $r_action = $result->{'action'};
        $reason   = $result->{'reason'};
    }

    unless ($r_action =~ /do_it/i) {
        unless (prevent_visibility_bypass()) {
            Sympa::Report::reject_report_web('auth', $reason,
                {'login' => $param->{'need_login'}},
                $param->{'action'});
        }
        wwslog(
            'info',   'Access denied in %s for %s',
            $subname, $param->{'user'}{'email'}
        );
        return undef;
    }

    return 1;
}

sub get_server_details {
    ## All Robots are shown to super listmaster
    if (Sympa::is_listmaster('*', $param->{'user'}{'email'})) {
        $param->{'main_robot'} = 1;
        #FIXME: Entry for main robot is missing.
        $param->{'robots'} = $Conf::Conf{'robots'};
    }

    ## Families
    my @families =
        sort map { $_->{'name'} } @{Sympa::Family::get_families($robot)};
    if (@families) {
        $param->{'families'} = \@families;
    }
}

# Set Sympa parameters in $param->{'conf'}
# Note: This have not been used yet.
sub get_safe_robot_conf {
    $param->{'conf'} = {};
    foreach my $p (
        qw(email host soap_url wwsympa_url listmaster_email
        logo_html_definition favicon_url
        main_menu_custom_button_1_url main_menu_custom_button_1_title
        main_menu_custom_button_1_target main_menu_custom_button_2_url
        main_menu_custom_button_2_title main_menu_custom_button_2_target
        main_menu_custom_button_3_url main_menu_custom_button_3_title
        main_menu_custom_button_3_target static_content_url
        dark_color light_color text_color bg_color error_color
        use_blacklist antispam_feature custom_robot_parameter
        selected_color shaded_color color_0 color_1 color_2 color_3
        color_4 color_5 color_6 color_7 color_8 color_9 color_10 color_11
        color_12 color_13 color_14 color_15
        reporting_spam_script_path automatic_list_families
        spam_protection
        )
        ) {
        $param->{'conf'}{$p} = $robot->$p;
        $param->{$p} = $robot->$p
            if $p =~ /_color\z/
                or $p =~ /\Acolor_/
                or $p =~ /_url\z/;
    }

    ## compatibility concern: deprecated attributes of Robot.
    $param->{'conf'}{'sympa'}   = $robot->get_address();           # <=6.1
    $param->{'conf'}{'request'} = $robot->get_address('owner');    # <=6.1
}

sub do_maintenance {
    wwslog('notice', '');
    return 1;
}

sub do_automatic_lists_management_request {
    wwslog('notice', 'Starting');
    $param->{'automatic_lists_description'} =
        Conf::load_automatic_lists_description();
    return 1;
}

sub do_automatic_lists_management {
    wwslog('notice', 'Starting');

    return 1;
}

sub do_automatic_lists_request {
    wwslog('notice', 'Starting');
    # check authorization
    my $family;
    unless ($family = Sympa::Family->new($in{'family'}, $robot)) {
        wwslog('err',
            'Failed to instantiate family %s. This family does not exist',
            $in{'family'});
        Sympa::send_notify_to_listmaster(
            $robot,
            'automatic_list_creation_failed',
            [   "Failed to instantiate family $in{'family'}. This family does not exist."
            ]
        );
        return undef;
    }

    unless (
        $family->is_allowed_to_create_automatic_lists(
            (   'auth_level' => 'md5',
                'sender'     => $session->{'email'},
                'message'    => undef,
                'listname'   => ''
            )
        )
        ) {
        Sympa::Report::reject_report_web('auth',
            "You are not allowed to create list in this family",
            {}, $param->{'action'});
        wwslog('err',
            'Access to automatic list creation form denied to user %s',
            $session->{'email'});
        return undef;
    }

    $param->{'family'} = $family;
    return 1;
}

sub do_automatic_lists {
    wwslog('notice', 'Starting');
    my $family_name = $in{'family'};
    my $family;

    my @list_name_parts;
    my $families_config =
        Conf::get_robot_conf($robot, 'automatic_list_families');
    my $family_config = $families_config->{$family_name};
    my $listname =
        $family_config->{'prefix'} . $family_config->{'prefix_separator'};
    foreach my $input (keys %in) {
        next unless ($input =~ /automatic_list_part_(\d+)/);
        $list_name_parts[$1] = $in{$input};
    }
    foreach my $list_name_part (@list_name_parts) {
        $listname .= "$list_name_part$family_config->{'classes_separator'}";
    }
    my $sep = $family_config->{'classes_separator'} . '$';
    if ($listname =~ /(.*)($sep)/) {
        $listname = $1;
    }

    $list = Sympa::List->new($listname, $robot);
    unless (defined $list) {
        ## Automatic creation of a mailing list, based on a family
        unless ($family = Sympa::Family->new($family_name, $robot)) {
            $log->syslog('err',
                "Failed to create the dynamic list $listname: family $family_name does not exist."
            );
            Sympa::send_notify_to_listmaster(
                $robot,
                'automatic_list_creation_failed',
                [   "Failed to create the dynamic list $listname: family $family_name does not exist."
                ]
            );
            return undef;
        }

        unless (
            $list = $family->create_automatic_list(
                (   'listname'   => $listname,
                    'auth_level' => 'md5',
                    'sender'     => $session->{'email'}
                )
            )
            ) {
            $log->syslog('err', 'Failed to create the dynamic list %s',
                $listname);
            Sympa::send_notify_to_listmaster(
                $robot,
                'automatic_list_creation_failed',
                ["Failed to create the dynamic list $listname."]
            );
            return undef;
        }
    }

    $in{'list'} = $listname;
    return 'compose_mail';
}

sub prevent_visibility_bypass {
    wwslog('debug2', 'Starting');
    if (defined $list and ref $list eq 'Sympa::List') {
        my $result = Sympa::Scenario::request_action(
            $list,
            'visibility',
            $param->{'auth_method'},
            {   'sender'      => $param->{'user'}{'email'},
                'remote_host' => $param->{'remote_host'},
                'remote_addr' => $param->{'remote_addr'}
            }
        );

        my $sub_is;
        my $reason;
        if (ref($result) eq 'HASH') {
            $sub_is = $result->{'action'};
            $reason = $result->{'reason'};
        }
        if ($sub_is =~ /reject/) {
            wwslog('info',
                'visibility: List must remain hidden. Returning "home" to prevent visibility bypass'
            );
            return "home";
        } else {
            return undef;
        }
    }
    return undef;
}

sub purely_closed {
    my $action   = shift;
    my $scenario = Sympa::Scenario->new(
        'robot'     => $robot,
        'directory' => $list->{'dir'},
        'file_path' => $list->{'admin'}{$action}{'file_path'},
        'options'   => undef
    );

    return $scenario->is_purely_closed;
}

# Old name: tools::add_in_blacklist().
sub _add_in_blacklist {
    my $entry = shift;
    my $robot = shift;
    my $list  = shift;

    $log->syslog('info', '(%s, %s, %s)', $entry, $robot, $list->{'name'});
    $entry = lc($entry);
    chomp $entry;

    # robot blacklist not yet availible
    unless ($list) {
        $log->syslog('info',
            'Robot blacklist not yet availible, missing list parameter');
        return undef;
    }
    unless (($entry) && ($robot)) {
        $log->syslog('info', 'Missing parameters');
        return undef;
    }
    if ($entry =~ /\*.*\*/) {
        $log->syslog('info', 'Incorrect parameter %s', $entry);
        return undef;
    }
    my $dir = $list->{'dir'} . '/search_filters';
    unless ((-d $dir) || mkdir($dir, 0755)) {
        $log->syslog('info', 'Unable to create dir %s', $dir);
        return undef;
    }
    my $file = $dir . '/blacklist.txt';

    my $fh;
    if (open $fh, '<', $file) {
        while (<$fh>) {
            next if (/^\s*$/o || /^[\#\;]/o);
            my $regexp = $_;
            chomp $regexp;
            $regexp =~ s/\*/.*/;
            $regexp = '^' . $regexp . '$';
            if ($entry =~ /$regexp/i) {
                $log->syslog('notice', '%s already in blacklist(%s)',
                    $entry, $_);
                return 0;
            }
        }
        close $fh;
    }
    unless (open $fh, '>>', $file) {
        $log->syslog('info', 'Append to file %s', $file);
        return undef;
    }
    print $fh "$entry\n";
    close $fh;

}

__END__

=encoding utf-8

=head1 NAME 

wwsympa, wwsympa.fcgi - WWSympa, Sympa's web interface 

=head1 DESCRIPTION 

This CGI/FastCGI script completely handles all aspects of the Sympa web interface.

To know details on WWSympa, see Reference Manual:
L<https://www.sympa.org/manual/web-interface>.

=head1 HISTORY

WWSympa was initially written by:

=over 

=item * Serge Aumont <sa AT cru.fr> 

=item * Olivier SalaE<252>n <os AT cru.fr> 

=back 

The first alpha version of WWSympa appeared on Sympa 2.3.4.

=cut 
