1e5a6b4d4SGreg Roach<?php 23976b470SGreg Roach 3e5a6b4d4SGreg Roach/** 4e5a6b4d4SGreg Roach * webtrees: online genealogy 5e5a6b4d4SGreg Roach * Copyright (C) 2019 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 15e5a6b4d4SGreg Roach * along with this program. If not, see <http://www.gnu.org/licenses/>. 16e5a6b4d4SGreg Roach */ 17fcfa147eSGreg Roach 18e5a6b4d4SGreg Roachdeclare(strict_types=1); 19e5a6b4d4SGreg Roach 20e5a6b4d4SGreg Roachnamespace Fisharebest\Webtrees\Services; 21e5a6b4d4SGreg Roach 22*2474349cSGreg Roachuse Closure; 23e5a6b4d4SGreg Roachuse Fisharebest\Webtrees\Auth; 24a00bcc63SGreg Roachuse Fisharebest\Webtrees\Carbon; 25*2474349cSGreg Roachuse Fisharebest\Webtrees\Contracts\UserInterface; 26e381f98dSGreg Roachuse Fisharebest\Webtrees\Http\RequestHandlers\ContactPage; 27e381f98dSGreg Roachuse Fisharebest\Webtrees\Http\RequestHandlers\MessagePage; 28e5a6b4d4SGreg Roachuse Fisharebest\Webtrees\Individual; 295229eadeSGreg Roachuse Fisharebest\Webtrees\Tree; 30e5a6b4d4SGreg Roachuse Fisharebest\Webtrees\User; 31e5a6b4d4SGreg Roachuse Illuminate\Database\Capsule\Manager as DB; 32e5a6b4d4SGreg Roachuse Illuminate\Support\Collection; 336ccdf4f0SGreg Roachuse Psr\Http\Message\ServerRequestInterface; 34f1d4b4a2SGreg Roach 356ccdf4f0SGreg Roachuse function app; 365229eadeSGreg Roachuse function assert; 37*2474349cSGreg Roachuse function max; 38e5a6b4d4SGreg Roach 39e5a6b4d4SGreg Roach/** 40e5a6b4d4SGreg Roach * Functions for managing users. 41e5a6b4d4SGreg Roach */ 42e5a6b4d4SGreg Roachclass UserService 43e5a6b4d4SGreg Roach{ 44e5a6b4d4SGreg Roach /** 45e5a6b4d4SGreg Roach * Find the user with a specified user_id. 46e5a6b4d4SGreg Roach * 47e5a6b4d4SGreg Roach * @param int|null $user_id 48e5a6b4d4SGreg Roach * 49e5a6b4d4SGreg Roach * @return User|null 50e5a6b4d4SGreg Roach */ 5125d7fe95SGreg Roach public function find($user_id): ?User 52e5a6b4d4SGreg Roach { 530b5fd0a6SGreg Roach return app('cache.array')->rememberForever(__CLASS__ . $user_id, static function () use ($user_id): ?User { 54e5a6b4d4SGreg Roach return DB::table('user') 55e5a6b4d4SGreg Roach ->where('user_id', '=', $user_id) 56e5a6b4d4SGreg Roach ->get() 57e5a6b4d4SGreg Roach ->map(User::rowMapper()) 58e5a6b4d4SGreg Roach ->first(); 59e5a6b4d4SGreg Roach }); 60e5a6b4d4SGreg Roach } 61e5a6b4d4SGreg Roach 62e5a6b4d4SGreg Roach /** 63e5a6b4d4SGreg Roach * Find the user with a specified email address. 64e5a6b4d4SGreg Roach * 65e5a6b4d4SGreg Roach * @param string $email 66e5a6b4d4SGreg Roach * 67e5a6b4d4SGreg Roach * @return User|null 68e5a6b4d4SGreg Roach */ 69e364afe4SGreg Roach public function findByEmail($email): ?User 70e5a6b4d4SGreg Roach { 71e5a6b4d4SGreg Roach return DB::table('user') 72e5a6b4d4SGreg Roach ->where('email', '=', $email) 73e5a6b4d4SGreg Roach ->get() 74e5a6b4d4SGreg Roach ->map(User::rowMapper()) 75e5a6b4d4SGreg Roach ->first(); 76e5a6b4d4SGreg Roach } 77e5a6b4d4SGreg Roach 78e5a6b4d4SGreg Roach /** 79e5a6b4d4SGreg Roach * Find the user with a specified user_name or email address. 80e5a6b4d4SGreg Roach * 81e5a6b4d4SGreg Roach * @param string $identifier 82e5a6b4d4SGreg Roach * 83e5a6b4d4SGreg Roach * @return User|null 84e5a6b4d4SGreg Roach */ 85e364afe4SGreg Roach public function findByIdentifier($identifier): ?User 86e5a6b4d4SGreg Roach { 87e5a6b4d4SGreg Roach return DB::table('user') 88e5a6b4d4SGreg Roach ->where('user_name', '=', $identifier) 89e5a6b4d4SGreg Roach ->orWhere('email', '=', $identifier) 90e5a6b4d4SGreg Roach ->get() 91e5a6b4d4SGreg Roach ->map(User::rowMapper()) 92e5a6b4d4SGreg Roach ->first(); 93e5a6b4d4SGreg Roach } 94e5a6b4d4SGreg Roach 95e5a6b4d4SGreg Roach /** 96e5a6b4d4SGreg Roach * Find the user(s) with a specified genealogy record. 97e5a6b4d4SGreg Roach * 98e5a6b4d4SGreg Roach * @param Individual $individual 99e5a6b4d4SGreg Roach * 10054c7f8dfSGreg Roach * @return Collection 101e5a6b4d4SGreg Roach */ 102e5a6b4d4SGreg Roach public function findByIndividual(Individual $individual): Collection 103e5a6b4d4SGreg Roach { 104e5a6b4d4SGreg Roach return DB::table('user') 105e5a6b4d4SGreg Roach ->join('user_gedcom_setting', 'user_gedcom_setting.user_id', '=', 'user.user_id') 106e5a6b4d4SGreg Roach ->where('gedcom_id', '=', $individual->tree()->id()) 107e5a6b4d4SGreg Roach ->where('setting_value', '=', $individual->xref()) 108e5a6b4d4SGreg Roach ->where('setting_name', '=', 'gedcomid') 109e5a6b4d4SGreg Roach ->select(['user.*']) 110e5a6b4d4SGreg Roach ->get() 111e5a6b4d4SGreg Roach ->map(User::rowMapper()); 112e5a6b4d4SGreg Roach } 113e5a6b4d4SGreg Roach 114e5a6b4d4SGreg Roach /** 115a00bcc63SGreg Roach * Find the user with a specified password reset token. 116a00bcc63SGreg Roach * 117a00bcc63SGreg Roach * @param string $token 118a00bcc63SGreg Roach * 119a00bcc63SGreg Roach * @return User|null 120a00bcc63SGreg Roach */ 121a00bcc63SGreg Roach public function findByToken(string $token): ?User 122a00bcc63SGreg Roach { 123a00bcc63SGreg Roach return DB::table('user') 124a00bcc63SGreg Roach ->join('user_setting AS us1', 'us1.user_id', '=', 'user.user_id') 125a00bcc63SGreg Roach ->where('us1.setting_name', '=', 'password-token') 126a00bcc63SGreg Roach ->where('us1.setting_value', '=', $token) 127a00bcc63SGreg Roach ->join('user_setting AS us2', 'us2.user_id', '=', 'user.user_id') 128a00bcc63SGreg Roach ->where('us2.setting_name', '=', 'password-token-expire') 129a00bcc63SGreg Roach ->where('us2.setting_value', '>', Carbon::now()->timestamp) 130a00bcc63SGreg Roach ->select(['user.*']) 131a00bcc63SGreg Roach ->get() 132a00bcc63SGreg Roach ->map(User::rowMapper()) 133a00bcc63SGreg Roach ->first(); 134a00bcc63SGreg Roach } 135a00bcc63SGreg Roach 136a00bcc63SGreg Roach /** 137e5a6b4d4SGreg Roach * Find the user with a specified user_name. 138e5a6b4d4SGreg Roach * 139e5a6b4d4SGreg Roach * @param string $user_name 140e5a6b4d4SGreg Roach * 141e5a6b4d4SGreg Roach * @return User|null 142e5a6b4d4SGreg Roach */ 143e364afe4SGreg Roach public function findByUserName($user_name): ?User 144e5a6b4d4SGreg Roach { 145e5a6b4d4SGreg Roach return DB::table('user') 146e5a6b4d4SGreg Roach ->where('user_name', '=', $user_name) 147e5a6b4d4SGreg Roach ->get() 148e5a6b4d4SGreg Roach ->map(User::rowMapper()) 149e5a6b4d4SGreg Roach ->first(); 150e5a6b4d4SGreg Roach } 151e5a6b4d4SGreg Roach 152e5a6b4d4SGreg Roach /** 153*2474349cSGreg Roach * Callback to sort users by their last-login (or registration) time. 154*2474349cSGreg Roach * 155*2474349cSGreg Roach * @return Closure 156*2474349cSGreg Roach */ 157*2474349cSGreg Roach public function sortByLastLogin(): Closure 158*2474349cSGreg Roach { 159*2474349cSGreg Roach return function (UserInterface $user1, UserInterface $user2) { 160*2474349cSGreg Roach $registered_at1 = (int) $user1->getPreference('reg_timestamp'); 161*2474349cSGreg Roach $logged_in_at1 = (int) $user1->getPreference('sessiontime'); 162*2474349cSGreg Roach $registered_at2 = (int) $user2->getPreference('reg_timestamp'); 163*2474349cSGreg Roach $logged_in_at2 = (int) $user2->getPreference('sessiontime'); 164*2474349cSGreg Roach 165*2474349cSGreg Roach return max($registered_at1, $logged_in_at1) <=> max($registered_at2, $logged_in_at2); 166*2474349cSGreg Roach }; 167*2474349cSGreg Roach } 168*2474349cSGreg Roach 169*2474349cSGreg Roach /** 170*2474349cSGreg Roach * Callback to filter users who have not logged in since a given time. 171*2474349cSGreg Roach * 172*2474349cSGreg Roach * @param int $timestamp 173*2474349cSGreg Roach * 174*2474349cSGreg Roach * @return Closure 175*2474349cSGreg Roach */ 176*2474349cSGreg Roach public function filterInactive(int $timestamp): Closure 177*2474349cSGreg Roach { 178*2474349cSGreg Roach return function (UserInterface $user) use ($timestamp): bool { 179*2474349cSGreg Roach $registered_at = (int) $user->getPreference('reg_timestamp'); 180*2474349cSGreg Roach $logged_in_at = (int) $user->getPreference('sessiontime'); 181*2474349cSGreg Roach 182*2474349cSGreg Roach return max($registered_at, $logged_in_at) < $timestamp; 183*2474349cSGreg Roach }; 184*2474349cSGreg Roach } 185*2474349cSGreg Roach 186*2474349cSGreg Roach /** 187e5a6b4d4SGreg Roach * Get a list of all users. 188e5a6b4d4SGreg Roach * 18954c7f8dfSGreg Roach * @return Collection 190e5a6b4d4SGreg Roach */ 191e5a6b4d4SGreg Roach public function all(): Collection 192e5a6b4d4SGreg Roach { 193e5a6b4d4SGreg Roach return DB::table('user') 194e5a6b4d4SGreg Roach ->where('user_id', '>', 0) 195e5a6b4d4SGreg Roach ->orderBy('real_name') 196e5a6b4d4SGreg Roach ->get() 197e5a6b4d4SGreg Roach ->map(User::rowMapper()); 198e5a6b4d4SGreg Roach } 199e5a6b4d4SGreg Roach 200e5a6b4d4SGreg Roach /** 201e5a6b4d4SGreg Roach * Get a list of all administrators. 202e5a6b4d4SGreg Roach * 20354c7f8dfSGreg Roach * @return Collection 204e5a6b4d4SGreg Roach */ 205e5a6b4d4SGreg Roach public function administrators(): Collection 206e5a6b4d4SGreg Roach { 207e5a6b4d4SGreg Roach return DB::table('user') 2081ab2f386SGreg Roach ->join('user_setting', 'user_setting.user_id', '=', 'user.user_id') 209e5a6b4d4SGreg Roach ->where('user_setting.setting_name', '=', 'canadmin') 2101ab2f386SGreg Roach ->where('user_setting.setting_value', '=', '1') 211e5a6b4d4SGreg Roach ->where('user.user_id', '>', 0) 212e5a6b4d4SGreg Roach ->orderBy('real_name') 213e5a6b4d4SGreg Roach ->select(['user.*']) 214e5a6b4d4SGreg Roach ->get() 215e5a6b4d4SGreg Roach ->map(User::rowMapper()); 216e5a6b4d4SGreg Roach } 217e5a6b4d4SGreg Roach 218e5a6b4d4SGreg Roach /** 219e5a6b4d4SGreg Roach * Get a list of all managers. 220e5a6b4d4SGreg Roach * 22154c7f8dfSGreg Roach * @return Collection 222e5a6b4d4SGreg Roach */ 223e5a6b4d4SGreg Roach public function managers(): Collection 224e5a6b4d4SGreg Roach { 225e5a6b4d4SGreg Roach return DB::table('user') 2261ab2f386SGreg Roach ->join('user_gedcom_setting', 'user_gedcom_setting.user_id', '=', 'user.user_id') 227e5a6b4d4SGreg Roach ->where('user_gedcom_setting.setting_name', '=', 'canedit') 2281ab2f386SGreg Roach ->where('user_gedcom_setting.setting_value', '=', 'admin') 229e5a6b4d4SGreg Roach ->where('user.user_id', '>', 0) 2301ab2f386SGreg Roach ->groupBy(['user.user_id']) 231e5a6b4d4SGreg Roach ->orderBy('real_name') 232e5a6b4d4SGreg Roach ->select(['user.*']) 233e5a6b4d4SGreg Roach ->get() 234e5a6b4d4SGreg Roach ->map(User::rowMapper()); 235e5a6b4d4SGreg Roach } 236e5a6b4d4SGreg Roach 237e5a6b4d4SGreg Roach /** 238e5a6b4d4SGreg Roach * Get a list of all moderators. 239e5a6b4d4SGreg Roach * 24054c7f8dfSGreg Roach * @return Collection 241e5a6b4d4SGreg Roach */ 242e5a6b4d4SGreg Roach public function moderators(): Collection 243e5a6b4d4SGreg Roach { 244e5a6b4d4SGreg Roach return DB::table('user') 2451ab2f386SGreg Roach ->join('user_gedcom_setting', 'user_gedcom_setting.user_id', '=', 'user.user_id') 246e5a6b4d4SGreg Roach ->where('user_gedcom_setting.setting_name', '=', 'canedit') 2471ab2f386SGreg Roach ->where('user_gedcom_setting.setting_value', '=', 'accept') 248e5a6b4d4SGreg Roach ->where('user.user_id', '>', 0) 2491ab2f386SGreg Roach ->groupBy(['user.user_id']) 250e5a6b4d4SGreg Roach ->orderBy('real_name') 251e5a6b4d4SGreg Roach ->select(['user.*']) 252e5a6b4d4SGreg Roach ->get() 253e5a6b4d4SGreg Roach ->map(User::rowMapper()); 254e5a6b4d4SGreg Roach } 255e5a6b4d4SGreg Roach 256e5a6b4d4SGreg Roach /** 257e5a6b4d4SGreg Roach * Get a list of all verified users. 258e5a6b4d4SGreg Roach * 25954c7f8dfSGreg Roach * @return Collection 260e5a6b4d4SGreg Roach */ 261e5a6b4d4SGreg Roach public function unapproved(): Collection 262e5a6b4d4SGreg Roach { 263e5a6b4d4SGreg Roach return DB::table('user') 2641ab2f386SGreg Roach ->join('user_setting', 'user_setting.user_id', '=', 'user.user_id') 265e5a6b4d4SGreg Roach ->where('user_setting.setting_name', '=', 'verified_by_admin') 2661ab2f386SGreg Roach ->where('user_setting.setting_value', '<>', '1') 267e5a6b4d4SGreg Roach ->where('user.user_id', '>', 0) 268e5a6b4d4SGreg Roach ->orderBy('real_name') 269e5a6b4d4SGreg Roach ->select(['user.*']) 270e5a6b4d4SGreg Roach ->get() 271e5a6b4d4SGreg Roach ->map(User::rowMapper()); 272e5a6b4d4SGreg Roach } 273e5a6b4d4SGreg Roach 274e5a6b4d4SGreg Roach /** 275e5a6b4d4SGreg Roach * Get a list of all verified users. 276e5a6b4d4SGreg Roach * 27754c7f8dfSGreg Roach * @return Collection 278e5a6b4d4SGreg Roach */ 279e5a6b4d4SGreg Roach public function unverified(): Collection 280e5a6b4d4SGreg Roach { 281e5a6b4d4SGreg Roach return DB::table('user') 2821ab2f386SGreg Roach ->join('user_setting', 'user_setting.user_id', '=', 'user.user_id') 283e5a6b4d4SGreg Roach ->where('user_setting.setting_name', '=', 'verified') 2841ab2f386SGreg Roach ->where('user_setting.setting_value', '<>', '1') 285e5a6b4d4SGreg Roach ->where('user.user_id', '>', 0) 286e5a6b4d4SGreg Roach ->orderBy('real_name') 287e5a6b4d4SGreg Roach ->select(['user.*']) 288e5a6b4d4SGreg Roach ->get() 289e5a6b4d4SGreg Roach ->map(User::rowMapper()); 290e5a6b4d4SGreg Roach } 291e5a6b4d4SGreg Roach 292e5a6b4d4SGreg Roach /** 293e5a6b4d4SGreg Roach * Get a list of all users who are currently logged in. 294e5a6b4d4SGreg Roach * 29554c7f8dfSGreg Roach * @return Collection 296e5a6b4d4SGreg Roach */ 297e5a6b4d4SGreg Roach public function allLoggedIn(): Collection 298e5a6b4d4SGreg Roach { 299e5a6b4d4SGreg Roach return DB::table('user') 300e5a6b4d4SGreg Roach ->join('session', 'session.user_id', '=', 'user.user_id') 301e5a6b4d4SGreg Roach ->where('user.user_id', '>', 0) 302e5a6b4d4SGreg Roach ->orderBy('real_name') 303e5a6b4d4SGreg Roach ->select(['user.*']) 304e5a6b4d4SGreg Roach ->distinct() 305e5a6b4d4SGreg Roach ->get() 306e5a6b4d4SGreg Roach ->map(User::rowMapper()); 307e5a6b4d4SGreg Roach } 308e5a6b4d4SGreg Roach 309e5a6b4d4SGreg Roach /** 310e5a6b4d4SGreg Roach * Create a new user. 311e5a6b4d4SGreg Roach * The calling code needs to check for duplicates identifiers before calling 312e5a6b4d4SGreg Roach * this function. 313e5a6b4d4SGreg Roach * 314e5a6b4d4SGreg Roach * @param string $user_name 315e5a6b4d4SGreg Roach * @param string $real_name 316e5a6b4d4SGreg Roach * @param string $email 317e5a6b4d4SGreg Roach * @param string $password 318e5a6b4d4SGreg Roach * 319e5a6b4d4SGreg Roach * @return User 320e5a6b4d4SGreg Roach */ 3216be338f5SGreg Roach public function create(string $user_name, string $real_name, string $email, string $password): User 322e5a6b4d4SGreg Roach { 323e5a6b4d4SGreg Roach DB::table('user')->insert([ 324e5a6b4d4SGreg Roach 'user_name' => $user_name, 325e5a6b4d4SGreg Roach 'real_name' => $real_name, 326e5a6b4d4SGreg Roach 'email' => $email, 327e5a6b4d4SGreg Roach 'password' => password_hash($password, PASSWORD_DEFAULT), 328e5a6b4d4SGreg Roach ]); 329e5a6b4d4SGreg Roach 330e5a6b4d4SGreg Roach $user_id = (int) DB::connection()->getPdo()->lastInsertId(); 331e5a6b4d4SGreg Roach 332e5a6b4d4SGreg Roach return new User($user_id, $user_name, $real_name, $email); 333e5a6b4d4SGreg Roach } 334e5a6b4d4SGreg Roach 335e5a6b4d4SGreg Roach /** 336e5a6b4d4SGreg Roach * Delete a user 337e5a6b4d4SGreg Roach * 338e5a6b4d4SGreg Roach * @param User $user 339e5a6b4d4SGreg Roach * 340e5a6b4d4SGreg Roach * @return void 341e5a6b4d4SGreg Roach */ 342e364afe4SGreg Roach public function delete(User $user): void 343e5a6b4d4SGreg Roach { 344e5a6b4d4SGreg Roach // Don't delete the logs, just set the user to null. 345e5a6b4d4SGreg Roach DB::table('log') 346e5a6b4d4SGreg Roach ->where('user_id', '=', $user->id()) 347e5a6b4d4SGreg Roach ->update(['user_id' => null]); 348e5a6b4d4SGreg Roach 349e5a6b4d4SGreg Roach // Take over the user’s pending changes. (What else could we do with them?) 350e5a6b4d4SGreg Roach DB::table('change') 351e5a6b4d4SGreg Roach ->where('user_id', '=', $user->id()) 352e5a6b4d4SGreg Roach ->where('status', '=', 'rejected') 353e5a6b4d4SGreg Roach ->delete(); 354e5a6b4d4SGreg Roach 355e5a6b4d4SGreg Roach DB::table('change') 356e5a6b4d4SGreg Roach ->where('user_id', '=', $user->id()) 357e5a6b4d4SGreg Roach ->update(['user_id' => Auth::id()]); 358e5a6b4d4SGreg Roach 359e5a6b4d4SGreg Roach // Delete settings and preferences 360e5a6b4d4SGreg Roach DB::table('block_setting') 361e5a6b4d4SGreg Roach ->join('block', 'block_setting.block_id', '=', 'block.block_id') 362e5a6b4d4SGreg Roach ->where('user_id', '=', $user->id()) 363e5a6b4d4SGreg Roach ->delete(); 364e5a6b4d4SGreg Roach 365e5a6b4d4SGreg Roach DB::table('block')->where('user_id', '=', $user->id())->delete(); 366e5a6b4d4SGreg Roach DB::table('user_gedcom_setting')->where('user_id', '=', $user->id())->delete(); 367e5a6b4d4SGreg Roach DB::table('user_setting')->where('user_id', '=', $user->id())->delete(); 368e5a6b4d4SGreg Roach DB::table('message')->where('user_id', '=', $user->id())->delete(); 369e5a6b4d4SGreg Roach DB::table('user')->where('user_id', '=', $user->id())->delete(); 370e5a6b4d4SGreg Roach } 37186730b84SGreg Roach 37286730b84SGreg Roach /** 3734db4b4a9SGreg Roach * @param User $contact_user 374a992e8c1SGreg Roach * @param ServerRequestInterface $request 37586730b84SGreg Roach * 37686730b84SGreg Roach * @return string 37786730b84SGreg Roach */ 378a992e8c1SGreg Roach public function contactLink(User $contact_user, ServerRequestInterface $request): string 379dcbe9044SGreg Roach { 380a992e8c1SGreg Roach $tree = $request->getAttribute('tree'); 38175964c75SGreg Roach assert($tree instanceof Tree); 3825229eadeSGreg Roach 383a992e8c1SGreg Roach $user = $request->getAttribute('user'); 38486730b84SGreg Roach 38586730b84SGreg Roach if ($contact_user->getPreference('contactmethod') === 'mailto') { 38686730b84SGreg Roach $url = 'mailto:' . $contact_user->email(); 38786730b84SGreg Roach } elseif ($user instanceof User) { 38886730b84SGreg Roach // Logged-in users send direct messages 389e381f98dSGreg Roach $url = route(MessagePage::class, ['to' => $contact_user->userName(), 'tree' => $tree->name()]); 39086730b84SGreg Roach } else { 39186730b84SGreg Roach // Visitors use the contact form. 392e381f98dSGreg Roach $url = route(ContactPage::class, [ 39386730b84SGreg Roach 'to' => $contact_user->userName(), 394d72b284aSGreg Roach 'tree' => $tree->name(), 395f567c3d8SGreg Roach 'url' => (string) $request->getUri(), 39686730b84SGreg Roach ]); 39786730b84SGreg Roach } 39886730b84SGreg Roach 39986730b84SGreg Roach return '<a href="' . e($url) . '" dir="auto">' . e($contact_user->realName()) . '</a>'; 40086730b84SGreg Roach } 401e5a6b4d4SGreg Roach} 402