1<?php 2 3/** 4 * webtrees: online genealogy 5 * Copyright (C) 2019 webtrees development team 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License as published by 8 * the Free Software Foundation, either version 3 of the License, or 9 * (at your option) any later version. 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * You should have received a copy of the GNU General Public License 15 * along with this program. If not, see <http://www.gnu.org/licenses/>. 16 */ 17 18declare(strict_types=1); 19 20namespace Fisharebest\Webtrees\Services; 21 22use Closure; 23use Fisharebest\Webtrees\Auth; 24use Fisharebest\Webtrees\Carbon; 25use Fisharebest\Webtrees\Contracts\UserInterface; 26use Fisharebest\Webtrees\Http\RequestHandlers\ContactPage; 27use Fisharebest\Webtrees\Http\RequestHandlers\MessagePage; 28use Fisharebest\Webtrees\Individual; 29use Fisharebest\Webtrees\Tree; 30use Fisharebest\Webtrees\User; 31use Illuminate\Database\Capsule\Manager as DB; 32use Illuminate\Support\Collection; 33use Psr\Http\Message\ServerRequestInterface; 34 35use function app; 36use function assert; 37use function max; 38 39/** 40 * Functions for managing users. 41 */ 42class UserService 43{ 44 /** 45 * Find the user with a specified user_id. 46 * 47 * @param int|null $user_id 48 * 49 * @return User|null 50 */ 51 public function find($user_id): ?User 52 { 53 return app('cache.array')->rememberForever(__CLASS__ . $user_id, static function () use ($user_id): ?User { 54 return DB::table('user') 55 ->where('user_id', '=', $user_id) 56 ->get() 57 ->map(User::rowMapper()) 58 ->first(); 59 }); 60 } 61 62 /** 63 * Find the user with a specified email address. 64 * 65 * @param string $email 66 * 67 * @return User|null 68 */ 69 public function findByEmail($email): ?User 70 { 71 return DB::table('user') 72 ->where('email', '=', $email) 73 ->get() 74 ->map(User::rowMapper()) 75 ->first(); 76 } 77 78 /** 79 * Find the user with a specified user_name or email address. 80 * 81 * @param string $identifier 82 * 83 * @return User|null 84 */ 85 public function findByIdentifier($identifier): ?User 86 { 87 return DB::table('user') 88 ->where('user_name', '=', $identifier) 89 ->orWhere('email', '=', $identifier) 90 ->get() 91 ->map(User::rowMapper()) 92 ->first(); 93 } 94 95 /** 96 * Find the user(s) with a specified genealogy record. 97 * 98 * @param Individual $individual 99 * 100 * @return Collection 101 */ 102 public function findByIndividual(Individual $individual): Collection 103 { 104 return DB::table('user') 105 ->join('user_gedcom_setting', 'user_gedcom_setting.user_id', '=', 'user.user_id') 106 ->where('gedcom_id', '=', $individual->tree()->id()) 107 ->where('setting_value', '=', $individual->xref()) 108 ->where('setting_name', '=', 'gedcomid') 109 ->select(['user.*']) 110 ->get() 111 ->map(User::rowMapper()); 112 } 113 114 /** 115 * Find the user with a specified password reset token. 116 * 117 * @param string $token 118 * 119 * @return User|null 120 */ 121 public function findByToken(string $token): ?User 122 { 123 return DB::table('user') 124 ->join('user_setting AS us1', 'us1.user_id', '=', 'user.user_id') 125 ->where('us1.setting_name', '=', 'password-token') 126 ->where('us1.setting_value', '=', $token) 127 ->join('user_setting AS us2', 'us2.user_id', '=', 'user.user_id') 128 ->where('us2.setting_name', '=', 'password-token-expire') 129 ->where('us2.setting_value', '>', Carbon::now()->timestamp) 130 ->select(['user.*']) 131 ->get() 132 ->map(User::rowMapper()) 133 ->first(); 134 } 135 136 /** 137 * Find the user with a specified user_name. 138 * 139 * @param string $user_name 140 * 141 * @return User|null 142 */ 143 public function findByUserName($user_name): ?User 144 { 145 return DB::table('user') 146 ->where('user_name', '=', $user_name) 147 ->get() 148 ->map(User::rowMapper()) 149 ->first(); 150 } 151 152 /** 153 * Callback to sort users by their last-login (or registration) time. 154 * 155 * @return Closure 156 */ 157 public function sortByLastLogin(): Closure 158 { 159 return function (UserInterface $user1, UserInterface $user2) { 160 $registered_at1 = (int) $user1->getPreference('reg_timestamp'); 161 $logged_in_at1 = (int) $user1->getPreference('sessiontime'); 162 $registered_at2 = (int) $user2->getPreference('reg_timestamp'); 163 $logged_in_at2 = (int) $user2->getPreference('sessiontime'); 164 165 return max($registered_at1, $logged_in_at1) <=> max($registered_at2, $logged_in_at2); 166 }; 167 } 168 169 /** 170 * Callback to filter users who have not logged in since a given time. 171 * 172 * @param int $timestamp 173 * 174 * @return Closure 175 */ 176 public function filterInactive(int $timestamp): Closure 177 { 178 return function (UserInterface $user) use ($timestamp): bool { 179 $registered_at = (int) $user->getPreference('reg_timestamp'); 180 $logged_in_at = (int) $user->getPreference('sessiontime'); 181 182 return max($registered_at, $logged_in_at) < $timestamp; 183 }; 184 } 185 186 /** 187 * Get a list of all users. 188 * 189 * @return Collection 190 */ 191 public function all(): Collection 192 { 193 return DB::table('user') 194 ->where('user_id', '>', 0) 195 ->orderBy('real_name') 196 ->get() 197 ->map(User::rowMapper()); 198 } 199 200 /** 201 * Get a list of all administrators. 202 * 203 * @return Collection 204 */ 205 public function administrators(): Collection 206 { 207 return DB::table('user') 208 ->join('user_setting', 'user_setting.user_id', '=', 'user.user_id') 209 ->where('user_setting.setting_name', '=', 'canadmin') 210 ->where('user_setting.setting_value', '=', '1') 211 ->where('user.user_id', '>', 0) 212 ->orderBy('real_name') 213 ->select(['user.*']) 214 ->get() 215 ->map(User::rowMapper()); 216 } 217 218 /** 219 * Get a list of all managers. 220 * 221 * @return Collection 222 */ 223 public function managers(): Collection 224 { 225 return DB::table('user') 226 ->join('user_gedcom_setting', 'user_gedcom_setting.user_id', '=', 'user.user_id') 227 ->where('user_gedcom_setting.setting_name', '=', 'canedit') 228 ->where('user_gedcom_setting.setting_value', '=', 'admin') 229 ->where('user.user_id', '>', 0) 230 ->groupBy(['user.user_id']) 231 ->orderBy('real_name') 232 ->select(['user.*']) 233 ->get() 234 ->map(User::rowMapper()); 235 } 236 237 /** 238 * Get a list of all moderators. 239 * 240 * @return Collection 241 */ 242 public function moderators(): Collection 243 { 244 return DB::table('user') 245 ->join('user_gedcom_setting', 'user_gedcom_setting.user_id', '=', 'user.user_id') 246 ->where('user_gedcom_setting.setting_name', '=', 'canedit') 247 ->where('user_gedcom_setting.setting_value', '=', 'accept') 248 ->where('user.user_id', '>', 0) 249 ->groupBy(['user.user_id']) 250 ->orderBy('real_name') 251 ->select(['user.*']) 252 ->get() 253 ->map(User::rowMapper()); 254 } 255 256 /** 257 * Get a list of all verified users. 258 * 259 * @return Collection 260 */ 261 public function unapproved(): Collection 262 { 263 return DB::table('user') 264 ->join('user_setting', 'user_setting.user_id', '=', 'user.user_id') 265 ->where('user_setting.setting_name', '=', 'verified_by_admin') 266 ->where('user_setting.setting_value', '<>', '1') 267 ->where('user.user_id', '>', 0) 268 ->orderBy('real_name') 269 ->select(['user.*']) 270 ->get() 271 ->map(User::rowMapper()); 272 } 273 274 /** 275 * Get a list of all verified users. 276 * 277 * @return Collection 278 */ 279 public function unverified(): Collection 280 { 281 return DB::table('user') 282 ->join('user_setting', 'user_setting.user_id', '=', 'user.user_id') 283 ->where('user_setting.setting_name', '=', 'verified') 284 ->where('user_setting.setting_value', '<>', '1') 285 ->where('user.user_id', '>', 0) 286 ->orderBy('real_name') 287 ->select(['user.*']) 288 ->get() 289 ->map(User::rowMapper()); 290 } 291 292 /** 293 * Get a list of all users who are currently logged in. 294 * 295 * @return Collection 296 */ 297 public function allLoggedIn(): Collection 298 { 299 return DB::table('user') 300 ->join('session', 'session.user_id', '=', 'user.user_id') 301 ->where('user.user_id', '>', 0) 302 ->orderBy('real_name') 303 ->select(['user.*']) 304 ->distinct() 305 ->get() 306 ->map(User::rowMapper()); 307 } 308 309 /** 310 * Create a new user. 311 * The calling code needs to check for duplicates identifiers before calling 312 * this function. 313 * 314 * @param string $user_name 315 * @param string $real_name 316 * @param string $email 317 * @param string $password 318 * 319 * @return User 320 */ 321 public function create(string $user_name, string $real_name, string $email, string $password): User 322 { 323 DB::table('user')->insert([ 324 'user_name' => $user_name, 325 'real_name' => $real_name, 326 'email' => $email, 327 'password' => password_hash($password, PASSWORD_DEFAULT), 328 ]); 329 330 $user_id = (int) DB::connection()->getPdo()->lastInsertId(); 331 332 return new User($user_id, $user_name, $real_name, $email); 333 } 334 335 /** 336 * Delete a user 337 * 338 * @param User $user 339 * 340 * @return void 341 */ 342 public function delete(User $user): void 343 { 344 // Don't delete the logs, just set the user to null. 345 DB::table('log') 346 ->where('user_id', '=', $user->id()) 347 ->update(['user_id' => null]); 348 349 // Take over the user’s pending changes. (What else could we do with them?) 350 DB::table('change') 351 ->where('user_id', '=', $user->id()) 352 ->where('status', '=', 'rejected') 353 ->delete(); 354 355 DB::table('change') 356 ->where('user_id', '=', $user->id()) 357 ->update(['user_id' => Auth::id()]); 358 359 // Delete settings and preferences 360 DB::table('block_setting') 361 ->join('block', 'block_setting.block_id', '=', 'block.block_id') 362 ->where('user_id', '=', $user->id()) 363 ->delete(); 364 365 DB::table('block')->where('user_id', '=', $user->id())->delete(); 366 DB::table('user_gedcom_setting')->where('user_id', '=', $user->id())->delete(); 367 DB::table('user_setting')->where('user_id', '=', $user->id())->delete(); 368 DB::table('message')->where('user_id', '=', $user->id())->delete(); 369 DB::table('user')->where('user_id', '=', $user->id())->delete(); 370 } 371 372 /** 373 * @param User $contact_user 374 * @param ServerRequestInterface $request 375 * 376 * @return string 377 */ 378 public function contactLink(User $contact_user, ServerRequestInterface $request): string 379 { 380 $tree = $request->getAttribute('tree'); 381 assert($tree instanceof Tree); 382 383 $user = $request->getAttribute('user'); 384 385 if ($contact_user->getPreference('contactmethod') === 'mailto') { 386 $url = 'mailto:' . $contact_user->email(); 387 } elseif ($user instanceof User) { 388 // Logged-in users send direct messages 389 $url = route(MessagePage::class, ['to' => $contact_user->userName(), 'tree' => $tree->name()]); 390 } else { 391 // Visitors use the contact form. 392 $url = route(ContactPage::class, [ 393 'to' => $contact_user->userName(), 394 'tree' => $tree->name(), 395 'url' => (string) $request->getUri(), 396 ]); 397 } 398 399 return '<a href="' . e($url) . '" dir="auto">' . e($contact_user->realName()) . '</a>'; 400 } 401} 402