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