<?php /*

 Composr
 Copyright (c) Christopher Graham, 2004-2024

 See docs/LICENSE.md for full licensing information.


 NOTE TO PROGRAMMERS:
   Do not edit this file. If you need to make changes, save your changed file to the appropriate *_custom folder
   **** If you ignore this advice, then your website upgrades (e.g. for bug fixes) will likely kill your changes ****

*/

/**
 * @license    http://opensource.org/licenses/cpal_1.0 Common Public Attribution License
 * @copyright  Christopher Graham
 * @package    newsletter
 */

require_code('crud_module');

/**
 * Module page class.
 */
class Module_admin_newsletter extends Standard_crud_module
{
    protected $lang_type = 'NEWSLETTER';
    protected $select_name = 'TITLE';
    protected $do_preview = null;
    protected $menu_label = 'NEWSLETTER';
    protected $table = 'newsletters';
    protected $donext_entry_content_type = 'newsletter';
    protected $donext_category_content_type = null;

    /**
     * Find entry-points available within this module.
     *
     * @param  boolean $check_perms Whether to check permissions
     * @param  ?MEMBER $member_id The member to check permissions as (null: current user)
     * @param  boolean $support_crosslinks Whether to allow cross links to other modules (identifiable via a full-page-link rather than a screen-name)
     * @param  boolean $be_deferential Whether to avoid any entry-point (or even return null to disable the page in the Sitemap) if we know another module, or page_group, is going to link to that entry-point. Note that "!" and "browse" entry points are automatically merged with container page nodes (likely called by page-groupings) as appropriate.
     * @return ?array A map of entry points (screen-name=>language-code/string or screen-name=>[language-code/string, icon-theme-image]) (null: disabled)
     */
    public function get_entry_points(bool $check_perms = true, ?int $member_id = null, bool $support_crosslinks = true, bool $be_deferential = false) : ?array
    {
        if (!addon_installed('newsletter')) {
            return null;
        }

        $ret = [
            'browse' => ['MANAGE_NEWSLETTER', 'menu/site_meta/newsletters'],
            'new' => ['NEWSLETTER_SEND', 'menu/site_meta/newsletters'],
            'whatsnew' => ['WHATSNEW', 'menu/adminzone/tools/newsletter/newsletter_from_changes'],
            'subscribers' => ['VIEW_NEWSLETTER_SUBSCRIBERS', 'menu/adminzone/tools/newsletter/subscribers'],
            'import_subscribers' => ['IMPORT_NEWSLETTER_SUBSCRIBERS', 'admin/import_spreadsheet'],
            'archive' => ['NEWSLETTER_ARCHIVE', 'admin/view_archive'],
        ];
        if (!GOOGLE_APPENGINE) {
            $ret['bounce_filter_a'] = ['BOUNCE_FILTER', 'menu/adminzone/tools/newsletter/newsletter_email_bounce'];
        }
        $ret += parent::get_entry_points();
        return $ret;
    }

    public $title;

    /**
     * Module pre-run function. Allows us to know metadata for <head> before we start streaming output.
     *
     * @param  boolean $top_level Whether this is running at the top level, prior to having sub-objects called
     * @param  ?ID_TEXT $type The screen type to consider for metadata purposes (null: read from environment)
     * @return ?Tempcode Tempcode indicating some kind of exceptional output (null: none)
     */
    public function pre_run(bool $top_level = true, ?string $type = null) : ?object
    {
        $error_msg = new Tempcode();
        if (!addon_installed__messaged('newsletter', $error_msg)) {
            return $error_msg;
        }

        $type = get_param_string('type', 'browse');

        require_lang('newsletter');

        set_helper_panel_tutorial('tut_newsletter');

        if ($type == 'browse') {
            set_helper_panel_text(comcode_lang_string('DOC_NEWSLETTER'));
        }

        if ($type == 'confirm') {
            breadcrumb_set_parents([['_SELF:_SELF:new', do_lang_tempcode('NEWSLETTER_SEND')]]);
            breadcrumb_set_self(do_lang_tempcode('CONFIRM'));
        }

        if ($type == 'send') {
            breadcrumb_set_parents([['_SELF:_SELF:new', do_lang_tempcode('NEWSLETTER_SEND')]]);
            breadcrumb_set_self(do_lang_tempcode('DONE'));
        }

        if ($type == 'import_subscribers') {
            if (either_param_integer('subscribe', null) === 0) {
                $this->title = get_screen_title('SOMETHING_NEWSLETTER_SUBSCRIBERS'); // Don't say import, so as to not confuse people given a pre-set link to unsubscribe people from
            } else {
                $this->title = get_screen_title('IMPORT_NEWSLETTER_SUBSCRIBERS');
            }
        }

        if ($type == 'subscribers') {
            $this->title = get_screen_title('VIEW_NEWSLETTER_SUBSCRIBERS');
        }

        if ($type == 'bounce_filter_a' || $type == 'bounce_filter_b' || $type == 'bounce_filter_c' || $type == 'bounce_filter_d') {
            $this->title = get_screen_title('BOUNCE_FILTER');
        }

        if ($type == 'whatsnew' || $type == 'whatsnew_2') {
            $this->title = get_screen_title('WHATSNEW');
        }

        if ($type == 'new' || $type == 'confirm' || $type == 'send' || $type == 'whatsnew_3') {
            $this->title = get_screen_title('NEWSLETTER_SEND');
        }

        if ($type == 'archive') {
            $this->title = get_screen_title((get_param_integer('queued', 0) == 1) ? 'NEWSLETTER_QUEUE' : 'NEWSLETTER_ARCHIVE');
        }

        if ($type == 'view') {
            breadcrumb_set_parents([['_SELF:_SELF:archive', do_lang_tempcode('NEWSLETTER_ARCHIVE')]]);
            breadcrumb_set_self(do_lang_tempcode('VIEW'));
        }

        return parent::pre_run($top_level);
    }

    /**
     * Standard crud_module run_start.
     *
     * @param  ID_TEXT $type The type of module execution
     * @return Tempcode The output of the run
     */
    public function run_start(string $type) : object
    {
        push_query_limiting(false);

        require_code('newsletter');
        require_css('newsletter');
        require_javascript('newsletter');

        $this->edit_this_label = do_lang_tempcode('EDIT_THIS_PERIODIC_NEWSLETTER');

        $this->extra_donext_entries = [
            ['menu/site_meta/newsletters', ['_SELF', ['type' => 'new'], '_SELF'], do_lang('NEWSLETTER_SEND')],
            ['menu/adminzone/tools/newsletter/newsletter_from_changes', ['_SELF', ['type' => 'whatsnew'], '_SELF'], do_lang('WHATSNEW'), 'DOC_WHATSNEW'],
            ['admin/view_archive', ['_SELF', ['type' => 'archive'], '_SELF'], do_lang('NEWSLETTER_ARCHIVE')],
            ['menu/adminzone/tools/newsletter/subscribers', ['_SELF', ['type' => 'subscribers'], '_SELF'], do_lang('VIEW_SUBSCRIBERS')],
            ['admin/import_spreadsheet', ['_SELF', ['type' => 'import_subscribers'], '_SELF'], do_lang('IMPORT_NEWSLETTER_SUBSCRIBERS')],
        ];

        if (!GOOGLE_APPENGINE) {
            $this->extra_donext_entries[] = ['menu/adminzone/tools/newsletter/newsletter_email_bounce', ['_SELF', ['type' => 'bounce_filter_a'], '_SELF'], do_lang('BOUNCE_FILTER')];
        }

        $this->add_one_label = do_lang_tempcode('ADD_NEWSLETTER');
        $this->edit_this_label = do_lang_tempcode('EDIT_THIS_NEWSLETTER');
        $this->edit_one_label = do_lang_tempcode('EDIT_NEWSLETTER');

        $this->add_text = do_lang_tempcode('HELP_ADD_NEWSLETTER', escape_html(static_evaluate_tempcode(build_url(['page' => '_SELF', 'type' => 'new'], '_SELF'))));

        if ($type == 'browse') {
            return $this->browse();
        }

        if ($type == 'import_subscribers') {
            return $this->import_subscribers();
        }
        if ($type == 'subscribers') {
            return $this->view_subscribers();
        }
        if (!GOOGLE_APPENGINE) {
            if ($type == 'bounce_filter_a') {
                return $this->bounce_filter_a();
            }
            if ($type == 'bounce_filter_b') {
                return $this->bounce_filter_b();
            }
            if ($type == 'bounce_filter_c') {
                return $this->bounce_filter_c();
            }
            if ($type == 'bounce_filter_d') {
                return $this->bounce_filter_d();
            }
        }

        if ($type == 'whatsnew') {
            return $this->whatsnew_1();
        }
        if ($type == 'whatsnew_2') {
            return $this->whatsnew_2();
        }
        if ($type == 'whatsnew_3') {
            return $this->whatsnew_3();
        }
        if ($type == 'new') {
            return $this->send_gui();
        }
        if ($type == 'confirm') {
            return $this->confirm_send();
        }
        if ($type == 'send') {
            return $this->send_message();
        }

        if ($type == 'archive') {
            return $this->archive();
        }
        if ($type == 'view') {
            return $this->view();
        }
        if ($type == 'new') {
            return $this->send_gui();
        }

        return new Tempcode();
    }

