<?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    cns_warnings
 */

/**
 * Hook class.
 */
class Hook_cns_warnings_content
{
    /**
     * Get details for this hook.
     *
     * @return ?array The details (null: hook disabled)
     */
    public function get_details() : ?array
    {
        if (!addon_installed('cns_warnings')) {
            return null;
        }

        return [
            'order' => 100, // Content has its own field separator, so orders 100-199 should be reserved for content
        ];
    }

    /**
     * Generate punitive action text from a punitive action database row.
     *
     * @param  array $row The database row
     * @return string The punitive action text
     */
    public function generate_text(array $row) : string
    {
        if (!addon_installed('cns_warnings')) {
            return '';
        }

        require_lang('cns_warnings');

        switch ($row['p_action']) {
            case '_PUNITIVE_DELETE_POST':
                return do_lang('_PUNITIVE_DELETE_POST', $row['p_param_b']);

            case '_PUNITIVE_DELETE_POST_AND_FOLLOWING':
                return do_lang('_PUNITIVE_DELETE_POST_AND_FOLLOWING', $row['p_param_b']);

            case '_PUNITIVE_DELETE_CONTENT':
                return do_lang('_PUNITIVE_DELETE_CONTENT', $row['p_param_a'], $row['p_param_b']);

            default:
                return '';
        }
    }

