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'] = "" . $url . ""; $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("

"); $message->append_text("lang_name"); $message->append_text(": " . $templatevars['name'] . "

"); $message->append_text("lang_email"); $message->append_text(": " . $templatevars['email'] . "

"); $message->append_text("lang_comment"); $message->append_text(": " . $templatevars['userrequestcomment'] . "

"); $message->append_text("lang_ipaddress"); $message->append_text(": " . get_ip() . "

"); if (trim($customContents) != "") { $message->append_text($customContents . "

"); } $message->append_text("lang_userrequestnotification3"); $message->append_text("

" . $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("

"); $message->append_text("lang_name"); $message->append_text(": " . $name . "

"); $message->append_text("lang_email"); $message->append_text(": " . $email . "

"); $message->append_text($user_registration_opt_in_message . "

"); $message->append_text("lang_comment"); $message->append_text(": " . $userrequestcomment . "

"); $message->append_text("lang_ipaddress"); $message->append_text(": " . get_ip() . "

"); if (trim($customContents) != "") { $message->append_text($customContents . "

"); } // 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; ?> 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 ?> <?php echo escape($applicationname) ?>