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