    /**
     * The do-next manager for before content management.
     *
     * @return Tempcode The UI
     */
    public function browse() : object
    {
        $set_pause = post_param_integer('set_pause', null);
        if ($set_pause !== null) {
            require_code('config2');
            set_option('newsletter_paused', strval($set_pause));
        }

        $num_in_queue = $GLOBALS['SITE_DB']->query_select_value('newsletter_drip_send', 'COUNT(*)');

        $minutes_between_sends = intval(get_option('minutes_between_sends'));
        $mails_per_send = intval(get_option('mails_per_send'));
        $_eta = intval((floatval($num_in_queue) / floatval($mails_per_send)) * floatval($minutes_between_sends) * 60.0);
        $eta = display_time_period($_eta);

        $queue_url = build_url(['page' => '_SELF', 'type' => 'archive', 'queued' => 1], '_SELF');

        $newsletter_intro = do_template('NEWSLETTER_STATUS_OVERVIEW', [
            '_GUID' => '31ecf2e57dc441ec5f153bdbad7a2fd6',
            'UPDATE_URL' => build_url(['page' => '_SELF'], '_SELF'),
            '_NUM_IN_QUEUE' => strval($num_in_queue),
            'NUM_IN_QUEUE' => integer_format($num_in_queue),
            '_ETA' => strval($_eta),
            'ETA' => $eta,
            'PAUSED' => (get_option('newsletter_paused') == '1'),
            'QUEUE_URL' => $queue_url,
        ]);

        require_code('templates_donext');
        return do_next_manager(
            get_screen_title('MANAGE_NEWSLETTER'),
            $newsletter_intro,
            array_merge([
                ['admin/add', ['_SELF', ['type' => 'add'], '_SELF'], do_lang('ADD_NEWSLETTER')],
                ['admin/edit', ['_SELF', ['type' => 'edit'], '_SELF'], do_lang('EDIT_NEWSLETTER')],
            ], $this->extra_donext_entries),
            do_lang('MANAGE_NEWSLETTER')
        );
    }

    /**
     * Count the number of users on a certain newsletter and language of the newsletter.
     *
     * @param  string $key The newsletter identifier (as expected in $send_details parameter of newsletter_who_send_to)
     * @param  ID_TEXT $lang The language
     * @return integer The count
     */
    public function _count_on_newsletter(string $key, string $lang) : int
    {
        $send_details = [
            $key => true,
        ];
        list(, $totals) = newsletter_who_send_to($send_details, $lang);
        return isset($totals[$key]) ? $totals[$key] : 0;
    }

    /**
     * The UI to import subscribers into the newsletter.
     *
     * @return Tempcode The UI
     */
    public function import_subscribers() : object
    {
        $_language = choose_language($this->title);
        if (is_object($_language)) {
            return $_language;
        }

        require_lang('cns');

        $newsletter_id = post_param_integer('id', null);
        $subscribe = (post_param_integer('subscribe', 1) == 1);

        // Select newsletter and attach spreadsheet
        if ($newsletter_id === null) {
            $default_newsletter_id = get_param_integer('id', db_get_first_id());

            $fields = new Tempcode();
            $hidden = new Tempcode();

            // Selection
            $newsletters = new Tempcode();
            $rows = $GLOBALS['SITE_DB']->query_select('newsletters', ['id', 'title']);
            foreach ($rows as $newsletter) {
                $newsletters->attach(form_input_list_entry(strval($newsletter['id']), $newsletter['id'] === $default_newsletter_id, get_translated_text($newsletter['title'])));
            }
            if (get_forum_type() == 'cns') {
                $newsletters->attach(form_input_list_entry('-1', -1 === $default_newsletter_id, do_lang_tempcode('NEWSLETTER_CNS')));
            }
            if ($newsletters->is_empty()) {
                inform_exit(do_lang_tempcode('NO_CATEGORIES'));
            }
            if (empty($rows)) {
                $hidden->attach(form_input_hidden('id', '-1'));
            } else {
                $fields->attach(form_input_list(do_lang_tempcode('NEWSLETTER'), '', 'id', $newsletters, null, true));
            }
            require_code('files_spreadsheets_read');
            $fields->attach(form_input_upload(do_lang_tempcode('UPLOAD'), do_lang_tempcode('DESCRIPTION_UPLOAD_SPREADSHEET_2'), 'file', true, null, null, true, spreadsheet_read_file_types()));
            $l = new Tempcode();
            $l->attach(form_input_list_entry('0', !$subscribe, do_lang_tempcode('NEWSLETTER_UNSUBSCRIBE')));
            $l->attach(form_input_list_entry('1', $subscribe, do_lang_tempcode('NEWSLETTER_SUBSCRIBE')));
            $fields->attach(form_input_list(do_lang_tempcode('SUBSCRIPTION_STATUS'), do_lang_tempcode('DESCRIPTION_SUBSCRIPTION_STATUS'), 'subscribe', $l));

            $submit_name = do_lang_tempcode('PROCEED');
            $post_url = get_self_url();

            $hidden->attach(form_input_hidden('lang', $_language));
            handle_max_file_size($hidden);

            return do_template('FORM_SCREEN', [
                '_GUID' => '7e0387bcc4a1b7e2846ba357d36dbc15',
                'SKIP_WEBSTANDARDS' => true,
                'HIDDEN' => $hidden,
                'TITLE' => $this->title,
                'TEXT' => '',
                'FIELDS' => $fields,
                'SUBMIT_ICON' => 'admin/import_spreadsheet',
                'SUBMIT_NAME' => $submit_name,
                'URL' => $post_url,
            ]);
        }

        // Read data
        require_code('uploads');
        $target_path = get_temporary_upload_path('file');

        require_code('files_spreadsheets_read');
        if (!is_spreadsheet_readable($_FILES['file']['name'])) {
            unlink($target_path);
            sync_file($target_path);
            warn_exit(do_lang_tempcode('UNKNOWN_FORMAT', escape_html(get_file_extension($_FILES['file']['name']))));
        }

        if (either_param_integer('subscribe', null) === 0) {
            $action_title = do_lang('SOMETHING_NEWSLETTER_SUBSCRIBERS'); // Don't say import, so as to not confuse people given a pre-set link to unsubscribe people from
        } else {
            $action_title = do_lang('IMPORT_NEWSLETTER_SUBSCRIBERS');
        }

        log_it('IMPORT_NEWSLETTER_SUBSCRIBERS');

        require_code('tasks');
        return call_user_func_array__long_task($action_title, $this->title, 'import_newsletter_subscribers', [$_language, $newsletter_id, $subscribe, $target_path]);
    }

    /**
     * The UI to view subscribers on the newsletter.
     *
     * @return Tempcode The UI
     */
    public function view_subscribers() : object
    {
        $lang = choose_language($this->title);
        if (is_object($lang)) {
            return $lang;
        }

        $key = either_param_string('id', null);

        require_lang('cns');

        // Select newsletter
        if ($key === null) {
            $fields = new Tempcode();

            // Selection
            $newsletters = new Tempcode();
            $rows = $GLOBALS['SITE_DB']->query_select('newsletters', ['id', 'title']);
            foreach ($rows as $i => $newsletter) {
                $_key = strval($newsletter['id']);
                $subscriber_count = $this->_count_on_newsletter($_key, $lang);
                $newsletters->attach(form_input_list_entry($_key, $i == 0, get_translated_text($newsletter['title']) . ' (' . integer_format($subscriber_count) . ')'));
            }
            if (get_forum_type() == 'cns') {
                $_key = '-1';
                $subscriber_count = $this->_count_on_newsletter($_key, $lang);
                $newsletters->attach(form_input_list_entry($_key, false, protect_from_escaping(do_lang('NEWSLETTER_CNS') . ' (' . integer_format($subscriber_count) . ')')));

                $groups = $GLOBALS['FORUM_DRIVER']->get_usergroup_list();
                foreach ($groups as $group_id => $group) {
                    if ($group_id != db_get_first_id()) {
                        $_key = 'g' . strval($group_id);
                        $subscriber_count = $this->_count_on_newsletter($_key, $lang);
                        if ($subscriber_count != 0) {
                            $newsletters->attach(form_input_list_entry($_key, false, protect_from_escaping(do_lang('THIS_WITH', do_lang('USERGROUP'), escape_html($group)) . ' (' . integer_format($subscriber_count) . ')')));
                        }
                    }
                }
            }
            if ($newsletters->is_empty()) {
                inform_exit(do_lang_tempcode('NO_CATEGORIES'));
            }
            $fields->attach(form_input_list(do_lang_tempcode('NEWSLETTER'), '', 'id', $newsletters, null, true));

            // Spreadsheet option
            $fields->attach(form_input_tick(do_lang_tempcode('EXPORT_AS_SPREADSHEET'), do_lang_tempcode('DESCRIPTION_EXPORT_AS_SPREADSHEET'), 'spreadsheet', false));

            $submit_name = do_lang_tempcode('VIEW_SUBSCRIBERS');
            $post_url = get_self_url();

            $hidden = new Tempcode();
            $hidden->attach(form_input_hidden('lang', $lang));

            $prune_url = build_url(['page' => '_SELF', 'type' => 'bounce_filter_a'], '_SELF');
            return do_template('FORM_SCREEN', [
                '_GUID' => '0100ae6565474bca0669de1654b6efcf',
                'GET' => true,
                'SKIP_WEBSTANDARDS' => true,
                'HIDDEN' => $hidden,
                'TITLE' => $this->title,
                'TEXT' => do_lang_tempcode('NEWSLETTER_SUBSCRIBERS_FORM', escape_html($prune_url->evaluate())),
                'FIELDS' => $fields,
                'SUBMIT_ICON' => 'admin/export_spreadsheet',
                'SUBMIT_NAME' => $submit_name,
                'URL' => $post_url,
            ]);
        }

        // Show subscribers...

        if (get_param_integer('spreadsheet', 0) == 1) {
            require_code('tasks');
            return call_user_func_array__long_task(do_lang('VIEW_SUBSCRIBERS'), $this->title, 'export_newsletter_subscribers', [$lang, $key]);
        }

        $max = get_param_integer('max', 100);
        $start = get_param_integer('start', 0);

        $send_details = [
            $key => true,
        ];
        $max_rows = $this->_count_on_newsletter($key, $lang);

        $send_details = [];
        $send_details[$key] = true;
        list($subscribers) = newsletter_who_send_to($send_details, $lang, $start, $max);

        $subscribers_table_rows = '';

        foreach ($subscribers as $email_address => $subscriber_map) {
            $tpl = do_template('NEWSLETTER_SUBSCRIBER', [
                '_GUID' => 'ca45867a23cbaa7c6788d3cd2ba2793c',
                'EMAIL_ADDRESS' => $email_address,
                'FORENAME' => $subscriber_map['forename'],
                'SURNAME' => $subscriber_map['surname'],
                'NAME' => $subscriber_map['name'],
                'SEND_ID' => $subscriber_map['send_id'],
                'HASH' => $subscriber_map['hash'],
            ]);
            $subscribers_table_rows .= $tpl->evaluate();
        }

        require_code('templates_pagination');
        $pagination = pagination(do_lang_tempcode('VIEW_NEWSLETTER_SUBSCRIBERS'), $start, 'start', $max, 'max', $max_rows);

        $domains = newsletter_domain_subscriber_stats($key);
        foreach ($domains as $key => $val) { // We don't want when there is only 1 subscriber on a domain, if we have over 100 domains
            $domains[$key] = strval($val);
            if (count($domains) > 100) {
                if ($val == 1) {
                    unset($domains[$key]);
                }
            }
        }

        $tpl = do_template('NEWSLETTER_SUBSCRIBERS_SCREEN', [
            '_GUID' => '52e5d97d451b622d59f87f021a5b8f01',
            'TITLE' => $this->title,
            'DOMAINS' => $domains,
            'SUBSCRIBERS_TABLE_ROWS' => $subscribers_table_rows,
            'PAGINATION' => $pagination,
        ]);

        require_code('templates_internalise_screen');
        return internalise_own_screen($tpl);
    }

