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