xref: /webtrees/app/Services/UserService.php (revision 4c96e13d7daf3e97fb6e9d448a6a4532bbd1b36b)
1e5a6b4d4SGreg Roach<?php
23976b470SGreg Roach
3e5a6b4d4SGreg Roach/**
4e5a6b4d4SGreg Roach * webtrees: online genealogy
5d11be702SGreg Roach * Copyright (C) 2023 webtrees development team
6e5a6b4d4SGreg Roach * This program is free software: you can redistribute it and/or modify
7e5a6b4d4SGreg Roach * it under the terms of the GNU General Public License as published by
8e5a6b4d4SGreg Roach * the Free Software Foundation, either version 3 of the License, or
9e5a6b4d4SGreg Roach * (at your option) any later version.
10e5a6b4d4SGreg Roach * This program is distributed in the hope that it will be useful,
11e5a6b4d4SGreg Roach * but WITHOUT ANY WARRANTY; without even the implied warranty of
12e5a6b4d4SGreg Roach * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13e5a6b4d4SGreg Roach * GNU General Public License for more details.
14e5a6b4d4SGreg Roach * You should have received a copy of the GNU General Public License
1589f7189bSGreg Roach * along with this program. If not, see <https://www.gnu.org/licenses/>.
16e5a6b4d4SGreg Roach */
17fcfa147eSGreg Roach
18e5a6b4d4SGreg Roachdeclare(strict_types=1);
19e5a6b4d4SGreg Roach
20e5a6b4d4SGreg Roachnamespace Fisharebest\Webtrees\Services;
21e5a6b4d4SGreg Roach
222474349cSGreg Roachuse Closure;
23e5a6b4d4SGreg Roachuse Fisharebest\Webtrees\Auth;
242474349cSGreg Roachuse Fisharebest\Webtrees\Contracts\UserInterface;
256f4ec3caSGreg Roachuse Fisharebest\Webtrees\DB;
26e381f98dSGreg Roachuse Fisharebest\Webtrees\Http\RequestHandlers\ContactPage;
27e381f98dSGreg Roachuse Fisharebest\Webtrees\Http\RequestHandlers\MessagePage;
28e5a6b4d4SGreg Roachuse Fisharebest\Webtrees\Individual;
29f0c88a96SGreg Roachuse Fisharebest\Webtrees\Registry;
30e5a6b4d4SGreg Roachuse Fisharebest\Webtrees\User;
31b55cbc6bSGreg Roachuse Fisharebest\Webtrees\Validator;
327c4add84SGreg Roachuse Illuminate\Database\Query\Builder;
337c4add84SGreg Roachuse Illuminate\Database\Query\JoinClause;
34e5a6b4d4SGreg Roachuse Illuminate\Support\Collection;
356ccdf4f0SGreg Roachuse Psr\Http\Message\ServerRequestInterface;
36f1d4b4a2SGreg Roach
372474349cSGreg Roachuse function max;
38d97083feSGreg Roachuse function time;
39e5a6b4d4SGreg Roach
40e5a6b4d4SGreg Roach/**
41e5a6b4d4SGreg Roach * Functions for managing users.
42e5a6b4d4SGreg Roach */
43e5a6b4d4SGreg Roachclass UserService
44e5a6b4d4SGreg Roach{
45e5a6b4d4SGreg Roach    /**
46e5a6b4d4SGreg Roach     * Find the user with a specified user_id.
47e5a6b4d4SGreg Roach     */
481ff45046SGreg Roach    public function find(int|null $user_id): User|null
49e5a6b4d4SGreg Roach    {
50f25fc0f9SGreg Roach        return Registry::cache()->array()
511ff45046SGreg Roach            ->remember('user-' . $user_id, static fn (): User|null => DB::table('user')
52e5a6b4d4SGreg Roach                ->where('user_id', '=', $user_id)
53e5a6b4d4SGreg Roach                ->get()
54e5a6b4d4SGreg Roach                ->map(User::rowMapper())
55f25fc0f9SGreg Roach                ->first());
56e5a6b4d4SGreg Roach    }
57e5a6b4d4SGreg Roach
58e5a6b4d4SGreg Roach    /**
59e5a6b4d4SGreg Roach     * Find the user with a specified email address.
60e5a6b4d4SGreg Roach     */
611ff45046SGreg Roach    public function findByEmail(string $email): User|null
62e5a6b4d4SGreg Roach    {
63e5a6b4d4SGreg Roach        return DB::table('user')
64e5a6b4d4SGreg Roach            ->where('email', '=', $email)
65e5a6b4d4SGreg Roach            ->get()
66e5a6b4d4SGreg Roach            ->map(User::rowMapper())
67e5a6b4d4SGreg Roach            ->first();
68e5a6b4d4SGreg Roach    }
69e5a6b4d4SGreg Roach
70e5a6b4d4SGreg Roach    /**
71e5a6b4d4SGreg Roach     * Find the user with a specified user_name or email address.
72e5a6b4d4SGreg Roach     */
731ff45046SGreg Roach    public function findByIdentifier(string $identifier): User|null
74e5a6b4d4SGreg Roach    {
75e5a6b4d4SGreg Roach        return DB::table('user')
76e5a6b4d4SGreg Roach            ->where('user_name', '=', $identifier)
77e5a6b4d4SGreg Roach            ->orWhere('email', '=', $identifier)
78e5a6b4d4SGreg Roach            ->get()
79e5a6b4d4SGreg Roach            ->map(User::rowMapper())
80e5a6b4d4SGreg Roach            ->first();
81e5a6b4d4SGreg Roach    }
82e5a6b4d4SGreg Roach
83e5a6b4d4SGreg Roach    /**
84e5a6b4d4SGreg Roach     * Find the user(s) with a specified genealogy record.
85e5a6b4d4SGreg Roach     *
86e5a6b4d4SGreg Roach     * @param Individual $individual
87e5a6b4d4SGreg Roach     *
8836779af1SGreg Roach     * @return Collection<int,User>
89e5a6b4d4SGreg Roach     */
90e5a6b4d4SGreg Roach    public function findByIndividual(Individual $individual): Collection
91e5a6b4d4SGreg Roach    {
92e5a6b4d4SGreg Roach        return DB::table('user')
93e5a6b4d4SGreg Roach            ->join('user_gedcom_setting', 'user_gedcom_setting.user_id', '=', 'user.user_id')
94e5a6b4d4SGreg Roach            ->where('gedcom_id', '=', $individual->tree()->id())
95e5a6b4d4SGreg Roach            ->where('setting_value', '=', $individual->xref())
961fe542e9SGreg Roach            ->where('setting_name', '=', UserInterface::PREF_TREE_ACCOUNT_XREF)
97e5a6b4d4SGreg Roach            ->select(['user.*'])
98e5a6b4d4SGreg Roach            ->get()
99e5a6b4d4SGreg Roach            ->map(User::rowMapper());
100e5a6b4d4SGreg Roach    }
101e5a6b4d4SGreg Roach
102e5a6b4d4SGreg Roach    /**
103a00bcc63SGreg Roach     * Find the user with a specified password reset token.
104a00bcc63SGreg Roach     */
1051ff45046SGreg Roach    public function findByToken(string $token): User|null
106a00bcc63SGreg Roach    {
107a00bcc63SGreg Roach        return DB::table('user')
108a00bcc63SGreg Roach            ->join('user_setting AS us1', 'us1.user_id', '=', 'user.user_id')
109a00bcc63SGreg Roach            ->where('us1.setting_name', '=', 'password-token')
110a00bcc63SGreg Roach            ->where('us1.setting_value', '=', $token)
111a00bcc63SGreg Roach            ->join('user_setting AS us2', 'us2.user_id', '=', 'user.user_id')
112a00bcc63SGreg Roach            ->where('us2.setting_name', '=', 'password-token-expire')
113d97083feSGreg Roach            ->where('us2.setting_value', '>', time())
114a00bcc63SGreg Roach            ->select(['user.*'])
115a00bcc63SGreg Roach            ->get()
116a00bcc63SGreg Roach            ->map(User::rowMapper())
117a00bcc63SGreg Roach            ->first();
118a00bcc63SGreg Roach    }
119a00bcc63SGreg Roach
120a00bcc63SGreg Roach    /**
121e5a6b4d4SGreg Roach     * Find the user with a specified user_name.
122e5a6b4d4SGreg Roach     */
1231ff45046SGreg Roach    public function findByUserName(string $user_name): User|null
124e5a6b4d4SGreg Roach    {
125e5a6b4d4SGreg Roach        return DB::table('user')
126e5a6b4d4SGreg Roach            ->where('user_name', '=', $user_name)
127e5a6b4d4SGreg Roach            ->get()
128e5a6b4d4SGreg Roach            ->map(User::rowMapper())
129e5a6b4d4SGreg Roach            ->first();
130e5a6b4d4SGreg Roach    }
131e5a6b4d4SGreg Roach
132e5a6b4d4SGreg Roach    /**
1332474349cSGreg Roach     * Callback to sort users by their last-login (or registration) time.
1342474349cSGreg Roach     *
135c6921a17SGreg Roach     * @return Closure(UserInterface,UserInterface):int
1362474349cSGreg Roach     */
1372474349cSGreg Roach    public function sortByLastLogin(): Closure
1382474349cSGreg Roach    {
1397c4add84SGreg Roach        return static function (UserInterface $user1, UserInterface $user2) {
1401fe542e9SGreg Roach            $registered_at1 = (int) $user1->getPreference(UserInterface::PREF_TIMESTAMP_REGISTERED);
1411fe542e9SGreg Roach            $logged_in_at1  = (int) $user1->getPreference(UserInterface::PREF_TIMESTAMP_ACTIVE);
1421fe542e9SGreg Roach            $registered_at2 = (int) $user2->getPreference(UserInterface::PREF_TIMESTAMP_REGISTERED);
1431fe542e9SGreg Roach            $logged_in_at2  = (int) $user2->getPreference(UserInterface::PREF_TIMESTAMP_ACTIVE);
1442474349cSGreg Roach
1452474349cSGreg Roach            return max($registered_at1, $logged_in_at1) <=> max($registered_at2, $logged_in_at2);
1462474349cSGreg Roach        };
1472474349cSGreg Roach    }
1482474349cSGreg Roach
1492474349cSGreg Roach    /**
1502474349cSGreg Roach     * Callback to filter users who have not logged in since a given time.
1512474349cSGreg Roach     *
1522474349cSGreg Roach     * @param int $timestamp
1532474349cSGreg Roach     *
154c6921a17SGreg Roach     * @return Closure(UserInterface):bool
1552474349cSGreg Roach     */
1562474349cSGreg Roach    public function filterInactive(int $timestamp): Closure
1572474349cSGreg Roach    {
1587c4add84SGreg Roach        return static function (UserInterface $user) use ($timestamp): bool {
1591fe542e9SGreg Roach            $registered_at = (int) $user->getPreference(UserInterface::PREF_TIMESTAMP_REGISTERED);
1601fe542e9SGreg Roach            $logged_in_at  = (int) $user->getPreference(UserInterface::PREF_TIMESTAMP_ACTIVE);
1612474349cSGreg Roach
1622474349cSGreg Roach            return max($registered_at, $logged_in_at) < $timestamp;
1632474349cSGreg Roach        };
1642474349cSGreg Roach    }
1652474349cSGreg Roach
1662474349cSGreg Roach    /**
167e5a6b4d4SGreg Roach     * Get a list of all users.
168e5a6b4d4SGreg Roach     *
16936779af1SGreg Roach     * @return Collection<int,User>
170e5a6b4d4SGreg Roach     */
171e5a6b4d4SGreg Roach    public function all(): Collection
172e5a6b4d4SGreg Roach    {
173e5a6b4d4SGreg Roach        return DB::table('user')
174e5a6b4d4SGreg Roach            ->where('user_id', '>', 0)
175e5a6b4d4SGreg Roach            ->orderBy('real_name')
176e5a6b4d4SGreg Roach            ->get()
177e5a6b4d4SGreg Roach            ->map(User::rowMapper());
178e5a6b4d4SGreg Roach    }
179e5a6b4d4SGreg Roach
180e5a6b4d4SGreg Roach    /**
181e5a6b4d4SGreg Roach     * Get a list of all administrators.
182e5a6b4d4SGreg Roach     *
18336779af1SGreg Roach     * @return Collection<int,User>
184e5a6b4d4SGreg Roach     */
185e5a6b4d4SGreg Roach    public function administrators(): Collection
186e5a6b4d4SGreg Roach    {
187e5a6b4d4SGreg Roach        return DB::table('user')
1881ab2f386SGreg Roach            ->join('user_setting', 'user_setting.user_id', '=', 'user.user_id')
1891fe542e9SGreg Roach            ->where('user_setting.setting_name', '=', UserInterface::PREF_IS_ADMINISTRATOR)
1901ab2f386SGreg Roach            ->where('user_setting.setting_value', '=', '1')
191e5a6b4d4SGreg Roach            ->where('user.user_id', '>', 0)
192e5a6b4d4SGreg Roach            ->orderBy('real_name')
193e5a6b4d4SGreg Roach            ->select(['user.*'])
194e5a6b4d4SGreg Roach            ->get()
195e5a6b4d4SGreg Roach            ->map(User::rowMapper());
196e5a6b4d4SGreg Roach    }
197e5a6b4d4SGreg Roach
198e5a6b4d4SGreg Roach    /**
199e5a6b4d4SGreg Roach     * Get a list of all managers.
200e5a6b4d4SGreg Roach     *
20136779af1SGreg Roach     * @return Collection<int,User>
202e5a6b4d4SGreg Roach     */
203e5a6b4d4SGreg Roach    public function managers(): Collection
204e5a6b4d4SGreg Roach    {
205e5a6b4d4SGreg Roach        return DB::table('user')
2061ab2f386SGreg Roach            ->join('user_gedcom_setting', 'user_gedcom_setting.user_id', '=', 'user.user_id')
2071fe542e9SGreg Roach            ->where('user_gedcom_setting.setting_name', '=', UserInterface::PREF_TREE_ROLE)
2081fe542e9SGreg Roach            ->where('user_gedcom_setting.setting_value', '=', UserInterface::ROLE_MANAGER)
209e5a6b4d4SGreg Roach            ->where('user.user_id', '>', 0)
210e5a6b4d4SGreg Roach            ->orderBy('real_name')
2110aed7de4SGreg Roach            ->distinct()
212e5a6b4d4SGreg Roach            ->select(['user.*'])
213e5a6b4d4SGreg Roach            ->get()
214e5a6b4d4SGreg Roach            ->map(User::rowMapper());
215e5a6b4d4SGreg Roach    }
216e5a6b4d4SGreg Roach
217e5a6b4d4SGreg Roach    /**
218e5a6b4d4SGreg Roach     * Get a list of all moderators.
219e5a6b4d4SGreg Roach     *
22036779af1SGreg Roach     * @return Collection<int,User>
221e5a6b4d4SGreg Roach     */
222e5a6b4d4SGreg Roach    public function moderators(): Collection
223e5a6b4d4SGreg Roach    {
224e5a6b4d4SGreg Roach        return DB::table('user')
2251ab2f386SGreg Roach            ->join('user_gedcom_setting', 'user_gedcom_setting.user_id', '=', 'user.user_id')
2261fe542e9SGreg Roach            ->where('user_gedcom_setting.setting_name', '=', UserInterface::PREF_TREE_ROLE)
2271fe542e9SGreg Roach            ->where('user_gedcom_setting.setting_value', '=', UserInterface::ROLE_MODERATOR)
228e5a6b4d4SGreg Roach            ->where('user.user_id', '>', 0)
229e5a6b4d4SGreg Roach            ->orderBy('real_name')
2300aed7de4SGreg Roach            ->distinct()
231e5a6b4d4SGreg Roach            ->select(['user.*'])
232e5a6b4d4SGreg Roach            ->get()
233e5a6b4d4SGreg Roach            ->map(User::rowMapper());
234e5a6b4d4SGreg Roach    }
235e5a6b4d4SGreg Roach
236e5a6b4d4SGreg Roach    /**
237e5a6b4d4SGreg Roach     * Get a list of all verified users.
238e5a6b4d4SGreg Roach     *
23936779af1SGreg Roach     * @return Collection<int,User>
240e5a6b4d4SGreg Roach     */
241e5a6b4d4SGreg Roach    public function unapproved(): Collection
242e5a6b4d4SGreg Roach    {
243e5a6b4d4SGreg Roach        return DB::table('user')
2447c4add84SGreg Roach            ->leftJoin('user_setting', static function (JoinClause $join): void {
2457c4add84SGreg Roach                $join
2467c4add84SGreg Roach                    ->on('user_setting.user_id', '=', 'user.user_id')
2471fe542e9SGreg Roach                    ->where('user_setting.setting_name', '=', UserInterface::PREF_IS_ACCOUNT_APPROVED);
2487c4add84SGreg Roach            })
2497c4add84SGreg Roach            ->where(static function (Builder $query): void {
2507c4add84SGreg Roach                $query
2511ab2f386SGreg Roach                    ->where('user_setting.setting_value', '<>', '1')
2527c4add84SGreg Roach                    ->orWhereNull('user_setting.setting_value');
2537c4add84SGreg Roach            })
254e5a6b4d4SGreg Roach            ->where('user.user_id', '>', 0)
255e5a6b4d4SGreg Roach            ->orderBy('real_name')
256090fa720SGreg Roach            ->select(['user.*'])
257e5a6b4d4SGreg Roach            ->get()
258e5a6b4d4SGreg Roach            ->map(User::rowMapper());
259e5a6b4d4SGreg Roach    }
260e5a6b4d4SGreg Roach
261e5a6b4d4SGreg Roach    /**
262e5a6b4d4SGreg Roach     * Get a list of all verified users.
263e5a6b4d4SGreg Roach     *
26436779af1SGreg Roach     * @return Collection<int,User>
265e5a6b4d4SGreg Roach     */
266e5a6b4d4SGreg Roach    public function unverified(): Collection
267e5a6b4d4SGreg Roach    {
268e5a6b4d4SGreg Roach        return DB::table('user')
2697c4add84SGreg Roach            ->leftJoin('user_setting', static function (JoinClause $join): void {
2707c4add84SGreg Roach                $join
2717c4add84SGreg Roach                    ->on('user_setting.user_id', '=', 'user.user_id')
2721fe542e9SGreg Roach                    ->where('user_setting.setting_name', '=', UserInterface::PREF_IS_EMAIL_VERIFIED);
2737c4add84SGreg Roach            })
2747c4add84SGreg Roach            ->where(static function (Builder $query): void {
2757c4add84SGreg Roach                $query
2761ab2f386SGreg Roach                    ->where('user_setting.setting_value', '<>', '1')
2777c4add84SGreg Roach                    ->orWhereNull('user_setting.setting_value');
2787c4add84SGreg Roach            })
279e5a6b4d4SGreg Roach            ->where('user.user_id', '>', 0)
280e5a6b4d4SGreg Roach            ->orderBy('real_name')
281090fa720SGreg Roach            ->select(['user.*'])
282e5a6b4d4SGreg Roach            ->get()
283e5a6b4d4SGreg Roach            ->map(User::rowMapper());
284e5a6b4d4SGreg Roach    }
285e5a6b4d4SGreg Roach
286e5a6b4d4SGreg Roach    /**
287e5a6b4d4SGreg Roach     * Get a list of all users who are currently logged in.
288e5a6b4d4SGreg Roach     *
28936779af1SGreg Roach     * @return Collection<int,User>
290e5a6b4d4SGreg Roach     */
291e5a6b4d4SGreg Roach    public function allLoggedIn(): Collection
292e5a6b4d4SGreg Roach    {
293e5a6b4d4SGreg Roach        return DB::table('user')
294e5a6b4d4SGreg Roach            ->join('session', 'session.user_id', '=', 'user.user_id')
295e5a6b4d4SGreg Roach            ->where('user.user_id', '>', 0)
296e5a6b4d4SGreg Roach            ->orderBy('real_name')
297e5a6b4d4SGreg Roach            ->distinct()
2980aed7de4SGreg Roach            ->select(['user.*'])
299e5a6b4d4SGreg Roach            ->get()
300e5a6b4d4SGreg Roach            ->map(User::rowMapper());
301e5a6b4d4SGreg Roach    }
302e5a6b4d4SGreg Roach
303e5a6b4d4SGreg Roach    /**
304e5a6b4d4SGreg Roach     * Create a new user.
305e5a6b4d4SGreg Roach     * The calling code needs to check for duplicates identifiers before calling
306e5a6b4d4SGreg Roach     * this function.
307e5a6b4d4SGreg Roach     *
308e5a6b4d4SGreg Roach     * @param string $user_name
309e5a6b4d4SGreg Roach     * @param string $real_name
310e5a6b4d4SGreg Roach     * @param string $email
311e5a6b4d4SGreg Roach     * @param string $password
312e5a6b4d4SGreg Roach     *
313e5a6b4d4SGreg Roach     * @return User
314e5a6b4d4SGreg Roach     */
315c5d4d20fSGreg Roach    public function create(string $user_name, string $real_name, string $email, #[\SensitiveParameter] string $password): User
316e5a6b4d4SGreg Roach    {
317e5a6b4d4SGreg Roach        DB::table('user')->insert([
318e5a6b4d4SGreg Roach            'user_name' => $user_name,
319e5a6b4d4SGreg Roach            'real_name' => $real_name,
320e5a6b4d4SGreg Roach            'email'     => $email,
321e5a6b4d4SGreg Roach            'password'  => password_hash($password, PASSWORD_DEFAULT),
322e5a6b4d4SGreg Roach        ]);
323e5a6b4d4SGreg Roach
324*4c96e13dSGreg Roach        $user_id = DB::lastInsertId();
325e5a6b4d4SGreg Roach
326e5a6b4d4SGreg Roach        return new User($user_id, $user_name, $real_name, $email);
327e5a6b4d4SGreg Roach    }
328e5a6b4d4SGreg Roach
329e5a6b4d4SGreg Roach    /**
330e5a6b4d4SGreg Roach     * Delete a user
331e5a6b4d4SGreg Roach     *
332e5a6b4d4SGreg Roach     * @param User $user
333e5a6b4d4SGreg Roach     *
334e5a6b4d4SGreg Roach     * @return void
335e5a6b4d4SGreg Roach     */
336e364afe4SGreg Roach    public function delete(User $user): void
337e5a6b4d4SGreg Roach    {
338e5a6b4d4SGreg Roach        // Don't delete the logs, just set the user to null.
339e5a6b4d4SGreg Roach        DB::table('log')
340e5a6b4d4SGreg Roach            ->where('user_id', '=', $user->id())
341e5a6b4d4SGreg Roach            ->update(['user_id' => null]);
342e5a6b4d4SGreg Roach
343e5a6b4d4SGreg Roach        // Take over the user’s pending changes. (What else could we do with them?)
344e5a6b4d4SGreg Roach        DB::table('change')
345e5a6b4d4SGreg Roach            ->where('user_id', '=', $user->id())
346e5a6b4d4SGreg Roach            ->where('status', '=', 'rejected')
347e5a6b4d4SGreg Roach            ->delete();
348e5a6b4d4SGreg Roach
349e5a6b4d4SGreg Roach        DB::table('change')
350e5a6b4d4SGreg Roach            ->where('user_id', '=', $user->id())
351e5a6b4d4SGreg Roach            ->update(['user_id' => Auth::id()]);
352e5a6b4d4SGreg Roach
353e5a6b4d4SGreg Roach        // Delete settings and preferences
354e5a6b4d4SGreg Roach        DB::table('block_setting')
355e5a6b4d4SGreg Roach            ->join('block', 'block_setting.block_id', '=', 'block.block_id')
356e5a6b4d4SGreg Roach            ->where('user_id', '=', $user->id())
357e5a6b4d4SGreg Roach            ->delete();
358e5a6b4d4SGreg Roach
359e5a6b4d4SGreg Roach        DB::table('block')->where('user_id', '=', $user->id())->delete();
360e5a6b4d4SGreg Roach        DB::table('user_gedcom_setting')->where('user_id', '=', $user->id())->delete();
361e5a6b4d4SGreg Roach        DB::table('user_setting')->where('user_id', '=', $user->id())->delete();
362e5a6b4d4SGreg Roach        DB::table('message')->where('user_id', '=', $user->id())->delete();
363e5a6b4d4SGreg Roach        DB::table('user')->where('user_id', '=', $user->id())->delete();
364e5a6b4d4SGreg Roach    }
36586730b84SGreg Roach
36686730b84SGreg Roach    /**
3674db4b4a9SGreg Roach     * @param User                   $contact_user
368a992e8c1SGreg Roach     * @param ServerRequestInterface $request
36986730b84SGreg Roach     *
37086730b84SGreg Roach     * @return string
37186730b84SGreg Roach     */
372a992e8c1SGreg Roach    public function contactLink(User $contact_user, ServerRequestInterface $request): string
373dcbe9044SGreg Roach    {
374b55cbc6bSGreg Roach        $tree = Validator::attributes($request)->tree();
375b55cbc6bSGreg Roach        $user = Validator::attributes($request)->user();
37686730b84SGreg Roach
3778cfb5e7bSGreg Roach        if ($contact_user->getPreference(UserInterface::PREF_CONTACT_METHOD) === MessageService::CONTACT_METHOD_MAILTO) {
37886730b84SGreg Roach            $url = 'mailto:' . $contact_user->email();
37986730b84SGreg Roach        } elseif ($user instanceof User) {
38086730b84SGreg Roach            // Logged-in users send direct messages
3810bf22806SGreg Roach            $url = route(MessagePage::class, [
3820bf22806SGreg Roach                'to' => $contact_user->userName(),
3830bf22806SGreg Roach                'tree' => $tree->name(),
3840bf22806SGreg Roach                'url'  => (string) $request->getUri(),
3850bf22806SGreg Roach            ]);
38686730b84SGreg Roach        } else {
38786730b84SGreg Roach            // Visitors use the contact form.
388e381f98dSGreg Roach            $url = route(ContactPage::class, [
38986730b84SGreg Roach                'to'   => $contact_user->userName(),
390d72b284aSGreg Roach                'tree' => $tree->name(),
391f567c3d8SGreg Roach                'url'  => (string) $request->getUri(),
39286730b84SGreg Roach            ]);
39386730b84SGreg Roach        }
39486730b84SGreg Roach
395310c0e49SGreg Roach        return '<a href="' . e($url) . '" dir="auto" rel="nofollow">' . e($contact_user->realName()) . '</a>';
39686730b84SGreg Roach    }
397e5a6b4d4SGreg Roach}
398