    /**
     * The UI to select an IMAP server for bounce filtering.
     *
     * @return Tempcode The UI
     */
    public function bounce_filter_a() : object
    {
        if (!function_exists('imap_open') && !addon_installed('imap')) {
            warn_exit(do_lang_tempcode('IMAP_NEEDED'));
        }

        $fields = new Tempcode();

        url_default_parameters__enable();

        require_lang('config');
        $mail_server_types = new Tempcode();
        foreach (['imap', 'imaps', 'imaps_nocert', 'imapt', 'imapt_nocert', 'pop3', 'pop3s', 'pop3s_nocert', 'pop3t', 'pop3t_nocert'] as $_server_type) {
            $mail_server_types->attach(form_input_list_entry($_server_type, $_server_type == get_option('mail_server_type')));
        }
        $fields->attach(form_input_list(do_lang_tempcode('SERVER_TYPE'), do_lang_tempcode('CONFIG_OPTION_mail_server_type'), 'type', $mail_server_types));
        $fields->attach(form_input_line(do_lang_tempcode('HOST'), do_lang_tempcode('CONFIG_OPTION_mail_server_host'), 'host', get_option('mail_server_host'), true));
        $fields->attach(form_input_integer(do_lang_tempcode('PORT'), do_lang_tempcode('CONFIG_OPTION_mail_server_port'), 'port', intval(get_option('mail_server_port')), true));
        $fields->attach(form_input_line(do_lang_tempcode('USERNAME'), do_lang_tempcode('CONFIG_OPTION_mail_username'), 'username', get_option('mail_username'), true));
        $fields->attach(form_input_password(do_lang_tempcode('PASSWORD'), do_lang_tempcode('CONFIG_OPTION_mail_password'), 'password', true, null, get_option('mail_password')));

        url_default_parameters__disable();

        $submit_name = do_lang_tempcode('PROCEED');
        $post_url = get_self_url();

        $hidden = new Tempcode();
        $hidden->attach(form_input_hidden('csrf_token_preserve', '1'));

        $post_url = build_url(['page' => '_SELF', 'type' => 'bounce_filter_b'], '_SELF');

        return do_template('FORM_SCREEN', [
            '_GUID' => '87f79d177931bab13f614b9cb24fb877',
            'SKIP_WEBSTANDARDS' => true,
            'HIDDEN' => $hidden,
            'TITLE' => $this->title,
            'TEXT' => do_lang_tempcode('ENTER_IMAP_DETAILS'),
            'FIELDS' => $fields,
            'SUBMIT_ICON' => 'buttons/proceed',
            'SUBMIT_NAME' => $submit_name,
            'URL' => $post_url,
        ]);
    }

    /**
     * The UI to select an inbox for bounce filtering.
     *
     * @return Tempcode The UI
     */
    public function bounce_filter_b() : object
    {
        require_code('mail');
        require_code('mail2');

        $type = post_param_string('type', false, INPUT_FILTER_POST_IDENTIFIER);
        $host = post_param_string('host', false, INPUT_FILTER_POST_IDENTIFIER);
        $port = post_param_integer('port');
        $username = post_param_string('username', false, INPUT_FILTER_POST_IDENTIFIER);
        $password = post_param_string('password', false, INPUT_FILTER_PASSWORD);

        $_folders = find_mail_folders($host, $port, $type, $username, $password);

        $folders = new Tempcode();
        foreach ($_folders as $folder => $label) {
            $selected = ((get_option('mail_folder') != '') && (get_option('mail_folder') == $folder)) || ((get_option('mail_folder') == '') && (stripos($folder, 'bounce') !== false));
            $folders->attach(form_input_list_entry($folder, $selected, $label));
        }

        $fields = new Tempcode();
        $fields->attach(form_input_list(do_lang_tempcode('FOLDER'), new Tempcode(), 'folder', $folders, null, true));

        $submit_name = do_lang_tempcode('PROCEED');
        $post_url = get_self_url();

        $post_url = build_url(['page' => '_SELF', 'type' => 'bounce_filter_c'], '_SELF');
        return do_template('FORM_SCREEN', [
            '_GUID' => '69437ad3611c0ee55d09907985df8205',
            'SKIP_WEBSTANDARDS' => true,
            'HIDDEN' => build_keep_post_fields(),
            'TITLE' => $this->title,
            'TEXT' => '',
            'FIELDS' => $fields,
            'SUBMIT_ICON' => 'buttons/proceed',
            'SUBMIT_NAME' => $submit_name,
            'URL' => $post_url,
        ]);
    }

    /**
     * The UI to confirm which subscribers to prune.
     *
     * @return Tempcode The UI
     */
    public function bounce_filter_c() : object
    {
        require_code('input_filter_2');
        if (get_value('disable_modsecurity_workaround') !== '1') {
            modsecurity_workaround_enable();
        }

        require_code('mail');
        require_code('mail2');

        $type = post_param_string('type', false, INPUT_FILTER_POST_IDENTIFIER);
        $host = post_param_string('host', false, INPUT_FILTER_POST_IDENTIFIER);
        $port = post_param_integer('port');
        $folder = post_param_string('folder', false, INPUT_FILTER_POST_IDENTIFIER);
        $username = post_param_string('username', false, INPUT_FILTER_POST_IDENTIFIER);
        $password = post_param_string('password', false, INPUT_FILTER_PASSWORD);

        $out = _find_mail_bounces($host, $port, $type, $folder, $username, $password, false);
        $num = count($out);

        $fields = '';

        $all_subscribers = [];
        $all_subscribers += collapse_2d_complexity('email', 'id', $GLOBALS['SITE_DB']->query_select('newsletter_subscribers', ['email', 'id']));
        if (get_forum_type() == 'cns') {
            $all_subscribers += collapse_2d_complexity('m_email_address', 'id', $GLOBALS['FORUM_DB']->query_select('f_members', ['m_email_address', 'id'], ['m_allow_emails_from_staff' => 1]));
        }

        $i = 0;
        foreach ($out as $email => $_details) {
            list($subject, $is_bounce) = $_details;

            if (array_key_exists($email, $all_subscribers)) {
                $tick = form_input_tick($email, $subject . '.', 'email_' . strval($num), $is_bounce, null, $email);
                $fields .= $tick->evaluate();

                $i++;
            }
        }

        if ($fields == '') {
            inform_exit(do_lang_tempcode('NO_ENTRIES'));
        }

        $submit_name = do_lang_tempcode('PROCEED');
        $post_url = get_self_url();

        $post_url = build_url(['page' => '_SELF', 'type' => 'bounce_filter_d'], '_SELF');
        return do_template('FORM_SCREEN', [
            '_GUID' => 'a517b87e2080204262d0bcf7fcebdf99',
            'SKIP_WEBSTANDARDS' => true,
            'HIDDEN' => build_keep_post_fields(['csrf_token_preserve']),
            'TITLE' => $this->title,
            'TEXT' => do_lang_tempcode('BOUNCE_WHICH'),
            'FIELDS' => $fields,
            'SUBMIT_ICON' => 'buttons/proceed',
            'SUBMIT_NAME' => $submit_name,
            'URL' => $post_url,
            'MODSECURITY_WORKAROUND' => true,
        ]);
    }

    /**
     * The actualiser to prune subscribers.
     *
     * @return Tempcode The UI
     */
    public function bounce_filter_d() : object
    {
        require_code('input_filter_2');
        if (get_value('disable_modsecurity_workaround') !== '1') {
            modsecurity_workaround_enable();
        }

        $title = get_screen_title('BOUNCE_FILTER');

        $bounces = [];

        foreach (array_keys($_POST) as $key) {
            if (substr($key, 0, 6) == 'email_') {
                $bounces[] = post_param_string($key);
            }
        }
        if (empty($bounces)) {
            warn_exit(do_lang_tempcode('NOTHING_SELECTED'), false, false, 400);
        }

        require_code('newsletter2');
        remove_email_bounces($bounces);

        return inform_screen($this->title, do_lang_tempcode('SUCCESS'));
    }

