/ src / controllers / components / FeedComponent.php
<?php
/**
 * SeekQuarry/Yioop --
 * Open Source Pure PHP Search Engine, Crawler, and Indexer
 *
 * Copyright (C) 2009 - 2026  Chris Pollett chris@pollett.org
 *
 * LICENSE:
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 * END LICENSE
 *
 * @author Chris Pollett chris@pollett.org
 * @license https://www.gnu.org/licenses/ GPL3
 * @link https://www.seekquarry.com/
 * @copyright 2009 - 2026
 * @filesource
 */
namespace seekquarry\yioop\controllers\components;

use seekquarry\yioop as B;
use seekquarry\yioop\configs as C;
use seekquarry\yioop\library as L;
use seekquarry\yioop\library\mail as ML;
use seekquarry\yioop\library\av_processing\VideoExtractor;
use seekquarry\yioop\models\MailAccountModel;
use seekquarry\yioop\models\SigninModel;
use seekquarry\yioop\library\CrawlConstants;
use seekquarry\yioop\library\mail\MailScheduledDispatcher;
use seekquarry\yioop\library\mail\MailHeaderParser;
use seekquarry\yioop\library\mail\MailSiteFactory;
use seekquarry\yioop\library\mail\SmtpClient;
use seekquarry\yioop\library\UrlParser;
use seekquarry\yioop\library\wiki\WikiParser;
use seekquarry\yioop\library\FetchUrl;
use seekquarry\yioop\library\language_processing\PhraseParser;
use seekquarry\yioop\library\processors\ImageProcessor;
use seekquarry\yioop\library\mail\ImapEnvelopeParser;
use seekquarry\yioop\library\mail\ImapListing;
use seekquarry\yioop\library\mail\ImapFolderListParser;
use seekquarry\yioop\library\mail\ImapResponseParser;
use seekquarry\yioop\library\mail\MimeMessage;
use seekquarry\yioop\library\mail\MailComposeBuilder;
use seekquarry\yioop\views\elements\MailElement;
use seekquarry\yioop\library\media_jobs as LMJ;
use seekquarry\yioop\library\version_control as LVC;
use seekquarry\yioop\library\wiki as LW;
/**
 * FeedComponent draws the group feed screens of Yioop and saves what
 * someone writes there. A feed is the list of threads a group writes,
 * and each thread holds posts.
 *
 * It draws a thread and the form for writing a post. It saves a post,
 * edits one, and deletes one. It handles a post a member has flagged as
 * unwanted, and what a moderator decides about it.
 *
 * This component extends SocialComponent, the base every component
 * about a group extends. The base holds what more than one of them
 * needs, so no component calls another.
 *
 * AdminController and GroupController offer its groupFeeds activity.
 *
 * @author Chris Pollett
 */
