. */ declare(strict_types=1); namespace Fisharebest\Webtrees\Services; use Closure; use Fisharebest\Webtrees\Auth; use Fisharebest\Webtrees\Contracts\UserInterface; use Fisharebest\Webtrees\DB; use Fisharebest\Webtrees\Http\RequestHandlers\ContactPage; use Fisharebest\Webtrees\Http\RequestHandlers\MessagePage; use Fisharebest\Webtrees\Individual; use Fisharebest\Webtrees\Registry; use Fisharebest\Webtrees\User; use Fisharebest\Webtrees\Validator; use Illuminate\Database\Query\Builder; use Illuminate\Database\Query\JoinClause; use Illuminate\Support\Collection; use Psr\Http\Message\ServerRequestInterface; use function max; use function time; /** * Functions for managing users. */ class UserService { /** * Find the user with a specified user_id. */ public function find(int|null $user_id): User|null { return Registry::cache()->array() ->remember('user-' . $user_id, static fn (): User|null => DB::table('user') ->where('user_id', '=', $user_id) ->get() ->map(User::rowMapper()) ->first()); } /** * Find the user with a specified email address. */ public function findByEmail(string $email): User|null { return DB::table('user') ->where('email', '=', $email) ->get() ->map(User::rowMapper()) ->first(); } /** * Find the user with a specified user_name or email address. */ public function findByIdentifier(string $identifier): User|null { return DB::table('user') ->where('user_name', '=', $identifier) ->orWhere('email', '=', $identifier) ->get() ->map(User::rowMapper()) ->first(); } /** * Find the user(s) with a specified genealogy record. * * @param Individual $individual * * @return Collection */ public function findByIndividual(Individual $individual): Collection { return DB::table('user') ->join('user_gedcom_setting', 'user_gedcom_setting.user_id', '=', 'user.user_id') ->where('gedcom_id', '=', $individual->tree()->id()) ->where('setting_value', '=', $individual->xref()) ->where('setting_name', '=', UserInterface::PREF_TREE_ACCOUNT_XREF) ->select(['user.*']) ->get() ->map(User::rowMapper()); } /** * Find the user with a specified password reset token. */ public function findByToken(string $token): User|null { return DB::table('user') ->join('user_setting AS us1', 'us1.user_id', '=', 'user.user_id') ->where('us1.setting_name', '=', 'password-token') ->where('us1.setting_value', '=', $token) ->join('user_setting AS us2', 'us2.user_id', '=', 'user.user_id') ->where('us2.setting_name', '=', 'password-token-expire') ->where('us2.setting_value', '>', time()) ->select(['user.*']) ->get() ->map(User::rowMapper()) ->first(); } /** * Find the user with a specified user_name. */ public function findByUserName(string $user_name): User|null { return DB::table('user') ->where('user_name', '=', $user_name) ->get() ->map(User::rowMapper()) ->first(); } /** * Callback to sort users by their last-login (or registration) time. * * @return Closure(UserInterface,UserInterface):int */ public function sortByLastLogin(): Closure { return static function (UserInterface $user1, UserInterface $user2) { $registered_at1 = (int) $user1->getPreference(UserInterface::PREF_TIMESTAMP_REGISTERED); $logged_in_at1 = (int) $user1->getPreference(UserInterface::PREF_TIMESTAMP_ACTIVE); $registered_at2 = (int) $user2->getPreference(UserInterface::PREF_TIMESTAMP_REGISTERED); $logged_in_at2 = (int) $user2->getPreference(UserInterface::PREF_TIMESTAMP_ACTIVE); return max($registered_at1, $logged_in_at1) <=> max($registered_at2, $logged_in_at2); }; } /** * Callback to filter users who have not logged in since a given time. * * @param int $timestamp * * @return Closure(UserInterface):bool */ public function filterInactive(int $timestamp): Closure { return static function (UserInterface $user) use ($timestamp): bool { $registered_at = (int) $user->getPreference(UserInterface::PREF_TIMESTAMP_REGISTERED); $logged_in_at = (int) $user->getPreference(UserInterface::PREF_TIMESTAMP_ACTIVE); return max($registered_at, $logged_in_at) < $timestamp; }; } /** * Get a list of all users. * * @return Collection */ public function all(): Collection { return DB::table('user') ->where('user_id', '>', 0) ->orderBy('real_name') ->get() ->map(User::rowMapper()); } /** * Get a list of all administrators. * * @return Collection */ public function administrators(): Collection { return DB::table('user') ->join('user_setting', 'user_setting.user_id', '=', 'user.user_id') ->where('user_setting.setting_name', '=', UserInterface::PREF_IS_ADMINISTRATOR) ->where('user_setting.setting_value', '=', '1') ->where('user.user_id', '>', 0) ->orderBy('real_name') ->select(['user.*']) ->get() ->map(User::rowMapper()); } /** * Get a list of all managers. * * @return Collection */ public function managers(): Collection { return DB::table('user') ->join('user_gedcom_setting', 'user_gedcom_setting.user_id', '=', 'user.user_id') ->where('user_gedcom_setting.setting_name', '=', UserInterface::PREF_TREE_ROLE) ->where('user_gedcom_setting.setting_value', '=', UserInterface::ROLE_MANAGER) ->where('user.user_id', '>', 0) ->orderBy('real_name') ->distinct() ->select(['user.*']) ->get() ->map(User::rowMapper()); } /** * Get a list of all moderators. * * @return Collection */ public function moderators(): Collection { return DB::table('user') ->join('user_gedcom_setting', 'user_gedcom_setting.user_id', '=', 'user.user_id') ->where('user_gedcom_setting.setting_name', '=', UserInterface::PREF_TREE_ROLE) ->where('user_gedcom_setting.setting_value', '=', UserInterface::ROLE_MODERATOR) ->where('user.user_id', '>', 0) ->orderBy('real_name') ->distinct() ->select(['user.*']) ->get() ->map(User::rowMapper()); } /** * Get a list of all verified users. * * @return Collection */ public function unapproved(): Collection { return DB::table('user') ->leftJoin('user_setting', static function (JoinClause $join): void { $join ->on('user_setting.user_id', '=', 'user.user_id') ->where('user_setting.setting_name', '=', UserInterface::PREF_IS_ACCOUNT_APPROVED); }) ->where(static function (Builder $query): void { $query ->where('user_setting.setting_value', '<>', '1') ->orWhereNull('user_setting.setting_value'); }) ->where('user.user_id', '>', 0) ->orderBy('real_name') ->select(['user.*']) ->get() ->map(User::rowMapper()); } /** * Get a list of all verified users. * * @return Collection */ public function unverified(): Collection { return DB::table('user') ->leftJoin('user_setting', static function (JoinClause $join): void { $join ->on('user_setting.user_id', '=', 'user.user_id') ->where('user_setting.setting_name', '=', UserInterface::PREF_IS_EMAIL_VERIFIED); }) ->where(static function (Builder $query): void { $query ->where('user_setting.setting_value', '<>', '1') ->orWhereNull('user_setting.setting_value'); }) ->where('user.user_id', '>', 0) ->orderBy('real_name') ->select(['user.*']) ->get() ->map(User::rowMapper()); } /** * Get a list of all users who are currently logged in. * * @return Collection */ public function allLoggedIn(): Collection { return DB::table('user') ->join('session', 'session.user_id', '=', 'user.user_id') ->where('user.user_id', '>', 0) ->orderBy('real_name') ->distinct() ->select(['user.*']) ->get() ->map(User::rowMapper()); } /** * Create a new user. * The calling code needs to check for duplicates identifiers before calling * this function. * * @param string $user_name * @param string $real_name * @param string $email * @param string $password * * @return User */ public function create(string $user_name, string $real_name, string $email, #[\SensitiveParameter] string $password): User { DB::table('user')->insert([ 'user_name' => $user_name, 'real_name' => $real_name, 'email' => $email, 'password' => password_hash($password, PASSWORD_DEFAULT), ]); $user_id = DB::lastInsertId(); return new User($user_id, $user_name, $real_name, $email); } /** * Delete a user * * @param User $user * * @return void */ public function delete(User $user): void { // Don't delete the logs, just set the user to null. DB::table('log') ->where('user_id', '=', $user->id()) ->update(['user_id' => null]); // Take over the user’s pending changes. (What else could we do with them?) DB::table('change') ->where('user_id', '=', $user->id()) ->where('status', '=', 'rejected') ->delete(); DB::table('change') ->where('user_id', '=', $user->id()) ->update(['user_id' => Auth::id()]); // Delete settings and preferences DB::table('block_setting') ->join('block', 'block_setting.block_id', '=', 'block.block_id') ->where('user_id', '=', $user->id()) ->delete(); DB::table('block')->where('user_id', '=', $user->id())->delete(); DB::table('user_gedcom_setting')->where('user_id', '=', $user->id())->delete(); DB::table('user_setting')->where('user_id', '=', $user->id())->delete(); DB::table('message')->where('user_id', '=', $user->id())->delete(); DB::table('user')->where('user_id', '=', $user->id())->delete(); } /** * @param User $contact_user * @param ServerRequestInterface $request * * @return string */ public function contactLink(User $contact_user, ServerRequestInterface $request): string { $tree = Validator::attributes($request)->tree(); $user = Validator::attributes($request)->user(); if ($contact_user->getPreference(UserInterface::PREF_CONTACT_METHOD) === MessageService::CONTACT_METHOD_MAILTO) { $url = 'mailto:' . $contact_user->email(); } elseif ($user instanceof User) { // Logged-in users send direct messages $url = route(MessagePage::class, [ 'to' => $contact_user->userName(), 'tree' => $tree->name(), 'url' => (string) $request->getUri(), ]); } else { // Visitors use the contact form. $url = route(ContactPage::class, [ 'to' => $contact_user->userName(), 'tree' => $tree->name(), 'url' => (string) $request->getUri(), ]); } return '' . e($contact_user->realName()) . ''; } }