<?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\configs as C;
use seekquarry\yioop\library as L;
use seekquarry\yioop\library\mail as ML;
/**
* AccountaccessComponent holds the activities of the control panel that
* are about who may do what: accounts, users, roles, and groups, which
* is to say the settings of users
* and groups, what roles and groups a user has, what roles and users
* a group has, and what activities make up a role. It is used by
* AdminController
*
* @author Chris Pollett
*/
class AccountaccessComponent extends Component
{
/**
* ADD_USER_FIELDS names the boxes on the add user form whose
* contents come back when the form is refused. A refusal sends the
* writer to the form again, and these travel in the address so the
* boxes open holding what was typed. The two password boxes are not
* among them, since a password does not belong in an address.
*/
const ADD_USER_FIELDS = ["user_name", "first_name", "last_name",
"email", "status"];
/**
* manageAccount draws the reader's own account page and saves what a
* form on it changed, their password among the rest.
*
* @return array $data SCRIPT field contains success or failure message
*/
public function manageAccount()
{
$parent = $this->parent;
$feed_model = $parent->model("feed");
$signin_model = $parent->model("signin");
$group_model = $parent->model("group");
$wiki_model = $parent->model("wiki");
$user_model = $parent->model("user");
$impression_model = $parent->model("impression");
$crawl_model = $parent->model("crawl");
$role_model = $parent->model("role");
$profile_model = $parent->model("profile");
$locale_model = $parent->model("locale");
$cron_model = $parent->model("cron");
$credit_model = $parent->model("credit");
$profile = $profile_model->getProfile(C\WORK_DIRECTORY);
$data["ELEMENT"] = "manageaccount";
$data['SCRIPT'] = "";
$data["INCLUDE_SCRIPTS"] ??= [];
$data["INCLUDE_SCRIPTS"][] = "register_validator";
$data['MESSAGE'] = "";
$changed_settings_flag = false;
$changed_locale_flag = true;
$changed_user = false;
$data['yioop_bot_configuration'] = $profile['CONFIGURE_BOT'];
if ($profile['CONFIGURE_BOT'] == 'enable_bot_users') {
$data['yioop_bot_configuration'] = true;
} else {
$data['yioop_bot_configuration'] = false;
}
$user_id = $_SESSION['USER_ID'];
if (isset($_REQUEST['arg']) &&
$_REQUEST['arg'] == "stopactingas" &&
!empty($_SESSION['IMPERSONATOR_RETURN_ID'])) {
$return_id = $_SESSION['IMPERSONATOR_RETURN_ID'];
$return_name = $signin_model->getUserName($return_id);
$supplied_password =
$_REQUEST['impersonator_password'] ?? "";
$check_name = $return_name;
$reauth = $signin_model->checkValidSignin($check_name,
$supplied_password);
if (!$reauth) {
return $parent->redirectWithMessage(
tl('accountaccess_component_reauth_failed'),
false, false, true);
}
$acting_as = $_SESSION['USER_NAME'] ?? '';
unset($_SESSION['IMPERSONATOR_RETURN_ID']);
unset($_SESSION['IMPERSONATOR_RETURN_NAME']);
$_SESSION['USER_ID'] = $return_id;
$_SESSION['USER_NAME'] = $return_name;
$_REQUEST[C\p('CSRF_TOKEN')] =
$parent->generateCSRFToken($return_id);
L\crawlLog("ACT AS: user " . $return_name .
" (id $return_id) stopped acting as user " .
$acting_as);
unset($_REQUEST['route']['c'], $_REQUEST['route']['a']);
$_REQUEST['c'] = 'admin';
$_REQUEST['a'] = 'manageUsers';
return $parent->redirectWithMessage(
tl('accountaccess_component_stopped_acting'),
false, false, true);
}
/* What is gathered here is what the page suggests the reader
might want to read: threads and groups other people like them
have taken part in. */
$cron_timestamp =
$cron_model->getCronTime("item_group_recommendations");
$data['THREAD_RECOMMENDATIONS'] =
$user_model->getRecommendations($cron_timestamp,
$user_id, C\THREAD_RECOMMENDATION);
$data['GROUP_RECOMMENDATIONS'] =
$user_model->getRecommendations($cron_timestamp,
$user_id, C\GROUP_RECOMMENDATION);
$data['RESOURCE_RECOMMENDATIONS'] =
$user_model->getResourceRecommendations($user_id);
$username = $signin_model->getUserName($user_id);
$data["USER"] = $user_model->getUser($username);
$data["CRAWL_MANAGER"] = false;
if ($user_model->isAllowedUserActivity($user_id, "manageCrawls")) {
$data['NO_TOPMARGIN'] = true;
$this->initCrawlBadges($user_id, $data);
}
if (isset($_REQUEST['edit']) && $_REQUEST['edit'] == "true") {
$data['EDIT_USER'] = true;
}
if (isset($_REQUEST['edit_pass'])) {
if ($_REQUEST['edit_pass'] == "true") {
$data['EDIT_USER'] = true;
$data['EDIT_PASSWORD'] = true;
} else {
$data['EDIT_USER'] = true;
}
}
if (isset($_REQUEST['edit_recovery'])) {
if ($_REQUEST['edit_recovery'] == "true") {
$data['EDIT_USER'] = true;
$data['EDIT_RECOVERY'] = true;
} else {
$data['EDIT_USER'] = true;
}
}
if (isset($_REQUEST['edit_purchase']) &&
$_REQUEST['edit_purchase'] == "true") {
$data['EDIT_USER'] = true;
}
if (!empty($data['EDIT_USER'])) {
$user_session = $user_model->getUserSession($user_id);
if (C\p('RECOVERY_MODE') == C\QUESTIONS_RECOVERY &&
!empty($data['EDIT_RECOVERY'])) {
$recovery_user = $user_model->getUserById($user_id);
$data['RECOVERY_QUESTION'] =
empty($recovery_user['RECOVERY_QUESTION']) ? "" :
$parent->clean($recovery_user['RECOVERY_QUESTION'],
"string");
$data['RECOVERY_ANSWER'] =
empty($recovery_user['RECOVERY_ANSWER']) ? "" :
C\RECOVERY_ANSWER_PLACEHOLDER;
}
}
$data['USERNAME'] = $username;
$data['NUM_SHOWN'] = 5;
$personal_group_name = C\PERSONAL_GROUP_PREFIX . $user_id;
if (($personal_group_id =
$group_model->getGroupId($personal_group_name)) < 0) {
$personal_group_id = $group_model->addGroup($personal_group_name,
$user_id, C\INVITE_ONLY_JOIN, C\ACTIVE_STATUS,
C\NON_VOTING_GROUP, C\FOREVER, 0);
}
$this->initSocialBadges($user_id, $data);
$groups = $group_model->getUserGroups($user_id, "", [], 0,
min($data['NUM_GROUPS'], 100));
$data['GROUPS'] = [];
$i = 0;
$len = strlen(C\PERSONAL_GROUP_PREFIX);
$group_ids = [];
foreach ($groups as $group) {
$group_ids[] = $group['GROUP_ID'];
}
$most_recent_views = $impression_model->mostRecentGroupViews($user_id,
$group_ids);
foreach ($groups as $group) {
$group_id = $group['GROUP_ID'];
$data['GROUPS'][$i] = $group;
$data['GROUPS'][$i]['MEMBER_STATUS'] =
$data['GROUPS'][$i]['STATUS'];
$data['GROUPS'][$i]["MOST_RECENT_VIEW"] =
$most_recent_views[$group_id] ?? 0;
$item = $feed_model->getMostRecentGroupPost($group_id);
$data['GROUPS'][$i]['NUM_POSTS'] =
$feed_model->getGroupPostCount($group_id);
$data['GROUPS'][$i]['NEW_POSTS'] =
$feed_model->getGroupPostCount($group_id,
$data['GROUPS'][$i]["MOST_RECENT_VIEW"]);
$data['GROUPS'][$i]['NUM_THREADS'] =
$feed_model->getGroupThreadCount($group_id);
$data['GROUPS'][$i]['NUM_PAGES'] =
$wiki_model->getGroupPageCount($group_id);
if (isset($item['TITLE'])) {
$data['GROUPS'][$i]["ITEM_TITLE"] = $item['TITLE'];
$data['GROUPS'][$i]["THREAD_ID"] = $item['PARENT_ID'];
} else {
$data['GROUPS'][$i]["ITEM_TITLE"] =
tl('accountaccess_component_no_posts_yet');
$data['GROUPS'][$i]["THREAD_ID"] = -1;
}
$i++;
}
usort($data['GROUPS'], function($group1, $group2) {
if ($group1["MOST_RECENT_VIEW"] == $group2["MOST_RECENT_VIEW"]) {
return 0;
}
return ($group1["MOST_RECENT_VIEW"] > $group2["MOST_RECENT_VIEW"]) ?
-1 : 1;
});
$data['GROUPS'] = array_slice($data['GROUPS'], 0, $data['NUM_SHOWN']);
$data['NUM_SHOWN'] = count($data['GROUPS']);
$languages = $locale_model->getLocaleList();
foreach ($languages as $language) {
$data['LANGUAGES'][$language['LOCALE_TAG']] =
$language['LOCALE_NAME'];
}
if (isset($_REQUEST['lang']) &&
in_array($_REQUEST['lang'],
array_keys($data['LANGUAGES']))) {
$old_value = isset($_SESSION['l']) ?
$_SESSION['l'] : L\getLocaleTag();
$_SESSION['l'] = $_REQUEST['lang'];
L\setLocaleObject($_SESSION['l']);
if ($old_value != $_SESSION['l'] || empty($user_session['l'])
|| $user_session['l'] != $_SESSION['l']) {
$changed_locale_flag = true;
$user_session['l'] = $_SESSION['l'];
}
}
$data['LANGUAGES_TO_SHOW'] = 1;
$data['LOCALE_TAG'] = L\getLocaleTag();
$data['NUM_MIXES'] = count($crawl_model->getMixList($user_id));
$arg = (isset($_REQUEST['arg'])) ? $_REQUEST['arg'] : "";
switch ($arg) {
case "updateuser":
if (isset($data['EDIT_PASSWORD'])) {
if (isset($_REQUEST['new_password']) ) {
$pass_len = strlen($_REQUEST['new_password']);
if ($pass_len > C\LONG_NAME_LEN) {
return $parent->redirectWithMessage(
tl('accountaccess_component_passwords_too_long'),
["edit", "edit_pass"]);
}
}
if (!isset($_REQUEST['retype_password']) ||
!isset($_REQUEST['new_password']) ||
$_REQUEST['retype_password'] !=
$_REQUEST['new_password']) {
return $parent->redirectWithMessage(
tl('accountaccess_component_passwords_dont_match'),
["edit", "edit_pass"]);
}
if (!empty(L\passwordPolicyViolations(
$_REQUEST['new_password']))) {
return $parent->redirectWithMessage(
tl('accountaccess_component_password_requirements'),
["edit", "edit_pass"]);
}
$result = $signin_model->checkValidSignin($username,
$parent->clean($_REQUEST['password'], "password") );
if (!$result) {
return $parent->redirectWithMessage(
tl('accountaccess_component_invalid_password'),
["edit", "edit_pass"]);
}
$signin_model->changePassword($username,
$parent->clean($_REQUEST['new_password'],
"password"));
if ($changed_settings_flag) {
$user_model->setUserSession($user_id, $user_session);
}
$_REQUEST['edit'] = "true";
return $parent->redirectWithMessage(
tl('accountaccess_component_password_updated'),
["edit"]);
}
if (!empty($data['EDIT_RECOVERY']) &&
C\p('RECOVERY_MODE') == C\QUESTIONS_RECOVERY &&
!empty($_REQUEST['recovery_question']) &&
!empty($_REQUEST['recovery_answer'])) {
$result = $signin_model->checkValidSignin($username,
$parent->clean($_REQUEST['password'] ?? "",
"password") );
if (!$result) {
$_REQUEST['edit_recovery'] = "true";
return $parent->redirectWithMessage(
tl('accountaccess_component_invalid_password'),
["edit", "edit_recovery"]);
}
$question = mb_substr($_REQUEST['recovery_question'], 0,
C\RECOVERY_QUESTION_LEN);
$answer = $_REQUEST['recovery_answer'];
if ($answer === C\RECOVERY_ANSWER_PLACEHOLDER) {
$answer = null;
}
$user_model->setRecoveryQuestion($user_id, $question,
$answer);
$_REQUEST['edit'] = "true";
return $parent->redirectWithMessage(
tl('accountaccess_component_answers_updated'),
["edit"]);
}
$user = [];
$user['USER_ID'] = $user_id;
$fields = ["EMAIL" => C\LONG_NAME_LEN,
"FIRST_NAME" => C\NAME_LEN, "LAST_NAME" => C\NAME_LEN];
foreach ($fields as $field => $len) {
if (isset($_REQUEST[$field])) {
$user[$field] = substr($parent->clean(
$_REQUEST[$field], "string"), 0, $len);
if ($data["USER"][$field] != $user[$field]) {
$data['USER'][$field] = $user[$field];
$changed_user = true;
}
}
}
if (isset($user['EMAIL']) &&
ML\MailSiteFactory::isForbiddenAccountEmail(
$user['EMAIL'])) {
return $parent->redirectWithMessage(
tl('accountaccess_component_email_system_address'),
["edit"]);
}
$is_bot_updated = false;
$is_bot_toggle = false;
$bot_role_id = $role_model->getRoleId('Bot User');
if (isset($_REQUEST['IS_BOT_USER']) && $bot_role_id) {
$is_bot_toggle = 1;
$is_bot_updated = true;
} else if ($bot_role_id) {
$is_bot_toggle = 0;
}
if (isset($_REQUEST['BOT_TOKEN'])) {
$user['BOT_TOKEN'] = $_REQUEST['BOT_TOKEN'];
$data['USER']['BOT_TOKEN'] = $user['BOT_TOKEN'];
$is_bot_updated = true;
}
if (isset($_REQUEST['BOT_CALLBACK_URL'])){
$user['CALLBACK_URL'] = $_REQUEST['BOT_CALLBACK_URL'];
$data['USER']['CALLBACK_URL'] = $user['CALLBACK_URL'];
$is_bot_updated = true;
}
if (isset($_FILES['user_icon']['name']) &&
$_FILES['user_icon']['name'] != "") {
if (!in_array($_FILES['user_icon']['type'],
['image/png', 'image/gif', 'image/jpeg',
'image/webp', 'image/x-icon'])) {
return $parent->redirectWithMessage(
tl('accountaccess_component_unknown_imagetype'),
["edit", "edit_pass"]);
}
if ($_FILES['user_icon']['size'] > C\THUMB_SIZE) {
return $parent->redirectWithMessage(
tl('accountaccess_component_icon_too_big'),
["edit", "edit_pass"]);
}
if (empty($_FILES['user_icon']['data'])) {
$user['IMAGE_STRING'] = file_get_contents(
$_FILES['user_icon']['tmp_name']);
$changed_user = true;
} else {
$user['IMAGE_STRING'] = $_FILES['user_icon']['data'];
$changed_user = true;
}
$folder = $user_model->getUserIconFolder(
$user['USER_ID']);
if (!$folder) {
return $parent->redirectWithMessage(
tl('accountaccess_component_no_user_folder'),
["edit", "edit_pass"]);
}
}
if ($changed_user || $is_bot_updated) {
$result = $signin_model->checkValidSignin($username,
$parent->clean($_REQUEST['password'] ?? "",
"password") );
if (!$result) {
return $parent->redirectWithMessage(
tl('accountaccess_component_valid_password_required'),
["edit"]);
}
}
if ($changed_user) {
$user_model->updateUser($user);
}
if ($changed_settings_flag || $changed_locale_flag) {
$user_model->setUserSession($user_id, $user_session);
}
if ($is_bot_updated) {
if ($is_bot_toggle === 1) {
$data['USER']['IS_BOT_USER'] = 1;
$role_model->addUserRole($user_id, $bot_role_id);
} else if ($is_bot_toggle === 0) {
$data['USER']['IS_BOT_USER'] = 0;
$role_model->deleteUserRole($user_id, $bot_role_id);
}
$user_model->updateBot($user);
}
$data['USER']['USER_ICON'] = $user_model->getUserIconUrl(
$user['USER_ID']);
unset($user['IMAGE_STRING']);
return $parent->redirectWithMessage(
tl('accountaccess_component_user_updated'),
["edit", "edit_pass"]);
case "purchaserole":
$message = $this->purchaseRole($role_model,
$credit_model, $user_id,
intval($_REQUEST['purchase_role_id'] ?? 0));
return $parent->redirectWithMessage($message,
["edit", "edit_purchase"]);
case "unsubscriberole":
$message = $this->unsubscribeRole($role_model, $user_id,
intval($_REQUEST['unsubscribe_role_id'] ?? 0));
return $parent->redirectWithMessage($message, ["edit"]);
}
$this->renewExpiredRoleSubscriptions($role_model, $credit_model,
$user_id);
$sellable_roles = $role_model->getSellableRoles();
$data['SELLABLE_ROLES_EXIST'] = !empty($sellable_roles);
if ($data['SELLABLE_ROLES_EXIST']) {
$data['USER_ROLE_LIST'] =
$role_model->getUserRoles($user_id, "");
$data['USER_SELLABLE_ROLES'] =
$role_model->getUserSellableRoles($user_id);
$data['CREDIT_BALANCE'] =
$credit_model->getCreditBalance($user_id);
$held_ids = [];
foreach ($data['USER_ROLE_LIST'] as $role) {
$held_ids[(int)$role['ROLE_ID']] = true;
}
$purchasable = [];
foreach ($sellable_roles as $role) {
if (empty($held_ids[(int)$role['ROLE_ID']])) {
$purchasable[] = $role;
}
}
$data['PURCHASABLE_ROLES'] = $purchasable;
if (isset($_REQUEST['edit_purchase']) &&
$_REQUEST['edit_purchase'] == "true") {
$data['EDIT_PURCHASE'] = true;
}
}
return $data;
}
/**
* chargeFrequency gives the number of seconds a purchase of a role
* lasts, from how often that role charges. A one-time ("once") purchase
* never lapses and
* returns the FOREVER sentinel; the recurring frequencies return
* their length in seconds, so a grant's expiry can be advanced by
* that amount.
*
* @param string $frequency one of the CHARGE_FREQUENCY values
* @return int seconds the purchase lasts, or FOREVER when permanent
*/
private function chargeFrequency($frequency)
{
$months_per_semi_annual = 6;
$seconds = [
'once' => C\FOREVER,
'monthly' => C\ONE_MONTH,
'semi_annual' => $months_per_semi_annual * C\ONE_MONTH,
'yearly' => C\ONE_YEAR,
'test_minute' => C\ONE_MINUTE];
return $seconds[$frequency] ?? C\FOREVER;
}
/**
* purchaseRole buys a role for an account, or starts up again one
* the account had cancelled.
* If the user still holds the role and it has not lapsed, buying it
* again only turns renewing back on and charges nothing until the
* current paid period ends. Otherwise it checks the role is on sale
* and the balance covers the cost, then debits the cost and grants
* (or re-dates) the role with an expiry one billing period out, or a
* never-lapsing grant for a one-time purchase.
*
* @param object $role_model model used to read and grant roles
* @param object $credit_model model used to read and debit credits
* @param int $user_id id of the buyer
* @param int $role_id id of the role to buy
* @return string the status message to show the buyer
*/
private function purchaseRole($role_model, $credit_model, $user_id,
$role_id)
{
$for_sale = null;
foreach ($role_model->getSellableRoles() as $role) {
if ((int)$role['ROLE_ID'] === (int)$role_id) {
$for_sale = $role;
break;
}
}
if (empty($for_sale)) {
return tl('accountaccess_component_role_not_for_sale');
}
$now = time();
$held = null;
foreach ($role_model->getUserSellableRoles($user_id) as $row) {
if ((int)$row['ROLE_ID'] === (int)$role_id) {
$held = $row;
break;
}
}
if ($held !== null) {
$held_expires = (int)$held['EXPIRES'];
if ($held_expires === C\FOREVER || $held_expires > $now) {
if ((int)$held['WILL_RENEW'] === 1) {
return tl('accountaccess_component_role_already_held');
}
$role_model->setUserRoleRenew($user_id, $role_id, 1);
return tl('accountaccess_component_role_resubscribed');
}
}
$cost = (int)$for_sale['ROLE_COST'];
if ($credit_model->getCreditBalance($user_id) < $cost) {
return tl('accountaccess_component_role_no_credits');
}
$period = $this->chargeFrequency($for_sale['CHARGE_FREQUENCY']);
$expires = ($period === C\FOREVER) ? C\FOREVER : $now + $period;
$credit_model->updateCredits($user_id, -$cost,
'accountaccess_component_role_purchase');
if ($held !== null) {
$role_model->updateUserRoleExpires($user_id, $role_id,
$expires);
$role_model->setUserRoleRenew($user_id, $role_id, 1);
} else {
$role_model->addUserRole($user_id, $role_id, $expires, 1);
}
return tl('accountaccess_component_role_purchased');
}
/**
* unsubscribeRole stops a role renewing for an account. If the grant is
* still within its paid period it is kept but stops renewing, so the
* user keeps the role until it lapses and it is removed then; the
* account page shows the expiry date meanwhile. A grant that has
* already lapsed, or a one-time permanent grant, is removed at once.
*
* @param object $role_model model used to read and update grants
* @param int $user_id id of the user cancelling
* @param int $role_id id of the role to cancel
* @return string the status message to show the user
*/
private function unsubscribeRole($role_model, $user_id, $role_id)
{
$held = null;
foreach ($role_model->getUserSellableRoles($user_id) as $row) {
if ((int)$row['ROLE_ID'] === (int)$role_id) {
$held = $row;
break;
}
}
if ($held === null) {
return tl('accountaccess_component_role_unsubscribed');
}
$expires = (int)$held['EXPIRES'];
if ($expires !== C\FOREVER && $expires > time()) {
$role_model->setUserRoleRenew($user_id, $role_id, 0);
return tl('accountaccess_component_role_cancelled');
}
$role_model->deleteUserRole($user_id, $role_id);
return tl('accountaccess_component_role_unsubscribed');
}
/**
* renewExpiredRoleSubscriptions settles the roles an account holds
* that have run out, which is done when the account page is read. A grant
* the user cancelled is removed once it lapses. A
* grant still set to renew has its expiry moved forward one billing
* period at a time, spending credits, for as many periods as the
* balance can cover. A one-time ("once") grant never lapses and is
* left alone.
*
* @param object $role_model model used to read and update grants
* @param object $credit_model model used to read and debit credits
* @param int $user_id id of the user whose subscriptions to settle
*/
private function renewExpiredRoleSubscriptions($role_model,
$credit_model, $user_id)
{
$balance = $credit_model->getCreditBalance($user_id);
$now = time();
foreach ($role_model->getUserSellableRoles($user_id) as $role) {
$expires = (int)$role['EXPIRES'];
if ($expires === C\FOREVER || $expires > $now) {
continue;
}
if ((int)$role['WILL_RENEW'] === 0) {
$role_model->deleteUserRole($user_id, $role['ROLE_ID']);
continue;
}
$period = $this->chargeFrequency($role['CHARGE_FREQUENCY']);
$cost = (int)$role['ROLE_COST'];
if ($period === C\FOREVER || $cost <= 0) {
continue;
}
$new_expires = $expires;
while ($new_expires <= $now && $balance >= $cost) {
$credit_model->updateCredits($user_id, -$cost,
'accountaccess_component_role_renewal');
$balance -= $cost;
$new_expires += $period;
}
if ($new_expires !== $expires) {
$role_model->updateUserRoleExpires($user_id,
$role['ROLE_ID'], $new_expires);
}
}
}
/**
* passwordStandIn gives the value the two password boxes are drawn
* holding when a form is opened, which stands for the password
* already set rather than being it.
*
* A reader who leaves those boxes alone is not changing their
* password, so the page has to tell the value it drew from anything
* a reader typed. What it draws must be the same string every time,
* since the form is drawn in one request and sent back in another.
* It is made with crawlCrypt, the hashing a sign-in uses, so that
* nothing in the account pages reaches for a weaker hash; the salt
* is written here rather than drawn at random for the same reason
* the value has to be steady. Neither is a secret: the word hashed
* is the word password and this file is public.
*
* The hashing is done once and kept, since it costs about a fifth of
* a second and the answer never changes. Under Yioop's own web
* server that is once for the life of the server.
*
* @return string the value the password boxes are drawn holding,
* sixty characters, which fits the length those boxes allow
*/
private function passwordStandIn()
{
static $stand_in = null;
if ($stand_in === null) {
$stand_in = L\crawlCrypt("password",
'$2y$12$yioopPasswordStandIn..');
}
return $stand_in;
}
/**
* manageUsers draws the accounts screen and carries out what a form
* on it asked for.
*
* This activity allows new users to be added, old users to be
* deleted and allows roles to be added to/deleted from a user
*
* @return array $data infomartion about users of the system, roles, etc.
* as well as status messages on performing a given sub activity
*/
public function manageUsers()
{
$parent = $this->parent;
$request_fields = ['start_row', 'num_show', 'end_row',
'visible_roles', 'visible_groups', 'role_filter', 'role_sorts',
'group_filter', 'group_sorts', 'role_limit', 'group_limit',
'context'];
$signin_model = $parent->model("signin");
$user_model = $parent->model("user");
$group_model = $parent->model("group");
$role_model = $parent->model("role");
$possible_arguments = ["adduser", 'edituser', 'search',
"deleteuser", "adduserrole", "deleteuserrole",
"addusergroup", "deleteusergroup", "updatestatus",
"actasuser"];
$data["ELEMENT"] = "manageusers";
$data['SCRIPT'] = "";
$data["INCLUDE_SCRIPTS"] ??= [];
$data["INCLUDE_SCRIPTS"][] = "register_validator";
/* The script says this when the two password boxes differ, and
it reads its wordings from a tl array the page writes out. */
$data['SCRIPT'] .= 'tl = ' . json_encode([
"register_validator_js_retype_password_matched" =>
tl('register_validator_js_retype_password_matched'),
"register_validator_js_retype_password_not_matched" =>
tl('register_validator_js_retype_password_not_matched')]) .
';';
$data['STATUS_CODES'] = [
C\ACTIVE_STATUS => tl('accountaccess_component_active_status'),
C\INACTIVE_STATUS => tl('accountaccess_component_inactive_status'),
C\EDITOR_STATUS => tl('accountaccess_component_editor_status'),
C\SUSPENDED_STATUS =>
tl('accountaccess_component_suspended_status'),
];
$data['MEMBERSHIP_CODES'] = [
C\INACTIVE_STATUS => tl('accountaccess_component_request_join'),
C\INVITED_STATUS => tl('accountaccess_component_invited'),
C\ACTIVE_STATUS => tl('accountaccess_component_active_status'),
C\EDITOR_STATUS => tl('accountaccess_component_editor_status'),
C\SUSPENDED_STATUS =>
tl('accountaccess_component_suspended_status')
];
$data['FORM_TYPE'] = "adduser";
$search_array = [];
$username = "";
if (isset($_REQUEST['user_name'])) {
$username = substr($parent->clean($_REQUEST['user_name'], "string"),
0, C\NAME_LEN);
}
if ($username == "" && isset($_REQUEST['arg']) && $_REQUEST['arg']
!= "search") {
unset($_REQUEST['arg']);
}
$select_group = isset($_REQUEST['selectgroup']) ?
$parent->clean($_REQUEST['selectgroup'],"string") : "";
$select_role = isset($_REQUEST['selectrole']) ?
$parent->clean($_REQUEST['selectrole'],"string") : "";
if (isset($_REQUEST['arg']) && $_REQUEST['arg'] == 'edituser') {
if ($select_role != "") {
$_REQUEST['arg'] = "adduserrole";
} else if ($select_group != ""){
$_REQUEST['arg'] = "addusergroup";
}
}
$user_id = -1;
$data['visible_roles'] = 'false';
$data['visible_groups'] = 'false';
if ($username != "") {
$user_id = $signin_model->getUserId($username);
if ($user_id) {
$this->getUserRolesData($data, $user_id);
$this->getUserGroupsData($data, $user_id);
}
}
$data['CURRENT_USER'] = ["user_name" => "", "first_name" => "",
"last_name" => "", "email" => "", "status" => "", "password" => "",
"repassword" => ""];
/* An attempt to add a user that does not go through comes back
here with what was typed still in the address, so the form
opens again holding it rather than empty. The two password
boxes are not among those fields and start empty, since a
password does not belong in an address. */
foreach (self::ADD_USER_FIELDS as $field) {
if (isset($_REQUEST[$field])) {
$data['CURRENT_USER'][$field] = $parent->clean(
$_REQUEST[$field], "string");
}
}
$data['CAN_SET_PASSWORD'] = true;
$data['PAGING'] = "";
if (isset($_REQUEST['arg']) &&
in_array($_REQUEST['arg'], $possible_arguments)) {
$arg = $_REQUEST['arg'];
$pass_len = (isset($_REQUEST['new_password'])) ?
strlen($_REQUEST['new_password']) : 0;
$default_search = false;
switch ($arg) {
case "adduser":
return $this->addUser($data, $username, $user_id,
$pass_len, $request_fields);
case "edituser":
$answer = $this->editUser($data, $username, $user_id,
$request_fields);
if ($answer !== null) {
return $answer;
}
break;
case "actasuser":
return $this->actAsUser($username, $request_fields);
case "deleteuser":
return $this->deleteUser($username, $user_id,
$request_fields);
case "adduserrole":
return $this->addUserRole($user_id, $select_role,
$request_fields);
case "addusergroup":
return $this->addUserGroup($data, $user_id,
$select_group, $request_fields);
case "deleteuserrole":
return $this->deleteUserRole($data, $user_id,
$request_fields);
case "deleteusergroup":
return $this->deleteUserGroup($data, $user_id,
$request_fields);
case "search":
$search_array = $this->searchUsers($data,
$default_search);
break;
case "updatestatus":
return $this->updateUserStatus($data, $username,
$user_id, $request_fields);
}
}
if ($search_array == [] || !empty($default_search)) {
if (!empty($_SESSION['LAST_SEARCH']['manageUsers_name'])) {
if (!empty($_REQUEST['arg']) && $_REQUEST['arg'] == 'search') {
$search_array =
$parent->restoreLastSearchFromSession($data,
'manageUsers_name');
} else if (!empty($_REQUEST['context'])) {
$search_array = $_SESSION['LAST_SEARCH'][
'manageUsers_name']['SEARCH_ARRAY'];
$data['PAGING'] =
$_SESSION['LAST_SEARCH']['manageUsers_name']['PAGING'];
}
}
if ($search_array == []) {
$search_array[] = ["user", "", "", "ASC"];
}
}
$parent->pagingLogic($data, $user_model, "USERS",
C\DEFAULT_ADMIN_PAGING_NUM, $search_array, "");
$num_users = count($data['USERS']);
for ($i = 0; $i < $num_users; $i++) {
$data['USERS'][$i]['NUM_GROUPS'] =
$group_model->countUserGroups($data['USERS'][$i]['USER_ID']);
}
/* The add user row is closed on arriving at Manage Users. A
refused attempt sends the writer back here with what was
typed in the address, and then it stays open. */
$was_refused = false;
foreach (self::ADD_USER_FIELDS as $field) {
if (isset($_REQUEST[$field])) {
$was_refused = true;
break;
}
}
if ($data['FORM_TYPE'] == 'adduser' && !$was_refused) {
$data['SCRIPT'] .= "setDisplay('admin-form-row', false);";
}
return $data;
}
/**
* addUser takes down a new account from what a form carried.
*
* manageUsers calls it for the adduser argument. A refusal comes back
* to the form with what was typed still in it, and the two password
* boxes start empty, since a password does not belong in an address.
*
* @param array &$data what the page will show, added to here
* @param string $username name the reader typed for the new account
* @param int $user_id the account that name already answers to, or -1
* @param int $pass_len how long the password typed is
* @param array $request_fields fields a redirect carries so the reader
* comes back to the row they were on
* @return mixed what is handed back to the browser,
* or null where the page is drawn as usual
*/
private function addUser(&$data, $username, $user_id, $pass_len,
$request_fields)
{
$parent = $this->parent;
$signin_model = $parent->model("signin");
$user_model = $parent->model("user");
$group_model = $parent->model("group");
/* A refusal keeps what was typed, so the form
opens again holding it. The arg is left behind:
carrying it would run this branch again on
arriving, with no passwords in the address, and
refuse a second time. The form the page opens
with is the add user form already. */
$keep_typed = array_merge($request_fields,
self::ADD_USER_FIELDS);
if ($pass_len > C\LONG_NAME_LEN ) {
return $parent->redirectWithMessage(
tl('accountaccess_component_passwords_too_long'),
$keep_typed);
} else if ($_REQUEST['retypepassword'] !=
$_REQUEST['password']) {
return $parent->redirectWithMessage(
tl('accountaccess_component_passwords_dont_match'),
$keep_typed);
} else if (!empty(L\passwordPolicyViolations(
$_REQUEST['password']))) {
return $parent->redirectWithMessage(tl(
'accountaccess_component_password_requirements'),
$keep_typed);
} else if (trim($username) == "") {
return $parent->redirectWithMessage(
tl('accountaccess_component_invalid_username'),
$keep_typed);
} else if ($signin_model->getUserId($username) > 0) {
return $parent->redirectWithMessage(
tl('accountaccess_component_user_exists'),
$keep_typed);
} else if (!empty($_REQUEST['email']) &&
ML\MailSiteFactory::isForbiddenAccountEmail(
substr(trim($parent->clean($_REQUEST['email'],
"string")), 0, C\LONG_NAME_LEN))) {
return $parent->redirectWithMessage(
tl('accountaccess_component_email_system_address'),
$request_fields);
} else if (!isset($data['STATUS_CODES'][
$_REQUEST['status']])) {
$_REQUEST['status'] = C\INACTIVE_STATUS;
} else {
$norm_password =
substr($parent->clean($_REQUEST['password'],
"password"), 0, C\LONG_NAME_LEN);
$username = trim($username);
$user_id = $user_model->addUser($username,
$norm_password,
substr(trim($parent->clean($_REQUEST['first_name'],
"string")), 0, C\NAME_LEN),
substr(trim($parent->clean($_REQUEST['last_name'],
"string")), 0, C\NAME_LEN),
substr(trim($parent->clean($_REQUEST['email'],
"string")), 0, C\LONG_NAME_LEN),
$_REQUEST['status']
);
$data['USER_NAMES'][$username] = $username;
$personal_group_name = C\PERSONAL_GROUP_PREFIX .
$user_id;
if ($group_model->getGroupId($personal_group_name) < 0){
$group_model->addGroup($personal_group_name,
$user_id, C\INVITE_ONLY_JOIN, C\ACTIVE_STATUS,
C\NON_VOTING_GROUP, C\FOREVER, 0);
}
return $parent->redirectWithMessage(
tl('accountaccess_component_user_added'),
$request_fields);
}
}
/**
* editUser draws one account's details and saves what a form changed.
*
* manageUsers calls it for the edituser argument. A password is
* written only where both boxes were filled and match, so opening
* the form and saving it changes nothing a reader did not type.
*
* @param array &$data what the page will show, added to here
* @param string $username name of the account being changed
* @param int $user_id which account that is
* @param array $request_fields fields a redirect carries so the reader
* comes back to the row they were on
* @return mixed what is handed back to the browser,
* or null where the page is drawn as usual
*/
private function editUser(&$data, $username, $user_id, $request_fields)
{
$parent = $this->parent;
$user_model = $parent->model("user");
$data['FORM_TYPE'] = "edituser";
if (!empty($_REQUEST['context'])) {
$data['context'] = "search";
}
$user = $user_model->getUser($username);
if (!$user) {
return $parent->redirectWithMessage(
tl('accountaccess_component_user_doesnt_exist'),
$request_fields);
}
$update = false;
$error = false;
if ($user["USER_ID"] == C\PUBLIC_USER_ID) {
return $parent->redirectWithMessage(
tl('accountaccess_component_cant_edit_public_user'),
$request_fields);
}
$editing_root = ($user["USER_ID"] == C\ROOT_ID);
$actor_is_root =
($_SESSION['USER_ID'] == C\ROOT_ID);
$can_set_password =
(!$editing_root || $actor_is_root);
$data['CAN_SET_PASSWORD'] = $can_set_password;
if (!$can_set_password) {
unset($_REQUEST['password']);
unset($_REQUEST['retypepassword']);
}
/* The two password boxes are drawn holding a
stand-in rather than the real password, so leaving
them alone means the password is not being changed.
Only a box holding something other than that
stand-in is a change, and only then do the two have
to agree. Every other field saves either way. */
$stand_in = $this->passwordStandIn();
$changing_password =
($_REQUEST["password"] ?? $stand_in) != $stand_in ||
($_REQUEST["retypepassword"] ?? $stand_in) !=
$stand_in;
foreach ($data['CURRENT_USER'] as $field => $value) {
$upper_field = strtoupper($field);
if (str_contains($field, "password") &&
!$changing_password) {
continue;
}
if (isset($_REQUEST[$field]) && $field != 'user_name') {
if ($field != "password" || ($_REQUEST["password"]
!= $stand_in && $_REQUEST["password"] ==
$_REQUEST["retypepassword"])) {
if ($field == "password" && !empty(
L\passwordPolicyViolations(
$_REQUEST["password"]))) {
return $parent->redirectWithMessage(tl(
'accountaccess_component_password_requirements'
), array_merge(array('arg', 'user_name'),
$request_fields));
}
$clean_type =
(!str_contains($field, 'password')) ?
"string" : "password";
$tmp = $parent->clean(
$_REQUEST[$field], $clean_type);
if ($tmp != $user[$upper_field]) {
$user[$upper_field] = $tmp;
if (!isset($_REQUEST['change_filter'])) {
$update = true;
}
}
$data['CURRENT_USER'][$field] =
$user[$upper_field];
} else if ($_REQUEST["password"] !=
$_REQUEST["retypepassword"]) {
$error = true;
return;
}
} else if (isset($user[$upper_field])){
if ($field != "password" &&
$field != "retypepassword") {
$data['CURRENT_USER'][$field] =
$user[$upper_field];
}
}
}
$data['CURRENT_USER']['password'] = $this->passwordStandIn();
$data['CURRENT_USER']['retypepassword'] = $this->passwordStandIn();
if ($error) {
return $parent->redirectWithMessage(
tl('accountaccess_component_passwords_dont_match'),
array_merge(array('arg', 'user_name'),
$request_fields));
} else if (isset($user['EMAIL']) &&
ML\MailSiteFactory::isForbiddenAccountEmail(
$user['EMAIL'])) {
return $parent->redirectWithMessage(
tl('accountaccess_component_email_system_address'),
array_merge(array('arg', 'user_name'),
$request_fields));
} else if ($update) {
$user_model->updateUser($user);
return $parent->redirectWithMessage(
tl('accountaccess_component_user_updated'),
array_merge(array('arg', 'user_name'),
$request_fields));
} else if (isset($_REQUEST['change_filter'])) {
if ($_REQUEST['change_filter'] == "group") {
$data['SCRIPT'] .= "doMessage('<h1 class=\"red\" >".
tl('accountaccess_component_user_filter_group').
"</h1>');";
} else {
$data['SCRIPT'] .= "doMessage('<h1 class=\"red\" >".
tl('accountaccess_component_user_filter_role').
"</h1>');";
}
}
$data['CURRENT_USER']['id'] = $user_id;
$actor_may_act_as = $user_model->
isAllowedUserActivity($_SESSION['USER_ID'],
'manageUsers');
$data['CAN_ACT_AS_USER'] = ($actor_may_act_as &&
!$editing_root &&
$user["USER_ID"] != C\PUBLIC_USER_ID &&
empty($_SESSION['IMPERSONATOR_RETURN_ID']));
$data['SCRIPT'] .= "elt('focus-button').focus();";
}
/**
* actAsUser signs the root account in as somebody else.
*
* manageUsers calls it for the actasuser argument. Only the root
* account may do this, and it cannot be done to the root account
* itself.
*
* @param string $username name of the account to act as
* @param array $request_fields fields a redirect carries so the reader
* comes back to the row they were on
* @return mixed what is handed back to the browser,
* or null where the page is drawn as usual
*/
private function actAsUser($username, $request_fields)
{
$parent = $this->parent;
$signin_model = $parent->model("signin");
$user_model = $parent->model("user");
$target_id = $signin_model->getUserId($username);
$actor_may_act_as = $user_model->
isAllowedUserActivity($_SESSION['USER_ID'],
'manageUsers');
if (!$actor_may_act_as ||
!empty($_SESSION['IMPERSONATOR_RETURN_ID']) ||
$target_id <= 0 || in_array($target_id,
[C\ROOT_ID, C\PUBLIC_USER_ID])) {
return $parent->redirectWithMessage(
tl('accountaccess_component_no_act_as'),
$request_fields);
}
$_SESSION['IMPERSONATOR_RETURN_ID'] =
$_SESSION['USER_ID'];
$_SESSION['IMPERSONATOR_RETURN_NAME'] =
$_SESSION['USER_NAME'] ?? '';
L\crawlLog("ACT AS: user " .
($_SESSION['USER_NAME'] ?? '') .
" (id " . $_SESSION['IMPERSONATOR_RETURN_ID'] .
") began acting as user " . $username);
$_SESSION['USER_ID'] = $target_id;
$_SESSION['USER_NAME'] = $username;
$_REQUEST[C\p('CSRF_TOKEN')] =
$parent->generateCSRFToken($target_id);
unset($_REQUEST['route']['c'],
$_REQUEST['route']['a']);
$_REQUEST['c'] = 'admin';
$_REQUEST['a'] = 'manageAccount';
return $parent->redirectWithMessage(
tl('accountaccess_component_now_acting_as',
$username), false, false, true);
}
/**
* deleteUser takes an account away.
*
* manageUsers calls it for the deleteuser argument. The root account
* and the public account cannot be taken away, since the site is
* built on both.
*
* @param string $username name of the account to take away
* @param int $user_id which account that is
* @param array $request_fields fields a redirect carries so the reader
* comes back to the row they were on
* @return mixed what is handed back to the browser,
* or null where the page is drawn as usual
*/
private function deleteUser($username, $user_id, $request_fields)
{
$parent = $this->parent;
$signin_model = $parent->model("signin");
$user_model = $parent->model("user");
if (!empty($_REQUEST['context'])) {
$_REQUEST['arg'] = 'search';
$request_fields[] = 'arg';
}
$user_id =
$signin_model->getUserId($username);
if ($user_id <= 0) {
return $parent->redirectWithMessage(
tl('accountaccess_component_username_doesnt_exists'
), $request_fields);
} else if (in_array(
$user_id, [C\ROOT_ID, C\PUBLIC_USER_ID])) {
return $parent->redirectWithMessage(
tl('accountaccess_component_cant_delete_builtin'),
$request_fields);
} else {
$user_model->deleteUser($username);
return $parent->redirectWithMessage(
tl('accountaccess_component_user_deleted'),
$request_fields);
}
}
/**
* addUserRole gives an account a role.
*
* manageUsers calls it for the adduserrole argument, and also when the
* edit form is sent with a role chosen, since choosing one there is
* the same act.
*
* @param int $user_id which account is being given the role
* @param string $select_role name of the role chosen
* @param array $request_fields fields a redirect carries so the reader
* comes back to the row they were on
* @return mixed what is handed back to the browser,
* or null where the page is drawn as usual
*/
private function addUserRole($user_id, $select_role, $request_fields)
{
$_REQUEST['arg'] = 'edituser';
/* The box a role is typed into is only drawn while the list of
the account's roles is open, so being here at all means it was
open. Saying so rather than relying on what the form sent
keeps it open however this was reached. */
$_REQUEST['visible_roles'] = 'true';
$parent = $this->parent;
$role_model = $parent->model("role");
if ($user_id <= 0 ) {
$_REQUEST['arg'] = 'adduser';
return $parent->redirectWithMessage(
tl('accountaccess_component_username_doesnt_exists'),
array_merge(['arg'], $request_fields));
} else if (($role_id = $role_model->getRoleId($select_role)) <= 0) {
return $parent->redirectWithMessage(
tl('accountaccess_component_rolename_doesnt_exists'
), array_merge(['arg', 'user_name'],
$request_fields));
} else if ($role_model->checkUserRole($user_id,
$role_id)) {
return $parent->redirectWithMessage(
tl('accountaccess_component_rolename_already_added'
), array_merge(['arg', 'user_name'],
$request_fields));
} else {
$role_model->addUserRole($user_id, $role_id);
return $parent->redirectWithMessage(
tl('accountaccess_component_rolename_added'),
array_merge(['arg', 'user_name'],
$request_fields));
}
}
/**
* addUserGroup puts an account into a group.
*
* manageUsers calls it for the addusergroup argument, and also when
* the edit form is sent with a group chosen, since choosing one
* there is the same act.
*
* @param array &$data what the page will show, added to here
* @param int $user_id which account is joining
* @param string $select_group name of the group chosen
* @param array $request_fields fields a redirect carries so the reader
* comes back to the row they were on
* @return mixed what is handed back to the browser,
* or null where the page is drawn as usual
*/
private function addUserGroup(&$data, $user_id, $select_group,
$request_fields)
{
$_REQUEST['arg'] = 'edituser';
/* As with a role: the box a group is typed into is only drawn
while the list of the account's groups is open. */
$_REQUEST['visible_groups'] = 'true';
$parent = $this->parent;
$group_model = $parent->model("group");
if ( $user_id <= 0 ) {
$_REQUEST['arg'] = 'adduser';
return $parent->redirectWithMessage(
tl('accountaccess_component_username_doesnt_exists'),
array_merge(['arg'], $request_fields));
} else if (($group_id = $group_model->getGroupId(
$select_group)) <= 0) {
return $parent->redirectWithMessage(
tl('accountaccess_component_groupname_doesnt_exists'
), array_merge(['arg', 'user_name'],
$request_fields));
} else if ($group_model->checkUserGroup($user_id,
$group_id)){
return $parent->redirectWithMessage(
tl('accountaccess_component_groupname_already_added'
), array_merge(['arg', 'user_name'],
$request_fields));
} else {
$group_model->addUserGroup($user_id,
$group_id);
return $parent->redirectWithMessage(
tl('accountaccess_component_groupname_added'),
array_merge(['arg', 'user_name'],
$request_fields));
}
}
/**
* deleteUserRole takes a role away from an account.
*
* manageUsers calls it for the deleteuserrole argument. The root
* account keeps its own role, since an account that could take it
* away could lock everybody out.
*
* @param array &$data what the page will show, added to here
* @param int $user_id which account is losing the role
* @param array $request_fields fields a redirect carries so the reader
* comes back to the row they were on
* @return mixed what is handed back to the browser,
* or null where the page is drawn as usual
*/
private function deleteUserRole(&$data, $user_id, $request_fields)
{
$parent = $this->parent;
$role_model = $parent->model("role");
$_REQUEST['arg'] = 'edituser';
if ($user_id <= 0) {
$_REQUEST['arg'] = 'adduser';
return $parent->redirectWithMessage(
tl('accountaccess_component_username_doesnt_exists'),
array_merge(['arg'],
$request_fields));
}
$deleted = 0;
if (!empty($_REQUEST['role_ids'])) {
$role_ids = $_REQUEST['role_ids'];
$ids = explode("*", $role_ids);
foreach ($ids as $role_id) {
$role_id = (!empty($role_id)) ?
$parent->clean($role_id, 'int'): 0;
if ($role_model->checkUserRole($user_id,
$role_id)) {
$role_model->deleteUserRole(
$user_id, $role_id);
$deleted++;
}
}
}
if ($deleted == 1) {
$this->getUserRolesData($data, $user_id);
return $parent->redirectWithMessage(
tl('accountaccess_component_rolename_deleted'
), array_merge(['arg', 'user_name'],
$request_fields));
} else if ($deleted > 1) {
$this->getUserRolesData($data, $user_id);
return $parent->redirectWithMessage(
tl('accountaccess_component_rolenames_deleted'
), array_merge(['arg', 'user_name'],
$request_fields));
}
return $parent->redirectWithMessage(
tl('accountaccess_component_rolename_doesnt_exists'
), array_merge(['arg', 'user_name'],
$request_fields));
}
/**
* deleteUserGroup takes an account out of a group.
*
* manageUsers calls it for the deleteusergroup argument. An account
* that owns a group cannot be taken out of it, since a group with no
* owner has nobody to set it up.
*
* @param array &$data what the page will show, added to here
* @param int $user_id which account is leaving
* @param array $request_fields fields a redirect carries so the reader
* comes back to the row they were on
* @return mixed what is handed back to the browser,
* or null where the page is drawn as usual
*/
private function deleteUserGroup(&$data, $user_id, $request_fields)
{
$parent = $this->parent;
$group_model = $parent->model("group");
$_REQUEST['arg'] = 'edituser';
if (!empty($_REQUEST['context'])) {
$_REQUEST['c'] = 'search';
$request_fields[] = 'arg';
}
if ($user_id <= 0) {
$_REQUEST['arg'] = 'adduser';
return $parent->redirectWithMessage(
tl('accountaccess_component_username_doesnt_exists'),
$request_fields);
}
$deleted = 0;
if (!empty($_REQUEST['group_ids'])) {
$group_ids = $_REQUEST['group_ids'];
$ids = explode("*", $group_ids);
foreach ($ids as $group_id) {
$group_id = (!empty($group_id)) ?
$parent->clean($group_id, 'int'): 0;
if ($group_model->checkUserGroup($user_id,
$group_id)) {
$group_model->deleteUserGroup(
$user_id, $group_id);
$deleted++;
}
}
}
if ($deleted == 1) {
$this->getUserGroupsData($data, $user_id);
return $parent->redirectWithMessage(
tl('accountaccess_component_group_deleted'
), array_merge(['arg', 'user_name'],
$request_fields));
} else if ($deleted > 1) {
$this->getUserGroupsData($data, $user_id);
return $parent->redirectWithMessage(
tl('accountaccess_component_groups_deleted'
), array_merge(['arg', 'user_name'],
$request_fields));
}
return $parent->redirectWithMessage(
tl('accountaccess_component_groupname_doesnt_exists'
), array_merge(['arg', 'user_name'],
$request_fields));
}
/**
* searchUsers reads what a reader is looking for among the accounts.
*
* manageUsers calls it for the search argument. What it works out is
* handed back rather than written into the page, since the listing
* further down is what uses it.
*
* @param array &$data what the page will show, added to here
* @param bool &$default_search set true where the reader sent no new
* terms, so the listing falls back on the ones kept in the
* session
* @return array what the reader is looking for, as
* the listing further down reads it
*/
private function searchUsers(&$data, &$default_search)
{
$parent = $this->parent;
$data["FORM_TYPE"] = "search";
$search_array =
$parent->tableSearchRequestHandler($data,
"manageUsers",
['ALL_FIELDS' =>
['user', 'first', 'last', 'email', 'status'],
'EQUAL_COMPARISON_TYPES' => ['status']], "_name");
if (empty($_SESSION['LAST_SEARCH']['manageUsers_name']) ||
(!empty($_SESSION['LAST_SEARCH']['manageUsers_name']) &&
isset($_REQUEST['user_name'])) ) {
$_SESSION['LAST_SEARCH']['manageUsers_name'] =
$_SESSION['SEARCH']['manageUsers_name'];
unset($_SESSION['SEARCH']['manageUsers_name']);
} else {
$default_search = true;
}
return $search_array;
}
/**
* updateUserStatus changes whether an account is active, inactive,
* an editor or suspended.
*
* manageUsers calls it for the updatestatus argument. The root account
* cannot be made anything but active, since suspending it would lock
* everybody out.
*
* @param array &$data what the page will show, added to here
* @param string $username name of the account being changed
* @param int $user_id which account that is
* @param array $request_fields fields a redirect carries so the reader
* comes back to the row they were on
* @return mixed what is handed back to the browser,
* or null where the page is drawn as usual
*/
private function updateUserStatus(&$data, $username, $user_id,
$request_fields)
{
$parent = $this->parent;
$signin_model = $parent->model("signin");
$user_model = $parent->model("user");
if (!empty($_REQUEST['context'])) {
$_REQUEST['arg'] = 'search';
$request_fields[] = 'arg';
}
$user_id = $signin_model->getUserId($username);
if (!isset($data['STATUS_CODES'][$_REQUEST['userstatus']])||
$user_id == 1) {
return $parent->redirectWithMessage(
tl('accountaccess_component_username_doesnt_exists'
), $request_fields);
} else {
$user_model->updateUserStatus($user_id,
$_REQUEST['userstatus']);
return $parent->redirectWithMessage(
tl('accountaccess_component_userstatus_updated'),
$request_fields);
}
}
/**
* getUserRolesData looks up the roles an account holds, narrowed by
* what the request asks for: $_REQUEST['role_limit'],
* $_REQUEST['role_filter'],
* and $_REQUEST['role_sorts']. Information about these roles is added as
* fields to $data[NUM_USER_ROLES'] and $data['USER_ROLES']
*
* @param array &$data data for the manageUsers view.
* @param int $user_id user to look up roles for
*/
public function getUserRolesData(&$data, $user_id)
{
$parent = $this->parent;
$role_model = $parent->model("role");
$data['visible_roles'] = (isset($_REQUEST['visible_roles']) &&
$_REQUEST['visible_roles']=='true') ? 'true' : 'false';
if ($data['visible_roles'] == 'false') {
unset($_REQUEST['role_filter']);
unset($_REQUEST['role_limit']);
}
if (isset($_REQUEST['role_filter'])) {
$role_filter = substr($parent->clean(
$_REQUEST['role_filter'], 'string'), 0, C\NAME_LEN);
} else {
$role_filter = "";
}
$data['ROLE_FILTER'] = $role_filter;
$data['ROLE_SORTS'] = (empty($_REQUEST['role_sorts'])) ? [] :
json_decode(urldecode($_REQUEST['role_sorts']), true);
if ($data['ROLE_SORTS'] === null) {
$data['ROLE_SORTS'] = json_decode(html_entity_decode(
urldecode($_REQUEST['role_sorts'])), true);
$data['ROLE_SORTS'] = ($data['ROLE_SORTS']) ? $data['ROLE_SORTS'] :
[];
}
$data['NUM_USER_ROLES'] =
$role_model->countUserRoles($user_id, $role_filter);
if (isset($_REQUEST['role_limit'])) {
$role_limit = min($parent->clean(
$_REQUEST['role_limit'], 'int'),
$data['NUM_USER_ROLES']);
if ($role_limit == $data['NUM_USER_ROLES']) {
$role_limit = $data['NUM_USER_ROLES']- C\NUM_RESULTS_PER_PAGE;
}
$role_limit = max($role_limit, 0);
} else {
$role_limit = 0;
}
$data['ROLE_LIMIT'] = $role_limit;
$data['USER_ROLES'] =
$role_model->getUserRoles($user_id, $role_filter,
$data['ROLE_SORTS'], $role_limit);
$data['SCRIPT'] .= "listenAll('input.role-id', 'click',".
" function(event){ updateCheckedIds(role_ids, event.target); });";
}
/**
* getUserGroupsData looks up the groups an account belongs to,
* narrowed by what the request asks for: $_REQUEST['group_limit'],
* $_REQUEST['group_filter'], and $_REQUEST['group_sorts']. Information
* about these roles is added as
* fields to $data[NUM_USER_GROUPS'] and $data['USER_GROUPS']
*
* @param array &$data data for the manageUsers view.
* @param int $user_id user to look up roles for
*/
public function getUserGroupsData(&$data, $user_id)
{
$parent = $this->parent;
$group_model = $parent->model("group");
$data['visible_groups'] = (isset($_REQUEST['visible_groups']) &&
$_REQUEST['visible_groups']=='true') ? 'true' : 'false';
if ($data['visible_groups'] == 'false') {
unset($_REQUEST['group_filter']);
unset($_REQUEST['group_limit']);
}
if (isset($_REQUEST['group_filter'])) {
$group_filter = substr($parent->clean(
$_REQUEST['group_filter'], 'string'), 0, C\SHORT_TITLE_LEN);
} else {
$group_filter = "";
}
$data['GROUP_FILTER'] = $group_filter;
$data['GROUP_SORTS'] = (empty($_REQUEST['group_sorts'])) ? [] :
json_decode(urldecode($_REQUEST['group_sorts']), true);
if ($data['GROUP_SORTS'] === null) {
$data['GROUP_SORTS'] = json_decode(html_entity_decode(
urldecode($_REQUEST['group_sorts'])), true);
$data['GROUP_SORTS'] = ($data['GROUP_SORTS']) ?
$data['GROUP_SORTS'] : [];
}
$data['NUM_USER_GROUPS'] =
$group_model->countUserGroups($user_id, $group_filter);
if (isset($_REQUEST['group_limit'])) {
$group_limit = min($parent->clean(
$_REQUEST['group_limit'], 'int'),
$data['NUM_USER_GROUPS']);
$group_limit = max($group_limit, 0);
} else {
$group_limit = 0;
}
$data['GROUP_LIMIT'] = $group_limit;
$data['USER_GROUPS'] =
$group_model->getUserGroups($user_id, $group_filter,
$data['GROUP_SORTS'], $group_limit);
$data['SCRIPT'] .= "listenAll('input.group-id', 'click',".
" function(event){ updateCheckedIds(group_ids, event.target); });";
}
/**
* manageRoles draws the roles screen and carries out what a form on
* it asked for.
*
* This activity allows new roles to be added, old roles to be
* deleted and allows activities to be added to/deleted from a role
*
* @return array $data information about roles in the system, activities,
* etc. as well as status messages on performing a given sub activity
*/
public function manageRoles()
{
$parent = $this->parent;
$role_model = $parent->model("role");
$possible_arguments = ["addactivity", "addmodifier", "addrole",
"deleteactivity", "deletemodifier", "deleterole", "editrole",
"search"];
$data["ELEMENT"] = "manageroles";
$data['SCRIPT'] = "";
$data['FORM_TYPE'] = "addrole";
$search_array = [];
$data['CURRENT_ROLE'] = ["name" => ""];
$data['PAGING'] = "";
$modifier_activity_id = -1;
if (isset($_REQUEST['arg']) && $_REQUEST['arg'] == 'editrole') {
if (isset($_REQUEST['selectactivity']) &&
$_REQUEST['selectactivity'] >= 0) {
$_REQUEST['arg'] = "addactivity";
}
/* Every activity row carries a modifier chooser, so a send
brings one entry per row and all but the chosen one read
as nothing picked. The chosen row is the one that is not
that, which is what is looked for here. Refusing the whole
send where any row read as nothing picked meant a modifier
was never added at all, and taking the first row's entry
named the wrong activity. */
if (!empty($_REQUEST['selectmodifier']) &&
is_array($_REQUEST['selectmodifier'])) {
foreach ($_REQUEST['selectmodifier'] as $activity_id =>
$chosen) {
if ($chosen !== "-1" && $chosen !== -1 &&
$chosen !== "") {
$modifier_activity_id = $activity_id;
break;
}
}
if ($modifier_activity_id > 0) {
$_REQUEST['arg'] = "addmodifier";
}
}
}
if (isset($_REQUEST['name'])) {
$name = substr($parent->clean($_REQUEST['name'], "string"),
0, C\NAME_LEN);
$data['CURRENT_ROLE']['name'] = $name;
} else {
$name = "";
}
if ($name != "") {
$role_id = $role_model->getRoleId($name);
$data['ROLE_ACTIVITIES'] =
$role_model->getRoleActivities($role_id);
$all_activities = $parent->model("activity")->getActivityList();
$activity_ids = [];
$activity_names = [];
$data['AVAILABLE_MODIFIERS'] = [];
foreach ($all_activities as $activity) {
$activity_ids[] = $activity['ACTIVITY_ID'];
$activity_names[$activity['ACTIVITY_ID']] =
$activity['ACTIVITY_NAME'];
$data['AVAILABLE_MODIFIERS'][$activity['ACTIVITY_ID']] =
$parent->getAccessModifiers($activity['METHOD_NAME']);
}
$available_activities = [];
$role_activity_ids = [];
$activities_by_id = [];
foreach ($data['ROLE_ACTIVITIES'] as $activity) {
$role_activity_ids[] = $activity["ACTIVITY_ID"];
$activities_by_id[$activity["ACTIVITY_ID"]] = $activity;
}
$tmp = [];
foreach ($all_activities as $activity) {
if (!in_array($activity["ACTIVITY_ID"], $role_activity_ids) &&
!isset($tmp[$activity["ACTIVITY_ID"]])) {
$tmp[$activity["ACTIVITY_ID"]] = true;
$available_activities[] = $activity;
}
}
$data['AVAILABLE_ACTIVITIES'][-1] =
tl('accountaccess_component_add_roleactivity');
foreach ($available_activities as $activity) {
$data['AVAILABLE_ACTIVITIES'][$activity['ACTIVITY_ID']] =
$activity['ACTIVITY_NAME'];
}
if (isset($_REQUEST['selectactivity'])) {
$select_activity =
$parent->clean($_REQUEST['selectactivity'], "int" );
} else {
$select_activity = "";
}
if ($select_activity != "") {
$data['SELECT_ACTIVITY'] = $select_activity;
} else {
$data['SELECT_ACTIVITY'] = -1;
}
}
if (isset($_REQUEST['arg']) &&
in_array($_REQUEST['arg'], $possible_arguments)) {
switch ($_REQUEST['arg']) {
case "addactivity":
$_REQUEST['arg'] = "editrole";
if (($role_id = $role_model->getRoleId($name)) <= 0) {
return $parent->redirectWithMessage(
tl('accountaccess_component_rolename_doesnt_exists'
), ["arg", "start_row", "end_row", "num_show",
"name", "context"]);
} else if (!in_array($select_activity, $activity_ids)) {
return $parent->redirectWithMessage(
tl(
'accountaccess_component_activityname_doesnt_exists'
), ["arg", "start_row", "end_row", "num_show",
"name", "context"]);
} else {
$role_model->addActivityRole(
$role_id, $select_activity);
unset($data['AVAILABLE_ACTIVITIES'][$select_activity]);
$data['ROLE_ACTIVITIES'] =
$role_model->getRoleActivities($role_id);
return $parent->redirectWithMessage(
tl('accountaccess_component_activity_added'),
["arg", "start_row", "end_row", "num_show", "name",
"context"]);
}
break;
case "addmodifier":
$_REQUEST['arg'] = "editrole";
$modifier =
$_REQUEST['selectmodifier'][$modifier_activity_id] ??
"none";
$available_modifiers = $data['AVAILABLE_MODIFIERS'][
$modifier_activity_id] ?? [];
if (($role_id = $role_model->getRoleId($name)) <= 0) {
return $parent->redirectWithMessage(
tl('accountaccess_component_rolename_doesnt_exists'
), ["arg", "start_row", "end_row", "num_show",
"name", "context"]);
} else if (!in_array($modifier_activity_id,
$activity_ids)) {
return $parent->redirectWithMessage(
tl(
'accountaccess_component_activityname_doesnt_exists'
), ["arg", "start_row", "end_row", "num_show",
"name", "context"]);
} else if (!isset($available_modifiers[$modifier])) {
return $parent->redirectWithMessage(
tl(
'accountaccess_component_modifier_doesnt_exists'
), ["arg", "start_row", "end_row", "num_show",
"name", "context"]);
} else {
$activity =
$activities_by_id[$modifier_activity_id];
$allowed_arguments = $activity["ALLOWED_ARGUMENTS"];
if ($modifier == "all") {
$allowed_arguments = "all";
} else {
if ($allowed_arguments == "all") {
$allowed_arguments = "";
}
$comma = (trim($allowed_arguments)) ?
"," : "";
if (!str_contains($allowed_arguments, $modifier)) {
$allowed_arguments .= $comma . $modifier;
} else {
return $parent->redirectWithMessage(
tl('accountaccess_component_modifier_exists'
), ["arg", "start_row", "end_row",
"num_show","name", "context"]);
}
}
$role_model->updateActivityRoleArguments($role_id,
$modifier_activity_id,
$allowed_arguments);
return $parent->redirectWithMessage(
tl('accountaccess_component_modifier_added'
), ["arg", "start_row", "end_row", "num_show",
"name", "context"]);
}
break;
case "addrole":
$name = trim($name);
if ($name != "" && $role_model->getRoleId($name) > 0) {
$data['SCRIPT'] .= "doMessage('<h1 class=\"red\" >".
tl('accountaccess_component_rolename_exists').
"</h1>');";
} else if ($name != "") {
$role_model->addRole($name);
$data['CURRENT_ROLE']['name'] = "";
$data['SCRIPT'] .= "doMessage('<h1 class=\"red\" >".
tl('accountaccess_component_rolename_added').
"</h1>');";
} else {
$data['SCRIPT'] .= "doMessage('<h1 class=\"red\" >".
tl('accountaccess_component_rolename_blank').
"</h1>');";
}
$data['CURRENT_ROLE']['name'] = "";
break;
case "deleteactivity":
$_REQUEST['arg'] = "editrole";
if (($role_id = $role_model->getRoleId($name)) <= 0) {
return $parent->redirectWithMessage(
tl('accountaccess_component_rolename_doesnt_exists'
), ["arg", "start_row", "end_row", "num_show",
"name", "context"]);
} else if (!in_array($select_activity, $activity_ids)) {
return $parent->redirectWithMessage(
tl(
'accountaccess_component_activityname_doesnt_exists'
), ["arg", "start_row", "end_row", "num_show",
"name", "context"]);
} else {
$role_model->deleteActivityRole(
$role_id, $select_activity);
$data['ROLE_ACTIVITIES'] =
$role_model->getRoleActivities($role_id);
$data['AVAILABLE_ACTIVITIES'][$select_activity] =
$activity_names[$select_activity];
$data['SELECT_ACTIVITY'] = -1;
return $parent->redirectWithMessage(
tl('accountaccess_component_activity_deleted'),
["arg", "start_row", "end_row", "num_show",
"name", "context"]);
}
break;
case "deletemodifier":
$_REQUEST['arg'] = "editrole";
$modifier = $parent->clean($_REQUEST['modifier'], "string")
?? "none";
$available_modifiers = $data['AVAILABLE_MODIFIERS'][
$select_activity] ?? [];
if (!isset($available_modifiers[$modifier])) {
return $parent->redirectWithMessage(
tl('accountaccess_component_invalid_modifier'
), ["arg", "start_row", "end_row", "num_show",
"name", "context"]);
} else if (($role_id = $role_model->getRoleId($name)) <= 0){
return $parent->redirectWithMessage(
tl('accountaccess_component_rolename_doesnt_exists'
), ["arg", "start_row", "end_row", "num_show",
"name", "context"]);
} else if (!in_array($select_activity, $activity_ids)) {
return $parent->redirectWithMessage(
tl(
'accountaccess_component_activityname_doesnt_exists'
), ["arg", "start_row", "end_row", "num_show",
"name", "context"]);
} else {
$activity =
$activities_by_id[$select_activity];
$allowed_arguments = $activity["ALLOWED_ARGUMENTS"];
if (!str_contains($allowed_arguments, $modifier)) {
return $parent->redirectWithMessage(
tl('accountaccess_component_mod_doesnt_exists'
), ["arg", "start_row", "end_row",
"num_show","name", "context"]);
}
$allowed_arguments = preg_replace(
"/".preg_quote($modifier)."\s*\,?\s*/", "",
$allowed_arguments);
if (empty($allowed_arguments)) {
$allowed_arguments = "all";
}
$role_model->updateActivityRoleArguments($role_id,
$select_activity,
$allowed_arguments);
return $parent->redirectWithMessage(
tl('accountaccess_component_modifier_deleted'
), ["arg", "start_row", "end_row",
"num_show","name", "context"]);
}
break;
case "deleterole":
$preserve = [];
if (!empty($_REQUEST['context'])) {
$_REQUEST['arg'] = 'search';
$preserve[] = 'arg';
}
if (($role_id = $role_model->getRoleId($name)) <= 0) {
return $parent->redirectWithMessage(
tl('accountaccess_component_rolename_doesnt_exists'
),array_merge($preserve, ["start_row", "end_row",
"num_show"]));
} else {
$role_model->deleteRole($role_id);
return $parent->redirectWithMessage(
tl('accountaccess_component_rolename_deleted'),
array_merge($preserve, ["start_row", "end_row",
"num_show"]));
}
break;
case "editrole":
$data['FORM_TYPE'] = "editrole";
$role = false;
if ($name) {
$role = $role_model->getRole($name);
}
if ($role === false) {
$data['FORM_TYPE'] = "addrole";
break;
}
if (!empty($_REQUEST['context'])) {
$data['context'] = 'search';
}
$num_activities = count($data['ROLE_ACTIVITIES']);
$update_activities = $_REQUEST['activities'] ?? [];
if (!$update_activities) {
break;
}
$update = false;
for ($i = 0; $i < $num_activities; $i++) {
$role_activity = $data['ROLE_ACTIVITIES'][$i];
$activity_id = $role_activity['ACTIVITY_ID'];
if (isset($update_activities[$activity_id]) &&
$update_activities[$activity_id] !=
$role_activity['ALLOWED_ARGUMENTS']) {
$role_model->updateActivityRoleArguments($role_id,
$activity_id, $update_activities[$activity_id]);
$update = true;
}
}
if ($update) {
return $parent->redirectWithMessage(
tl('accountaccess_component_role_updated'),
["arg", "name", "start_row", "end_row", "num_show",
"context"]);
}
break;
case "search":
$search_array = $parent->tableSearchRequestHandler($data,
"manageRoles", ['ALL_FIELDS' => ['name']]);
if (empty($_SESSION['LAST_SEARCH']['manageRoles']) ||
isset($_REQUEST['name'])) {
$_SESSION['LAST_SEARCH']['manageRoles'] =
$_SESSION['SEARCH']['manageRoles'];
unset($_SESSION['SEARCH']['manageRoles']);
} else {
$default_search = true;
}
break;
}
}
if ($search_array == [] || !empty($default_search)) {
if (!empty($_SESSION['LAST_SEARCH']['manageRoles'])) {
if (!empty($_REQUEST['arg']) && $_REQUEST['arg'] == 'search') {
$search_array =
$parent->restoreLastSearchFromSession($data,
'manageRoles');
} else if (!empty($_REQUEST['context'])) {
$search_array =
$_SESSION['LAST_SEARCH']['manageRoles']['SEARCH_ARRAY'];
$data['PAGING'] =
$_SESSION['LAST_SEARCH']['manageRoles']['PAGING'];
}
}
if ($search_array == []) {
$search_array[] = ["name", "", "", "ASC"];
}
}
if ($data['FORM_TYPE'] == 'addrole') {
$data['SCRIPT'] .= "setDisplay('admin-form-row', false);";
}
$parent->pagingLogic($data, $role_model, "ROLES",
C\DEFAULT_ADMIN_PAGING_NUM, $search_array, "");
return $data;
}
}