<?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\library as L;
use seekquarry\yioop\library\CrawlConstants;
use seekquarry\yioop\library\UrlParser;
use seekquarry\yioop\library\PageRuleParser;
use seekquarry\yioop\library\Classifiers\Classifier;
use seekquarry\yioop\library\CrawlDaemon;
/**
* AdminController answers the requests of a signed-in user's settings
* screens: their account, and for those whose role allows it the users,
* roles, groups, locales, crawls and server settings of the site. Most of
* its activities live in components, which it loads and calls by name.
*
* @author Chris Pollett
*/
class AdminController extends Controller implements CrawlConstants
{
/**
* Says which activities (roughly methods invoke from the web) this
* controller will respond to (note: more activities will be loaded from
* components)
* @var array
*/
public $activities = ["crawlStatus", "machineStatus", "signout"];
/**
* $status_activities lists the activities a screen calls again every
* so often to refresh part of itself, such as the crawl status the
* Manage Crawls screen updates about every 20 seconds. They answer
* with that part only, not a whole screen.
* @var array
*/
public $status_activities = ["crawlStatus", "machineStatus"];
/**
* $component_activities maps each component's name to the activities
* it carries out for this controller. A component is a set of
* activities that more than one controller can use.
* @var array
*/
public static $component_activities = [
"message" => ["userMessages"],
"wiki" => ["wiki"],
"feed" => ["groupFeeds"],
"mail" => ["userMail"],
"accountaccess" =>
["manageAccount", "manageUsers", "manageRoles"],
"crawl" => ["manageCrawls", "manageClassifiers", "mixCrawls",
"pageOptions", "resultsEditor", "scrapers", "searchSources"],
"social" => ["manageGroups"],
"chatbot" => ["botStory"],
"store" => ["manageCredits", "manageAdvertisements"],
"system" => ["manageMachines", "manageLocales", "servers",
"userRolesGroups", "security", "appearance", "configure"],
];
/**
* $group_controller_activities lists the activities the group
* controller answers rather than this one. A request for one of them
* here is redirected there.
* @var array
*/
public static $group_controller_activities = ["userMessages"];
/**
* processRequest answers a request to one of the site's settings
* screens. It checks the user is signed in and may use the activity
* asked for, such as signin or manageAccount, calls the method or
* component that carries it out, and draws the screen it gives.
*
* @return mixed configureRequest result when no profile is configured;
* redirectWithMessage result on auth-related early-outs; under
* HTTP this typically exits without returning
*/
public function processRequest()
{
$data = [];
if (!C\PROFILE) {
return $this->configureRequest();
}
$view = "signin";
$polling_restart = isset($_REQUEST['arg']) &&
$_REQUEST['arg'] == "restart_status" &&
isset($_REQUEST['a']) &&
($_REQUEST['a'] == "servers" ||
$_REQUEST['a'] == "userRolesGroups" ||
$_REQUEST['a'] == "configure");
if ($polling_restart) {
/* The Restarting Server page polls this read-only status
while the server restarts. It is answered here without
the usual signed-in and CSRF-freshness checks: the
token can age out or be regenerated across a restart,
which would otherwise break the poll, and the very first
configuration of a brand-new install restarts before any
user has signed in. Only the current restart phase (a
short word the page shows, or none when finished) is
returned, so answering it openly reveals nothing
sensitive. */
$phase = L\CrawlDaemon::getRestartStatus();
$this->web_site->header("Content-Type: application/json");
e(json_encode(["phase" => $phase]));
\seekquarry\atto\webExit();
}
if (empty($_SESSION['USER_ID']) && !empty($this->web_site)) {
/* The visitor's session is not in memory (dropped under
load, or lost when the server restarted). If their cookie
still names a session that belongs to a signed-in user,
put that session back so they stay signed in. The lookup
runs only here, when it is actually needed, and uses the
connection this request already has. */
$candidate = $this->web_site->rehydrateCandidate();
if (!empty($candidate)) {
$user_model = $this->model("user");
$restore_user = $user_model->getSessionUser($candidate);
$current_id = $_COOKIE[C\p('SESSION_NAME')] ?? "";
if (!empty($restore_user) && $current_id !== "") {
/* The user each session belongs to is recorded for
every sign-in; a copy of the session itself is
only kept when settings are changed, so use that
when it is there and otherwise put back just the
sign-in, which is what the rest of the request
reads. This host has already made a session id of
its own for this visitor and is handing it to them
with this response, so the sign-in goes under that
id: the id they arrived with is one they will
never send again. */
$restored = $user_model->getUserSession($restore_user);
if (empty($restored)) {
$restored = [];
}
$restored['USER_ID'] = $restore_user;
/* A signed-in visitor whose request carries no token
is only handed a fresh one if their session shows
they had one recently, so put that back too. Their
session is being restored because they are signed
in, and signing in is what hands out the first
token, so the record went with the session that
was dropped. */
$ip_address = L\remoteAddress();
if (empty($restored['OLD_CSRF_TIME']) ||
!is_array($restored['OLD_CSRF_TIME'])) {
$restored['OLD_CSRF_TIME'] = [];
}
if (empty($restored['OLD_CSRF_TIME'][$ip_address])) {
$restored['OLD_CSRF_TIME'][$ip_address] = time();
}
$this->web_site->adoptSession($current_id, $restored);
$user_model->setSessionUser($current_id, $restore_user,
time() + C\p('AUTOLOGOUT'));
}
}
}
if (empty($_SESSION['USER_ID']) && !empty($this->web_site) &&
!empty($_REQUEST['domain_handoff'])) {
/* A domain switch from the Appearance activity carries a
single-use handoff token. Redeem it: look up the user it
was minted for and sign that user in under this host's own
session, the way signing in on this host would. The
server-side lookup and the one-time delete are what keep
the token from being replayed or from planting a session
on someone else, so it is spent once presented whether or
not this host had a session to sign into. */
$handoff = $this->clean($_REQUEST['domain_handoff'], "string");
if (preg_match('/^[a-f0-9]{64}$/', $handoff)) {
$user_model = $this->model("user");
$handoff_user = $user_model->getSessionUser($handoff);
$current_id = $_COOKIE[C\p('SESSION_NAME')] ?? "";
if (!empty($handoff_user)) {
if ($current_id !== "") {
$_SESSION['USER_ID'] = $handoff_user;
$user_model->setSessionUser($current_id,
$handoff_user, time() + C\p('AUTOLOGOUT'));
}
$user_model->deleteSessionUser($handoff);
}
}
}
if (!empty($_SESSION['USER_ID'])) {
$user = $_SESSION['USER_ID'];
if ($this->getCSRFTime(C\p('CSRF_TOKEN')) == 0 &&
$_SERVER['REQUEST_METHOD'] == "GET") {
$_REQUEST[C\p('CSRF_TOKEN')] = $this->generateCSRFToken(
$_SESSION['USER_ID']);
$this->redirectLocation(C\SHORT_BASE_URL . "?" .
http_build_query($_REQUEST));
exit();
}
} else if (!empty($_SESSION['USER_NAME']) && empty($_REQUEST['u'])) {
$user = $this->model("signin")->getUserId($_SESSION['USER_NAME'],
"string");
$_SESSION['USER_ID'] = $user;
} else {
$user = L\remoteAddress();
}
$data['SCRIPT'] = "";
$data[C\p('CSRF_TOKEN')] = $this->generateCSRFToken($user);
$token_okay = $this->checkCSRFToken(C\p('CSRF_TOKEN'), $user);
if ($token_okay || isset($_REQUEST['u'])) {
if (isset($_SESSION['USER_ID']) && !isset($_REQUEST['u'])) {
$_SESSION['USER_NAME'] = $this->model("signin")->
getUserName($user);
$data = array_merge($data, $this->processSession());
if (!isset($data['REFRESH'])) {
$view = "admin";
} else {
$view = $data['REFRESH'];
}
} else if (!isset($_SESSION['REMOTE_ADDR'])
&& !isset($_REQUEST['u'])) {
$data['SCRIPT'] = "doMessage('<h1 class=\"red\" >".
tl('admin_controller_need_cookies') . "</h1>');";
unset($_SESSION['USER_ID']);
} else if ($this->checkSignin()) {
$username = $this->clean($_REQUEST['u'], "string");
$_SESSION['USER_NAME'] = $username;
// successful login.
$user_id = $this->model("signin")->getUserId($username);
$session = $this->model("user")->getUserSession($user_id);
$last_activity = [];
if (isset($_SESSION['LAST_ACTIVITY']) &&
is_array($_SESSION['LAST_ACTIVITY'])) {
$_REQUEST = array_merge($_REQUEST,
$_SESSION['LAST_ACTIVITY']);
}
if (is_array($session)) {
$_SESSION = $session;
if (!empty($_REQUEST['page_name']) ||
!empty($_REQUEST['page_id'])) {
$_REQUEST['preserve'] = 'true';
}
}
$allowed_activities =
$this->model("user")->getUserActivities($user_id);
// now don't want to use remote address anymore
if (empty($allowed_activities)) {
unset($_SESSION['USER_ID']);
unset($_REQUEST);
$_REQUEST['c'] = "admin";
return $this->redirectWithMessage(
tl('admin_controller_account_not_active'));
} else {
$_SESSION['USER_ID'] = $user_id;
/* Record this session id so the sign-in can be
restored if the in-memory session is later
dropped or the server restarts. */
if (!empty($_COOKIE[C\p('SESSION_NAME')])) {
$this->model("user")->setSessionUser(
$_COOKIE[C\p('SESSION_NAME')], $user_id,
time() + C\p('AUTOLOGOUT'));
}
$_REQUEST[C\p('CSRF_TOKEN')] = $this->generateCSRFToken(
$_SESSION['USER_ID']);
$preserve_array = [];
if (!empty($_REQUEST['preserve']) &&
$_REQUEST['preserve'] == 'true') {
$preserve_array = [
'a', 'arg', 'filter', 'group_id',
'just_thread', 'just_group_id', 'page_id',
'page_name', 'visible_users',
'user_filter'
];
}
$signin_message =
tl('admin_controller_login_successful');
if (C\p('RECOVERY_MODE') == C\QUESTIONS_RECOVERY) {
$recovery_user =
$this->model("user")->getUserById($user_id);
if (empty($recovery_user['RECOVERY_QUESTION'])) {
$signin_message = tl(
'admin_controller_set_recovery_question');
}
}
return $this->redirectWithMessage(
$signin_message,
$preserve_array);
}
} else {
unset($_SESSION['USER_ID']);
$login_attempted = false;
if (isset($_REQUEST['u'])) {
$login_attempted = true;
}
unset($_REQUEST);
$_REQUEST['c'] = "admin";
if ($login_attempted) {
return $this->redirectWithMessage(
tl('admin_controller_login_failed'));
}
}
} else if ($this->checkCSRFToken(C\p('CSRF_TOKEN'), "config")) {
$data['SCRIPT'] = "doMessage('<h1 class=\"red\" >".
tl('admin_controller_login_to_config')."</h1>')";
} else if (isset($_REQUEST['a']) &&
in_array($_REQUEST['a'], $this->status_activities)) {
e("<p class='red'>".
tl('admin_controller_status_updates_stopped')."</p>");
\seekquarry\atto\webExit();
}
if ($token_okay && isset($_SESSION["USER_ID"])) {
$data["ADMIN"] = true;
} else {
$data["ADMIN"] = false;
}
if ($view == 'signin') {
$data[C\p('CSRF_TOKEN')] = $this->generateCSRFToken(
L\remoteAddress());
$data['SCRIPT'] .= "var u; if ((u = elt('username')) && u.focus) ".
"u.focus();";
}
$_SESSION['REMOTE_ADDR'] = L\remoteAddress();
if (!isset($data["USERNAME"]) && isset($_SESSION['USER_ID'])) {
$signin_model = $this->model("signin");
$data['USERNAME'] = $signin_model->getUserName(
$_SESSION['USER_ID']);
}
$this->initializeAdFields($data, false);
$this->displayView($view, $data);
}
/**
* configureRequest draws the Configure screen with no sign-in, which
* processRequest does when no work directory or profile is set up yet.
* In that case the screen is only shown to a user connected from the
* machine itself.
*/
public function configureRequest()
{
$data = $this->processSession();
$data[C\p('CSRF_TOKEN')] = $this->generateCSRFToken("config");
$this->displayView("admin", $data);
}
/**
* checkSignin says whether the user name and password the sign-in form
* sent, as the request fields u and p, match a user in the database.
*
* @return bool whether they do or not
*/
public function checkSignin()
{
$result = false;
$username = $this->clean($_REQUEST['u'], "string");
$password = $this->clean($_REQUEST['p'], "password");
if (isset($_REQUEST['u']) && isset($_REQUEST['p']) ) {
$result = $this->model("signin")->checkValidSignin(
$username, $password);
}
return $result;
}
/**
* processSession works out the activities a signed-in user may use and
* the one the request asks for, then calls the method or component
* that carries it out. processRequest calls it once the user is
* signed in.
*
* @return array $data the results of doing the activity for display in the
* view
*/
public function processSession()
{
$allowed = false;
if (!C\PROFILE || (C\nsdefined("FIX_NAME_SERVER") &&
C\FIX_NAME_SERVER)) {
$activity = "configure";
} else if (isset($_REQUEST['a']) &&
in_array($_REQUEST['a'], $this->activities)) {
$activity = $_REQUEST['a'];
if (in_array($activity, self::$group_controller_activities)) {
unset($_REQUEST['c']);
$_REQUEST[C\p('CSRF_TOKEN')] = $this->generateCSRFToken(
$_SESSION['USER_ID']);
$location = B\controllerUrl("group", true) .
http_build_query($_REQUEST);
$this->redirectLocation($location);
}
} else {
$activity = "manageAccount";
}
$activity_model = $this->model("activity");
if (!C\PROFILE) {
$allowed_activities = [ [
"ACTIVITY_NAME" =>
$activity_model->getActivityNameFromMethodName($activity),
'METHOD_NAME' => $activity,
'ALLOWED_ARGUMENTS' => 'all'],
];
$allowed = true;
} else {
$allowed_activities =
$this->model("user")->getUserActivities($_SESSION['USER_ID']);
}
if ($allowed_activities == []) {
$data['INACTIVE'] = true;
return $data;
}
$allowed_argument = false;
$arguments = [];
$group_modifiers = [];
$activity_index = 0;
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']));
}
if ($allowed_activity['METHOD_NAME'] == "manageCrawls" &&
$activity == "crawlStatus") {
$allowed = true;
break;
}
if ($allowed_activity['METHOD_NAME'] == "manageMachines" &&
$activity == "machineStatus") {
$allowed = true;
break;
}
if ($allowed_activity['METHOD_NAME'] == "groupFeeds" &&
$activity == "wiki") {
$allowed = true;
break;
}
$activity_index++;
}
// always allow managing account
if (!$allowed && $activity == "manageAccount") {
$activity = $allowed_activities[0]['METHOD_NAME'];
$_REQUEST["a"] = $activity;
$allowed = true;
}
$data['ALLOWED_ARGUMENTS'] = "";
//for now we allow anyone to get crawlStatus
if ($allowed) {
$data = $this->call($activity, $arguments);
if (!is_array($data)) {
$data = [];
}
$data['ACTIVITY_METHOD'] = $activity;
$data['ACTIVITIES'] = $allowed_activities;
$data['ALLOWED_ARGUMENTS'] = $allowed_activity['ALLOWED_ARGUMENTS'];
$data['GROUP_MODIFIERS'] = $group_modifiers;
}
if (!in_array($activity, $this->status_activities)) {
$name_activity = $activity;
if ($activity == "wiki") {
$name_activity = "groupFeeds";
}
$data['CURRENT_ACTIVITY'] =
$activity_model->getActivityNameFromMethodName($name_activity);
}
$data['COMPONENT_ACTIVITIES'] = isset($data["ACTIVITIES"]) ?
self::computeComponentActivities($data["ACTIVITIES"]) : [];
/* The menu makes the messages activity pulse while somebody's
call waits, and asks the server about that every few seconds.
Where calling is turned off there is nothing to wait for, so
the menu is told and never asks. */
$data['CALL_ENABLED'] =
($this->component("message")->callServiceMode() !== 'off');
return $data;
}
/**
* computeComponentActivities groups the activities a user may use by
* the heading the settings menu shows them under, such as Access
* Control or Social, in the user's language. The menu is drawn from
* what it gives.
*
* @param array $user_activities a list of activities that a
* user is allowed to access
* @return array translated heading => list of the user's activities
* under it
*/
public static function computeComponentActivities($user_activities)
{
$user_component_activities = [];
$component_translations = [
"accountaccess" => tl('admin_controller_access_control'),
"social" => tl('admin_controller_social'),
/* Mail sits under the Social heading, where a reader
looked for it before it had a component of its own. */
"mail" => tl('admin_controller_social'),
"feed" => tl('admin_controller_social'),
"wiki" => tl('admin_controller_social'),
"message" => tl('admin_controller_social'),
"git" => tl('admin_controller_git'),
"crawl" => tl('admin_controller_crawl_settings'),
"system" => tl('admin_controller_system_settings'),
"store" => tl('admin_controller_store'),
"chatbot" => tl('admin_controller_chatbot')
];
foreach (self::$component_activities as $component => $activities) {
foreach ($user_activities as $activity) {
if (in_array($activity['METHOD_NAME'], $activities)) {
$user_component_activities[
$component_translations[$component]][] =
$activity;
}
}
}
return $user_component_activities;
}
/**
* Used to handle crawlStatus REST activities requesting the status of the
* current web crawl
*
* @return array $data contains crawl status of current crawl as well as
* info about prior crawls and which crawl is being used for default
* search results
*/
public function crawlStatus()
{
$data = [];
$data['SCRIPT'] = "";
$data['REFRESH'] = "crawlstatus";
$crawl_model = $this->model("crawl");
$crawl_time = $crawl_model->getCurrentIndexDatabaseName();
if (isset($crawl_time) ) {
$data['CURRENT_INDEX'] = (int)$crawl_time;
} else {
$data['CURRENT_INDEX'] = -1;
}
$machine_urls = $this->model("machine")->getQueueServerUrls();
list($stalled, $status, $data['RECENT_CRAWLS']) =
$crawl_model->combinedCrawlInfo($machine_urls);
$crawl_model->markStopRequested($status);
$data["STALLED"] = $stalled ?? [];
$data['CRAWL_TOGGLE'] = (!isset($_REQUEST['crawl_toggle'])) ? [] :
array_filter(
explode(",", $this->clean($_REQUEST['crawl_toggle'], "string")
));
$data["ACTIVE_CRAWLS"] = $status;
if (!empty($data["ACTIVE_CRAWLS"])) {
$num_crawls = count($data['RECENT_CRAWLS']);
foreach($data["ACTIVE_CRAWLS"] as $channel => $crawl) {
//erase from previous crawl list any active crawl
for ($i = 0; $i < $num_crawls; $i++) {
if (!empty($crawl['CRAWL_TIME']) &&
$data['RECENT_CRAWLS'][$i]['CRAWL_TIME'] ==
$crawl['CRAWL_TIME'] && !empty($crawl['DESCRIPTION'])) {
$data['RECENT_CRAWLS'][$i] = false;
}
}
}
$data['RECENT_CRAWLS']= array_filter($data['RECENT_CRAWLS']);
}
if (isset($data['RECENT_CRAWLS'][0])) {
L\rorderCallback($data['RECENT_CRAWLS'][0],
$data['RECENT_CRAWLS'][0], 'CRAWL_TIME');
usort($data['RECENT_CRAWLS'], C\NS_LIB . "rorderCallback");
}
$this->pagingLogic($data, 'RECENT_CRAWLS', 'RECENT_CRAWLS',
C\DEFAULT_ADMIN_PAGING_NUM);
return $data;
}
/**
* machineStatus gathers, from the machine model, whether the queue
* servers and fetchers of each machine this Yioop instance manages are
* on or off, for the Manage Machines screen, which calls it every so
* often to refresh that list.
*
* @return array $data MACHINES field has information about each
* machine managed by this Yioop instance as well the on off
* status of its queue_servers and fetchers.
* The REFRESH field is used to tell the controller that the
* view shouldn't have its own sidemenu.
*/
public function machineStatus()
{
$machine_model = $this->model("machine");
$data = [];
$data['REFRESH'] = "machinestatus";
$data['MACHINE_NAMES'] = [];
$data['CHANNELS'] = range(0, C\MAX_CHANNELS -1);
$data['CHANNELS'][-1 ] = tl('admin_controller_is_replica');
ksort($data['CHANNELS']);
$data['FETCHER_NUMBERS'] = [
0 => 0,
1 => 1,
2 => 2,
3 => 3,
4 => 4,
5 => 5,
6 => 6,
7 => 7,
8 => 8,
16 => 16
];
$machine_names = $machine_model->getQueueServerNames();
$data['PARENT_MACHINES'] = array_combine($machine_names,
$machine_names);
$data['PARENT'] = $machine_names[0] ?? "";
$data['FETCHER_NUMBER'] = 2;
$data['CHANNEL'] = 0;
/* Which machines have their service list folded away is kept in
the session, which is saved per user, so the choice outlives
the page and the login. A request naming a machine in toggle
flips it. */
$hidden = $_SESSION['MACHINE_SERVICES_HIDDEN'] ?? [];
if (!empty($_REQUEST['toggle'])) {
$name = $this->clean($_REQUEST['toggle'], "string");
if (empty($hidden[$name])) {
$hidden[$name] = true;
} else {
unset($hidden[$name]);
}
$_SESSION['MACHINE_SERVICES_HIDDEN'] = $hidden;
}
$data['HIDDEN_MACHINES'] = $hidden;
$this->pagingLogic($data, $machine_model, 'MACHINES',
C\DEFAULT_ADMIN_PAGING_NUM);
$profile = $this->model("profile")->getProfile(C\WORK_DIRECTORY);
$media_mode = isset($profile['MEDIA_MODE']) ?
$profile['MEDIA_MODE']: "name_server";
$data['MEDIA_MODE'] = $media_mode;
$data['MAIL_MODE'] = $profile['MAIL_MODE'] ?? "disabled";
if ($data['MEDIA_MODE'] == "name_server" &&
$data['MACHINES']['NAME_SERVER']["MEDIA_UPDATER_TURNED_ON"] &&
$data['MACHINES']['NAME_SERVER']["MediaUpdater"] == 0) {
// try to restart news server if dead
CrawlDaemon::start("MediaUpdater", 'none', "", -1);
}
return $data;
}
/**
* updateProfileFields copies the profile settings a settings form sent
* into $data and $profile, cleaned. Colors are checked as colors, the
* name server gets a closing slash, and a checkbox not sent is saved
* as off. The settings screens of SystemComponent and CrawlComponent
* call it before saving the profile.
*
* @param array &$data field data to be sent to the view
* @param array &$profile used to contain the current and updated profile
* field values
* @param array $check_box_fields fields whose data comes from a html
* checkbox
*/
public function updateProfileFields(&$data, &$profile,
$check_box_fields = [])
{
$script_array = ['SIDE_ADSCRIPT', 'TOP_ADSCRIPT', 'GLOBAL_ADSCRIPT'];
foreach ($script_array as $value) {
if (isset($_REQUEST[$value])) {
$_REQUEST[$value] = strtr($_REQUEST[$value], ["(" => "(",
")" => ")"]);
}
}
$color_fields = ['BACKGROUND_COLOR', 'FOREGROUND_COLOR',
'SIDEBAR_COLOR', 'TOPBAR_COLOR'];
foreach ($this->model("profile")->profile_fields as $field) {
if (isset($_REQUEST[$field])) {
if (!in_array($field, ["ROBOT_DESCRIPTION", "PROXY_SERVERS",
"AUXILIARY_CSS"])) {
if (in_array($field, $color_fields)) {
$clean_value =
$this->clean($_REQUEST[$field], "color");
} else {
$clean_value =
$this->clean($_REQUEST[$field], "string");
}
} else {
$clean_value = $_REQUEST[$field];
}
if ($field == "NAME_SERVER" &&
!str_ends_with($clean_value, "/")) {
$clean_value .= "/";
}
$data[$field] = $clean_value;
$profile[$field] = $data[$field];
if ($field == "PROXY_SERVERS") {
$proxy_array = preg_split("/(\s)+/", $clean_value);
$profile[$field] = $this->convertArrayLines(
$proxy_array, "|Z|", true);
}
}
if (!isset($data[$field])) {
if (C\nsdefined($field) && C\p($field) !== "" &&
!in_array($field, $check_box_fields)) {
$data[$field] = C\p($field);
} else if (defined($field) &&
!in_array($field, $check_box_fields)) {
$data[$field] = constant($field);
} else {
$data[$field] = "";
}
if (in_array($field, $check_box_fields)) {
$profile[$field] = false;
}
}
}
}
/**
* tableSearchRequestHandler sets up what a table's search form needs,
* such as its comparison choices, and reads a search the form sent so
* the form keeps its choices when the results are shown. The Manage
* Users, Manage Roles and Manage Groups screens use such a form to
* search their lists.
*
* @param array &$data modified to contain the field data needed for
* the view to draw the search form
* @param string $activity activity name (e.g. "manageUsers",
* "manageRoles") in which this search is being conducted; used
* to namespace the form fields in $_REQUEST
* @param array $comparison_fields those fields of the entity
* in question ( for example, users) which we can search both with
* string comparison operators and equality operators
* @param string $field_postfix suffix to append onto field names in
* case there are multiple forms on the same page
* @return array $search_array of [field, value, op] triples extracted
* from $_REQUEST for the dispatch back to the model layer
*/
public function tableSearchRequestHandler(&$data, $activity,
$comparison_fields = [], $field_postfix = "")
{
$data['FORM_TYPE'] = "search";
$activity_postfix = $activity . $field_postfix;
$data['EQUAL_COMPARISON_TYPES'] = [
"=" => tl('admin_controller_equal'),
"!=" => tl('admin_controller_not_equal'),
];
$data['INEQUALITY_COMPARISON_TYPES'] = array_merge(
$data['EQUAL_COMPARISON_TYPES'], [
"<" => tl('admin_controller_less_than'),
"<=" => tl('admin_controller_less_equal'),
">" => tl('admin_controller_greater_than'),
">=" => tl('admin_controller_greater_equal'),
]);
$data['BETWEEN_COMPARISON_TYPES'] = [
'BETWEEN' => tl('admin_controller_between'),
'NOT BETWEEN' => tl('admin_controller_not_between'),
];
$data['COMPARISON_TYPES'] = array_merge(
$data['EQUAL_COMPARISON_TYPES'],[
"=" => tl('admin_controller_equal'),
"!=" => tl('admin_controller_not_equal'),
"CONTAINS" => tl('admin_controller_contains'),
"BEGINS WITH" => tl('admin_controller_begins_with'),
"ENDS WITH" => tl('admin_controller_ends_with'),
]);
$_SESSION['SEARCH'][$activity_postfix]['COMPARISON_TYPES'] =
$data['COMPARISON_TYPES'];
$_SESSION['SEARCH'][$activity_postfix]['EQUAL_COMPARISON_TYPES'] =
$data['EQUAL_COMPARISON_TYPES'];
$data['SORT_TYPES'] = [
"NONE" => tl('admin_controller_no_sort'),
"ASC" => tl('admin_controller_sort_ascending'),
"DESC" => tl('admin_controller_sort_descending'),
];
$_SESSION['SEARCH'][$activity_postfix]['SORT_TYPES'] =
$data['SORT_TYPES'];
$paging = "";
$comparisons_all = $comparison_fields['ALL_FIELDS'] ?? [];
$equal_comparison_fields = $comparison_fields['EQUAL_COMPARISON_TYPES']
?? [];
$between_comparison_fields =
$comparison_fields['BETWEEN_COMPARISON_TYPES'] ?? [];
$timestamp_comparison_fields =
$comparison_fields['TIMESTAMP_COMPARISON_TYPES'] ?? [];
$inequality_comparison_fields =
$comparison_fields['INEQUALITY_COMPARISON_TYPES'] ?? [];
$between_fields = array_merge($between_comparison_fields,
$timestamp_comparison_fields);
foreach ($comparisons_all as $comparison_start) {
$comparison = $comparison_start . $field_postfix . "_comparison";
$comparison_types = (in_array($comparison_start,
$equal_comparison_fields)) ? 'EQUAL_COMPARISON_TYPES' :
((in_array($comparison_start, $inequality_comparison_fields)) ?
'INEQUALITY_COMPARISON_TYPES' :
((in_array($comparison_start, $between_fields)) ?
'BETWEEN_COMPARISON_TYPES' : 'COMPARISON_TYPES'));
$default_type = ($comparison_types == 'BETWEEN_COMPARISON_TYPES') ?
"BETWEEN" : ($comparison_types == 'COMPARISON_TYPES' ?
"CONTAINS" : "=");
$data[$comparison] = (isset($_REQUEST[$comparison]) &&
isset($data[$comparison_types][
$_REQUEST[$comparison]])) ? $_REQUEST[$comparison] :
$default_type;
$_SESSION['SEARCH'][$activity_postfix]['COMPARISON_FIELDS'
][$comparison] = $data[$comparison];
$paging .= "&$comparison=".
urlencode($data[$comparison]);
}
foreach ($comparisons_all as $sort_start) {
$sort = $sort_start . $field_postfix . "_sort";
$data[$sort] = (isset($_REQUEST[$sort]) &&
isset($data['SORT_TYPES'][
$_REQUEST[$sort]])) ? $_REQUEST[$sort] :
"NONE";
$_SESSION['SEARCH'][$activity_postfix]['SORT'][$sort] =
$data[$sort];
$paging .= "&$sort=" . urlencode($data[$sort]);
}
$search_array = [];
foreach ($comparisons_all as $field) {
$field_comparison = $field . $field_postfix . "_comparison";
$field_sort = $field . $field_postfix . "_sort";
$is_timestamp_type = in_array($field, $timestamp_comparison_fields);
$is_between_type = in_array($field, $between_comparison_fields);
if ($is_between_type || $is_timestamp_type) {
$low = "_low";
$field_name_low = $field. $low . $field_postfix;
$data[$field_name_low] = (isset($_REQUEST[$field_name_low]) &&
$_REQUEST[$field_name_low] != '-1') ?
$this->clean($_REQUEST[$field_name_low], "string") :
"";
$high = "_high";
$field_name_high = $field. $high . $field_postfix;
$data[$field_name_high] = (isset($_REQUEST[$field_name_high]) &&
$_REQUEST[$field_name_high] != '-1') ?
$this->clean($_REQUEST[$field_name_high], "string") :
"";
$_SESSION['SEARCH'][$activity_postfix]['FIELD_NAMES'
][$field_name_low] = $data[$field_name_low];
$_SESSION['SEARCH'][$activity_postfix]['FIELD_NAMES'
][$field_name_high] = $data[$field_name_high];
if ($is_timestamp_type) {
$search_array[] = [$field, $data[$field_comparison],
strtotime($data[$field_name_low]),
strtotime($data[$field_name_high]), $data[$field_sort]];
} else {
$search_array[] = [$field,
$data[$field_comparison], $data[$field_name_low],
$data[$field_name_high], $data[$field_sort]];
}
$paging .= "&$field_name_low=" . urlencode(
$data[$field_name_low]). "&$field_name_high=" .
urlencode($data[$field_name_high]);
} else {
$field_name = $field . $field_postfix;
$data[$field_name] = (isset($_REQUEST[$field_name]) &&
$_REQUEST[$field_name] != '-1') ?
$this->clean($_REQUEST[$field_name], "string") :
"";
$_SESSION['SEARCH'][$activity_postfix]['FIELD_NAMES'
][$field_name] = $data[$field_name];
if ($field_name == 'access' && $data[$field_name] >= 10) {
$search_array[] = ["status",
$data[$field_comparison], $data[$field_name]/10,
$data[$field_sort]];
} else {
$search_array[] = [$field,
$data[$field_comparison], $data[$field_name],
$data[$field_sort]];
}
$paging .= "&$field_name=" . urlencode($data[$field_name]);
}
}
$data['PAGING'] = $paging;
$_SESSION['SEARCH'][$activity_postfix]['SEARCH_ARRAY'] =
$search_array;
$_SESSION['SEARCH'][$activity_postfix]['PAGING'] =
$data['PAGING'];
return $search_array;
}
/**
* restoreLastSearchFromSession puts back, from the session, the last
* search a user made on a list such as users, groups or roles, so the
* list shows the same search when the user returns to it.
*
* @param array &$data field variables used by view to draw itself
* @param string $activity current activity marshalling last search for
* @param string $field_postfix some activities support multiple search
* forms. The field postfix is used to select among these.
* @return array the restored SEARCH_ARRAY from session (a list of
* [field, value, op] triples); void when no last-search entry
* exists for the activity in session
*/
function restoreLastSearchFromSession(&$data, $activity,
$field_postfix = "")
{
$activity_postfix = $activity . $field_postfix;
if (empty($_SESSION['LAST_SEARCH'][$activity_postfix])) {
return;
}
$last_search = $_SESSION['LAST_SEARCH'][$activity_postfix];
foreach (['COMPARISON_TYPES', 'EQUAL_COMPARISON_TYPES',
'SORT_TYPES', 'SEARCH_ARRAY', 'PAGING'] as $field) {
$data[$field] = (empty($last_search[$field])) ? [] :
$last_search[$field];
}
foreach (['COMPARISON_FIELDS', 'SORT', 'FIELD_NAMES'] as $field) {
foreach ($last_search[$field] as $name => $value) {
$data[$name] = $value;
}
}
return $data['SEARCH_ARRAY'];
}
}