display_name = $username; $user->level = $this->getDefaultLevel($username); $user->email = $this->getDefaultEmail($username); return $user; } public function getUser(string $username) : ?User { // Cache results for performance as getting user details in // most authentication types is expensive. static $users = array(); // Use array_key_exists() rather than isset() in case the value is NULL if (!array_key_exists($username, $users)) { // Check to see if this is the current user. If it is, then we // can save ourselves a potentially expensive operation. // But we can only do this if we are not being called by getCurrentUser(), which // some session schemes do, as otherwise we'll end up with an infinite recursion. // TODO: is there a better way of handling this?? $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); $backtrace_functions = array_column($backtrace, 'function'); if (!in_array('getCurrentUser', $backtrace_functions)) { $mrbs_user = session()->getCurrentUser(); } if (isset($mrbs_user) && ($mrbs_user->username === $username)) { $user = $mrbs_user; } else { $user = $this->getUserFresh($username); // Make sure we've got a sensible display name if (isset($user) && (!isset($user->display_name) || ($user->display_name === ''))) { $user->display_name = $user->username; } } $users[$username] = $user; } return $users[$username]; } /** * Returns a username given an email address. * * @return string|null The username (or the first found if two or more users share the same email address), or * NULL if no user is found. */ public function getUsernameByEmail(string $email) : ?string { global $mail_settings; // Default: return the email address, unless we're constructing email // addresses by adding a domain name onto the username. In which case, // strip off the domain name and then add on any necessary suffix. // This should be the inverse of getDefaultEmail(). $result = $email; if (isset($mail_settings['domain']) && ($mail_settings['domain'] !== '')) { $at_domain = '@' . self::trimDomain($mail_settings['domain']); if (str_ends_with($email, $at_domain)) { // Strip the @domain $result = str_replace($at_domain, '', $result); // And add on the suffix if there is one if (isset($mail_settings['username_suffix'])) { $result .= $mail_settings['username_suffix']; } } } return $result; } public function getDisplayName(?string $username) : ?string { global $get_display_names_all_at_once; static $display_names = null; // Cache for performance // Easy case 1: $username is null if (!isset($username)) { return null; } // Easy case 2: it's the current user $mrbs_user = session()->getCurrentUser(); if (isset($mrbs_user) && ($mrbs_user->username === $username)) { return $mrbs_user->display_name; } // If we can (and want to) then get all the usernames at the same time. It's // much faster than getting them one at a time when they are stored externally. // Check to see if $display_names is set before getting the usernames, so that // if getUsernames returns false we don't keep on trying it for every username // (the else block will set $display_names). if ($get_display_names_all_at_once && method_exists($this, 'getUsernames') && !isset($display_names) && (false !== ($usernames = $this->getUsernames()))) { $display_names = array_column($usernames, 'display_name', 'username'); } // Otherwise just get them one at a time else { if (!isset($display_names[$username])) { $user = $this->getUser($username); $display_names[$username] = (isset($user)) ? $user->display_name : $username; } } if (isset($display_names[$username]) && ($display_names[$username] !== '')) { return $display_names[$username]; } return $username; } /** * Checks whether the authentication type allows the creation of new users. * * This will normally return false if users are managed elsewhere (e.g. on * an external database, or on an LDAP server). */ public function canCreateUsers() : bool { return false; } /** * Checks whether validation of a user by email address is possible and allowed. */ public function canValidateByEmail() : bool { return false; } /** * Checks whether validation of a user by username is possible and allowed. */ public function canValidateByUsername() : bool { return true; } /** * Checks whether the method has a password reset facility */ public function canResetPassword() : bool { return false; } /** * Checks whether the password can be reset by supplying an email address */ public function canResetByEmail() : bool { return false; } /** * Validates that the password conforms to the password policy. * * Ideally this function should also be matched by client-side * validation, but unfortunately JavaScript's native support for Unicode * pattern matching is very limited. Would need to be implemented using * an add-in library. */ public function validatePassword( #[\SensitiveParameter] string $password) : bool { global $pwd_policy; if (isset($pwd_policy)) { // Set up regular expressions. Use p{Ll} instead of [a-z] etc. // to make sure accented characters are included $pattern = array('alpha' => '/\p{L}/', 'lower' => '/\p{Ll}/', 'upper' => '/\p{Lu}/', 'numeric' => '/\p{N}/', 'special' => '/[^\p{L}|\p{N}]/'); // Check for conformance to each rule foreach($pwd_policy as $rule => $value) { switch($rule) { case 'length': if (mb_strlen($password) < $value) { return false; } break; default: // turn on Unicode matching $pattern[$rule] .= 'u'; $n = preg_match_all($pattern[$rule], $password, $matches); if (($n === false) || ($n < $value)) { return false; } break; } } } // Everything is OK return true; } /** * Returns an array of registrants' display names * * @return string[] */ public function getRegistrantsDisplayNames (array $entry, bool $with_registered_by=false, bool $with_registrant_username=false) : array { $display_names = array(); // Only bother getting the names if we don't already know how many there are, // or if we know there are definitely some if (!isset($entry['n_registered']) || ($entry['n_registered'] > 0)) { $display_names = $this->getRegistrantsDisplayNamesUnsorted($entry['id'], $with_registered_by, $with_registrant_username); usort($display_names, 'MRBS\compare_display_names'); } return $display_names; } /** * @return string[] */ protected function getRegistrantsDisplayNamesUnsorted(int $id, bool $with_registered_by, bool $with_registrant_username) : array { $display_names = array(); $registrants = get_registrants($id, false); foreach ($registrants as $registrant) { $display_name = $this->getDisplayName($registrant['username']); // Add in the name of the person who registered this user, if required and if different. if ($with_registered_by && isset($registrant['create_by']) && ($registrant['create_by'] !== $registrant['username'])) { if ($with_registrant_username) { $display_names[] = get_vocab("registrant_username_and_registered_by", $registrant['username'], $display_name, $this->getDisplayName($registrant['create_by'])); } else { $display_names[] = get_vocab("registrant_registered_by", $display_name, $this->getDisplayName($registrant['create_by'])); } } else { $display_names[] = ($with_registrant_username) ? format_compound_name($registrant['username'], $display_name) : $display_name; } } return $display_names; } /** * Gets the level from the $auth['admin'] array in the config file */ protected function getDefaultLevel(?string $username) : int { global $auth, $max_level; // User not logged in, user level '0' if(!isset($username)) { return 0; } // Check whether the user is an admin; if not they are level 1. return (isset($auth['admin']) && in_arrayi($username, $auth['admin'])) ? $max_level : 1; } /** * Gets the default email address using config file settings */ protected function getDefaultEmail(?string $username) : string { global $mail_settings; if (!isset($username) || $username === '') { return ''; } $email = $username; // Remove the suffix, if there is one if (isset($mail_settings['username_suffix']) && ($mail_settings['username_suffix'] !== '')) { $suffix = $mail_settings['username_suffix']; if (substr($email, -strlen($suffix)) === $suffix) { $email = substr($email, 0, -strlen($suffix)); } } // Add on the domain, if there is one if (isset($mail_settings['domain']) && ($mail_settings['domain'] !== '')) { $email .= '@' . self::trimDomain($mail_settings['domain']); } return $email; } /** * Trim any leading '@' character. * * Older versions of MRBS required the '@' character to be included in $mail_settings['domain'], * and we still allow this for backwards compatibility. */ private static function trimDomain(string $domain) : string { return ltrim($domain, '@'); } /** * Callback function for comparing two users. * * Compares first by 'display_name' and then by 'username'. * * @param array{username: string, display_name: string} $user1 * @param array{username: string, display_name: string} $user2 */ private static function compareUsers(array $user1, array $user2) : int { $display_name1 = get_sortable_name($user1['display_name']); $display_name2 = get_sortable_name($user2['display_name']); // Provide fallbacks just in case the display names are NULL or empty $display_name1 = (isset($display_name1) && ($display_name1 !== '')) ? $display_name1 : $user1['username']; $display_name2 = (isset($display_name2) && ($display_name2 !== '')) ? $display_name2 : $user2['username']; $collator = new \Collator(Language::getInstance()->getWebLocale()); $collator->setStrength(\Collator::SECONDARY); // Case-insensitive, but accent-sensitive $display_name_comparison = $collator->compare($display_name1, $display_name2); if ($display_name_comparison === 0) { return $collator->compare($user1['username'], $user2['username']); } return $display_name_comparison; } /** * Sorts an array of users indexed by 'username' and 'display_name', eg the * output of getUsernames(). * * Sorts by display_name then username. * * @param array{array{username: string, display_name: string}} $users * @return void */ protected static function sortUsers(array &$users) : void { usort($users, [__CLASS__, 'compareUsers']); } /** * Check we've got the right session scheme for the type. * * To be called for those authentication types which require the same session scheme. * * @return void|never */ protected function checkSessionMatchesType() { global $auth; if ($auth['session'] !== $auth['type']) { $class = get_called_class(); $message = "MRBS configuration error: $class needs \$auth['session'] set to '" . $auth['type'] . "'"; die($message); } } /** * Writes the debug message to the error log together with the calling method and line number. * It assumes that it has been called by a debug method. */ protected static function logDebugMessage(string $message) : void { // Need to go three levels back to get the real calling method. list( , $called, $caller) = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3); error_log( "[MRBS DEBUG] " . $caller['class'] . $caller['type'] . $caller['function'] . '(' . $called['line'] . ')' . ": $message" ); } }