Files
resourcespace/include/user_functions.php
2025-07-18 16:20:14 +07:00

3716 lines
146 KiB
PHP

<?php
# User functions
# Functions to create, edit and generally deal with user accounts
include_once __DIR__ . '/login_functions.php';
/**
* Validate user - check we have a valid user based on SQL criteria e.g. session that is passed in as $user_select_sql
* Will always return false if matches criteria but the user account is not approved or has expired
*
* $user_select_sql example u.session=$variable.
* Joins to usergroup table as g which can be used in criteria
*
* @param object $user_select_sql PreparedStatementQuery instance - to validate user usually session hash or key
* @param boolean $getuserdata default true. Return user data as required by authenticate.php
*
* @return boolean|array
*/
function validate_user($user_select_sql, $getuserdata = true)
{
if (!is_a($user_select_sql, 'PreparedStatementQuery')) {
return false;
}
$validatesql = $user_select_sql->sql;
$validateparams = $user_select_sql->parameters;
$full_user_select_sql = "
approved = 1
AND (
account_expires IS NULL
OR account_expires = '0000-00-00 00:00:00'
OR account_expires > now()
) "
. ((strtoupper(trim(substr($validatesql, 0, 4))) == 'AND') ? ' ' : ' AND ')
. $validatesql;
if ($getuserdata) {
return ps_query(
" SELECT u.ref,
u.username,
u.origin,
if(find_in_set('permissions',g.inherit_flags) AND pg.permissions IS NOT NULL,pg.permissions,g.permissions) permissions,
g.parent,
u.usergroup,
u.current_collection,
(select count(*) from collection where ref=u.current_collection) as current_collection_valid,
u.last_active,
timestampdiff(second, u.last_active, now()) AS idle_seconds,
u.email,
u.email_rate_limit_active,
u.password,
u.fullname,
g.search_filter,
g.edit_filter,
g.ip_restrict ip_restrict_group,
g.name groupname,
u.ip_restrict ip_restrict_user,
u.search_filter_override,
u.search_filter_o_id,
g.resource_defaults,
u.password_last_change,
if(find_in_set('config_options',g.inherit_flags) AND pg.config_options IS NOT NULL,pg.config_options,g.config_options) config_options,
g.request_mode,
g.derestrict_filter,
u.hidden_collections,
u.accepted_terms,
u.session,
g.search_filter_id,
g.download_limit,
g.download_log_days,
g.edit_filter_id,
g.derestrict_filter_id,
u.processing_messages processing_messages
FROM user AS u
LEFT JOIN usergroup AS g on u.usergroup = g.ref
LEFT JOIN usergroup AS pg ON g.parent=pg.ref
WHERE {$full_user_select_sql}",
$validateparams
);
} else {
$validuser = ps_value(
" SELECT u.ref AS `value`
FROM user AS u
LEFT JOIN usergroup g ON u.usergroup = g.ref
WHERE {$full_user_select_sql}",
$validateparams,
''
);
if ('' != $validuser) {
debug("[validate_user()] User #{$validuser} is valid!");
return true;
}
}
return false;
}
/**
*
* Given an array of user data loaded from the user table, set up all necessary global variables for this user
* including permissions, current collection, config overrides and so on.
*
* @param array $userdata Array of user data obtained by validate_user() from user/usergroup tables
*
* @return boolean success/failure flag - used for example to prevent certain users from making API calls
*/
function setup_user(array $userdata)
{
global $userpermissions, $usergroup, $usergroupname, $usergroupparent, $useremail, $useremail_rate_limit_active, $userpassword, $userfullname,
$ip_restrict_group, $ip_restrict_user, $rs_session, $global_permissions, $userref, $username, $useracceptedterms,
$anonymous_user_session_collection, $global_permissions_mask, $user_preferences, $userrequestmode,
$usersearchfilter, $usereditfilter, $userderestrictfilter, $hidden_collections, $userresourcedefaults,
$userrequestmode, $request_adds_to_collection, $usercollection, $lang, $validcollection,
$userorigin, $actions_enable, $actions_permissions, $actions_on, $usersession, $anonymous_login, $resource_created_by_filter,
$user_dl_limit,$user_dl_days, $USER_SELECTION_COLLECTION, $plugins, $userprocessing_messages;
# Hook to modify user permissions
if (hook("userpermissions")) {
$userdata["permissions"] = hook("userpermissions");
}
$userref = $userdata['ref'];
$username = $userdata['username'];
$useracceptedterms = $userdata['accepted_terms'];
# Create userpermissions array for checkperm() function
$userpermissions = array_diff(
array_merge(
explode(",", trim($global_permissions ?? "")),
explode(",", trim($userdata["permissions"] ?? ""))
),
explode(",", trim($global_permissions_mask ?? ""))
);
$userpermissions = array_values($userpermissions);# Resequence array as the above array_diff() causes out of step keys.
$actions_on = $actions_enable;
# Enable actions functionality if based on user permissions
if (!$actions_enable && count($actions_permissions) > 0) {
foreach ($actions_permissions as $actions_permission) {
if (in_array($actions_permission, $userpermissions)) {
$actions_on = true;
break;
}
}
}
$usergroup = $userdata["usergroup"];
$usergroupname = $userdata["groupname"];
$usergroupparent = $userdata["parent"];
$useremail = $userdata["email"];
$userpassword = $userdata["password"];
$userfullname = $userdata["fullname"];
$userorigin = $userdata["origin"];
$usersession = $userdata["session"];
$userprocessing_messages = $userdata["processing_messages"];
if (isset($userdata["email_rate_limit_active"])) {
$useremail_rate_limit_active = $userdata["email_rate_limit_active"];
}
$ip_restrict_group = trim((string) $userdata["ip_restrict_group"]);
$ip_restrict_user = trim((string) $userdata["ip_restrict_user"]);
if (isset($anonymous_login) && $username == $anonymous_login && isset($rs_session) && !checkperm('b')) { // This is only required if anonymous user has collection functionality
if ($anonymous_user_session_collection) {
// Get all the collections that relate to this session
$sessioncollections = get_session_collections($rs_session, $userref, true);
// Just get the first one if more
$usercollection = $sessioncollections[0];
} else {
// Unlikely scenario, but maybe we do allow anonymous users to change the selected collection for all other anonymous users
$usercollection = $userdata["current_collection"];
}
} else {
$usercollection = $userdata["current_collection"];
// Check collection actually exists
$validcollection = $userdata["current_collection_valid"];
if ($validcollection == 0 || $usercollection == 0 || !is_numeric($usercollection)) {
// Not a valid collection - switch to user's primary collection if there is one
$usercollection = get_default_user_collection(true);
}
}
$USER_SELECTION_COLLECTION = get_user_selection_collection($userref);
if (is_null($USER_SELECTION_COLLECTION) && !(isset($anonymous_login) && $username == $anonymous_login)) {
// Don't create a new collection on every anonymous page load, it will be created when an action is performed
$USER_SELECTION_COLLECTION = create_collection($userref, "Selection Collection (for batch edit)", 0, 1);
update_collection_type($USER_SELECTION_COLLECTION, COLLECTION_TYPE_SELECTION, false);
clear_query_cache('user_selection_collection' . $userref);
}
$newfilter = false;
if (isset($userdata["search_filter_o_id"]) && is_numeric($userdata["search_filter_o_id"]) && $userdata['search_filter_o_id'] > 0) {
// User search filter override
$usersearchfilter = $userdata["search_filter_o_id"];
$newfilter = true;
} elseif (isset($userdata["search_filter_id"]) && is_numeric($userdata["search_filter_id"]) && $userdata['search_filter_id'] > 0) {
// Group search filter
$usersearchfilter = $userdata["search_filter_id"];
$newfilter = true;
}
if (!$newfilter) {
// Old style search filter that hasn't been migrated
$usersearchfilter = isset($userdata["search_filter_override"]) && $userdata["search_filter_override"] != '' ? $userdata["search_filter_override"] : $userdata["search_filter"];
}
$usereditfilter = (isset($userdata["edit_filter_id"]) && is_numeric($userdata["edit_filter_id"]) && $userdata['edit_filter_id'] > 0) ? $userdata['edit_filter_id'] : $userdata["edit_filter"];
$userderestrictfilter = (isset($userdata["derestrict_filter_id"]) && is_numeric($userdata["derestrict_filter_id"]) && $userdata['derestrict_filter_id'] > 0) ? $userdata['derestrict_filter_id'] : $userdata["derestrict_filter"];
$hidden_collections = explode(",", (string) $userdata["hidden_collections"]);
$userresourcedefaults = $userdata["resource_defaults"];
$userrequestmode = trim((string) $userdata["request_mode"]);
$user_dl_limit = trim((string) $userdata["download_limit"]);
$user_dl_days = trim((string) $userdata["download_log_days"]);
if (
(int)$user_dl_limit > 0
&& defined("API_CALL") // API cannot be used by these users as would open up opportunities to bypass limits
) {
return false;
}
# Apply config override options
$config_options = trim((string) $userdata["config_options"]);
override_rs_variables_by_eval($GLOBALS, $config_options, 'usergroup');
// Set default workflow states to show actions for, if not manually set by user
get_config_option(['user' => $userref, 'usergroup' => $usergroup], 'actions_notify_states', $user_actions_notify_states, false);
// Check if user has already explicitly asked not to see these
get_config_option(['user' => $userref, 'usergroup' => $usergroup], 'actions_resource_review', $legacy_resource_review, true); // Deprecated option
if ($user_actions_notify_states === false && $legacy_resource_review) {
$default_notify_states = get_default_notify_states();
$GLOBALS['actions_notify_states'] = implode(",", $default_notify_states);
} elseif ($legacy_resource_review) {
$GLOBALS['actions_notify_states'] = $user_actions_notify_states;
}
$plugins = register_group_access_plugins($usergroup, $plugins);
hook('after_setup_user');
return true;
}
/**
* Returns a user list. Group or search term is optional. The standard user group names are translated using $lang. Custom user group names are i18n translated.
*
* @param integer $group Can be a single group, or a comma separated list of groups used to limit the results
* If blank, zero or NULL then all users will be returned irrespective of their group
* @param string $find Search string to filter returned results
* @param string $order_by
* @param boolean $usepermissions
* @param integer $fetchrows
* @param string $approvalstate
* @param boolean $returnsql Return prepared statement object containing sql query and parameters.
* @param string $selectcolumns
* @param boolean $exact_username_match Denotes $find must be an exact username
*
* @return array|object Matching user records Returns an array of user information or prepared statement object containing sql query and parameters.
*/
function get_users($group = 0, $find = "", $order_by = "u.username", $usepermissions = false, $fetchrows = -1, $approvalstate = "", $returnsql = false, $selectcolumns = "", $exact_username_match = false)
{
global $usergroup, $usergroupparent;
$order_by_parts = explode(" ", ($order_by ?? ""));
$order_by = $order_by_parts[0] ?? "u.username";
$sort = strtoupper(($order_by_parts[1] ?? "ASC")) == "DESC" ? "DESC" : "ASC";
if (!in_array($order_by, array("u.created", "u.username", "approved", "u.fullname", 'g.name', 'email', 'created', 'last_active'))) {
$order_by = "u.username";
}
$sql = "";
$sql_params = array();
$find = strtolower($find);
# Sanitise the incoming group(s), stripping out any which are non-numeric
$grouparray = array_filter(explode(",", $group), 'ctype_digit');
if ($group != 0 && count($grouparray) > 0) {
$sql = "where usergroup IN (" . ps_param_insert(count($grouparray)) . ")";
$sql_params = ps_param_fill($grouparray, "i");
}
if ($exact_username_match) {
# $find is an exact username
if ($sql == "") {
$sql = "where ";
} else {
$sql .= " and ";
}
$sql .= "LOWER(username) = ?";
$sql_params = array_merge($sql_params, array("s", $find));
} else {
if (strlen($find) > 1) {
if ($sql == "") {
$sql = "where ";
} else {
$sql .= " and ";
}
$sql .= "(LOWER(username) like ? or LOWER(fullname) like ? or LOWER(email) like ? or LOWER(comments) like ?)";
$sql_params = array_merge($sql_params, array("s", "%" . $find . "%", "s", "%" . $find . "%", "s", "%" . $find . "%", "s", "%" . $find . "%"));
}
if (strlen($find) == 1) {
if ($sql == "") {
$sql = "where ";
} else {
$sql .= " and ";
}
$sql .= "LOWER(username) like ?";
$sql_params = array_merge($sql_params, array("s", $find . "%"));
}
}
$approver_groups = get_approver_usergroups($usergroup);
if ($usepermissions && checkperm('E')) {
# Return users in child, parent and own groups
if ($sql == "") {
$sql = "where ";
} else {
$sql .= " and ";
}
$parent_own_sql = " g.ref = ? or g.ref = ? or ";
$sql_params = array_merge($sql_params, ['i', $usergroup, 'i', $usergroupparent]);
if (count($approver_groups) > 0) {
$sql .= "(" . $parent_own_sql . "find_in_set(?, g.parent) or g.ref in (" . ps_param_insert(count($approver_groups)) . "))";
$sql_params = array_merge($sql_params, array("i", $usergroup), ps_param_fill($approver_groups, "i"));
} else {
$sql .= "(" . $parent_own_sql . "find_in_set(?, g.parent))";
$sql_params = array_merge($sql_params, array("i", $usergroup));
}
$sql_hook_return = hook("getuseradditionalsql");
if (is_a($sql_hook_return, 'PreparedStatementQuery')) {
$sql .= $sql_hook_return->sql;
$sql_params = array_merge($sql_params, $sql_hook_return->parameters);
}
}
if (is_numeric($approvalstate)) {
if ($sql == "") {
$sql = "where ";
} else {
$sql .= " and ";
}
$sql .= "u.approved = ?";
$sql_params = array_merge($sql_params, array("i", $approvalstate));
}
// Return users in both user's user group and children groups
if ($usepermissions && (checkperm('U') || count($approver_groups) > 0)) {
if (count($approver_groups) > 0) {
$sql .= sprintf('%1$s (g.ref = ? OR find_in_set(?, g.parent) OR g.ref IN (' . ps_param_insert(count($approver_groups)) . '))', ($sql == '') ? 'WHERE' : ' AND');
$sql_params = array_merge($sql_params, array("i", $usergroup, "i", $usergroup), ps_param_fill($approver_groups, "i"));
} else {
$sql .= sprintf('%1$s (g.ref = ? OR find_in_set(?, g.parent))', ($sql == '') ? 'WHERE' : ' AND');
$sql_params = array_merge($sql_params, array("i", $usergroup, "i", $usergroup));
}
}
if ($selectcolumns != "") {
$selectcolumns = explode(",", $selectcolumns);
$selectcolumns = array_map('trim', $selectcolumns);
$selectcolumns = array_unique(array_merge($selectcolumns, array('u.created', 'u.fullname', 'u.email', 'u.username', 'u.comments', 'u.ref', 'u.usergroup', 'u.approved')));
$select = implode(",", $selectcolumns);
} else {
$select = "u.*, g.name groupname, g.ref groupref, g.parent groupparent";
}
$query = "SELECT " . $select . " from user u left outer join usergroup g on u.usergroup = g.ref $sql order by $order_by $sort";
# Executes query.
if ($returnsql) {
$return_sql = new PreparedStatementQuery();
$return_sql->sql = $query;
$return_sql->parameters = $sql_params;
return $return_sql;
}
$r = ps_query($query, $sql_params, '', $fetchrows);
# Translates group names in the newly created array.
for ($n = 0; $n < count($r); $n++) {
if (strpos($select, "groupname") === false || !is_array($r[$n])) {
break;
} # The padded rows can't be and don't need to be translated.
$r[$n]["groupname"] = lang_or_i18n_get_translated($r[$n]["groupname"], "usergroup-");
}
return $r;
}
/**
* Returns all the users who have the permission $permission.
* The standard user group names are translated using $lang. Custom user group names are i18n translated.
*
* @param string $permission The permission code to search for
* @return array Matching user records
*/
function get_users_with_permission($permission)
{
# First find all matching groups.
$groups = ps_query("SELECT ref,permissions FROM usergroup");
$matched = array();
for ($n = 0; $n < count($groups); $n++) {
$perms = trim_array(explode(",", (string) $groups[$n]["permissions"]));
if (in_array($permission, $perms)) {
$matched[] = $groups[$n]["ref"];
}
}
if (count($matched) < 1) {
return array();
}
# Executes query.
$r = ps_query(
"SELECT u.*, g.name groupname, g.ref groupref, g.parent groupparent FROM user u
LEFT OUTER JOIN usergroup g ON u.usergroup = g.ref
WHERE (g.ref IN (" . ps_param_insert(count($matched)) . ") OR (find_in_set('permissions', g.inherit_flags) > 0
AND g.parent IN (" . ps_param_insert(count($matched)) . "))) ORDER BY username",
array_merge(ps_param_fill($matched, "i"), ps_param_fill($matched, "i"))
);
# Translates group names in the newly created array.
$return = array();
for ($n = 0; $n < count($r); $n++) {
$r[$n]["groupname"] = lang_or_i18n_get_translated($r[$n]["groupname"], "usergroup-");
$return[] = $r[$n]; # Adds to return array.
}
return $return;
}
/**
* Retrieve user records by e-mail address
*
* @param string $email The e-mail address to search for
* @return array Matching user records
*/
function get_user_by_email($email)
{
$r = ps_query("SELECT " . columns_in('user', 'u') . ", g.name groupname, g.ref groupref, g.parent groupparent FROM user u LEFT OUTER JOIN usergroup g ON u.usergroup = g.ref WHERE u.email LIKE ? ORDER BY username", array("s", "%" . $email . "%"));
# Translates group names in the newly created array.
$return = array();
for ($n = 0; $n < count($r); $n++) {
$r[$n]["groupname"] = lang_or_i18n_get_translated($r[$n]["groupname"], "usergroup-");
$return[] = $r[$n]; # Adds to return array.
}
return $return;
}
/**
* Retrieve user ID by username
*
* @param string $username The username to search for (will match email if not found)
* @return mixed The matching user ID or false if not found
*/
function get_user_by_username($username)
{
if (!is_string($username)) {
return false;
}
$params = ["s",$username];
$usermatch = ps_value("SELECT ref value FROM user WHERE username = ?", $params, 0);
if ($usermatch == 0 && filter_var($username, FILTER_VALIDATE_EMAIL)) {
// Check if trying to use email address
$emailmatches = ps_array("SELECT ref value FROM user WHERE email = ?", $params);
if (count($emailmatches) == 1) {
debug("Matched user to email address");
$usermatch = $emailmatches[0];
}
}
return $usermatch > 0 ? $usermatch : false;
}
/**
* Returns a list of user groups. The standard user groups are translated using $lang. Custom user groups are i18n translated.
* Puts anything starting with 'General Staff Users' - in the English default names - at the top (e.g. General Staff).
*
* @param boolean $usepermissions Use permissions (user access)
* @param string $find Search string
* @param boolean $id_name_pair_array Return an array of ID->name instead of full records
* @return array Matching user group records
*/
function get_usergroups($usepermissions = false, $find = '', $id_name_pair_array = false)
{
# Creates a query, taking (if required) the permissions into account.
global $usergroup;
$approver_groups = get_approver_usergroups($usergroup);
$sql = "";
$sql_params = array();
if ($usepermissions && (checkperm("U") || count($approver_groups) > 0)) {
# Only return users in children groups to the user's group
if ($sql == "") {
$sql = "where ";
} else {
$sql .= " and ";
}
if (count($approver_groups) > 0) {
$sql .= "(ref = ? or find_in_set(?, parent) or ref in (" . ps_param_insert(count($approver_groups)) . "))";
$sql_params = array_merge(array("i", $usergroup, "i", $usergroup), ps_param_fill($approver_groups, "i"));
} else {
$sql .= "(ref = ? or find_in_set(?, parent))";
$sql_params = array("i", $usergroup, "i", $usergroup);
}
}
# Executes query.
global $default_group;
$r = ps_query("select ref, `name`, permissions, parent, search_filter, edit_filter, derestrict_filter, ip_restrict, resource_defaults, config_options,
welcome_message, request_mode, allow_registration_selection, group_specific_logo, inherit_flags, search_filter_id, download_limit, download_log_days,
edit_filter_id, derestrict_filter_id, group_specific_logo_dark from usergroup $sql order by (ref = ?) desc, name", array_merge($sql_params, array("i", $default_group)));
# Translates group names in the newly created array.
$return = array();
for ($n = 0; $n < count($r); $n++) {
$r[$n]["name"] = lang_or_i18n_get_translated($r[$n]["name"], "usergroup-");
$return[] = $r[$n]; # Adds to return array.
}
if (strlen($find) > 0) {
# Searches for groups with names which contains the string defined in $find.
$initial_length = count($return);
for ($n = 0; $n < $initial_length; $n++) {
if (strpos(strtolower($return[$n]["name"]), strtolower($find)) === false) {
unset($return[$n]); # Removes this group.
}
}
$return = array_values($return); # Reassigns the indices.
}
// Return only an array with ref => name pairs
if ($id_name_pair_array) {
$return_id_name_array = array();
foreach ($return as $user_group) {
$return_id_name_array[$user_group['ref']] = $user_group['name'];
}
return $return_id_name_array;
}
return $return;
}
/**
* Returns the user group corresponding to the $ref. A standard user group name is translated using $lang. A custom user group name is i18n translated.
*
* @param integer $ref User group ID
* @return mixed False if not found, or the user group record if found.
*/
function get_usergroup($ref)
{
$return = ps_query("SELECT ref, name, permissions, parent, search_filter, search_filter_id, edit_filter, ip_restrict, resource_defaults, config_options, welcome_message,
request_mode, allow_registration_selection, derestrict_filter, group_specific_logo, inherit_flags, download_limit, download_log_days, edit_filter_id, derestrict_filter_id, group_specific_logo_dark "
. hook('get_usergroup_add_columns') . " FROM usergroup WHERE ref = ?", array("i", $ref), 'usergroup');
if (count($return) == 0) {
return false;
} else {
$return[0]["name"] = lang_or_i18n_get_translated($return[0]["name"], "usergroup-");
$return[0]["inherit"] = explode(",", trim($return[0]["inherit_flags"] ?? ""));
return $return[0];
}
}
/**
* Return the user group record matching $ref
*
* @param integer $ref
* @return array|bool
*/
function get_user($ref)
{
global $udata_cache;
if (!isset($udata_cache[$ref])) {
$user_columns = columns_in("user", "u");
$user_columns = str_replace("ip_restrict`", "ip_restrict` ip_restrict_user", $user_columns);
$udata_cache[$ref] = ps_query(
"SELECT
{$user_columns},
if(find_in_set('permissions',g.inherit_flags)>0
AND pg.permissions IS NOT NULL,
pg.permissions,
g.permissions) permissions,
g.parent,
g.search_filter,
g.edit_filter,
g.ip_restrict ip_restrict_group,
g.name groupname,
(select count(*) from collection where ref=u.current_collection) as current_collection_valid,
g.resource_defaults,
if(find_in_set('config_options',g.inherit_flags)>0
AND pg.config_options IS NOT NULL,
pg.config_options,
g.config_options) config_options,
g.request_mode,
g.derestrict_filter,
g.search_filter_id,
g.download_limit,
g.download_log_days,
g.edit_filter_id,
g.derestrict_filter_id
FROM user u
LEFT JOIN usergroup g ON u.usergroup = g.ref
LEFT JOIN usergroup pg ON g.parent = pg.ref
WHERE u.ref = ?",
array("i", $ref)
);
}
# Return a user's credentials.
if (count($udata_cache[$ref]) > 0) {
return $udata_cache[$ref][0];
} else {
return false;
}
}
/**
* Function used to update or delete a user.
* Note: data is taken from the submitted form
*
* @param string $ref ID of the user
*
* @return boolean|string True if successful or a descriptive string if there's an issue
*/
function save_user($ref)
{
global $lang, $home_dash;
$current_user_data = get_user($ref);
if (!$current_user_data) {
return $lang['accountdoesnotexist'];
}
// Save user details, data is taken from the submitted form.
if ('' != getval('deleteme', '')) {
delete_profile_image($ref);
ps_query("DELETE FROM user WHERE ref = ?", array("i", $ref));
hook('on_delete_user', "", array($ref));
include_once __DIR__ . "/dash_functions.php";
empty_user_dash($ref);
log_activity("{$current_user_data['username']} ({$ref})", LOG_CODE_DELETED, null, 'user', null, $ref);
return true;
} else {
// Get submitted values
$username = trim(getval('username', ''));
$password = trim(getval('password', ''));
$fullname = str_replace("\t", ' ', trim(getval('fullname', '')));
$email = trim(getval('email', ''));
$usergroup = trim(getval('usergroup', '', false, 'is_int_loose'));
$ip_restrict = trim(getval('ip_restrict', ''));
$search_filter_override = trim(getval('search_filter_override', ''));
$search_filter_o_id = trim(getval('search_filter_o_id', 0, true));
$comments = trim(getval('comments', ''));
$suggest = getval('suggest', '');
$emailresetlink = getval('emailresetlink', '');
$approved = getval('approved', 0, true);
$expires = getval('account_expires', '');
// Create SQL to check for username or e-mail address conflict
$conditions = "username = ? OR email = ?";
$params = ["s", $username, "s", $username];
// Add code to set different values depending on what has conflicted
$typecheck = "WHEN (username = ?) THEN 1 "; // username matches another username
$typecheck .= "WHEN (email = ?) THEN 2 "; // username matches another account's email
$typeparams = ["s", $username, "s", $username];
if ($email !== "") {
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
return $lang['error_invalid_email'];
}
// Add checks for email conflicts
$conditions .= " OR username = ? OR email = ?";
$params = array_merge($params, ["s", $email, "s", $email]);
$typecheck .= "WHEN (email = ?) THEN 3 "; // email matches another account's email
$typecheck .= "WHEN (username = ?) THEN 4 "; // email matches the username of another account
$typeparams = array_merge($typeparams, ["s", $email, "s", $email]);
}
$matchsql = " SELECT MIN(CASE $typecheck END) AS value
FROM user
WHERE ref <> ? AND ($conditions)";
$c = ps_value($matchsql, array_merge($params, ["i",$ref], $typeparams), 0);
if ($c > 0 && checkperm("U")) {
// Return an ambiguous message if delegated user admin
return $lang["useralreadyexists"];
}
switch ($c) {
case 1:
return $lang["useralreadyexists"]; // An account with that username already exists
break;
case 2:
return $lang["username_conflicts_email"]; // Username matches another account's e-mail
break;
case 3:
return $lang["useremailalreadyexists"]; // An account with that e-mail address exists
break;
case 4:
return $lang["email_conflicts_username"]; // Email matches another account's username
break;
case 0:
default:
// All ok
break;
}
// Enabling a disabled account but at the user limit?
if (user_limit_reached() && $current_user_data["approved"] != 1 && $approved == 1) {
return $lang["userlimitreached"]; // Return error message
}
// Password checks:
if ($suggest != '' || ($password == '' && $emailresetlink != '')) {
$password = make_password();
} elseif ($password != $lang['hidden']) {
$message = check_password($password);
if ($message !== true) {
return $message;
}
}
# Validate expiry date
if ($expires != "" && (preg_match("/^\d{4}-\d{2}-\d{2}$/", $expires) === 0 || strtotime($expires) === false)) {
return str_replace('[value]', $expires, $lang['error_invalid_date_format']);
}
if ($expires == "") {
$expires = null;
} else {
$expires = date("Y-m-d", strtotime($expires));
}
$passsql = '';
$sql_params = array();
if ($password != $lang['hidden']) {
# Save password.
if ($suggest == '') {
$password = rs_password_hash("RS{$username}{$password}");
}
$passsql = ", password = ?, password_last_change = now()";
$sql_params = array("s", $password);
}
// Full name checks
if ('' == $fullname && '' == $suggest) {
return $lang['setup-admin_fullname_error'];
}
/*Make sure IP restrict filter is a proper IP, otherwise make it blank
Note: we do this check only when wildcards are not used*/
if (false === strpos($ip_restrict, '*')) {
$ip_restrict = (false === filter_var($ip_restrict, FILTER_VALIDATE_IP) ? '' : $ip_restrict);
}
$additional_sql = '';
$additional_sql_params = array();
$sql_hook_return = hook('additionaluserfieldssave');
if (is_a($sql_hook_return, 'PreparedStatementQuery')) {
$additional_sql = $sql_hook_return->sql;
$additional_sql_params = $sql_hook_return->parameters;
}
log_activity(null, LOG_CODE_EDITED, $username, 'user', 'username', $ref);
log_activity(null, LOG_CODE_EDITED, $fullname, 'user', 'fullname', $ref);
log_activity(null, LOG_CODE_EDITED, $email, 'user', 'email', $ref);
if ((isset($current_user_data['usergroup']) && '' != $current_user_data['usergroup']) && $current_user_data['usergroup'] != $usergroup) {
if (can_set_admin_usergroup($usergroup) && can_set_admin_usergroup($current_user_data['usergroup'])) {
log_activity(null, LOG_CODE_EDITED, $usergroup, 'user', 'usergroup', $ref);
ps_query("DELETE FROM resource WHERE ref = -?", array("i", $ref));
} else {
# User cannot set $usergroup to one with "super admin" level permissions - "a". Make no changes.
# Check on current user group prevents permission de-escalation of "super admin" level user by user with less permissions.
global $userref;
debug("User $userref unable to change user group for user $ref from user group {$current_user_data['usergroup']} to user group $usergroup as this would involve granting or revoking the 'a' permission which they do not them self have.");
$usergroup = $current_user_data['usergroup'];
}
}
if ($email != $current_user_data["email"]) {
$additional_sql .= ",email_invalid=0 ";
}
log_activity(null, LOG_CODE_EDITED, $ip_restrict, 'user', 'ip_restrict', $ref, null, '');
log_activity(null, LOG_CODE_EDITED, $search_filter_override, 'user', 'search_filter_override', $ref, null, '');
log_activity(null, LOG_CODE_EDITED, $expires, 'user', 'account_expires', $ref);
log_activity(null, LOG_CODE_EDITED, $comments, 'user', 'comments', $ref);
log_activity(null, LOG_CODE_EDITED, $approved, 'user', 'approved', $ref);
$sql_params = array_merge(
array("s", $username),
$sql_params,
array("s", $fullname,
"s", $email,
"i", $usergroup,
"s", $expires,
"s", $ip_restrict,
"s", $search_filter_override,
"i", $search_filter_o_id,
"s", $comments,
"i", $approved),
$additional_sql_params,
array("i", $ref)
);
ps_query("update user set username = ?" . $passsql . ", fullname = ?, email = ?, usergroup = ?, account_expires = ?, ip_restrict = ?,
search_filter_override = ?, search_filter_o_id = ?, comments = ?, approved = ? " . $additional_sql . " where ref = ?", $sql_params);
}
// Add user group dash tiles as soon as we've changed the user group
if (
$home_dash
&& $current_user_data['usergroup'] != $usergroup
) {
// If user group has changed, remove all user dash tiles that were valid for the old user group
ps_query("DELETE FROM user_dash_tile WHERE user = ? AND dash_tile IN (SELECT dash_tile FROM usergroup_dash_tile WHERE usergroup = ?)", array("i", $ref, "i", $current_user_data['usergroup']));
include_once __DIR__ . '/dash_functions.php';
build_usergroup_dash($usergroup, $ref);
}
if ($emailresetlink != '') {
$result = email_reset_link($email, empty($current_user_data["last_active"])); // Message differs if new user
if ($result !== true) {
return $result;
}
}
if (getval('approved', '') != '') {
# Clear any user request messages
message_remove_related(USER_REQUEST, $ref);
}
return true;
}
/**
* E-mail the user the welcome message on account creation.
*
* @param string $email
* @param string $username
* @param integer $usergroup
*/
function email_user_welcome(string $email, string $username, int $usergroup): void
{
global $applicationname, $baseurl,$lang;
# Fetch any welcome message for this user group
$welcome = ps_value("SELECT welcome_message value FROM usergroup WHERE ref = ?", ["i",$usergroup], "");
if (trim($welcome) === "") {
$welcome = str_replace("%applicationname", $applicationname, $lang["welcome_generic"]);
}
$templatevars['welcome'] = i18n_get_translated($welcome) . "\n\n";
$templatevars['username'] = $username;
$templatevars['url'] = $baseurl;
$message = $templatevars['welcome'] . $lang["newlogindetails"] . "\n\n" . $lang["username"] . ": " . $templatevars['username'] . "\n\n" . $templatevars['url'];
send_mail($email, $applicationname . ": " . $lang["youraccountdetails"], $message, "", "", "emaillogindetails", $templatevars);
}
/**
* Email password reset link to the user
*
* @param string $email Email address of user
* @param string $newuser Is this a new user account? If so a welcome message template will be used
*
* @return bool|string true if success or error message
*
*/
function email_reset_link(string $email, bool $newuser = false)
{
global $applicationname, $baseurl, $lang;
debug("password_reset - checking for email: " . $email);
# Send a link to reset password
global $password_brute_force_delay, $scramble_key, $lang;
if ($email == '') {
// Password reset link not sent because the account has no email address
return $lang["accountnoemail-reset-not-emailed"];
}
# The reset link is sent after the principal user update has completed
# It will only be sent if (after the user update) there is an approved and unexpired user with the specified email address
$details = ps_query(
"SELECT ref, username, usergroup, origin, approved,
CASE
WHEN isnull(account_expires) THEN false
WHEN account_expires > NOW() THEN false
ELSE true
END
AS has_expired
FROM user WHERE email = ?;",
["s", $email]
);
if ($GLOBALS["pagename"] !== "team_user_edit") {
// Don't add delay if admin is resetting password
sleep($password_brute_force_delay);
}
if (count($details) == 0) {
return $lang["accountnotfound-reset-not-emailed"]; # Password reset link was not sent because there is no account with that email
}
# Process the user with the email address
$details = $details[0];
if ($details["has_expired"]) {
// Password reset link was not sent because the account has expired
return $lang["accountexpired-reset-not-emailed"];
}
if ($details["approved"] != 1) {
// Password reset link was not sent because the account is not approved
return $lang["accountnotapproved-reset-not-emailed"];
}
// Don't send password reset links if we don't control the password
$blockreset = isset($details["origin"]) && trim($details["origin"]) != "";
if (!$blockreset) {
$password_reset_url_key = create_password_reset_key($details['username']);
$templatevars['url'] = $baseurl . '/?rp=' . $details['ref'] . $password_reset_url_key;
}
$templatevars['username'] = $details["username"];
$email_subject = $applicationname . ": " . $lang["youraccountdetails"];
if ($newuser) {
// Fetch any welcome message for this user group
$welcome = ps_value('SELECT welcome_message AS value FROM usergroup WHERE ref = ?', ["i",$details['usergroup']], '');
if (trim($welcome) === "") {
$welcome = str_replace("%applicationname", $applicationname, $lang["welcome_generic"]);
}
if (hook("ssologindefault")) {
$loginurl = $baseurl . "/login.php";
} else {
$loginurl = $baseurl;
}
$templatevars['welcome'] = i18n_get_translated($welcome) . "\n\n";
if ($blockreset) {
$message = $templatevars['welcome'] . "\n\n" . $lang["passwordresetexternalauth"] . "\n\n" . $loginurl . "\n\n" . $lang["username"] . ": " . $templatevars['username'];
$result = send_mail($email, $email_subject, $message);
} else {
$message = $templatevars['welcome'] . $lang["newlogindetails"] . "\n\n" . $loginurl . "\n\n" . $lang["username"] . ": " . $templatevars['username'] . "\n\n" . $lang["passwordnewemail"] . "\n" . $templatevars['url'];
$result = send_mail($email, $email_subject, $message, "", "", "passwordnewemailhtml", $templatevars);
}
} else {
// Existing user
$message = $lang["username"] . ": " . $templatevars['username'];
if ($blockreset) {
$message .= "\n\n" . $lang["passwordresetnotpossible"] . "\n\n" . $lang["passwordresetexternalauth"] . "\n\n" . $baseurl;
$result = send_mail($email, $email_subject, $message);
} else {
$message .= "\n\n" . $lang["passwordresetemail"] . "\n\n" . $templatevars['url'];
$result = send_mail($email, $applicationname . ": " . $lang["resetpassword"], $message, "", "", "password_reset_email_html", $templatevars);
}
}
// Pass any e-mail errors back, or return true if successful
return $result;
}
/**
* Automatically creates a user account
* The request can be auto approved if $auto_approve_accounts is true
* Otherwise the approval is managed by admins via notification messages and/or emails
*
* @param string $hash
* @return boolean Success?
*/
function auto_create_user_account($hash = "")
{
global $user_email, $baseurl, $lang, $user_account_auto_creation_usergroup, $registration_group_select,
$auto_approve_accounts, $auto_approve_domains, $customContents, $language, $home_dash, $applicationname,
$username, $userref;
// Feature disabled if user limit reached.
if (user_limit_reached()) {
return false;
}
# Work out which user group to set. Allow a hook to change this, if necessary.
$altgroup = hook("auto_approve_account_switch_group");
if ($altgroup !== false) {
$usergroup = $altgroup;
} else {
$usergroup = $user_account_auto_creation_usergroup;
}
if ($registration_group_select) {
$usergroup = getval("usergroup", 0, true);
# Check this is a valid selectable usergroup (should always be valid unless this is a hack attempt)
if (ps_value("SELECT allow_registration_selection value FROM usergroup WHERE ref = ?", ["i",$usergroup], 0) != 1) {
exit("Invalid user group selection");
}
}
// Check valid email
if (!filter_var($user_email, FILTER_VALIDATE_EMAIL)) {
return $lang['setup-emailerr'];
}
$newusername = make_username(getval("name", ""), $user_email);
// Check if account already exists
$emailmatches = ps_value("SELECT COUNT(*) value FROM user WHERE email = ?", ["s",$user_email], 0);
if ($emailmatches > 0) {
// Return an ambiguous message if delegated user admin
return checkperm("U") ? $lang["useralreadyexists"] : $lang["useremailalreadyexists"];
}
# Prepare to create the user.
$password = make_password();
$password = rs_password_hash("RS{$newusername}{$password}");
# Work out if we should automatically approve this account based on $auto_approve_accounts or $auto_approve_domains
$approve = false;
if ($auto_approve_accounts) {
// No longer taken direct to password page as this can be used by bots. User must click on an email link
$approve = true;
} elseif (count($auto_approve_domains) > 0) {
# Check e-mail domain.
foreach ($auto_approve_domains as $domain => $set_usergroup) {
// If a group is not specified the variables don't get set correctly so we need to correct this
if (is_numeric($domain)) {
$domain = $set_usergroup;
$set_usergroup = "";
}
if (substr(strtolower($user_email), strlen($user_email) - strlen($domain) - 1) == ("@" . strtolower($domain))) {
# E-mail domain match.
$approve = true;
# If user group is supplied, set this
if (is_numeric($set_usergroup)) {
$usergroup = $set_usergroup;
}
}
}
}
# Create the user
$name = getval("name", "");
$comment = getval("userrequestcomment", "");
$newparams = [
"s",$newusername,
"s",$password,
"s",$name,
"s",$user_email,
"i",$usergroup,
"s",$customContents . (trim($comment) != "" ? "\n" . $comment : ""),
"i",($approve ? 1 : 0),
"s",$language,
"s",($hash != "" ? $hash : null),
];
ps_query("INSERT INTO user (username,password,fullname,email,usergroup,comments,approved,lang,unique_hash) VALUES (?,?,?,?,?,?,?,?,?)", $newparams);
$new = sql_insert_id();
// Create dash tiles for the new user
if ($home_dash) {
include_once __DIR__ . '/dash_functions.php';
create_new_user_dash($new);
build_usergroup_dash($usergroup, $new);
}
global $user_registration_opt_in;
if ($user_registration_opt_in && getval("login_opt_in", "") == "yes") {
log_activity($lang["user_registration_opt_in_message"], LOG_CODE_USER_OPT_IN, null, "user", null, null, null, null, $new, false);
}
hook("afteruserautocreated", "all", array("new" => $new));
global $anonymous_login;
if (isset($anonymous_login)) {
global $rs_session;
$rs_session = get_rs_session_id();
if ($rs_session !== false) {
# Copy any anonymous session collections to the new user account
if (is_array($anonymous_login) && array_key_exists($baseurl, $anonymous_login)) {
$anonymous_login = $anonymous_login[$baseurl];
}
$username = $anonymous_login;
$userref = ps_value("SELECT ref value FROM user where username=?", array("s",$anonymous_login), "");
$sessioncollections = get_session_collections($rs_session, $userref, false);
if (count($sessioncollections) > 0) {
foreach ($sessioncollections as $sessioncollection) {
update_collection_user($sessioncollection, $new);
}
ps_query("UPDATE user SET current_collection = ? WHERE ref = ?", array("i", $sessioncollection, "i", $new));
}
}
}
if ($approve) {
email_reset_link($user_email, true);
redirect($baseurl . "/pages/done.php?text=user_request");
exit();
} else {
# Managed approving
# Build a message to send to an admin notifying of unapproved user (same as email_user_request(),
# but also adds the new user name to the mail)
global $user_pref_user_management_notifications;
$templatevars['name'] = getval("name", "");
$templatevars['email'] = $user_email;
$templatevars['userrequestcomment'] = strip_tags(getval("userrequestcomment", ""));
$templatevars['userrequestcustom'] = strip_tags($customContents);
$url = $baseurl . "?u=" . $new;
$templatevars['linktouser'] = "<a href='" . $url . "'>" . $url . "</a>";
$approval_notify_users = get_notification_users("USER_ADMIN", $usergroup);
$message = new ResourceSpaceUserNotification();
$eventdata = [
"type" => USER_REQUEST,
"ref" => $new,
"extra" => ["usergroup" => $usergroup],
];
$message->set_subject("lang_requestuserlogin");
$message->set_text("lang_userrequestnotification1");
$message->append_text("<br/><br/>");
$message->append_text("lang_name");
$message->append_text(": " . $templatevars['name'] . "<br/><br/>");
$message->append_text("lang_email");
$message->append_text(": " . $templatevars['email'] . "<br/><br/>");
$message->append_text("lang_comment");
$message->append_text(": " . $templatevars['userrequestcomment'] . "<br/><br/>");
$message->append_text("lang_ipaddress");
$message->append_text(": " . get_ip() . "<br/><br/>");
if (trim($customContents) != "") {
$message->append_text($customContents . "<br/><br/>");
}
$message->append_text("lang_userrequestnotification3");
$message->append_text("<br/><br/>" . $templatevars['linktouser']);
$message->user_preference = [
"user_pref_user_management_notifications" => ["requiredvalue" => true, "default" => $user_pref_user_management_notifications],
"actions_account_requests" => ["requiredvalue" => false,"default" => true],
];
$message->url = $url;
$message->template = "account_request";
$message->templatevars = $templatevars;
$message->eventdata = $eventdata;
send_user_notification($approval_notify_users, $message);
}
// Send a confirmation e-mail to requester
send_mail(
$user_email,
"{$applicationname}: {$lang['account_request_label']}",
$lang['account_request_confirmation_email_to_requester']
);
return true;
}
/**
* Send user request to admins in form of notification messages and/or emails
* Send email confirmation to requesting user
*
* @return boolean
*/
function email_user_request()
{
// E-mails the submitted user request form to the team.
global $applicationname, $baseurl, $lang, $customContents, $account_email_exists_notify, $user_registration_opt_in, $user_account_auto_creation, $user_pref_user_management_notifications;
// Get posted vars sanitized:
$name = strip_tags(getval('name', ''));
$email = strip_tags(getval('email', ''));
$userrequestcomment = strip_tags(getval('userrequestcomment', ''));
$user_limit_reached = user_limit_reached();
$usergroup = 0;
if ($account_email_exists_notify) {
$usergroup = ps_value("SELECT usergroup AS `value` FROM user WHERE email = ?", ["s", $email], 0);
}
$user_registration_opt_in_message = "";
if ($user_registration_opt_in && getval("login_opt_in", "") == "yes") {
$user_registration_opt_in_message .= $lang["user_registration_opt_in_message"];
}
$requestedgroup = getval("usergroup", 0, true);
$approval_notify_users = get_notification_users("USER_ADMIN", $usergroup ?: $requestedgroup);
$message = new ResourceSpaceUserNotification();
$message->set_subject($applicationname . ": ");
$message->append_subject("lang_requestuserlogin");
$message->append_subject(" - " . $name);
$message->set_text($account_email_exists_notify && !$user_limit_reached ? "lang_userrequestnotificationemailprotection1" : "lang_userrequestnotification1");
$message->append_text("<br/><br/>");
$message->append_text("lang_name");
$message->append_text(": " . $name . "<br/><br/>");
$message->append_text("lang_email");
$message->append_text(": " . $email . "<br/><br/>");
$message->append_text($user_registration_opt_in_message . "<br/><br/>");
$message->append_text("lang_comment");
$message->append_text(": " . $userrequestcomment . "<br/><br/>");
$message->append_text("lang_ipaddress");
$message->append_text(": " . get_ip() . "<br/><br/>");
if (trim($customContents) != "") {
$message->append_text($customContents . "<br/><br/>");
}
// User limit reached? Add a message explaining.
if ($user_limit_reached) {
$message->append_text("lang_userlimitreached");
} else {
$message->append_text($account_email_exists_notify ? "lang_userrequestnotificationemailprotection2" : "lang_userrequestnotification2");
}
$message->user_preference = ["user_pref_user_management_notifications" => ["requiredvalue" => true, "default" => $user_pref_user_management_notifications]];
$message->url = $baseurl . "/pages/team/team_user.php";
send_user_notification($approval_notify_users, $message);
// Send a confirmation e-mail to requester
send_mail(
$email,
"{$applicationname}: {$lang['account_request_label']}",
$lang['account_request_confirmation_email_to_requester']
);
return true;
}
/**
* Check to see if the user limit has been reached.
* *
* @return boolean - true if user limit has been reached or exceeded
*/
function user_limit_reached()
{
global $user_limit;
if (
isset($user_limit)
&& get_total_approved_users() >= $user_limit
) {
return true;
}
return false;
}
/**
* Create a new user
* *
* @param string $newuser - username to create
* @param integer $usergroup - optional usergroup to assign
*
* @return boolean|integer - id of new user or false if user already exists, or -2 if user limit reached
*/
function new_user($newuser, $usergroup = 0)
{
global $lang,$home_dash,$user_limit;
# Username already exists?
$c = ps_value("SELECT COUNT(*) value FROM user WHERE username = ?", ["s",$newuser], 0);
if ($c > 0) {
return false;
}
# User limit reached?
if (user_limit_reached()) {
return -2;
}
$cols = array("username");
$sqlparams = ["s",$newuser];
$cols[] = 'password';
$sqlparams[] = 's';
$sqlparams[] = rs_password_hash("RS{$newuser}" . make_password());
if ($usergroup > 0) {
$cols[] = "usergroup";
$sqlparams[] = "i";
$sqlparams[] = $usergroup;
}
$sql = "INSERT INTO user (" . implode(",", $cols) . ") VALUES (" . ps_param_insert(count($cols)) . ")";
ps_query($sql, $sqlparams);
$newref = sql_insert_id();
#Create Default Dash for the new user
if ($home_dash) {
include_once __DIR__ . "/dash_functions.php";
create_new_user_dash($newref);
}
# Create a collection for this user, the collection name is translated when displayed!
$new = create_collection($newref, "Default Collection", 0, 1); # Do not translate this string!
# set this to be the user's current collection
ps_query("UPDATE user SET current_collection=? WHERE ref=?", ["i",$new,"i",$newref]);
log_activity($lang["createuserwithusername"], LOG_CODE_CREATED, $newuser, 'user', 'ref', $newref, null, '');
return $newref;
}
/**
* Returns a list of active users
*
* @return array
*/
function get_active_users()
{
global $usergroup;
// Establish which user groups the supplied user group acts as an approver for
$approver_groups = get_approver_usergroups($usergroup);
$sql = "where logged_in = 1 and unix_timestamp(now()) - unix_timestamp(last_active) < (3600*2)";
$sql_params = array();
if (checkperm("U") || count($approver_groups) > 0) {
if (count($approver_groups) > 0) {
$sql .= "and (find_in_set(?, g.parent) or usergroup in (" . ps_param_insert(count($approver_groups)) . "))";
$sql_params = array_merge(array("i", $usergroup), ps_param_fill($approver_groups, "i"));
} else {
$sql .= " and find_in_set(?, g.parent) ";
$sql_params = array("i", $usergroup);
}
}
# Returns a list of all active users, i.e. users still logged on with a last-active time within the last 2 hours.
return ps_query("SELECT u.ref, u.username, round((unix_timestamp(now()) - unix_timestamp(u.last_active)) / 60, 0) t
from user u left outer join usergroup g on u.usergroup = g.ref
$sql order by t;", $sql_params);
}
/**
* Sets a new password for the current user.
*
* @param string $password
* @return mixed True if a success or a descriptive string if there's an issue.
*/
function change_password($password)
{
global $userref,$username,$lang,$userpassword, $password_reset_mode;
# Check password
$message = check_password($password);
if ($message !== true) {
return $message;
}
# Generate new password hash
$password_hash = rs_password_hash("RS{$username}{$password}");
# Check password is not the same as the current
if ($userpassword == $password_hash) {
return $lang["password_matches_existing"];
}
ps_query("update user set password = ?, password_reset_hash = NULL, login_tries = 0, password_last_change = now() where ref = ? limit 1", array("s", $password_hash, "i", $userref));
return true;
}
/**
* Generate a password using the configured settings.
*
* @return string The generated password
*/
function make_password()
{
global $password_min_length, $password_min_alpha, $password_min_uppercase, $password_min_numeric, $password_min_special;
$lowercase = "abcdefghijklmnopqrstuvwxyz";
$uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
$alpha = $uppercase . $lowercase;
$numeric = "0123456789";
$special = "!@$%^&*().?";
$password = "";
# Add alphanumerics
for ($n = 0; $n < $password_min_alpha; $n++) {
$password .= substr($alpha, rand(0, strlen($alpha) - 1), 1);
}
# Add upper case
for ($n = 0; $n < $password_min_uppercase; $n++) {
$password .= substr($uppercase, rand(0, strlen($uppercase) - 1), 1);
}
# Add numerics
for ($n = 0; $n < $password_min_numeric; $n++) {
$password .= substr($numeric, rand(0, strlen($numeric) - 1), 1);
}
# Add special
for ($n = 0; $n < $password_min_special; $n++) {
$password .= substr($special, rand(0, strlen($special) - 1), 1);
}
# Pad with lower case
$padchars = $password_min_length - strlen($password);
for ($n = 0; $n < $padchars; $n++) {
$password .= substr($lowercase, rand(0, strlen($lowercase) - 1), 1);
}
# Shuffle the password.
$password = str_shuffle($password);
# Check the password
$check = check_password($password);
if ($check !== true) {
exit("Error: unable to automatically produce a password that met the criteria. Please check the password criteria in config.php. Generated password was '$password'. Error was: " . $check);
}
return $password;
}
/**
* Send a bulk e-mail using the bulk e-mail tool.
*
* @param string $userlist
* @param string $subject
* @param string $text
* @param string $html
* @param integer $message_type
* @param string $url
* @return string The empty string if all OK, a descriptive string if there's an issue.
*/
function bulk_mail($userlist, $subject, $text, $html = false, $message_type = MESSAGE_ENUM_NOTIFICATION_TYPE_EMAIL, $url = "")
{
global $email_from,$lang,$applicationname;
# Attempt to resolve all users in the string $userlist to user references.
if (trim($userlist) == "") {
return $lang["mustspecifyoneuser"];
}
$userlist = resolve_userlist_groups($userlist);
$ulist = trim_array(explode(",", $userlist));
$templatevars['text'] = stripslashes(str_replace("\\r\\n", "\n", $text));
$body = $templatevars['text'];
if ($message_type == MESSAGE_ENUM_NOTIFICATION_TYPE_EMAIL || $message_type == (MESSAGE_ENUM_NOTIFICATION_TYPE_EMAIL | MESSAGE_ENUM_NOTIFICATION_TYPE_SCREEN)) {
$emails = resolve_user_emails($ulist);
if (0 === count($emails)) {
return $lang['email_error_user_list_not_valid'];
}
$emails = $emails['emails'];
# Send an e-mail to each resolved user
foreach ($emails as $email) {
if (filter_var($email, FILTER_VALIDATE_EMAIL)) {
send_mail($email, $subject, $body, $applicationname, $email_from, "emailbulk", $templatevars, $applicationname, "", $html);
}
}
}
if ($message_type == MESSAGE_ENUM_NOTIFICATION_TYPE_SCREEN || $message_type == (MESSAGE_ENUM_NOTIFICATION_TYPE_EMAIL | MESSAGE_ENUM_NOTIFICATION_TYPE_SCREEN)) {
$user_refs = array();
foreach ($ulist as $user) {
$user_ref = ps_value("SELECT ref AS value FROM user WHERE username=?", array("s",$user), false);
if ($user_ref !== false) {
array_push($user_refs, $user_ref);
}
}
if ($message_type == (MESSAGE_ENUM_NOTIFICATION_TYPE_EMAIL | MESSAGE_ENUM_NOTIFICATION_TYPE_SCREEN) && $html) {
# strip the tags out
$body = strip_tags($body);
}
message_add($user_refs, $body, $url, null, $message_type);
}
# Return an empty string (all OK).
return "";
}
/**
* Returns a user action log for $user.
* Standard field titles are translated using $lang. Custom field titles are i18n translated.
*
* @param integer $user
* @param integer $fetchrows How many rows to fetch?
* @return array
*/
function get_user_log($user, $fetchrows = -1)
{
global $view_title_field;
# Executes query.
$r = ps_query("select r.ref resourceid, r.field" . (int) $view_title_field . " resourcetitle, l.date, l.type, f.title, l.notes, l.diff from resource_log l left outer join resource r on l.resource = r.ref left outer join resource_type_field f on f.ref = l.resource_type_field where l.user = ? order by l.date desc", array("i", $user), '', $fetchrows);
# Translates field titles in the newly created array.
$return = array();
for ($n = 0; $n < count($r); $n++) {
if (is_array($r[$n])) {
$r[$n]["title"] = lang_or_i18n_get_translated($r[$n]["title"], "fieldtitle-");
}
$return[] = $r[$n];
}
return $return;
}
/**
* Given an array or comma separated user list (from the user select include file) turn all Group: entries into fully resolved list of usernames.
* Note that this function can't decode default groupnames containing special characters.
*
* @param string|array $userlist
* @return string The resolved list
*/
function resolve_userlist_groups($userlist)
{
global $lang;
if (!is_array($userlist)) {
$userlist = explode(",", $userlist);
}
$newlist = "";
for ($n = 0; $n < count($userlist); $n++) {
$u = trim($userlist[$n]);
if (strpos($u, $lang["group"] . ": ") === 0) {
# Group entry, resolve
# Find the translated groupname.
$translated_groupname = trim(substr($u, strlen($lang["group"] . ": ")));
# Search for corresponding $lang indices.
$default_group = false;
$langindices = array_keys($lang, $translated_groupname);
if (count($langindices) > 0) {
foreach ($langindices as $langindex) {
# Check if it is a default group
if (strstr($langindex, "usergroup-") !== false) {
# Decode the groupname by using the code from lang_or_i18n_get_translated the other way around (it could be possible that someone have renamed the English groupnames in the language file).
$untranslated_groupname = trim(substr($langindex, strlen("usergroup-")));
$untranslated_groupname = str_replace(array("_", "and"), array(" "), $untranslated_groupname);
$groupref = ps_value("select ref as value from usergroup where lower(name)=?", array("s",$untranslated_groupname), false);
if ($groupref !== false) {
$default_group = true;
break;
}
}
}
}
if (!$default_group) {
# Custom group
# Decode the groupname
$untranslated_groups = ps_query("select ref, name from usergroup");
foreach ($untranslated_groups as $group) {
if (i18n_get_translated($group['name']) == $translated_groupname) {
$groupref = $group['ref'];
break;
}
}
}
# Find and add the users.
if (isset($groupref)) {
$users = ps_array("SELECT username AS `value` FROM user WHERE usergroup = ?", array("i", $groupref));
if ($newlist != "") {
$newlist .= ",";
}
$newlist .= join(",", $users);
}
} else {
# Username, just add as-is
if ($newlist != "") {
$newlist .= ",";
}
$newlist .= $u;
}
}
return $newlist;
}
/**
* Given a comma separated user list (from the user select include file) turn all Group: entries into fully resolved list of usernames.
* Note that this function can't decode default groupnames containing special characters.
*
* @param string $userlist
* @param boolean $return_usernames
* @return string The resolved list
*/
function resolve_userlist_groups_smart($userlist, $return_usernames = false)
{
global $lang;
if (!is_array($userlist)) {
$userlist = explode(",", $userlist);
}
$newlist = "";
for ($n = 0; $n < count($userlist); $n++) {
$u = trim($userlist[$n]);
if (strpos($u, $lang["groupsmart"] . ": ") === 0) {
# Group entry, resolve
# Find the translated groupname.
$translated_groupname = trim(substr($u, strlen($lang["groupsmart"] . ": ")));
# Search for corresponding $lang indices.
$default_group = false;
$langindices = array_keys($lang, $translated_groupname);
if (count($langindices) > 0) {
foreach ($langindices as $langindex) {
# Check if it is a default group
if (strstr($langindex, "usergroup-") !== false) {
# Decode the groupname by using the code from lang_or_i18n_get_translated the other way around (it could be possible that someone have renamed the English groupnames in the language file).
$untranslated_groupname = trim(substr($langindex, strlen("usergroup-")));
$untranslated_groupname = str_replace(array("_", "and"), array(" "), $untranslated_groupname);
$groupref = ps_value("select ref as value from usergroup where lower(name)=?", array("s",$untranslated_groupname), false);
if ($groupref !== false) {
$default_group = true;
break;
}
}
}
}
if (!$default_group) {
# Custom group
# Decode the groupname
$untranslated_groups = ps_query("select ref, name from usergroup");
foreach ($untranslated_groups as $group) {
if (i18n_get_translated($group['name']) == $translated_groupname) {
$groupref = $group['ref'];
break;
}
}
}
if (isset($groupref)) {
if ($return_usernames) {
$users = ps_array("select username value from user where usergroup = ?", array("i", $groupref));
if ($newlist != "") {
$newlist .= ",";
}
$newlist .= join(",", $users);
} else {
# Find and add the users.
if ($newlist != "") {
$newlist .= ",";
}
$newlist .= $groupref;
}
}
}
}
return $newlist;
}
/**
* Remove smart lists from the provided user lists.
*
* @param string|array $ulist Comma separated list of user list names
* @return string The updated list with smart groups removed.
*/
function remove_groups_smart_from_userlist($ulist)
{
global $lang;
if (!is_array($ulist)) {
$ulist = explode(",", $ulist);
}
$new_ulist = '';
foreach ($ulist as $option) {
if (strpos($option, $lang["groupsmart"] . ": ") === false) {
if ($new_ulist != "") {
$new_ulist .= ",";
}
$new_ulist .= $option;
}
}
return $new_ulist;
}
/**
* Checks that a password conforms to the configured paramaters.
*
* @param string $password The password
* @return mixed True if OK, or a descriptive string if it isn't
*/
function check_password($password)
{
global $lang, $password_min_length, $password_min_alpha, $password_min_uppercase, $password_min_numeric, $password_min_special;
$password = trim($password);
if (strlen($password) < $password_min_length) {
return str_replace("?", $password_min_length, $lang["password_not_min_length"]);
}
$uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
$alpha = $uppercase . "abcdefghijklmnopqrstuvwxyz";
$numeric = "0123456789";
$a = 0;
$u = 0;
$n = 0;
$s = 0;
for ($m = 0; $m < strlen($password); $m++) {
$l = substr($password, $m, 1);
if (strpos($uppercase, $l) !== false) {
$u++;
}
if (strpos($alpha, $l) !== false) {
$a++;
} elseif (strpos($numeric, $l) !== false) {
$n++;
} else {
$s++;
} # Not alpha/numeric, must be a special char.
}
if ($a < $password_min_alpha) {
return str_replace("?", $password_min_alpha, $lang["password_not_min_alpha"]);
}
if ($u < $password_min_uppercase) {
return str_replace("?", $password_min_uppercase, $lang["password_not_min_uppercase"]);
}
if ($n < $password_min_numeric) {
return str_replace("?", $password_min_numeric, $lang["password_not_min_numeric"]);
}
if ($s < $password_min_special) {
return str_replace("?", $password_min_special, $lang["password_not_min_special"]);
}
return true;
}
/**
* For a given comma-separated list of user refs (e.g. returned from a group_concat()), return a string of matching usernames.
*
* @param string $users User list - caution, used directly in SQL so must not contain user input
* @return string Matching usernames.
*/
function resolve_users($users)
{
if (trim($users) == "") {
return "";
}
$resolved = ps_array("select concat(fullname, ' (',username,')') value from user where ref in (?)", array("s", $users));
return join(", ", $resolved);
}
/**
* Verify a supplied external access key
*
* @param array | integer $resources Resource ID | Array of resource IDs
* @param string $key The external access key
* @param boolean $checkcollection Check collection access key? true by default but required to prevent infinite recursion
* @return boolean Valid?
*/
function check_access_key($resources, $key, $checkcollection = true)
{
if (trim($key) == '') {
return false;
}
if (!is_array($resources)) {
$resources = array($resources);
}
array_filter($resources, 'is_int_loose');
foreach ($resources as $resource) {
$resource = (int)$resource;
# Option to plugin in some extra functionality to check keys
if (hook("check_access_key", "", array($resource, $key)) === true) {
return true;
}
}
hook("external_share_view_as_internal_override");
global $external_share_view_as_internal, $baseurl, $baseurl_short;
if ($external_share_view_as_internal && isset($_COOKIE["user"])) {
$user_select_sql = new PreparedStatementQuery();
$user_select_sql->sql = "u.session = ?";
$user_select_sql->parameters = ["s",$_COOKIE["user"]];
if (validate_user($user_select_sql, false) && !is_authenticated()) {
// Authenticate the user if not already authenticated so page can appear as internal
return false;
}
}
$keys = ps_query(
"
SELECT k.user,
k.usergroup,
k.expires,
k.password_hash,
k.access,
k.resource
FROM external_access_keys k
LEFT JOIN user u ON u.ref = k.user
WHERE k.access_key = ?
AND k.resource IN (" . ps_param_insert(count($resources)) . ")
AND (k.expires IS NULL OR k.expires > now())
AND u.approved = 1
ORDER BY k.access",
array_merge(array("s", $key), ps_param_fill($resources, "i"))
);
if (count($keys) == 0 || count(array_diff($resources, array_column($keys, "resource"))) > 0) {
// Check if this is a request for a resource uploaded to an upload_share
$upload_sharecol = upload_share_active();
if ($checkcollection && $upload_sharecol && check_access_key_collection($upload_sharecol, $key, false)) {
$uploadsession = get_rs_session_id();
$uploadcols = get_session_collections($uploadsession);
foreach ($uploadcols as $uploadcol) {
$sessioncol_resources = get_collection_resources($uploadcol);
if (!array_diff($sessioncol_resources, $resources)) {
return true;
}
}
}
return false;
}
if ($keys[0]["access"] == -1) {
// If the resources have -1 as access they may have been added without the correct expiry etc.
ps_query("UPDATE external_access_keys ak
LEFT JOIN (SELECT * FROM external_access_keys ake WHERE access_key = ? ORDER BY access DESC, expires ASC LIMIT 1) ake
ON ake.access_key = ak.access_key
AND ake.collection = ak.collection
SET ak.expires = ake.expires,
ak.access = ake.access,
ak.usergroup = ake.usergroup,
ak.email = ake.email,
ak.password_hash = ake.password_hash
WHERE ak.access_key = ?
AND ak.access = '-1'
AND ak.expires IS NULL", array("s", $key, "s", $key));
return false;
}
if ($keys[0]["password_hash"] != "" && PHP_SAPI != "cli") {
// A share password has been set. Check if user has a valid cookie set
$share_access_cookie = isset($_COOKIE["share_access"]) ? $_COOKIE["share_access"] : "";
$check = check_share_password($key, "", $share_access_cookie);
if (!$check) {
$url = generateURL($baseurl . "/pages/share_access.php", array("k" => $key,"resource" => $resources[0],"return_url" => $baseurl . (isset($_SERVER["REQUEST_URI"]) ? urlencode(str_replace($baseurl_short, "/", $_SERVER["REQUEST_URI"])) : "/r=" . $resource . "&k=" . $key)));
redirect($url);
exit();
}
}
$user = $keys[0]["user"];
$group = $keys[0]["usergroup"];
$expires = $keys[0]["expires"];
# Has this expired?
if ($expires != "" && strtotime($expires) < time()) {
if (is_authenticated()) {
return false;
}
global $lang;
?>
<script type="text/javascript">
alert("<?php echo escape($lang["externalshareexpired"]); ?>");
history.go(-1);
</script>
<?php
exit();
}
# "Emulate" the user that e-mailed the resource by setting the same group and permissions
emulate_user($user, $group);
global $usergroup,$userpermissions,$userrequestmode,$usersearchfilter,$external_share_groups_config_options;
$groupjoin = "u.usergroup = g.ref";
$permissionselect = "g.permissions";
$groupjoin_params = array();
if ($keys[0]["usergroup"] != "") {
# Select the user group from the access key instead.
$groupjoin = "g.ref = ? LEFT JOIN usergroup pg ON g.parent = pg.ref";
$groupjoin_params = array("i", $keys[0]["usergroup"]);
$permissionselect = "if(find_in_set('permissions', g.inherit_flags) AND pg.permissions IS NOT NULL, pg.permissions, g.permissions) permissions";
}
$userinfo = ps_query("select g.ref usergroup," . $permissionselect . " , g.search_filter, g.config_options, g.search_filter_id, g.derestrict_filter_id, u.search_filter_override, u.search_filter_o_id from user u join usergroup g on $groupjoin where u.ref = ?", array_merge($groupjoin_params, array("i", $user)));
if (count($userinfo) > 0) {
$usergroup = $userinfo[0]["usergroup"]; # Older mode, where no user group was specified, find the user group out from the table.
// Usergroup that the key is trying to emulate has no permissions
if (trim((string) $userinfo[0]['permissions']) == '') {
return false;
}
$userpermissions = explode(",", $userinfo[0]["permissions"]);
if (isset($userinfo[0]["search_filter_o_id"]) && is_numeric($userinfo[0]["search_filter_o_id"]) && $userinfo[0]['search_filter_o_id'] > 0) {
// User search filter override
$usersearchfilter = $userinfo[0]["search_filter_o_id"];
} elseif (isset($userinfo[0]["search_filter_id"]) && is_numeric($userinfo[0]["search_filter_id"]) && $userinfo[0]['search_filter_id'] > 0) {
// Group search filter
$usersearchfilter = $userinfo[0]["search_filter_id"];
}
if (hook("modifyuserpermissions")) {
$userpermissions = hook("modifyuserpermissions");
}
$userrequestmode = 0; # Always use 'email' request mode for external users
# Load any plugins specific to the group of the sharing user, but only once as may be checking multiple keys
global $emulate_plugins_set;
if ($emulate_plugins_set !== true) {
global $plugins;
$enabled_plugins = (ps_query("SELECT name,enabled_groups, config, config_json FROM plugins WHERE inst_version>=0 AND length(enabled_groups)>0 ORDER BY priority"));
foreach ($enabled_plugins as $plugin) {
$s = explode(",", $plugin['enabled_groups']);
if (in_array($usergroup, $s)) {
include_plugin_config($plugin['name'], $plugin['config'], $plugin['config_json']);
register_plugin($plugin['name']);
$plugins[] = $plugin['name'];
}
}
for ($n = count($plugins) - 1; $n >= 0; $n--) {
if (!isset($plugins[$n])) {
continue;
}
register_plugin_language($plugins[$n]);
}
$emulate_plugins_set = true;
}
if ($external_share_groups_config_options || stripos(trim(isset($userinfo[0]["config_options"])), "external_share_groups_config_options=true") !== false) {
# Apply config override options
$config_options = trim($userinfo[0]["config_options"] ?? "");
// We need to get all globals as we don't know what may be referenced here
override_rs_variables_by_eval($GLOBALS, $config_options, 'usergroup');
}
}
# Special case for anonymous logins.
# When a valid key is present, we need to log the user in as the anonymous user so they will be able to browse the public links.
global $anonymous_login;
if (isset($anonymous_login)) {
global $username,$baseurl;
if (is_array($anonymous_login)) {
foreach ($anonymous_login as $key => $val) {
if ($baseurl == $key) {
$anonymous_login = $val;
}
}
}
$username = $anonymous_login;
}
# Set the 'last used' date for this key
ps_query("UPDATE external_access_keys SET lastused = now() WHERE resource IN (" . ps_param_insert(count($resources)) . ") AND access_key = ?", array_merge(ps_param_fill($resources, "i"), array("s", $key)));
return true;
}
/**
* Check access key for a collection. For a featured collection category, the check will be done on all sub featured collections.
*
* @param integer $collection Collection ID
* @param string $key Access key
* @param boolean $checkresource Check for resource access key? true by default but required to prevent infinite recursion
*
* @return boolean
*/
function check_access_key_collection($collection, $key, $checkresource = true)
{
if (!is_int_loose($collection)) {
return false;
}
hook("external_share_view_as_internal_override");
global $external_share_view_as_internal, $baseurl, $baseurl_short, $pagename;
if ($external_share_view_as_internal && isset($_COOKIE["user"])) {
$user_select_sql = new PreparedStatementQuery();
$user_select_sql->sql = "u.session = ?";
$user_select_sql->parameters = ["s",$_COOKIE["user"]];
if (validate_user($user_select_sql, false) && !is_authenticated()) {
// Authenticate the user if not already authenticated so page can appear as internal
return false;
}
}
$collection = get_collection($collection);
if ($collection === false) {
return false;
}
// Get key info
$keyinfo = ps_query("
SELECT user,
usergroup,
expires,
upload,
password_hash,
collection
FROM external_access_keys
WHERE access_key = ?
AND (expires IS NULL OR expires > now())", array("s", $key));
if (count($keyinfo) == 0) {
return false;
}
$collection_resources = get_collection_resources($collection["ref"]);
$collection["has_resources"] = (is_array($collection_resources) && !empty($collection_resources) ? 1 : 0);
$is_featured_collection_category = is_featured_collection_category($collection);
if (!$is_featured_collection_category && (!$collection["has_resources"] && !(bool)$keyinfo[0]["upload"])) {
return false;
}
// From this point all collections should have resources. For FC categories, its sub FCs will have resources because
// get_featured_collection_categ_sub_fcs() does the check internally
$collections = (!$is_featured_collection_category ? array($collection["ref"]) : get_featured_collection_categ_sub_fcs($collection, array("access_control" => false)));
if ($keyinfo[0]["password_hash"] != "" && PHP_SAPI != "cli") {
// A share password has been set. Check if user has a valid cookie set
$share_access_cookie = isset($_COOKIE["share_access"]) ? $_COOKIE["share_access"] : "";
$check = check_share_password($key, "", $share_access_cookie);
if (!$check) {
$url = generateURL($baseurl . "/pages/share_access.php", array("k" => $key,"return_url" => $baseurl . (isset($_SERVER["REQUEST_URI"]) ? urlencode(str_replace($baseurl_short, "/", $_SERVER["REQUEST_URI"])) : "/c=" . $collection["ref"] . "&k=" . $key)));
redirect($url);
exit();
}
}
$sql = "UPDATE external_access_keys SET lastused = NOW() WHERE collection = ? AND access_key = ?";
if (in_array($collection["ref"], array_column($keyinfo, "collection")) && (bool)$keyinfo[0]["upload"] === true) {
// External upload link -set session to use for creating temporary collection
$shareopts = array(
"collection" => $collection["ref"],
"usergroup" => $keyinfo[0]["usergroup"],
"user" => $keyinfo[0]["user"],
);
upload_share_setup($key, $shareopts);
return true;
}
foreach ($collections as $collection_ref) {
$resources_alt = hook("GetResourcesToCheck", "", array($collection));
$resources = (is_array($resources_alt) && !empty($resources_alt) ? $resources_alt : get_collection_resources($collection_ref));
if (!check_access_key($resources, $key, false)) {
return false;
}
ps_query($sql, array("i", $collection_ref, "s", $key));
}
if ($is_featured_collection_category) {
// Update the last used for the dummy record we have for the featured collection category (ie. no resources since
// a category contains only collections)
ps_query($sql, array("i", $collection["ref"], "s", $key));
}
return true;
}
/**
* Generates a unique username for the given name
*
* @param string $name The user's full name
* @param string $email Optional email address
*
* @return string The username to use
*/
function make_username(string $name, string $email = ""): string
{
if ($GLOBALS["username_from_email"] && trim($email) !== "") {
if (filter_var($email, FILTER_VALIDATE_EMAIL)) {
$c = ps_value("SELECT COUNT(*) value FROM user WHERE username = ?", ["s",$email], 0);
if ($c === 0) {
return trim($email);
}
}
debug("make_username() - Unable to use invalid e-mail address: " . $email);
}
# First compress the various name parts
$s = trim_array(explode(" ", $name));
$name = $s[count($s) - 1];
for ($n = count($s) - 2; $n >= 0; $n--) {
$name = substr($s[$n], 0, 1) . $name;
}
$name = safe_file_name(strtolower($name));
# Create fullname usernames:
global $user_account_fullname_create;
if ($user_account_fullname_create) {
$name = '';
foreach ($s as $name_part) {
$name .= '_' . $name_part;
}
$name = substr($name, 1);
$name = safe_file_name($name);
}
# Check for uniqueness... append an ever-increasing number until unique.
$unique = false;
$num = -1;
while (!$unique) {
$num++;
$c = ps_value("select count(*) value from user where username=?", array("s",($name . (($num == 0) ? "" : $num))), 0);
$unique = ($c == 0);
}
return $name . (($num == 0) ? "" : $num);
}
/**
* Returns a list of user groups selectable in the registration . The standard user groups are translated using $lang. Custom user groups are i18n translated.
*
* @return array
*/
function get_registration_selectable_usergroups()
{
# Executes query.
$r = ps_query("select ref,name from usergroup where allow_registration_selection=1 order by name");
# Translates group names in the newly created array.
$return = array();
for ($n = 0; $n < count($r); $n++) {
$r[$n]["name"] = lang_or_i18n_get_translated($r[$n]["name"], "usergroup-");
$return[] = $r[$n]; # Adds to return array.
}
return $return;
}
/**
* Give the user full access to the given resource. Used when approving requests.
*
* @param integer $user
* @param integer $resource
* @param string $expires
* @return boolean
*/
function open_access_to_user($user, $resource, $expires)
{
# Delete any existing custom access
ps_query("delete from resource_custom_access where user = ? and resource = ?", array("i", $user, "i", $resource));
# Insert new row
ps_query("insert into resource_custom_access (resource, access, user, user_expires) values (?, '0', ?, ?)", array("i", $resource, "i", $user, "s", $expires == "" ? null : $expires));
return true;
}
/**
* Give the user full access to the given resource. Used when approving requests.
*
* @param integer $group
* @param integer $resource
* @param string $expires
* @return boolean
*/
function open_access_to_group($group, $resource, $expires)
{
# Delete any existing custom access
ps_query("delete from resource_custom_access where usergroup = ? and resource = ?", array("i", $group, "i", $resource));
# Insert new row
ps_query("insert into resource_custom_access (resource, access, usergroup, user_expires) values (?, '0', ?, ?)", array("i", $resource, "i", $group, "s", $expires == "" ? null : $expires));
return true;
}
/**
* Grants open access to the user list for the specified resource.
*
* @param string $userlist
* @param integer $resource
* @param string $expires
* @return void
*/
function resolve_open_access($userlist, $resource, $expires)
{
global $open_internal_access,$lang;
$groupids = resolve_userlist_groups_smart($userlist);
debug("smart_groups: list=" . $groupids);
if ($groupids != '') {
$groupids = explode(",", $groupids);
foreach ($groupids as $group) {
open_access_to_group($group, $resource, $expires);
}
$userlist = remove_groups_smart_from_userlist($userlist);
}
if ($userlist != '') {
$userlist_array = explode(",", $userlist);
debug("smart_groups: userlist=" . $userlist);
foreach ($userlist_array as $option) {
#user
$userid = ps_value("select ref value from user where username=?", array("s",$option), "");
if ($userid != "") {
open_access_to_user($userid, $resource, $expires);
}
}
}
}
/**
* Remove any user-specific access granted by an 'approve'. Used when declining requests.
*
* @param integer $user
* @param integer $resource
* @return boolean
*/
function remove_access_to_user($user, $resource)
{
# Delete any existing custom access
ps_query("delete from resource_custom_access where user = ? and resource = ?", array("i", $user, "i", $resource));
return true;
}
/**
* Returns true if a user account exists with e-mail address $email
*
* @param string $email
* @return boolean
*/
function user_email_exists($email)
{
return ps_value("SELECT COUNT(*) value FROM user WHERE email LIKE ?", ["s", trim(strtolower($email))], 0) > 0;
}
/**
* Return an array of emails from a list of usernames and email addresses.
* with 'key_required' sibling array preserving the intent of internal/external sharing
*
* @param array $user_list
*
* @return array
*/
function resolve_user_emails($user_list)
{
global $lang, $user_select_internal;
$emails_key_required = array();
foreach ($user_list as $user) {
$email_details = ps_query("SELECT ref, email, approved, account_expires FROM user WHERE username = ?", ['s', $user]);
if (
isset($email_details[0])
&& !(is_null($email_details[0]['account_expires']) || trim($email_details[0]['account_expires']) === '')
&& (time() > strtotime($email_details[0]['account_expires']))
) {
debug('EMAIL: ' . __FUNCTION__ . '() Username ' . $user . ' skipped as their user account has expired.');
continue;
}
// Not a recognised user, if @ sign present, assume e-mail address specified
if (0 === count($email_details)) {
if (false === strpos($user, '@') || (isset($user_select_internal) && $user_select_internal)) {
error_alert(escape("{$lang['couldnotmatchallusernames']}: {$user}"));
die();
}
$emails_key_required['unames'][] = $user;
$emails_key_required['emails'][] = $user;
$emails_key_required['key_required'][] = true;
continue;
}
// Skip internal, not approved/disabled accounts
if ($email_details[0]['approved'] != 1) {
debug('EMAIL: ' . __FUNCTION__ . '() skipping e-mail "' . $email_details[0]['email'] . '" because it belongs to user account which is not approved');
continue;
}
if (!filter_var($email_details[0]['email'], FILTER_VALIDATE_EMAIL)) {
debug("Skipping invalid e-mail address: " . $email_details[0]['email']);
continue;
}
// Internal unexpired approved user account - add e-mail address from user account
$emails_key_required['unames'][] = $user;
$emails_key_required['emails'][] = $email_details[0]['email'];
$emails_key_required['refs'][] = $email_details[0]['ref'];
$emails_key_required['key_required'][] = false;
}
return $emails_key_required;
}
/**
* Finds all users with matching email and marks them as having an invalid email
*
* @param string $email
* @return boolean
*/
function mark_email_as_invalid(string $email)
{
if ($email == "") {
return false;
}
$users = get_user_by_email($email);
$matched_user = false;
foreach ($users as $user) {
if (strtolower($email) == strtolower($user["email"])) {
$matched_user = true;
ps_query("UPDATE user SET email_invalid = 1 WHERE ref = ?", ["i",$user["ref"]]);
}
}
return $matched_user;
}
/**
* Checks if the email entered is marked as invalid for any users
*
* @param string $email
* @return boolean true if email is marked invalid for any users with matching email address
*/
function check_email_invalid(string $email)
{
if ($email == "") {
return false;
}
$users = get_user_by_email($email);
$email_invalid = false;
foreach ($users as $user) {
# Check email is exact match
if (strtolower($email) == strtolower($user["email"]) && $user["email_invalid"] == 1) {
$email_invalid = true;
}
}
return $email_invalid;
}
/**
* Creates a reset key for password reset e-mails
*
* @param string $username The user's username
* @return string The reset key
*/
function create_password_reset_key($username)
{
global $scramble_key;
$resetuniquecode = make_password();
$password_reset_hash = hash('sha256', date("Ymd") . md5("RS" . $resetuniquecode . $username . $scramble_key));
ps_query("update user set password_reset_hash = ? where username = ?", array("s", $password_reset_hash, "s", $username));
return substr(hash('sha256', date("Ymd") . $password_reset_hash . $username . $scramble_key), 0, 15);
}
/**
* For anonymous access - a unique session key to identify the user (e.g. so they can still have their own collections)
*
* @param boolean $create Create one if it doesn't already exist
* @return mixed False on failure, the key on success
*/
function get_rs_session_id($create = false)
{
global $baseurl, $anonymous_login, $usergroup, $rs_session;
// Note this is not a PHP session, we are using this to create an ID so we can distinguish between anonymous users or users accessing external upload links
$existing_session = (string) $rs_session !== "" ? $rs_session : ($_COOKIE["rs_session"] ?? "");
if ($existing_session != "") {
if (!headers_sent()) {
rs_setcookie("rs_session", $existing_session, 7, "", "", substr($baseurl, 0, 5) == "https", true); // extend the life of the cookie
}
return $existing_session;
}
if ($create) {
// Create a new ID - numeric values only so we can search for it easily
$rs_session = rand();
global $baseurl;
if (!headers_sent()) {
rs_setcookie("rs_session", $rs_session, 7, "", "", substr($baseurl, 0, 5) == "https", true);
}
if (!upload_share_active()) {
if (is_array($anonymous_login)) {
foreach ($anonymous_login as $key => $val) {
if ($baseurl == $key) {
$anonymous_login = $val;
}
}
}
$valid = ps_query("select ref, usergroup, account_expires from user where username = ?", array("s", $anonymous_login));
if (count($valid) >= 1) {
// setup_user hasn't been called yet, we just need the usergroup
$usergroup = $valid[0]["usergroup"];
// Log this in the daily stats
daily_stat("User session", $valid[0]["ref"]);
}
}
return $rs_session;
}
return false;
}
/**
* Returns an array of users (refs and emails) for use when sending email notifications (messages that in the past went
* to $email_notify, which can be emulated by using $email_notify_usergroups)
*
* Can be passed a specific user type or an array of permissions
* Types supported:-
* SYSTEM_ADMIN
* RESOURCE_ACCESS
* RESEARCH_ADMIN
* USER_ADMIN
* RESOURCE_ADMIN
*
* @param string $userpermission Permission string
* @param int|null $usergroup Optional id of usergroup to find notification users for e.g. the parent group of
* new user or as defined in $usergroup_approval_mappings
*
* @return array
*/
function get_notification_users($userpermission = "SYSTEM_ADMIN", $usergroup = null)
{
global $notification_users_cache, $email_notify_usergroups;
if (is_null($usergroup)) {
global $usergroup;
}
$userpermissionindex = is_array($userpermission) ? implode("_", $userpermission) : $userpermission;
if (isset($notification_users_cache[$userpermissionindex])) {
return $notification_users_cache[$userpermissionindex];
}
if (is_array($email_notify_usergroups) && count($email_notify_usergroups) > 0) {
// If email_notify_usergroups is set we use these over everything else, as long as they have an email address set
$notification_users_cache[$userpermissionindex] = ps_query("select ref, email, lang, usergroup from user where usergroup in (" . ps_param_insert(count($email_notify_usergroups)) . ") and email <> '' AND approved = 1 AND (account_expires IS NULL OR account_expires > NOW())", ps_param_fill($email_notify_usergroups, "i"));
return $notification_users_cache[$userpermissionindex];
}
if (!is_array($userpermission)) {
// We have been passed a specific type of administrator to find
switch ($userpermission) {
case "USER_ADMIN":
$sql_approver_groups = '';
$sql_approver_groups_params = array();
global $usergroup_approval_mappings;
if (is_numeric($usergroup)) {
$sql_approver_groups = " and ((FIND_IN_SET(BINARY 'U', ug.permissions) = 0 OR (FIND_IN_SET('permissions', ug.inherit_flags) <> 0 AND FIND_IN_SET(BINARY 'U', pg.permissions) = 0)) or ug.ref = (select parent from usergroup where ref = ?)) ";
$sql_approver_groups_params = array("i", $usergroup);
if (isset($usergroup_approval_mappings)) {
// Determine which user groups should be excluded from notifications. If mapping exists it must be valid to send notification.
$approver_groups = array_keys($usergroup_approval_mappings);
$defined_approvers_for_group = get_usergroup_approvers($usergroup);
$affective_approver_groups = array_diff($approver_groups, $defined_approvers_for_group);
if (count($affective_approver_groups) > 0) {
$sql_approver_groups .= 'and ug.ref not in (' . ps_param_insert(count($affective_approver_groups)) . ')';
$sql_approver_groups_params = array_merge($sql_approver_groups_params, ps_param_fill($affective_approver_groups, "i"));
}
}
}
// Return all users in groups with u permissions AND either no 'U' restriction, or with 'U' but in appropriate group
$notification_users_cache[$userpermissionindex] = ps_query("select u.ref, u.email, u.lang, u.usergroup from usergroup ug join user u on u.usergroup = ug.ref left join usergroup pg on ug.parent = pg.ref where (FIND_IN_SET(BINARY 'u', ug.permissions) <> 0 OR (FIND_IN_SET('permissions', ug.inherit_flags) <> 0 AND FIND_IN_SET(BINARY 'u', pg.permissions) <> 0)) and u.ref <> '' and u.approved = 1 AND (u.account_expires IS NULL OR u.account_expires > NOW())" . $sql_approver_groups, $sql_approver_groups_params);
return $notification_users_cache[$userpermissionindex];
break;
case "RESOURCE_ACCESS":
// Notify users who can grant access to resources, get all users in groups with R permissions without Rb permissions
$notification_users_cache[$userpermissionindex] =
ps_query("select u.ref, u.email, u.usergroup from usergroup ug
join user u on u.usergroup=ug.ref
left join usergroup pg on ug.parent = pg.ref
where (FIND_IN_SET('permissions', coalesce(ug.inherit_flags,'')) = 0
AND FIND_IN_SET(BINARY 'R', ug.permissions) <> 0
AND FIND_IN_SET(BINARY 'Rb', ug.permissions) = 0)
OR (FIND_IN_SET('permissions', coalesce(ug.inherit_flags,'')) <> 0
AND FIND_IN_SET(BINARY 'R', pg.permissions) <> 0
AND FIND_IN_SET(BINARY 'Rb', pg.permissions) = 0)
AND u.approved=1 AND (u.account_expires IS NULL OR u.account_expires > NOW())");
return $notification_users_cache[$userpermissionindex];
break;
case "RESEARCH_ADMIN":
// Notify research admins, get all users in groups with r permissions
$notification_users_cache[$userpermissionindex] = ps_query("select u.ref, u.email, u.usergroup from usergroup ug join user u on u.usergroup=ug.ref left join usergroup pg on ug.parent = pg.ref where (FIND_IN_SET(BINARY 'r', ug.permissions) <> 0 OR (FIND_IN_SET('permissions', ug.inherit_flags) <> 0 AND FIND_IN_SET(BINARY 'r', pg.permissions) <> 0)) AND u.approved=1 AND (u.account_expires IS NULL OR u.account_expires > NOW())");
return $notification_users_cache[$userpermissionindex];
break;
case "RESOURCE_ADMIN":
// Get all users in groups with t and e0 permissions
$notification_users_cache[$userpermissionindex] = ps_query("select u.ref, u.email, u.usergroup from usergroup ug join user u on u.usergroup=ug.ref left join usergroup pg on ug.parent = pg.ref where (FIND_IN_SET(BINARY 't', ug.permissions) <> 0 OR (FIND_IN_SET('permissions', ug.inherit_flags) <> 0 AND FIND_IN_SET(BINARY 't', pg.permissions) <> 0)) AND (FIND_IN_SET(BINARY 'e0', ug.permissions) OR (FIND_IN_SET('permissions', ug.inherit_flags) <> 0 AND FIND_IN_SET(BINARY 'e0', pg.permissions))) and u.approved=1 AND (u.account_expires IS NULL OR u.account_expires > NOW())");
return $notification_users_cache[$userpermissionindex];
break;
case "SYSTEM_ADMIN":
default:
// Get all users in groups with a permission (default if incorrect admin type has been passed)
$notification_users_cache[$userpermissionindex] = ps_query("select u.ref, u.email, u.usergroup from usergroup ug join user u on u.usergroup=ug.ref left join usergroup pg on ug.parent = pg.ref where (FIND_IN_SET(BINARY 'a', ug.permissions) <> 0 OR (FIND_IN_SET('permissions', ug.inherit_flags) <> 0 AND FIND_IN_SET(BINARY 'a', pg.permissions) <> 0)) AND u.approved=1 AND (u.account_expires IS NULL OR u.account_expires > NOW())");
return $notification_users_cache[$userpermissionindex];
break;
}
} else {
// An array has been passed, find all users with these permissions
$condition = "";
$condition_sql_params = array();
foreach ($userpermission as $permission) {
if ($condition != "") {
$condition .= " and ";
}
$condition .= "find_in_set(binary ?, ug.permissions) <> 0 AND u.approved = 1 AND (u.account_expires IS NULL OR u.account_expires > NOW())";
$condition_sql_params = array_merge($condition_sql_params, array("s", $permission));
}
$notification_users_cache[$userpermissionindex] = ps_query("select u.ref, u.email, u.usergroup from usergroup ug join user u on u.usergroup = ug.ref where $condition", $condition_sql_params);
return $notification_users_cache[$userpermissionindex];
}
}
/**
* Validates the user entered antispam code
*
* @param string $spamcode The antispam hash to check against
* @param string $usercode The antispam code the user entered
* @param string $spamtime The antispam timestamp
* @return boolean Return true if the code was successfully validated, otherwise false
*/
function verify_antispam($spamcode = "", $usercode = "", $spamtime = 0)
{
global $scramble_key, $password_brute_force_delay;
$data_valid = ($usercode !== '' || $spamcode !== '' || (int) $spamtime > 0);
$honeypot_intact = (isset($_REQUEST['antispam_user_code']) && $_REQUEST['antispam_user_code'] === '');
$antispam_code_valid = ($spamcode === hash("SHA256", strtoupper($usercode) . $scramble_key . $spamtime));
$previous_hash = in_array(
md5($usercode . $spamtime),
ps_array("SELECT unique_hash AS `value` FROM user WHERE unique_hash IS NOT null", array(), '')
);
if ($data_valid && $honeypot_intact && $antispam_code_valid && !$previous_hash) {
return true;
}
if (!$antispam_code_valid) {
$dbgr = 'invalid code entered';
} elseif ($previous_hash) {
$dbgr = 'code has previously been used';
} else {
$dbgr = 'unknown';
}
debug(sprintf('antispam failed: Reason: %s. IP: %s', $dbgr, get_ip()));
sleep($password_brute_force_delay);
return false;
}
/**
* Check that access for given external share key is correct
*
* @param array $key External access key
* @param string $password Share password to check
* @param string $cookie Share session cookie that has been set previously
*
* @return boolean
*/
function check_share_password($key, $password, $cookie)
{
global $scramble_key, $baseurl;
$sharehash = ps_value("SELECT password_hash value FROM external_access_keys WHERE access_key=?", array("s",$key), "");
if ($password != "") {
$hashcheck = hash('sha256', $key . $password . $scramble_key);
$valid = $hashcheck === $sharehash;
debug("checking share access password for key: " . $key);
} else {
$hashcheck = hash('sha256', date("Ymd") . $key . $sharehash . $scramble_key);
$valid = $hashcheck === $cookie;
debug("checking share access cookie for key: " . $key);
}
if (!$valid) {
debug("failed share access password for key: " . $key);
return false;
}
if ($cookie == "") {
// Set a cookie for this session so password won't be required again
$sharecookie = hash('sha256', date("Ymd") . $key . $sharehash . $scramble_key);
rs_setcookie("share_access", $sharecookie, 0, "", "", substr($baseurl, 0, 5) == "https", true);
}
return true;
}
/**
* Offset a datetime to user local time zone
*
* IMPORTANT: the offset is fixed, there is no calculation for summertime!
*
* @param string $datetime A date/time string. @see https://www.php.net/manual/en/datetime.formats.php
* @param string $format The format of the outputted date string. @see https://www.php.net/manual/en/function.date.php
*
* @return string The date in the specified format
*/
function offset_user_local_timezone($datetime, $format)
{
global $user_local_timezone;
$server_dtz = new DateTimeZone(date_default_timezone_get());
$user_local_dtz = new DateTimeZone($user_local_timezone);
// Create two DateTime objects that will contain the same Unix timestamp, but have different timezones attached to them
$server_dt = new DateTime($datetime, $server_dtz);
$user_local_dt = new DateTime($datetime, $user_local_dtz);
$time_offset = $user_local_dt->getOffset() - $server_dt->getOffset();
$user_local_dt->add(DateInterval::createFromDateString((string) $time_offset . ' seconds'));
return $user_local_dt->format($format);
}
/**
* Returns whether a user is anonymous or not
*
* @return boolean
*/
function checkPermission_anonymoususer()
{
global $baseurl, $anonymous_login, $username, $usergroup;
return isset($anonymous_login)
&& (
(is_string($anonymous_login) && '' != $anonymous_login && $anonymous_login == $username)
|| (
is_array($anonymous_login)
&& array_key_exists($baseurl, $anonymous_login)
&& $anonymous_login[$baseurl] == $username
)
);
}
/**
* Does the current user have the ability to administer the dash (the tiles for all users)
*
* @return boolean
*/
function checkPermission_dashadmin()
{
return (checkperm("h") && !checkperm("hdta")) || (checkperm("dta") && !checkperm("h"));
}
/**
* Can the user manage their own dash tiles.
*
* @return boolean
*/
function checkPermission_dashuser()
{
return !checkperm("dtu");
}
/**
* Can the user manage their dash?
*
* Logic:
* Home_dash is on, And not the Anonymous user with default dash, And (Dash tile user (Not with a managed dash) || Dash Tile Admin)
*
* @return boolean
*/
function checkPermission_dashmanage()
{
global $managed_home_dash;
return (!checkPermission_anonymoususer()) && (!$managed_home_dash && (checkPermission_dashuser() || checkPermission_dashadmin()));
}
/**
* Can the user create tiles?
*
* Logic:
* Home_dash is on, And not Anonymous use, And (Dash tile user (Not with a managed dash) || Dash Tile Admin)
*
* @return boolean
*/
function checkPermission_dashcreate()
{
global $managed_home_dash, $system_read_only;
return !checkPermission_anonymoususer()
&&
!$system_read_only
&&
(
(!$managed_home_dash && (checkPermission_dashuser() || checkPermission_dashadmin()))
||
($managed_home_dash && checkPermission_dashadmin())
);
}
/**
* Check that the user has the $perm permission
*
* @param string $perm
* @return boolean Do they have the permission?
*/
function checkperm($perm)
{
#
global $userpermissions;
if (!(isset($userpermissions))) {
return false;
}
return in_array($perm, $userpermissions);
}
/**
* Check if the current user is allowed to edit user with passed reference
*
* @param integer $user The user to be edited
* @return boolean
*/
function checkperm_user_edit($user)
{
if (!checkperm('u')) { // does not have edit user permission
return false;
}
if (!is_array($user)) { // allow for passing of user array or user ref to this function.
$user = get_user($user);
}
$editusergroup = $user['usergroup'];
global $usergroup;
$approver_groups = get_approver_usergroups($usergroup);
if ((!checkperm('U') && count($approver_groups) == 0) || $editusergroup == '') { // no user editing restriction, or is not defined so return true
return true;
}
// Get all the groups that the logged in user can manage
$sql = "SELECT `ref` AS 'value' FROM `usergroup` WHERE ";
$sql_params = array();
if (count($approver_groups) > 0) {
$sql .= "ref in (" . ps_param_insert(count($approver_groups)) . ") or ";
$sql_params = array_merge($sql_params, ps_param_fill($approver_groups, "i"));
}
$sql .= "`ref` = ? OR FIND_IN_SET(?, parent)";
$sql_params = array_merge($sql_params, array("i", $usergroup, "i", $usergroup));
$validgroups = ps_array($sql, $sql_params);
// Return true if the target user we are checking is in one of the valid groups
return in_array($editusergroup, $validgroups);
}
/**
* Check if the current user has sufficient permissions to log in as the specified user
*
* The regex used is to check if the a permission is present in the permission string of the target user
*
* @param mixed $user Either a user reference or user array
* @return bool
*/
function checkperm_login_as_user($user): bool
{
if (!checkperm('u')) { // does not have edit user permission
return false;
}
if (!is_array($user)) { // allow for passing of user array or user ref to this function.
$user = get_user($user);
}
if (!checkperm("a") && preg_match("/(?:^|\\W|,)a(?:$|\\W|,)/", $user['permissions'])) {
// Target user has 'a' permission but current user does not
return false;
}
return true;
}
/**
* Determine if this is an internal share access request
*
* @return boolean
*/
function internal_share_access()
{
global $k, $external_share_view_as_internal;
return $k != "" && $external_share_view_as_internal && is_authenticated();
}
/**
* Save changes to a usergroup or create usergroup
*
* @param int $ref Group ref. Set to 0 to create a new group
* @param array $groupoptions array of options to set for group in the form array("columnname" => $value)
*
* @return mixed bool|int True to indicate existing group has been updated or ID of newly created group
*/
function save_usergroup($ref, $groupoptions)
{
$validcolumns = array(
"name",
"permissions",
"parent",
"search_filter",
"search_filter_id",
"edit_filter",
"edit_filter_id",
"derestrict_filter",
"derestrict_filter_id",
"resource_defaults",
"config_options",
"welcome_message",
"ip_restrict",
"request_mode",
"allow_registration_selection",
"inherit_flags",
"download_limit",
"download_log_days"
);
$sqlcols = array();
$sqlvals = array();
$n = 0;
foreach ($validcolumns as $column) {
if (isset($groupoptions[$column])) {
$sqlcols[$n] = $column;
$sqlvals[$n] = $groupoptions[$column];
$n++;
}
}
if ($ref > 0) {
$sqlsetvals = array();
for ($n = 0; $n < count($sqlcols); $n++) {
$sqlsetvals[] = $sqlcols[$n] . " = ?";
}
$sql = "UPDATE usergroup SET " . implode(",", $sqlsetvals) . " WHERE ref = ?";
ps_query($sql, array_merge(ps_param_fill($sqlvals, "s"), array("i", (int) $ref)));
clear_query_cache('usergroup');
return true;
} else {
$sqlsetvals = array();
for ($n = 0; $n < count($sqlcols); $n++) {
$sqlsetvals[] = $sqlcols[$n] . " = ?";
}
$sql = "INSERT INTO usergroup (" . implode(",", $sqlcols) . ") VALUES (" . ps_param_insert(count($sqlvals)) . ")";
ps_query($sql, ps_param_fill($sqlvals, "s"));
clear_query_cache('usergroup');
return sql_insert_id();
}
}
/**
* Copy the permissions string from another usergroup
*
* @param int $src_id The group ID to copy from
* @param int $dst_id The group ID to copy to
* @return mixed bool|int True to indicate existing group has been updated or ID of newly created group
*/
function copy_usergroup_permissions(int $src_id, int $dst_id)
{
$src_group = get_usergroup($src_id);
$dst_group = get_usergroup($dst_id);
if (!$src_group || !$dst_group) {
return false;
}
$dst_group = ["permissions" => $src_group["permissions"]];
return save_usergroup($dst_id, $dst_group);
}
/**
* Set user's profile image and profile description (bio). Used by ../pages/user/user_profile_edit.php to setup user's profile.
*
* @param int $user_ref User id of user who's profile is being set.
* @param string $profile_text User entered profile description text (bio).
* @param string $image_path Path to temp file created if user chose to upload a profile image.
*
* @return boolean If an error is encountered saving the profile image return will be false.
*/
function set_user_profile($user_ref, $profile_text, $image_path)
{
global $storagedir,$imagemagick_path, $scramble_key, $config_windows;
# Check for presence of filestore/user_profiles directory - if it doesn't exist, create it.
if (!is_dir($storagedir . '/user_profiles')) {
mkdir($storagedir . '/user_profiles', 0777);
}
# Locate imagemagick.
$convert_fullpath = get_utility_path("im-convert");
if (!$convert_fullpath) {
debug("ERROR: Could not find ImageMagick 'convert' utility at location '$imagemagick_path'.");
return false;
}
if ($image_path != "" && file_exists($image_path)) {
# Work out the extension.
$extension = explode(".", $image_path);
$extension = trim(strtolower($extension[count($extension) - 1]));
if ($extension != 'jpg' && $extension != 'jpeg') {
return false;
}
# Remove previous profile image.
delete_profile_image($user_ref);
# Create profile image filename .
$profile_image_name = $user_ref . "_" . md5($scramble_key . $user_ref . time()) . "." . $extension;
$profile_image_path = $storagedir . '/user_profiles' . '/' . $profile_image_name;
# Create profile image - cropped to square from centre.
$command = $convert_fullpath . ' ' . escapeshellarg((!$config_windows && strpos($image_path, ':') !== false ? $extension . ':' : '') . $image_path) . " -resize 400x400 -thumbnail 200x200^^ -gravity center -extent 200x200" . " " . escapeshellarg($profile_image_path);
run_command($command);
# Store reference to user image.
ps_query("update user set profile_image = ? where ref = ?", array("s", $profile_image_name, "i", $user_ref));
# Remove temp file.
if (file_exists($profile_image_path)) {
unlink($image_path);
} else {
return false;
}
}
# Update user to set user.profile
ps_query("update user set profile_text = ? where ref = ?", array("s", substr(strip_tags($profile_text), 0, 500), "i", $user_ref));
return true;
}
/**
* Delete a user's profile image. This will first remove the file and then update the db to clear the existing value.
*
* @param mixed $user_ref User id of the user who's profile image is to be deleted.
*
* @return void
*/
function delete_profile_image($user_ref)
{
global $storagedir;
$profile_image_name = ps_value("select profile_image value from user where ref = ?", array("i",$user_ref), "");
if ($profile_image_name != "") {
$path_to_file = $storagedir . '/user_profiles' . '/' . $profile_image_name;
if (file_exists($path_to_file)) {
unlink($path_to_file);
}
ps_query("update user set profile_image = '' where ref = ?", array("i", $user_ref));
}
}
/**
* Generate the url to the user's profile image. Fetch the url by the user's id or by the profile image filename.
*
* @param int $user_ref User id of the user who's profile image is requested.
* @param string $by_image The filename of the profile image to fetch having been collected from the db separately: user.profile_image
*
* @return string The url to the user's profile image if available or blank if not set.
*/
function get_profile_image($user_ref = "", $by_image = "")
{
global $storagedir, $storageurl, $baseurl;
if (is_dir($storagedir . '/user_profiles')) {
# Only check the db if the profile image name has not been provided.
if ($by_image == "" && $user_ref != "") {
$profile_image_name = ps_value("select profile_image value from user where ref = ?", array("i",$user_ref), "");
} else {
$profile_image_name = $by_image;
}
if ($profile_image_name != "") {
return $storageurl . '/user_profiles/' . $profile_image_name;
} else {
return "";
}
}
return "";
}
/**
* Return user profile for a defined user.
*
* @param int $user_ref User id to fetch profile details for.
*
* @return string Profile details for the requested user.
*/
function get_profile_text($user_ref)
{
return ps_value("select profile_text value from user where ref = ?", array("i",$user_ref), "");
}
/**
* load language files for all users that need to be notified into an array - use for message and email notification
* load in default language strings first and then overwrite with preferred language strings
*
* @param array $languages - array of language strings
* @return array $language_strings_all
* */
function get_languages_notify_users(array $languages = array())
{
global $applicationname,$defaultlanguage;
$language_strings_all = array();
$lang_file_en = __DIR__ . "/../languages/en.php";
$lang_file_default = __DIR__ . "/../languages/" . safe_file_name($defaultlanguage) . ".php";
// add en and default language lang array values - always need en as some lang arrays do not contain all strings
include $lang_file_en;
$language_strings_all["en"] = $lang;
include $lang_file_default;
$language_strings_all[$defaultlanguage] = $lang;
// remove en and default language from array of languages to use
$langs2remove = array_unique(array("en", $defaultlanguage));
foreach ($langs2remove as $lang2remove) {
if (in_array($lang2remove, $languages)) {
unset($languages[$lang2remove]);
}
}
// load lang array values into array for each language
foreach ($languages as $language) {
$lang = array();
// include en and default lang array values
include $lang_file_en;
include $lang_file_default;
$lang_file = __DIR__ . "/../languages/" . safe_file_name($language) . ".php";
if (file_exists($lang_file)) {
include $lang_file;
}
$language_strings_all[$language] = $lang; // append $lang array
}
return $language_strings_all;
}
/**
* Generate upload URL - alters based on $upload_then_edit setting and external uploads
*
* @param string $collection - optional collection
* @param string $accesskey - used for external users
* @return string
*/
function get_upload_url($collection = "", $k = "")
{
global $upload_then_edit, $userref, $baseurl,$terms_upload;
if ($upload_then_edit || $k != "" || !isset($userref)) {
$url = generateURL($baseurl . "/pages/upload_batch.php", array("k" => $k,"collection_add" => $collection));
} elseif (isset($userref)) {
$url = generateURL($baseurl . "/pages/edit.php", array("ref" => "-" . $userref,"collection_add" => $collection));
}
if ($terms_upload && !check_upload_terms((int) $collection, $k)) {
$url = generateURL($baseurl . "/pages/terms.php", array("k" => $k,"collection" => $collection,"url" => $url,"upload" => true));
}
return $url;
}
/**
* Used to emulate system users when accessing system anonymously or via external shares
* Sets global array such as $userpermissions, $username and sets any relevant config options
*
* @param int $user User ID
* @param int $usergroup usergroup ID
* @return void
*/
function emulate_user($user, $usergroup = "")
{
debug_function_call("emulate_user", func_get_args());
global $userref, $userpermissions, $userrequestmode, $usersearchfilter, $userresourcedefaults;
global $external_share_groups_config_options, $emulate_plugins_set, $plugins;
global $username,$baseurl, $anonymous_login, $upload_link_workflow_state;
if (!is_numeric($user) || ($usergroup != "" && !is_numeric($usergroup))) {
return false;
}
$groupjoin = "u.usergroup = g.ref";
$permissionselect = "g.permissions";
$groupjoin_params = array();
if ($usergroup != "") {
# Select the user group from the access key instead.
$groupjoin = "g.ref = ? LEFT JOIN usergroup pg ON g.parent = pg.ref";
$groupjoin_params = array("i", $usergroup);
$permissionselect = "if(find_in_set('permissions', g.inherit_flags) AND pg.permissions IS NOT NULL, pg.permissions, g.permissions) permissions";
}
$userinfo = ps_query(
"select g.ref usergroup," . $permissionselect . " , g.search_filter, g.config_options, g.search_filter_id, g.derestrict_filter_id, u.search_filter_override,
u.search_filter_o_id, g.resource_defaults from user u join usergroup g on $groupjoin where u.ref = ?",
array_merge($groupjoin_params, array("i", $user))
);
if (count($userinfo) > 0) {
$usergroup = $userinfo[0]["usergroup"]; # Older mode, where no user group was specified, find the user group out from the table.
$userpermissions = explode(",", $userinfo[0]["permissions"] ?? "");
if (upload_share_active()) {
// Disable some permissions for added security
$addperms = array('D','b','p');
$removeperms = array('v','q','i','A','h','a','t','r','m','u','exup');
// add access to the designated workflow state
$addperms[] = "e" . $upload_link_workflow_state;
$userpermissions = array_merge($userpermissions, $addperms);
$userpermissions = array_diff($userpermissions, $removeperms);
$userpermissions = array_values($userpermissions);
$userref = $user;
}
if (isset($userinfo[0]["resource_defaults"])) {
$userresourcedefaults = $userinfo[0]["resource_defaults"];
}
if (isset($userinfo[0]["search_filter_o_id"]) && is_numeric($userinfo[0]["search_filter_o_id"]) && $userinfo[0]['search_filter_o_id'] > 0) {
// User search filter override
$usersearchfilter = $userinfo[0]["search_filter_o_id"];
} elseif (isset($userinfo[0]["search_filter_id"]) && is_numeric($userinfo[0]["search_filter_id"]) && $userinfo[0]['search_filter_id'] > 0) {
// Group search filter
$usersearchfilter = $userinfo[0]["search_filter_id"];
}
if (hook("modifyuserpermissions")) {
$userpermissions = hook("modifyuserpermissions");
}
$userrequestmode = 0; # Always use 'email' request mode for external users
# Load any plugins specific to the group of the sharing user, but only once as may be checking multiple keys
if ($emulate_plugins_set !== true) {
$enabled_plugins = (ps_query("SELECT name, enabled_groups, config, config_json FROM plugins WHERE inst_version >= 0 AND length(enabled_groups) > 0 ORDER BY priority"));
foreach ($enabled_plugins as $plugin) {
$s = explode(",", $plugin['enabled_groups']);
if (in_array($usergroup, $s) && !in_array($plugin['name'], $plugins)) {
include_plugin_config($plugin['name'], $plugin['config'], $plugin['config_json']);
register_plugin($plugin['name']);
$plugins[] = $plugin['name'];
}
}
foreach (array_reverse($plugins) as $plugin) {
register_plugin_language($plugin);
}
$emulate_plugins_set = true;
}
if ($external_share_groups_config_options || stripos(trim(isset($userinfo[0]["config_options"])), "external_share_groups_config_options=true") !== false) {
# Apply config override options
$config_options = trim($userinfo[0]["config_options"] ?? "");
// We need to get all globals as we don't know what may be referenced here
override_rs_variables_by_eval($GLOBALS, $config_options, 'usergroup');
}
}
# Special case for anonymous logins.
# When a valid key is present, we need to log the user in as the anonymous user so they will be able to browse the public links.
if (isset($anonymous_login)) {
if (is_array($anonymous_login)) {
foreach ($anonymous_login as $key => $val) {
if ($baseurl == $key) {
$anonymous_login = $val;
}
}
}
$username = $anonymous_login;
}
}
function is_authenticated()
{
global $is_authenticated;
return isset($is_authenticated) && $is_authenticated;
}
/**
* Returns an array of the user groups the supplied user group acts as an approver for.
* Uses config $usergroup_approval_mappings.
*
* @param int $usergroup Approving user group
*
* @return array Array of subordinate user group ids.
*/
function get_approver_usergroups($usergroup = "")
{
if ($usergroup == "" || !is_numeric($usergroup)) {
return array();
}
global $usergroup_approval_mappings;
$approval_groups = array();
if (
isset($usergroup_approval_mappings)
&& array_key_exists((int)$usergroup, $usergroup_approval_mappings)
) {
$approval_groups = $usergroup_approval_mappings[(int)$usergroup];
}
return $approval_groups;
}
/**
* Returns an array of user groups who act as user request approvers to the user group supplied.
* Uses config $usergroup_approval_mappings.
*
* @param int $usergroup Subordinate user group who's approval user group we need to find.
*
* @return array Approval user group ids for supplied user group. Likely one value but its possible to have multiple approving groups.
*/
function get_usergroup_approvers($usergroup = "")
{
if ($usergroup == "" || !is_numeric($usergroup)) {
return array();
}
global $usergroup_approval_mappings;
$approver_groups = array();
if (isset($usergroup_approval_mappings)) {
foreach ($usergroup_approval_mappings as $approver => $groups) {
if (in_array($usergroup, $groups)) {
$approver_groups[] = $approver;
}
}
}
return $approver_groups;
}
/**
* Retrieve all user records in groups with/without the specified permissions
*
* @param array $permissions array of permission strings to check
*
* @return array Matching user records (only returns a subset of columns)
*
* Note that this can't use a straight FIND_IN_SET for permissions since that is case insensitive
*
**/
function get_users_by_permission(array $permissions)
{
global $usergroup;
if (!(checkperm("a") || checkperm("u"))) {
return [];
}
$groupsql_filter = "";
$groupsql_params = [];
if (checkperm("U")) {
# Only return users in children groups to the user's group
$groupsql_filter = "WHERE (g.ref = ? or find_in_set(?, g.parent))";
$groupsql_params = array("i", $usergroup, "i", $usergroup);
}
$usergroups = ps_query(
"SELECT g.ref,
IF(FIND_IN_SET('permissions',g.inherit_flags) AND pg.permissions IS NOT NULL,pg.permissions,g.permissions) permissions
FROM usergroup g
LEFT JOIN usergroup AS pg ON g.parent=pg.ref " .
$groupsql_filter,
$groupsql_params
);
$validgroups = [];
foreach ($usergroups as $usergroup) {
$groupperms = explode(",", (string) $usergroup["permissions"]);
if (count(array_diff($permissions, $groupperms)) == 0) {
$validgroups[] = $usergroup["ref"];
}
}
if (count($validgroups) == 0) {
return [];
}
$r = ps_query("SELECT " . columns_in('user', 'u') . ", IF(FIND_IN_SET('permissions',g.inherit_flags) AND pg.permissions IS NOT NULL,pg.permissions,g.permissions) permissions, g.name groupname, g.ref groupref, g.parent groupparent FROM user u LEFT OUTER JOIN usergroup g ON u.usergroup = g.ref LEFT JOIN usergroup AS pg ON g.parent=pg.ref WHERE g.ref IN (" . ps_param_insert(count($validgroups)) . ") ORDER BY username", ps_param_fill($validgroups, "i"));
$return = [];
for ($n = 0; $n < count($r); $n++) {
# Translates group names in the newly created array.
$r[$n]["groupname"] = lang_or_i18n_get_translated($r[$n]["groupname"], "usergroup-");
$return[] = array_filter($r[$n], function ($k) {
return in_array($k, ["ref","username","fullname","email","groupname","usergroup","approved","comments","simplesaml_custom_attributes","origin","profile_image","profile_text","last_ip","account_expires","created","last_active"]);
}, ARRAY_FILTER_USE_KEY);
}
return $return;
}
/**
* Determine whether user is anonymous user
*/
function is_anonymous_user(): bool
{
global $anonymous_login, $username;
return isset($anonymous_login) && $username == $anonymous_login;
}
/**
* Retrieve all user records with the user preference specified
*
* @param string $preference Preference to check
* @param string $value Preference value to check for
*
* @return array Array of user refs with the preference set as specified
*
*
**/
function get_users_by_preference(string $preference, string $value): array
{
$sql = "SELECT up.user value
FROM user_preferences up
RIGHT JOIN user u
ON u.ref=up.user
WHERE u.approved=1
AND parameter = ?
AND value = ?";
$params = ["s",$preference,"s", $value];
return ps_array($sql, $params);
}
/**
* Get the default notification workflow states for the current user. Used by setup_user() and get_user_actions() if no user preference has been set
*
* @return array Array of workflow state references
*
*/
function get_default_notify_states(): array
{
$default_notify_states = [];
// Add action for users who can submit 'pending submission' resources for review
if (checkperm("e-2") && checkperm("e-1") && checkperm('d')) {
$default_notify_states[] = -2;
}
if (checkperm("e-1") && checkperm("e0")) {
// Add action for users who can make pending resources active
$default_notify_states[] = -1;
}
return $default_notify_states;
}
/**
* Generate a temporary download key for user. Used to enable temporary resource access to a file via download.php so that API can access resources after calling get_resource_path()
*
* @param int $user User ID
* @param int $resource Resource ID
* @param string $size Download size to access.
*
* @return string Access key - empty if not permitted
*/
function generate_temp_download_key(int $user, int $resource, string $size): string
{
if (!in_array($size, array('col', 'thm', 'pre')) && (($GLOBALS["userref"] != $user && !checkperm_user_edit($user)) || get_resource_access($resource) != 0)) {
return "";
}
$data = $user
. ":" . $resource
. ":" . $size
. ":" . time();
return base64_encode(
rsEncrypt($data, hash_hmac('sha256', 'dld_key', $GLOBALS['scramble_key']), 32)
);
}
/**
* Validate the provided download key to authenticate a download or override an access check.
*
* @param int $ref Resource ID
* @param string $keystring Key string - includes a nonce prefix
* @param string $size Download size to access.
* @param int $expire_seconds Optional parameter to set specified expiry time in seconds. Use 0 to set system default.
* @param bool $setup_user Set to false where there is no need to initialise the user.
*
* @return bool
*
*/
function validate_temp_download_key(int $ref, string $keystring, string $size, int $expire_seconds = 0, bool $setup_user = true): bool
{
if ($expire_seconds < 1) {
global $api_resource_path_expiry_hours;
$expiry_time_limit = 60 * 60 * $api_resource_path_expiry_hours;
} else {
$expiry_time_limit = $expire_seconds;
}
$decoded_keystring = mb_strpos($keystring, '@@', 0, 'UTF-8') !== false ? $keystring : base64_decode($keystring);
if (strlen($keystring) > 300) {
// Support older keys that used the combined scramble keys
$usekey = hash_hmac('sha512', 'dld_key', $GLOBALS['api_scramble_key'] . $GLOBALS['scramble_key']);
} else {
$usekey = hash_hmac('sha256', 'dld_key', $GLOBALS['scramble_key']);
}
$keydata = rsDecrypt($decoded_keystring, $usekey);
if ($keydata != false) {
$download_key_parts = explode(":", $keydata);
if (count($download_key_parts) == 6) {
// Support the old longer keys - multiple nonces are no longer used
array_shift($download_key_parts);
}
if ($download_key_parts[1] == $ref && $download_key_parts[2] == $size) {
$ak_user = $download_key_parts[0];
$ak_userdata = get_user($ak_user);
$key_time = $download_key_parts[3];
if (
$ak_userdata !== false
&& ((time() - $key_time) < $expiry_time_limit)
) {
if ($setup_user) {
setup_user($ak_userdata);
}
return true;
}
}
} else {
debug("Failed to decrypt temp_download_key");
}
return false;
}
/**
* Set up a dummy user with required permissions etc. to pass permission checks if running scripts from the command line
*
* @param array $options[]] Array of optional user options. Will default to generic system admin permissions if not set
* e.g.
* ["username" => "My Application",
* "permissions" => "h,v,e0",
* "groupname => "My Application",
* "resource_defaults => "region=EMEA",
* ]
*
* @return bool
*
*/
function setup_command_line_user(array $setoptions = []): bool
{
global $lang;
$defaultusername = $lang["system_user_default"];
// Set defaults, these can then be overidden by $setoptions
$dummyuserdata = [];
$dummyuserdata["ref"] = 0;
$dummyuserdata["username"] = $defaultusername;
$dummyuserdata["fullname"] = $defaultusername;
$dummyuserdata["groupname"] = $defaultusername;
# Command line user needs permission to update resources in all workflow states.
$dummyuserdata["permissions"] = 'c,a,t,v,e' . implode(',e', get_workflow_states());
$dummyuserdata["accepted_terms"] = 1;
$dummyuserdata["ip_restrict_user"] = "";
$dummyuserdata["ip_restrict_group"] = "";
$dummyuserdata["current_collection_valid"] = 1;
$dummyuserdata["usergroup"] = 0;
// Add any columns from user table, plus any extra array
// elements normally obtained from get_user()
$requiredelements = columns_in("user", null, null, true);
$requiredelements = array_merge($requiredelements, columns_in("usergroup", null, null, true));
foreach ($requiredelements as $requiredelement) {
if (!isset($dummyuserdata[$requiredelement])) {
$dummyuserdata[$requiredelement] = "";
}
}
// Override with any settings passed
foreach ($setoptions as $setoption => $setvalue) {
$dummyuserdata[$setoption] = $setvalue;
}
return setup_user($dummyuserdata);
}
/**
* Update user table to record access by a user
*
* @param int $user User ID
* @param array $set_values Optional array of column names and values to set
*/
function update_user_access(int $user = 0, array $set_values = []): bool
{
$user = $user > 0 ? $user : ($GLOBALS["userref"] ?? 0);
if ($user == 0) {
return false;
}
$validcolumns = [
"lang" => ["s",$GLOBALS["language"] ?? $GLOBALS["defaultlanguage"]],
"last_browser" => ["s",isset($_SERVER["HTTP_USER_AGENT"]) ? substr($_SERVER["HTTP_USER_AGENT"], 0, 250) : false],
"last_ip" => ["s",get_ip()],
"logged_in" => ["i",0],
"session" => ["s"],
"login_tries" => ["i"],
];
$col_sql = [];
$update_params = [];
foreach ($validcolumns as $column => $setparams) {
$setval = $set_values[$column] ?? ($setparams[1] ?? false);
if ($setval !== false) {
// Only update if a value has been passed or we have a default - so session is not accidentally wiped
$col_sql[] = $column . " = ?";
$update_params = array_merge(
$update_params,
[$setparams[0],$setval] // Override the default if passed
);
}
}
$update_sql = "UPDATE user SET last_active = NOW(), " . implode(",", $col_sql) . " WHERE ref = ?";
$update_params = array_merge($update_params, ["i",$user]);
ps_query($update_sql, $update_params, '', -1, true, 0);
return true;
}
/**
* Check if the user can manage users.
*
* @return boolean
*/
function checkPermission_manage_users(): bool
{
return checkperm('t') && checkperm('u');
}
/**
* Get the processing status message for the current user.
*
* @return false|array
*/
function get_processing_message()
{
global $userref,$userprocessing_messages;
if ($userprocessing_messages != "") {
ps_query("UPDATE user SET processing_messages='' WHERE ref=?", ["i",$userref]); // Clear out messages as now collected.
return explode(";;", $userprocessing_messages);
} else {
return false;
}
}
/**
* Set a new processing message for the current user.
*
* @param string $message The processing status message to add.
*
* @return void
*/
$set_processing_message_first_call = true;
function set_processing_message(string $message)
{
global $userref,$userprocessing_messages,$set_processing_message_first_call;
if (PHP_SAPI === "cli" || defined("API_CALL")) {
// Messages don't work unless using browser
return;
}
$userprocessing_messages = ps_value("SELECT processing_messages value FROM user WHERE ref=?", ["i",$userref], ''); // Fetch fresh from the DB as it may have been cleared by get_processing_message() since we started processing.
if ($set_processing_message_first_call || trim($message) === "") {
// Blank existing messages if present for first command this page load
// Passing an empty string should always clear out any messages
$userprocessing_messages = "";
$set_processing_message_first_call = false;
}
if ($userprocessing_messages != "") {
// Add delimiter
$userprocessing_messages .= ";;";
}
$userprocessing_messages .= $message;
ps_query("UPDATE user SET processing_messages=? WHERE ref=?", ["s",$userprocessing_messages,"i",$userref]);
}
/**
* Consider if the current user is able to escalate the permissions of a user to the level of a "super admin".
* Only users with "a" permission should be able to make other users super admins (user groups with "a" permission).
* Also used to determine if "super admin" level user groups should be displayed.
*
* @param int $new_usergroup ID of user group to be set
*/
function can_set_admin_usergroup(?int $new_usergroup): bool
{
if (is_null($new_usergroup)) {
# No usergroup supplied e.g. when creating new user account.
return true;
}
global $userpermissions;
if (in_array('a', $userpermissions)) {
return true;
}
global $can_set_admin_usergroup_perms_array;
if (!isset($can_set_admin_usergroup_perms_array)) {
# Get permissions once and store in global "cache" variable to avoid multiple queries to db as multiple groups maybe checked.
$usergroup_permissions = ps_query("SELECT ug.ref as `ref`, IF(FIND_IN_SET('permissions', ug.inherit_flags) AND pg.permissions IS NOT NULL, pg.permissions, ug.permissions) AS `permissions` FROM usergroup ug LEFT JOIN usergroup pg ON pg.ref = ug.parent;");
foreach ($usergroup_permissions as $usergroup_permission) {
$can_set_admin_usergroup_perms_array[$usergroup_permission['ref']] = $usergroup_permission['permissions'];
}
$GLOBALS['can_set_admin_usergroup_perms_array'] = $can_set_admin_usergroup_perms_array;
}
$new_usergroup_permissions = $can_set_admin_usergroup_perms_array[$new_usergroup];
$new_usergroup_permissions = explode(',', str_replace(' ', '', (string) $new_usergroup_permissions));
if (!in_array('a', $new_usergroup_permissions)) {
// New usergroup doesn't have 'a' permission.
return true;
}
return false;
}
/**
* Checks if the origin matches a whitelist entry, supporting wildcards like "*.example.com".
*
* @param string $origin The URL to check.
* @param array $whitelist Array of valid URLs - can include wildcards.
* @return bool True if the origin is allowed, false otherwise.
*/
function cors_is_origin_allowed(string $origin, array $whitelist): bool {
foreach ($whitelist as $allowed) {
// Escape dots and replace wildcard '*' with regex '.*'
$pattern = preg_quote($allowed, '/');
$pattern = str_replace('\*', '.*', $pattern); // Convert '*' to '.*' for wildcard matching
// Ensure it matches the entire string (^...$)
if (preg_match("/^{$pattern}$/i", $origin)) {
return true;
}
}
return false;
}
/**
* Delete a user group and associated records.
*
* @param int $usergroup_ref
*/
function delete_usergroup(int $usergroup_ref): bool
{
$dependant_user_count = ps_value("SELECT COUNT(*) AS `value` FROM user WHERE usergroup = ?", array("i", $usergroup_ref), 0);
$dependant_groups = ps_value("SELECT COUNT(*) AS `value` FROM usergroup WHERE parent = ?", array("i", $usergroup_ref), 0);
$has_dependants = $dependant_user_count + $dependant_groups > 0;
if ($has_dependants) {
return false;
}
ps_query("DELETE FROM usergroup WHERE ref = ?", array("i", $usergroup_ref));
log_activity('', LOG_CODE_DELETED, null, 'usergroup', null, $usergroup_ref);
# No need to keep any records of language content for this user group
ps_query('DELETE FROM site_text WHERE specific_to_group = ?', array("i", $usergroup_ref));
# Remove dash tiles related to deleted user group. Don't delete from dash_tile as they maybe in use elsewhere.
ps_query("DELETE FROM usergroup_dash_tile WHERE usergroup = ?", ['i', $usergroup_ref]);
clear_query_cache('usergroup');
return true;
}
/**
* Check that this is a real browser by executing JS to set an expected cookie.
*/
function browser_check()
{
global $browser_check_key, $applicationname, $disable_browser_check;
// Exceptions
if (PHP_SAPI == 'cli') {return;}
if (isset($disable_browser_check) && $disable_browser_check) {return;} // e.g. API/IIIF
if (!isset($_SERVER["HTTP_USER_AGENT"])) {exit();} // Terminate requests that do not specify a user agent
$question_key=hash_hmac("sha512", $_SERVER["HTTP_USER_AGENT"] . date('Ymd'), $browser_check_key);
$answer_key=xor_base64_encode($question_key);
// Look for the answer already set as a cookie
if (getval("browser_check_cookie","")==$answer_key) {return;} // We're good
// Output the JS to calculate the answer and set the cookie
?>
<html><title><?php echo escape($applicationname) ?></title><head>
<script>
function x9Zq(str){var a=[90,51,127],b='',c=0;for(var d=0;d<str.length;d++)b+=String.fromCharCode(str.charCodeAt(d)^a[c++%3]);return btoa(b);}
document.cookie = "browser_check_cookie=" + x9Zq(<?php echo json_encode($question_key) ?>) + "; path=/; max-age=172800";
setTimeout(function() {
window.location.reload(true);
}, 2000);
</script>
</head></html>
<?php
exit();
}
/**
* Obfuscates a string using a fixed XOR pattern and encodes it in Base64.
*
* This function performs a basic transformation by XOR-ing each character of the input
* with a repeating fixed byte pattern, then encodes the result in Base64.
* Designed to be mirrored easily in JavaScript for lightweight bot detection.
*
* @param string $str The input string to obfuscate.
* @return string The Base64-encoded, XOR-obfuscated string.
*/
function xor_base64_encode($str) {
$pattern = [0x5A, 0x33, 0x7F]; // Fixed XOR byte pattern
$out = '';
for ($i = 0; $i < strlen($str); $i++) {
$xor_byte = $pattern[$i % count($pattern)];
$out .= chr(ord($str[$i]) ^ $xor_byte);
}
return base64_encode($out);
}