    /**
     * The UI to create an automated what's new newsletter. Select cut-off time and other settings.
     *
     * @return Tempcode The UI
     */
    public function whatsnew_1() : object
    {
        $lang = choose_language($this->title);
        if (is_object($lang)) {
            return $lang;
        }

        $fields = new Tempcode();

        $fields->attach(do_template('FORM_SCREEN_FIELD_SPACER', ['_GUID' => 'fb0234e441fa7673769b34ccfcad0cf4', 'TITLE' => do_lang('AUTOMATIC_ISSUE_SETTINGS')]));

        $_cutoff_time = get_value('newsletter_whatsnew');
        $cutoff_time = ($_cutoff_time === null) ? null : intval($_cutoff_time);
        if ($cutoff_time === null) {
            $cutoff_time = time() - 60 * 60 * 24 * 365 * 3;
        }
        $fields->attach(form_input_date(do_lang_tempcode('CUTOFF_DATE'), do_lang_tempcode('DESCRIPTION_CUTOFF_DATE'), 'cutoff', true, false, true, $cutoff_time, -3));

        $fields->attach(form_input_tick(do_lang_tempcode('EMBED_FULL_ARTICLES'), do_lang_tempcode('DESCRIPTION_EMBED_FULL_ARTICLES'), 'in_full', post_param_integer('in_full', 0) == 1));

        $hidden = new Tempcode();

        $text = new Tempcode();

        if (cron_installed()) {
            $periodic_options = new Tempcode();

            $current_periodic_newsletters = $GLOBALS['SITE_DB']->query_select('newsletter_periodic', ['*']);
            if (empty($current_periodic_newsletters)) {
                $extra_help = do_lang('PERIODIC_NEWSLETTER_EMPTY');
                $periodic_choice_name = do_lang('PERIODIC_CREATE');
                $periodic_choice_help = do_lang('PERIODIC_CREATE_HELP');
                $periodic_options->attach(form_input_list_entry('no_change', true, do_lang('DONT_MAKE_PERIODIC_NEWSLETTER'), false, false));
                $periodic_options->attach(form_input_list_entry('make_periodic', false, do_lang('MAKE_PERIODIC_NEWSLETTER'), false, false));
            } else {
                $extra_help = do_lang('PERIODIC_NEWSLETTER_EXISTS');
                $periodic_choice_name = do_lang('PERIODIC_REPLACE');
                $periodic_choice_help = do_lang('PERIODIC_REPLACE_HELP');
                $periodic_options->attach(form_input_list_entry('no_change', true, do_lang('LEAVE_PERIODIC_NEWSLETTER'), false, false));
                $periodic_options->attach(form_input_list_entry('make_periodic', false, do_lang('MAKE_PERIODIC_NEWSLETTER'), false, false));
                foreach ($current_periodic_newsletters as $current_periodic_newsletter) {
                    $periodic_options->attach(form_input_list_entry('remove_existing_' . strval($current_periodic_newsletter['id']), false, do_lang('REMOVE_PERIODIC', $current_periodic_newsletter['np_subject'], strval($current_periodic_newsletter['id'])), false, false));
                    $periodic_options->attach(form_input_list_entry('replace_existing_' . strval($current_periodic_newsletter['id']), false, do_lang('REPLACE_PERIODIC', $current_periodic_newsletter['np_subject'], strval($current_periodic_newsletter['id'])), false, false));
                }
            }
            $fields->attach(do_template('FORM_SCREEN_FIELD_SPACER', [
                '_GUID' => '9360e476c6fd8ed95176d05b866ee553',
                'TITLE' => do_lang('PERIODIC_NEWSLETTER_SETTINGS'),
                'HELP' => do_lang('PERIODIC_NEWSLETTER_HELP', $extra_help),
            ]));

            $fields->attach(form_input_list($periodic_choice_name, $periodic_choice_help, 'periodic_choice', $periodic_options, null, false, false));

            if (!empty($current_periodic_newsletters)) {
                $text = do_lang_tempcode('PERIODIC_NEWSLETTER_AMEND');
            }
        }

        $hidden->attach(form_input_hidden('csrf_token_preserve', '1'));

        return do_template('FORM_SCREEN', [
            '_GUID' => 'ce1af424e01219c8dee2a7867c1647ef',
            'SKIP_WEBSTANDARDS' => true,
            'HIDDEN' => $hidden,
            'TITLE' => $this->title,
            'TEXT' => $text,
            'FIELDS' => $fields,
            'PREVIEW' => true,
            'SUBMIT_ICON' => 'buttons/proceed',
            'SUBMIT_NAME' => do_lang_tempcode('NEXT'),
            'URL' => get_self_url(false, false, ['lang' => $lang, 'type' => 'whatsnew_2']),
        ]);
    }

    /**
     * The UI to create an automated what's new newsletter. Select content categories.
     *
     * @return Tempcode The UI
     */
    public function whatsnew_2() : object
    {
        cms_extend_time_limit(TIME_LIMIT_EXTEND__SLOW);
        disable_php_memory_limit();

        $cutoff_time = post_param_date('cutoff');
        if ($cutoff_time === null) {
            warn_exit(do_lang_tempcode('NO_PARAMETER_SENT', escape_html('cutoff')));
        }

        $lang = choose_language($this->title);
        $chosen_categories = newsletter_get_category_choices($cutoff_time, $lang);

        $fields = new Tempcode();
        $fields->attach(form_input_huge(do_lang_tempcode('WHATSNEW_CATEGORIES_SELECT'), do_lang('DESCRIPTION_WHATSNEW_CATEGORIES_SELECT'), 'chosen_categories', $chosen_categories, true));

        $hidden = new Tempcode();
        $hidden->attach(build_keep_post_fields());

        $text = do_lang_tempcode('SELECT_CATEGORIES_WANTED');

        return do_template('FORM_SCREEN', [
            '_GUID' => 'bacc372b7338d8e1103facc05ae4598f',
            'SKIP_WEBSTANDARDS' => true,
            'HIDDEN' => $hidden,
            'TITLE' => $this->title,
            'TEXT' => $text,
            'FIELDS' => $fields,
            'PREVIEW' => true,
            'SUBMIT_ICON' => 'buttons/proceed',
            'SUBMIT_NAME' => do_lang_tempcode('NEXT'),
            'URL' => get_self_url(false, false, ['lang' => $lang, 'type' => 'whatsnew_3']),
        ]);
    }

    /**
     * The UI to create an automated what's new newsletter. Pipe through to the newsletter screen.
     *
     * @return Tempcode The UI
     */
    public function whatsnew_3() : object
    {
        // Handle requested periodic "What's new" newsletter maintenance
        // =============================================================

        $matches = [];

        // Confirm screen for removal
        if (preg_match('#^remove_existing_(\d+)$#', post_param_string('periodic_choice', ''), $matches) != 0) {
            $hidden = new Tempcode();
            $hidden->attach(form_input_hidden('periodic_choice', 'periodic_remove_confirmed_' . $matches[1]));
            return do_template('PERIODIC_NEWSLETTER_REMOVE_SCREEN', [
                '_GUID' => '4fe61ba93e2a05ae9f987e462687d6d5',
                'TITLE' => get_screen_title('REMOVE_PERIODIC_NEWSLETTER'),
                'URL' => get_self_url(),
                'HIDDEN' => $hidden,
            ]);
        }
        // Actualiser for removal
        if (preg_match('#^periodic_remove_confirmed_(\d+)$#', post_param_string('periodic_choice', ''), $matches) != 0) {
            require_code('newsletter2');
            delete_periodic_newsletter(intval($matches[1]));

            // We redirect back to the admin_newsletter main page
            $url = build_url(['page' => '_SELF', 'type' => 'browse', 'redirected' => '1'], '_SELF');
            return redirect_screen(do_lang('PERIODIC_REMOVED'), $url, do_lang('PERIODIC_REMOVED_TEXT'));
        }

        // Build the "What's new" newsletter and proceed to editing
        // ========================================================

        $lang = choose_language($this->title);

        $cutoff_time = post_param_date('cutoff');
        $in_full = post_param_integer('in_full', 0);
        $chosen_categories = post_param_string('chosen_categories');
        $message = generate_whatsnew_comcode($chosen_categories, $in_full, $lang, $cutoff_time);
        if ($message === null) {
            $message = do_lang('NO_ENTRIES');
        }

        return $this->send_gui($message);
    }