class FeedComponent extends SocialComponent
{
    /**
     * groupFeedsModifiers returns the extra choices, called modifiers, that a
     * role can attach to the Feed and Wiki activity. Attaching
     * no_git_repository to a role takes away that role's ability to turn a wiki
     * page into a git repository. By default no role carries it, so the ability
     * is present unless removed.
     * @return array modifier key => human readable label
     */
    public function groupFeedsModifiers()
    {
        return [
                "no_git_repository" =>
                    tl("social_component_no_git_repository"),
            ];
    }
    /**
     * groupFeeds used to support requests related to posting, editing,
     * modifying, and deleting group feed items.
     * @return array $data fields to be used by GroupfeedElement
     */
    public function groupFeeds()
    {
        $parent = $this->parent;
        $wiki_model = $parent->model("wiki");
        $controller_name = (get_class($parent) == C\NS_CONTROLLERS .
            "AdminController") ? "admin" : "group";
        $data["CONTROLLER"] = $controller_name;
        $group_model = $parent->model("group");
        $feed_model = $parent->model("feed");
        $user_model = $parent->model("user");
        $cron_model = $parent->model("cron");
        $cron_time = $cron_model->getCronTime("cull_old_items");
        $delta = time() - $cron_time;
        if ($delta > C\ONE_HOUR) {
            $cron_model->updateCronTime("cull_old_items");
            $feed_model->cullExpiredGroupItems();
        } else if ($delta == 0) {
            $cron_model->updateCronTime("cull_old_items");
        }
        $data["ELEMENT"] = "groupfeed";
        $data['SCRIPT'] = "";
        $data["INCLUDE_STYLES"] = ["editor"];
        if (isset($_SESSION['USER_ID'])) {
            $user_id = $_SESSION['USER_ID'];
        } else {
            $user_id = C\PUBLIC_USER_ID;
        }
        $username = $user_model->getUsername($user_id);
        if (isset($_REQUEST['num'])) {
            $results_per_page = $parent->clean($_REQUEST['num'], "int");
        } else if (isset($_SESSION['MAX_PAGES_TO_SHOW']) &&
            $_SESSION['MAX_PAGES_TO_SHOW'] > 0) {
            $results_per_page = $_SESSION['MAX_PAGES_TO_SHOW'];
        } else {
            $results_per_page = C\NUM_RESULTS_PER_PAGE;
        }
        if (isset($_REQUEST['limit'])) {
            $limit = $parent->clean($_REQUEST['limit'], "int");
        } else {
            $limit = 0;
        }
        if (isset($_SESSION['OPEN_IN_TABS'])) {
            $data['OPEN_IN_TABS'] = $_SESSION['OPEN_IN_TABS'];
        } else {
            $data['OPEN_IN_TABS'] = false;
        }
        $clean_array = [ "title" => "string", "description" => "string",
            "contact_id" => 'int', "just_group_id" => "int",
            "just_thread" => "int", "just_user_id" => "int"];
        $strings_array = [ "title" => C\TITLE_LEN, "description" =>
            C\MAX_GROUP_POST_LEN];
        if ($user_id == C\PUBLIC_USER_ID) {
            $_SESSION['LAST_ACTIVITY']['a'] = 'groupFeeds';
            $_SESSION['LAST_ACTIVITY']['c'] = $controller_name;
        } else {
            unset($_SESSION['LAST_ACTIVITY']);
        }
        foreach ($clean_array as $field => $type) {
            $$field = ($type == "string") ? "" : 0;
            if (isset($_REQUEST[$field])) {
                $tmp = $parent->clean($_REQUEST[$field], $type);
                if (isset($strings_array[$field])) {
                    $tmp = substr($tmp, 0, $strings_array[$field]);
                }
                if ($user_id == C\PUBLIC_USER_ID) {
                    $_SESSION['LAST_ACTIVITY'][$field] = $tmp;
                }
                $$field = $tmp;
            }
        }
        $possible_arguments = ["status"];
        if ($user_id !=  C\PUBLIC_USER_ID) {
            $user_messages_id = $group_model->getPersonalGroupId($user_id);
            $data['USER_MESSAGES_ID'] = $user_messages_id;
            $data['CONTACTS'] = array_diff($group_model->getGroupUserIds(
                $user_messages_id), [$user_id]);
            $possible_arguments = ["addcomment", "addcontact","approveflagged",
                "addgroup", "copypost", "cutpost", "deleteflagged",
                "deleteclip", "deletepost", "downvote",
                "emptyclip", "flagpost", "newthread", "pasteall", "pastepost",
                "status", "togglethreadmail", "updatepost", "upvote",  ];
        }
        if (isset($_REQUEST['arg']) &&
            in_array($_REQUEST['arg'], $possible_arguments)) {
            $arg = $_REQUEST['arg'];
            /* Each of these values of the request names the
               method that answers it. */
            $answered_by = [
                "togglethreadmail" => "toggleThreadMail",
                "addcomment" => "addComment",
                "addcontact" => "addFeedContact",
                "addgroup" => "addFeedGroup",
                "deletepost" => "deletePost",
                "deleteclip" => "deleteClip", "emptyclip" => "emptyClip",
                "pasteall" => "pasteAllPosts", "pastepost" => "pastePost",
                "flagpost" => "flagPost",
                "approveflagged" => "approveFlagged",
                "deleteflagged" => "deleteFlagged",
                "downvote" => "downVotePost",
                "newthread" => "startNewThread", "status" => "feedStatus",
                "updatepost" => "updatePost", "upvote" => "upVotePost"];
            $asked_for = $arg;
            if (isset($answered_by[$asked_for])) {
                $method = $answered_by[$asked_for];
                $this->$method($data, $user_id);
            }
            if ($asked_for == "copypost" || $asked_for == "cutpost") {
                /* A copy and a cut both copy the post; a cut then
                   takes it away as well. */
                $this->copyOrCutPost($data, $user_id);
                if ($asked_for == "cutpost") {
                    $this->deletePost($data, $user_id);
                }
            }
        }
        $view_mode = (isset($_REQUEST['v'])) ?
            $parent->clean($_REQUEST['v'], "string") : "grouped";
        $data['VIEW_MODE'] = $view_mode;
        $view_mode = (!$just_group_id && !$just_user_id
            && !$just_thread) ? $view_mode : "ungrouped";
        if ($view_mode == "grouped") {
            if ($user_id == C\PUBLIC_USER_ID) {
                $_REQUEST = ['c' => "admin", 'a' => '',
                    C\p('CSRF_TOKEN') => ''];
                return $parent->redirectWithMessage(
                    tl("social_component_login_first"));
            }
            $this->initSocialBadges($user_id, $data);
            $this->calculateRecentFeedsAndThread($data, $user_id);
            return $this->calculateGroupedFeeds($user_id, $limit,
                $results_per_page, $controller_name, $data);
        } else {
            $this->initSocialBadges($user_id, $data);
        }
        $groups_count = 0;
        $pages = [];
        $page = [];
        if (!$just_user_id && (!$just_thread || $just_thread < 0)) {
            $search_array = [
                ["group_id", "=", max(-$just_thread, $just_group_id), ""],
                ["access", "!=", C\GROUP_PRIVATE, ""],
                ["status", "IN", [C\ACTIVE_STATUS, C\EDITOR_STATUS], ""],
                ["join_date", "=", "", "DESC"]
            ];
            $groups = $group_model->getRows(
                0, $limit + $results_per_page, $groups_count,
                $search_array, [$user_id, false]);
            // create feed items for first join dates to given groups
            foreach ($groups as $group) {
                $page = [];
                $page['USER_ICON'] = C\SHORT_BASE_URL .
                    "resources/anonymous.png";
                $page[self::TITLE] = tl('social_component_join_group',
                    $username, $group['GROUP_NAME']);
                $page[self::DESCRIPTION] =
                    tl('social_component_join_group_detail',
                        date("r", $group['JOIN_DATE']), $group['GROUP_NAME']);
                $page['ID'] = -$group['GROUP_ID'];
                $page['PARENT_ID'] = -$group['GROUP_ID'];
                $page['USER_NAME'] = "";
                $page['USER_ID'] = "";
                $page['GROUP_ID'] = $group['GROUP_ID'];
                $page[self::SOURCE_NAME] = $group['GROUP_NAME'];
                $page['MEMBER_ACCESS'] = $group['MEMBER_ACCESS'];
                $page['STATUS'] = $group['STATUS'];
                if ($group['OWNER_ID'] == $user_id || $user_id == C\ROOT_ID) {
                    $page['MEMBER_ACCESS'] = C\GROUP_READ_WIKI;
                }
                $page['PUBDATE'] = $group['JOIN_DATE'];
                $pages[$group['JOIN_DATE']] = $page;
            }
        }
        $pub_clause = ['pub_date', "=", "", "DESC"];
        $sort = "krsort";
        if ($just_thread) {
            $thread_parent = $feed_model->getGroupItem($just_thread);
            $group_id = $thread_parent['GROUP_ID'] ?? false;
            if ($group_id && $user_id != C\PUBLIC_USER_ID &&
                $group_model->checkUserGroup($user_id, $group_id) !==
                false) {
                $data['SHOW_THREAD_FOLLOW'] = true;
                $data['THREAD_FOLLOWED'] = $feed_model->
                    isThreadSubscribed($user_id, $just_thread);
            }
            if (isset($thread_parent["TYPE"]) &&
                $thread_parent["TYPE"] == C\WIKI_GROUP_ITEM) {
                $page_info = $feed_model->getPageInfoByThread($just_thread);
                if (isset($page_info["PAGE_NAME"])) {
                    $group_id = $page_info['GROUP_ID'];
                    $data["WIKI_PAGE_NAME"] = $page_info["PAGE_NAME"];
                    $group = $group_model->getGroupById($group_id, $user_id,
                        true);
                    if (!empty($group) && ($group["OWNER_ID"] == $user_id ||
                        $group["STATUS"] == C\EDITOR_STATUS ||
                        ($group["STATUS"] == C\ACTIVE_STATUS &&
                        $group["MEMBER_ACCESS"] == C\GROUP_READ_WIKI &&
                        $user_id != C\PUBLIC_USER_ID))) {
                        $data["CAN_EDIT"] = true;
                        $edit_or_source = "edit";
                    } else {
                        $data["CAN_EDIT"] = false;
                        $edit_or_source = "source";
                    }
                    if (empty($data["CAN_EDIT"]) &&
                        $user_id == C\PUBLIC_USER_ID) {
                        $thread_source_allowed =
                            !empty($group['PAGE_SOURCE_ALLOWED']);
                        if ($thread_source_allowed) {
                            $thread_page_info =
                                $wiki_model->getPageInfoByName($group_id,
                                $page_info["PAGE_NAME"],
                                $page_info["LOCALE_TAG"], "read");
                            $thread_head = WikiParser::parsePageHeadVars(
                                $thread_page_info["PAGE"] ?? "");
                            $thread_source_allowed =
                                empty($thread_head['public_source']) ||
                                $thread_head['public_source'] == 'true';
                        }
                        if (!$thread_source_allowed) {
                            $parent->web_site->header(
                                "HTTP/1.0 404 Not Found");
                            $parent->displayView("nocache", $data);
                            \seekquarry\atto\webExit();
                        }
                    }
                }
            } else {
                $group = $group_model->getGroupById($group_id, $user_id,
                    true);
            }
            if ((!isset($_REQUEST['f']) ||
                !in_array($_REQUEST['f'], ["rss", "json", "serial"]))) {
                $pub_clause = ['pub_date', "=", "", "ASC"];
                $sort = "ksort";
                $parent->model("impression")->add($user_id, $just_thread,
                    C\THREAD_IMPRESSION);
                $parent->model("impression")->add($user_id, $group_id,
                    C\GROUP_IMPRESSION);
                $parser = new WikiParser("", true);
                $math = false;
                if (!empty($group) && $user_id == $group["OWNER_ID"]) {
                    $clipboard_id = $feed_model->getFeedClipboardId($user_id);
                    $clip_group_id = $group_model->getPersonalGroupId($user_id);
                    $clip_search_array = [
                        ["parent_id", "=", $clipboard_id, ""]];
                    $clip_items = $feed_model->getGroupItems(0,
                        1000, $clip_search_array, $user_id, -2);
                    $data['CLIP_ITEMS'] = [];
                    $csrf_token = C\p('CSRF_TOKEN') . "=" .
                        $parent->generateCSRFToken($user_id);
                    foreach ($clip_items as $clip_item) {
                        if ($clip_item['DESCRIPTION'] == 'start_feed_clip') {
                            continue;
                        }
                        $clip_item["DESCRIPTION"] =
                            $wiki_model->insertResourcesParsePage(
                            $clip_group_id,
                            "post" . $clip_item['ID'], L\getLocaleTag(),
                            $clip_item["DESCRIPTION"]);
                        $clip_item["DESCRIPTION"] = preg_replace(
                            '/\[{rtoken}\]/', $csrf_token,
                            $clip_item["DESCRIPTION"]);
                        $clip_item['USER_ICON'] = $user_model->getUserIconUrl(
                            $clip_item['USER_ID']);
                        $data['CLIP_ITEMS'][] = $clip_item;
                        if (!$math && strpos(
                            $clip_item["DESCRIPTION"], "`") !== false) {
                            $math = true;
                            if (!isset($data["INCLUDE_SCRIPTS"])) {
                                $data["INCLUDE_SCRIPTS"] = [];
                            }
                            $data["INCLUDE_SCRIPTS"][] = "math";
                        }
                    }
                }
            }
        }
        $search_array = [
            ["parent_id", "=", $just_thread, ""],
            ["group_id", "=", $just_group_id, ""],
            ["group_name", "NOT BEGINS WITH", C\PERSONAL_GROUP_PREFIX, ""],
            ["user_id", "=", $just_user_id, ""],
            $pub_clause];
        $for_group = ($just_group_id) ? $just_group_id : (($just_thread) ?
            -2 : -1);
        if (!empty($just_thread) ) {
            $data['JUST_THREAD'] = $just_thread;
        }
        list($item_count, $pages) = $this->initializeFeedItems($data, $pages,
            $user_id, $search_array, $for_group, $sort, $limit,
            $results_per_page);
        $data['SUBTITLE'] = "";
        $type = "";
        $type_id = "";
        if (!empty($just_thread) ) {
            $thread_start_item = $feed_model->getGroupItem($just_thread);
            if (empty($thread_start_item) ||
                empty($thread_start_item["TITLE"])) {
                $data['NO_POSTS_IN_THREAD'] = true;
                if ($just_thread < 0) {
                    $data['SUBTITLE'] = empty($pages[0][self::TITLE]) ?
                        "" : $pages[0][self::TITLE];
                    $data["GROUP_ID"] = -$just_thread;
                    $group = $group_model->getGroupById(
                        $data["GROUP_ID"], $user_id);
                    if (!empty($group)) {
                        $data["GROUP_NAME"] = $group["GROUP_NAME"];
                        $data['GROUP_STATUS'] = $group['STATUS'];
                    } else {
                        $title = "";
                        $data['SUBTITLE'] = "";
                    }
                }
            } else {
                $title = $thread_start_item["TITLE"];
                $data['SUBTITLE'] = trim($title, "\- \t\n\r\0\x0B");
                $type = "thread";
                $type_id = $just_thread;
                $group = $group_model->getGroupById(
                    $thread_start_item['GROUP_ID'], $user_id);
                $data["GROUP_ID"] = $thread_start_item['GROUP_ID'];
                if (!empty($group)) {
                    $data["GROUP_NAME"] = $group["GROUP_NAME"];
                    $data['GROUP_STATUS'] = $group['STATUS'];
                } else {
                    $title = "";
                    $data['SUBTITLE'] = "";
                    $data['NO_POSTS_IN_THREAD'] = true;
                }
            }
        }
        if (!$just_group_id && !$just_thread) {
           $data['GROUP_STATUS'] = C\ACTIVE_STATUS;
        }
        if ($just_group_id) {
            $group = $group_model->getGroupById($just_group_id, $user_id);
            if (!$group) {
                if ($user_id == C\PUBLIC_USER_ID) {
                    $_REQUEST = ['c' => "admin", 'a' => '',
                        C\p('CSRF_TOKEN') => ''];
                    return $parent->redirectWithMessage(
                        tl("social_component_login_first"));
                }
                unset($_REQUEST['route']);
                $_REQUEST['just_group_id'] = C\PUBLIC_GROUP_ID;
                return $parent->redirectWithMessage(
                    tl("social_component_no_group_access"), false, false,
                    true);
            }
            $data['GROUP_STATUS'] = $group['STATUS'];
            if (!isset($page[self::SOURCE_NAME]) ) {
                $page[self::SOURCE_NAME] = $group['GROUP_NAME'];
            }
            if (empty($pages) ) {
                $data['NO_POSTS_YET'] = true;
                if ($user_id == $group['OWNER_ID'] || $user_id == C\ROOT_ID) {
                        // this case happens when a group is no read
                        $data['NO_POSTS_START_THREAD'] = true;
                }
            }
            if ($user_id != C\PUBLIC_USER_ID &&
                !$group_model->checkUserGroup($user_id, $just_group_id)) {
                $data['SUBSCRIBE_LINK'] = $group_model->getRegisterType(
                    $just_group_id);
            }
            $data['SUBTITLE'] = $page[self::SOURCE_NAME];
            $type= "group";
            $type_id = $just_group_id;
            $data['JUST_GROUP_ID'] = $just_group_id;
            $parent->model("impression")->add($user_id, $just_group_id,
                C\GROUP_IMPRESSION);
        }
        if ($just_user_id) {
            $page["USER_NAME"] = $user_model->getUsername($just_user_id);
            $data['SUBTITLE'] = $page["USER_NAME"];
            $type = "user";
            $type_id = $just_user_id;
            $data['JUST_USER_ID'] = $just_user_id;
        }
        if ($user_id != C\PUBLIC_USER_ID) {
            $thread_ids = $parent->model("impression")->recent($user_id,
                C\THREAD_IMPRESSION, 3);
        }
        $this->calculateRecentFeedsAndThread($data, $user_id);
        $data['TOTAL_ROWS'] = $item_count + $groups_count;
        $data['RESULTS_PER_PAGE'] = $results_per_page;
        $token_string = ($user_id !=  C\PUBLIC_USER_ID )? C\p('CSRF_TOKEN') .
            "=".
            $parent->generateCSRFToken($user_id) : "";
        $data['PAGING_QUERY'] = htmlentities(B\feedsUrl($type, $type_id,
            true, $controller_name));
        $paging_query = $data['PAGING_QUERY'];
        if (!empty($type)) {
            $data['RSS_FEED_URL'] = $paging_query . "f=rss";
        }
        if ($view_mode == 'ungrouped') {
            $connector = (str_ends_with($paging_query, "?")) ? "" :
                "&";
            $paging_query .= "{$connector}v=ungrouped";
            $data['RSS_FEED_URL'] = $paging_query . "&f=rss";
        }
        $paging_query = html_entity_decode($paging_query);
        $data['SCRIPT'] .= " let nextPage = initNextResultsPage($limit," .
            " {$data['TOTAL_ROWS']}, $results_per_page, ".
            "'$paging_query&$token_string', '', 'results-container', ".
            "'result-batch');\n";
        if ($limit > 0) {
            $data['SCRIPT'] .= " let previousPage = initPreviousResultsPage(".
                "$limit, {$data['TOTAL_ROWS']}, $results_per_page, ".
                "'$paging_query&$token_string', 'results-container', ".
                "'result-batch');\n";
        }
        if (!empty($data['REFRESH_TIMESTAMP']) && !empty($pages)) {
            $max_pubdate = 0;
            $num_pages = count($pages);
            for ($i = 0; $i < $num_pages; $i++) {
                $page = $pages[$i];
                if (empty($page['PUBDATE']) ||
                    ($page['PUBDATE'] <= $data['REFRESH_TIMESTAMP'] &&
                    !empty($page['EDIT_DATE']) &&
                    $page['EDIT_DATE'] > $max_pubdate)) {
                    unset($pages[$i]);
                    $limit++;
                    continue;
                }
                if ($page['PUBDATE'] > $max_pubdate) {
                    $max_pubdate = $page['PUBDATE'];
                }
                if (!empty($page['EDIT_DATE']) &&
                    $page['EDIT_DATE'] > $max_pubdate) {
                    $max_pubdate = $page['EDIT_DATE'];
                }
            }
            if ($max_pubdate <= $data['REFRESH_TIMESTAMP']) {
                \seekquarry\atto\webExit();
            }
        }
        $data['PAGES'] = array_values($pages);
        if ($user_id != C\PUBLIC_USER_ID && !$just_thread) {
            $feed_thread_ids = [];
            foreach ($data['PAGES'] as $feed_page) {
                if (!empty($feed_page['PARENT_ID']) &&
                    $feed_page['PARENT_ID'] > 0) {
                    $feed_thread_ids[] = $feed_page['PARENT_ID'];
                }
            }
            $data['FOLLOWED_THREADS'] = $feed_model->getFollowedThreads(
                $user_id, $feed_thread_ids);
        }
        $data['LIMIT'] = $limit;
        $this->initializeWikiEditor($data, -1);
        return $data;
    }
    /**
     * calculateRecentFeedsAndThread used to add to $data information about the
     * most recently view threads and groups of the current user. This will be
     * used to populate the navigation dropdown in WikiView or WikiElement
     * @param array &$data associative array of values to be echoed by the view
     * @param int $user_id id of user requesting thread info
     */
    public function calculateRecentFeedsAndThread(&$data, $user_id)
    {
        if ($user_id == C\PUBLIC_USER_ID) {
            return;
        }
        $parent = $this->parent;
        $group_model = $parent->model("group");
        $wiki_model = $parent->model("wiki");
        $feed_model = $parent->model("feed");
        $personal_group_id = $group_model->getPersonalGroupId($user_id);
        $thread_ids = $parent->model("impression")->recent($user_id,
            C\THREAD_IMPRESSION, 5);
        $group_ids = $parent->model("impression")->recent($user_id,
            C\GROUP_IMPRESSION, 10);
        if (!empty($thread_ids)) {
            $data['RECENT_THREADS'] = [];
            foreach ($thread_ids as $recent_thread_id) {
                $thread_start_item = $feed_model->getGroupItem(
                    $recent_thread_id);
                $thread_name = $thread_start_item['TITLE'] ?? "";
                if (!empty($thread_name) &&
                    (empty($data['JUST_THREAD']) ||
                    $recent_thread_id != $data['JUST_THREAD'])) {
                    $data['RECENT_THREADS'][$thread_name] =
                        htmlentities(B\feedsUrl("thread", $recent_thread_id,
                        true,  $data['CONTROLLER']));
                }
            }
        }
        if (!empty($group_ids)) {
            $data['RECENT_GROUPS'] = [];
            $thread_group_id = (empty($data["JUST_GROUP_ID"])) ?
                (empty($data["GROUP_ID"]) ? -1 : $data["GROUP_ID"] ) :
                $data["JUST_GROUP_ID"];
            $len = strlen(C\PERSONAL_GROUP_PREFIX);
            foreach ($group_ids as $recent_group_id) {
                $group_name = $group_model->getGroupName(
                    $recent_group_id);
                if (!empty($group_name) && !empty($thread_group_id) &&
                    $recent_group_id != $thread_group_id &&
                    $recent_group_id != $personal_group_id &&
                    substr($group_name, 0, $len) !=
                        C\PERSONAL_GROUP_PREFIX) {
                    $data['RECENT_GROUPS'][$group_name] =
                        htmlentities(B\feedsUrl("group",  $recent_group_id,
                        false,  $data['CONTROLLER']));
                }
            }
            if (count($data['RECENT_GROUPS']) > 5) {
                array_pop($data['RECENT_GROUPS']);
            }
        }
    }
    /**
     * calculateGroupedFeeds gathers what the view of a person's
     * feeds needs to draw them gathered under the name of each
     * group, rather than as one list of threads and posts
     * @param int $user_id id of current user
     * @param int $limit lower bound on the groups to display feed data for
     * @param int $results_per_page number of groups to display feed data for
     * @param string $controller_name name of controller on which this this
     *     component lives (either admin or group). Used by view to draw expand
     *     or collapse link
     * @param array $data field data for view to draw itself
     * @return array $data after being augmented with the grouped-feed keys
     *     (MODE, group_sorts, plus per-group thread/post slices) that
     *     GroupfeedView consumes
     */
    public function calculateGroupedFeeds($user_id, $limit, $results_per_page,
        $controller_name, $data)
    {
        $parent = $this->parent;
        $group_model = $parent->model("group");
        $wiki_model = $parent->model("wiki");
        $feed_model = $parent->model("feed");
        $data['MODE'] = 'grouped';
        $data['group_sorts'] = [ "name_asc" =>
            html_entity_decode(tl('social_component_name_asc')),
            "name_desc" => html_entity_decode(tl('social_component_name_desc')),
            "join_asc" => html_entity_decode(tl('social_component_join_asc')),
            "join_desc" => html_entity_decode(tl('social_component_join_desc')),
        ];
        $data['GROUP_SORT'] = (!empty($_REQUEST['group_sort']) &&
            isset($data['group_sorts'][$_REQUEST['group_sort']])) ?
            $_REQUEST['group_sort'] : "join_desc";
        $data["GROUP_FILTER"] = (empty($_REQUEST['group_filter'])) ?
            "" : $parent->clean($_REQUEST['group_filter'], "string");
        $search_array = [];
        if ($data["GROUP_FILTER"]) {
            if ($data["GROUP_FILTER"][0] == '=') {
                $name_clause = ["name", "=", substr($data["GROUP_FILTER"],1)];
            } else {
                $name_clause = ["name", "CONTAINS", $data["GROUP_FILTER"]];
            }
        } else {
            $name_clause = ["name", "", ""];
        }
        $name_clause[3] = ($data['GROUP_SORT'] == "name_asc") ?
            "ASC" : ($data['GROUP_SORT'] == "name_desc" ? "DESC" : "");
        if ($name_clause != ["name", "", "", ""]) {
            $search_array[] = $name_clause;
        }
        $join_clause = ["join_date", "", ""];
        $join_clause[3] = ($data['GROUP_SORT'] == "join_asc") ?
            "ASC" : ($data['GROUP_SORT'] == "join_desc" ? "DESC" : "");
        if ($join_clause != ["join_date", "", "", ""]) {
            $search_array[] = $join_clause;
        }
        $data['GROUPS'] = $group_model->getRows($limit, $results_per_page,
            $data['NUM_GROUPS'], $search_array, [$user_id, false]);
        $this->addActivityInfoToGroups($data);
        $data['LIMIT'] = $limit;
        $data['RESULTS_PER_PAGE'] = $results_per_page;
        $data['PAGING_QUERY'] = B\feedsUrl("", "",
            true, $controller_name);
        return $data;
    }

