#!--PERL-- # -*- indent-tabs-mode: nil; -*- # vim:ft=perl:et:sw=4 # $Id$ # 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, 2017 GIP RENATER # Copyright 2017, 2018, 2019, 2020, 2021 The Sympa Community. See the # AUTHORS.md file at the top-level directory of this distribution and at # . # # 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 . ## Copyright 1999 Comité Réseaux des Universités ## web interface to Sympa mailing lists manager ## Sympa: http://www.sympa.org/ ## Authors : ## Serge Aumont ## Olivier Salaün use strict; ##use warnings; use lib split(/:/, $ENV{SYMPALIB} || ''), '--modulesdir--'; use Archive::Zip qw(); use DateTime; use DateTime::Format::Mail; use Digest::MD5; use Encode qw(); use English qw(-no_match_vars); use IO::File qw(); 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::Archive; 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::List::Config; use Sympa::List::Users; use Sympa::Log; use Sympa::Message; use Sympa::Regexps; use Sympa::Robot; use Sympa::Scenario; use Sympa::Spindle::ProcessRequest; use Sympa::Spindle::ResendArchive; use Sympa::Spool::Archive; use Sympa::Spool::Auth; use Sympa::Spool::Held; use Sympa::Spool::Incoming; use Sympa::Spool::Listmaster; use Sympa::Spool::Moderation; use Sympa::Spool::Outgoing; use Sympa::Spool::Topic; use Sympa::Task; use Sympa::Template; use Sympa::Ticket; use Sympa::Tools::Data; use Sympa::Tools::File; use Sympa::Tools::Password; use Sympa::Tools::Text; use Sympa::Tracking; use Sympa::User; use Sympa::WWW::Auth; use Sympa::WWW::FastCGI; use Sympa::WWW::Marc::Search; use Sympa::WWW::Report; use Sympa::WWW::Session; use Sympa::WWW::SharedDocument; use Sympa::WWW::Tools; ## WWSympa librairies my %options; my $sympa_conf_file = Sympa::Constants::CONFIG; our $list; our $param = {}; our $robot_id; our $session; my $robot; my $cookie_domain; 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'}); Sympa::Spool::Listmaster->instance->{use_bulk} = 1; # 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 = ( 'confirm_action' => 'do_confirm_action', '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', 'decl_add' => 'do_decl_add', 'decl_del' => 'do_decl_del', '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', 'set' => 'do_set', 'admin' => 'do_admin', 'import' => 'do_import', 'add' => 'do_add', 'auth_add' => 'do_auth_add', 'del' => 'do_del', 'auth_del' => 'do_auth_del', 'mass_del' => 'do_mass_del', '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', #XXX'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', 'edit' => 'do_edit', 'viewbounce' => 'do_viewbounce', 'redirect' => 'do_redirect', 'rename_list_request' => 'do_rename_list_request', 'move_list' => 'do_move_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' => 'do_close_list', 'open_list' => 'do_open_list', 'purge_list' => 'do_purge_list', 'upload_pictures' => 'do_upload_pictures', 'delete_pictures' => 'do_delete_pictures', 'd_read' => 'do_d_read', 'd_create_child' => 'do_d_create_child', 'd_unzip' => 'do_d_unzip', 'd_editfile' => 'do_d_editfile', 'd_properties' => 'do_d_properties', 'd_update' => 'do_d_update', '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', 'export_member' => 'do_export_member', 'remind' => 'do_remind', 'move_user' => 'do_move_user', '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', 'blocklist' => 'do_blocklist', 'edit_attributes' => 'do_edit_attributes', 'ticket' => 'do_ticket', 'manage_template' => 'do_manage_template', 'rt_create' => 'do_rt_create', 'rt_delete' => 'do_rt_delete', 'rt_edit' => 'do_rt_edit', 'rt_setdefault' => 'do_rt_setdefault', 'rt_update' => 'do_rt_update', #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', #XXX'automatic_lists_management_request' => #XXX 'do_automatic_lists_management_request', #XXX'automatic_lists_management' => 'do_automatic_lists_management', 'create_automatic_list' => 'do_create_automatic_list', 'create_automatic_list_request' => 'do_create_automatic_list_request', 'auth' => 'do_auth', 'delete_account' => 'do_delete_account', ); my %comm_aliases = ( 'add_fromsub' => 'auth_add', 'add_request' => 'import', 'automatic_lists' => 'create_automatic_list', 'automatic_lists_request' => 'create_automatic_list_request', 'blacklist' => 'blocklist', 'change_email' => 'move_user', 'change_email_request' => 'move_user', 'del_fromsig' => 'auth_del', 'dump' => 'export_member', 'family_signoff_request' => 'family_signoff', 'ignoresig' => 'decl_del', 'ignoresub' => 'decl_add', 'loginrequest' => 'login', 'rename_list' => 'move_list', 'restore_list' => 'open_list', 'sigrequest' => 'signoff', 'subrequest' => 'subscribe', ); # No longer used. #my %auth_action; # Arguments awaited in the PATH_INFO, depending on the action. # NOTE: # * The email addresses should NOT be embedded in PATH_INFO, because included # slashes (/) cannot be handled correctly by web servers. They are kept just # for compatibility to earlier releases of Sympa. Use query parameters # instead. 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' => ['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'], #XXX'css' => ['file'], 'pref' => ['previous_action', 'previous_list'], 'reject' => ['list', 'id'], 'distribute' => ['list', 'id'], 'add_frommod' => ['list', 'id'], 'dump_scenario' => ['list', 'scenario_function'], 'd_reject_shared' => ['list', 'id'], 'd_install_shared' => ['list', 'id'], 'modindex' => ['list'], 'docindex' => ['list'], 'viewmod' => ['list', 'id', '@file'], 'add' => ['list', 'email'], 'import' => ['list'], 'del' => ['list', 'email'], #'editsubscriber' => # ['list', 'email', 'previous_action', 'custom_attribute'], #'editsubscriber' => ['list', 'email', 'previous_action'], 'editsubscriber' => ['list'], 'edit' => ['list', 'role'], #'viewbounce' => ['list', 'email', '@file'], 'viewbounce' => ['list', 'dir', '@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'], #'subrequest' => ['list','email'], 'subindex' => ['list'], 'decl_add' => ['list'], 'signoff' => ['list'], 'auto_signoff' => ['list'], 'family_signoff' => ['family'], #'family_signoff_request' => ['family', 'email'], #'sigrequest' => ['list', 'email'], 'sigindex' => ['list'], 'decl_del' => ['list'], '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_child' => ['list', '@path'], 'd_update' => ['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'], 'export_member' => ['list', 'format'], 'search' => ['list', 'filter'], 'search_user' => ['email'], 'set_lang' => ['lang'], 'attach' => ['list', 'dir', 'file'], 'stats' => ['list'], 'edit_list_request' => ['list', 'group'], 'move_list' => ['list', 'new_listname', 'new_robot'], 'copy_list' => ['list', 'new_listname', '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'], 'ticket' => ['ticket'], 'move_user' => [], 'manage_template' => ['subaction', 'list', 'message_template'], 'rt_delete' => ['list', 'message_template'], 'rt_edit' => ['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'], #XXX'automatic_lists_management_request' => [], #XXX'automatic_lists_management' => [], 'create_automatic_list' => ['family'], 'create_automatic_list_request' => ['family'], 'auth' => ['id', 'heldaction', 'listname'], 'auth_add' => ['list'], 'auth_del' => ['list'], ); ## 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'], 'import' => ['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'], 'arcsearch' => ['param.list'], 'arcsearch_form' => ['param.list'], 'arcsearch_id' => ['param.list'], 'auth' => ['id', 'heldaction', 'email'], 'auth_add' => ['param.list', 'param.user.email', 'id'], 'auth_del' => ['param.list', 'param.user.email', 'id'], 'auto_signoff' => ['param.list', 'email'], 'attach' => ['param.list'], 'blocklist' => ['param.list'], 'move_user' => ['param.user.email', 'current_email|old_email', 'email|new_email'], 'close_list' => ['param.user.email', 'param.list'], 'compose_mail' => ['param.user.email', 'param.list'], 'copy_template' => ['webormail'], ## other required parameters are checked in the subroutine 'create_automatic_list' => ['param.user.email', 'family'], 'create_automatic_list_request' => ['param.user.email', 'family'], 'create_list' => ['param.user.email', 'info'], 'create_list_request' => ['param.user.email'], #XXX'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_child' => ['param.list', 'param.user.email', 'new_name|uploaded_file'], '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_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_update' => ['param.list', 'param.user.email', 'content|url|uploaded_file'], 'd_set_owner' => ['param.list', 'param.user.email'], 'd_unzip' => ['param.list', 'param.user.email', 'uploaded_file'], '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_scenario' => ['param.list', 'scenario_function|pname'], 'edit' => ['param.list', 'param.user.email', 'role', 'email'], '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'], 'export_member' => ['param.list'], 'family_signoff' => ['family', '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'], 'decl_del' => ['param.list', 'param.user.email', 'id'], 'decl_add' => ['param.list', 'param.user.email', 'id'], 'delete_account' => ['passwd', 'i_understand_the_consequences'], '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'], 'my' => ['param.user.email'], 'rt_create' => ['param.list', 'param.user.email', 'new_template_name'], 'rt_delete' => ['param.list', 'param.user.email', 'message_template'], 'rt_edit' => ['param.list', 'param.user.email', 'message_template'], 'rt_setdefault' => ['param.list', 'param.user.email', 'new_default'], 'rt_update' => ['param.list', 'param.user.email', 'message_template', 'content'], 'modindex' => ['param.list', 'param.user.email'], 'docindex' => ['param.list', 'param.user.email'], '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'], 'move_list' => ['param.user.email', 'param.list', 'new_listname', 'new_robot'], 'copy_list' => ['param.user.email', 'param.list', 'new_listname', 'new_robot'], 'open_list' => ['param.user.email', 'param.list'], 'rename_list_request' => ['param.user.email', 'param.list'], 'request_topic' => ['param.list', 'authkey'], 'resetbounce' => ['param.list', 'param.user.email', 'email'], 'review' => ['param.list'], 'review_family' => ['param.user.email', 'family_name'], 'reviewbouncing' => ['param.list'], 'rss_request' => [], 'savefile' => ['param.user.email', 'file'], 'search' => ['param.list'], '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.user.email', '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'], 'skinsedit' => ['param.user.email'], 'sso_login' => ['auth_service_name'], 'stats' => ['param.list'], 'subindex' => ['param.list', 'param.user.email'], 'suboptions' => ['param.list', 'param.user.email'], '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|file'], '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'], 'auth_add' => ['owner', 'editor'], 'auth_del' => ['owner', 'editor'], 'blocklist' => ['owner', 'editor'], 'close_list' => ['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' => ['editor', 'owner', 'listmaster'], 'edit_list' => ['owner'], 'edit_list_request' => ['owner'], 'edit_template' => ['listmaster'], 'editfile' => ['owner', 'listmaster'], 'editsubscriber' => ['owner', 'editor'], 'get_closed_lists' => ['listmaster'], 'get_inactive_lists' => ['listmaster'], 'get_latest_lists' => ['listmaster'], 'get_biggest_lists' => ['listmaster'], 'get_pending_lists' => ['listmaster'], 'decl_del' => ['owner', 'editor'], 'decl_add' => ['owner', 'editor'], 'including_lists' => ['owner', 'listmaster'], 'install_pending_list' => ['listmaster'], 'edit_config' => ['listmaster'], 'ls_templates' => ['listmaster'], 'manage_template' => ['owner'], 'mass_del' => ['listmaster'], 'rt_create' => ['owner'], 'rt_delete' => ['owner'], 'rt_edit' => ['owner'], 'rt_setdefault' => ['owner'], 'rt_update' => ['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'], 'move_list' => ['privileged_owner'], 'copy_list' => ['owner', 'listmaster'], 'open_list' => ['listmaster'], 'rename_list_request' => ['privileged_owner'], 'resetbounce' => ['owner', 'editor'], 'review_family' => ['listmaster'], 'reviewbouncing' => ['owner', 'editor'], 'savefile' => ['owner', 'listmaster'], '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'], #XXX'automatic_lists_management_request' => ['listmaster'], #XXX'automatic_lists_management' => ['listmaster'], ); # An action is a candidate for this list if it modifies an object or setting. # # Why not just protect all actions? Many of them are used in GET requests # without any forms, making it more difficult to supply a CSRF token. # This list intentionally starts out small in the name of breaking as little # as possible. our %require_csrftoken = ( 'add' => 1, 'del' => 1, 'move_user' => 1, 'savefile' => 1, 'setpasswd' => 1, 'setpref' => 1, ); # 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', 'admin' => 'admin', 'import' => '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' => 'admin', 'edit_list_request' => 'admin', 'edit_list' => 'admin', 'editsubscriber' => 'admin', 'viewbounce' => 'admin', 'resetbounce' => 'admin', 'scenario_test' => 'admin', 'close_list' => 'admin', 'd_admin' => 'admin', 'd_reject_shared' => 'admin', 'd_install_shared' => 'admin', 'dump_scenario' => 'admin', 'export_member' => 'admin', 'open_list' => 'admin', 'remind' => 'admin', #'subindex' => 'admin', 'stats' => 'admin', 'decl_del' => 'admin', 'decl_add' => 'admin', 'move_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', 'blocklist' => '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', #XXX'automatic_lists_management_request' => 'serveradmin', #XXX'automatic_lists_management' => 'serveradmin', ); # Actions that are not used in return of login, my %temporary_actions = ( 'confirm_action' => 1, 'logout' => 1, 'loginrequest' => 1, 'login' => 1, 'sso_login' => 1, 'sso_login_succeeded' => 1, 'ticket' => 1, #XXX'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' => '.+', 'deleted_param' => '.+', ## Textarea content 'template_content' => '.+', 'content' => '.+', 'body' => '.+', 'info' => '.+', 'new_scenario_content' => '.+', 'blacklist' => '.*', # Compat.<=6.2.60 'blocklist' => '.*', ## Integer 'page' => '\d+|owner|editor', '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 "'") 'dir' => '[^<>\\\*\$\n]+', 'new_name' => '[^<>\\\*\$\[\]\/\n]+', 'shortname' => '[^<>\\\*\$\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]+', 'msg_subject' => '.*', ## 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\-\.\+]*', 'listname' => '[\w\-\.\+]*', 'new_listname' => '[\w\-\.\+]*', 'selected_lists' => '[\w\-\.\+]*', ## Family names 'family_name' => Sympa::Regexps::family_name(), 'family' => Sympa::Regexps::family_name(), # Email addresses 'current_email' => Sympa::Regexps::email(), '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(), 'del_emails' => '.*', '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_name(), 'read_access' => Sympa::Regexps::scenario_name(), 'edit_access' => Sympa::Regexps::scenario_name(), ## 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' => '.*', 'color_0' => '\#[0-9a-fA-F]+', 'color_1' => '\#[0-9a-fA-F]+', 'color_2' => '\#[0-9a-fA-F]+', 'color_3' => '\#[0-9a-fA-F]+', 'color_4' => '\#[0-9a-fA-F]+', 'color_5' => '\#[0-9a-fA-F]+', 'color_6' => '\#[0-9a-fA-F]+', 'color_7' => '\#[0-9a-fA-F]+', 'color_8' => '\#[0-9a-fA-F]+', 'color_9' => '\#[0-9a-fA-F]+', 'color_10' => '\#[0-9a-fA-F]+', 'color_11' => '\#[0-9a-fA-F]+', 'color_12' => '\#[0-9a-fA-F]+', 'color_13' => '\#[0-9a-fA-F]+', 'color_14' => '\#[0-9a-fA-F]+', 'color_15' => '\#[0-9a-fA-F]+', ## 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+', # Role 'role' => 'member|editor|owner', ); ## 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|\.)'); # No longer used as of 6.2.19b. #my %filtering; ## 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; ## 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'); } our %in; my $query; my $birthday = [stat $PROGRAM_NAME]->[9]; my $bulk = Sympa::Spool::Outgoing->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 = 0; my $start_time = time; while ($query = Sympa::WWW::FastCGI->new) { $loop_count++; undef $param; undef $list; undef $robot; undef $cookie_domain; undef $ip; undef $rss; undef $ajax; undef $session; $log->{level} = $Conf::Conf{'log_level'}; $language->set_lang(Sympa::best_language('*')); # Process grouped notifications. Sympa::Spool::Listmaster->instance->flush; ## Check effective ID unless ($EUID eq (getpwnam(Sympa::Constants::USER))[2]) { $maintenance_mode = 1; Sympa::WWW::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::WWW::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 ($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(); foreach my $envvar ( qw(ORIG_PATH_INFO ORIG_SCRIPT_NAME PATH_INFO QUERY_STRING REMOTE_ADDR REMOTE_HOST REQUEST_METHOD SCRIPT_NAME SERVER_NAME SERVER_PORT SYMPA_DOMAIN) ) { $log->syslog('debug', '%s=%s', $envvar, $ENV{$envvar}); } ## Get params in a hash %in = $query->Vars; # Determin robot. $robot = $ENV{SYMPA_DOMAIN}; unless ($robot) { # No robot providing web service found. print "Status: 421 Misdirected Request\n"; print "\n\n"; next; } # Default robot. $param->{'default_robot'} = 1 if $robot eq $Conf::Conf{'domain'}; $ip = $ENV{'REMOTE_HOST'} || $ENV{'REMOTE_ADDR'} || 'undef'; $cookie_domain = Sympa::WWW::Tools::get_cookie_domain($robot); $log->{level} = Conf::get_robot_conf($robot, 'log_level'); ## Sympa parameters in $param->{'conf'} $param->{'conf'} = {}; foreach my $p ( 'email', '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', 'use_blocklist', 'antispam_feature', 'custom_robot_parameter', 'reporting_spam_script_path', 'automatic_list_families', 'spam_protection', 'pictures_max_size', 'show_report_abuse', 'quiet_subscription', 'allow_account_deletion', ) { $param->{'conf'}{$p} = Conf::get_robot_conf($robot, $p); $param->{$p} = Conf::get_robot_conf($robot, $p) if $p =~ /_url\z/; } # Compat.: deprecated attributes of Robot. $param->{'conf'}{'sympa'} = Sympa::get_address($robot); $param->{'conf'}{'request'} = Sympa::get_address($robot, 'owner'); # Compat <= 6.2.16: CSS related. $param->{'css_path'} = sprintf '%s/%s', $Conf::Conf{'css_path'}, $robot; $param->{'css_url'} = sprintf '%s/%s', $Conf::Conf{'css_url'}, $robot; # Compat. < 6.2.32: "host" parameter was deprecated. $param->{'conf'}{'host'} = Conf::get_robot_conf($robot, 'domain'); # Compat. < 6.2.62: Renamed parameters. $param->{'conf'}{'use_blacklist'} = Conf::get_robot_conf($robot, 'use_blocklist'); 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, '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::WWW::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::WWW::Report::init_report_web(); ## Get PATH_INFO parameters get_parameters($robot); # Propagate plugins parameters $param->{'plugin'} = $in{'plugin'}; $session = Sympa::WWW::Session->new( $robot, { 'cookie' => Sympa::WWW::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) { wwslog('info', 'Failed to create session'); $session = Sympa::WWW::Session->new($robot, {}); } # Generate session-specific CSRF token if (not defined($session->{'csrftoken'})) { $session->{'csrftoken'} = Digest::MD5::md5_hex(sprintf("%d %d", time, rand 0xFFFFFFFF)); wwslog('debug', "Session CSRF token: %s", $session->{'csrftoken'}); } $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->{'csrftoken'} = $session->{'csrftoken'}; ## RSS does not require user authentication unless ($rss) { if ( $Crypt::OpenSSL::X509::VERSION and $ENV{SSL_CLIENT_VERIFY} and $ENV{SSL_CLIENT_VERIFY} eq 'SUCCESS' and $in{'action'} ne 'sso_login') { # Get rfc822Name in X.509v3 subjectAltName, otherwise # emailAddress attribute in subject DN (the first one of either). # Note: Earlier efforts getting attribute such as MAIL, Email in # subject DN are no longer supported. my $x509 = eval { Crypt::OpenSSL::X509->new_from_string($ENV{SSL_CLIENT_CERT}); }; my $email = Sympa::Tools::Text::canonic_email($x509->email) if $x509 and Sympa::Tools::Text::valid_email($x509->email); if ($email) { $param->{'user'}{'email'} = $email; $session->{'email'} = $email; $param->{'auth_method'} = 'smime'; $session->{'auth'} = 'x509'; $param->{'ssl_client_s_dn'} = $x509->subject; $param->{'ssl_client_v_end'} = $x509->notAfter; $param->{'ssl_client_i_dn'} = $x509->issuer; # Only with Apache+mod_ssl or lighttpd+mod_openssl. $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::WWW::Tools::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::WWW::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::WWW::Tools::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'; #FIXME $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); } } if ($param->{'user'}{'email'}) { 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'}; # Resolve alias. $action = $comm_aliases{$action} while $action and exists $comm_aliases{$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. # - Embedded images in archive should be ignored. # - 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. if (not $temporary_actions{$action} and $ENV{'REQUEST_METHOD'} eq 'GET') { my $arc_file = $in{'arc_file'} // ''; unless ( $action eq 'arc' and not($arc_file eq '' or $arc_file =~ m{/\z} or $arc_file =~ m{\A(?:mail|msg|thrd)\d+[.]html\z}) ) { my $redirect_url = Sympa::WWW::Tools::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'}; $session->confirm_action($action, 'init'); #if ($in{'action'} eq 'css') { # do_css(); # $param->{'action'} = 'css'; #} elsif if ($maintenance_mode) { do_maintenance(); $param->{'action'} = 'maintenance'; } else { ## Session loop while ($action) { if (defined $in{'list'} and length $in{'list'}) { # Create a new Sympa::List instance. unless ($list = Sympa::List->new($in{'list'}, $robot)) { wwslog('info', 'Unknown list "%s"', $in{'list'}); if ($action eq 'info') { # To prevent sniffing lists, don't notice error to # users. $action = Conf::get_robot_conf($robot, 'default_home'); } else { Sympa::WWW::Report::reject_report_web('user', 'unknown_list', {listname => $in{'list'}}, $action, $list); last; } } } check_param_in(); if (not $comm{$action} or _is_action_disabled($action)) { # Previously we searched the list using value of action here. # To prevent sniffing lists, we no longer do. Sympa::WWW::Report::reject_report_web('user', 'unknown_action', {}, $action, $list); wwslog('info', 'Unknown action %s', $action); $action = Conf::get_robot_conf($robot, 'default_home'); unless ($comm{$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::WWW::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); # The last resort. Never use default_home. $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'}; } $param->{'is_user_allowed_to'} = sub { my $function = shift; my $list = shift; return 0 unless $function and $list; $list = Sympa::List->new($list, $robot) unless ref $list eq 'Sympa::List'; return 0 if $function eq 'subscribe' and $param->{'user'}{'email'} and $list->is_list_member($param->{'user'}{'email'}); my $result = Sympa::Scenario->new($list, $function)->authz( $param->{'auth_method'}, { 'sender' => $param->{'user'}{'email'}, 'remote_host' => $param->{'remote_host'}, 'remote_addr' => $param->{'remote_addr'} } ); return 0 unless ref $result eq 'HASH'; return 0 if $result->{action} =~ /\Areject\b/i; return 1; }; ## 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' or $maintenance_mode or $rss or $ajax) { $session->renew unless $param->{'use_ssl'}; $session->set_cookie($cookie_domain, $param->{'user'}{'cookie_delay'}, $param->{'use_ssl'}); if ($param->{'user'}{'email'}) { $session->{'auth'} ||= 'classic'; } } ## 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} = {}; } if (my $lang = $language->set_lang($param->{'lang'})) { #current 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 ; close FILE; } elsif (Sympa::WWW::Report::is_there_any_reject_report_web()) { ## for compatibility : it could be better my $intern = Sympa::WWW::Report::get_intern_error_web(); my $system = Sympa::WWW::Report::get_system_error_web(); my $user = Sympa::WWW::Report::get_user_error_web(); my $auth = Sympa::WWW::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) { #FIXME: Not used by default. $param->{'list_conf'} = $list->{'admin'}; } my $template = Sympa::Template->new( $list || $robot, 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\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) { #FIXME: Probably not used by default. $param->{'list_conf'} = $list->{'admin'}; } # 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); } } my $template = Sympa::Template->new( $list || $robot, 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_copy, '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\n", Sympa::Tools::Text::encode_html($error); } # close FILE; } elsif ($param->{'redirect_to'}) { $log->syslog('notice', 'Redirecting to %s', $param->{'redirect_to'}); _redirect($param->{'redirect_to'}); } else { prepare_report_user(); send_html('main.tt2'); } # Exit if wwsympa.fcgi itself has changed. if (defined $birthday) { my $age = [stat $PROGRAM_NAME]->[9]; if (defined $age and $birthday != $age) { $log->syslog( 'notice', 'Exiting because %s has changed since FastCGI server started', $PROGRAM_NAME ); exit(0); } } } # Purge grouped notifications Sympa::Spool::Listmaster->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'}; $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; } # No longer used. #sub new_loop; # DEPRECATED. Use Sympa::WWW::Tools::get_server_name() or # Sympa::WWW::Tools::get_http_host(). #sub get_header_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; $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+)*)$/) { # Getting $in{'action'}, $in{'response_action'} etc. $in{$1} = $3; if ($4) { foreach my $v (split /\./, $4) { $v =~ s/^\.?(\w+)\.?/$1/; $in{$v} = 1; } } undef $in{$p}; } } $param->{'nomenu'} = $in{'nomenu'}; } # Compatibility to the templates customized for version <=6.2.60: # 'blacklist' was renamed to 'blocklist'. $in{'blocklist'} //= $in{'blacklist'} if defined $in{'blacklist'}; # 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 eq 'custom_input') { my $key = $tokens[1]; $regexp = $in_regexp{'custom_input'}; $log->syslog('debug2', '(%s) %s', $p, $in{$p}); $custom_input ||= {}; $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_update' && $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) ) { Sympa::WWW::Report::reject_report_web('user', 'syntax_errors', {p_name => $p}, '', ''); wwslog( 'err', 'Syntax error for parameter %s value "%s" not conform to regexp:%s', $pname, $one_p, $regexp ); $in{$p} = ''; last; } } } $in{custom_attribute} = $custom_attribute; $in{custom_input} = $custom_input if $custom_input; $in{plugin} = $plugin; 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::WWW::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 ($param->{'user'} and $param->{'user'}{'email'}) { if (prevent_visibility_bypass()) { Sympa::WWW::Report::reject_report_web('user', 'authorization_reject', {}, $param->{'action'}, ''); } else { Sympa::WWW::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. # Once logged in, they will be redirected to the URL in # $session->{'redirect_url'}. delete $in{'submit'}; # Clear it. return 'login'; } ## 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::WWW::Report::reject_report_web('user', 'authorization_reject', {'list' => $in{'list'}}, $param->{'action'}, ''); } Sympa::WWW::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; } } } } ## Validate CSRF token when one is required if (defined($require_csrftoken{$param->{'action'}})) { wwslog('debug', 'Action %s: CSRF token required', $param->{'action'}); unless (defined($in{'csrftoken'}) and ($in{'csrftoken'} eq $session->{'csrftoken'})) { Sympa::WWW::Report::reject_report_web('user', 'authorization_reject', {'list' => $in{'list'}}, $param->{'action'}, ''); wwslog('info', 'CSRF token mismatch: in="%s" session="%s"', $in{'csrftoken'}, $session->{'csrftoken'}); web_db_log( { 'status' => 'error', 'error_type' => 'authorization' } ); delete $param->{'list'}; # invalidate the CSRF token so a new one will be generated delete $session->{'csrftoken'}; 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::WWW::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'} = Sympa::Tools::Data::clone_var($list->{'admin'}); #FIXME # Compat. < 6.2.32 $param->{'list_conf'}{'host'} = $list->{'domain'}; } ## 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'}; } } # Main CSS, possiblly customized. my $main_css; if ($session->{'custom_css'}) { $main_css = Sympa::WWW::Tools::get_css_url( $robot, custom_css => { map { ($_ => $session->{$_}) } grep { /\Acolor_/ and $session->{$_} } keys %$session } ); unless ($main_css) { wwslog('info', 'Error while parsing custom CSS'); delete $session->{'custom_css'}; } } $main_css ||= Sympa::WWW::Tools::get_css_url($robot); $param->{'main_css'} = $main_css; # Per-locale CSS. $param->{'lang_css'} = Sympa::WWW::Tools::get_css_url($robot, lang => $param->{'lang'}) if $param->{'lang'}; # 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); } } # Now include locale paths (lang parameter). my $template = Sympa::Template->new( $list || $robot, allow_absolute => $allow_absolute_path, 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. my $output = ''; unless ( $template->parse($param_copy, $tt2_file, \$output, 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; $output = ''; $template->parse($param_copy, 'tt2_error.tt2', \$output, has_header => 1); $output .= "\n\n"; # when tt2 failed to parse } # Insert CSRF token. if ($session->{'csrftoken'}) { my $csrf_field = sprintf '', $session->{'csrftoken'}; $output =~ s{ (
]* \s method="post" (?=[\s>]) [^>]* > ) ( .*? ) (
) }{ my ($beg, $content, $end) = ($1, $2, $3); $content =~ s/(
]) [^>]* > )/$1$csrf_field/ix or $content =~ s/\A/$csrf_field/; $beg . $content . $end; }egisx; } # Add autocomplete="off" to all forms unless explicitly enabled. $output =~ s{
]*? /? ) > }{ my $attrs = $1; $attrs =~ s/(\s*\/?)\z/ autocomplete="off"$1/ unless $attrs =~ /\sautocomplete="[^"]*"/i; ""; }egisx; print $output; } sub prepare_report_user { $param->{'intern_errors'} = Sympa::WWW::Report::get_intern_error_web(); $param->{'system_errors'} = Sympa::WWW::Report::get_system_error_web(); $param->{'user_errors'} = Sympa::WWW::Report::get_user_error_web(); $param->{'auth_rejects'} = Sympa::WWW::Report::get_auth_reject_web(); $param->{'notices'} = Sympa::WWW::Report::get_notice_web(); $param->{'errors'} = Sympa::WWW::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 just after List object is created and initialize output parameters. # #=head3 Arguments # #=over # #=item * I # #=back # #=head3 Return # #=over # #=item C<1> # #=back # #=cut ## Analysis of incoming parameters sub check_param_in { wwslog('debug2', ''); # Restore last login info if any: See do_login() & do_sso_login(). $param->{'last_login_epoch'} = delete $session->{'last_login_date'}; $param->{'last_login_host'} = delete $session->{'last_login_host'}; # listmaster has owner and editor privileges for the list. if (Sympa::is_listmaster($robot, $param->{'user'}{'email'})) { $param->{'is_listmaster'} = 1; } unless (ref $list eq 'Sympa::List') { $param->{'domain'} = $robot; # Compat. < 6.2.32 $param->{'host'} = $robot; } else { # Gather list configuration information for further output. $param->{'list'} = $list->{'name'}; $param->{'domain'} = $list->{'domain'}; # Compat. < 6.2.32 $param->{'host'} = $list->{'domain'}; $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->new($list, 'send')->authz( $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; my $shared_doc = Sympa::WWW::SharedDocument->new($list); $param->{'mod_total_shared'} = $shared_doc->count_moderated_descendants; if ($param->{'total'}) { $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->new($list, 'unsubscribe')->authz( $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->new($list, 'subscribe')->authz( $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 $shared_doc = Sympa::WWW::SharedDocument->new($list); my %access = $shared_doc->get_privileges( mode => 'read', sender => $param->{'user'}{'email'}, auth_method => $param->{'auth_method'}, scenario_context => { sender => $param->{'user'}{'email'}, remote_host => $param->{'remote_host'}, remote_addr => $param->{'remote_addr'} } ); $param->{'may_d_read'} = $access{'may'}{'read'}; # Check the status (exists, deleted, doesn't exist) of the shared # directory. $param->{'shared'} = $shared_doc->{status}; } ## Check if the current user can create a list. my $result = Sympa::Scenario->new($robot, 'create_list')->authz( $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'}); } # Check if the current user can create automatic list. $param->{'may_create_automatic_list'} = {}; my $automatic_list_families = Conf::get_robot_conf($robot, 'automatic_list_families'); foreach my $key (keys %{$automatic_list_families || {}}) { my $family = Sympa::Family->new($key, $robot); next unless $family; my $result = Sympa::Scenario->new($family->{'domain'}, 'automatic_list_creation')->authz( $param->{'auth_method'}, { 'sender' => $param->{'user'}{'email'}, 'message' => undef, 'family' => $family, 'automatic_listname' => '', } ); my $r_action = $result->{'action'} if ref $result eq 'HASH'; $param->{'may_create_automatic_list'}{$key} = 1 if $r_action and $r_action =~ /do_it/; } # Compat. <= 6.2.22. $param->{'session'}{'is_family_owner'} = $param->{'may_create_automatic_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); 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)) { my @users = grep { $_->{role} eq $role } @{$list->get_current_admins || []}; foreach my $u (@users) { 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->new($list, 'send')->authz( $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; } $param->{'may_include'} = { member => ( $param->{'is_owner'} and $list->has_data_sources('member') ), owner => ( $param->{'is_privileged_owner'} and $list->has_data_sources('owner') ), editor => ( $param->{'is_privileged_owner'} and $list->has_data_sources('editor') ), }; # Compat.<=6.2.54 $param->{'may_sync'} = $param->{'may_include'}{'member'}; } ## Should Not be used anymore ## $param->{'may_subunsub'} = 1 if ($param->{'may_signoff'} || $param->{'may_subscribe'}); ## May review my $result = Sympa::Scenario->new($list, 'review')->authz( $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->new($list, 'unsubscribe')->authz( $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->new($list, 'subscribe')->authz( $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->new($list, 'add')->authz( $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->new($list, 'del')->authz( $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->is_archiving_enabled) { $param->{'is_archived'} = 1; ## Check if the current user may access web archives my $result = Sympa::Scenario->new($list, 'archive_web_access')->authz( $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->new($list, 'archive_web_access') ->authz($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; } } if (Conf::get_robot_conf($robot, 'shared_feature') eq 'on') { $param->{'is_shared_allowed'} = 1; # Shared documents access control. my $shared_doc = Sympa::WWW::SharedDocument->new($list); if ($shared_doc and $shared_doc->{status} eq 'exist') { # Check if shared is publically accessible (useful information # for RSS). my %access = $shared_doc->get_privileges( mode => 'read', sender => undef, auth_method => $param->{'auth_method'}, scenario_context => {sender => 'nobody'} ); $param->{'shared_public_access'} = $access{'may'}{'read'}; } } # 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; } ); } sub do_confirm_action { $param->{confirm_action} = $session->{confirm_action}; return 1; } ## 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'}; # Save and update last login info. $session->{'last_login_host'} = $param->{'user'}{'last_login_host'}; $session->{'last_login_date'} = $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 $email = Sympa::Tools::Text::canonic_email($in{'email'}); my $passwd = delete $in{'passwd'}; # Clear it. my $previous_action = $in{'previous_action'} if $in{'previous_action'} and $in{'previous_action'} =~ /\A\w+\z/; my $listname_re = Sympa::Regexps::listname(); #FIXME:Check required? my $previous_list = $in{'previous_list'} if $in{'previous_list'} and $in{'previous_list'} =~ /\A$listname_re\z/; my $only_passwd = $in{'only_passwd'}; $only_passwd ||= $in{'login_method'}; # Compat. <= 6.2.36 my $success_referer = _clean_referer($in{'referer'}); my $failure_referer = _clean_referer($in{'failure_referer'}); my $ldap_auth_info = is_ldap_user($email); if ($param->{'user'}{'email'}) { Sympa::WWW::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' } ); return _do_login_exit($success_referer, $previous_action, $previous_list); } $param->{'email'} = $email; $param->{'previous_action'} = $previous_action; $param->{'previous_list'} = $previous_list; $param->{'only_passwd'} = $only_passwd; $param->{'referer'} = $success_referer; $param->{'failure_referer'} = $failure_referer; $param->{'is_ldap_user'} = ($ldap_auth_info ? 1 : 0); $param->{'unauthenticated_email'} = $email; # Compat. <= 6.2.36 $param->{'init_email'} = $email; # Compat. <= 6.2.36 # Show form if not yet submitted. return 1 unless delete $in{'submit'}; # Clear it. # Show form if HTTP POST method not used. return 1 unless $ENV{'REQUEST_METHOD'} eq 'POST'; unless ($email) { Sympa::WWW::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 1; } unless ($passwd) { Sympa::WWW::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" } ); return 1; } my $data; unless ($data = Sympa::WWW::Auth::check_auth($robot, $email, $passwd)) { $log->syslog('notice', 'Authentication failed'); web_db_log( { 'parameters' => $in{'email'}, 'target_email' => $in{'email'}, 'status' => 'error', 'error_type' => 'authentication' } ); my $u = Sympa::User::get_global_user($email); if ( $u and $u->{'wrong_login_count'} and $u->{'wrong_login_count'} > Conf::get_robot_conf($robot, 'max_wrong_password')) { $param->{'login_error'} = 'password_reset'; return _do_login_exit($failure_referer || $ldap_auth_info, 'renewpasswd'); } else { #$param->{'login_error'} = 'wrong_password'; return _do_login_exit($failure_referer, 1); } } $param->{'user'} = $data->{'user'}; $session->{'auth'} = $data->{'auth'}; $session->{'email'} = $email = Sympa::Tools::Text::canonic_email($param->{'user'}{'email'}); # Save and update information of last login. $session->{'last_login_host'} = $param->{'user'}{'last_login_host'}; $session->{'last_login_date'} = $param->{'user'}{'last_login_date'}; Sympa::User::update_global_user( $param->{'user'}{'email'}, { last_login_date => time(), last_login_host => $ip, wrong_login_count => 0 } ); 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($param->{'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 => $param->{'user'}{'prefs'}}); } if ($session->{'shared_mode'}) { # user did choose a shared expert/standard mode Sympa::User::update_global_user($param->{'user'}{'email'}, {data => $param->{'user'}{'prefs'}}); } web_db_log( { 'parameters' => $in{'email'}, 'target_email' => $in{'email'}, 'status' => 'success' } ); web_db_stat_log(); return _do_login_exit($success_referer, $previous_action, $previous_list); } sub _do_login_exit { my $referer = shift; my $action = shift; my $listname = shift; if ($param->{'nomenu'}) { $param->{'back_to_mom'} = 1; return 1; } elsif ($referer and $referer =~ m{\Ahttps?://}i) { $param->{'redirect_to'} = $referer; return 1; } elsif ($action and not $temporary_actions{$action} and not($action eq 'referer')) { # Compat. <= 6.2.36 $in{'list'} = $listname; return $action; } else { $param->{'redirect_to'} = $session->{'redirect_url'} || Sympa::get_url($robot, undef, authority => 'local'); return 1; } } sub _clean_referer { my $referer = shift; return undef unless $referer and $referer =~ m{\Ahttps?://}i; # Allow referer within scope of cookie domain. my $host = lc(URI->new($referer)->host); my $mydom = lc($cookie_domain || 'localhost'); if ($mydom eq 'localhost') { my $myhost = Sympa::WWW::Tools::get_http_host() || ''; $myhost =~ s/:\d+\z//; return undef unless $host eq $myhost; } else { $mydom =~ s/\A(?![.])/./; return undef unless substr($host, -length $mydom) eq $mydom or ".$host" eq $mydom; } return $referer; } ## 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 their email address with the challenge sub do_sso_login { wwslog('info', '(%s)', $in{'auth_service_name'}); # When user require CAS login, reset do_not_use_cas cookie. delete $session->{'do_not_use_cas'}; my $next_action; if ($param->{'user'}{'email'}) { wwslog( 'info', 'User %s already logged in. Session reset', $param->{'user'}{'email'} ); delete $param->{'user'}; $session->{'email'} = 'nobody'; delete $session->{'cas_server'}; delete $session->{'sso_id'}; } ## 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'; #FIXME $param->{'redirect_to'} = $redirect_url; _redirect($redirect_url); } } 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'; #FIXME $param->{'redirect_to'} = $service; _redirect($service); 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::WWW::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::WWW::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::WWW::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'}; Sympa::WWW::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::WWW::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::WWW::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::WWW::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::WWW::Auth::get_email_by_net_id($robot, $sso_id, \%ENV); } } unless ($email) { Sympa::WWW::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_attrs; if (my $list_of_headers = $Conf::Conf{'auth_services'}{$robot}[$sso_id]{'http_header_list'}) { $sso_attrs = { map { ($_ => $ENV{$_}) } grep { defined $ENV{$_} } split(/\s*,\s*/, $list_of_headers) }; } elsif (my $prefix = $Conf::Conf{'auth_services'}{$robot}[$sso_id] {'http_header_prefix'}) { $sso_attrs = { map { ($_ => $ENV{$_}) } grep {/^($prefix)/} keys %ENV }; } else { $sso_attrs = {}; } ## Create user entry if required unless (Sympa::User::is_global_user($email)) { unless (Sympa::User::add_global_user({'email' => $email})) { Sympa::WWW::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 => $sso_attrs} ) ) { Sympa::WWW::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::WWW::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'}; _redirect( $session->{'redirect_url'} || Sympa::get_url( $robot, undef, nomenu => $param->{'nomenu'}, authority => 'local' ) ); return 1; } else { ## Unknown SSO service Sympa::WWW::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::WWW::Report::notice_report_web('you_have_been_authenticated', {}, $param->{'action'}); web_db_log( { 'parameters' => $in{'auth_service_name'}, 'status' => 'success' } ); } else { Sympa::WWW::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 { _redirect( $session->{'redirect_url'} || Sympa::get_url( $robot, undef, nomenu => $param->{'nomenu'}, authority => 'local' ) ); return 1; } } 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 $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 # No longer used. #sub do_loginrequest; ## Help / about WWSympa sub do_help { wwslog('info', '(%s)', $in{'help_topic'}); # Strip extensions. $in{'help_topic'} =~ s/[.].*// if $in{'help_topic'}; # Given partial top URI, redirect to base. unless ($in{'help_topic'} or ($ENV{PATH_INFO} // '') =~ m{/\z}) { $param->{'redirect_to'} = Sympa::get_url( $robot, 'help', nomenu => $param->{'nomenu'}, paths => [''], # Ends with '/'. authority => 'local' ); return 1; } $param->{'help_topic'} = $in{'help_topic'} if $in{'help_topic'}; return 1; } #FIXME: Would be obsoleted. Used internally only. sub do_redirect { _redirect($param->{'redirect_to'}); return 1; } # update session cookie and redirect the client to redirect_to parameter or # glob var; sub _redirect { my $redirect_to = shift; $session->set_cookie($cookie_domain, 'session', $param->{'use_ssl'}); print "Status: 302 Moved\n"; 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'; if (length($session->{'cas_server'} // '') and $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'}; delete $session->{'cas_server'}; $param->{'redirect_to'} = $cas_server->getServerLogoutURL(Sympa::get_url($robot)); return 1; } elsif (defined $session->{'sso_id'}) { # This user was logged using a generic_sso. my $sso = Conf::get_sso_by_id( robot => $robot, service_id => $session->{'sso_id'} ); unless ($sso) { wwslog('err', 'Unknown SSO service_id "%s"', $session->{'sso_id'}); return undef; } delete $session->{'sso_id'}; if ($sso->{logout_url}) { $param->{'redirect_to'} = $sso->{logout_url}; return 1; } } Sympa::WWW::Report::notice_report_web('logout', {}, $param->{'action'}); wwslog('info', 'Logout performed'); web_db_log( { 'parameters' => $param->{'user'}{'email'}, 'target_email' => $in{'email'}, 'status' => 'success' } ); web_db_stat_log(); return Conf::get_robot_conf($robot, 'default_home'); } sub sendssopasswd { my $email = shift; $log->syslog('info', '(%s)', $email); my ($passwd, $user); unless ($email) { Sympa::WWW::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::WWW::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; $param->{'newuser'} = Sympa::User::get_global_user($email) || {'email' => $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->{'reason'} = '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::WWW::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', } ); 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 $email = $in{'email'}; my $reason = $in{'reason'}; $param->{'account_creation'} = 1; $param->{'email'} = $email; $param->{'reason'} = $reason; # Action confirmed? my $next_action = $session->confirm_action( $in{'action'}, $in{'response_action'}, arg => join(',', $email, $reason), previous_action => ( $in{'previous_action'} || ($reason ? 'firstpasswd' : 'renewpasswd') ) ); return $next_action unless $next_action eq '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::WWW::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::WWW::Auth::may_use_sympa_native_auth($robot, $in{'email'})) { ## TODO: Error handling Sympa::WWW::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'})}; } 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::WWW::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) # DEPRECATED: No longer used. #sub do_which { ## The list of list sub do_lists { my @lists; wwslog('info', '(%s, %s)', $in{'topic'}, $in{'subtopic'}); # Get member/owner/editor data used to avoid lookups in the loop my $which = {member => {}, owner => {}, editor => {}}; if ($param->{'user'}{'email'}) { foreach my $role ('member', 'owner', 'editor') { foreach my $list ( Sympa::List::get_which( $param->{'user'}{'email'}, $robot, $role ) ) { $which->{$role}->{$list->{'name'}} = $list; } } } my $all_lists = []; if ($in{'topic'} and $in{'topic'} eq '@which') { my %lists = (); foreach my $role ('member', 'owner', 'editor') { foreach my $list (values %{$which->{$role}}) { $lists{$list->{'name'}} = $list; } } $all_lists = [map { $lists{$_} } sort keys %lists]; $param->{'subtitle'} = $language->gettext('Your lists'); } elsif ($in{'topic'}) { my $topic = join '/', grep {$_} ($in{'topic'}, $in{'subtopic'}); $param->{'topic'} = $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 ($topic) { $options->{'filter'} = ['topics' => $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 $listname = $list->{'name'}; my $result = Sympa::Scenario->new($list, 'visibility', dont_reload_scenario => 1)->authz( $param->{'auth_method'}, { 'sender' => $sender, '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'); my $list_info = {}; $list_info->{'subject'} = $list->{'admin'}{'subject'}; $list_info->{'date_epoch'} = $list->{'admin'}{'creation'}{'date_epoch'}; $list_info->{'topics'} = $list->{'admin'}{'topics'}; #Compat.<6.2.32 $list_info->{'host'} = $list->{'domain'}; if ($param->{'user'}{'email'}) { if ($which->{owner}->{$listname}) { if ($list->is_admin( 'privileged_owner', $param->{'user'}{'email'} ) ) { $list_info->{is_privileged_owner} = 1; } if (not $which->{editor}->{$listname} and $list->is_admin( 'actual_editor', $param->{'user'}{'email'} ) ) { $list_info->{is_editor} = 1; } $list_info->{is_owner} = 1; # Compat. < 6.2b.2. $list_info->{'admin'} = 1; } if ($which->{editor}->{$listname}) { $list_info->{is_editor} = 1; # Compat. < 6.2b.2. $list_info->{'admin'} = 1; } if ($which->{member}->{$listname}) { $list_info->{'is_subscriber'} = 1; } } $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::WWW::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::WWW::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 () { 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->new($l, 'visibility', dont_reload_scenario => 1)->authz( $param->{'auth_method'}, { 'sender' => $param->{'user'}{'email'}, 'remote_host' => $param->{'remote_host'}, 'remote_addr' => $param->{'remote_addr'}, } ); my $action = $result->{'action'} if ref $result eq 'HASH'; next unless $action; $which{$l->get_id} = { name => $l->{'name'}, domain => $l->{'domain'}, host => $l->{'domain'}, # Compat.<6.2.32 robot => $l->{'domain'}, # Compat. 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'}; # To prevent sniffing lists, we behave the same as when list was # unknown. return Conf::get_robot_conf($robot, 'default_home'); } ## Get List Description if (-r $list->{'dir'} . '/homepage') { my $file_path = $list->{'dir'} . '/homepage'; $param->{'homepage_content'} = Sympa::Tools::Text::slurp($file_path); unless (defined $param->{'homepage_content'}) { wwslog('err', 'Failed to open file %s: %m', $file_path); Sympa::WWW::Report::reject_report_web('intern', 'cannot_open_file', {'file' => $file_path}, $param->{'action'}, $list, $param->{'user'}{'email'}, $robot); web_db_log( { 'parameters' => $file_path, 'status' => 'error', 'error_type' => 'internal' } ); return undef; } ## Used by previous templates $param->{'homepage'} = 1; } elsif (-r $list->{'dir'} . '/info') { my $file_path = $list->{'dir'} . '/info'; $param->{'info_content'} = Sympa::Tools::Text::slurp($file_path); unless (defined $param->{'info_content'}) { wwslog('err', 'Failed to open file %s: %m', $file_path); Sympa::WWW::Report::reject_report_web('intern', 'cannot_open_file', {'file' => $file_path}, $param->{'action'}, $list, $param->{'user'}{'email'}, $robot); web_db_log( { 'parameters' => $file_path, 'status' => 'error', 'error_type' => 'internal' } ); return undef; } #FIXME: needed? $param->{'info_content'} =~ s/\n/\/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'}); $param->{'page'} = $in{'page'} || 1; if ($param->{'page'} eq 'owner') { return _review_user('owner'); } elsif ($in{'page'} eq 'editor') { return _review_user('editor'); } else { return _review_member(); } } # List of owners / editors sub _review_user { wwslog('info', '(%s)', @_); my $role = shift; # Access control return undef unless Sympa::is_listmaster($list, $param->{'user'}{'email'}) or $list->is_admin('owner', $param->{'user'}{'email'}); my $new_admin = _deserialize_changes(); if ($in{'submit'} and $new_admin and %$new_admin) { delete $in{'submit'}; my $users = [grep { $_->{role} eq $role } @{$list->get_current_admins || []}]; my @deleted_emails = map { $in{$_} } grep {/\Adeleted_param[.]$role[.]\d+\z/} keys %in; my $update_admin = { $role => [ map { my $email = $_->{email}; (grep { $email eq $_ } @deleted_emails) ? {email => undef} : $_; } ( @$users, grep { $_ and $_->{email} } @{$new_admin->{$role} || []} ) ] }; my $config = Sympa::List::Users->new($list, config => {$role => $users}); my $errors = []; my $validity = $config->submit($update_admin, $param->{'user'}{'email'}, $errors); unless (defined $validity) { if (my @intern = grep { $_->[0] eq 'intern' } @$errors) { foreach my $err (@intern) { Sympa::WWW::Report::reject_report_web($err->[0], $err->[1], {}, $param->{'action'}, $list); wwslog('err', 'Internal error %s', $err->[1]); } } else { Sympa::WWW::Report::reject_report_web('intern', 'unknown', {}, $param->{'action'}, $list); wwslog('err', 'Unknown error'); } web_db_log( { 'status' => 'error', 'error_type' => 'internal' } ); return undef; } my $error_return = 0; foreach my $err (grep { $_->[0] eq 'user' } @$errors) { $error_return = 1 unless $err->[1] eq 'mandatory_parameter'; Sympa::WWW::Report::reject_report_web( $err->[0], $err->[1], { 'p_name' => $language->gettext($err->[2]->{p_info}->{gettext_id}), %{$err->[2]} }, $param->{'action'}, $list ); wwslog( 'err', 'Error on parameter %s: %s', join('.', @{$err->[2]->{p_paths}}), $err->[1] ); web_db_log( { 'status' => 'error', 'error_type' => 'syntax_errors' } ); } if ($error_return) { ; } elsif ($validity eq '') { Sympa::WWW::Report::notice_report_web('no_parameter_edited', {}, $param->{'action'}); wwslog('info', 'No parameter was edited by user'); } else { # Validation of the form finished. Start of valid data treatments. # FIXME: Use commit(). # Delete/add users. my @del_users = map { my $email; if ($_ =~ /\Adeleted_param[.]$role[.]\d+\z/) { $email = Sympa::Tools::Text::canonic_email($in{$_}); $email ? ($email) : (); } else { (); } } keys %in; my $new_users = [grep { $_ and $_->{email} } @{($new_admin || {})->{$role} || []}]; foreach my $email (@del_users) { next if grep { $email eq $_->{email} } @$new_users; $list->delete_list_admin($role, $email); } foreach my $user (@{(ref $new_users eq 'ARRAY') ? $new_users : []}) { my $email = $user->{email}; if (grep { $email eq $_ } @del_users) { ; #FIXME: Update user? } elsif ($list->add_list_admin($role, $user)) { # Notify the new list owner/editor Sympa::send_notify_to_user( $list, 'added_as_listadmin', $email, { admin_type => $role, delegator => $param->{'user'}{'email'} } ); Sympa::WWW::Report::notice_report_web('user_notified', {'notified_user' => $email}, $param->{'action'}); } else { #FIXME: Report error } } if ($list->get_family and (@del_users or @{$new_users || []})) { $list->update_config_changes('param', $role); } } } my $users = [grep { $_->{role} eq $role } @{$list->get_current_admins || []}]; my $config = Sympa::List::Users->new($list, config => {$role => $users}); my $schema = $config->get_schema($param->{'user'}{'email'}); my @schema = _do_edit_list_request($config, $schema->{$role}, [$role]); # If at least one param was editable, make the update button appear in # the form. $param->{'is_form_editable'} = grep { $_->{privilege} eq 'write' } @schema; $param->{'config_schema'} = [@schema]; $param->{'config_values'} = { map { my @value = $config->get($_->{name}); @value ? ($_->{name} => $value[0]) : (); } @schema }; return 1; } sub _review_member { my $record; my @users; my $size; my $sortby = lc($in{'sortby'} || 'email'); ## 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 => $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::WWW::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 # 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); 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; } sub do_edit { wwslog('info', '(%s, %s)', $in{'role'}, $in{'email'}); my $role = $in{'role'}; my $email = $in{'email'}; $param->{'role'} = $role; $param->{'page'} = $role; # For review action my $users = [grep { $_ and $_->{email} eq $email and $_->{role} eq $role } @{$list->get_current_admins || []}]; #FIXME return 1 unless @$users; my $config = Sympa::List::Users->new($list, config => {$role => $users}); my $schema = $config->get_schema($param->{'user'}{'email'}); my @schema = _do_edit_list_request($config, $schema->{$role}, [$role]); # Initial access. show current value. my $new_admin = _deserialize_changes(); if ($in{'submit'} and $new_admin and %$new_admin) { delete $in{'submit'}; #FIXME return 1 unless $new_admin->{$role} and $new_admin->{$role}->[0]; # Prevent changing email. $new_admin->{$role}->[0]->{email} = $email; # Start parsing the data sent by the edition form. my $errors = []; my $validity = $config->submit( $new_admin, $param->{'user'}{'email'}, $errors, no_global_validations => 1 ); unless (defined $validity) { if (my @intern = grep { $_->[0] eq 'intern' } @$errors) { foreach my $err (@intern) { Sympa::WWW::Report::reject_report_web($err->[0], $err->[1], {}, $param->{'action'}, $list); wwslog('err', 'Internal error %s', $err->[1]); } } else { Sympa::WWW::Report::reject_report_web('intern', 'unknown', {}, $param->{'action'}, $list); wwslog('err', 'Unknown error'); } web_db_log( { 'status' => 'error', 'error_type' => 'internal' } ); return undef; } my $error_return = 0; foreach my $err (grep { $_->[0] eq 'user' } @$errors) { $error_return = 1 unless $err->[1] eq 'mandatory_parameter'; Sympa::WWW::Report::reject_report_web( $err->[0], $err->[1], { 'p_name' => $language->gettext($err->[2]->{p_info}->{gettext_id}), %{$err->[2]} }, $param->{'action'}, $list ); wwslog( 'err', 'Error on parameter %s: %s', join('.', @{$err->[2]->{p_paths}}), $err->[1] ); web_db_log( { 'status' => 'error', 'error_type' => 'syntax_errors' } ); } if ($error_return) { ; } elsif ($validity eq '') { Sympa::WWW::Report::notice_report_web('no_parameter_edited', {}, $param->{'action'}); wwslog('info', 'No parameter was edited by user'); } else { # Validation of the form finished. Start of valid data # treatments. # FIXME: Use commit(). $list->update_list_admin($email, $role, $new_admin->{$role}->[0]); # Keep track of changes for family. if ($list->get_family) { $list->update_config_changes('param', $role); } } $in{'page'} = $role; # For review. return $in{'previous_action'} || 'review'; } # If at least one param was editable, make the update button appear in # the form. $param->{'is_form_editable'} = grep { $_->{privilege} eq 'write' } @schema; $param->{'config_schema'} = [@schema]; $param->{'config_values'} = {$role => $users} if $users and @$users; $param->{'previous_action'} = $in{'previous_action'} || 'review'; return 1; } ## Show the table of exclude sub do_show_exclude { wwslog('info', ''); return undef unless $param->{'user'}{'email'}; # 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 %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. # GH #341: Keep search key in session store. $param->{'filter'} = $in{'filter'} || $session->{'search__filter'}; my $searchkey = Sympa::Tools::Text::foldcase($param->{'filter'}) if defined $param->{'filter'} and length $param->{'filter'}; $session->{'search__filter'} = $param->{'filter'}; return 1 unless defined $searchkey; 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); $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::WWW::Tools::cookie_period) { my $entry = {'value' => $p}; ## Set description from NLS $entry->{'desc'} = $language->gettext( $Sympa::WWW::Tools::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::WWW::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::WWW::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' } ); } $in{'previous_action'} = 'choosepasswd'; delete $in{'submit'}; # Clear it. 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::WWW::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 = $in{'email'}; } else { $email = $param->{'user'}{'email'}; } unless ($list->is_list_member($email)) { Sympa::WWW::Report::reject_report_web('user', 'not_subscriber', {email => $email, listname => $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::WWW::Report::reject_report_web( 'user', 'not_available_reception_mode', { reception_modes => [$list->available_reception_mode], recpetion_mode => $reception, listname => $list->{'name'}, }, $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::WWW::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::WWW::Report::reject_report_web('user', 'already_subscriber', {email => $in{'new_email'}, listname => $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::WWW::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::WWW::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::WWW::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::WWW::Report::reject_report_web('user', 'syntax_errors', {p_name => $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::WWW::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::WWW::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 # No longer used. #sub do_viewfile; # Subscribes a user to the list # IN : email, gecos, custom_attribute. # OUT :'subscribe' | 'info' | $in{'previous_action'} | undef sub do_subscribe { wwslog('info', '(%s)', $in{'email'}); my $scenario = Sympa::Scenario->new($list, 'subscribe') or return undef; return $in{'previous_action'} || 'info' if $scenario->is_purely_closed; if ( $param->{'user'}{'email'} and $list->is_list_member($param->{'user'}{'email'})) { # Already subscribed and logged in. return 1; } my ($sender, $email, $gecos); if ($param->{'user'} and $param->{'user'}{'email'}) { $sender = $param->{'user'}{'email'}; $email = $param->{'user'}{'email'}; $gecos = $in{'gecos'} || $param->{'user'}{'gecos'}; } else { # User is not autenticated. $sender = 'nobody'; $email = Sympa::Tools::Text::canonic_email($in{'email'}); $gecos = $in{'gecos'}; } @{$param}{qw(email gecos custom_attribute)} = ($email, $gecos, $in{'custom_attribute'}); # Initial access. show empty form. unless ($in{'email'}) { return 1; } if ($list->{'admin'}{'custom_attribute'} and not _check_custom_attribute( $list, $param->{'action'}, $in{'custom_attribute'} ) ) { wwslog('notice', "Missing required custom attributes"); return 1; } unless ($email and Sympa::Tools::Text::valid_email($email)) { return 1; } # Action confirmed? my $next_action = $session->confirm_action( $in{'action'}, $in{'response_action'}, arg => join(',', grep {$_} ($email, $gecos)), previous_action => ($in{'previous_action'} || 'info') ); return $next_action unless $next_action eq '1'; my $spindle = Sympa::Spindle::ProcessRequest->new( context => $list, action => 'subscribe', sender => $sender, email => $email, gecos => $gecos, ( $in{'custom_attribute'} ? (custom_attribute => $in{'custom_attribute'}) : () ), ( $param->{'user'}{'email'} ? (md5_check => 1) : () ), scenario_context => { sender => $sender, remote_host => $param->{'remote_host'}, remote_addr => $param->{'remote_addr'}, }, ); unless ($spindle and $spindle->spin) { wwslog('err', 'Failed to add user'); return undef; } foreach my $report (@{$spindle->{stash} || []}) { if ($report->[1] eq 'notice') { Sympa::WWW::Report::notice_report_web(@{$report}[2, 3], $param->{'action'}); } else { Sympa::WWW::Report::reject_report_web(@{$report}[1 .. 3], $param->{action}); } } unless (@{$spindle->{stash} || []}) { Sympa::WWW::Report::notice_report_web('performed', {}, $param->{'action'}); web_db_log({'parameters' => $in{'email'}, 'status' => 'success'}); } return ($in{'previous_action'} || 'info'); } # No longer used. #sub do_multiple_subscribe; sub do_suboptions { wwslog('info', ''); my ($s, $m); unless ($s = $param->{'subscriber'}) { Sympa::WWW::Report::reject_report_web( 'user', 'not_subscriber', {email => $param->{'user'}{'email'}, listname => $list->{'name'}}, $param->{'action'}, $list, $param->{'user'}{'email'}, $robot ); wwslog('info', 'Subscriber %s not found', $param->{'user'}{'email'}); return $in{'previous_action'} || 'info'; } foreach $m ($list->available_reception_mode) { 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)) { if ($s->{'visibility'} eq $m) { $param->{'visibility'}{$m}{'selected'} = ' selected'; } else { $param->{'visibility'}{$m}{'selected'} = ''; } } #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; } #OBSOLETED. Now 'subrequest' is an alias of 'subscribe'. #sub do_subrequest; # Unsubcribes a user from a list, without authentication. # This function will be used for unsubscription link in such as the message # footer. sub do_auto_signoff { wwslog('info', '(%s)', $in{'email'}); # 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. my $default_home = Conf::get_robot_conf($robot, 'default_home'); my $scenario = Sympa::Scenario->new($list, 'unsubscribe') or return undef; return $default_home if $scenario->is_purely_closed; my $email = Sympa::Tools::Text::canonic_email($in{'email'}); return $default_home unless $email and Sympa::Tools::Text::valid_email($email); $param->{'email'} = $email; # Action confirmed? my $next_action = $session->confirm_action( $in{'action'}, $in{'response_action'}, arg => $email, previous_action => $default_home ); return $next_action unless $next_action eq '1'; my $spindle = Sympa::Spindle::ProcessRequest->new( context => $list, action => 'signoff', sender => 'nobody', email => $email, scenario_context => { sender => 'nobody', remote_host => $param->{'remote_host'}, remote_addr => $param->{'remote_addr'}, }, ); unless ($spindle and $spindle->spin) { wwslog('err', 'Failed to delete user'); return undef; } foreach my $report (@{$spindle->{stash} || []}) { if ($report->[1] eq 'notice') { Sympa::WWW::Report::notice_report_web(@{$report}[2, 3], $param->{'action'}); } else { Sympa::WWW::Report::reject_report_web(@{$report}[1 .. 3], $param->{action}); } } unless (@{$spindle->{stash} || []}) { Sympa::WWW::Report::notice_report_web('performed', {}, $param->{'action'}); web_db_log({'parameters' => $in{'email'}, 'status' => 'success'}); } return $default_home; } # Became an alias of do_family_signoff(). #sub do_family_signoff_request { sub do_family_signoff { wwslog('info', '(%s, %s)', $in{'family'}, $in{'email'}); # 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. my $default_home = Conf::get_robot_conf($robot, 'default_home'); my $scenario = Sympa::Scenario->new($robot, 'family_signoff') or return undef; return $default_home if $scenario->is_purely_closed; return $default_home unless $in{'email'} and $in{'family'}; #FIXME my $family = Sympa::Family->new($in{'family'}, $robot); return $default_home unless $family; my $email = Sympa::Tools::Text::canonic_email($in{'email'}); return $default_home unless $email and Sympa::Tools::Text::valid_email($email); $param->{'email'} = $email; $param->{'family'} = $family->{name}; # Action confirmed? my $next_action = $session->confirm_action( $in{'action'}, $in{'response_action'}, arg => $email, previous_action => $default_home ); return $next_action unless $next_action eq '1'; my $spindle = Sympa::Spindle::ProcessRequest->new( context => $family, action => 'family_signoff', sender => 'nobody', email => $email, scenario_context => { sender => 'nobody', remote_host => $param->{'remote_host'}, remote_addr => $param->{'remote_addr'}, }, ); unless ($spindle and $spindle->spin) { wwslog('err', 'Failed to delete user'); return undef; } foreach my $report (@{$spindle->{stash} || []}) { if ($report->[1] eq 'notice') { Sympa::WWW::Report::notice_report_web(@{$report}[2, 3], $param->{'action'}); } else { Sympa::WWW::Report::reject_report_web(@{$report}[1 .. 3], $param->{action}); } } unless (@{$spindle->{stash} || []}) { Sympa::WWW::Report::notice_report_web('performed_soon', {}, $param->{'action'}); web_db_log({'parameters' => $in{'email'}, 'status' => 'success'}); } return $default_home; } # Unsubcribes a user from a list # IN : email # OUT : 'signoff' | 'info' | undef sub do_signoff { wwslog('info', '(%s)', $in{'email'}); my $scenario = Sympa::Scenario->new($list, 'unsubscribe') or return undef; return $in{'previous_action'} || 'info' if $scenario->is_purely_closed; if ($param->{'user'}{'email'} and not $list->is_list_member($param->{'user'}{'email'})) { # Not yet subscribed and already logged in. return 1; } my ($sender, $email); if ($param->{'user'} and $param->{'user'}{'email'}) { $sender = $param->{'user'}{'email'}; $email = $param->{'user'}{'email'}; } else { # User is not autenticated. $sender = 'nobody'; $email = Sympa::Tools::Text::canonic_email($in{'email'}); } $param->{email} = $email; unless ($email and Sympa::Tools::Text::valid_email($email)) { return 1; } # Action confirmed? my $next_action = $session->confirm_action( $in{'action'}, $in{'response_action'}, arg => $email, previous_action => ($in{'previous_action'} || 'info') ); return $next_action unless $next_action eq '1'; my $spindle = Sympa::Spindle::ProcessRequest->new( context => $list, action => 'signoff', sender => $sender, email => $email, ( $param->{'user'}{'email'} ? (md5_check => 1) : () ), scenario_context => { sender => $sender, remote_host => $param->{'remote_host'}, remote_addr => $param->{'remote_addr'}, }, ); unless ($spindle and $spindle->spin) { wwslog('err', 'Failed to delete user'); return undef; } foreach my $report (@{$spindle->{stash} || []}) { if ($report->[1] eq 'notice') { Sympa::WWW::Report::notice_report_web(@{$report}[2, 3], $param->{'action'}); } else { Sympa::WWW::Report::reject_report_web(@{$report}[1 .. 3], $param->{action}); } } unless (@{$spindle->{stash} || []}) { Sympa::WWW::Report::notice_report_web('performed', {}, $param->{'action'}); web_db_log({'parameters' => $in{'email'}, 'status' => 'success'}); } return ($in{'previous_action'} || 'info'); } # No longer used. #sub unsubscribe; #OBSOLETED: Now an alias of 'signoff'. #sub do_sigrequest; ## Update of password sub do_setpasswd { wwslog('info', ''); my $user; if ($in{'newpasswd1'} =~ /^\s+$/) { Sympa::WWW::Report::reject_report_web('user', 'no_passwd', {}, $param->{'action'}); wwslog('info', 'No newpasswd1'); web_db_log( { 'status' => 'error', 'error_type' => 'missing_parameter' } ); if ($in{'previous_action'}) { $in{'list'} = $in{'previous_list'}; return $in{'previous_action'}; } else { return 'pref'; } } unless ($in{'newpasswd1'} eq $in{'newpasswd2'}) { Sympa::WWW::Report::reject_report_web('user', 'diff_passwd', {}, $param->{'action'}); wwslog('info', 'Different newpasswds'); web_db_log( { 'status' => 'error', 'error_type' => 'bad_parameter' } ); if ($in{'previous_action'}) { $in{'list'} = $in{'previous_list'}; return $in{'previous_action'}; } else { return 'pref'; } } if (my $reason = Sympa::Tools::Password::password_validation($in{'newpasswd1'})) { Sympa::WWW::Report::reject_report_web('user', 'passwd_validation', {'reason' => $reason}, $param->{'action'}); wwslog('info', 'Password validation'); web_db_log({'status' => 'error', 'error_type' => 'bad_parameter'}); if ($in{'previous_action'}) { $in{'list'} = $in{'previous_list'}; return $in{'previous_action'}; } else { return 'pref'; } } 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::WWW::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::WWW::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::WWW::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', ''); 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_header', 'message_footer', 'remind.tt2', 'invite.tt2', 'reject.tt2', 'your_infected_msg.tt2' ) { if ($Sympa::WWW::Tools::filenames{$f}{'gettext_id'}) { $param->{'lists_default_files'}{$f}{'complete'} = $language->gettext( $Sympa::WWW::Tools::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::WWW::Tools::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 = [ map { Sympa::Tools::Data::dup_var($_) } grep { not $_->{obsolete} } @Sympa::ConfDef::params ]; get_server_details(); unless ($param->{'main_robot'}) { Sympa::WWW::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::WWW::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::WWW::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::WWW::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::WWW::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::WWW::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::WWW::Tools::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}}) { foreach my $subdir (keys %{$param->{'templates'}{$file}{$level}}) { # Allow unknown lang. my $lang = Sympa::Language::canonic_lang($subdir); $param->{'lang_per_level'}{$level}{$subdir} = {lang => ($lang || $subdir)}; } } } ## 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', ''); if ($in{'scope'} eq 'list' and ref $list ne 'Sympa::List') { Sympa::WWW::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; } $param->{'webormail'} = $in{'webormail'}; $param->{'scope'} = $in{'scope'}; $param->{'template_name'} = $in{'template_name'}; $param->{'tpl_lang'} = $in{'tpl_lang'}; # Action confirmed? my $next_action = $session->confirm_action( $in{'action'}, $in{'response_action'}, arg => join('/', @in{qw(webormail scope template_name tpl_lang)}), previous_action => 'ls_templates' ); return $next_action unless $next_action eq '1'; my $template_path = Sympa::WWW::Tools::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::WWW::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::WWW::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' } ); 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::WWW::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::WWW::Tools::get_template_path( $list || $robot, $in{'webormail'}, $in{'scope'}, $in{'template_name'}, $in{'tpl_lang'} ); my $fh; unless ($template_path and open $fh, '<', $template_path) { Sympa::WWW::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'} = do { local $RS; <$fh> }; close $fh; $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') { # Allow unknown lang. $param->{'tpl_lang_lang'} = Sympa::Language::canonic_lang($tpl_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::WWW::Report::reject_report_web('user', 'unknown_list', {listname => $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::WWW::Tools::get_template_path($list_out, $in{'webormail'}, 'list', $in{'template_name_out'}, $in{'tpl_lang_out'}); } else { Sympa::WWW::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::WWW::Tools::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::WWW::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; } my $ofh; unless (open $ofh, '>', $param->{'template_path_out'}) { Sympa::WWW::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 $ofh $param->{'template_content'}; close $ofh; 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') { # Allow unknown lang. $param->{'tpl_lang_lang'} = Sympa::Language::canonic_lang($tpl_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. #FIXME: Would rename to do_rt_XXX(). sub do_manage_template { wwslog('info'); my $base = $list->{'dir'} . '/mail_tt2/'; # Build the list of available templates. my $available_files = Sympa::WWW::Tools::get_templates_list($list, 'mail', ignore_global => 1); foreach my $file (keys %$available_files) { if ($file eq 'reject.tt2') { my $absolute_file = $base . 'reject.tt2'; if (-l $absolute_file) { my $default = readlink $absolute_file; if (-f $default or -f $base . $default) { $default =~ s/\A.*reject_//; $default =~ s/[.]tt2\z//; $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/\Areject_//; $file =~ s/[.]tt2\z//; $file =~ s/_/ /g; push @{$param->{'available_files'}}, $file; } } return 1; } sub _rt_canonic_name { my $name = shift; return $name unless $name; $name =~ s/^reject_//; $name =~ s/\s/_/g; return $name; } # Old name: do_manage_template() with subaction "save". sub do_rt_update { wwslog('info', '(%s, ...)', $in{'message_template'}); my $template_name = _rt_canonic_name($in{'message_template'}); my $template_path = Sympa::WWW::Tools::get_template_path($list, 'mail', 'list', 'reject_' . $template_name . '.tt2') if $template_name; # Create the parent directory if it doesn't already exist. unless ($template_path and Sympa::Tools::File::mk_parent_dir($template_path)) { my $errno = $ERRNO; Sympa::WWW::Report::reject_report_web('intern', 'cannot_open_file', {'path' => $template_name}, $param->{'action'}, '', $param->{'user'}{'email'}, $robot); wwslog('err', 'Can\'t create parent directory for %s: %s', $template_path, $errno); web_db_log( { 'parameters' => $template_name, 'status' => 'error', 'error_type' => 'internal' } ); return undef; } # Open the template. my $ofh; unless (open $ofh, '>', $template_path) { my $errno = $ERRNO; Sympa::WWW::Report::reject_report_web('intern', 'cannot_open_file', {'path' => $template_name}, $param->{'action'}, '', $param->{'user'}{'email'}, $robot); wwslog('err', 'Can\'t open file %s: %s', $template_path, $errno); web_db_log( { 'parameters' => $template_name, 'status' => 'error', 'error_type' => 'internal' } ); return undef; } ## save template contents print $ofh $in{'content'}; close $ofh; Sympa::WWW::Report::notice_report_web('performed', {}, $in{'subaction'}); return 'manage_template'; } # Old name: do_manage_template() with subaction "create_new". sub do_rt_create { wwslog('info', '(%s)', $in{'new_template_name'}); my $new_template_name = _rt_canonic_name($in{'new_template_name'}); my $new_template_path = Sympa::WWW::Tools::get_template_path($list, 'mail', 'list', 'reject_' . $new_template_name . '.tt2') if $new_template_name; my $default_file = Sympa::search_fullpath($list, 'reject.tt2', subdir => 'mail_tt2'); unless ($new_template_path) { Sympa::WWW::Report::reject_report_web( 'user', 'missing template name', {'path' => ''}, $param->{'action'}, '', $param->{'user'}{'email'}, $robot ); return undef; } if (-f $new_template_path) { Sympa::WWW::Report::reject_report_web( 'intern', 'template already exist', {'path' => $new_template_name}, $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)) { my $errno = $ERRNO; Sympa::WWW::Report::reject_report_web('intern', 'cannot_open_file', {'path' => $new_template_name}, $param->{'action'}, '', $param->{'user'}{'email'}, $robot); wwslog('err', 'Can\'t create parent directory for %s: %s', $new_template_path, $errno); web_db_log( { 'parameters' => $new_template_name, 'status' => 'error', 'error_type' => 'internal' } ); return undef; } my $fh; unless (open $fh, '<', $default_file) { my $errno = $ERRNO; Sympa::WWW::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; } my $ofh; unless (open $ofh, '>', $new_template_path) { my $errno = $ERRNO; Sympa::WWW::Report::reject_report_web('intern', 'cannot_open_file', {'path' => $new_template_name}, $param->{'action'}, '', $param->{'user'}{'email'}, $robot); wwslog('err', 'Can\'t open file %s: %s', $new_template_path, $errno); return undef; } my $content = do { local $RS; <$fh> }; print $ofh $content; close $fh; close $ofh; #XXX$in{'subaction'} = 'modify'; $in{'message_template'} = $new_template_name; #XXXreturn 'manage_template'; return 'rt_edit'; } # Old name: do_manage_template() with subaction "modify". sub do_rt_edit { wwslog('info', '(%s, ...)', $in{'message_template'}); my $template_name = _rt_canonic_name($in{'message_template'}); my $template_path = Sympa::WWW::Tools::get_template_path($list, 'mail', 'list', 'reject_' . $template_name . '.tt2') if $template_name; my $fh; unless ($template_path and open $fh, '<', $template_path) { my $errno = $ERRNO; Sympa::WWW::Report::reject_report_web('intern', 'cannot_open_file', {'path' => $template_name}, $param->{'action'}, '', $param->{'user'}{'email'}, $robot); wwslog('err', 'Can\'t open file MODIFY %s: %s', $template_path, $errno); web_db_log( { 'parameters' => $template_name, 'status' => 'error', 'error_type' => 'internal' } ); return undef; } $param->{'content'} = do { local $RS; <$fh> }; close $fh; $param->{'message_template'} = $template_name; return 'manage_template'; } # Old name: do_manage_template() with subaction "setdefault". sub do_rt_setdefault { wwslog('info', '(%s)', $in{'new_default'}); # 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/'; my $new_default = _rt_canonic_name($in{'new_default'}); my $absolute_file = $base . 'reject_' . $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'); } return 'manage_template'; } # Old name: (part of) do_manage_template() with subaction "delete". sub do_rt_delete { wwslog('info', '(%s)', $in{'message_template'}); my $template_name = _rt_canonic_name($in{'message_template'}); my $template_path = Sympa::WWW::Tools::get_template_path($list, 'mail', 'list', 'reject_' . $template_name . '.tt2') if $template_name; $param->{'message_template'} = $template_name; # Action confirmed? my $next_action = $session->confirm_action( $in{'action'}, $in{'response_action'}, arg => $template_name, previous_action => 'manage_template' ); return $next_action unless $next_action eq '1'; unless ($template_path and unlink $template_path) { my $errno = $ERRNO; Sympa::WWW::Report::reject_report_web('intern', 'cannot_delete', {'file_del' => $template_name}, '', '', '', $robot); wwslog('err', 'Can\'t open file %s: %s', $template_path, $errno); web_db_log( { 'parameters' => $template_name, 'status' => 'error', 'error_type' => 'internal' } ); return undef; } Sympa::WWW::Report::notice_report_web('performed', {}, $in{'subaction'}); return 'manage_template'; } ## 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::WWW::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::WWW::Tools::get_template_path( $list || $robot, $in{'webormail'}, $in{'scope'}, $in{'template_name'}, $in{'tpl_lang'} ); my $ofh; unless ($param->{'template_path'} and open $ofh, '>', $param->{'template_path'}) { Sympa::WWW::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 $ofh $in{'content'}; close $ofh; $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. sub do_skinsedit { wwslog('info', '(%s)', $in{'subaction'}); my @std_color_names = map { 'color_' . $_ } (0 .. 15); my @obs_color_names = qw(dark_color light_color text_color bg_color error_color selected_color shaded_color); if ($in{'editcolors'} and $in{'subaction'}) { if ($in{'subaction'} eq 'test') { my $custom_css; foreach my $cn (@std_color_names) { $session->{$cn} = lc $in{$cn} if $in{$cn} and $in{$cn} =~ /\A#[0-9a-z]+\z/i; my $cur_color = Conf::get_robot_conf($robot, $cn); unless ($session->{$cn}) { $session->{$cn} = $cur_color; } elsif ($session->{$cn} ne $cur_color) { $custom_css = 1; } } $session->{'custom_css'} = $custom_css; } else { # 'install' or 'reset'. if ($in{'subaction'} eq 'install') { # Update config. my @keys = grep { $session->{$_} } @std_color_names; foreach my $key (@keys) { Conf::set_robot_conf($robot, $key, $session->{$key}); } # Force update CSS. Sympa::WWW::Tools::get_css_url($robot, force => 1); $param->{'css_result'} = 1; } delete @{$session}{'custom_css', @std_color_names}; delete @{$param->{'session'}}{'custom_css', @std_color_names}; } } $param->{'custom_css'} = $session->{'custom_css'}; foreach my $cn (@std_color_names) { $param->{$cn} = $session->{$cn} || Conf::get_robot_conf($robot, $cn); } # Compat. foreach my $cn (@obs_color_names) { $param->{$cn} = Conf::get_robot_conf($robot, $cn); } return 1; } # Adds multiple users to a list. sub do_import { wwslog('info', '(...)'); my $content; my $fh = $query->upload('uploaded_file'); if (defined $fh) { my $ioh = $fh->handle; $content = do { local $RS; <$ioh> }; } else { $content = $in{'dump'}; } $param->{'dump'} = $content; $param->{'quiet'} = $in{'quiet'}; return 1 unless $content and $content =~ /\S/; ## Action confirmed? #my $next_action = $session->confirm_action( # $in{'action'}, $in{'response_action'}, # arg => $in{'dump'}, # previous_action => ($in{'previous_action'} || 'reviw'), #); #return $next_action unless $next_action eq '1'; my $spindle = Sympa::Spindle::ProcessRequest->new( context => $list, action => 'import', dump => $content, sender => $param->{'user'}{'email'}, quiet => $param->{'quiet'}, md5_check => 1, scenario_context => { sender => $param->{'user'}{'email'}, remote_host => $param->{'remote_host'}, remote_addr => $param->{'remote_addr'} }, ); unless ($spindle and $spindle->spin) { return $in{'previous_action'} || 'review'; } foreach my $report (@{$spindle->{stash} || []}) { if ($report->[1] eq 'notice') { Sympa::WWW::Report::notice_report_web(@{$report}[2, 3], $param->{'action'}); } else { Sympa::WWW::Report::reject_report_web(@{$report}[1 .. 3], $param->{action}); } } unless (@{$spindle->{stash} || []}) { Sympa::WWW::Report::notice_report_web('performed', {}, $param->{'action'}); } return $in{'previous_action'} || 'review'; } # Adds a user to a list (requested by another user). sub do_add { wwslog('info', '(%s)', $in{'email'}); # Access control. return undef unless defined check_authz('do_add', 'add'); my @emails = grep {$_} map { Sympa::Tools::Text::canonic_email($_) } split /\0/, $in{'email'}; return $in{'previous_action'} || 'review' unless @emails; $param->{'email'} = [@emails]; $param->{'quiet'} = $in{'quiet'}; # Action confirmed? my $next_action = $session->confirm_action( $in{'action'}, $in{'response_action'}, arg => join(',', sort @emails), previous_action => ($in{'previous_action'} || 'review') ); return $next_action unless $next_action eq '1'; my $stash = []; my $processed = 0; foreach my $email (@emails) { my $spindle = Sympa::Spindle::ProcessRequest->new( context => $list, action => 'add', email => $email, sender => $param->{'user'}{'email'}, quiet => $param->{'quiet'}, md5_check => 1, scenario_context => { email => $email, sender => $param->{'user'}{'email'}, remote_host => $param->{'remote_host'}, remote_addr => $param->{'remote_addr'} }, stash => $stash, ); $spindle and $processed += $spindle->spin; } unless ($processed) { return $in{'previous_action'} || 'review'; } foreach my $report (@$stash) { if ($report->[1] eq 'notice') { Sympa::WWW::Report::notice_report_web(@{$report}[2, 3], $param->{'action'}); } else { Sympa::WWW::Report::reject_report_web(@{$report}[1 .. 3], $param->{action}); } } unless (@$stash) { Sympa::WWW::Report::notice_report_web('performed', {}, $param->{'action'}); } return $in{'previous_action'} || 'review'; } # By owner, authorizes held subscribe (add) requests. # Old name: do_add_fromsub(). sub do_auth_add { wwslog('info', '(%s)', $in{'id'}); my @ids = grep { $_ and /\A\w+\z/ } split /\0/, $in{'id'}; return ($in{'previous_action'} || 'subindex') unless @ids; $param->{'id'} = [@ids]; # Action confirmed? my $next_action = $session->confirm_action( $in{'action'}, $in{'response_action'}, arg => join(',', sort @ids), previous_action => ($in{'previous_action'} || 'subindex'), ); return $next_action unless $next_action eq '1'; my $spindle = Sympa::Spindle::ProcessRequest->new( context => $robot, action => 'auth', keyauth => [@ids], request => {context => $list, action => 'add'}, sender => $param->{'user'}{'email'}, scenario_context => { sender => $param->{'user'}{'email'}, remote_host => $param->{'remote_host'}, remote_addr => $param->{'remote_addr'} }, ); unless ($spindle and $spindle->spin) { return ($in{'previous_action'} || 'subindex'); } foreach my $report (@{$spindle->{stash} || []}) { if ($report->[1] eq 'notice') { Sympa::WWW::Report::notice_report_web(@{$report}[2, 3], $param->{'action'}); } else { Sympa::WWW::Report::reject_report_web(@{$report}[1 .. 3], $param->{action}); } } unless (@{$spindle->{stash} || []}) { Sympa::WWW::Report::notice_report_web('performed', {}, $param->{'action'}); } return ($in{'previous_action'} || 'subindex'); } # Deletes user(s) from a list (requested by owner) sub do_del { wwslog('info', '(%s)', $in{'email'}); # Access control. return undef unless defined check_authz('do_del', 'del'); my @emails = grep {$_} map { Sympa::Tools::Text::canonic_email($_) } split /\0/, $in{'email'}; return $in{'previous_action'} || 'review' unless @emails; $param->{'email'} = [@emails]; $param->{'quiet'} = $in{'quiet'}; # Action confirmed? my $next_action = $session->confirm_action( $in{'action'}, $in{'response_action'}, arg => join(',', sort @emails), previous_action => ($in{'previous_action'} || 'review') ); return $next_action unless $next_action eq '1'; my $stash = []; my $processed = 0; foreach my $email (@emails) { my $spindle = Sympa::Spindle::ProcessRequest->new( context => $list, action => 'del', email => $email, sender => $param->{'user'}{'email'}, quiet => $param->{'quiet'}, md5_check => 1, scenario_context => { email => $email, sender => $param->{'user'}{'email'}, remote_host => $param->{'remote_host'}, remote_addr => $param->{'remote_addr'} }, stash => $stash, ); $spindle and $processed += $spindle->spin; } unless ($processed) { return $in{'previous_action'} || 'review'; } foreach my $report (@$stash) { if ($report->[1] eq 'notice') { Sympa::WWW::Report::notice_report_web(@{$report}[2, 3], $param->{'action'}); } else { Sympa::WWW::Report::reject_report_web(@{$report}[1 .. 3], $param->{action}); } } unless (@$stash) { Sympa::WWW::Report::notice_report_web('performed', {}, $param->{'action'}); } # 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'; } # By owner, authorizes held signoff (del) requests. # Old name: do_del_fromsig(). sub do_auth_del { wwslog('info', '(%s)', $in{'id'}); my @ids = grep { $_ and /\A\w+\z/ } split /\0/, $in{'id'}; return ($in{'previous_action'} || 'sigindex') unless @ids; $param->{'id'} = [@ids]; # Action confirmed? my $next_action = $session->confirm_action( $in{'action'}, $in{'response_action'}, arg => join(',', sort @ids), previous_action => ($in{'previous_action'} || 'sigindex'), ); return $next_action unless $next_action eq '1'; my $spindle = Sympa::Spindle::ProcessRequest->new( context => $robot, action => 'auth', keyauth => [@ids], request => {context => $list, action => 'del'}, sender => $param->{'user'}{'email'}, scenario_context => { sender => $param->{'user'}{'email'}, remote_host => $param->{'remote_host'}, remote_addr => $param->{'remote_addr'} }, ); unless ($spindle and $spindle->spin) { return ($in{'previous_action'} || 'sigindex'); } foreach my $report (@{$spindle->{stash} || []}) { if ($report->[1] eq 'notice') { Sympa::WWW::Report::notice_report_web(@{$report}[2, 3], $param->{'action'}); } else { Sympa::WWW::Report::reject_report_web(@{$report}[1 .. 3], $param->{action}); } } unless (@{$spindle->{stash} || []}) { Sympa::WWW::Report::notice_report_web('performed', {}, $param->{'action'}); } return ($in{'previous_action'} || 'sigindex'); } # Deletes user from lists (requested by listmaster) sub do_mass_del { wwslog('info', '(%s) (%s)', $in{'email'}, join(', ', split /\0/, $in{'lists'})); # Access control is done by %required_privileges # Turn data into usable structures my @lists = split /\0/, $in{'lists'}; my $email = Sympa::Tools::Text::canonic_email($in{'email'}); return $in{'previous_action'} || 'serveradmin' unless Sympa::Tools::Text::valid_email($email); # Action confirmed? $param->{'email'} = $email; $param->{'lists'} = \@lists; $param->{'quiet'} = $in{'quiet'}; my $next_action = $session->confirm_action( $in{'action'}, $in{'response_action'}, arg => join(',', @lists), previous_action => 'serveradmin' ); return $next_action unless $next_action eq '1'; for my $list (@lists) { return $in{'previous_action'} || 'serveradmin' unless $email; next unless Sympa::List->new($list, $robot, {just_try => 1}); $list = Sympa::List->new($list, $robot); my $stash = []; my $processed = 0; my $spindle = Sympa::Spindle::ProcessRequest->new( context => $list, action => 'del', email => $email, sender => $param->{'user'}{'email'}, quiet => $param->{'quiet'}, md5_check => 1, scenario_context => { email => $email, sender => $param->{'user'}{'email'}, remote_host => $param->{'remote_host'}, remote_addr => $param->{'remote_addr'} }, stash => $stash, ); $spindle and $processed += $spindle->spin; unless ($processed) { return $in{'previous_action'} || 'serveradmin'; } foreach my $report (@$stash) { if ($report->[1] eq 'notice') { Sympa::WWW::Report::notice_report_web(@{$report}[2, 3], $param->{'action'}); } else { Sympa::WWW::Report::reject_report_web(@{$report}[1 .. 3], $param->{action}); } } unless (@$stash) { Sympa::WWW::Report::notice_report_web('performed', {}, $param->{'action'}); } # Skip search because we don't have the expression anymore. delete $in{'previous_action'} if $in{'previous_action'} eq 'search'; } return $in{'previous_action'} || 'serveradmin'; } #################################################### # 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_smtp, $date_epoch, $date); $date_smtp = $message->get_header('Date') || undef; if ($date_smtp) { $date_epoch = eval { DateTime::Format::Mail->new->loose->parse_datetime($date_smtp) ->epoch; }; if (defined $date_epoch) { $date = $language->gettext_strftime('%a, %d %b %Y %H:%M:%S', localtime $date_epoch); } } push @{$param->{'spool'}}, { key => $id, value => { size => int($message->{size} / 1024 + 0.5), subject => $message->{decoded_subject}, date_smtp => $date_smtp, date_epoch => $date_epoch, date => $date, from => $message->{sender}, gecos => $message->{gecos}, spam_status => $message->{spam_status}, is_subscriber => $list->is_list_member($message->{sender}), } }; } #if ($list->is_there_msg_topic()) { # $param->{'request_topic'} = 1; # Compat. <= 6.2.16. # # 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::WWW::Tools::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. my $shared_doc = Sympa::WWW::SharedDocument->new($list); unless ($shared_doc and -r $shared_doc->{fs_path}) { wwslog('err', 'There is no shared documents'); Sympa::WWW::Report::reject_report_web('user', 'no_shared', {}, $param->{'action'}, $list); web_db_log( { 'parameters' => '', 'status' => 'error', 'error_type' => 'internal' } ); return undef; } $param->{'shared_doc'} = $shared_doc->as_hashref; my @mod = map { $_->as_hashref } $shared_doc->get_moderated_descendants; $param->{'shared_doc'}{'children'} = [@mod] if @mod; 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 $shared_doc = Sympa::WWW::SharedDocument->new($list); unless ($shared_doc and -r $shared_doc->{fs_path}) { wwslog('err', 'There is no shared documents'); Sympa::WWW::Report::reject_report_web('user', 'no_shared', {}, $param->{'action'}, $list); web_db_log( { 'parameters' => '', 'status' => 'error', 'error_type' => 'internal' } ); return undef; } $param->{'shared_doc'} = $shared_doc->as_hashref; my @id = split /\0/, $in{'id'}; unless ($in{'mode_confirm'} || $in{'mode_cancel'}) { # File already exists ? my @children_hash = map { $_->as_hashref } grep { $_ and not $_->{moderate} } map { Sympa::WWW::SharedDocument->new($list, $_) } @id; if (@children_hash) { $param->{'shared_doc'}{'children'} = [@children_hash]; $param->{'id'} = [@id]; return 1; } } # Install the file(s) selected foreach my $id (@id) { next unless $id; my $child = Sympa::WWW::SharedDocument->new($list, $id); next unless $child and $child->{moderate}; unless ($child->install) { my $errno = $ERRNO; Sympa::WWW::Report::reject_report_web('intern', 'install_shared_failed', {}, $param->{'action'}, $list, $param->{'user'}{'email'}, $robot); wwslog('err', 'Failed to nstall %s; %s', $child, $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'} = join '/', @{$child->{paths}}; my $sender = $child->{owner}; unless ( Sympa::send_file($list, 'd_install_shared', $sender, \%context)) { wwslog('notice', 'Unable to send template "d_install_shared" to %s', $sender); } } Sympa::WWW::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 $shared_doc = Sympa::WWW::SharedDocument->new($list); unless ($shared_doc and -r $shared_doc->{fs_path}) { wwslog('err', 'There is no shared documents'); Sympa::WWW::Report::reject_report_web('user', 'no_shared', {}, $param->{'action'}, $list); web_db_log( { 'parameters' => '', 'status' => 'error', 'error_type' => 'internal' } ); return undef; } $param->{'shared_doc'} = $shared_doc->as_hashref; my @id = split /\0/, $in{'id'}; foreach my $id (@id) { my $child = Sympa::WWW::SharedDocument->new($list, $id); next unless $child and $child->{moderate}; unless ($in{'quiet'}) { my %context; my $sender; $context{'rejected_by'} = $param->{'user'}{'email'}; $context{'filename'} = join '/', @{$child->{paths}}; $sender = $child->{owner}; unless ( Sympa::send_file( $list, 'd_reject_shared', $sender, \%context ) ) { wwslog('notice', 'Unable to send template "d_reject_shared" to %s', $sender); } } unless ($child->unlink) { Sympa::WWW::Report::reject_report_web( 'intern', 'erase_file', {'file' => join('/', @{$child->{paths}})}, $param->{'action'}, $list, $param->{'user'}{'email'}, $robot ); wwslog('err', 'Failed to erase %s', $child->{fs_path}); web_db_log( { 'parameters' => $id, 'status' => 'error', 'error_type' => 'internal' } ); return undef; } } Sympa::WWW::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{'blocklist'} add sender to list blocklist # # 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{'blocklist'}) { $in{'quiet'} = 1; } wwslog('info', '(%s)', $in{'id'}); my $file; $param->{'blocklist_added'} = 0; $param->{'blocklist_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::WWW::Report::reject_report_web('user', 'already_moderated', {key => $id, listname => $list->{'name'}}, $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 blocklist if (($in{'quiet'} ne '1') || ($in{'blocklist'})) { 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{'blocklist'}) { if (_add_in_blocklist($rejected_sender, $robot, $list)) { $param->{'blocklist_added'} += 1; wwslog('info', "added $rejected_sender to $list->{'name'} blocklist" ); } else { wwslog('notice', "Unable to add $rejected_sender to $list->{'name'} blocklist" ); $param->{'blocklist_ignored'} += 0; } } } else { $log->syslog( 'err', 'No sender found for message %s. Unable to use her address to add to blocklist 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::WWW::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 { wwslog('info', '(%s)', $in{'id'}); my @ids = split /\0/, $in{'id'}; $param->{'id'} = [@ids]; my @topics = grep { defined $_ and length $_ } split /\0/, $in{'topic'}; $param->{'topic'} = [@topics]; $param->{'topic_required'} = ($list->is_there_msg_topic and $list->is_msg_topic_tagging_required); # Action confirmed? if ($param->{'topic_required'}) { my $response_action = ( @topics # Topics are required. or ($in{'response_action'} and $in{'response_action'} eq 'cancel') ) ? $in{'response_action'} : undef; my $next_action = $session->confirm_action( $in{'action'}, $response_action, arg => join(',', sort @ids), previous_action => ($in{'previous_action'} || 'modindex') ); return $next_action unless $next_action eq '1'; } # Load message list. my @mail_command = (); foreach my $id (@ids) { # 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::WWW::Report::reject_report_web('user', 'already_moderated', {key => $id, listname => $list->{'name'}}, $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 (@topics) { Sympa::Spool::Topic->new( topic => join(',', @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 => Sympa::get_address($robot, 'owner'), 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::WWW::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::WWW::Report::notice_report_web('performed_soon', {}, $param->{'action'}); return 'modindex'; } # Adds user from moderation index. sub do_add_frommod { wwslog('info', '(%s)', $in{'id'}); my @ids = split /\0/, $in{'id'}; $param->{'id'} = [@ids]; my @users; foreach my $id (@ids) { 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::WWW::Report::reject_report_web('user', 'already_moderated', {key => $id, listname => $list->{'name'}}, $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 $email and Sympa::Tools::Text::valid_email($email); my $fullname = $message->{gecos} if defined $message->{gecos} and $message->{gecos} =~ /\S/; push @users, ( defined $fullname ? {email => $email, gecos => $fullname} : {email => $email} ); } return 'modindex' unless @users; $param->{'email'} = [@users]; # Action confirmed? my $next_action = $session->confirm_action( $in{'action'}, $in{'response_action'}, arg => join(',', sort @ids), previous_action => 'modindex' ); return $next_action unless $next_action eq '1'; my $stash = []; my $processed = 0; foreach my $u (@users) { my $spindle = Sympa::Spindle::ProcessRequest->new( context => $list, action => 'add', email => $u->{email}, gecos => $u->{gecos}, sender => $param->{'user'}{'email'}, md5_check => 1, scenario_context => { email => $u->{email}, sender => $param->{'user'}{'email'}, remote_host => $param->{'remote_host'}, remote_addr => $param->{'remote_addr'} }, stash => $stash, ); $processed += $spindle->spin if $spindle; } unless ($processed) { return 'modindex'; } foreach my $report (@$stash) { if ($report->[1] eq 'notice') { Sympa::WWW::Report::notice_report_web(@{$report}[2, 3], $param->{'action'}); } else { Sympa::WWW::Report::reject_report_web(@{$report}[1 .. 3], $param->{action}); } } unless (@$stash) { Sympa::WWW::Report::notice_report_web('performed', {}, $param->{'action'}); } 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::WWW::Tools::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_id . '/' . $in{'id'}; unless (-d $html_dir) { Sympa::WWW::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; return 1; } if (open my $fh, '<', $html_dir . '/msg00000.html') { $param->{'html_content'} = do { local $RS; <$fh> }; close $fh; } #XXX#FIXME: Is this required? #XXXpush @other_include_path, $html_dir; my $id = $in{'id'}; 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 and not $message->{validated}; } unless ($message) { Sympa::WWW::Report::reject_report_web('user', 'already_moderated', {key => $id, listname => $list->{'name'}}, $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' } ); return undef; } my ($date_smtp, $date_epoch, $date); $date_smtp = $message->get_header('Date') || undef; if ($date_smtp) { $date_epoch = eval { DateTime::Format::Mail->new->loose->parse_datetime($date_smtp) ->epoch; }; if (defined $date_epoch) { $date = $language->gettext_strftime('%a, %d %b %Y %H:%M:%S', localtime $date_epoch); } } $param->{'msg'} = { key => $id, value => { size => int($message->{size} / 1024 + 0.5), subject => $message->{decoded_subject}, date_smtp => $date_smtp, date_epoch => $date_epoch, date => $date, from => $message->{sender}, gecos => $message->{gecos}, spam_status => $message->{spam_status}, is_subscriber => $list->is_list_member($message->{sender}), } }; 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_header', 'message_footer', 'remind.tt2', 'invite.tt2', 'reject.tt2', 'your_infected_msg.tt2' ], all_templates => [ 'info', 'homepage', 'welcome.tt2', 'bye.tt2', 'removed.tt2', 'message_header', 'message_footer', '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 ($role, $right) = $list->may_edit($f, $param->{'user'}{'email'}, file => 1); next unless $right eq 'write'; if ($Sympa::WWW::Tools::filenames{$f}{'gettext_id'}) { $param->{'files'}{$f}{'complete'} = $language->gettext( $Sympa::WWW::Tools::filenames{$f}{'gettext_id'}); } else { $param->{'files'}{$f}{'complete'} = $f; } } return 1; } unless (defined $Sympa::WWW::Tools::filenames{$in{'file'}}) { Sympa::WWW::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::WWW::Tools::filenames{$in{'file'}}{'gettext_id'}); my $subdir = ''; if ($in{'file'} =~ /\.tt2$/) { $subdir = 'mail_tt2/'; } if ($param->{'list'}) { my ($role, $right) = $list->may_edit($in{'file'}, $param->{'user'}{'email'}, file => 1); unless ($right eq 'write') { Sympa::WWW::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 $file_path = $param->{'filepath'}; $param->{'filecontent'} = Sympa::Tools::Text::slurp($file_path); unless (defined $param->{'filecontent'}) { wwslog('err', 'Failed to open file %s: %m', $file_path); Sympa::WWW::Report::reject_report_web( 'intern', 'cannot_open_file', {'file' => $file_path}, $param->{'action'}, $list, $param->{'user'}{'email'}, $robot ); web_db_log( { 'parameters' => $in{'file'}, 'status' => 'error', 'error_type' => 'internal' } ); return undef; } } 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::WWW::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 { $param->{'filepath'} = Sympa::search_fullpath($list || $robot, $file, subdir => $subdir); } $param->{'filecontent'} = Sympa::Tools::Text::slurp($param->{'filepath'}); unless (defined $param->{'filecontent'}) { wwslog('err', 'Failed to open file %s: %m', $param->{'filepath'}); Sympa::WWW::Report::reject_report_web( 'intern', 'cannot_open_file', {'file' => $param->{'file_path'}}, $param->{'action'}, $list, $param->{'user'}{'email'}, $robot ); web_db_log( { 'parameters' => $in{'file'}, 'status' => 'error', 'error_type' => 'internal' } ); return undef; } } if (-f $param->{'filepath'} && (!-r $param->{'filepath'})) { Sympa::WWW::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'}; unless ($in{'file'} and $Sympa::WWW::Tools::filenames{$in{'file'}}) { Sympa::WWW::Report::reject_report_web('user', 'file_not_editable', {'file' => $in{'file'}}, $param->{'action'}); wwslog('info', 'File %s not editable', $in{'file'}); return undef; } if ($param->{'list'}) { my ($role, $right) = $list->may_edit($in{'file'}, $param->{'user'}{'email'}, file => 1); unless ($right eq 'write') { Sympa::WWW::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; } 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::WWW::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::WWW::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'}"; } elsif ($in{'file'} =~ /\.tt2$/) { $param->{'filepath'} = "$Conf::Conf{'etc'}/$robot/mail_tt2/$in{'file'}"; } else { $param->{'filepath'} = "$Conf::Conf{'etc'}/$robot/$in{'file'}"; } } else { if ($in{'file'} eq 'list_aliases.tt2') { $param->{'filepath'} = "$Conf::Conf{'etc'}/$in{'file'}"; } elsif ($in{'file'} =~ /\.tt2$/) { $param->{'filepath'} = "$Conf::Conf{'etc'}/mail_tt2/$in{'file'}"; } else { $param->{'filepath'} = "$Conf::Conf{'etc'}/$in{'file'}"; } } } unless ((!-e $param->{'filepath'}) or (-w $param->{'filepath'})) { Sympa::WWW::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::WWW::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 my $ofh; unless (open $ofh, '>', $param->{'filepath'}) { Sympa::WWW::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; } print $ofh Sympa::Tools::Text::canonic_text($in{'content'}); close $ofh; } elsif (-f $param->{'filepath'}) { wwslog('info', 'Deleting %s', $param->{'filepath'}); unlink $param->{'filepath'}; } web_db_log( { 'parameters' => $in{'file'}, 'status' => 'success' } ); Sympa::WWW::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->new($list, 'tracking')->authz( $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; } if ( ($session->{'archive_sniffer'} || '') ne 'false' and not $param->{'user'}{'email'} and $list->{'admin'}{'web_archive_spam_protection'} eq 'cookie') { my $month = $in{'month'} || ''; my $arc_file = $in{'arc_file'} || ''; $param->{'month'} = $month; $param->{'arc_file'} = $arc_file; # Action confirmed? my $next_action = $session->confirm_action( $in{'action'}, $in{'response_action'}, arg => join('/', $month, $arc_file), previous_action => ($in{'previous_action'} || 'info') ); return $next_action unless $next_action eq '1'; # If confirmed, set flag and redirect to the file. $session->{'archive_sniffer'} = 'false'; $param->{'redirect_to'} = Sympa::get_url( $list, 'arc', paths => ($month ? [$month, $arc_file] : ['']), authority => 'local' ); return 1; } my $archive = Sympa::Archive->new(context => $list); # Calendar my @arcs = $archive->get_archives; unless (@arcs) { Sympa::WWW::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{PATH_INFO} // '') =~ m{/\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::WWW::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::WWW::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 # they consult archives $param->{'user'}{'prefs'}{'arc_mode'} = $session->{'arc_mode'}; Sympa::User::update_global_user($param->{'user'}{'email'}, {data => $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::WWW::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::WWW::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 ($date_smtp, $date_epoch, $date); $date_smtp = $message->get_header('Date') || undef; unless ($date_smtp) { wwslog('err', 'No date found in message %s', $message); next; } $date_epoch = eval { DateTime::Format::Mail->new->loose->parse_datetime($date_smtp) ->epoch; }; if (defined $date_epoch) { if ($date_epoch < $oldest_day) { $stop_search = 1; last; } $date = $language->gettext_strftime("%d %b %Y", localtime $date_epoch); } push @archives, { subject => $message->{decoded_subject}, date_smtp => $date_smtp, date_epoch => $date_epoch, date => $date, from => $message->{sender}, gecos => $message->{gecos}, message_id => $message->{message_id}, year_month => $arc, }; $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'}; my @msg_subjects = split /\0/, $in{'msg_subject'}; unless (@msgids) { Sympa::WWW::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; } $param->{'yyyy'} = $in{'yyyy'}; $param->{'month'} = $in{'month'}; $param->{'signal_as_spam'} = $in{'signal_as_spam'}; $param->{'msgid'} = [@msgids]; $param->{'msg_subject'} = [@msg_subjects]; # Action confirmed? my $next_action = $session->confirm_action( $in{'action'}, $in{'response_action'}, arg => join(',', $in{'yyyy'}, $in{'month'}, @msgids), previous_action => 'arc' ); unless ($next_action eq '1') { $in{'month'} = sprintf '%s-%s', $in{'yyyy'}, $in{'month'} if $next_action eq 'arc'; return $next_action; } my $msg_string = "\n\n"; my $tracking = Sympa::Tracking->new(context => $list); foreach my $msgid (@msgids) { chomp $msgid; if (defined($in{signal_as_spam}) && $Conf::Conf{'reporting_spam_script_path'} ne '') { $msg_string .= sprintf "signal_spam %s %s-%s %s\n", $list->{'name'}, $in{'yyyy'}, $in{'month'}, $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::WWW::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::WWW::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::WWW::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::WWW::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->new($list, 'tracking')->authz( $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::WWW::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::WWW::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::WWW::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::WWW::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::WWW::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::WWW::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'); if ( ($session->{'archive_sniffer'} || '') ne 'false' and not $param->{'user'}{'email'} and $list->{'admin'}{'web_archive_spam_protection'} eq 'cookie') { my $archive_name = $in{'archive_name'} || ''; my $msgid = $in{'msgid'} || ''; $param->{'archive_name'} = $archive_name; $param->{'msgid'} = $msgid; # Action confirmed? my $next_action = $session->confirm_action( $in{'action'}, $in{'response_action'}, arg => join('/', $archive_name, $msgid), previous_action => 'info' ); return $next_action unless $next_action eq '1'; # If confirmed, set flag. $session->{'archive_sniffer'} = 'false'; } my $search = Sympa::WWW::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::WWW::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::WWW::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'}{'update'}{'email'}; $param->{'pending'}{$list->{'name'}}{'date_epoch'} = $list->{'admin'}{'update'}{'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_epoch' => $list->{'admin'}{'creation'}{'date_epoch'} }; } foreach my $l ( sort { $b->{'creation_date_epoch'} <=> $a->{'creation_date_epoch'} } @unordered_lists) { push @{$param->{'latest_lists'}}, $l; } 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 () { $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'} } grep { $_->{role} eq 'owner' } @{$list->get_current_admins}), 'editors' => join(", ", map { $_->{'email'} } grep { $_->{role} eq 'editor' } @{$list->get_current_admins}), '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'}, }; } 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_epoch' => $list->{'admin'}{'creation'}{'date_epoch'}, 'subscribers' => $list->get_total }; } foreach my $l (sort { $b->{'subscribers'} <=> $a->{'subscribers'} } @unordered_lists) { 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'}{'update'}{'email'}; $param->{'list_request_date_epoch'} = $list->{'admin'}{'update'}{'date_epoch'}; $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. # Kept for comaptibility <= 6.2.22. sub do_install_pending_list { my $status = $in{'status'}; $in{'mode'} = 'install'; return ($status eq 'closed') ? 'close_list' : ($status eq 'open') ? 'open_list' : undef; } #=head2 sub do_create_list # #Creates a list using a list template # #=head3 Arguments # #=over # #=item * I # #=back # #=head3 Return # #=over # #=item * I<1>, if no problem is encountered # #=item * I, 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'} ); my $spindle = Sympa::Spindle::ProcessRequest->new( context => $robot, action => 'create_list', parameters => { listname => $in{'listname'}, owner => [ { email => $param->{'user'}{'email'}, gecos => $param->{'user'}{'gecos'}, } ], subject => $in{'subject'}, creation_email => $param->{'user'}{'email'}, lang => $param->{'lang'}, status => $param->{'status'}, #FIXME type => $in{'template'}, topics => $in{'topics'}, description => $in{'info'}, custom_input => $in{'custom_input'}, }, sender => $param->{'user'}{'email'}, ( $param->{'user'}{'email'} ? (md5_check => 1) : () ), scenario_context => { 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'}, } ); unless ($spindle and $spindle->spin) { return 'create_list_request'; } foreach my $report (@{$spindle->{stash} || []}) { if ($report->[1] eq 'notice') { Sympa::WWW::Report::notice_report_web(@{$report}[2, 3], $param->{'action'}); } else { Sympa::WWW::Report::reject_report_web(@{$report}[1 .. 3], $param->{action}); } } unless (@{$spindle->{stash} || []}) { Sympa::WWW::Report::notice_report_web('performed', {}, $param->{'action'}); } unless ($spindle->success) { return 'create_list_request'; } # Were aliases installed? if (grep { $_->[1] eq 'notice' and $_->[2] eq 'auto_aliases' } @{$spindle->{stash} || []}) { $param->{'auto_aliases'} = 1; } else { $param->{'auto_aliases'} = 0; } # Switch to new list context. $list = Sympa::List->new($in{'listname'}, $robot); $param->{'list'} = $in{'listname'}; $param->{'redirect_to'} = Sympa::get_url($list, 'admin', nomenu => $param->{'nomenu'}); return 1; } #=head2 sub do_create_list_request # #Sends back the list creation edition form. # #=head3 Arguments # #=over # #=item * I # #=back # #=head3 Return # #=over # #=item * I<1>, if no problem is encountered # #=item * I, 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->new($robot, 'create_list')->authz( $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::WWW::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 = map { ($_ => {}) } Sympa::Robot::topic_keys($robot); if ($in{'topics'} and exists $topics{$in{'topics'}}) { $topics{$in{'topics'}}->{selected} = 1; } $param->{'list_of_topics'} = {%topics}; unless ($param->{'list_list_tpl'} = Sympa::WWW::Tools::get_list_list_tpl($robot)) { Sympa::WWW::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', ''); return 1; } sub do_editsubscriber { wwslog('info', '(%s)', $in{'email'}); my $subscriber; unless ($subscriber = $list->get_list_member($in{'email'})) { Sympa::WWW::Report::reject_report_web( 'user', 'user_not_subscriber', {email => $in{'email'}, listname => $list->{'name'}}, $param->{'action'}, $list, $param->{'user'}{'email'}, $robot ); wwslog('info', 'Subscriber %s not found', $in{'email'}); return $in{'previous_action'} || 'review'; } $param->{'current_subscriber'} = $subscriber; $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; $param->{'current_subscriber'}{'lang'} = $language->set_lang($user->{'lang'}, Sympa::best_language($list)); $language->pop_lang; foreach my $m ($list->available_reception_mode) { if ($param->{'current_subscriber'}{'reception'} eq $m) { $param->{'reception'}{$m}{'selected'} = ' selected'; } else { $param->{'reception'}{$m}{'selected'} = ''; } } foreach my $m (qw(conceal noconceal)) { 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::WWW::Tools::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', '(dir/file=%s/%s, email=%s, envid=%s)', $in{'dir'}, $in{'file'}, $in{'email'}, $in{'envid'}); # Prevent directory traversal. if ($in{'file'}) { my $subpath = $in{'file'}; $subpath =~ s{\Amsg00000/}{}; delete $in{'file'} if $subpath =~ m{/}; } if ($in{'dir'}) { delete $in{'dir'} if 0 <= index($in{'dir'}, '/'); } my $html_relpath; if ($in{'email'}) { my $escaped_email = Sympa::Tools::Text::escape_chars($in{'email'}); $html_relpath = $in{'envid'} ? sprintf('%s_%08s', $escaped_email, $in{'envid'}) : $escaped_email; } elsif ($in{'dir'} and $in{'file'}) { $html_relpath = $in{'dir'}; } else { return undef; } my $bounce_path = $list->get_bounce_dir() . '/' . $html_relpath; unless (-r $bounce_path) { Sympa::WWW::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_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); Sympa::Archive::html_format( $bounce_message, 'destination_dir' => $html_dir, 'attachment_url' => ['viewbounce', $list->{'name'}, $html_relpath] ) if $bounce_message; } unless (-d $html_dir) { Sympa::WWW::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; } if ($in{'email'} and $in{'envid'}) { my $tracking = Sympa::Tracking->new(context => $list); my $info = $tracking->db_fetch( recipient => $in{'email'}, envid => $in{'envid'} ); if ($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'} = $info; } } elsif ($in{'email'}) { $param->{'tracking_info'} = {recipient => $in{'email'},}; } $param->{'previous_action'} = $in{'previous_action'} || 'editsubscriber'; return 1; } ## some help for listmaster and developpers #FIXME Works only under doamin context. sub do_scenario_test { wwslog('info', ''); # List available scenarios. # FIXME Use get_scenarios(). my $dh; unless (opendir $dh, Sympa::Constants::DEFAULTDIR . '/scenari/') { Sympa::WWW::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 $dh) { if ($scfile =~ /^([-\w]+)[.](\w+)/) { $param->{'scenario'}{$1}{'defined'} = 1; } } closedir $dh; 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 $function = $in{'scenario'}; wwslog('debug3', 'Perform scenario_test'); my $result = Sympa::Scenario->new($robot, $function)->authz( $in{'auth_method'}, { 'listname' => $in{'listname'}, # FIXME: Unavailable list 'sender' => $in{'sender'}, 'email' => $in{'email'}, 'remote_host' => $in{'remote_host'}, 'remote_addr' => $in{'remote_addr'} }, debug => 1 ); 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::WWW::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'})); push @{$param->{'members'}}, $i; } if ($param->{'page'} > 1) { $param->{'prev_page'} = $param->{'page'} - 1; } $param->{'size'} = $size; return 1; } sub do_resetbounce { wwslog('info', ''); 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::WWW::Report::reject_report_web('user', 'user_not_subscriber', {email => $email, listname => $list->{'name'}}, $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::WWW::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::WWW::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::WWW::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::WWW::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'}); ## trim leading/trailing whitespace if (defined $in{'filter_list'}) { $in{'filter_list'} =~ s/^\s+|\s+$//g; } 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->new($list, 'visibility')->authz( $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'}} = { 'subject' => $list->{'admin'}{'subject'}, 'admin' => $is_admin, 'export' => 'no', # Compat. < 6.2.32 'host' => $list->{'domain'}, }; } $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 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::WWW::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; } # Start parsing the data sent by the edition form. my $new_admin = _deserialize_changes(); my $config = Sympa::List::Config->new($list, config => $list->{'admin'}); my $errors = []; my $validity = $config->submit($new_admin, $param->{'user'}{'email'}, $errors); unless (defined $validity) { if (my @intern = grep { $_->[0] eq 'intern' } @$errors) { foreach my $err (@intern) { Sympa::WWW::Report::reject_report_web($err->[0], $err->[1], {}, $param->{'action'}, $list); wwslog('err', 'Internal error %s', $err->[1]); } } else { Sympa::WWW::Report::reject_report_web('intern', 'unknown', {}, $param->{'action'}, $list); wwslog('err', 'Unknown error'); } web_db_log( { 'status' => 'error', 'error_type' => 'internal' } ); return undef; } my $error_return = 0; foreach my $err (grep { $_->[0] eq 'user' } @$errors) { $error_return = 1 unless $err->[1] eq 'mandatory_parameter'; Sympa::WWW::Report::reject_report_web( $err->[0], $err->[1], { 'p_name' => $language->gettext($err->[2]->{p_info}->{gettext_id}), %{$err->[2]} }, $param->{'action'}, $list ); wwslog( 'err', 'Error on parameter %s: %s', join('.', @{$err->[2]->{p_paths}}), $err->[1] ); web_db_log( { 'status' => 'error', 'error_type' => 'syntax_errors' } ); } return 'edit_list_request' if $error_return; if ($validity eq '') { Sympa::WWW::Report::notice_report_web('no_parameter_edited', {}, $param->{'action'}); wwslog('info', 'No parameter was edited by user'); return 'edit_list_request'; } # Validation of the form finished. Start of valid data treatments. # For changed msg_topic.name. if (_notify_deleted_topic($config)) { Sympa::WWW::Report::notice_report_web( 'subscribers_noticed_deleted_topics', {}, $param->{'action'}); } my $data_source_updated_member = 1 if grep { $config->get_change($_) } grep { $_ =~ /\Ainclude_/ or $_ eq 'member_include' } $config->keys; my $data_source_updated_owner = 1 if $config->get_change('owner_include'); my $data_source_updated_editor = 1 if $config->get_change('editor_include'); # Update config in memory. $config->commit; ## Save config file unless ($list->save_config($param->{'user'}{'email'})) { Sympa::WWW::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::WWW::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 ($data_source_updated_member) { Sympa::WWW::Report::notice_report_web('member_updated_soon', {}, $param->{'action'}); } if ($data_source_updated_owner) { Sympa::WWW::Report::notice_report_web('owner_updated_soon', {}, $param->{'action'}); } if ($data_source_updated_editor) { Sympa::WWW::Report::notice_report_web('editor_updated_soon', {}, $param->{'action'}); } Sympa::WWW::Report::notice_report_web('list_config_updated', {}, $param->{'action'}); web_db_log({'status' => 'success'}); return 'edit_list_request'; } # Parses all the data sent from the web interface to the FCGI. # Context: # $list: Sympa::List instance. # %in: Input from form. # Parameters: # None. # Returns: # Hashref containing parsed input. sub _deserialize_changes { my $new_admin = {}; foreach my $key (sort keys %in) { next unless $key =~ /\A(single_param|multiple_param)[.](\S+)\z/; my ($type, $name) = ($1, $2); # If the parameter is a multiple values parameter, store the values # into an array. my $value; if ($type eq 'multiple_param') { $value = [grep {/\S/} split /\0/, $in{$key}]; } else { $value = ($in{$key} =~ /\S/) ? $in{$key} : undef; } # $in{'owner.0.gecos'} is stored into $new_admin->{owner}[0]{gecos}. # Inconsistent subscripts will be ignored. my @subscripts = map { if (/\A\d+\z/) { sprintf '[%s]', $_; } elsif (/\A[-\w]+\z/) { sprintf "{'%s'}", $_; } else { "{''}"; } } split /[.]/, $name; eval sprintf '$new_admin->%s = $value', join('->', @subscripts); } # Deleted parameters or paragraphs. foreach my $key (sort keys %in) { next unless $key =~ /\Adeleted_param[.](\S+)\z/; my $name = $1; next unless defined $in{$key} and $in{$key} =~ /\S/; my @subscripts = map { if (/\A\d+\z/) { sprintf '[%s]', $_; } elsif (/\A[-\w]+\z/) { sprintf "{'%s'}", $_; } else { "{''}"; } } split /[.]/, $name; my $var = sprintf '$new_admin->%s', join('->', @subscripts); if (eval "exists $var and ref $var eq 'HASH'") { my %hash = map { ($_ => undef) } keys %{eval $var}; eval "$var = {%hash}"; } else { eval "$var = undef"; } } return $new_admin; } # No longer used. #sub _shift_var; # 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 $config = shift; my @msg_topics = @{$config->get('msg_topic') || []}; my @msg_topics_changes = $config->get_change('msg_topic'); return 0 unless @msg_topics_changes; # No changes. my @msg_topics_deleted; my ($msg_topics_changes) = @msg_topics_changes; unless (defined $msg_topics_changes) { @msg_topics_deleted = @msg_topics; } else { my %msg_topics_changes = %{$config->get_change('msg_topic') || {}}; @msg_topics_deleted = map { $msg_topics[$_] ? ($msg_topics[$_]->{name}) : (); } grep { not defined $msg_topics_changes{$_} } sort { $a <=> $b } keys %msg_topics_changes; } my $deleted = 0; if (@msg_topics_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_topics_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 $deleted; } # Sends back the list config edition form. sub do_edit_list_request { wwslog('info', '(%s)', $in{'group'}); return 1 unless $in{'group'}; my $config = Sympa::List::Config->new($list, config => $list->{'admin'}); my $schema = $config->get_schema($param->{'user'}{'email'}); my @schema = map { # Skip comments and default values. # Skip parameters belonging to another group. if ( $_ eq 'comment' or $_ eq 'defaults' or $schema->{$_}->{group} ne $in{'group'}) { (); } else { my @p = _do_edit_list_request($config, $schema->{$_}, [$_]); if (@p) { # Store if the parameter is still at its default value or not. # FIXME:Multiple levels of keys should be possible. $p[0]->{'default_value'} = $config->get('defaults')->{$_}; } @p; } } $config->keys; # If at least one param was editable, make the update button appear in # the form. $param->{'is_form_editable'} = grep { $_->{privilege} eq 'write' } @schema; $param->{'config_schema'} = [@schema]; $param->{'config_values'} = { map { my @value = $config->get($_->{name}); @value ? ($_->{name} => $value[0]) : (); } @schema }; $param->{'group'} = $in{'group'}; $param->{'serial'} = $config->get('serial'); return 1; } sub _do_edit_list_request { my $config = shift; my $pitem = shift; my $pnames = shift; # Skip obsolete parameters and alias names. # Skip hidden parameters. return () if $pitem->{obsolete}; return () if $pitem->{privilege} eq 'hidden'; $pitem->{name} = $pnames->[-1]; $pitem->{title} = $language->gettext($pitem->{gettext_id}) if exists $pitem->{gettext_id}; $pitem->{comment} = $language->gettext($pitem->{gettext_comment}) if exists $pitem->{gettext_comment}; $pitem->{unit} = $language->gettext($pitem->{gettext_unit}) if exists $pitem->{gettext_unit}; if (ref $pitem->{format} eq 'ARRAY' and $pitem->{occurrence} =~ /n$/) { $pitem->{type} = 'set'; } elsif (ref $pitem->{format} eq 'HASH') { $pitem->{type} = 'paragraph'; my @format = map { _do_edit_list_request( $config, $pitem->{format}->{$_}, [@$pnames, $_] ); } $config->keys(join '.', @$pnames); if (@format) { $pitem->{format} = [@format]; } else { return (); } } else { $pitem->{type} = 'leaf'; $pitem->{enum} = 1 if ref $pitem->{format} eq 'ARRAY'; if ($pitem->{scenario}) { my $scenarios = Sympa::Scenario::get_scenarios($list, $pitem->{scenario}); $pitem->{format} = { map { my $name = $_->{name}; my $title = $_->get_current_title; ($name => {name => $name, title => $title}); } @$scenarios }; } elsif ($pitem->{task}) { my $tasks = Sympa::Task::get_tasks($list, $pitem->{task}); $pitem->{format} = {map { ($_->{name} => $_) } @$tasks}; } elsif ($pitem->{datasource}) { my $list_of_data_sources = $list->load_data_sources_list($robot); $pitem->{format} = $list_of_data_sources; } } return ($pitem); } # No longer used. #sub _check_new_values; # DEPRECATED. #sub _prepare_edit_form; # DEPRECATED. #sub _prepare_data; # No longer used. #sub _restrict_values; ## NOT USED anymore (expect chinese) #sub do_close_list_request; # 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->new($robot, 'create_list')->authz( $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::WWW::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'; } # Compat. <= 6.2.20 sub do_copy_list { $in{'mode'} = 'copy'; goto &do_move_list; # "&" is required. } # In order to rename a list you must be list owner and you must be allowed to # create new list. sub do_move_list { wwslog('info', '(%s, %s, mode=%s)', $in{'new_listname'}, $in{'new_robot'}, $in{'mode'}); unless ($in{'new_robot'} and Conf::valid_robot($in{'new_robot'})) { wwslog('err', 'Unknown robot %s', $robot); Sympa::WWW::Report::reject_report_web('user', 'unknown_robot', {new_robot => $in{'new_robot'}}, $param->{action}); return undef; } $param->{'new_listname'} = $in{'new_listname'}; $param->{'new_robot'} = $in{'new_robot'}; $param->{'mode'} = $in{'mode'}; # Action confirmed? my $next_action = $session->confirm_action( 'move_list', $in{'response_action'}, arg => $in{'new_listname'} . '@' . $in{'new_robot'}, previous_action => ($in{'previous_action'} || 'admin') ); return $next_action unless $next_action eq '1'; my $spindle = Sympa::Spindle::ProcessRequest->new( context => $in{'new_robot'}, action => 'move_list', listname => $in{'new_listname'}, current_list => $list, mode => $in{'mode'}, sender => $param->{'user'}{'email'}, ( $param->{'user'}{'email'} ? (md5_check => 1) : () ), scenario_context => { sender => $param->{'user'}{'email'}, remote_host => $param->{'remote_host'}, remote_addr => $param->{'remote_addr'}, }, ); unless ($spindle and $spindle->spin) { return 'rename_list_request'; } foreach my $report (@{$spindle->{stash} || []}) { if ($report->[1] eq 'notice') { Sympa::WWW::Report::notice_report_web(@{$report}[2, 3], $param->{'action'}); } else { Sympa::WWW::Report::reject_report_web(@{$report}[1 .. 3], $param->{action}); } } unless (@{$spindle->{stash} || []}) { Sympa::WWW::Report::notice_report_web('performed', {}, $param->{'action'}); } if (grep { $_->[1] ne 'notice' } @{$spindle->{stash} || []}) { return 'rename_list_request'; } # Were aliases installed? if (grep { $_->[1] eq 'notice' and $_->[2] eq 'auto_aliases' } @{$spindle->{stash} || []}) { $param->{'auto_aliases'} = 1; } else { $param->{'auto_aliases'} = 0; } # Switch to new list context. $list = Sympa::List->new($in{'new_listname'}, $in{'new_robot'}); $robot = $list->{'domain'}; $param->{'list'} = $in{'new_listname'}; if ($in{'new_robot'} eq $robot) { $param->{'redirect_to'} = Sympa::get_url( $list, 'admin', nomenu => $param->{'nomenu'}, authority => 'local' ); } else { $param->{'redirect_to'} = Sympa::get_url($list, 'admin', nomenu => $param->{'nomenu'}); } return 1; } sub do_purge_list { wwslog('info', ''); my @lists = grep {$_} map { Sympa::List->new($_, $robot) } grep {$_} split /\0/, $in{'selected_lists'}; return 'get_closed_lists' unless @lists; $param->{'selected_lists'} = [map { $_->{'name'} } @lists]; # Action confirmed? my $next_action = $session->confirm_action( 'purge_list', $in{'response_action'}, arg => join(',', @{$param->{'selected_lists'} || []}), previous_action => 'get_closed_lists', ); return $next_action unless $next_action eq '1'; my $spindle = Sympa::Spindle::ProcessRequest->new( context => $robot, action => 'close_list', current_list => [@lists], mode => 'purge', sender => $param->{'user'}{'email'}, ( $param->{'user'}{'email'} ? (md5_check => 1) : () ), scenario_context => { sender => $param->{'user'}{'email'}, remote_host => $param->{'remote_host'}, remote_addr => $param->{'remote_addr'}, }, ); unless ($spindle and $spindle->spin) { wwslog('err', 'Cannot purge lists'); return 'get_closed_lists'; } foreach my $report (@{$spindle->{stash} || []}) { if ($report->[1] eq 'notice') { Sympa::WWW::Report::notice_report_web(@{$report}[2, 3], $param->{'action'}); } else { Sympa::WWW::Report::reject_report_web(@{$report}[1 .. 3], $param->{action}); } } unless (@{$spindle->{stash} || []}) { Sympa::WWW::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, mode=%s)', $list, $in{'mode'}); my $mode = $in{'mode'}; my $notify = !!$in{'notify'}; # Sanitize parameter: non-listmasters are allowed "close" mode only. $mode = 'close' unless Sympa::is_listmaster($list, $param->{'user'}{'email'}); $mode = 'close' unless $mode and grep { $mode eq $_ } qw(close install); $param->{'mode'} = $mode; $param->{'previous_action'} = $in{'previous_action'} || 'admin'; $param->{'notify'} = $notify; # Action confirmed? my $next_action = $session->confirm_action( $in{'action'}, $in{'response_action'}, arg => $list->{'name'}, previous_action => ($in{'previous_action'} || 'admin') ); return $next_action unless $next_action eq '1'; my $spindle = Sympa::Spindle::ProcessRequest->new( context => $robot, action => 'close_list', current_list => $list, mode => $mode, notify => $notify, sender => $param->{'user'}{'email'}, ( $param->{'user'}{'email'} ? (md5_check => 1) : () ), scenario_context => { sender => $param->{'user'}{'email'}, remote_host => $param->{'remote_host'}, remote_addr => $param->{'remote_addr'}, }, ); unless ($spindle and $spindle->spin) { wwslog('err', 'Cannot close list %s', $list); return $in{'previous_action'} || 'admin'; } foreach my $report (@{$spindle->{stash} || []}) { if ($report->[1] eq 'notice') { Sympa::WWW::Report::notice_report_web(@{$report}[2, 3], $param->{'action'}); } else { Sympa::WWW::Report::reject_report_web(@{$report}[1 .. 3], $param->{action}); } } unless (@{$spindle->{stash} || []}) { Sympa::WWW::Report::notice_report_web('performed', {}, $param->{'action'}); } elsif (not $spindle->success) { return $in{'previous_action'} || 'admin'; } web_db_log({'status' => 'success'}); if ($mode eq 'install') { return 'get_pending_lists'; } else { return ( Sympa::is_listmaster($list, $param->{'user'}{'email'}) ? ($in{'previous_action'} || 'admin') : Conf::get_robot_conf($robot, 'default_home') ); } } # Old name: do_restore_list(). sub do_open_list { wwslog('info', '(mode=%s)', $in{'mode'}); my $mode = $in{'mode'}; my $notify = !!$in{'notify'}; # Sanitize parameter. $mode = 'open' unless $mode and grep { $mode eq $_ } qw(open install); $param->{'mode'} = $mode; $param->{'previous_action'} = $in{'previous_action'} || 'admin'; $param->{'notify'} = $notify; # Action confirmed? my $next_action = $session->confirm_action( $in{'action'}, $in{'response_action'}, arg => join(',', $list->{'name'}, $mode), previous_action => ($in{'previous_action'} || 'admin') ); return $next_action unless $next_action eq '1'; my $spindle = Sympa::Spindle::ProcessRequest->new( context => $robot, action => 'open_list', current_list => $list, mode => $mode, notify => $notify, sender => $param->{'user'}{'email'}, ( $param->{'user'}{'email'} ? (md5_check => 1) : () ), scenario_context => { sender => $param->{'user'}{'email'}, remote_host => $param->{'remote_host'}, remote_addr => $param->{'remote_addr'}, }, ); unless ($spindle and $spindle->spin) { return $in{'previous_action'} || 'admin'; } foreach my $report (@{$spindle->{stash} || []}) { if ($report->[1] eq 'notice') { Sympa::WWW::Report::notice_report_web(@{$report}[2, 3], $param->{'action'}); } else { Sympa::WWW::Report::reject_report_web(@{$report}[1 .. 3], $param->{action}); } } unless (@{$spindle->{stash} || []}) { Sympa::WWW::Report::notice_report_web('performed', {}, $param->{'action'}); } unless ($spindle->success) { return $in{'previous_action'} || 'admin'; } web_db_log({'status' => 'success'}); if ($mode eq 'install') { return 'get_pending_lists'; } else { return $in{'previous_action'} || 'admin'; } } # Moved to Sympa::WWW::SharedDocument::_load_desc_file(). #sub get_desc_file ($file, $ligne); sub do_show_cert { return 1; } # Return true if the file in parameter can be overwrited # false if it has changes since the parameter date_epoch # DEPRECATED: No longer used. #sub synchronize; # DEPRECATED. Use Sympa::WWW::SharedDocument::get_privileges(). #sub d_access_control; # create the root shared document sub do_d_admin { wwslog('info', '(%s, %s)', $in{'list'}, $in{'d_admin'}); my $shared_doc = Sympa::WWW::SharedDocument->new($list); my %access = $shared_doc->get_privileges( mode => 'edit', sender => $param->{'user'}{'email'}, auth_method => $param->{'auth_method'}, scenario_context => { sender => $param->{'user'}{'email'}, remote_host => $param->{'remote_host'}, remote_addr => $param->{'remote_addr'} } ); unless ($access{may}{edit}) { wwslog('info', 'Permission denied for %s', $param->{'user'}{'email'}); Sympa::WWW::Report::reject_report_web('auth', $access{reason}{edit}, {}, $param->{'action'}, $list); web_db_log( { 'parameters' => '', 'status' => 'error', 'error_type' => 'authorization' } ); return undef; } if ($in{'d_admin'} eq 'create') { unless ($shared_doc->create) { wwslog('info', 'Could not create the shared %s: %m', $shared_doc); Sympa::WWW::Report::reject_report_web('intern', 'create_shared', {}, $param->{'action'}, $list, $param->{'user'}{'email'}, $robot); web_db_log( { 'parameters' => '', 'status' => 'error', 'error_type' => 'internal' } ); return undef; } return 'd_read'; } elsif ($in{'d_admin'} eq 'restore') { unless ($shared_doc->restore) { wwslog('info', 'Couldnot restore the shared %s; %m', $shared_doc); Sympa::WWW::Report::reject_report_web('intern', 'restore_shared', {}, $param->{'action'}, $list, $param->{'user'}{'email'}, $robot); web_db_log( { 'parameters' => '', 'status' => 'error', 'error_type' => 'internal' } ); return undef; } web_db_log( { 'parameters' => $in{'path'}, 'status' => 'success' } ); return 'd_read'; } elsif ($in{'d_admin'} eq 'delete') { $param->{'d_admin'} = $in{'d_admin'}; # Action confirmed? my $next_action = $session->confirm_action( $in{'action'}, $in{'response_action'}, arg => $in{'d_admin'} . '/' . $list->{'name'}, previous_action => 'admin' ); return $next_action unless $next_action eq '1'; unless ($shared_doc->delete) { wwslog('info', 'Couldnot delete the shared %s: %m', $shared_doc); Sympa::WWW::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'; } # Moved. Use Sympa::WWW::SharedDocument::by_order(). #sub by_order; #******************************************* # Function : do_d_read # Description : reads a file or a directory #****************************************** ## ## Function do_d_read sub do_d_read { wwslog('info', '(%s)', $in{'path'}); my $path = $in{'path'}; # Is list open ? unless ($list->{'admin'}{'status'} eq 'open') { Sympa::WWW::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; } my $shared_doc = Sympa::WWW::SharedDocument->new($list, $path); # Document exists ? unless ($shared_doc and -r $shared_doc->{fs_path}) { wwslog('err', 'Unable to read %s: no such file or directory', $shared_doc); Sympa::WWW::Report::reject_report_web('user', 'no_such_document', {'path' => $path}, $param->{'action'}, $list); web_db_log( { 'parameters' => $in{'path'}, 'status' => 'error', 'error_type' => 'internal' } ); return undef; } $param->{'shared_doc'} = $shared_doc->as_hashref; # Access control. my %access = $shared_doc->get_privileges( mode => 'read,edit,control', sender => $param->{'user'}{'email'}, auth_method => $param->{'auth_method'}, scenario_context => { sender => $param->{'user'}{'email'}, remote_host => $param->{'remote_host'}, remote_addr => $param->{'remote_addr'} } ); my $may_read = $access{may}{read}; unless ($may_read) { Sympa::WWW::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 ($shared_doc->{type} eq 'url') { $param->{'redirect_to'} = $shared_doc->{url} if $shared_doc->{url} and $shared_doc->{url} =~ m{\Ahttps?://}i; return 1; } elsif ($shared_doc->{type} eq 'file') { $param->{'content_type'} = $shared_doc->{mime_type}; $param->{'file'} = $shared_doc->{fs_path}; $param->{'bypass'} = 1; return 1; } # 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; #} # To sort subdirs and files. my $order = $in{'order'} || 'order_by_doc'; $param->{'order_by'} = $order; my @children; if ($list->is_admin('actual_editor', $param->{'user'}{'email'})) { @children = $shared_doc->get_children(order_by => $order); } else { @children = grep { $_->{moderate} and $_->{owner} eq $param->{'user'}{'email'} or not $_->{moderate} } $shared_doc->get_children(order_by => $order); } # Empty directory? $param->{'empty'} = !scalar @children; # 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 $user = $param->{'user'}{'email'} || 'nobody'; my @children_hash = map { my $child = $_; my $child_hash = $_->as_hashref; # Case subdirectory if ($child->{type} eq 'directory') { if ($child->{scenario}) { # Check access permission for reading. my $result = Sympa::Scenario->new($list, 'd_read', name => $child->{scenario}{read})->authz( $param->{'auth_method'}, { 'sender' => $param->{'user'}{'email'}, 'remote_host' => $param->{'remote_host'}, 'remote_addr' => $param->{'remote_addr'}, } ); my $action; $action = $result->{'action'} if ref $result eq 'HASH'; if ( $user eq $child->{owner} or $may_control or $action =~ /\Ado_it\b/i) { # 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->new($list, 'd_edit', name => $child->{scenario}{edit})->authz( $param->{'auth_method'}, { 'sender' => $param->{'user'}{'email'}, 'remote_host' => $param->{'remote_host'}, 'remote_addr' => $param->{'remote_addr'}, } ); my $action_edit; $action_edit = $result->{'action'} if ref $result eq 'HASH'; $action_edit ||= ''; # may_action_edit = 0, 0.5 or 1 my $may_action_edit = ($action_edit =~ /\Ado_it\b/i) ? 1 : ($action_edit =~ /\Aeditor\b/i) ? 0.5 : 0; $may_action_edit = !($may_action_edit and $may_edit) ? 0 : ($may_action_edit == 0.5 or $may_edit == 0.5) ? 0.5 : 1; if ($may_control or $user eq $child->{owner}) { $child_hash->{may_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) { # $may_action_edit = 0.5 or 1 $child_hash->{may_edit} = $may_action_edit; # If index.html, must know if something can be # edit in the dir. $normal_mode = 1; } } if ($may_control or $user eq $child->{owner}) { $child_hash->{may_control} = 1; } } } else { # No description file = no need to check access for read # access for edit and control if ($may_control) { $child_hash->{may_edit} = 1; # ...or = $may_action_edit ? $normal_mode = 1; } elsif ($may_edit) { # $may_action_edit = 1 or 0.5 $child_hash->{may_edit} = $may_edit; $normal_mode = 1; } if ($may_control) { $child_hash->{may_control} = 1; } } } else { # case file my $may = 1; my $def_desc = 0; if ($child->{scenario}) { # a desc file was found $def_desc = 1; my $result = Sympa::Scenario->new($list, 'd_read', name => $child->{scenario}{read})->authz( $param->{'auth_method'}, { 'sender' => $param->{'user'}{'email'}, 'remote_host' => $param->{'remote_host'}, 'remote_addr' => $param->{'remote_addr'}, } ); my $action; $action = $result->{'action'} if ref $result eq 'HASH'; unless ($user eq $child->{owner} or $may_control or $action =~ /\Ado_it\b/i) { $may = 0; } } # If permission or no description file. if ($may) { # Exception of index.html. if ($child->{name} =~ /\Aindex[.]html?\z/i) { $indexhtml = $child->{name}; } ## 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->new($list, 'd_edit', name => $child->{scenario}{edit})->authz( $param->{'auth_method'}, { 'sender' => $param->{'user'}{'email'}, 'remote_host' => $param->{'remote_host'}, 'remote_addr' => $param->{'remote_addr'}, } ); my $action_edit; $action_edit = $result->{'action'} if ref $result eq 'HASH'; $action_edit ||= ''; # may_action_edit = 0, 0.5 or 1 my $may_action_edit = ($action_edit =~ /\Ado_it\b/i) ? 1 : ($action_edit =~ /\Aeditor\b/i) ? 0.5 : 0; $may_action_edit = !($may_action_edit and $may_edit) ? 0 : ($may_action_edit == 0.5 or $may_edit == 0.5) ? 0.5 : 1; if ($may_control or $user eq $child->{owner}) { $normal_mode = 1; $child_hash->{may_edit} = 1; # ...or = $may_action_edit ? } elsif ($may_action_edit) { # $may_action_edit = 1 or 0.5 $normal_mode = 1; $child_hash->{may_edit} = $may_action_edit; } if ($user eq $child->{owner} or $may_control) { $child_hash->{may_control} = 1; } } else { if ($may_edit) { $child_hash->{may_edit} = $may_edit; $normal_mode = 1; } if ($may_control) { $child_hash->{may_control} = 1; } } } } } $child_hash; } @children; # map {...} # Exception : index.html if ($indexhtml) { unless ($normal_mode) { $param->{'content_type'} = 'text/html'; $param->{'bypass'} = 1; $param->{'file'} = $shared_doc->{fs_path} . '/' . $indexhtml; return 1; } } # parameters for the template file $param->{'list'} = $list->{'name'}; $param->{'shared_doc'}{'may_edit'} = $may_edit; $param->{'shared_doc'}{'may_control'} = $may_control; $param->{'shared_doc'}{'children'} = \@children_hash if @children_hash; # Show expert commands / user page. # For the curent directory. unless ($may_edit or $may_control) { $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'} and $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 => $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 => $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( { '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::WWW::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; } my $shared_doc = Sympa::WWW::SharedDocument->new($list); # Shared exist? unless ($shared_doc and -r $shared_doc->{fs_path}) { wwslog('err', 'Unable to read %s: no such file or directory', $shared_doc); Sympa::WWW::Report::reject_report_web('user', 'no_shared', {}, $param->{'action'}, $list); return undef; } $param->{'shared_doc'} = $shared_doc->as_hashref; # Access control. my %access = $shared_doc->get_privileges( mode => 'read,control', sender => $param->{'user'}{'email'}, auth_method => $param->{'auth_method'}, scenario_context => { sender => $param->{'user'}{'email'}, remote_host => $param->{'remote_host'}, remote_addr => $param->{'remote_addr'} } ); unless ($access{may}{read}) { Sympa::WWW::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::WWW::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 @children = sort { $b->{'date_epoch'} <=> $a->{'date_epoch'} } _latest_d_read($shared_doc, $oldest_day, $access{may}{control}); $param->{'shared_doc'}{'children'} = [map { $_->as_hashref } splice @children, 0, $nb_doc]; return 1; } # Browse a directory recursively and return documents younger than # $oldest_day. # Old name: directory_browsing() in wwsympa.fcgi. sub _latest_d_read { wwslog('debug2', '(%s, %s, %s)', @_); my $shared_doc = shift; my $oldest_day = shift; my $may_control = shift; my @result; my $user = $param->{'user'}{'email'} || 'nobody'; foreach my $child ($shared_doc->get_children) { if ($child->{type} eq 'directory') { if ($child->{scenario}) { # Check access permission for reading. my $result = Sympa::Scenario->new($list, 'd_read', name => $child->{scenario}{read})->authz( $param->{'auth_method'}, { 'sender' => $param->{'user'}{'email'}, 'remote_host' => $param->{'remote_host'}, 'remote_addr' => $param->{'remote_addr'}, } ); my $action = $result->{'action'} if ref $result eq 'HASH'; $action ||= ''; if ( $user eq $child->{owner} or $may_control or $action =~ /\Ado_it\b/i) { push @result, _latest_d_read($child, $oldest_day); } } } else { next if $child->{date_epoch} < $oldest_day; # Exception of index.html. next if $child->{name} =~ /\Aindex[.]html?\z/i; my $may = 1; if ($child->{scenario}) { my $result = Sympa::Scenario->new($list, 'd_read', name => $child->{scenario}{read})->authz( $param->{'auth_method'}, { 'sender' => $param->{'user'}{'email'}, 'remote_host' => $param->{'remote_host'}, 'remote_addr' => $param->{'remote_addr'}, } ); my $action = $result->{'action'} if ref $result eq 'HASH'; $action ||= ''; unless ($user eq $child->{owner} or $may_control or $action =~ /\Ado_it\b/i) { $may = 0; } } push @result, $child if $may; } } return @result; } #******************************************* # Function : do_d_editfile # Description : prepares the parameters to # edit a file #******************************************* sub do_d_editfile { wwslog('info', '(%s)', $in{'path'}); my $path = $in{'path'}; # Is list open? unless ($list->{'admin'}{'status'} eq 'open') { Sympa::WWW::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; } my $shared_doc = Sympa::WWW::SharedDocument->new($list, $path, allow_empty => 1); # Existing document? File? unless ($shared_doc and -r $shared_doc->{fs_path} and -w $shared_doc->{fs_path} and not(grep { $shared_doc->{type} eq $_ } qw(root directory))) { wwslog('err', 'Unable to read %s: no such file or directory', $path); Sympa::WWW::Report::reject_report_web('user', 'no_such_document', {'path' => $path}, $param->{'action'}, $list); web_db_log( { 'parameters' => $in{'path'}, 'status' => 'error', 'error_type' => 'internal' } ); return undef; } $param->{'shared_doc'} = $shared_doc->as_hashref; # Access control. my %access = $shared_doc->get_privileges( mode => 'edit,control', sender => $param->{'user'}{'email'}, auth_method => $param->{'auth_method'}, scenario_context => { sender => $param->{'user'}{'email'}, remote_host => $param->{'remote_host'}, remote_addr => $param->{'remote_addr'} } ); unless ($access{may}{edit}) { Sympa::WWW::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->{'shared_doc'}{'may_edit'} = $access{may}{edit}; $param->{'shared_doc'}{'may_control'} = $access{may}{control}; # Test if it's a text file. if (-T $shared_doc->{fs_path}) { #FIXME:Better check $param->{'textfile'} = 1; if (open my $fh, '<', $shared_doc->{fs_path}) { $param->{'shared_doc'}{'content'} = do { local $RS; <$fh> }; close $fh; } } else { $param->{'textfile'} = 0; } 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'}); my $path = $in{'path'}; my $shared_doc = Sympa::WWW::SharedDocument->new($list, $path); # Existing document? File? unless ($shared_doc and -r $shared_doc->{fs_path} and -w $shared_doc->{fs_path} and $shared_doc->{type} ne 'root') { wwslog('err', '%s: no such file or directory', $path); Sympa::WWW::Report::reject_report_web('user', 'no_such_document', {'path' => $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; } $param->{'shared_doc'} = $shared_doc->as_hashref; # Access control. my %access = $shared_doc->get_privileges( mode => 'edit,control', sender => $param->{'user'}{'email'}, auth_method => $param->{'auth_method'}, scenario_context => { sender => $param->{'user'}{'email'}, remote_host => $param->{'remote_host'}, remote_addr => $param->{'remote_addr'} } ); unless ($access{may}{edit}) { Sympa::WWW::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; } $param->{'list'} = $list->{'name'}; $param->{'shared_doc'}{'may_edit'} = $access{may}{edit}; $param->{'shared_doc'}{'may_control'} = $access{may}{control}; ##FIXME: Required? #$allow_absolute_path = 1; 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, %s)', $in{'path'}, $in{'content'}); my $path = $in{'path'}; my $shared_doc = Sympa::WWW::SharedDocument->new($list, $path); # The description file of repository root doesn't exist. unless ($shared_doc and -r $shared_doc->{fs_path} and $shared_doc->{type} ne 'root') { Sympa::WWW::Report::reject_report_web('user', 'no_doc_to_describe', {'path' => $path}, $param->{'action'}, $list, $param->{'user'}{'email'}, $robot); wwslog('info', 'Cannot describe %s', $path); web_db_log( { 'parameters' => $in{'path'}, 'status' => 'error', 'error_type' => 'no_file' } ); return undef; } $param->{shared_doc} = $shared_doc->as_hashref; # Access control. my %access = $shared_doc->get_privileges( mode => 'edit', sender => $param->{'user'}{'email'}, auth_method => $param->{'auth_method'}, scenario_context => { sender => $param->{'user'}{'email'}, remote_host => $param->{'remote_host'}, remote_addr => $param->{'remote_addr'} } ); unless ($access{may}{edit}) { Sympa::WWW::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 (defined $in{'content'} and $in{'content'} =~ /\S/) { $shared_doc->{title} = $in{'content'}; if (exists $shared_doc->{serial_desc} and defined $shared_doc->{serial_desc}) { # If description file already exists: Open it and modify it. # Synchronization unless ($shared_doc->{serial_desc} == $in{'serial'}) { Sympa::WWW::Report::reject_report_web('user', 'synchro_failed', {}, $param->{'action'}, $list); wwslog('info', 'Synchronization failed for description of %s', $shared_doc); web_db_log( { 'parameters' => $in{'path'}, 'status' => 'error', 'error_type' => 'internal' } ); return undef; } } else { $shared_doc->{scenario} = $access{scenario}; } # Fill the description file. unless ($shared_doc->save_description) { wwslog('info', 'Cannot save description of %s: %s', $shared_doc, $ERRNO); Sympa::WWW::Report::reject_report_web('intern', 'cannot_open_file', {'path' => $path}, $param->{'action'}, $list, $param->{'user'}{'email'}, $robot); web_db_log( { 'parameters' => $in{'path'}, 'status' => 'error', 'error_type' => 'internal' } ); return undef; } $in{'path'} = join '/', @{$shared_doc->{parent}->{paths}}; } web_db_log( { 'parameters' => $in{'path'}, 'status' => 'success' } ); return 'd_read'; } #******************************************* # Function : do_d_update # Description : Overwrites existing file. #****************************************** # Old names: do_d_savefile() and do_d_overwrite(). sub do_d_update { wwslog('info', '(%s, %s)', $in{'path'}, $in{'type'}); my $path = $in{'path'}; my $type = $in{'type'} || 'file'; my $content; if ($type eq 'upload') { # Parameters of the uploaded file. my $fh = $query->upload('uploaded_file'); if (defined $fh) { my $ioh = $fh->handle; $content = do { local $RS; <$ioh> }; } } elsif ($type eq 'url') { $content = sprintf "%s\n", $in{'url'} if $in{'url'}; } else { $content = $in{'content'}; } unless (defined $content and ($type eq 'upload' and length $content or $content =~ /\S/)) { Sympa::WWW::Report::reject_report_web('user', 'no_content', {}, $param->{'action'}, $list); wwslog('err', 'Cannot save file %s: no content', $path); web_db_log( { 'parameters' => $in{'path'}, 'status' => 'error', 'error_type' => 'missing_parameter' } ); return undef; } my $shared_doc = Sympa::WWW::SharedDocument->new($list, $path, allow_empty => 1); # Existing document? File? unless ($shared_doc and -r $shared_doc->{fs_path} and -w $shared_doc->{fs_path} and not(grep { $shared_doc->{type} eq $_ } qw(root directory))) { wwslog('err', 'Unable to read %s: no such file or directory', $path); Sympa::WWW::Report::reject_report_web('user', 'no_such_document', {'path' => $path}, $param->{'action'}, $list); web_db_log( { 'parameters' => $in{'path'}, 'status' => 'error', 'error_type' => 'internal' } ); return undef; } $param->{shared_doc} = $shared_doc->as_hashref; # Access control. my %access = $shared_doc->get_privileges( mode => 'edit', sender => $param->{'user'}{'email'}, auth_method => $param->{'auth_method'}, scenario_context => { sender => $param->{'user'}{'email'}, remote_host => $param->{'remote_host'}, remote_addr => $param->{'remote_addr'} } ); unless ($access{may}{edit}) { Sympa::WWW::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; } # Synchronization unless ($type eq 'url') { # Only for files. unless ($shared_doc->{date_epoch} == $in{'serial'}) { Sympa::WWW::Report::reject_report_web('user', 'synchro_failed', {}, $param->{'action'}, $list); wwslog('err', 'Synchronization failed for %s', $shared_doc); web_db_log( { 'parameters' => $in{'path'}, 'status' => 'error', 'error_type' => 'internal' } ); return undef; } } # Renaming of the old file # Isn't url ? rename $shared_doc->{fs_path}, $shared_doc->{fs_path} . '.old'; # Creation of the shared file my $ofh; unless (open $ofh, '>', $shared_doc->{fs_path}) { my $errno = $ERRNO; rename $shared_doc->{fs_path} . '.old', $shared_doc->{fs_path}; Sympa::WWW::Report::reject_report_web( 'user', 'cannot_overwrite', { 'reason' => $errno, 'path' => $path }, $param->{'action'}, $list ); wwslog('err', 'Cannot open for replace %s: %s', $shared_doc, $errno); web_db_log( { 'parameters' => $in{'path'}, 'status' => 'error', 'error_type' => 'internal' } ); return undef; } print $ofh $content; close $ofh; unlink $shared_doc->{fs_path} . '.old'; $shared_doc->{scenario} ||= $access{scenario}; $shared_doc->{owner} = $param->{'user'}{'email'}; $shared_doc->{date_epoch} = Sympa::Tools::File::get_mtime($shared_doc->{fs_path}); $shared_doc->save_description; $in{'list'} = $list->{'name'}; Sympa::WWW::Report::notice_report_web('save_success', {'path' => $path}, $param->{'action'}); web_db_log( { 'parameters' => $in{'path'}, 'status' => 'success' } ); if ($in{'previous_action'}) { return $in{'previous_action'}; } else { $in{'path'} = $param->{'path'} = join '/', @{$shared_doc->{parent}->{paths}}; return 'd_read'; } } # Merged to do_d_update(). #sub do_d_overwrite; # Merged to do_d_create_child(). #sub do_d_upload; ## 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::WWW::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"; } # No longer used (subroutine of deprecated do_d_upload()). #sub creation_shared_file; # No longer used (subroutine of deprecated do_d_upload()). #sub creation_desc_file; #******************************************* # Function : do_d_unzip # Description : unzip a file or a tree structure # from an uploaded zip file #****************************************** sub do_d_unzip { wwslog('info', '(%s, %s)', $in{'path'}); my $path = $in{'path'}; my $zip_name; my $fn = $in{'uploaded_file'}; if (defined $fn) { # Guess client charset. $zip_name = Sympa::Tools::Text::guessed_to_utf8($fn, Sympa::Language::implicated_langs($language->get_lang)); # Name without path. $zip_name = $1 if $zip_name =~ /([^\/\\]+)$/; } unless ($zip_name and $zip_name =~ /.+[.]zip\z/i) { Sympa::WWW::Report::reject_report_web( 'user', 'incorrect_name', { 'name' => $zip_name, 'reason' => "must have the '.zip' extension" }, $param->{'action'}, $list ); wwslog('err', '(%s, %s) The file must have ".zip" extension', $path, $zip_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; } my $shared_doc = Sympa::WWW::SharedDocument->new($list, $path); # The file must be uploaded in a directory existing. unless ($shared_doc and -r $shared_doc->{fs_path} and -w $shared_doc->{fs_path} and grep { $shared_doc->{type} eq $_ } qw(root directory)) { Sympa::WWW::Report::reject_report_web('user', 'no_such_document', {'path' => $path}, $param->{'action'}, $list); wwslog('err', '%s: Not a directory', $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; } $param->{shared_doc} = $shared_doc->as_hashref; # Access control for the directory where there is the uploading # only for (is_author || !moderated) my %access = $shared_doc->get_privileges( mode => 'edit', sender => $param->{'user'}{'email'}, auth_method => $param->{'auth_method'}, scenario_context => { sender => $param->{'user'}{'email'}, remote_host => $param->{'remote_host'}, remote_addr => $param->{'remote_addr'} } ); unless ($access{may}{edit} and $access{may}{edit} == 1) { Sympa::WWW::Report::reject_report_web('auth', ($access{reason}{edit} || '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{'path'}", 'target_email' => "", 'msg_id' => '', 'status' => 'error', 'error_type' => 'authorization', 'user_email' => $param->{'user'}{'email'}, } ); return undef; } # Check quota. if ($list->{'admin'}{'shared_doc'}{'quota'}) { if (Sympa::WWW::SharedDocument->new($list)->get_size >= $list->{'admin'}{'shared_doc'}{'quota'} * 1024) { Sympa::WWW::Report::reject_report_web('user', 'shared_full', {}, $param->{'action'}, $list); wwslog('err', 'Shared Quota exceeded for list %s', $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; } } # Uploaded of the file.zip my ($zip, $az); my $fh = $query->upload('uploaded_file'); if (defined $fh) { my $ioh = $fh->handle; # The handle must know seek() and so on in addition to opened(). # CGI derives handles from IO::Handle and/or File::Temp which lack # some of methods. That's why destructive bless-ing is here. bless $ioh => 'IO::File'; $zip = Archive::Zip->new(); $az = $zip->readFromFileHandle($ioh); } unless (defined $az and $az == Archive::Zip::AZ_OK()) { Sympa::WWW::Report::reject_report_web('intern', 'cannot_unzip', {name => $zip_name}, $param->{'action'}, $list, $param->{'user'}{'email'}, $robot); wwslog('err', 'Unable to read the zip file: %s', $az); 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 $status = 1; my %subpaths; my @langs = Sympa::Language::implicated_langs($language->get_lang); foreach my $member ($zip->members) { next if $member->isEncrypted; my @subpaths = split m{/+}, Sympa::Tools::Text::guessed_to_utf8($member->fileName, @langs); next unless @subpaths; my $name; unless ($member->isDirectory) { $name = pop @subpaths; $name = $language->gettext('New file') unless Sympa::WWW::SharedDocument::valid_name($name); } foreach my $p (@subpaths) { $p = $language->gettext('New directory') unless Sympa::WWW::SharedDocument::valid_name($p); } unless ($member->isDirectory) { push @subpaths, $name; } # Does file alreay exist? if (Sympa::WWW::SharedDocument->new( $list, [@{$shared_doc->{paths}}, @subpaths] ) ) { Sympa::WWW::Report::reject_report_web('user', 'doc_already_exist', {'name' => join('/', @subpaths)}, $param->{'action'}, $list); wwslog( 'err', 'Can\'t create %s: file already exists', join('/', @subpaths) ); web_db_log( { 'robot' => $robot, 'list' => $list->{'name'}, 'action' => $param->{'action'}, 'parameters' => join('/', @subpaths), 'target_email' => "", 'msg_id' => '', 'status' => 'error', 'error_type' => 'file_already_exists', 'user_email' => $param->{'user'}{'email'}, } ); return undef; } $subpaths{$member->fileName} = [@subpaths]; } foreach my $member ($zip->members) { next if $member->isEncrypted; my $subpaths = $subpaths{$member->fileName}; next unless $subpaths and @$subpaths; my ($content, $az); unless ($member->isDirectory) { ($content, $az) = $member->contents; unless (defined $az and $az == Archive::Zip::AZ_OK()) { wwslog('err', 'Unable to extract member %s of the zip file: %s', $member->fileName, $az); web_db_log( { 'robot' => $robot, 'list' => $list->{'name'}, 'action' => $param->{'action'}, 'parameters' => $member->fileName, 'target_email' => "", 'msg_id' => '', 'status' => 'error', 'error_type' => 'internal', 'user_email' => $param->{'user'}{'email'}, } ); $status = 0; next; } } unless ( _d_create_descendant( $shared_doc, $subpaths, owner => $param->{'user'}{'email'}, scenario => $access{scenario}, type => ($member->isDirectory ? 'directory' : 'file'), ($member->isDirectory ? () : (content => $content)) ) ) { wwslog('err', 'Unable to create member %s of the zip file as %s: %s', $member->fileName, join('/', @$subpaths)); web_db_log( { 'robot' => $robot, 'list' => $list->{'name'}, 'action' => $param->{'action'}, 'parameters' => $member->fileName, 'target_email' => "", 'msg_id' => '', 'status' => 'error', 'error_type' => 'internal', 'user_email' => $param->{'user'}{'email'}, } ); $status = 0; } } unless ($status) { Sympa::WWW::Report::reject_report_web('intern', 'cannot_unzip', {name => $zip_name}, $param->{'action'}, $list, $param->{'user'}{'email'}, $robot); } $in{'list'} = $list->{'name'}; Sympa::WWW::Report::notice_report_web('unzip_success', {'path' => $zip_name}, $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'; } sub _d_create_descendant { my $shared_doc = shift; my $subpaths = shift; my %opts = @_; return $shared_doc unless @$subpaths; my $parent_subpaths = [@$subpaths]; my $new_name = pop @$parent_subpaths; my $parent = _d_create_descendant($shared_doc, $parent_subpaths, %opts, type => 'directory'); return undef unless $parent; my ($child) = $parent->get_children(name => $new_name); if ($child) { if ($opts{type} eq 'file') { # Duplicate file: Add a suffix (2), (3), ... my ($g, $alt_name); for ($g = 2; $child; $g++) { $alt_name = $new_name; $alt_name =~ s/((?:[.]\w+)+)\z/ ($g)$1/ or $alt_name = "$new_name ($g)"; ($child) = $parent->get_children(name => $alt_name); } $new_name = $alt_name; } elsif ($child->{type} ne 'directory') { # Non-directory with the same name: Add a suffix (2), (3), ... my ($g, $alt_name); for ($g = 2; $child && $child->{type} ne 'directory'; $g++) { $alt_name = "$new_name ($g)"; ($child) = $parent->get_children(name => $alt_name); } return $child if $child; $new_name = $alt_name; } else { # Directory already exists. return $child; } } return $parent->create_child($new_name, %opts); } # Unzip a shared file in the tmp directory. # No longer used. #sub d_unzip_shared_file; ## Install file hierarchy from $tmp_dir directory to $shareddir/$path ## directory # No longer used. #sub d_install_file_hierarchy; ## copy $dname from $from to $list->{shared}/$path if rights are ok # No longer used. #sub d_copy_rec_dir; ## copy $from/$fname to $list->{shared}/$path if rights are ok # No longer used. #sub d_copy_file; ## return information on file or dir : existing and edit rights for the user ## in $param # No longer used. #sub d_test_existing_and_rights; #******************************************* # Function : do_d_delete # Description : Delete an existing document # (file or directory) #****************************************** sub do_d_delete { wwslog('info', '(%s)', $in{'path'}); my $path = $in{'path'}; my $shared_doc = Sympa::WWW::SharedDocument->new($list, $path); # Document exists? unless ($shared_doc and -r $shared_doc->{fs_path} and $shared_doc->{type} ne 'root') { wwslog('err', '%s: no such file or directory', $path); Sympa::WWW::Report::reject_report_web('user', 'no_such_document', {'path' => $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; } $param->{'shared_doc'} = $shared_doc->as_hashref; # Access control. my %access; if ($shared_doc) { %access = $shared_doc->get_privileges( mode => 'edit', sender => $param->{'user'}{'email'}, auth_method => $param->{'auth_method'}, scenario_context => { sender => $param->{'user'}{'email'}, remote_host => $param->{'remote_host'}, remote_addr => $param->{'remote_addr'} } ); } unless ($access{may}{edit}) { Sympa::WWW::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; } # End of control # Action confirmed? my $next_action = $session->confirm_action( $in{'action'}, $in{'response_action'}, arg => join('/', @{$shared_doc->{paths}}), previous_action => ($in{'previous_action'} || 'd_read') ); return $next_action unless $next_action eq '1'; if ($shared_doc->{type} eq 'directory') { # Directory. unless ($shared_doc->rmdir) { my $errno = $ERRNO; Sympa::WWW::Report::reject_report_web('intern', 'erase_file', {'file' => $path}, $param->{'action'}, $list, $param->{'user'}{'email'}, $robot); wwslog('err', 'Failed to erase %s: %s', $shared_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; } } else { # Removing of the document. unless ($shared_doc->unlink) { my $errno = $ERRNO; Sympa::WWW::Report::reject_report_web('intern', 'erase_file', {'file' => $path}, $param->{'action'}, $list, $param->{'user'}{'email'}, $robot); wwslog('err', 'Failed to erase %s: %s', $shared_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; } } 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'} = join '/', @{$shared_doc->{parent}->{paths}}; return 'd_read'; } #******************************************* # Function : do_d_rename # Description : Rename a document # (file or directory) #****************************************** sub do_d_rename { wwslog('info', '(%s, %s)', $in{'path'}, $in{'new_name'}); my $path = $in{'path'}; my $shared_doc = Sympa::WWW::SharedDocument->new($list, $path); # Document exists? unless ($shared_doc and -e $shared_doc->{fs_path}) { wwslog('err', '%s: no such file or directory', $path); Sympa::WWW::Report::reject_report_web('user', 'no_such_document', {'path' => $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; } $param->{'shared_doc'} = $shared_doc->as_hashref; # Access control. my %access = $shared_doc->get_privileges( mode => 'edit', sender => $param->{'user'}{'email'}, auth_method => $param->{'auth_method'}, scenario_context => { sender => $param->{'user'}{'email'}, remote_host => $param->{'remote_host'}, remote_addr => $param->{'remote_addr'} } ); unless ($access{may}{edit}) { Sympa::WWW::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; } unless ($shared_doc->rename($in{'new_name'})) { my $errno = $ERRNO; Sympa::WWW::Report::reject_report_web( 'intern', 'rename_file', { 'old' => join('/', $shared_doc->{paths}), 'new' => $in{'new_name'} }, $param->{'action'}, $list, $param->{'user'}{'email'}, $robot ); wwslog('err', 'Failed to rename %s to %s: %s', $shared_doc, $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; } 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'}; $in{'path'} = join '/', @{$shared_doc->{parent}->{paths}}; return 'd_read'; } #******************************************* # Function : do_d_create_child # Description : Creates a new file / directory #****************************************** # Old names: do_d_create_dir() and do_d_upload(). sub do_d_create_child { wwslog('info', '(%s, %s, %s)', $in{'path'}, $in{'new_name'}, $in{'type'}); my $path = $in{'path'}; my $new_name = $in{'new_name'}; my $type = $in{'type'} || 'directory'; my $content; if ($type eq 'upload') { my $fh = $query->upload('uploaded_file'); if (defined $fh) { my $ioh = $fh->handle; $content = do { local $RS; <$ioh> }; } my $fn = $query->upload('uploaded_file'); if (defined $fn) { # Guess client encoding. $new_name = Sympa::Tools::Text::guessed_to_utf8($fn, Sympa::Language::implicated_langs($language->get_lang)); # Name without path. $new_name = $1 if $new_name =~ m{([^/\\]+)\z}; # Avoid invalid names. $new_name = $language->gettext('New file') unless Sympa::WWW::SharedDocument::valid_name($new_name); } } elsif ($type eq 'url') { $content = sprintf "%s\n", $in{'url'} if $in{'url'}; $new_name = $language->gettext('New bookmark') unless Sympa::WWW::SharedDocument::valid_name($new_name); $new_name = $new_name . '.url'; } wwslog('info', '(%s, %s, %s)', $path, $new_name, $type); $param->{'list'} = $list->{'name'}; my $shared_doc = Sympa::WWW::SharedDocument->new($list, $path); unless ($shared_doc and -r $shared_doc->{fs_path} and -w $shared_doc->{fs_path} and grep { $shared_doc->{type} eq $_ } qw(root directory)) { wwslog('err', 'Unable to read %s: no such directory', $path); Sympa::WWW::Report::reject_report_web('user', 'no_such_document', {'path' => $path}, $param->{'action'}, $list); web_db_log( { 'parameters' => $in{'path'}, 'status' => 'error', 'error_type' => 'internal' } ); return undef; } $param->{shared_doc} = $shared_doc->as_hashref; # Access control. my %access = $shared_doc->get_privileges( mode => 'edit,control', sender => $param->{'user'}{'email'}, auth_method => $param->{'auth_method'}, scenario_context => { sender => $param->{'user'}{'email'}, remote_host => $param->{'remote_host'}, remote_addr => $param->{'remote_addr'} } ); if ($type eq 'directory') { # only when (is_author or !moderated) unless ($access{may}{edit}) { Sympa::WWW::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{'new_name'}", 'target_email' => "", 'msg_id' => '', 'status' => 'error', 'error_type' => 'authorization', 'user_email' => $param->{'user'}{'email'}, } ); return undef; } if ($access{may}{edit} == 0.5) { Sympa::WWW::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{'new_name'}", 'target_email' => "", 'msg_id' => '', 'status' => 'error', 'error_type' => 'authorization', 'user_email' => $param->{'user'}{'email'}, } ); return undef; } } else { unless ($access{may}{edit}) { Sympa::WWW::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{'new_name'}", 'target_email' => "", 'msg_id' => '', 'status' => 'error', 'error_type' => 'authorization', 'user_email' => $param->{'user'}{'email'}, } ); return undef; } # Exception for index.html. if ( $type eq 'upload' and $new_name =~ /\Aindex[.]html?\z/i and not $access{may}{control}) { Sympa::WWW::Report::reject_report_web('user', 'index_html', {dir => $path, reason => 'd_access_control'}, $param->{'action'}, $list); wwslog('err', 'Not authorized to upload a INDEX.HTML file in %s', $path); web_db_log( { 'robot' => $robot, 'list' => $list->{'name'}, 'action' => $param->{'action'}, 'parameters' => "$path,$new_name", 'target_email' => "", 'msg_id' => '', 'status' => 'error', 'error_type' => 'authorization', 'user_email' => $param->{'user'}{'email'}, } ); return undef; } } my ($child) = $shared_doc->get_children(name => $new_name); # The file mustn't already exist except if: # - it is uploaded, # - it is moderated and its author can erase it. if ($child) { if ($type eq 'upload') { # Add a suffix (2), (3), ... my ($g, $alt_name); for ($g = 2; $child; $g++) { $alt_name = $new_name; $alt_name =~ s/((?:[.]\w+)+)\z/ ($g)$1/ or $alt_name = "$new_name ($g)"; $child = $shared_doc->get_children(name => $alt_name); } $new_name = $alt_name; } elsif (not $child->{moderate} or $child->{owner} ne $param->{'user'}{'email'}) { Sympa::WWW::Report::reject_report_web('user', 'doc_already_exist', {'name' => $path . '/' . $new_name}, $param->{'action'}, $list); wwslog('err', 'Can\'t create %s/%s: file already exists', $path, $new_name); web_db_log( { 'robot' => $robot, 'list' => $list->{'name'}, 'action' => $param->{'action'}, 'parameters' => "$in{'new_name'}", 'target_email' => "", 'msg_id' => '', 'status' => 'error', 'error_type' => 'file_already_exists', 'user_email' => $param->{'user'}{'email'}, } ); return undef; } } # Check quota. if ($type eq 'upload' #FIXME:Check in other cases too. and $list->{'admin'}{'shared_doc'}{'quota'} and Sympa::WWW::SharedDocument->new($list)->get_size >= $list->{'admin'}{'shared_doc'}{'quota'} * 1024 ) { Sympa::WWW::Report::reject_report_web('user', 'shared_full', {}, $param->{'action'}, $list); wwslog('err', 'Shared Quota exceeded for list %s', $list); web_db_log( { 'robot' => $robot, 'list' => $list->{'name'}, 'action' => $param->{'action'}, 'parameters' => "$path,$new_name", 'target_email' => "", 'msg_id' => '', 'status' => 'error', 'error_type' => 'shared_full', 'user_email' => $param->{'user'}{'email'}, } ); return undef; } # XSS Protection for HTML files. if ($type eq 'upload' #FIXME:Check in other cases too. and $new_name =~ /[.]html?\z/i ) { my $sanitized_html = Sympa::HTMLSanitizer->new($robot)->sanitize_html($content); if (defined $sanitized_html) { $content = $sanitized_html; } else { $log->syslog('err', 'Unable to sanitize file %s', $new_name); } } my $new_child = $shared_doc->create_child( $new_name, type => ($type eq 'upload' ? 'file' : $type), moderate => ($access{may}{edit} == 0.5 && $type ne 'directory'), owner => $param->{'user'}{'email'}, scenario => $access{'scenario'}, (($type eq 'upload' or $type eq 'url') ? (content => $content) : ()) ); unless ($new_child) { my $errno = $ERRNO; my $error_type; if ($errno == POSIX::EINVAL()) { # The name of the directory must be correct Sympa::WWW::Report::reject_report_web('user', 'incorrect_name', {'name' => $new_name}, $param->{'action'}, $list); $error_type = 'bad_parameter'; } else { Sympa::WWW::Report::reject_report_web('intern', 'cannot_create_child', {'name' => $new_name}, $param->{'action'}, $list); $error_type = 'intern'; } wwslog('err', 'Unable to create directory %s: %s', $new_name, $errno); web_db_log( { 'robot' => $robot, 'list' => $list->{'name'}, 'action' => $param->{'action'}, 'parameters' => "$in{'new_name'}", 'target_email' => "", 'msg_id' => '', 'status' => 'error', 'error_type' => $error_type, 'user_email' => $param->{'user'}{'email'}, } ); return undef; } # Moderation if ($access{may}{edit} == 0.5 and $type ne 'directory') { unless ($child and $child->{moderate}) { # Moderated at first time my @rcpt = $list->get_admins_email('receptive_editor'); @rcpt = $list->get_admins_email('actual_editor') unless @rcpt; unless (@rcpt) { # Since shared document has already been marked moderated, # notification to editors should not fail. Fallback to # listmasters. $log->syslog( 'notice', 'No editor and owner defined at all in list %s; notification is sent to listmasters', $list ); @rcpt = Sympa::get_listmasters_email($list); } Sympa::send_file( $list, 'shared_moderate', \@rcpt, { auto_submitted => 'auto-generated', filename => join('/', @{$new_child->{paths}}), who => $param->{'user'}{'email'}, } ); } } web_db_log( { 'robot' => $robot, 'list' => $list->{'name'}, 'action' => $param->{'action'}, 'parameters' => "$in{'new_name'}", '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'); } elsif ($type eq 'upload') { web_db_stat_log( operation => 'd_upload', parameter => length $content ); } else { web_db_stat_log(operation => 'd_create_file'); } if ($type eq 'file') { $in{'path'} = join '/', @{$new_child->{paths}}; return 'd_editfile'; } else { return 'd_read'; } } ############## Control #******************************************* # Function : do_d_control # Description : prepares the parameters # to edit access for a doc #******************************************* sub do_d_control { wwslog('info', '%s', $in{'path'}); my $path = $in{'path'}; my $shared_doc = Sympa::WWW::SharedDocument->new($list, $path); # Existing document? unless ($shared_doc and -r $shared_doc->{fs_path} and $shared_doc->{type} ne 'root') { wwslog('err', '%s: no such file or directory', $path); Sympa::WWW::Report::reject_report_web('user', 'no_such_document', {'path' => $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; } $param->{'shared_doc'} = $shared_doc->as_hashref; # Access control. my %access = $shared_doc->get_privileges( mode => 'edit,control', sender => $param->{'user'}{'email'}, auth_method => $param->{'auth_method'}, scenario_context => { sender => $param->{'user'}{'email'}, remote_host => $param->{'remote_host'}, remote_addr => $param->{'remote_addr'} } ); unless ($access{may}{control}) { Sympa::WWW::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; } # Description of the file my $read; my $edit; if ($shared_doc->{scenario}) { $read = $shared_doc->{scenario}{read}; $edit = $shared_doc->{scenario}{edit}; } else { $read = $access{'scenario'}{'read'}; $edit = $access{'scenario'}{'edit'}; } # template parameters $param->{'list'} = $list->{'name'}; $param->{'shared_doc'}{'may_edit'} = $access{may}{edit}; $param->{'shared_doc'}{'may_control'} = $access{may}{control}; my $lang = $param->{'lang'}; # Only get required scenario attributes. # "web_title" is for compatibility to <= 6.2.38. my $scenarios = Sympa::Scenario::get_scenarios($list, 'd_read'); $param->{'scenari_read'} = { map { my $name = $_->{name}; my $title = $_->get_current_title; ($name => {name => $name, title => $title, web_title => $title}); } @$scenarios }; $param->{'scenari_read'}{$read}{'selected'} = 'selected="selected"'; $scenarios = Sympa::Scenario::get_scenarios($list, 'd_edit'); $param->{'scenari_edit'} = { map { my $name = $_->{name}; my $title = $_->get_current_title; ($name => {name => $name, title => $title, web_title => $title}); } @$scenarios }; $param->{'scenari_edit'}{$edit}{'selected'} = 'selected="selected"'; $param->{'set_owner'} = 1; 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'}); my $path = $in{'path'}; my $shared_doc = Sympa::WWW::SharedDocument->new($list, $path); # The document to describe must already exist. unless ($shared_doc and -r $shared_doc->{fs_path} and $shared_doc->{type} ne 'root') { Sympa::WWW::Report::reject_report_web('user', 'no_doc_to_describe', {'path' => $path}, $param->{'action'}, $list); wwslog('info', 'Unable to change access %s: No such document', $path); 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; } $param->{'shared_doc'} = $shared_doc->as_hashref; # Access control. my %access = $shared_doc->get_privileges( mode => 'control', sender => $param->{'user'}{'email'}, auth_method => $param->{'auth_method'}, scenario_context => { sender => $param->{'user'}{'email'}, remote_host => $param->{'remote_host'}, remote_addr => $param->{'remote_addr'} } ); unless ($access{may}{control}) { Sympa::WWW::Report::reject_report_web('auth', 'action_listmaster_or_privileged_owner_or_author', {}, $param->{'action'}, $list); wwslog( 'info', 'Access denied for %s by %s', $path, $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 (exists $shared_doc->{serial_desc} and defined $shared_doc->{serial_desc}) { # If description file already exists : open it and modify it. # Synchronization. unless ($shared_doc->{serial_desc} == $in{'serial'}) { Sympa::WWW::Report::reject_report_web('user', 'synchro_failed', {}, $param->{'action'}, $list); wwslog('info', 'Synchronization failed for %s', $shared_doc); 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; } } else { $shared_doc->{scenario} = { read => $access{scenario}{read}, edit => $access{scenario}{edit} }; } $shared_doc->{scenario}{read} = $in{'read_access'} if $in{'read_access'}; $shared_doc->{scenario}{edit} = $in{'edit_access'} if $in{'edit_access'}; unless ($shared_doc->save_description) { wwslog('info', 'Cannot open description of %s: %m', $shared_doc); Sympa::WWW::Report::reject_report_web('intern', 'cannot_open_file', {'path' => $path}, $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; } return 'd_control'; } sub do_d_set_owner { wwslog('info', '(%s, %s)', $in{'path'}, $in{'content'}); my $path = $in{'path'}; # The email must look like an email "somebody@somewhere". my $email = Sympa::Tools::Text::canonic_email($in{'content'}) if $in{'content'}; unless ($email and Sympa::Tools::Text::valid_email($email)) { Sympa::WWW::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{'path'}, 'target_email' => "", 'msg_id' => '', 'status' => 'error', 'error_type' => 'incorrect_email', 'user_email' => $param->{'user'}{'email'}, } ); return undef; } my $shared_doc = Sympa::WWW::SharedDocument->new($list, $path); # The document to describe must already exist. unless ($shared_doc and -r $shared_doc->{fs_path} and $shared_doc->{type} ne 'root') { Sympa::WWW::Report::reject_report_web('user', 'no_doc_to_describe', {'path' => $path}, $param->{'action'}, $list); wwslog('info', 'Unable to change access %s: No such document', $path); 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; } $param->{'shared_doc'} = $shared_doc->as_hashref; #XXX# Must be authorized to control father directory. #XXXmy $shared_doc = Sympa::WWW::SharedDocument->new($list, $1); my %access = $shared_doc->get_privileges( mode => 'control', sender => $param->{'user'}{'email'}, auth_method => $param->{'auth_method'}, scenario_context => { sender => $param->{'user'}{'email'}, remote_host => $param->{'remote_host'}, remote_addr => $param->{'remote_addr'} } ); unless ($access{may}{control}) { Sympa::WWW::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' => 'authentication', 'user_email' => $param->{'user'}{'email'}, } ); return undef; } if (exists $shared_doc->{serial_desc} and defined $shared_doc->{serial_desc}) { # If description file already exists : open it and modify it. # Synchronization. unless ($shared_doc->{serial_desc} == $in{'serial'}) { Sympa::WWW::Report::reject_report_web('user', 'synchro_failed', {}, $param->{'action'}, $list); wwslog('info', 'Synchronization failed for %s', $shared_doc); 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; } } else { $shared_doc->{scenario} = $access{scenario}; } $shared_doc->{owner} = $email; unless ($shared_doc->save_description) { wwslog('info', 'Cannot save description of %s: %m', $shared_doc); Sympa::WWW::Report::reject_report_web('intern', 'cannot_open_file', {'path' => $path}, $param->{'action'}, $list, $param->{'user'}{'email'}, $robot); web_db_log( { 'robot' => $robot, 'list' => $list->{'name'}, 'action' => $param->{'action'}, 'parameters' => "$in{'content'}", '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'}, } ); # ONLY IF SET_OWNER can be performed even if not control of the parent # directory. unless ($access{may}{control}) { $in{'path'} = join '/', @{$shared_doc->{parent}->{paths}}; return 'd_read'; } else { return 'd_control'; } } ## Protecting archives from Email Sniffers # No longer used. #sub do_arc_protect; #################################################### # 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'); # Action confirmed? my $next_action = $session->confirm_action( $in{'action'}, $in{'response_action'}, arg => $list->{'name'}, previous_action => ($in{'previous_action'} || 'admin') ); return $next_action unless $next_action eq '1'; my $extention = time . "." . int(rand 9999); my $mail_command; ## Sympa will require a confirmation my $result = Sympa::Scenario->new($list, 'remind')->authz( '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::WWW::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 => Sympa::get_address($robot, 'owner'), 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::WWW::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::WWW::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::WWW::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::WWW::Report::reject_report_web('user', 'no_name', {}, $param->{'action'}); wwslog('err', 'No file specified to upload'); return 'suboptions'; } unless ($filetype) { Sympa::WWW::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::WWW::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::WWW::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::WWW::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::WWW::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'; } } # No longer used: use do_move_user(). #sub do_change_email_request; # No longer used: use do_move_user(). #sub do_change_email; ## Changes a user's email address in Sympa environment sub do_move_user { wwslog('info', '(%s, %s)', $in{'current_email'}, $in{'email'}); my ($current_email, $email); if ($in{'old_email'} and $in{'new_email'}) { # Compatibility to 6.1.x or earlier. $current_email = Sympa::Tools::Text::canonic_email($in{'old_email'}); $email = Sympa::Tools::Text::canonic_email($in{'new_email'}); } elsif ($in{'current_email'} and $in{'email'}) { $current_email = Sympa::Tools::Text::canonic_email($in{'current_email'}); $email = Sympa::Tools::Text::canonic_email($in{'email'}); } $param->{'current_email'} = $current_email; $param->{'email'} = $email; $param->{'previous_action'} = $in{'previous_action'} || 'pref'; unless (Sympa::Tools::Text::valid_email($current_email) and Sympa::Tools::Text::valid_email($email)) { return $in{'previous_action'} || 'pref'; } # Prevent changing addresses of others unless user is listmaster. unless (Sympa::is_listmaster($robot, $param->{'user'}{'email'}) or $param->{'user'}{'email'} eq $current_email) { return $in{'previous_action'} || 'pref'; } # Action confirmed? my $next_action = $session->confirm_action( $param->{'action'}, $in{'response_action'}, arg => "$current_email,$email", previous_action => ($in{'previous_action'} || 'pref'), ); return $next_action unless $next_action eq '1'; # Do the move_user my $spindle = Sympa::Spindle::ProcessRequest->new( context => $robot, action => 'move_user', current_email => $current_email, email => $email, sender => $param->{'user'}{'email'}, md5_check => 1, scenario_context => { sender => $param->{'user'}{'email'}, remote_host => $param->{'remote_host'}, remote_addr => $param->{'remote_addr'}, current_email => $current_email, email => $email, } ); unless ($spindle and $spindle->spin) { wwslog('err', 'Failed to change user email address'); return undef; } foreach my $report (@{$spindle->{stash} || []}) { if ($report->[1] eq 'notice') { Sympa::WWW::Report::notice_report_web(@{$report}[2, 3], $param->{'action'}); } else { Sympa::WWW::Report::reject_report_web(@{$report}[1 .. 3], $param->{action}); } } unless (@{$spindle->{stash} || []}) { Sympa::WWW::Report::notice_report_web('performed', {}, $param->{'action'}); } return $in{'previous_action'} || '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->new($list, 'visibility')->authz( $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}{'status'} = $list->{'admin'}{'status'}; # new 6.2.46 $which->{$l}{'is_subscriber'} = 1; # New on 6.2b.2. # Compat. < 6.2b.2. $which->{$l}{'info'} = 1; # Compat. < 6.2.32 (Not used by default) $which->{$l}{'host'} = $list->{'domain'}; 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) { 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}{'status'} = $list->{'admin'}{'status'}; # new 6.2.46 $which->{$l}{'is_owner'} = 1; # New on 6.2b.2. # Compat. < 6.2b.1. $which->{$l}{'info'} = 1; $which->{$l}{'admin'} = 1; # Compat. < 6.2.32 (Not used by default) $which->{$l}{'host'} = $list->{'domain'}; } foreach my $list (@{$get_which{editor}}) { my $l = $list->{'name'}; $which->{$l}{'subject'} = $list->{'admin'}{'subject'}; $which->{$l}{'status'} = $list->{'admin'}{'status'}; # new 6.2.46 $which->{$l}{'is_editor'} = 1; # New on 6.2b.2. # Compat. < 6.2b.1. $which->{$l}{'info'} = 1; $which->{$l}{'admin'} = 1; # Compat. < 6.2.32 (Not used by default) $which->{$l}{'host'} = $list->{'domain'}; } } $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::WWW::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::WWW::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::WWW::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::WWW::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::WWW::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::WWW::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::WWW::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::WWW::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::WWW::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::WWW::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::WWW::Report::notice_report_web('performed', {}, "Resume the subscription for the list(s)"); } else { Sympa::WWW::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::WWW::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') eq 'on' and length(Conf::get_robot_conf($robot, 'html_editor_url') // '')) { $param->{'use_html_editor'} = 'on'; my $html_editor_url = Conf::get_robot_conf($robot, 'html_editor_url'); if ($html_editor_url =~ /^([-.\w]+:\/\/|\/)/i) { $param->{'html_editor_url'} = $html_editor_url; } else { $param->{'html_editor_url'} = Conf::get_robot_conf($robot, 'static_content_url') . '/' . $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'} = Sympa::get_address($list); } 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::WWW::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 = Sympa::get_address($list); } unless ($param->{'may_post'}) { Sympa::WWW::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 their 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::WWW::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::WWW::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::WWW::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'); my $ctype = $query->uploadInfo($fh)->{'Content-Type'} if $fh; unless ($ctype and lc $ctype eq 'text/html') { wwslog('err', 'Can\'t upload %s (%s)', $in{'uploaded_file'}, $ctype || 'unknown type'); Sympa::WWW::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) { my $error = join("\n", $mail_html->errstr) || 'Unknown error'; Sympa::WWW::Report::reject_report_web('user', 'unable_to_parse', {error => $error}, $param->{'action'}); wwslog( 'info', 'A MIME part could not be created with the supplied data, %s, %s: %s', $in{'url'}, $in{'uploaded_file'}, $error ); 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::WWW::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; # Format current time. # If setting local timezone fails, fallback to UTC. my $date = (eval { DateTime->now(time_zone => 'local') } || DateTime->now) ->strftime('%a, %{day} %b %Y %H:%M:%S %z'); $msg_string .= sprintf "Date: %s\n", $date; if (Conf::get_robot_conf($robot, 'use_html_editor') eq 'on' and length(Conf::get_robot_conf($robot, 'html_editor_url') // '')) { $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/(?new($msg_string, context => $list); $message->reformat_utf8_message([], $charset); } # Roughly check TT2 syntax for personalization. if ( 'on' eq ($list->{'admin'}{'personalization_feature'} || 'off') and 'all' eq ($list->{'admin'}{'personalization'}{'web_apply_on'} || 'none')) { my $new_message = $message->dup; unless (defined $new_message->personalize($list)) { # FIXME: Get last_error of template object. Sympa::WWW::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 Sympa::get_address($list) } split /\s*,\s*/, $to; my @to_user = grep { $_ and $_ ne Sympa::get_address($list) and $_ ne $param->{'user'}{'email'} } split /\s*,\s*/, $to; my @to_me = grep { $_ and $_ ne Sympa::get_address($list) 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->shelve_personalization(type => 'web'); $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_me])) { Sympa::WWW::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 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::WWW::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::Spool::Topic->new( topic => $list_topics, method => 'sender' )->store($message); } my $l_message = $message->dup; # - Shelve personalization. $l_message->shelve_personalization(type => 'web'); $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::WWW::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::WWW::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 their 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::WWW::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'} = Sympa::get_address($list); $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::WWW::Report::reject_report_web('intern', 'already_confirmed', {key => $in{'authkey'}}, $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->{'from'} = $message->get_decoded_header('From'); $param->{'date'} = $message->get_decoded_header('Date'); $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::WWW::Report::reject_report_web('intern', 'already_confirmed', {key => $in{'authkey'}}, $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::WWW::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::WWW::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::Spool::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 => Sympa::get_address($robot, 'owner'), 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::WWW::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::WWW::Report::notice_report_web('performed_soon', {}, $param->{'action'}); return 'info'; } sub do_search_user { wwslog('info', ''); if ($in{'email'} =~ /[<>\\\*\$]/) { Sympa::WWW::Report::reject_report_web('user', 'syntax_errors', {p_name => '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'}; # Compat. < 6.2.32 $param->{'which'}{$l}{'host'} = $list->{'domain'}; # 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}{'subscribed'} = 1 if $list->{'user'}{'subscribed'}; my @keys = qw(reception bounce topic); @{$param->{'which'}{$l}}{@keys} = @{$list->{'user'}}{@keys}; # Compat. <= 6.2.44 $param->{'which'}{$l}{'included'} = 1 if defined $list->{'user'}{'inclusion'}; } 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::WWW::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::WWW::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::WWW::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'}); # Avoid directory traversal. return undef if 0 <= index $in{'dir'}, '/' or 0 <= index $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::WWW::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::WWW::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'; print "Content-Disposition: attachment\n"; ## 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; } # By owner, declines held subscribe (add) requests. # Old name: do_ignoresub(). sub do_decl_add { wwslog('info', '(%s)', $in{'id'}); my @ids = grep { $_ and /\A\w+\z/ } split /\0/, $in{'id'}; return ($in{'previous_action'} || 'subindex') unless @ids; $param->{'id'} = [@ids]; # Action confirmed? my $next_action = $session->confirm_action( $in{'action'}, $in{'response_action'}, arg => join(',', sort @ids), previous_action => ($in{'previous_action'} || 'subindex'), ); return $next_action unless $next_action eq '1'; my $spindle = Sympa::Spindle::ProcessRequest->new( context => $robot, action => 'decl', keyauth => [@ids], request => {context => $list, action => 'add'}, sender => $param->{'user'}{'email'}, scenario_context => { sender => $param->{'user'}{'email'}, remote_host => $param->{'remote_host'}, remote_addr => $param->{'remote_addr'} }, ); unless ($spindle and $spindle->spin) { return ($in{'previous_action'} || 'subindex'); } foreach my $report (@{$spindle->{stash} || []}) { if ($report->[1] eq 'notice') { Sympa::WWW::Report::notice_report_web(@{$report}[2, 3], $param->{'action'}); } else { Sympa::WWW::Report::reject_report_web(@{$report}[1 .. 3], $param->{action}); } } unless (@{$spindle->{stash} || []}) { Sympa::WWW::Report::notice_report_web('performed', {}, $param->{'action'}); } return ($in{'previous_action'} || '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; } # By owner, declines held signoff (del) requests. # Old name: do_ignoresig(). sub do_decl_del { wwslog('info', '(%s)', $in{'id'}); my @ids = grep { $_ and /\A\w+\z/ } split /\0/, $in{'id'}; return ($in{'previous_action'} || 'sigindex') unless @ids; $param->{'id'} = [@ids]; # Action confirmed? my $next_action = $session->confirm_action( $in{'action'}, $in{'response_action'}, arg => join(',', sort @ids), previous_action => ($in{'previous_action'} || 'sigindex'), ); return $next_action unless $next_action eq '1'; my $spindle = Sympa::Spindle::ProcessRequest->new( context => $robot, action => 'decl', keyauth => [@ids], request => {context => $list, action => 'del'}, sender => $param->{'user'}{'email'}, scenario_context => { sender => $param->{'user'}{'email'}, remote_host => $param->{'remote_host'}, remote_addr => $param->{'remote_addr'} }, ); unless ($spindle and $spindle->spin) { return ($in{'previous_action'} || 'sigindex'); } foreach my $report (@{$spindle->{stash} || []}) { if ($report->[1] eq 'notice') { Sympa::WWW::Report::notice_report_web(@{$report}[2, 3], $param->{'action'}); } else { Sympa::WWW::Report::reject_report_web(@{$report}[1 .. 3], $param->{action}); } } unless (@{$spindle->{stash} || []}) { Sympa::WWW::Report::notice_report_web('performed', {}, $param->{'action'}); } return ($in{'previous_action'} || 'sigindex'); } sub do_stats { wwslog('info', ''); $param->{'shared_size'} = int((Sympa::WWW::SharedDocument->new($list)->get_size + 512) / 1024); $param->{'arc_size'} = int((Sympa::Archive->new(context => $list)->get_size + 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; } sub _purge_subtopics { my ($robot, $topic_name, $topic) = @_; if ($topic->{sub}) { my @names = (keys %{$topic->{sub}}); for my $name (@names) { my $result = Sympa::Scenario->new($robot, 'topics_visibility', name => $topic->{sub}{$name}->{visibility})->authz( $param->{'auth_method'}, { 'topicname' => join('/', $topic_name, $name), 'sender' => $param->{'user'}{'email'}, 'remote_host' => $param->{'remote_host'}, 'remote_addr' => $param->{'remote_addr'} } ); my $action; $action = $result->{'action'} if (ref($result) eq 'HASH'); if ($action =~ /do_it/) { _purge_subtopics($robot, $topic->{sub}->{$name}); } else { delete $topic->{sub}->{$name}; } } } } ## setting the topics list for templates sub export_topics { my $robot = shift; wwslog('debug2', '(%s)', $robot); my %topics_orig = Sympa::Robot::load_topics($robot); unless (%topics_orig) { wwslog('err', 'No topics defined'); return undef; } my $dup = Sympa::Tools::Data::dup_var(\%topics_orig); my %topics = %$dup; ## 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->new($robot, 'topics_visibility', name => $topics{$t}->{visibility})->authz( $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/); # Purge concealed subtopics _purge_subtopics($robot, $t, $topics{$t}); 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 blocklist sub do_blocklist { wwslog('info', '(%s)', $param->{'list'}); unless ($param->{'list'}) { Sympa::WWW::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/blocklist.txt'; $param->{'rows'} = 0; if (defined $in{'blocklist'}) { wwslog('info', 'Submit blocklist update'); my $dir = $list->{'dir'} . '/search_filters'; unless ((-d $dir) || mkdir($dir, 0755)) { Sympa::WWW::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 . '/blocklist.txt'; my $ofh; unless (open $ofh, '>', $file) { Sympa::WWW::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{'blocklist'}); $param->{'ignored'} = 0; my $count = 0; # count utils lines in order to remove empty blocklist file foreach my $line (@lines) { if ($line =~ /\*.*\*/) { $param->{'ignored_linest'} .= $line . "\n"; $param->{'ignored'} += 1; } else { print $ofh "$line\n"; $param->{'blocklist'} .= $line . "\n"; $param->{'rows'} += 1; $count += 1 unless ($line =~ /^\s*$/o || /^[\#\;]/o); } } close $ofh; if ($count == 0) { unless (unlink $file) { Sympa::WWW::Report::reject_report_web('intern', 'unable to remove empty blocklist file'); wwslog('info', 'Unable to remove empty blocklist 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 blocklist file %s', $file); } } else { if (-f $file) { my $ifh; unless (open $ifh, $file) { Sympa::WWW::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 (<$ifh>) { $param->{'blocklist'} .= $_; $param->{'rows'} += 1; } close $ifh; } } 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{'scenario_function'}); $in{'scenario_function'} ||= $in{'pname'}; # Compat. <= 6.2.38 my $scenario = Sympa::Scenario->new($list, $in{'scenario_function'}); unless ($scenario) { Sympa::WWW::Report::reject_report_web('intern', 'cannot_open_file', {}, $param->{'action'}, $list); wwslog('info', 'Failed to load scenario'); return undef; } $param->{'dumped_scenario'} = $scenario->to_string; $param->{'scenario_path'} = $scenario->{file_path}; $param->{'scenario_function'} = $scenario->{function}; $param->{'scenario_name'} = $scenario->{name}; $param->{'pname'} = $scenario->{function}; # Compat. <= 6.2.38 if ($in{'new_scenario_name'}) { # in this case it's a submit. my $scenario_dir = $list->{'dir'} . '/scenari'; my $scenario_file = $scenario_dir . '/' . $in{'scenario_function'} . '.' . $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, 0775) { wwslog('err', '%s: %s', $scenario_dir, $ERRNO); Sympa::WWW::Report::reject_report_web( 'intern', 'cannot_create_dir', { 'file' => $scenario_dir, $param->{'action'}, '', $param->{'user'}{'email'} }, $robot ); return undef; } } my $ofh; unless (open $ofh, '>', $scenario_file) { wwslog('info', '%s', $scenario_file); Sympa::WWW::Report::reject_report_web( 'intern', 'cannot_open_file', { 'file' => $scenario_file, $param->{'action'}, '', $param->{'user'}{'email'} }, $robot ); return undef; } print $ofh $in{'new_scenario_content'}; close $ofh; # 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 # Old name: do_dump(). sub do_export_member { wwslog('info', '(%s, %s, %s)', $param->{'list'}, $in{'format'}, $in{'filter'}); # Access control return undef unless defined check_authz('do_export_member', 'review'); my $format = $in{'format'} || 'full'; my $filter = $in{'filter'}; $filter = '' unless defined $filter; $param->{'bypass'} = 'extreme'; printf "Content-Type: text/plain; Charset=\"UTF-8\"; name=\"%s.txt\"\n" . "Content-Disposition: attachment; filename=\"%s.txt\"\n" . "Content-Transfer-Encoding: 8BIT\n" . "\n", $list->get_id, $list->get_id; if ($format eq 'bounce') { print '# ' . join("\t", 'Email', 'Name', 'Bounce score', 'Bounce count', 'First bounce', 'Last bounce') . "\n"; } elsif ($format eq 'light') { ; } elsif (defined($in{'filter'})) { printf "# Exported subscribers with search filter \"%s\"\n", $filter; } my $searchkey = Sympa::Tools::Text::foldcase($filter) if defined $filter and length $filter; for ( my $subscriber = _subscriber_first($list, type => $format); $subscriber; $subscriber = _subscriber_next($list, type => $format) ) { my $email = $subscriber->{email}; my $gecos = $subscriber->{gecos}; next unless defined $email and length $email; # malformed record. if (defined $searchkey and length $searchkey) { my $e = Sympa::Tools::Text::foldcase($email); my $g = Sympa::Tools::Text::foldcase($gecos); next unless 0 <= index $e, $searchkey or 0 <= index $g, $searchkey; } if ($format eq 'bounce') { print join "\t", $email, $gecos, @{$subscriber} {qw(bounce_score bounce_count first_bounce last_bounce)}; print "\n"; } elsif ($format eq 'light') { print "$email\n"; } else { print join "\t", $email, $gecos; print "\n"; } } return 1; } sub _subscriber_first { my $list = shift; my %options = @_; if ($options{type} and $options{type} eq 'bounce') { my $i = $list->get_first_bouncing_list_member; $list->parse_list_member_bounce($i) if $i; return $i; } else { return $list->get_first_list_member; } } sub _subscriber_next { my $list = shift; my %options = @_; if ($options{type} and $options{type} eq 'bounce') { my $i = $list->get_next_bouncing_list_member; $list->parse_list_member_bounce($i) if $i; return $i; } else { return $list->get_next_list_member; } } ## 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'}; $param->{'total_results'} = 0; my @dates = $log->get_log_date; ($param->{'date_from_formated'}, $param->{'date_to_formated'}) = @dates if @dates; # Display and search parameters preparation. my $select = { robot => $robot, list => $param->{'list'}, }; foreach my $p (qw(target_type target date_from date_to type ip sortby)) { $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::WWW::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::WWW::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; } 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 return undef unless defined check_authz('do_arc', 'archive_web_access'); ##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::WWW::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::WWW::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::WWW::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::WWW::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::WWW::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::WWW::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 ; close ZIP; ## remove zip file from server disk unless (unlink($zip_abs_file)) { Sympa::WWW::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'); my @directories = sort split /\0/, ($in{'directories'} || ''); unless (@directories) { Sympa::WWW::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'; } $param->{'directories'} = [@directories]; # Action confirmed? my $next_action = $session->confirm_action( $in{'action'}, $in{'response_action'}, arg => join(',', @directories), previous_action => 'arc_manage' ); return $next_action unless $next_action eq '1'; ## 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 (@directories) { unless ($archive->purge_archive($arc)) { wwslog('info', 'Error while purging archive %s in %s', $arc, $archive); } } Sympa::WWW::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'; } # DEPRECATED. No longer used. #sub do_css; sub do_rss_request { wwslog('info', ''); if (ref $list eq 'Sympa::List') { my $result = Sympa::Scenario->new($list, 'visibility')->authz( $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::WWW::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::WWW::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, %s)', $in{'list'}, $in{'role'}); my $role = $in{'role'} || 'member'; # Compat.<=6.2.54 $in{'page'} = $role unless $role eq 'member'; $param->{'list'} = $list->{'name'}; $param->{'role'} = $role; $param->{'page'} = $role unless $role eq 'member'; my $spindle = Sympa::Spindle::ProcessRequest->new( context => $list, action => 'include', role => $role, sender => $param->{'user'}{'email'}, scenario_context => {skip => 1}, ); unless ($spindle and $spindle->spin) { wwslog('err', 'Failed to sync role %s of list %s with data sources', $role, $list); return undef; } foreach my $report (@{$spindle->{stash} || []}) { if ($report->[1] eq 'notice') { Sympa::WWW::Report::notice_report_web(@{$report}[2, 3], $param->{'action'}); } else { Sympa::WWW::Report::reject_report_web(@{$report}[1 .. 3], $param->{action}); } } unless (@{$spindle->{stash} || []}) { Sympa::WWW::Report::notice_report_web('performed', {}, $param->{'action'}); web_db_log({'parameters' => $in{'email'}, 'status' => 'success'}); } 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::WWW::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_epoch' => $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 .pm module under etc/custom_actions or etc/ ## /custom_actions (_plugin package) with a "process" sub ## to add custom processing. ## You can also create a .tt2 file at the same place to display ## your template. You don't need the section or the tag. ## The HTML code in '.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 .pm module with a specific ## .tt2 for each robot as the file (.pm or .tt2) is conducted in ## this order : ## - expl///custom_actions (if list context and robot support) ## - expl//custom_actions (if list context and no robot support) ## - etc//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 ## to display its template ## ca: 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 .pm module under etc/custom_actions or etc/ ## /custom_actions or expl(/)?//custom_actions ## (_plugin package) with a "process" sub to add custom ## processing. ## You can also create a .tt2 file at the same place to display ## your template. You don't need the section or the tag. ## The HTML code in '.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 .pm module with a specific ## .tt2 for each robot as the file (.pm or .tt2) is conducted in ## this order : ## - expl///custom_actions (if list context and robot support) ## - expl//custom_actions (if list context and no robot support) ## - etc//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 ## to display its template ## ca: 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; #FIXME: don't overwrite. $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'}); if (@{$additional_fields}) { my @fields; foreach my $f (@{$additional_fields}) { push @fields, $user->{$f}; } $user->{'additional'} = join ',', @fields; } # Compat. <= 6.2.44 if (defined $user->{'inclusion'}) { $user->{'included'} = 1; $user->{'sources'} = $language->gettext('included'); } 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, $function) = @_; my $result = Sympa::Scenario->new($list, $function)->authz( $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::WWW::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; # If there are two or more robots, 'robots' variable will be filled. my @robots = Sympa::List::get_robots(); if (@robots and 1 < scalar @robots) { $param->{'robots'} = { map { my $r = $_; ( $r => { (host => $r), # Compat.<6.2.32 ( listmasters => [Sympa::get_listmasters_email($r)] ), map { ($_ => Conf::get_robot_conf($r, $_)) } qw(listmaster title wwsympa_url) } ) } @robots }; } else { # No virtual robots. delete $param->{'robots'}; } } ## Families my @families = sort map { $_->{'name'} } @{Sympa::Family::get_families($robot)}; if (@families) { $param->{'families'} = \@families; } } # Set Sympa parameters in $param->{'conf'} # Never used. #sub get_safe_robot_conf; sub do_maintenance { wwslog('notice', ''); return 1; } # Never used. #sub do_automatic_lists_management_request; # Never used. #sub do_automatic_lists_management; # Old name: do_automatic_lists_request(). sub do_create_automatic_list_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'}); return undef; } unless ($param->{'may_create_automatic_list'}{$family->{'name'}}) { Sympa::WWW::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; } # Old name: do_automatic_lists(). sub do_create_automatic_list { wwslog('notice', '(%s)', $in{'family'}); my $family_name = $in{'family'}; # Automatic creation of a mailing list, based on a family. my $family; unless ($family = Sympa::Family->new($family_name, $robot)) { $log->syslog('err', 'Failed to create the dynamic list: Family %s does not exist', $family_name); return undef; } $param->{'family'} = $family; my $family_config = (Conf::get_robot_conf($robot, 'automatic_list_families') || {}) ->{$family_name}; my @list_name_parts; foreach my $input (keys %in) { next unless $input =~ /automatic_list_part_(\d+)/; $list_name_parts[$1] = $in{$input}; } while ( @list_name_parts and not(defined $list_name_parts[$#list_name_parts] and length $list_name_parts[$#list_name_parts]) ) { pop @list_name_parts; } my $listname = $family_config->{'prefix'} . $family_config->{'prefix_separator'} . join($family_config->{'classes_separator'}, @list_name_parts); my $spindle = Sympa::Spindle::ProcessRequest->new( context => $family, action => 'create_automatic_list', parameters => {listname => $listname}, abort_on_error => 1, sender => $param->{'user'}{'email'}, md5_check => 1, scenario_context => { sender => $param->{'user'}{'email'}, message => undef, family => $family, automatic_listname => $listname, }, ); unless ($spindle and $spindle->spin) { wwslog('err', 'Failed to create the dynamic list %s', $listname); return 'create_automatic_list_request'; } foreach my $report (@{$spindle->{stash} || []}) { if ($report->[1] eq 'notice') { Sympa::WWW::Report::notice_report_web(@{$report}[2, 3], $param->{'action'}); } elsif ($report->[1] eq 'user' and $report->[2] eq 'list_already_exists') { # Pass the list already exists. ; } else { Sympa::WWW::Report::reject_report_web(@{$report}[1 .. 3], $param->{action}); } } unless (@{$spindle->{stash} || []}) { Sympa::WWW::Report::notice_report_web('performed', {}, $param->{'action'}); } elsif ( grep { $_->[1] eq 'user' and $_->[2] eq 'list_already_exists' } @{$spindle->{stash} || []} ) { ; } elsif (not $spindle->success) { $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 'create_automatic_list_request'; } $list = Sympa::List->new($listname, $robot); $in{'list'} = $list ? $list->{'name'} : undef; return 'compose_mail'; } sub do_auth { wwslog('info', '(%s, %s, %s, %s)', $in{'id'}, $in{'heldaction'}, $in{'listname'}, $in{'email'}); my $keyauth = $in{'id'}; my $heldaction = $in{'heldaction'}; my $listname = $in{'listname'}; my $email = Sympa::Tools::Text::canonic_email($in{'email'}); my $default_home = Conf::get_robot_conf($robot, 'default_home'); return $default_home unless $email and Sympa::Tools::Text::valid_email($email); @{$param}{qw(id heldaction listname email)} = ($keyauth, $heldaction, $listname, $email); my $spool_req = Sympa::Spool::Auth->new( context => $list, action => $heldaction, keyauth => $keyauth ); my ($request, $handle); while (1) { ($request, $handle) = $spool_req->next(no_lock => 1); last unless $handle; last if $request; } return $default_home unless $request and $handle; $param->{'request'} = {map { (exists $request->{$_}) ? ($_ => $request->{$_}) : () } qw(listname mode email gecos)}; # Action confirmed? my $next_action = $session->confirm_action( $in{'action'}, $in{'response_action'}, arg => join(',', grep {$_} ($keyauth, $heldaction, $listname, $email)), previous_action => $default_home ); return $next_action unless $next_action eq '1'; my $spindle = Sympa::Spindle::ProcessRequest->new( context => $robot, action => 'auth', keyauth => $keyauth, sender => $email, scenario_context => { sender => $email, remote_host => $param->{'remote_host'}, remote_addr => $param->{'remote_addr'} }, ); unless ($spindle and $spindle->spin) { return $default_home; } foreach my $report (@{$spindle->{stash} || []}) { if ($report->[1] eq 'notice') { Sympa::WWW::Report::notice_report_web(@{$report}[2, 3], $param->{'action'}); } else { Sympa::WWW::Report::reject_report_web(@{$report}[1 .. 3], $param->{action}); } } unless (@{$spindle->{stash} || []}) { Sympa::WWW::Report::notice_report_web('performed', {}, $param->{'action'}); } return $default_home; } sub do_delete_account { if (Sympa::Tools::Data::smart_eq( Conf::get_robot_conf($robot, 'allow_account_deletion'), 'on' ) ) { wwslog( 'info', sprintf( 'Account deletion: %s asked for its account to be deleted', $param->{'user'}->{'email'}) ); # Show form if HTTP POST method not used. return 1 unless $ENV{'REQUEST_METHOD'} eq 'POST'; my $email = Sympa::Tools::Text::canonic_email($param->{'user'}->{'email'}); my $passwd = delete $in{'passwd'}; # Clear it. unless ($email) { Sympa::WWW::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 'pref'; } unless ($session->{auth} eq 'classic') { Sympa::WWW::Report::reject_report_web('user', 'no_classic_session', {}, $param->{'action'}); wwslog('info', 'No classic session'); web_db_log( { 'parameters' => $email, 'target_email' => $email, 'status' => 'error', 'error_type' => "no_classic_session" } ); return 'pref'; } my $next_action = $session->confirm_action($in{'action'}, $in{'response_action'}, previous_action => 'pref'); unless ($passwd) { Sympa::WWW::Report::reject_report_web('user', 'missing_arg', {'argument' => 'passwd'}, $param->{'action'}); wwslog('info', 'Missing parameter passwd'); web_db_log( { 'parameters' => $email, 'target_email' => $email, 'status' => 'error', 'error_type' => "missing_parameter" } ); return 'pref'; } my $data; unless (($next_action eq '1') || ($data = Sympa::WWW::Auth::check_auth($robot, $email, $passwd)) ) { $log->syslog('notice', 'Authentication failed'); web_db_log( { 'parameters' => $email, 'target_email' => $email, 'status' => 'error', 'error_type' => 'authentication' } ); return 'pref'; } return $next_action unless $next_action eq '1'; $param->{'email'} = $email; _set_my_lists_info(); my @only_owner; for my $list (sort keys %{$param->{'which'}}) { my $l = Sympa::List->new($list, $robot); # Unsubscribe $l->delete_list_member('users' => [$email]) if $param->{'which'}->{$list}->{'is_subscriber'}; # Remove from the editors $l->delete_list_admin('editor', $email) if $param->{'which'}->{$list}->{'is_editor'}; # Remove from the owners if ($param->{'which'}->{$list}->{'is_owner'}) { my @admins = $l->get_admins('owner'); if (scalar(@admins) > 1) { $l->delete_list_admin('owner', $email); # Don't let a list without a privileged admin my @privileged_admins = $l->get_admins('privileged_owner'); unless (scalar(@privileged_admins)) { @admins = $l->get_admins('owner'); for my $admin (@admins) { $l->update_list_admin($admin->{email}, 'owner', {profile => 'privileged'}); } } } else { wwslog( 'info', sprintf( 'Account deletion: %s is the only owner of %s. The account will not be deleted.', $email, $list ) ); push @only_owner, $list; } } } if (@only_owner) { Sympa::WWW::Report::reject_report_web('user', 'still_owner', {lists => join(', ', @only_owner)}, $param->{'action'}); return 'pref'; } my $user = Sympa::User->new($email); $user->expire; wwslog( 'info', sprintf('Account deletion: the account of %s has been deleted', $email) ); Sympa::WWW::Report::notice_report_web('account_deleted', {}, $param->{'action'}); do_logout(); } else { wwslog( 'info', 'Account deletion: %s asked for its account to be deleted but allow_account_deletion is not set to "on".', $param->{'user'}->{'email'} ); } } sub _is_action_disabled { my $action = shift; unless (Conf::get_robot_conf($robot, 'shared_feature') eq 'on') { return 1 if grep { $action eq $_ } qw(d_admin d_change_access d_control d_create_child d_delete d_describe d_editfile d_install_shared d_properties d_read d_reject_shared d_rename d_set_owner d_unzip d_update); } return undef; } sub prevent_visibility_bypass { wwslog('debug2', 'Starting'); if (defined $list and ref $list eq 'Sympa::List') { my $result = Sympa::Scenario->new($list, 'visibility')->authz( $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' ); # The last resort. Never use default_home. return "home"; } else { return undef; } } return undef; } # No longer used. #sub purely_closed; # Old name: tools::add_in_blacklist(). sub _add_in_blocklist { 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 blocklist not yet availible unless ($list) { $log->syslog('info', 'Robot blocklist 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 . '/blocklist.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 blocklist(%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 FastCGI script completely handles all aspects of the Sympa web interface. To know details on WWSympa, see Sympa Administration Manual: L. =head1 HISTORY WWSympa was initially written by: =over =item * Serge Aumont =item * Olivier SalaE<252>n =back The first alpha version of WWSympa appeared on Sympa 2.3.4. =cut