    /**
     * The UI to send a newsletter.
     *
     * @param  LONG_TEXT $message Default newsletter to put in
     * @return Tempcode The UI
     */
    public function send_gui(string $message = '') : object
    {
        $blocked = newsletter_block_list();
        if (!empty($blocked)) {
            attach_message(do_lang_tempcode('BLOCK_LIST_IN_PLACE', escape_html(number_format(count($blocked)))), 'notice');
        }

        // If this is a periodic newsletter, we make some changes to the regular language strings.
        $periodic_action_raw = post_param_string('periodic_choice', '');
        $periodic_subject = '';
        $defaults = null;
        switch (preg_replace('#_\d+$#', '', $periodic_action_raw)) {
            case 'remove_existing':
                // Remove whatever is already set. We don't need any changes for this, but we do need a hidden form field.
                $periodic_action = 'remove';
                break;
            case 'replace_existing':
                // Make the current newsletter periodic. This requires language fiddling.
                $periodic_action = 'replace';
                $periodic_subject = do_lang('PERIODIC_SUBJECT_HELP');
                $periodic_id = intval(preg_replace('#^[^\d]+#', '', $periodic_action_raw));
                $_defaults = $GLOBALS['SITE_DB']->query_select('newsletter_periodic', ['*'], ['id' => $periodic_id], '', 1);
                if (!array_key_exists(0, $_defaults)) {
                    warn_exit(do_lang_tempcode('MISSING_RESOURCE'));
                }
                $defaults = $_defaults[0];
                break;
            case 'make_periodic':
                // Make the current newsletter periodic. This requires language fiddling.
                $periodic_action = 'make';
                $periodic_subject = do_lang('PERIODIC_SUBJECT_HELP');
                break;
            case 'no_change':
            default:
                // The default action is to leave the current settings as-is.
                $periodic_action = 'none';
                break;
        }

        $lang = choose_language($this->title);
        if (is_object($lang)) {
            return $lang;
        }

        $post_url = build_url(['page' => '_SELF', 'type' => 'confirm', 'old_type' => get_param_string('type', '')], '_SELF');

        $submit_name = do_lang_tempcode('PREVIEW');

        $hidden = new Tempcode();
        $hidden->attach(form_input_hidden('lang', $lang));

        $num_in_queue = $GLOBALS['SITE_DB']->query_select_value('newsletter_drip_send', 'COUNT(*)');
        if ($num_in_queue > 0) {
            attach_message(do_lang_tempcode('ALREADY_HAS_MAIL_IN_QUEUE'), 'notice');
        }

        // Build up form...
        // ================

        $fields = new Tempcode();

        $default_subject = get_option('newsletter_title');
        if ($defaults !== null) {
            $default_subject = $defaults['np_subject'];
        }
        if ($periodic_action != 'make' && $periodic_action != 'replace') {
            $default_subject .= ' - ' . get_timezoned_date(time(), false);
        }
        $default_subject = post_param_string('subject', $default_subject);

        $fields->attach(form_input_line_comcode(do_lang_tempcode('SUBJECT'), do_lang_tempcode('NEWSLETTER_DESCRIPTION_TITLE', escape_html($periodic_subject)), 'subject', $default_subject, true));

        $in_full = post_param_integer('in_full', 0);
        $chosen_categories = post_param_string('chosen_categories', '');

        $message_is_html = newsletter_is_html();

        // Read in default newsletter message overridden by GET/POST
        $message = post_param_string('message', $message);
        if ($message == '') {
            // from_news GET parameter?
            $from_news = get_param_integer('from_news', null);
            if (($from_news !== null) && (addon_installed('news'))) {
                $rows = $GLOBALS['SITE_DB']->query_select('news', ['*'], ['id' => $from_news], '', 1);
                if (!array_key_exists(0, $rows)) {
                    require_lang('news');
                    warn_exit(do_lang_tempcode('MISSING_RESOURCE'));
                }
                $myrow = $rows[0];

                $_message = get_translated_text($myrow['news_article'], null, $lang);
                if ($_message == '') {
                    $_message = get_translated_text($myrow['news'], null, $lang);
                }

                if ($message_is_html) {
                    $message = static_evaluate_tempcode(comcode_to_tempcode($_message, get_member(), true));
                }
            }
        }

        // Newsletter message (complex, as will depend if an automatic periodical being made, meaning no message defined now)
        if ($periodic_action == 'make' || $periodic_action == 'replace') {
            // We are making a periodic newsletter. This means we need to pass through the chosen categories - add extra fields to the form - and there's no direct editing
            if ($defaults !== null) {
                $chosen_categories = $defaults['np_message'];
                $in_full = $defaults['np_in_full'];

                $fields->attach(form_input_tick(do_lang_tempcode('EMBED_FULL_ARTICLES'), do_lang_tempcode('DESCRIPTION_EMBED_FULL_ARTICLES'), 'in_full', $in_full == 1));
                $fields->attach(form_input_huge(do_lang_tempcode('WHATSNEW_CATEGORIES_SELECT'), do_lang('DESCRIPTION_WHATSNEW_CATEGORIES_SELECT'), 'chosen_categories', $chosen_categories, true));
            } else {
                $hidden->attach(form_input_hidden('in_full', strval($in_full)));
                $hidden->attach(form_input_hidden('chosen_categories', $chosen_categories));
            }
            if (post_param_string('cutoff', null) !== null) {
                $hidden->attach(form_input_hidden('cutoff', post_param_string('cutoff')));
                $hidden->attach(form_input_hidden('cutoff_time', post_param_string('cutoff_time')));
            } else {
                $hidden->attach(form_input_hidden('cutoff_day', post_param_string('cutoff_day')));
                $hidden->attach(form_input_hidden('cutoff_month', post_param_string('cutoff_month')));
                $hidden->attach(form_input_hidden('cutoff_year', post_param_string('cutoff_year')));
                $hidden->attach(form_input_hidden('cutoff_hour', post_param_string('cutoff_hour')));
                $hidden->attach(form_input_hidden('cutoff_minute', post_param_string('cutoff_minute')));
            }

            $hidden->attach(form_input_hidden('message', $message));
        } else {
            $hidden->attach(form_input_hidden('in_full', strval($in_full)));

            if ($message_is_html) {
                // Raw HTML format
                $fields->attach(form_input_huge(do_lang_tempcode('MESSAGE'), do_lang_tempcode('DESCRIPTION_MESSAGE_NEWSLETTER'), 'message', $message, true));
            } else {
                // Comcode format
                $fields->attach(form_input_huge_comcode(do_lang_tempcode('MESSAGE'), do_lang_tempcode('DESCRIPTION_MESSAGE_NEWSLETTER'), 'message', $message, true));
            }
        }

        // Some general details of how to send
        if ((addon_installed('calendar')) && ($periodic_action == 'none') && (cron_installed())) {
            $fields->attach(form_input_date__cron(do_lang_tempcode('DEFER_TIME'), do_lang_tempcode('DESCRIPTION_DEFER_TIME'), 'schedule', false, true, true));
        }
        $from_email = post_param_string('from_email', get_option('staff_address'), INPUT_FILTER_POST_IDENTIFIER);
        if ($defaults !== null) {
            $from_email = post_param_string('from_email', $defaults['np_from_email'], INPUT_FILTER_POST_IDENTIFIER);
        }
        $fields->attach(form_input_email(do_lang_tempcode('FROM_EMAIL'), do_lang_tempcode('DESCRIPTION_NEWSLETTER_FROM_EMAIL'), 'from_email', $from_email, true));
        $from_name = post_param_string('from_name', get_site_name());
        if ($defaults !== null) {
            $from_name = post_param_string('from_name', $defaults['np_from_name']);
        }
        $fields->attach(form_input_line(do_lang_tempcode('FROM_NAME'), do_lang_tempcode('DESCRIPTION_NEWSLETTER_FROM_NAME'), 'from_name', $from_name, true));
        if (get_option('dual_format_newsletters') == '0' || $message_is_html) {
            $hidden->attach(form_input_hidden('html_only', '1'));
        } else {
            $html_only = (post_param_integer('html_only', ($defaults === null) ? 0 : $defaults['np_html_only']) == 1);
            $fields->attach(form_input_tick(do_lang_tempcode('HTML_ONLY'), do_lang_tempcode('DESCRIPTION_HTML_ONLY'), 'html_only', $html_only));
        }
        $l = new Tempcode();
        $priority = post_param_integer('priority', 3);
        if ($defaults !== null) {
            $priority = post_param_integer('priority', $defaults['np_priority']);
        }
        for ($i = 1; $i <= 5; $i++) {
            $l->attach(form_input_list_entry(strval($i), $i == $priority, do_lang_tempcode('PRIORITY_' . strval($i))));
        }
        $fields->attach(form_input_list(do_lang_tempcode('PRIORITY'), do_lang_tempcode('DESCRIPTION_NEWSLETTER_PRIORITY'), 'priority', $l));

        // Where to send to
        $spreadsheet_data = post_param_string('spreadsheet_data', null);
        $send_to_help = null;
        if ($spreadsheet_data === null) { // Maybe discern it from passed parameters from search module
            $_spreadsheet_data = [];
            $_spreadsheet_data[] = [do_lang('EMAIL_ADDRESS'), do_lang('NAME'), do_lang('NEWSLETTER_SEND_ID')];
            foreach (array_keys($_POST) as $post_key) {
                if (!is_string($post_key)) {
                    $post_key = strval($post_key);
                }

                $matches = [];
                if ((preg_match('#^result__member_(\d+)$#', $post_key, $matches) != 0) && (post_param_integer($post_key, 0) == 1)) {
                    $member_id = intval($matches[1]);
                    $_spreadsheet_data[] = [$GLOBALS['FORUM_DRIVER']->get_member_email_address($member_id), $GLOBALS['FORUM_DRIVER']->get_username($member_id), 'm' . strval($member_id)];
                }
            }
            if (count($_spreadsheet_data) > 1) {
                $spreadsheet_data = json_encode($_spreadsheet_data);
            }
        }
        if ($spreadsheet_data !== null) {
            $hidden->attach(form_input_hidden('spreadsheet_data', $spreadsheet_data));
            $_spreadsheet_data = json_decode($spreadsheet_data, true);
            $num_spreadsheet_data = count($_spreadsheet_data) - 1;
            $send_to_help = do_lang_tempcode('SOME_NEWSLETTER_TARGETS_KNOWN', escape_html(integer_format($num_spreadsheet_data, 0)));
        }
        $fields->attach(do_template('FORM_SCREEN_FIELD_SPACER', ['_GUID' => '7e1c75fef01054164abfa72f55e5ba86', 'TITLE' => do_lang_tempcode('CHOOSE_SEND_TO'), 'HELP' => $send_to_help]));
        $send_details = ($defaults === null) ? [] : unserialize($defaults['np_send_details']);
        $newsletters = $GLOBALS['SITE_DB']->query_select('newsletters', ['*']);
        foreach ($newsletters as $newsletter) {
            $key = strval($newsletter['id']);

            $send_to = (post_param_integer($key, empty($send_details[$key]) ? 0 : 1) == 1);
            $subscriber_count = $this->_count_on_newsletter($key, $lang);

            $newsletter_title = get_translated_text($newsletter['title']);
            $newsletter_description = get_translated_text($newsletter['the_description']);
            $fields->attach(form_input_tick(do_lang_tempcode('NEWSLETTER_SEND_TO', escape_html($newsletter_title)), do_lang_tempcode('DESCRIPTION_NEWSLETTER_SEND_TO', escape_html(integer_format($subscriber_count, 0)), escape_html($newsletter_description)), $key, $send_to));
        }
        if (get_forum_type() == 'cns') {
            $key = '-1';

            $send_to = (post_param_integer($key, empty($send_details[$key]) ? 0 : 1) == 1);
            $newsletter_subscriber_count = $this->_count_on_newsletter($key, $lang);

            $fields->attach(form_input_tick(do_lang_tempcode('NEWSLETTER_CNS'), do_lang_tempcode('NUM_READERS', escape_html(integer_format($newsletter_subscriber_count, 0))), $key, $subscriber_count > 0));

            $groups = $GLOBALS['FORUM_DRIVER']->get_usergroup_list();
            foreach ($groups as $group_id => $group) {
                if ($group_id != db_get_first_id()) {
                    $key = 'g' . strval($group_id);

                    $send_to = (post_param_integer($key, empty($send_details[$key]) ? 0 : 1) == 1);
                    $subscriber_count = $this->_count_on_newsletter($key, $lang);

                    if ($subscriber_count != 0) {
                        $fields->attach(form_input_tick(do_lang_tempcode('THIS_WITH', do_lang_tempcode('USERGROUP'), make_string_tempcode(escape_html($group))), do_lang_tempcode('NUM_READERS', escape_html(integer_format($subscriber_count, 0))), $key, $send_to));
                    }
                }
            }
        }
        if ($spreadsheet_data === null) {
            require_code('files_spreadsheets_read');
            $fields->attach(form_input_upload(do_lang_tempcode('UPLOAD'), do_lang_tempcode('DESCRIPTION_UPLOAD_SPREADSHEET'), 'file', false, null, null, true, spreadsheet_read_file_types()));
        }

        handle_max_file_size($hidden);

        $fields->attach(form_input_mail_template(do_lang_tempcode('NEWSLETTER_TEMPLATE'), do_lang_tempcode('DESCRIPTION_NEWSLETTER_TEMPLATE'), 'template', null, false, true));

        // If we're making a periodic newsletter then we need to know when it
        // should be sent
        if ($periodic_action == 'make' || $periodic_action == 'replace') {
            require_code('temporal');

            $hidden->attach(form_input_hidden('make_periodic', '1'));
            $hidden->attach(form_input_hidden('periodic_choice', post_param_string('periodic_choice')));
            $fields->attach(do_template('FORM_SCREEN_FIELD_SPACER', ['_GUID' => '1e6e0f900f85aa4ed54318801a1810bb', 'TITLE' => do_lang('PERIODIC_WHEN'), 'HELP' => do_lang('PERIODIC_WHEN_HELP')]));

            // The choices are given as radio buttons: weekly or bi-weekly or monthly?
            // In the labels for these radio buttons, we put a dropdown for day of
            // the week and day of the month.

            $frequency = post_param_string('periodic_when', 'weekly');
            if ($defaults !== null) {
                $frequency = post_param_string('periodic_when', $defaults['np_frequency']);
            }
            $current_day_weekly = post_param_integer('periodic_weekly', 5);
            if ($defaults !== null) {
                $current_day_weekly = post_param_integer('periodic_weekly', $defaults['np_day']);
            }
            $current_day_biweekly = post_param_integer('periodic_biweekly', 5);
            if ($defaults !== null) {
                $current_day_biweekly = post_param_integer('periodic_biweekly', $defaults['np_day']);
            }
            $current_day_of_month = post_param_integer('periodic_monthly', 1);
            if ($defaults !== null) {
                $current_day_of_month = post_param_integer('periodic_monthly', $defaults['np_day']);
            }

            $radios = new Tempcode();

            $week_days_weekly = new Tempcode();
            $week_days_biweekly = new Tempcode();
            require_lang('dates');
            $week_days = [1 => do_lang('MONDAY'), 2 => do_lang('TUESDAY'), 3 => do_lang('WEDNESDAY'), 4 => do_lang('THURSDAY'), 5 => do_lang('FRIDAY'), 6 => do_lang('SATURDAY'), 7 => do_lang('SUNDAY')];
            foreach ($week_days as $i => $this_day) {
                $week_days_weekly->attach(form_input_list_entry(strval($i), ($i == $current_day_weekly), $this_day, false, false));
                $week_days_biweekly->attach(form_input_list_entry(strval($i), ($i == $current_day_biweekly), $this_day, false, false));
            }

            $weekly_desc = new Tempcode();
            $weekly_desc->attach(do_lang('PERIODIC_WEEKLY_ON'));
            $weekly_desc->attach(do_template('FORM_SCREEN_INPUT_LIST', ['_GUID' => 'b0c43b5f6883be80af5911a587fc85bf', 'TABINDEX' => strval(get_form_field_tabindex(null)), 'REQUIRED' => '0', 'NAME' => 'periodic_weekday_weekly', 'CONTENT' => $week_days_weekly, 'INLINE_LIST' => '0', 'SIZE' => '9']));
            $radios->attach(form_input_radio_entry('periodic_when', 'weekly', $frequency == 'weekly', $weekly_desc, null, ''));

            $biweekly_desc = new Tempcode();
            $biweekly_desc->attach(do_lang('PERIODIC_BIWEEKLY_ON'));
            $biweekly_desc->attach(do_template('FORM_SCREEN_INPUT_LIST', ['_GUID' => '533afb6cdf1da813dd55ae694b962151', 'TABINDEX' => strval(get_form_field_tabindex(null)), 'REQUIRED' => '0', 'NAME' => 'periodic_weekday_biweekly', 'CONTENT' => $week_days_biweekly, 'INLINE_LIST' => '0', 'SIZE' => '9']));
            $radios->attach(form_input_radio_entry('periodic_when', 'biweekly', $frequency == 'biweekly', $biweekly_desc, null, ''));

            $month_days = new Tempcode();
            foreach (range(1, 28) as $this_day) {
                $suffix = gmdate('S', cms_gmmktime(0, 0, 0, 1, $this_day, 1990));
                $month_days->attach(form_input_list_entry(strval($this_day), ($this_day == 1), strval($this_day) . $suffix, $current_day_of_month == $this_day));
            }
            $monthly_desc = new Tempcode();
            $monthly_desc->attach(do_lang('PERIODIC_MONTHLY_ON'));
            $monthly_desc->attach(do_template('FORM_SCREEN_INPUT_LIST', ['_GUID' => '352012c3153342f5a954fcfa16c5503b', 'TABINDEX' => strval(get_form_field_tabindex(null)), 'REQUIRED' => '0', 'NAME' => 'periodic_monthly', 'CONTENT' => $month_days, 'INLINE_LIST' => '0', 'SIZE' => '9']));
            $radios->attach(form_input_radio_entry('periodic_when', 'monthly', $frequency == 'monthly', $monthly_desc, null, ''));
            $fields->attach(form_input_radio(do_lang('PERIODIC_WHEN_CHOICE'), '', 'periodic_when', $radios, true));

            $radios = new Tempcode();
            $radios->attach(form_input_radio_entry('periodic_for', 'all', false, do_lang_tempcode('CREATE_PERIODIC_FOR_ALL'), null, ''));
            $radios->attach(form_input_radio_entry('periodic_for', 'future', true, do_lang_tempcode('CREATE_PERIODIC_FOR_FUTURE'), null, ''));
            $fields->attach(form_input_radio(do_lang('CREATE_PERIODIC_FOR'), '', 'periodic_for', $radios, true));
        }

        return do_template('FORM_SCREEN', [
            '_GUID' => '0b2a4825ec586d9ff557026d9a1e0cca',
            'TITLE' => $this->title,
            'TEXT' => (($periodic_action == 'make' || $periodic_action == 'replace') ? do_lang_tempcode('PERIODIC_NO_EDIT') : do_lang_tempcode('NEWSLETTER_SEND_TEXT')),
            'HIDDEN' => $hidden,
            'FIELDS' => $fields->evaluate()/*Conserve memory*/,
            'SUBMIT_ICON' => 'buttons/preview',
            'SUBMIT_NAME' => $submit_name,
            'URL' => $post_url,
            'SUPPORT_AUTOSAVE' => true,
        ]);
    }