    /**
     * Render form fields for the warnings screen.
     *
     * @param  Tempcode &$add_text Tempcode to be included on the intro paragraph of the warnings screen (passed by reference)
     * @param  Tempcode &$fields The fields to be rendered (passed by reference)
     * @param  Tempcode &$hidden The hidden fields to be included (passed by reference)
     * @param  boolean $new Whether it is a new warning/punishment record
     * @param  LONG_TEXT $explanation The explanation for the warning/punishment record
     * @param  BINARY $is_warning Whether to make this a formal warning
     * @param  MEMBER $member_id The member the warning is for
     * @param  BINARY $spam_mode Whether this is a spam warning
     * @param  ?AUTO_LINK $post_id The ID of the forum post of which we clicked warn (null: we are not warning on a forum post)
     * @param  ?SHORT_TEXT $ip_address The IP address of the poster (null: we are not warning on a forum post)
     */
    public function get_form_fields(&$add_text, &$fields, &$hidden, bool $new, string $explanation, int $is_warning, int $member_id, int $spam_mode, ?int $post_id, ?string $ip_address)
    {
        if (!addon_installed('cns_warnings')) {
            return;
        }

        if (!$new) {
            return;
        }

        define('POST_STANDALONE_AT_END', 0);
        define('POST_STANDALONE_IN_MIDDLE', 1);
        define('POST_AS_TOPIC_FULL', 2);
        define('POST_AS_TOPIC_STARTER', 3);

        require_code('comcode');
        require_code('comcode_from_html');

        $username = $GLOBALS['FORUM_DRIVER']->get_username($member_id);

        // Get form fields for actions on forum topics / posts
        $posts_deletable = [];
        if (has_delete_permission('mid', get_member(), $member_id, 'topics')) {
            $time_limit = time() - (60 * 60 * 24 * 7); // Limit to posts within the last 7 days
            if (
                (($GLOBALS['DEV_MODE']) || ($spam_mode)) && // i.e. we are dealing with this user as a spammer
                ((!is_guest($member_id)) || ($ip_address !== null))
            ) {
                $where = [];
                if (is_guest($member_id)) {
                    $where['p_ip_address'] = $ip_address;
                } else {
                    $where['p_poster'] = $member_id;
                }
                $sup = ' AND p_time>' . strval($time_limit) . ' ORDER BY p_time DESC';
                if (!has_privilege(get_member(), 'view_other_pt')) {
                    $sup = ' AND p_cache_forum_id IS NOT NULL ' . $sup;
                }
                $posts_by_member = $GLOBALS['FORUM_DB']->query_select('f_posts p JOIN ' . $GLOBALS['FORUM_DB']->get_table_prefix() . 'f_topics t ON t.id=p.p_topic_id', ['p.*', 't_cache_first_post_id', 't_cache_last_post_id', 't_cache_num_posts', 't_cache_first_title', 'p_cache_forum_id', 'p_time', 'p_title'], $where, $sup, 50);
                $spam_urls = [];
                foreach ($posts_by_member as $post) {
                    $just_post_row = db_map_restrict($post, ['id', 'p_post'], ['id' => 'p_id']);
                    $post_text = get_translated_tempcode('f_posts', $just_post_row, 'p_post', $GLOBALS['FORUM_DB']);
                    $matches = [];
                    $num_matches = preg_match_all('#<a\s[^<>]*href=["\']([^"\']*)["\']#', $post_text->evaluate(), $matches);
                    for ($i = 0; $i < $num_matches; $i++) {
                        $spam_url = $matches[1][$i];
                        if (!url_is_local($spam_url)) {
                            $domain = cms_parse_url_safe(normalise_idn_url($spam_url), PHP_URL_HOST);
                            if (($domain != get_domain()) && (!empty($domain))) {
                                if (!isset($spam_urls[$domain])) {
                                    require_code('mail');
                                    $ip = cms_gethostbyname($domain);
                                    $spam_urls[$domain] = ['DOMAIN' => $domain, 'IP' => $ip, 'URLS' => [], 'POSTS' => []];
                                }
                                if (!isset($spam_urls[$domain]['URLS'][$spam_url])) {
                                    $spam_urls[$domain]['URLS'][$spam_url] = ['I' => strval(count($spam_urls[$domain]['URLS'])), 'URL' => $spam_url];
                                }
                                if (!isset($spam_urls[$domain]['POSTS'][$post['id']])) {
                                    $spam_urls[$domain]['POSTS'][$post['id']] = ['I' => strval(count($spam_urls[$domain]['POSTS'])), 'POST_TITLE' => $post['p_title'], 'POST' => strip_comcode(semihtml_to_comcode($post_text->evaluate(), true), false, [], false)];
                                }
                            }
                        }
                    }

                    if ($post['t_cache_first_post_id'] == $post['id']) {
                        if ($post['t_cache_num_posts'] == 1) {
                            $post_context = POST_AS_TOPIC_STARTER;
                        } else {
                            $post_context = POST_AS_TOPIC_FULL;
                        }
                    } else {
                        if ($post['t_cache_last_post_id'] == $post['id']) {
                            $post_context = POST_STANDALONE_AT_END;
                        } else {
                            $post_context = POST_STANDALONE_IN_MIDDLE;
                        }
                    }
                    $posts_deletable[$post['id']] = [$post_context, $post['id'], $post['p_topic_id'], $post['t_cache_first_title'], $post['p_time'], $post['p_cache_forum_id']];
                }
                if (!empty($spam_urls)) {
                    $add_text->attach(paragraph(do_template('CNS_WARN_SPAM_URLS', ['_GUID' => '54bf9592bb5cf793370ff4b38fff92a6', 'USERNAME' => $username, 'SPAM_URLS' => $spam_urls])));
                }
            }
        }

        $_fields = new Tempcode();
        $available_posts_to_delete = [];
        foreach ($posts_deletable as $_post_id => $_post_deletable) {
            list($post_context, $_post_id, $topic_id, $topic_title, $post_time, $forum_id) = $_post_deletable;
            $available_posts_to_delete[] = $_post_id;

            $post_url = $GLOBALS['FORUM_DRIVER']->post_url($_post_id, $forum_id, true);

            $list_options = new Tempcode();
            $list_options->attach(form_input_list_entry('', !$spam_mode, do_lang_tempcode('HANDLE_POST__NOTHING')));
            switch ($post_context) {
                case POST_STANDALONE_AT_END:
                    $handle_label = do_lang_tempcode('HANDLE_POST__POST_STANDALONE_AT_END', escape_html($topic_title), escape_html(strval($_post_id)), [escape_html(strval($topic_id)), escape_html($post_url->evaluate())]);
                    $list_options->attach(form_input_list_entry('delete_post', $spam_mode, do_lang_tempcode('HANDLE_POST__DELETE_POST', escape_html($topic_title), escape_html(strval($_post_id)), [escape_html(strval($topic_id)), escape_html($post_url->evaluate())])));
                    break;

                case POST_STANDALONE_IN_MIDDLE:
                    $num_replies = $GLOBALS['FORUM_DB']->query_value_if_there('SELECT COUNT(*) FROM ' . $GLOBALS['FORUM_DB']->get_table_prefix() . 'f_posts WHERE p_topic_id=' . strval($topic_id) . ' AND p_time>' . strval($post_time));
                    $handle_label = do_lang_tempcode('HANDLE_POST__POST_STANDALONE_IN_MIDDLE', escape_html($topic_title), escape_html(strval($_post_id)), [escape_html(strval($topic_id)), escape_html($post_url->evaluate()), escape_html(integer_format($num_replies, 0))]);
                    $list_options->attach(form_input_list_entry('delete_post', $spam_mode, do_lang_tempcode('HANDLE_POST__DELETE_POST_WITH_GAP', escape_html($topic_title), escape_html(strval($_post_id)), [escape_html(strval($topic_id)), escape_html($post_url->evaluate()), escape_html(integer_format($num_replies, 0))])));
                    $list_options->attach(form_input_list_entry('delete_post_and_following', false, do_lang_tempcode('HANDLE_POST__DELETE_POST_AND_FOLLOWING', escape_html($topic_title), escape_html(strval($_post_id)), [escape_html(strval($topic_id)), escape_html($post_url->evaluate()), escape_html(strval($num_replies))])));
                    break;

                case POST_AS_TOPIC_FULL:
                    $num_replies = $GLOBALS['FORUM_DB']->query_value_if_there('SELECT COUNT(*) FROM ' . $GLOBALS['FORUM_DB']->get_table_prefix() . 'f_posts WHERE p_topic_id=' . strval($topic_id) . ' AND p_time>' . strval($post_time));
                    $handle_label = do_lang_tempcode('HANDLE_POST__POST_AS_TOPIC_FULL', escape_html($topic_title), escape_html(strval($_post_id)), [escape_html(strval($topic_id)), escape_html($post_url->evaluate()), escape_html(integer_format($num_replies, 0))]);
                    $list_options->attach(form_input_list_entry('delete_post_and_following', $spam_mode, do_lang_tempcode('HANDLE_POST__DELETE_TOPIC_WITH_REPLIES', escape_html($topic_title), escape_html(strval($_post_id)), [escape_html(strval($topic_id)), escape_html($post_url->evaluate()), escape_html(integer_format($num_replies, 0))])));
                    break;

                case POST_AS_TOPIC_STARTER:
                    $handle_label = do_lang_tempcode('HANDLE_POST__POST_AS_TOPIC_STARTER', escape_html($topic_title), escape_html(strval($_post_id)), [escape_html(strval($topic_id)), escape_html($post_url->evaluate())]);
                    $list_options->attach(form_input_list_entry('delete_post_and_following', $spam_mode, do_lang_tempcode('HANDLE_POST__DELETE_TOPIC', escape_html($topic_title), escape_html(strval($_post_id)), [escape_html(strval($topic_id)), escape_html($post_url->evaluate())])));
                    break;
            }
            $_fields->attach(form_input_list($handle_label, '', 'handle_post__' . strval($_post_id), $list_options, null, false, false));
        }
        $hidden->attach(form_input_hidden('available_posts_to_delete', implode("\n", $available_posts_to_delete)));

        if (addon_installed('points')) {
            $description = do_lang_tempcode('DESCRIPTION_DELETE_CONTENT_POINTS');
        } else {
            $description = do_lang_tempcode('DESCRIPTION_DELETE_CONTENT');
        }

        // See also hooks/systems/tasks/privacy_purge.php - this code handles deletion of individually-identified high-level content items, while privacy-purging will delete/anonymise on mass for any kinds of database record
        if (addon_installed('commandr') && has_privilege(get_member(), 'delete_highrange_content')) {
            $content = find_member_content($member_id, 7);
            $list_options = new Tempcode();
            $content_deletable = [];
            foreach ($content as $content_details) {
                list($content_type_title, $content_type, $content_id, $content_title, $content_url, $content_timestamp, $auto_selected) = $content_details;
                if (is_object($content_url)) {
                    $content_url = $content_url->evaluate();
                }

                $list_options->attach(form_input_list_entry($content_type . '::' . $content_id, $spam_mode, do_lang_tempcode('CONTENT_IS_OF_TYPE', $content_type_title, escape_html($content_title))));
                $content_deletable[] = $content_type . '::' . $content_id;
            }
            $_fields->attach(form_input_all_and_not(do_lang_tempcode('DELETE_CONTENT'), $description, 'delete_content', $list_options));
            $hidden->attach(form_input_hidden('available_content_to_delete', implode("\n", $content_deletable)));
        }

        if (!$_fields->is_empty()) {
            $fields->attach(do_template('FORM_SCREEN_FIELD_SPACER', ['_GUID' => 'c7eb70b13be74d8f3bd1f1c5e739d9ab', 'TITLE' => do_lang_tempcode('DELETE_CONTENT'), 'HELP' => $description, 'SECTION_HIDDEN' => !$spam_mode]));
            $fields->attach($_fields);
        }
    }

