. */ declare(strict_types=1); namespace Fisharebest\Webtrees\Services; use Closure; use Fisharebest\Webtrees\Auth; use Fisharebest\Webtrees\Carbon; use Fisharebest\Webtrees\Contracts\UserInterface; use Fisharebest\Webtrees\Registry; use Fisharebest\Webtrees\Http\RequestHandlers\ContactPage; use Fisharebest\Webtrees\Http\RequestHandlers\MessagePage; use Fisharebest\Webtrees\Individual; use Fisharebest\Webtrees\Tree; use Fisharebest\Webtrees\User; use Illuminate\Database\Capsule\Manager as DB; use Illuminate\Database\Query\Builder; use Illuminate\Database\Query\JoinClause; use Illuminate\Support\Collection; use Psr\Http\Message\ServerRequestInterface; use function assert; use function max; /** * Functions for managing users. */ class UserService { /** * Find the user with a specified user_id. * * @param int|null $user_id * * @return User|null */ public function find($user_id): ?User { return Registry::cache()->array()->remember('user-' . $user_id, static function () use ($user_id): ?User { return DB::table('user') ->where('user_id', '=', $user_id) ->get() ->map(User::rowMapper()) ->first(); }); } /** * Find the user with a specified email address. * * @param string $email * * @return User|null */ public function findByEmail($email): ?User { return DB::table('user') ->where('email', '=', $email) ->get() ->map(User::rowMapper()) ->first(); } /** * Find the user with a specified user_name or email address. * * @param string $identifier * * @return User|null */ public function findByIdentifier($identifier): ?User { 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', '=', User::PREF_TREE_ACCOUNT_XREF) ->select(['user.*']) ->get() ->map(User::rowMapper()); } /** * Find the user with a specified password reset token. * * @param string $token * * @return User|null */ public function findByToken(string $token): ?User { 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', '>', Carbon::now()->timestamp) ->select(['user.*']) ->get() ->map(User::rowMapper()) ->first(); } /** * Find the user with a specified user_name. * * @param string $user_name * * @return User|null */ public function findByUserName($user_name): ?User { 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 */ public function sortByLastLogin(): Closure { return static function (UserInterface $user1, UserInterface $user2) { $registered_at1 = (int) $user1->getPreference(User::PREF_TIMESTAMP_REGISTERED); $logged_in_at1 = (int) $user1->getPreference(User::PREF_TIMESTAMP_ACTIVE); $registered_at2 = (int) $user2->getPreference(User::PREF_TIMESTAMP_REGISTERED); $logged_in_at2 = (int) $user2->getPreference(User::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 */ public function filterInactive(int $timestamp): Closure { return static function (UserInterface $user) use ($timestamp): bool { $registered_at = (int) $user->getPreference(User::PREF_TIMESTAMP_REGISTERED); $logged_in_at = (int) $user->getPreference(User::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', '=', User::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', '=', User::PREF_TREE_ROLE) ->where('user_gedcom_setting.setting_value', '=', User::ROLE_MANAGER) ->where('user.user_id', '>', 0) ->groupBy(['user.user_id']) ->orderBy('real_name') ->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', '=', User::PREF_TREE_ROLE) ->where('user_gedcom_setting.setting_value', '=', User::ROLE_MODERATOR) ->where('user.user_id', '>', 0) ->groupBy(['user.user_id']) ->orderBy('real_name') ->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', '=', User::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', '=', User::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') ->select(['user.*']) ->distinct() ->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, 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 = (int) DB::connection()->getPdo()->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 = $request->getAttribute('tree'); assert($tree instanceof Tree); $user = $request->getAttribute('user'); if ($contact_user->getPreference(User::PREF_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()) . ''; } }