    /**
     * The UI to confirm sending of our newsletter.
     *
     * @return Tempcode The UI
     */
    public function confirm_send() : object
    {
        // Read in details
        $message = post_param_string('message');
        $subject = post_param_string('subject');
        $lang = choose_language($this->title);
        $template = post_param_string('template', 'MAIL');
        $in_full = post_param_integer('in_full', 0);
        $html_only = post_param_integer('html_only', 0);
        $from_email = post_param_string('from_email', '', INPUT_FILTER_POST_IDENTIFIER);
        $from_name = post_param_string('from_name', '');
        $email_address = $GLOBALS['FORUM_DRIVER']->get_member_email_address(get_member());
        if ($email_address == '') {
            $email_address = get_option('staff_address');
        }
        $name = $GLOBALS['FORUM_DRIVER']->get_username(get_member(), true);

        // Read in spreadsheet target
        $extra_post_data = [];
        require_code('uploads');
        $_spreadsheet_data = post_param_string('spreadsheet_data', null);
        if ($_spreadsheet_data !== null) {
            $extra_post_data['spreadsheet_data'] = $_spreadsheet_data;
        } else {
            if (((is_plupload(true)) && (array_key_exists('file', $_FILES))) || ((array_key_exists('file', $_FILES)) && ($_FILES['file']['name'] != ''))) {
                $tmp_name = $_FILES['file']['tmp_name'];

                if ((!is_plupload()) && (!is_uploaded_file($tmp_name))) {
                    $upload_error_message = get_upload_error_message($_FILES['file']);
                    warn_exit($upload_error_message);
                }

                $__spreadsheet_data = [];
                require_code('files_spreadsheets_read');
                $sheet_reader = spreadsheet_open_read($tmp_name, $_FILES['file']['name'], CMS_Spreadsheet_Reader::ALGORITHM_RAW);
                while (($spreadsheet_line = $sheet_reader->read_row()) !== false) {
                    $__spreadsheet_data[] = $spreadsheet_line;
                }
                $sheet_reader->close();

                $extra_post_data['spreadsheet_data'] = json_encode($__spreadsheet_data);
            }
        }

        // Periodic save?
        if (post_param_integer('make_periodic', 0) == 1) {
            // We're making a periodic newsletter. Thus we need to pass this info through to the next step
            $extra_post_data['make_periodic'] = '1';

            // Re-generate preview from latest chosen_categories
            $message = generate_whatsnew_comcode(post_param_string('chosen_categories', ''), $in_full, $lang, post_param_date('cutoff'));
            if ($message === null) {
                $message = do_lang('NO_ENTRIES');
            }
        }

        // Render
        list($html_version, $text_version, $in_html) = newsletter_preview($message, $subject, $lang, $html_only == 1, null, null, null, null, null, null, $template);

        // Subject line
        $_full_subject = $subject;
        if (post_param_integer('make_periodic', 0) == 1) {
            $_full_subject .= ' - ' . get_timezoned_date(time(), false);
        }
        $full_subject = do_lang('NEWSLETTER_PREVIEW_SUBJECT', $_full_subject);

        // Send e-mail
        require_code('mail');
        $mail_dispatcher = dispatch_mail(
            $full_subject,
            $in_html ? $html_version->evaluate() : $text_version,
            [$email_address],
            $name/*do_lang('NEWSLETTER_SUBSCRIBER',get_site_name())*/,
            $from_email,
            $from_name,
            [
                'no_cc' => true,
                'as_admin' => true,
                'in_html' => $in_html,
                'mail_template' => $template,
            ]
        );

        // Spam check, if possible
        $spam_report = null;
        $spam_score = null;
        if ($mail_dispatcher->mime_data !== null) {
            require_code('mail');
            require_code('mail2');
            try {
                list($spam_report, $spam_score) = email_spam_check($mail_dispatcher->mime_data);
            } catch (Exception $e) {
                require_code('failure');
                cms_error_log('Postmark: WARNING ' . $e->getMessage(), 'error_occurred_api');
            }
        }

        // Inline preview
        $preview = do_template('NEWSLETTER_CONFIRM_WRAP', [
            '_GUID' => '02bd5a782620141f8589e647e2c6d90b',
            'SUBJECT' => $full_subject,
            'TEXT_PREVIEW' => $text_version,
            'HTML_PREVIEW' => $html_version,
            'SPAM_REPORT' => $spam_report,
            'SPAM_SCORE' => ($spam_score === null) ? null : $spam_score,
        ]);

        // Confirm screen
        require_code('templates_confirm_screen');
        return confirm_screen($this->title, $preview, 'send', get_param_string('old_type', 'new'), $extra_post_data);
    }

