/ src / controllers / GroupController.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;

use seekquarry\yioop as B;
use seekquarry\yioop\configs as C;
use seekquarry\yioop\controllers\AdminController;
use seekquarry\yioop\library as L;
use seekquarry\yioop\library\CrawlConstants;
use seekquarry\yioop\library\WikiParser;

/**
 * Controller used to handle user group activities outside of
 * the admin panel setting. This either could be because the admin panel
 * is "collapsed" or because the request concerns a wiki page.
 *
 * @author Chris Pollett
 */
class GroupController extends Controller implements CrawlConstants
{
    /**
     * Associative array of $components activities for this controller
     * Components are collections of activities (a little like traits) which
     * can be reused.
     *
     * @var array
     */
    public static $component_activities = ["social" => ["groupFeeds",
        "userMessages", "userMail", "wiki"], "git" => ["gitService"]];
    /**
     * Whether this request is reading a page that is served as a static
     * HTML folder. A signed-in reader is normally sent once to the same
     * page with a token on the address, which is harmless for a page but
     * not for a folder of files: the address it lands on is no longer
     * the folder, so every stylesheet, script and link the file names
     * relative to itself is looked for in the wrong place, and the file
     * arrives unstyled with dead links. Such a request is answered where
     * it was asked for instead.
     *
     * @param object $group_model used to look the page up
     * @return bool whether this reads a static HTML folder
     */
    private function isStaticHtmlFolderRead($group_model)
    {
        if (($_REQUEST['a'] ?? "") !== 'wiki') {
            return false;
        }
        $arg = $_REQUEST['arg'] ?? "";
        if ($arg !== "" && $arg !== 'read') {
            return false;
        }
        $page_name = $this->clean($_REQUEST['page_name'] ?? "", "string");
        $group_id = intval($_REQUEST['group_id'] ?? 0);
        if ($page_name === "" || $group_id <= 0) {
            return false;
        }
        $page_info = $group_model->getPageInfoByName($group_id,
            $page_name, L\getLocaleTag(), "read");
        if (empty($page_info['PAGE'])) {
            return false;
        }
        $head = L\WikiParser::parsePageHeadVars($page_info['PAGE']);
        return $this->component("social")->isStaticFolderPage($head);
    }
    /**
     * Used to process requests related to user group activities outside of
     * the admin panel setting. This either could be because the admin panel
     * is "collapsed" or because the request concerns a wiki page.
     *
     * @return mixed configureRequest result when no profile is
     *      configured; under HTTP this typically exits via
     *      displayView or redirectLocation without returning
     */
    public function processRequest()
    {
        $data = [];
        $signin_model = $this->model("signin");
        $group_model = $this->model("group");
        if (!C\PROFILE) {
            return $this->configureRequest();
        }
        if (isset($_SESSION['USER_ID'])) {
            if ($this->getCSRFTime(C\p('CSRF_TOKEN')) == 0 &&
                $_SERVER['REQUEST_METHOD'] == "GET" &&
                !$this->isStaticHtmlFolderRead($group_model)) {
                $_REQUEST[C\p('CSRF_TOKEN')] = $this->generateCSRFToken(
                    $_SESSION['USER_ID']);
                $this->redirectLocation(C\SHORT_BASE_URL . "?" .
                    http_build_query($_REQUEST));
                exit();
            }
            $user_id = $_SESSION['USER_ID'];
            $data['ADMIN'] = 1;
            $data['USERNAME'] = $signin_model->getUserName($user_id);
            $_SESSION['USER_NAME'] = $data['USERNAME'];
        } else {
            $user_id = C\PUBLIC_GROUP_ID;
        }
        $data['SCRIPT'] = "";
        $token_okay = $this->checkCSRFToken(C\p('CSRF_TOKEN'), $user_id);
        if (($_REQUEST["a"] ?? "") == "userMessages"
            && !empty($_REQUEST["arg"]) &&
            in_array($_REQUEST["arg"], ["call_source", "call_event"])) {
            $token_okay = true;
        }
        $data[C\p('CSRF_TOKEN')] = $this->generateCSRFToken($user_id);
        if (!$token_okay) {
            $keep_fields = ["a", "arg",  "callback", "f", "group_id",
                "group_name", "just_group_id", "just_user_id", "just_thread",
                "layout", "limit", "n", "num", "page_id", "page_name",
                "repo_branch", "repo_commit", "repo_dir", "repo_download",
                "repo_issue", "repo_issue_filter", "repo_offset", "repo_path",
                "repo_ref", "repo_rows", "repo_search", "repo_sort",
                "repo_view", "sf", "sort", "v",];
            $request = $_REQUEST;
            $_REQUEST = [];
            foreach ($keep_fields as $field) {
                if (isset($request[$field])) {
                    if ($field == "arg" && (!in_array($request[$field],
                        ["diff", "history", "read", "pages", "media",
                        "media-detail-read", "media-detail-edit", "source",
                        "statistics"]))) {
                        continue;
                    }
                    $_REQUEST[$field] =
                        $this->clean($request[$field], "string");
                }
            }
            $_REQUEST["c"] = "group";
        }
        $data['MENU'] = 'groupmenu';
        $data['MENU_NAME'] = tl('group_controller_groupmenu');
        $data = array_merge($data, $this->processSession());
        if (!isset($data['REFRESH'])) {
            $view = "group";
        } else {
            $view = $data['REFRESH'];
        }
        if ($data['ACTIVITY_METHOD'] == "wiki") {
            if (isset($data["VIEW"]) && !isset($data['REFRESH'])) {
                $view = $data["VIEW"];
            }
        } else if (isset($_REQUEST['f']) &&
            in_array($_REQUEST['f'], ["api", "json", "rss", "serial"])) {
            $this->setupViewFormatOutput($_REQUEST['f'], $view, $data);
        }
        $_SESSION['REMOTE_ADDR'] = L\remoteAddress();
        $this->displayView($view, $data);
    }
    /**
     * Used to perform the actual activity call to be done by the
     * group_controller.
     * processSession is called from @see processRequest, which does some
     * cleaning of fields if the CSRFToken is not valid. It is more likely
     * that that group_controller may be involved in such requests as it can
     * be invoked either when a user is logged in or not and for users with and
     * without accounts. processSession makes sure the $_REQUEST'd activity is
     * valid (or falls back to groupFeeds) then calls it. If someone uses
     * the Settings link to change the language or default number of feed
     * elements to view, this method sets up the $data variable so that
     * the back/cancel button on that page works correctly.
     *
     * @return array $data field variables produced by the dispatched
     *      activity, augmented with CONTROLLER, COMPONENT_ACTIVITIES,
     *      ACTIVITY_METHOD, and (when applicable) PAGE_TITLE / REFRESH
     */
    public function processSession()
    {
        if (isset($_REQUEST['a']) &&
            in_array($_REQUEST['a'], $this->activities)) {
            $activity = $_REQUEST['a'];
        } else {
            $activity = "groupFeeds";
        }
        //Always allow groupFeeds (for public feeds) by default, check all else
        $allowed = ($activity == "groupFeeds") ? true : false;
        $data = ['CONTROLLER' => 'group'];
        $user_id =  (!empty($_SESSION['USER_ID'])) ? $_SESSION['USER_ID'] :
            C\PUBLIC_USER_ID;
        $allowed_activities =
             $this->model("user")->getUserActivities($user_id);
        $data['COMPONENT_ACTIVITIES'] =
            AdminController::computeComponentActivities(
                $allowed_activities);
        $allowed_argument = false;
        $activity_index = 0;
        $allowed_activity = [];
        $arguments = [];
        $group_modifiers = [];
        foreach ($allowed_activities as $allowed_activity) {
            if ($activity == $allowed_activity['METHOD_NAME']) {
                 $allowed = true;
                 $arguments = preg_split("/\s*\,\s*/",
                    trim($allowed_activity['ALLOWED_ARGUMENTS']));
            }
            if ($allowed_activity['METHOD_NAME'] == 'manageGroups') {
                $group_modifiers = preg_split("/\s*\,\s*/",
                   trim($allowed_activity['ALLOWED_ARGUMENTS']));
            }
            $activity_index++;
        }
        $data['ALLOWED_ARGUMENTS'] = "";
        $activity_data = [];
        /* always allow access to wiki, groupFeeds, and the git service
           as each does its own access control
         */
        if (!$allowed && in_array($activity,
            ["wiki", "groupFeeds", "gitService"])) {
            $allowed = true;
        }
        if ($allowed) {
            $activity_data = $this->call($activity, $arguments);
            if (!is_array($activity_data)) {
                $activity_data = [];
            }
            $data['ALLOWED_ARGUMENTS'] =
                $allowed_activity['ALLOWED_ARGUMENTS'] ?? "";
            $data['GROUP_MODIFIERS'] = $group_modifiers;
        }
        $data = array_merge($data, $activity_data);
        $data['ACTIVITIES'] = $allowed_activities;
        $data['ACTIVITY_METHOD'] = $activity;
        return $data;
    }
    /**
     * Responsible for setting the view for a feed if something other
     * than HTML (for example, RSS or JSON) is desired. It also
     * sets up any particular $data fields needed for displaying that
     * view correctly.
     *
     * @param string $format can be one of rss, json, or serialize,
     *      if different, default HTML GroupView used.
     * @param string &$view variable used to set the view in calling
     *     method
     * @param array &$data used to send data to the view for drawing
     */
    public function setupViewFormatOutput($format, &$view, &$data)
    {
        $data["QUERY"] = "groups:feed";
        if (isset($data["JUST_GROUP_ID"])) {
            $data["QUERY"] = "groups:just_group_id:" . $data["JUST_GROUP_ID"];
        }
        if (isset($data["JUST_USER_ID"])) {
            $data["QUERY"] = "groups:just_user_id:" . $data["JUST_USER_ID"];
        }
        if (isset($data["JUST_THREAD"])) {
            $data["QUERY"] = "groups:just_thread:" . $data["JUST_THREAD"];
        }
        $data["its"] = 0;
        $num_pages = empty($data["PAGES"]) ? 0 : count($data["PAGES"]);
        $token = empty($data['admin']) ? "" :
            C\p('CSRF_TOKEN') . "=".  $data[C\p('CSRF_TOKEN')];
        for ($i = 0; $i < $num_pages; $i++) {
            $data["PAGES"][$i][self::URL] = htmlentities(B\feedsUrl(
                "thread", $data["PAGES"][$i]['PARENT_ID'],
                !empty($data['admin']), $data['CONTROLLER'])) . $token;
        }
        switch ($format) {
            case "api":
                $view = "api";
                break;
            case "json":
                $out_data = [];
                $out_data["language"] = L\getLocaleTag();
                $out_data["link"] =
                    C\p('NAME_SERVER')."?f=$format&q={$data['QUERY']}";
                $out_data["totalResults"] = $data['TOTAL_ROWS'];
                $out_data["startIndex"] = $data['LIMIT'];
                $out_data["itemsPerPage"] = $data['RESULTS_PER_PAGE'];
                foreach ($data['PAGES'] as $page) {
                    $item = [];
                    $item["title"] = $page[self::TITLE];
                    if (!isset($page[self::TYPE]) ||
                    (isset($page[self::TYPE])
                    && $page[self::TYPE] != "link")) {
                        $item["link"] = $page[self::URL];
                    } else {
                        $item["link"] = strip_tags($page[self::TITLE]);
                    }
                    $item["description"] = strip_tags($page[self::DESCRIPTION]);
                    if (isset($page[self::THUMB])
                    && $page[self::THUMB] != 'null') {
                        $item["thumb"] = $page[self::THUMB];
                    }
                    if (isset($page[self::TYPE])) {
                        $item["type"] = $page[self::TYPE];
                    }
                    $out_data['item'][] =$item;
                }
                $out = json_encode($out_data);
                //jsonp format
                if (isset($_REQUEST['callback'])) {
                    $callback = $this->clean($_REQUEST['callback'], "string");
                    $out = "// API callback\n$callback($out);";
                    $this->web_site->header("
                        Content-Type: text/javascript; charset=UTF-8");
                } else {
                    $this->web_site->header("Content-Type: application/json");
                }
                e($out);
                \seekquarry\atto\webExit();
            case "rss":
                $view = "rss";
                break;
            case "serial":
                e(serialize($out_data));
                \seekquarry\atto\webExit();
        }
    }
}
X