    /**
     * Actualise punitive actions.
     * Note that this assumes action was applied through the warnings form, and that post parameters still exist.
     *
     * @param  array &$punitive_messages Punitive action text to potentially be included in the PT automatically (passed by reference)
     * @param  AUTO_LINK $warning_id The ID of the warning that was created for this punitive action
     * @param  MEMBER $member_id The member this warning is being applied to
     * @param  SHORT_TEXT $username The username of the member this warning is being applied to
     * @param  SHORT_TEXT $explanation The defined explanation for this warning
     * @param  LONG_TEXT &$message The message to be sent as a PT (passed by reference; you should generally use $punitive_text instead if you want to add PT text)
     */
    public function actualise_punitive_action(array &$punitive_messages, int $warning_id, int $member_id, string $username, string $explanation, string &$message)
    {
        if (!addon_installed('cns_warnings')) {
            return;
        }

        // Post deletion
        $deleted_all = false;
        if (has_delete_permission('mid', get_member(), $member_id, 'topics')) {
            $posts_deletable = post_param_string('available_posts_to_delete');
            foreach (explode("\n", $posts_deletable) as $_post_id) {
                $post_id = intval($_post_id);

                if (isset($posts_already_deleted[$post_id])) {
                    continue;
                }

                $_post = $GLOBALS['FORUM_DB']->query_select('f_posts', ['*'], ['id' => $post_id], '', 1);
                if (!array_key_exists(0, $_post)) { // Sanity check
                    warn_exit(do_lang_tempcode('INTERNAL_ERROR'));
                }
                $post = $_post[0];
                $post_title = $post['p_title'];

                $p_title = do_lang('POST_IN_TITLED', strval($post_title), $GLOBALS['FORUM_DB']->query_select_value('f_topics', 't_cache_first_title', ['id' => $GLOBALS['FORUM_DB']->query_select_value('f_posts', 'p_topic_id', ['id' => $post_id])]));
                $_p_title = do_lang('_POST_IN_TITLED', strval($post_title), $GLOBALS['FORUM_DB']->query_select_value('f_topics', 't_cache_first_title', ['id' => $GLOBALS['FORUM_DB']->query_select_value('f_posts', 'p_topic_id', ['id' => $post_id])]));
                if (empty($post_title)) {
                    $p_title = do_lang('POST_IN_NUMBERED', $_post_id, $GLOBALS['FORUM_DB']->query_select_value('f_topics', 't_cache_first_title', ['id' => $GLOBALS['FORUM_DB']->query_select_value('f_posts', 'p_topic_id', ['id' => $post_id])]));
                    $_p_title = do_lang('_POST_IN_NUMBERED', $_post_id, $GLOBALS['FORUM_DB']->query_select_value('f_topics', 't_cache_first_title', ['id' => $GLOBALS['FORUM_DB']->query_select_value('f_posts', 'p_topic_id', ['id' => $post_id])]));
                }

                require_code('cns_posts_action3');
                $post_action = post_param_string('handle_post__' . $_post_id, '');
                $posts = [];
                switch ($post_action) {
                    case 'delete_post':
                        $posts[] = $post_id;
                        $posts_already_deleted[$post_id] = true;
                        $punitive_messages[] = do_lang('PUNITIVE_DELETE_POST', strval($p_title), null, null, null, false);
                        $GLOBALS['FORUM_DB']->query_insert('f_warnings_punitive', [
                            'p_warning_id' => $warning_id,
                            'p_member_id' => $member_id,
                            'p_ip_address' => '',
                            'p_email_address' => '',
                            'p_hook' => 'content',
                            'p_action' => '_PUNITIVE_DELETE_POST',
                            'p_param_a' => $_post_id,
                            'p_param_b' => $_p_title,
                            'p_reversed' => 0,
                        ]);
                        break;
                    case 'delete_post_and_following':
                        $posts[] = $post_id;
                        $further_posts = $GLOBALS['FORUM_DB']->query_select('f_posts', ['id'], ['p_topic_id' => $post['p_topic_id']], 'AND p_time>' . strval($post['p_time']));
                        foreach ($further_posts as $_post) {
                            $posts[] = $_post['id'];
                            $posts_already_deleted[$_post['id']] = true;
                        }
                        $GLOBALS['FORUM_DB']->query_insert('f_warnings_punitive', [
                            'p_warning_id' => $warning_id,
                            'p_member_id' => $member_id,
                            'p_ip_address' => '',
                            'p_email_address' => '',
                            'p_hook' => 'content',
                            'p_action' => '_PUNITIVE_DELETE_POST_AND_FOLLOWING',
                            'p_param_a' => $_post_id,
                            'p_param_b' => $_p_title,
                            'p_reversed' => 0,
                        ]);
                        $punitive_messages[] = do_lang('PUNITIVE_DELETE_POST_AND_FOLLOWING', strval($p_title), null, null, null, false);
                        break;
                    default:
                        $deleted_all = false;
                        break;
                }
                cns_delete_posts_topic($post['p_topic_id'], $posts, $explanation, false);
            }
        }

        // If we deleted all the posts in a topic, we should redirect to the member profile since the topic itself is now gone.
        if ($deleted_all) {
            require_code('site2');
            assign_refresh($GLOBALS['FORUM_DRIVER']->member_profile_url($member_id, true), 0.0); // redirect_screen not used because there is already a legitimate output screen happening
            unset($_GET['redirect']);
        }

        // Delete content
        if (addon_installed('commandr') && has_privilege(get_member(), 'delete_highrange_content')) {
            $content_deletable = explode("\n", post_param_string('available_content_to_delete'));
            $content_to_delete = [];
            $multi_code = read_multi_code('delete_content');

            require_code('selectcode');
            if ((substr($multi_code, 0, 1) == '-') || (substr($multi_code, 0, 1) == '*')) {
                foreach ($content_deletable as $key => $value) {
                    $content_to_delete[$value] = true;
                }
            }
            if ($multi_code != '') {
                foreach (explode(',', substr($multi_code, 1)) as $m) {
                    if (substr($multi_code, 0, 1) == '-') {
                        unset($content_to_delete[$m]);
                    } elseif (substr($multi_code, 0, 1) == '+') {
                        $content_to_delete[$m] = true;
                    }
                }
            }
            $content_to_delete = array_keys($content_to_delete);

            foreach ($content_to_delete as $content) {
                list($content_type, $content_id) = explode('::', $content);

                require_code('content');
                require_code('resource_fs');
                require_all_lang();

                list($content_title, $submitter_id, $cma_info, $content_row, $content_url, $content_url_email_safe, $cma_ob) = content_get_details($content_type, $content_id, true);
                $content_type_title = $cma_ob->get_content_type_label($content_row);

                $object_fs = get_resource_commandr_fs_object($content_type);
                if ($object_fs !== null) {
                    $filename = $object_fs->convert_id_to_filename($content_type, $content_id);
                    if ($filename !== null) {
                        $subpath = $object_fs->search($content_type, $content_id, true);
                        $object_fs->resource_delete($content_type, $filename, dirname($subpath));

                        // Reverse associated points as well
                        if (addon_installed('points')) {
                            require_code('points2');
                            points_transactions_reverse_all(null, null, $member_id, $content_type, 'add', $content_id);
                        }

                        $GLOBALS['FORUM_DB']->query_insert('f_warnings_punitive', [
                            'p_warning_id' => $warning_id,
                            'p_member_id' => $member_id,
                            'p_ip_address' => '',
                            'p_email_address' => '',
                            'p_hook' => 'content',
                            'p_action' => '_PUNITIVE_DELETE_CONTENT',
                            'p_param_a' => $content_type_title->evaluate(),
                            'p_param_b' => $content_title,
                            'p_reversed' => 0,
                        ]);

                        $punitive_messages[] = do_lang('PUNITIVE_DELETE_CONTENT', $content_type_title->evaluate(), $content_title, null, null, false);

                        $done_deleting = true;
                    }
                }
            }
        }
    }
}
