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