    /**
     * The actualiser to send a newsletter.
     *
     * @return Tempcode The UI
     */
    public function send_message() : object
    {
        $lang = choose_language($this->title);
        if (is_object($lang)) {
            return $lang;
        }

        if (get_param_string('old_type', '') == 'whatsnew') {
            set_value('newsletter_whatsnew', strval(time()));
        }

        $message = post_param_string('message');
        $subject = post_param_string('subject');
        $spreadsheet_data = post_param_string('spreadsheet_data', '[]'); // JSON

        $template = post_param_string('template', 'MAIL');
        $in_full = post_param_integer('in_full', 0);

        $html_only = post_param_integer('html_only', 0);
        $from_email = post_param_string('from_email', '', INPUT_FILTER_POST_IDENTIFIER);
        $from_name = post_param_string('from_name', '');
        $priority = post_param_integer('priority', 3);

        $newsletters = $GLOBALS['SITE_DB']->query_select('newsletters', ['id']);
        $send_details = [];
        foreach ($newsletters as $newsletter) {
            $key = strval($newsletter['id']);
            $send_details[$key] = (post_param_integer($key, 0) == 1);
        }
        if (get_forum_type() == 'cns') {
            $groups = $GLOBALS['FORUM_DRIVER']->get_usergroup_list();
            foreach (array_keys($groups) as $id) {
                $key = 'g' . strval($id);
                $send_details[$key] = (post_param_integer($key, 0) == 1);
            }

            $key = '-1';
            $send_details[$key] = (post_param_integer($key, 0) == 1);
        }

        if (post_param_integer('make_periodic', 0) == 1) {
            // We're a periodic newsletter, so we don't actually want to be sent
            // out now. Rather, we store the newsletter settings so that it can be
            // regenerated as needed.

            // Next we store all of our settings in the newsletter_periodic table
            $when = post_param_string('periodic_when');
            $day = 1;
            if ($when == 'monthly') {
                $day = post_param_integer('periodic_monthly') % 29;
            } elseif ($when == 'biweekly') {
                $day = post_param_integer('periodic_weekday_biweekly', 5);
            } elseif ($when == 'weekly') {
                $day = post_param_integer('periodic_weekday_weekly', 5);
            }
            require_lang('dates');
            $week_days = [1 => do_lang('MONDAY'), 2 => do_lang('TUESDAY'), 3 => do_lang('WEDNESDAY'), 4 => do_lang('THURSDAY'), 5 => do_lang('FRIDAY'), 6 => do_lang('SATURDAY'), 7 => do_lang('SUNDAY')];
            if ($when == 'weekly') {
                $each = $week_days[$day];
            } elseif ($when == 'biweekly') {
                $each = $week_days[$day];
            } else {
                require_code('temporal');
                $suffix = gmdate('S', cms_gmmktime(0, 0, 0, 1, $day, 1990));
                $each = strval($day) . $suffix;
            }

            $matches = [];
            if (preg_match('#^replace_existing_(\d+)$#', post_param_string('periodic_choice', ''), $matches) != 0) {
                $last_sent = null;
                if (post_param_string('periodic_for') != 'future') {
                    $last_sent = 0;
                }
                require_code('newsletter2');
                edit_periodic_newsletter(intval($matches[1]), $subject, post_param_string('chosen_categories', ''), $lang, serialize($send_details), $html_only, $from_email, $from_name, $priority, $spreadsheet_data, $when, $day, $in_full, $template, $last_sent);
                $message = do_lang('PERIODIC_SUCCESS_MESSAGE_EDIT', $when, $each);
            } else {
                $last_sent = (post_param_string('periodic_for') == 'future') ? time() : 0;

                require_code('newsletter2');
                add_periodic_newsletter($subject, post_param_string('chosen_categories', ''), $lang, serialize($send_details), $html_only, $from_email, $from_name, $priority, $spreadsheet_data, $when, $day, $in_full, $template, $last_sent);
                $message = do_lang('PERIODIC_SUCCESS_MESSAGE_ADD', $when, $each);
            }

            $url = build_url(['page' => '_SELF', 'type' => 'browse', 'redirected' => '1'], '_SELF');
            return redirect_screen(do_lang('SUCCESS'), $url, $message, false, 'inform');
        }

        if (addon_installed('calendar')) {
            $schedule = post_param_date('schedule');
            if ($schedule !== null) {
                require_code('calendar');
                require_code('calendar2');
                $send_details_string_exp = '';

                $parameters = [$message, $subject, $lang, $send_details, $html_only, $from_email, $from_name, $priority, $spreadsheet_data, $template];
                $start_year = intval(date('Y', $schedule));
                $start_month = intval(date('m', $schedule));
                $start_day = intval(date('d', $schedule));
                $start_hour = intval(date('H', $schedule));
                $start_minute = intval(date('i', $schedule));
                schedule_code('send_newsletter', '', $parameters, do_lang('NEWSLETTER_SEND', $subject), $start_year, $start_month, $start_day, $start_hour, $start_minute);

                return inform_screen($this->title, do_lang_tempcode('NEWSLETTER_DEFERRED', get_timezoned_date_time($schedule)));
            }
        }

        log_it('NEWSLETTER_SEND', $subject);

        return send_newsletter($message, $subject, $lang, $send_details, $html_only, $from_email, $from_name, $priority, json_decode($spreadsheet_data, true), $template);
    }

    /**
     * The UI to select to view a past newsletter.
     *
     * @return Tempcode The UI
     */
    public function archive() : object
    {
        $lang = choose_language($this->title);
        if (is_object($lang)) {
            return $lang;
        }

        $queued = (get_param_integer('queued', 0) == 1);

        $where = '1=1';
        if (multi_lang()) {
            $where .= ' AND ' . db_string_equal_to('language', $lang);
        }
        $queued_sql = '(SELECT COUNT(*) FROM ' . get_table_prefix() . 'newsletter_drip_send WHERE d_message_id=a.id)';
        if ($queued) {
            $where .= ' AND ' . $queued_sql . '>0';
        }
        $sql = 'SELECT a.id,a.subject,a.date_and_time,' . $queued_sql . ' AS queued FROM ' . get_table_prefix() . 'newsletter_archive a WHERE ' . $where . ' ORDER BY date_and_time DESC';
        $rows = $GLOBALS['SITE_DB']->query($sql);

        $newsletters = new Tempcode();
        foreach ($rows as $newsletter) {
            $newsletter_line = do_lang('NEWSLETTER_IN_ARCHIVE_LIST', $newsletter['subject'], get_timezoned_date_time($newsletter['date_and_time']), [integer_format($newsletter['queued'], 0), strval($newsletter['id'])]);
            $newsletters->attach(form_input_list_entry(strval($newsletter['id']), false, $newsletter_line));
        }
        if ($newsletters->is_empty()) {
            inform_exit(do_lang_tempcode('NO_ENTRIES'));
        }

        $fields = new Tempcode();
        $fields->attach(form_input_huge_list(do_lang_tempcode('NEWSLETTER'), '', 'id', $newsletters, null, true));

        $hidden = form_input_hidden('lang', $lang);

        $submit_name = do_lang_tempcode('VIEW');

        $post_url = build_url(['page' => '_SELF', 'type' => 'view'], '_SELF', [], false, true);

        return do_template('FORM_SCREEN', [
            '_GUID' => 'ee295e41dc86c4583c123e6e0e445380',
            'GET' => true,
            'SKIP_WEBSTANDARDS' => true,
            'HIDDEN' => $hidden,
            'TITLE' => $this->title,
            'TEXT' => '',
            'FIELDS' => $fields,
            'SUBMIT_ICON' => 'admin/view_archive',
            'SUBMIT_NAME' => $submit_name,
            'URL' => $post_url,
        ]);
    }

