xref: /webtrees/app/Services/UserService.php (revision f25fc0f929f69ab8124cf0cecde45e457db7574a)
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()
55            ->remember('user-' . $user_id, static fn(): ?User => DB::table('user')
56                ->where('user_id', '=', $user_id)
57                ->get()
58                ->map(User::rowMapper())
59                ->first());
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(string $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(string $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<int,User>
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', '=', UserInterface::PREF_TREE_ACCOUNT_XREF)
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', '>', time())
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(string $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(UserInterface,UserInterface):int
156     */
157    public function sortByLastLogin(): Closure
158    {
159        return static function (UserInterface $user1, UserInterface $user2) {
160            $registered_at1 = (int) $user1->getPreference(UserInterface::PREF_TIMESTAMP_REGISTERED);
161            $logged_in_at1  = (int) $user1->getPreference(UserInterface::PREF_TIMESTAMP_ACTIVE);
162            $registered_at2 = (int) $user2->getPreference(UserInterface::PREF_TIMESTAMP_REGISTERED);
163            $logged_in_at2  = (int) $user2->getPreference(UserInterface::PREF_TIMESTAMP_ACTIVE);
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(UserInterface):bool
175     */
176    public function filterInactive(int $timestamp): Closure
177    {
178        return static function (UserInterface $user) use ($timestamp): bool {
179            $registered_at = (int) $user->getPreference(UserInterface::PREF_TIMESTAMP_REGISTERED);
180            $logged_in_at  = (int) $user->getPreference(UserInterface::PREF_TIMESTAMP_ACTIVE);
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<int,User>
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<int,User>
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', '=', UserInterface::PREF_IS_ADMINISTRATOR)
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<int,User>
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', '=', UserInterface::PREF_TREE_ROLE)
228            ->where('user_gedcom_setting.setting_value', '=', UserInterface::ROLE_MANAGER)
229            ->where('user.user_id', '>', 0)
230            ->orderBy('real_name')
231            ->distinct()
232            ->select(['user.*'])
233            ->get()
234            ->map(User::rowMapper());
235    }
236
237    /**
238     * Get a list of all moderators.
239     *
240     * @return Collection<int,User>
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', '=', UserInterface::PREF_TREE_ROLE)
247            ->where('user_gedcom_setting.setting_value', '=', UserInterface::ROLE_MODERATOR)
248            ->where('user.user_id', '>', 0)
249            ->orderBy('real_name')
250            ->distinct()
251            ->select(['user.*'])
252            ->get()
253            ->map(User::rowMapper());
254    }
255
256    /**
257     * Get a list of all verified users.
258     *
259     * @return Collection<int,User>
260     */
261    public function unapproved(): Collection
262    {
263        return DB::table('user')
264            ->leftJoin('user_setting', static function (JoinClause $join): void {
265                $join
266                    ->on('user_setting.user_id', '=', 'user.user_id')
267                    ->where('user_setting.setting_name', '=', UserInterface::PREF_IS_ACCOUNT_APPROVED);
268            })
269            ->where(static function (Builder $query): void {
270                $query
271                    ->where('user_setting.setting_value', '<>', '1')
272                    ->orWhereNull('user_setting.setting_value');
273            })
274            ->where('user.user_id', '>', 0)
275            ->orderBy('real_name')
276            ->select(['user.*'])
277            ->get()
278            ->map(User::rowMapper());
279    }
280
281    /**
282     * Get a list of all verified users.
283     *
284     * @return Collection<int,User>
285     */
286    public function unverified(): Collection
287    {
288        return DB::table('user')
289            ->leftJoin('user_setting', static function (JoinClause $join): void {
290                $join
291                    ->on('user_setting.user_id', '=', 'user.user_id')
292                    ->where('user_setting.setting_name', '=', UserInterface::PREF_IS_EMAIL_VERIFIED);
293            })
294            ->where(static function (Builder $query): void {
295                $query
296                    ->where('user_setting.setting_value', '<>', '1')
297                    ->orWhereNull('user_setting.setting_value');
298            })
299            ->where('user.user_id', '>', 0)
300            ->orderBy('real_name')
301            ->select(['user.*'])
302            ->get()
303            ->map(User::rowMapper());
304    }
305
306    /**
307     * Get a list of all users who are currently logged in.
308     *
309     * @return Collection<int,User>
310     */
311    public function allLoggedIn(): Collection
312    {
313        return DB::table('user')
314            ->join('session', 'session.user_id', '=', 'user.user_id')
315            ->where('user.user_id', '>', 0)
316            ->orderBy('real_name')
317            ->distinct()
318            ->select(['user.*'])
319            ->get()
320            ->map(User::rowMapper());
321    }
322
323    /**
324     * Create a new user.
325     * The calling code needs to check for duplicates identifiers before calling
326     * this function.
327     *
328     * @param string $user_name
329     * @param string $real_name
330     * @param string $email
331     * @param string $password
332     *
333     * @return User
334     */
335    public function create(string $user_name, string $real_name, string $email, #[\SensitiveParameter] string $password): User
336    {
337        DB::table('user')->insert([
338            'user_name' => $user_name,
339            'real_name' => $real_name,
340            'email'     => $email,
341            'password'  => password_hash($password, PASSWORD_DEFAULT),
342        ]);
343
344        $user_id = (int) DB::connection()->getPdo()->lastInsertId();
345
346        return new User($user_id, $user_name, $real_name, $email);
347    }
348
349    /**
350     * Delete a user
351     *
352     * @param User $user
353     *
354     * @return void
355     */
356    public function delete(User $user): void
357    {
358        // Don't delete the logs, just set the user to null.
359        DB::table('log')
360            ->where('user_id', '=', $user->id())
361            ->update(['user_id' => null]);
362
363        // Take over the user’s pending changes. (What else could we do with them?)
364        DB::table('change')
365            ->where('user_id', '=', $user->id())
366            ->where('status', '=', 'rejected')
367            ->delete();
368
369        DB::table('change')
370            ->where('user_id', '=', $user->id())
371            ->update(['user_id' => Auth::id()]);
372
373        // Delete settings and preferences
374        DB::table('block_setting')
375            ->join('block', 'block_setting.block_id', '=', 'block.block_id')
376            ->where('user_id', '=', $user->id())
377            ->delete();
378
379        DB::table('block')->where('user_id', '=', $user->id())->delete();
380        DB::table('user_gedcom_setting')->where('user_id', '=', $user->id())->delete();
381        DB::table('user_setting')->where('user_id', '=', $user->id())->delete();
382        DB::table('message')->where('user_id', '=', $user->id())->delete();
383        DB::table('user')->where('user_id', '=', $user->id())->delete();
384    }
385
386    /**
387     * @param User                   $contact_user
388     * @param ServerRequestInterface $request
389     *
390     * @return string
391     */
392    public function contactLink(User $contact_user, ServerRequestInterface $request): string
393    {
394        $tree = Validator::attributes($request)->tree();
395        $user = Validator::attributes($request)->user();
396
397        if ($contact_user->getPreference(UserInterface::PREF_CONTACT_METHOD) === MessageService::CONTACT_METHOD_MAILTO) {
398            $url = 'mailto:' . $contact_user->email();
399        } elseif ($user instanceof User) {
400            // Logged-in users send direct messages
401            $url = route(MessagePage::class, [
402                'to' => $contact_user->userName(),
403                'tree' => $tree->name(),
404                'url'  => (string) $request->getUri(),
405            ]);
406        } else {
407            // Visitors use the contact form.
408            $url = route(ContactPage::class, [
409                'to'   => $contact_user->userName(),
410                'tree' => $tree->name(),
411                'url'  => (string) $request->getUri(),
412            ]);
413        }
414
415        return '<a href="' . e($url) . '" dir="auto" rel="nofollow">' . e($contact_user->realName()) . '</a>';
416    }
417}
418