    /**
     * addComment adds a comment to a thread and tells whoever follows it.
     *
     * groupFeeds calls it for the addcomment argument, so each thing
     * a reader can do to a feed is read on its own rather than
     * inside one long switch.
     *
     * @param array $data what the page will show, added to here
     * @param int $user_id which reader is asking
     * @return mixed what is handed back to the browser, or
     *     nothing where the page is drawn as usual
     */
    public function addComment(&$data, $user_id)
    {
        $parent = $this->parent;
        $group_model = $parent->model("group");
        $feed_model = $parent->model("feed");
        $user_model = $parent->model("user");
        $username = $user_model->getUsername($user_id);
        list(, $description) = $this->feedPostFields();
        if (!empty($_REQUEST['page_type']) &&
             $_REQUEST['page_type'] == "page_and_feedback") {
             $_REQUEST['a'] = "wiki";
             unset($_REQUEST['just_thread']);
             unset($_REQUEST['limit']);
             unset($_REQUEST['num']);
        }
        if (!isset($_REQUEST['parent_id'])
            || !$_REQUEST['parent_id']
            || !isset($_REQUEST['group_id'])
            || !$_REQUEST['group_id']) {
            return $parent->redirectWithMessage(
                tl('social_component_comment_error'),
                ['page_name']);
        }
        if (!$description) {
            return $parent->redirectWithMessage(
                tl('social_component_no_comment'),
                ['page_name']);
        }
        $parent_id = $parent->clean($_REQUEST['parent_id'], "int");
        $group_id = $parent->clean($_REQUEST['group_id'], "int");
        $group = $group_model->getGroupById($group_id,
            $user_id, true);
        $read_comment = [C\GROUP_READ_COMMENT, C\GROUP_READ_WRITE,
            C\GROUP_READ_WIKI];
        if (!$group || $user_id == C\PUBLIC_USER_ID ||
            ($group["OWNER_ID"] != $user_id &&
            !in_array($group["MEMBER_ACCESS"], $read_comment) &&
            $user_id != C\ROOT_ID)) {
            return $parent->redirectWithMessage(
                tl('social_component_no_post_access'),
                ['page_name']);
        }
        if ($parent_id >= 0) {
            $parent_item = $feed_model->getGroupItem($parent_id);
            if (!$parent_item) {
                return $parent->redirectWithMessage(
                    tl('social_component_no_post_access'),
                    ['page_name']);
            }
        } else {
            $parent_item = [
                'TITLE' => tl('social_component_join_group',
                    $username, $group['GROUP_NAME']),
                'DESCRIPTION' =>
                    tl('social_component_join_group_detail',
                        date("r", $group['JOIN_DATE']),
                        $group['GROUP_NAME']),
                'ID' => -$group_id,
                'PARENT_ID' => -$group_id,
                'GROUP_ID' => $group_id
            ];
        }
        $title = "-- " . $parent_item['TITLE'];
        $id = $this->addGroupItemWithinLimits($parent_item["ID"],
            $group_id, $user_id, $title, $description);
        $feed_model->setThreadSubscribe($user_id,
            $parent_item["ID"], true);
        list($bots_called, $post_parts) =
            $this->getRequestedBots($group_id, $description);
        $result = $this->handleResourceUploads(
                $group_id, "post" . $id);
        if ($result == self::UPLOAD_FAILED) {
            return $parent->redirectWithMessage(
                tl('social_component_upload_error'));
        }
        $followers = $feed_model->getThreadSubscribers(
            $parent_item["ID"], $user_id);
        $server = new SmtpClient(C\p('MAIL_SENDER'),
            C\p('MAIL_SERVER'),
            C\p('MAIL_SERVERPORT'), C\p('MAIL_USERNAME'),
                C\p('MAIL_PASSWORD'),
            C\p('MAIL_SECURITY'));
        $post_url = "";
        if (in_array($group['REGISTER_TYPE'],
            [C\PUBLIC_BROWSE_REQUEST_JOIN, C\PUBLIC_JOIN])) {
            $post_url = B\feedsUrl("thread", $parent_item["ID"],
                true, "group", false) . "preserve=true\n";
        }
        $subject = tl('social_component_thread_notification',
            $parent_item['TITLE']);
        $body = tl('social_component_notify_body') . "\n" .
            $parent_item['TITLE']."\n".
            $post_url .
            tl('social_component_notify_closing')."\n".
            tl('social_component_notify_signature');
        foreach ($followers as $follower) {
            if (empty($follower['USER_ID'])) {
                continue;
            }
            if (!$user_model->isBotUser($follower['USER_ID'])) {
                $message = tl('social_component_notify_salutation',
                    $follower['USER_NAME']) . "\n\n";
                $message .= $body;
                $server->send($subject, C\p('MAIL_SENDER'),
                    $follower['EMAIL'], $message);
            }
        }
        $this->addAnyBotResponses($parent_item["ID"], $group_id,
            $bots_called, $title, $post_parts);
        return $parent->redirectWithMessage(
            tl('social_component_comment_added'), ['page_name']);
    }
    /**
     * addFeedContact adds the writer of a post to the reader's contacts.
     *
     * groupFeeds calls it for the addcontact argument, so each thing
     * a reader can do to a feed is read on its own rather than
     * inside one long switch.
     *
     * @param array $data what the page will show, added to here
     * @param int $user_id which reader is asking
     * @return mixed what is handed back to the browser, or
     *     nothing where the page is drawn as usual
     */
    public function addFeedContact(&$data, $user_id)
    {
        $contact_id = (empty($_REQUEST["contact_id"]) ? 0 :
            $this->parent->clean($_REQUEST["contact_id"], "int"));
        $data['CONTACT_ID'] = $contact_id;
        return $this->addContact($user_id, $data);
    }
    /**
     * addFeedGroup joins the reader to the group a post was made in.
     *
     * groupFeeds calls it for the addgroup argument, so each thing
     * a reader can do to a feed is read on its own rather than
     * inside one long switch.
     *
     * @param array $data what the page will show, added to here
     * @param int $user_id which reader is asking
     * @return mixed what is handed back to the browser, or
     *     nothing where the page is drawn as usual
     */
    public function addFeedGroup(&$data, $user_id)
    {
        $parent = $this->parent;
        $group_model = $parent->model("group");
        $wiki_model = $parent->model("wiki");
        $feed_model = $parent->model("feed");
        $just_group_id = (empty($_REQUEST["just_group_id"]) ? 0 :
            $parent->clean($_REQUEST["just_group_id"], "int"));
        if ($_SESSION['USER_ID'] == C\PUBLIC_USER_ID) {
            return $parent->redirectWithMessage(
                tl('social_component_public_cant_add'));
        }
        $register =
            $group_model->getRegisterType($just_group_id);
        if ($just_group_id > 0 && !empty($register)
            && $register != C\INVITE_ONLY_JOIN) {
            if ($register >= C\LOW_JOIN_FEE &&
                !in_array(C\p('MONETIZATION_TYPE'),
                ['group_fees','fees_and_keywords'])) {
                $register = C\INVITE_ONLY_JOIN;
                return $parent->redirectWithMessage(
                    tl('social_component_groupname_cant_add'));
            }
            return
                $this->addGroup($data, $just_group_id, $register);
        } else {
            return $parent->redirectWithMessage(
                tl('social_component_groupname_cant_add'));
        }
    }
    /**
     * copyOrCutPost puts a post on the clipboard, and marks it to be
     * removed once pasted where a cut was asked for.
     *
     * groupFeeds calls it for the copypost or cutpost argument, so each thing
     * a reader can do to a feed is read on its own rather than
     * inside one long switch.
     *
     * @param array $data what the page will show, added to here
     * @param int $user_id which reader is asking
     * @return mixed what is handed back to the browser, or
     *     nothing where the page is drawn as usual
     */
    public function copyOrCutPost(&$data, $user_id)
    {
        $parent = $this->parent;
        $group_model = $parent->model("group");
        $wiki_model = $parent->model("wiki");
        $feed_model = $parent->model("feed");
        $arg = $_REQUEST['arg'] ?? "";
        $post_id = $parent->clean($_REQUEST['post_id'], "int");
        $post = $feed_model->getGroupItem($post_id);
        if (empty($post)) {
            return $parent->redirectWithMessage(
                tl('social_component_invalid_message_id'));
        }
        $group = $group_model->getGroupById($post['GROUP_ID'],
            $user_id);
        if (empty($group) || $group['OWNER_ID'] != $user_id) {
            return $parent->redirectWithMessage(
                tl('social_component_invalid_access'));
        }
        $clipboard_id = $feed_model->getFeedClipboardId($user_id);
        if (!$clipboard_id) {
            return $parent->redirectWithMessage(
                tl('social_component_clipboard_unavailable'));
        }
        if ($feed_model->getThreadPostCount($clipboard_id)
            >= C\MAX_THREAD_CLIPBOARD_ITEMS) {
            return $parent->redirectWithMessage(
                tl('social_component_clipboard_full'));
        }
        $clip_group_id = $group_model->getPersonalGroupId($user_id);
        $clipped_item_id = $this->addGroupItemWithinLimits(
            $clipboard_id,
            $clip_group_id, $post['USER_ID'], $post['TITLE'],
            $post['DESCRIPTION'], $post['TYPE'], $post['PUBDATE'],
            "", $post['UPS'], $post['DOWNS'], $post['FLAG']);
        $feed_model->copyThreadResources($post["GROUP_ID"],
            $post_id, $clip_group_id, $clipped_item_id);
        if ($arg == 'copypost') {
            return $parent->redirectWithMessage(
                tl('social_component_message_clipcopied'));
        }
        //note a cut will do a delete after the copy
    }
    /**
     * deletePost removes a post, and the thread with it where the post
     * started one.
     *
     * groupFeeds calls it for the deletepost argument, so each thing
     * a reader can do to a feed is read on its own rather than
     * inside one long switch.
     *
     * @param array $data what the page will show, added to here
     * @param int $user_id which reader is asking
     * @return mixed what is handed back to the browser, or
     *     nothing where the page is drawn as usual
     */
    public function deletePost(&$data, $user_id)
    {
        $parent = $this->parent;
        $group_model = $parent->model("group");
        $wiki_model = $parent->model("wiki");
        $feed_model = $parent->model("feed");
        $just_thread = (empty($_REQUEST["just_thread"]) ? 0 :
            $parent->clean($_REQUEST["just_thread"], "int"));
        $arg = $_REQUEST['arg'] ?? "";
        if (!empty($_REQUEST['page_type']) &&
            $_REQUEST['page_type'] == "page_and_feedback") {
            $_REQUEST['a'] = "wiki";
        }
        if (!isset($_REQUEST['post_id'])) {
            return $parent->redirectWithMessage(
                tl('social_component_delete_error'),
                ['page_name']);
        }
        $post_id = $parent->clean($_REQUEST['post_id'], "int");
        $group_item = $feed_model->getGroupItem($post_id);
        $success = false;
        if ($group_item) {
            // this method checks if user can delete post
            $success =
                $feed_model->deleteGroupItem($post_id, $user_id);
        }
        $search_array = [["parent_id", "=", $just_thread, ""]];
        $item_count = $feed_model->getGroupItemCount($search_array,
            $user_id, -1);
        if (!empty($_REQUEST['page_type']) &&
             $_REQUEST['page_type'] == "page_and_feedback") {
             unset($_REQUEST['just_thread']);
             $_REQUEST['group_id'] = $group_item['GROUP_ID'];
        }
        if ($success) {
            $wiki_model->deleteResources($group_item["GROUP_ID"],
                "post" . $post_id);
            if ($item_count == 0) {
                unset($_REQUEST['just_thread']);
            }
            if ($arg == 'cutpost') {
                return $parent->redirectWithMessage(
                    tl('social_component_item_clipcut'),
                    ['page_name']);
            } else {
                return $parent->redirectWithMessage(
                    tl('social_component_item_deleted'),
                    ['page_name']);
            }
        } else {
            if ($arg == 'cutpost') {
                return $parent->redirectWithMessage(
                    tl('social_component_item_cut_nodelete'),
                    ['page_name']);
            } else {
                return $parent->redirectWithMessage(
                    tl('social_component_no_item_deleted'),
                    ['page_name']);
            }
        }
    }
    /**
     * pasteAllPosts pastes every post on the clipboard into a thread.
     *
     * groupFeeds calls it for the pasteall argument, so each thing
     * a reader can do to a feed is read on its own rather than
     * inside one long switch.
     *
     * @param array $data what the page will show, added to here
     * @param int $user_id which reader is asking
     * @return mixed what is handed back to the browser, or
     *     nothing where the page is drawn as usual
     */
    public function pasteAllPosts(&$data, $user_id)
    {
        $parent = $this->parent;
        $group_model = $parent->model("group");
        $feed_model = $parent->model("feed");
        $just_thread = (empty($_REQUEST["just_thread"]) ? 0 :
            $parent->clean($_REQUEST["just_thread"], "int"));
        $clipboard_id = $feed_model->getFeedClipboardId($user_id);
        $clip_group_id = $group_model->getPersonalGroupId($user_id);
        if (empty($just_thread)) {
            return $parent->redirectWithMessage(
                tl('social_component_no_target_thread'));
        }
        $thread_item = $feed_model->getGroupItem($just_thread);
        if (!$thread_item) {
            return $parent->redirectWithMessage(
                tl('social_component_no_thread_access'));
        }
        $paste_group_id = $thread_item['GROUP_ID'];
        $clip_search_array = [
            ["parent_id", "=", $clipboard_id, ""]];
        $clip_items = $feed_model->getGroupItems(0,
            C\MAX_THREAD_CLIPBOARD_ITEMS, $clip_search_array,
            $user_id, -2);
        foreach ($clip_items as $clip_item) {
            if ($clip_item['DESCRIPTION'] == 'start_feed_clip') {
                continue;
            }
            $pasted_id = $this->addGroupItemWithinLimits(
                $just_thread,
                $paste_group_id , $clip_item['USER_ID'],
                $clip_item['TITLE'], $clip_item['DESCRIPTION'],
                $clip_item['TYPE'], $clip_item['PUBDATE'], "",
                $clip_item['UPS'], $clip_item['DOWNS'],
                $clip_item['FLAG']);
            $feed_model->copyThreadResources($clip_group_id,
                $clip_item['ID'], $paste_group_id, $pasted_id);
        }
        return $parent->redirectWithMessage(
            tl('social_component_clipboard_pasted'));
    }
    /**
     * pastePost pastes one post from the clipboard into a thread.
     *
     * groupFeeds calls it for the pastepost argument, so each thing
     * a reader can do to a feed is read on its own rather than
     * inside one long switch.
     *
     * @param array $data what the page will show, added to here
     * @param int $user_id which reader is asking
     * @return mixed what is handed back to the browser, or
     *     nothing where the page is drawn as usual
     */
    public function pastePost(&$data, $user_id)
    {
        $parent = $this->parent;
        $group_model = $parent->model("group");
        $feed_model = $parent->model("feed");
        $just_thread = (empty($_REQUEST["just_thread"]) ? 0 :
            $parent->clean($_REQUEST["just_thread"], "int"));
        $clipboard_id = $feed_model->getFeedClipboardId($user_id);
        $clip_group_id = $group_model->getPersonalGroupId($user_id);
        $post_id = $parent->clean($_REQUEST['post_id'], "int");
        $post = $feed_model->getGroupItem($post_id);
        if (empty($post)) {
            return $parent->redirectWithMessage(
                tl('social_component_invalid_message_id'));
        }
        if (empty($just_thread)) {
            return $parent->redirectWithMessage(
                tl('social_component_no_target_thread'));
        }
        $thread_item = $feed_model->getGroupItem($just_thread);
        if (!$thread_item) {
            return $parent->redirectWithMessage(
                tl('social_component_no_thread_access'));
        }
        $paste_group_id = $thread_item['GROUP_ID'];
        $pasted_id = $this->addGroupItemWithinLimits($just_thread,
            $paste_group_id , $post['USER_ID'],
            $post['TITLE'], $post['DESCRIPTION'],
            $post['TYPE'], $post['PUBDATE'], "",
            $post['UPS'], $post['DOWNS'],
            $post['FLAG']);
        $feed_model->copyThreadResources($clip_group_id,
            $post_id, $paste_group_id, $pasted_id);
        return $parent->redirectWithMessage(
            tl('social_component_clipboard_pasted'));
    }
    /**
     * flagPost marks a post for a moderator to look at.
     *
     * groupFeeds calls it for the flagpost argument, so each thing
     * a reader can do to a feed is read on its own rather than
     * inside one long switch.
     *
     * @param array $data what the page will show, added to here
     * @param int $user_id which reader is asking
     * @return mixed what is handed back to the browser, or
     *     nothing where the page is drawn as usual
     */
    public function flagPost(&$data, $user_id)
    {
        $parent = $this->parent;
        $group_model = $parent->model("group");
        $feed_model = $parent->model("feed");
        if (!isset($_REQUEST['post_id'])) {
            return $parent->redirectWithMessage(
                tl('social_component_flag_error'),
                ['page_name']);
        }
        $post_id = $parent->clean($_REQUEST['post_id'], "int");
        $group_item = $feed_model->getGroupItem($post_id);
        if ($group_model->alreadyFlagged($user_id, $post_id)) {
            return $parent->redirectWithMessage(
                tl('social_component_already_flagged'));
        }
        $success = false;
        if ($group_item) {
            $success =
                $feed_model->flagGroupItem($post_id, $user_id);
        }
        if ($success) {
            return $parent->redirectWithMessage(
                tl('social_component_item_flagged'),
                ['page_name']);
        } else {
            return $parent->redirectWithMessage(
                tl('social_component_no_item_flagged'),
                ['page_name']);
        }
    }
    /**
     * approveFlagged clears a mark a reader put on a post.
     *
     * groupFeeds calls it for the approveflagged argument, so each thing
     * a reader can do to a feed is read on its own rather than
     * inside one long switch.
     *
     * @param array $data what the page will show, added to here
     * @param int $user_id which reader is asking
     * @return mixed what is handed back to the browser, or
     *     nothing where the page is drawn as usual
     */
    public function approveFlagged(&$data, $user_id)
    {
        $parent = $this->parent;
        $group_model = $parent->model("group");
        $feed_model = $parent->model("feed");
        if (!isset($_REQUEST['post_id'])){
            return $parent->redirectWithMessage(
              tl('social_component_no_approve_access'));
        }
        $post_id = $parent->clean($_REQUEST['post_id'], "int");
        $group_item = $feed_model->getGroupItem($post_id);
        $success = false;
        if ($group_item) {
           $success = $feed_model->approveFlaggedPost($post_id);
        }
        if ($success) {
            return $parent->redirectWithMessage(
                tl('social_component_flagged_item_approved'),
                ['page_name']);
        } else {
            return $parent->redirectWithMessage(
                tl('social_component_no_flagged_item_approved'),
                ['page_name']);
        }
    }
    /**
     * deleteFlagged removes a post a reader marked.
     *
     * groupFeeds calls it for the deleteflagged argument, so each thing
     * a reader can do to a feed is read on its own rather than
     * inside one long switch.
     *
     * @param array $data what the page will show, added to here
     * @param int $user_id which reader is asking
     * @return mixed what is handed back to the browser, or
     *     nothing where the page is drawn as usual
     */
    public function deleteFlagged(&$data, $user_id)
    {
        $parent = $this->parent;
        $group_model = $parent->model("group");
        $feed_model = $parent->model("feed");
        if (!isset($_REQUEST['post_id'])){
            return $parent->redirectWithMessage(
              tl('social_component_no_delete_access'));
        }
        $post_id = $parent->clean($_REQUEST['post_id'], "int");
        $group_item = $feed_model->getGroupItem($post_id);
        $success = false;
        if ($group_item) {
            $success = $feed_model->deleteFlaggedPost($post_id,
                tl('social_component_flagged_and_removed'));
        }
        if ($success) {
            return $parent->redirectWithMessage(
                tl('social_component_flagged_item_deleted'),
                ['page_name']);
        } else {
            return $parent->redirectWithMessage(
                tl('social_component_no_flagged_item_deleted'),
                ['page_name']);
        }
    }
    /**
     * downVotePost records a reader's vote against a post.
     *
     * groupFeeds calls it for the downvote argument, so each thing
     * a reader can do to a feed is read on its own rather than
     * inside one long switch.
     *
     * @param array $data what the page will show, added to here
     * @param int $user_id which reader is asking
     * @return mixed what is handed back to the browser, or
     *     nothing where the page is drawn as usual
     */
    public function downVotePost(&$data, $user_id)
    {
        $parent = $this->parent;
        $group_model = $parent->model("group");
        $feed_model = $parent->model("feed");
        if (!isset($_REQUEST['group_id']) || !$_REQUEST['group_id']
            ||!isset($_REQUEST['post_id']) ||
            !$_REQUEST['post_id']) {
            return $parent->redirectWithMessage(
                tl('social_component_vote_error'));
        }
        $post_id = $parent->clean($_REQUEST['post_id'], "int");
        $group_id = $parent->clean($_REQUEST['group_id'], "int");
        $group = $group_model->getGroupById($group_id,
            $user_id, true);
        if (!$group || $user_id == C\PUBLIC_USER_ID
            || (!in_array($group["VOTE_ACCESS"],
            [C\UP_DOWN_VOTING_GROUP] ) ) ) {
            return $parent->redirectWithMessage(
                tl('social_component_no_vote_access'));
        }
        $post_item = $feed_model->getGroupItem($post_id);
        if (!$post_item || $post_item['GROUP_ID'] != $group_id) {
            return $parent->redirectWithMessage(
                tl('social_component_no_post_access'));
        }
        if ($group_model->alreadyVoted($user_id, $post_id)) {
            return $parent->redirectWithMessage(
                tl('social_component_already_voted'));
        }
        $group_model->voteDown($user_id, $post_id);
        return $parent->redirectWithMessage(
            tl('social_component_vote_recorded'));
    }
    /**
     * startNewThread starts a thread in a group and tells its members.
     *
     * groupFeeds calls it for the newthread argument, so each thing
     * a reader can do to a feed is read on its own rather than
     * inside one long switch.
     *
     * @param array $data what the page will show, added to here
     * @param int $user_id which reader is asking
     * @return mixed what is handed back to the browser, or
     *     nothing where the page is drawn as usual
     */
    public function startNewThread(&$data, $user_id)
    {
        $parent = $this->parent;
        $group_model = $parent->model("group");
        $feed_model = $parent->model("feed");
        $user_model = $parent->model("user");
        list($title, $description) = $this->feedPostFields();
        if (!isset($_REQUEST['group_id']) ||
            !$_REQUEST['group_id']) {
            return $parent->redirectWithMessage(
                tl('social_component_comment_error'));
        }
        $group_id = $parent->clean($_REQUEST['group_id'], "int");
        if (!$description || !$title) {
            return $parent->redirectWithMessage(
                tl('social_component_need_title_description'));
        }
        $group_id = $parent->clean($_REQUEST['group_id'], "int");
        $group = $group_model->getGroupById($group_id,
            $user_id, true);
        $new_thread = [C\GROUP_READ_WRITE, C\GROUP_READ_WIKI];
        if (!$group || $user_id == C\PUBLIC_USER_ID ||
            ($group["OWNER_ID"] != $user_id &&
            !in_array($group["MEMBER_ACCESS"], $new_thread) &&
            $user_id != C\ROOT_ID)) {
            return $parent->redirectWithMessage(
                tl('social_component_no_post_access'));
        }
        $thread_id = $this->addGroupItemWithinLimits(0,
            $group_id, $user_id, $title, $description);
        $feed_model->setThreadSubscribe($user_id,
            $thread_id, true);
        list($bots_called, $post_parts) =
            $this->getRequestedBots($group_id, $description);
        $result = $this->handleResourceUploads(
                $group_id, "post" . $thread_id);
        if ($result == self::UPLOAD_FAILED) {
            return $parent->redirectWithMessage(
                tl('social_component_upload_error'));
        }
        if ($user_id == $group['OWNER_ID']) {
            $followers = $group_model->getGroupUsers(
                $group_id, "", [], "",
                C\NUM_RESULTS_PER_PAGE, true);
        } else {
            $owner_name = $user_model->getUsername(
                $group['OWNER_ID']);
            $follower = $user_model->getUser($owner_name);
            $followers = [$follower];
        }
        $server = new SmtpClient(C\p('MAIL_SENDER'),
            C\p('MAIL_SERVER'),
            C\p('MAIL_SERVERPORT'), C\p('MAIL_USERNAME'),
                C\p('MAIL_PASSWORD'),
            C\p('MAIL_SECURITY'));
        $subject = tl('social_component_new_thread_mail',
            $group['GROUP_NAME']);
        $post_url = B\feedsUrl("thread", $thread_id, true,
            "group", false)."preserve=true\n";
        $body = tl('social_component_new_thread_body',
            $group['GROUP_NAME'])."\n".
            "\"".$title."\"\n".
            $post_url .
            tl('social_component_notify_closing')."\n".
            tl('social_component_notify_signature');
        $unsubscribe_mailto =
            ML\MailSiteFactory::unsubscribeMailtoAddress(
            C\p('MAIL_SENDER'));
        foreach ($followers as $follower) {
            if ($follower['USER_ID'] != $user_id &&
                ($user_id == $group['OWNER_ID'] ||
                 $follower['USER_ID'] == $group['OWNER_ID'])
            && !$user_model->isBotUser($follower['USER_ID'])) {
                $message = tl('social_component_notify_salutation',
                    $follower['USER_NAME'])."\n\n";
                $message .= $body;
                $extra_headers =
                    ML\UnsubscribeToken::listUnsubscribeHeaders(
                    $follower['USER_ID'], $group_id,
                    C\baseUrl(), $unsubscribe_mailto);
                $server->send($subject, C\p('MAIL_SENDER'),
                    $follower['EMAIL'], $message,
                    $extra_headers);
            }
        }
        $this->addAnyBotResponses($thread_id, $group_id,
            $bots_called, "--" . $title, $post_parts);
        $thread_url = B\feedsUrl('thread', $thread_id) .
            C\p('CSRF_TOKEN') . "=" .
            $parent->generateCSRFToken($_SESSION["USER_ID"]) ;
        $_SESSION['DISPLAY_MESSAGE'] =
            tl('social_component_thread_created');
        return $parent->redirectWithMessage(
            tl('social_component_thread_created'));
    }
    /**
     * feedStatus gives the part of a feed that has changed since it was
     * last drawn.
     *
     * groupFeeds calls it for the status argument, so each thing
     * a reader can do to a feed is read on its own rather than
     * inside one long switch.
     *
     * @param array $data what the page will show, added to here
     * @param int $user_id which reader is asking
     */
    public function feedStatus(&$data, $user_id)
    {
        $parent = $this->parent;
        $data['REFRESH'] = "feedstatus";
        if (!empty($_REQUEST['feed_time'])) {
            $data['REFRESH_TIMESTAMP'] = $parent->clean(
                $_REQUEST['feed_time'], "int");
        }
    }
    /**
     * updatePost saves an edit to a post.
     *
     * groupFeeds calls it for the updatepost argument, so each thing
     * a reader can do to a feed is read on its own rather than
     * inside one long switch.
     *
     * @param array $data what the page will show, added to here
     * @param int $user_id which reader is asking
     * @return mixed what is handed back to the browser, or
     *     nothing where the page is drawn as usual
     */
    public function updatePost(&$data, $user_id)
    {
        $parent = $this->parent;
        $group_model = $parent->model("group");
        $feed_model = $parent->model("feed");
        list($title, $description) = $this->feedPostFields();
        if (!empty($_REQUEST['page_type']) &&
             $_REQUEST['page_type'] == "page_and_feedback") {
             $_REQUEST['a'] = "wiki";
             unset($_REQUEST['limit']);
             unset($_REQUEST['num']);
        }
        if (!isset($_REQUEST['post_id'])) {
            return $parent->redirectWithMessage(
                tl('social_component_comment_error'),
                ['page_name']);
        }
        if (!$description || !$title) {
            return $parent->redirectWithMessage(
                tl('social_component_need_title_description'),
                ['page_name']);
        }
        $post_id = $parent->clean($_REQUEST['post_id'], "int");
        $action = "updatepost" . $post_id;
        if (!$parent->checkCSRFTime(C\p('CSRF_TOKEN'), $action)) {
            return $parent->redirectWithMessage(
                tl('social_component_post_edited_elsewhere'),
                ['page_name']);
        }
        $items = $feed_model->getGroupItems(0, 1,
            [["post_id", "=", $post_id, ""]], $user_id);
        if (isset($items[0])) {
            $item = $items[0];
        } else {
            return $parent->redirectWithMessage(
                tl('social_component_no_update_access'),
                ['page_name']);
        }
        $group_id = $item['GROUP_ID'];
        $_REQUEST['group_id'] = $group_id;
        $group = $group_model->getGroupById($group_id, $user_id,
            true);
        $update_thread = [C\GROUP_READ_WRITE, C\GROUP_READ_WIKI];
        if ($post_id != $item['PARENT_ID'] && $post_id > 0) {
            $update_thread[] = C\GROUP_READ_COMMENT;
            $parent_items = $feed_model->getGroupItems(0, 1,
                [["post_id", "=", $item['PARENT_ID'], ""]],
                $user_id);
            if (!empty($parent_items[0])) {
                $parent_item = $parent_items[0];
                $title = "-- " . $parent_item['TITLE'];
            }
        }
        if (!$group || $user_id == C\PUBLIC_USER_ID ||
            ($group["OWNER_ID"] != $user_id &&
            !in_array($group["MEMBER_ACCESS"], $update_thread) &&
            $user_id != C\ROOT_ID)) {
            return $parent->redirectWithMessage(
                tl('social_component_no_update_access'),
                ['page_name']);
        }
        $feed_model->updateGroupItem($post_id, $title,
            $description);
        $result = $this->handleResourceUploads(
            $group_id, "post" . $post_id);
        if ($result == self::UPLOAD_FAILED) {
            return $parent->redirectWithMessage(
                tl('social_component_upload_error'),
                ['page_name']);
        }
        return $parent->redirectWithMessage(
            tl('social_component_post_updated'),
            ['page_name']);
    }
    /**
     * upVotePost records a reader's vote for a post.
     *
     * groupFeeds calls it for the upvote argument, so each thing
     * a reader can do to a feed is read on its own rather than
     * inside one long switch.
     *
     * @param array $data what the page will show, added to here
     * @param int $user_id which reader is asking
     * @return mixed what is handed back to the browser, or
     *     nothing where the page is drawn as usual
     */
    public function upVotePost(&$data, $user_id)
    {
        $parent = $this->parent;
        $group_model = $parent->model("group");
        $feed_model = $parent->model("feed");
        if (!isset($_REQUEST['group_id']) || !$_REQUEST['group_id']
            ||!isset($_REQUEST['post_id']) ||
            !$_REQUEST['post_id']) {
            return $parent->redirectWithMessage(
                tl('social_component_vote_error'));
        }
        $post_id = $parent->clean($_REQUEST['post_id'], "int");
        $group_id = $parent->clean($_REQUEST['group_id'], "int");
        $group = $group_model->getGroupById($group_id, $user_id,
            true);
        if (!$group || $user_id == C\PUBLIC_USER_ID ||
            (!in_array($group["VOTE_ACCESS"],
            [C\UP_VOTING_GROUP, C\UP_DOWN_VOTING_GROUP] ) ) ) {
            return $parent->redirectWithMessage(
                tl('social_component_no_vote_access'));
        }
        $post_item = $feed_model->getGroupItem($post_id);
        if (!$post_item || $post_item['GROUP_ID'] != $group_id) {
            return $parent->redirectWithMessage(
                tl('social_component_no_post_access'));
        }
        if ($group_model->alreadyVoted($user_id, $post_id)) {
            return $parent->redirectWithMessage(
                tl('social_component_already_voted'));
        }
        $group_model->voteUp($user_id, $post_id);
        return $parent->redirectWithMessage(
            tl('social_component_vote_recorded'));
    }
}
X