    /**
     * The UI to view a past newsletter.
     *
     * @return Tempcode The UI
     */
    public function view() : object
    {
        $id = get_param_integer('id');

        if (post_param_integer('flush_queue', 0) == 1) {
            $GLOBALS['SITE_DB']->query_delete('newsletter_drip_send', ['d_message_id' => $id]);

            attach_message(do_lang_tempcode('SUCCESS'), 'inform');
        }

        $rows = $GLOBALS['SITE_DB']->query_select('newsletter_archive', ['*'], ['id' => $id], '', 1);
        if (!isset($rows[0])) {
            warn_exit(do_lang_tempcode('MISSING_RESOURCE'));
        }

        $fields = [];

        $date = get_timezoned_date_time($rows[0]['date_and_time']);
        $fields['DATE_TIME'] = $date;

        $subject = $rows[0]['subject'];
        $fields['SUBJECT'] = $subject;

        $lang = $rows[0]['language'];

        $message = $rows[0]['newsletter'];
        list($html_version, $text_version) = newsletter_preview($message, $subject, $lang, $rows[0]['html_only'] == 1, null, null, null, null, null, null, $rows[0]['template']);
        $fields['HTML_VERSION'] = do_template('NEWSLETTER_PREVIEW', ['_GUID' => '5efb08a7867bd1cd90271568853fcbb9', 'HTML_PREVIEW' => $html_version]);
        if ($text_version != '') {
            $fields['TEXT_VERSION'] = $text_version;
        }

        if (count(find_all_langs()) > 1) {
            $language = $rows[0]['language'];
            require_code('lang2');
            $language = lookup_language_full_name($language);
            $fields['LANGUAGE'] = $language;
        }

        $from_name = $rows[0]['from_name'];
        $from_email = $rows[0]['from_email'];
        if ($from_name != '' && $from_email != '') {
            $fields['FROM'] = $from_name . ' <' . $from_email . '>';
        }

        $priority = $rows[0]['priority'];
        $fields['PRIORITY'] = integer_format($priority);

        $template = $rows[0]['template'];
        if ($template != 'MAIL' && $template != '') {
            $fields['NEWSLETTER_TEMPLATE'] = $template;
        }

        $html_only = $rows[0]['html_only'];
        $fields['HTML_ONLY'] = ($html_only == 1) ? do_lang('YES') : do_lang('NO');

        $queued = $GLOBALS['SITE_DB']->query_select_value('newsletter_drip_send', 'COUNT(*)', ['d_message_id' => $id]);
        $fields['NUM_IN_SEND_QUEUE'] = integer_format($queued);

        $buttons = new Tempcode();

        if ($queued > 0) {
            $dequeue_url = build_url(['page' => '_SELF', 'type' => 'view', 'id' => $id], '_SELF');
            $hidden = new Tempcode();
            $hidden->attach(form_input_hidden('flush_queue', '1'));
            $buttons->attach(do_template('BUTTON_SCREEN', ['_GUID' => 'cda2585b0d58b1f15ba46a94e9ef9663', 'IMMEDIATE' => true, 'URL' => $dequeue_url, 'TITLE' => do_lang_tempcode('EMPTY_QUEUE'), 'IMG' => 'admin/delete3', 'HIDDEN' => $hidden]));
        }

        $copy_url = build_url(['page' => '_SELF', 'type' => 'new'], '_SELF');
        $hidden = new Tempcode();
        $hidden->attach(form_input_hidden('subject', $rows[0]['subject']));
        $hidden->attach(form_input_hidden('lang', $lang));
        $hidden->attach(form_input_hidden('from_email', $rows[0]['from_email']));
        $hidden->attach(form_input_hidden('from_name', $rows[0]['from_name']));
        $hidden->attach(form_input_hidden('priority', strval($rows[0]['priority'])));
        $hidden->attach(form_input_hidden('template', $rows[0]['template']));
        $hidden->attach(form_input_hidden('html_only', strval($rows[0]['html_only'])));
        $hidden->attach(form_input_hidden('message', $message));
        $buttons->attach(do_template('BUTTON_SCREEN', ['_GUID' => '6b9800d582a0930ede988837ea4670e0', 'IMMEDIATE' => true, 'URL' => $copy_url, 'TITLE' => do_lang_tempcode('RESEND_NEWSLETTER'), 'IMG' => 'buttons/send', 'HIDDEN' => $hidden]));

        $text = do_lang_tempcode('NEWSLETTER_WITH_SAMPLE_NAME');

        require_code('templates_map_table');
        return map_table_screen(get_screen_title('NEWSLETTER'), $fields, true, $text, $buttons, true);
    }

    /**
     * Get Tempcode for an adding form.
     *
     * @return mixed Either Tempcode; or a tuple of: (fields, hidden-fields[, delete-fields][, edit-text][, whether all delete fields are specified][, posting form text, more fields][, parsed WYSIWYG editable text])
     */
    public function get_form_fields_for_add()
    {
        return $this->get_form_fields();
    }

    /**
     * Get Tempcode for adding/editing form.
     *
     * @param  SHORT_TEXT $title The title
     * @param  LONG_TEXT $description The description
     * @return array A pair: The input fields, Hidden fields
     */
    public function get_form_fields(string $title = '', string $description = '') : array
    {
        $fields = new Tempcode();
        $fields->attach(form_input_line(do_lang_tempcode('TITLE'), do_lang_tempcode('DESCRIPTION_TITLE'), 'title', $title, true));
        $fields->attach(form_input_text(do_lang_tempcode('DESCRIPTION'), do_lang_tempcode('DESCRIPTION_DESCRIPTION'), 'description', $description, true, false));

        return [$fields, new Tempcode()];
    }

    /**
     * Standard crud_module table function.
     *
     * @param  array $url_map Details to go to build_url for link to the next screen
     * @return array A pair: The choose table, Whether re-ordering is supported from this screen
     */
    public function create_selection_list_choose_table(array $url_map) : array
    {
        require_code('templates_results_table');

        $current_ordering = get_param_string('sort', 'title ASC', INPUT_FILTER_GET_COMPLEX);
        list($sortable, $sort_order) = [substr($current_ordering, 0, strrpos($current_ordering, ' ')), substr($current_ordering, strrpos($current_ordering, ' ') + 1)];
        $sortables = [
            'title' => do_lang_tempcode('TITLE'),
        ];
        $sortables['(SELECT COUNT(*) FROM ' . get_table_prefix() . 'newsletter_subscribers n JOIN ' . get_table_prefix() . 'newsletter_subscribe s ON n.id=s.newsletter_id WHERE code_confirm=0)'] = do_lang_tempcode('COUNT_MEMBERS');
        if (((cms_strtoupper_ascii($sort_order) != 'ASC') && (cms_strtoupper_ascii($sort_order) != 'DESC')) || (!array_key_exists($sortable, $sortables))) {
            log_hack_attack_and_exit('ORDERBY_HACK');
        }

        $header_row = results_header_row([
            do_lang_tempcode('TITLE'),
            do_lang_tempcode('COUNT_MEMBERS'),
            do_lang_tempcode('ACTIONS'),
        ], $sortables, 'sort', $sortable . ' ' . $sort_order);

        $result_entries = new Tempcode();

        list($rows, $max_rows) = $this->get_entry_rows(false, $current_ordering);
        foreach ($rows as $row) {
            $edit_url = build_url($url_map + ['id' => $row['id']], '_SELF');

            $num_readers = $GLOBALS['SITE_DB']->query_select_value('newsletter_subscribers n JOIN ' . get_table_prefix() . 'newsletter_subscribe s ON n.id=s.newsletter_id', 'COUNT(*)', ['code_confirm' => 0]);

            $result_entries->attach(results_entry([get_translated_text($row['title']), integer_format($num_readers, 0), protect_from_escaping(hyperlink($edit_url, do_lang_tempcode('EDIT'), false, false, do_lang('EDIT') . ' #' . strval($row['id'])))], true));
        }

        return [results_table(do_lang($this->menu_label), get_param_integer('start', 0), 'start', either_param_integer('max', 20), 'max', $max_rows, $header_row, $result_entries, $sortables, $sortable, $sort_order), false];
    }

    /**
     * Standard crud_module list function.
     *
     * @return Tempcode The selection list
     */
    public function create_selection_list_entries() : object
    {
        $_m = $GLOBALS['SITE_DB']->query_select('newsletters', ['id', 'title']);
        $entries = new Tempcode();
        foreach ($_m as $m) {
            $entries->attach(form_input_list_entry(strval($m['id']), false, get_translated_text($m['title'], $GLOBALS['SITE_DB'])));
        }

        return $entries;
    }

    /**
     * Standard crud_module edit form filler.
     *
     * @param  ID_TEXT $id The entry being edited
     * @return mixed Either Tempcode; or a tuple of: (fields, hidden-fields[, delete-fields][, edit-text][, whether all delete fields are specified][, posting form text, more fields][, parsed WYSIWYG editable text])
     */
    public function fill_in_edit_form(string $id)
    {
        $m = $GLOBALS['SITE_DB']->query_select('newsletters', ['*'], ['id' => intval($id)], '', 1);
        if (!array_key_exists(0, $m)) {
            warn_exit(do_lang_tempcode('MISSING_RESOURCE'));
        }
        $r = $m[0];

        return $this->get_form_fields(get_translated_text($r['title']), get_translated_text($r['the_description']));
    }

    /**
     * Standard crud_module add actualiser.
     *
     * @return array A pair: The entry added, description about usage
     */
    public function add_actualisation() : array
    {
        $title = post_param_string('title');
        $description = post_param_string('description');

        require_code('newsletter2');
        $id = add_newsletter($title, $description);

        return [strval($id), null];
    }

    /**
     * Standard crud_module edit actualiser.
     *
     * @param  ID_TEXT $id The entry being edited
     * @return ?Tempcode Description about usage (null: none)
     */
    public function edit_actualisation(string $id) : ?object
    {
        $title = post_param_string('title');
        $description = post_param_string('description');

        require_code('newsletter2');
        edit_newsletter(intval($id), $title, $description);

        return null;
    }

    /**
     * Standard crud_module delete actualiser.
     *
     * @param  ID_TEXT $id The entry being deleted
     */
    public function delete_actualisation(string $id)
    {
        require_code('newsletter2');
        delete_newsletter(intval($id));
    }
}
