<?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'));
}
}