xref: /webtrees/app/User.php (revision 873953697c930fadbf3243d2b8c0029fd684da0e)
1<?php
2/**
3 * webtrees: online genealogy
4 * Copyright (C) 2018 webtrees development team
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 * You should have received a copy of the GNU General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15 */
16declare(strict_types=1);
17
18namespace Fisharebest\Webtrees;
19
20use stdClass;
21
22/**
23 * Provide an interface to the wt_user table.
24 */
25class User
26{
27    /** @var  int The primary key of this user. */
28    private $user_id;
29
30    /** @var  string The login name of this user. */
31    private $user_name;
32
33    /** @var  string The real (display) name of this user. */
34    private $real_name;
35
36    /** @var  string The email address of this user. */
37    private $email;
38
39    /** @var string[] Cached copy of the wt_user_setting table. */
40    private $preferences = [];
41
42    /** @var  User[] Only fetch users from the database once. */
43    private static $cache = [];
44
45    /**
46     * Create a new user object from a row in the database.
47     *
48     * @param stdClass $user A row from the wt_user table
49     */
50    public function __construct(stdClass $user)
51    {
52        $this->user_id   = (int) $user->user_id;
53        $this->user_name = $user->user_name;
54        $this->real_name = $user->real_name;
55        $this->email     = $user->email;
56    }
57
58    /**
59     * Create a new user.
60     * The calling code needs to check for duplicates identifiers before calling
61     * this function.
62     *
63     * @param string $user_name
64     * @param string $real_name
65     * @param string $email
66     * @param string $password
67     *
68     * @return User
69     */
70    public static function create($user_name, $real_name, $email, $password): User
71    {
72        Database::prepare(
73            "INSERT INTO `##user` (user_name, real_name, email, password) VALUES (:user_name, :real_name, :email, :password)"
74        )->execute([
75            'user_name' => $user_name,
76            'real_name' => $real_name,
77            'email'     => $email,
78            'password'  => password_hash($password, PASSWORD_DEFAULT),
79        ]);
80
81        // Set default blocks for this user
82        $user = self::findByIdentifier($user_name);
83        Database::prepare(
84            "INSERT INTO `##block` (`user_id`, `location`, `block_order`, `module_name`)" .
85            " SELECT :user_id , `location`, `block_order`, `module_name` FROM `##block` WHERE `user_id` = -1"
86        )->execute([
87            'user_id' => $user->getUserId(),
88        ]);
89
90        return $user;
91    }
92
93    /**
94     * Delete a user
95     *
96     * @return void
97     */
98    public function delete()
99    {
100        // Don't delete the logs.
101        Database::prepare("UPDATE `##log` SET user_id=NULL WHERE user_id =?")->execute([$this->user_id]);
102        // Take over the user’s pending changes. (What else could we do with them?)
103        Database::prepare("DELETE FROM `##change` WHERE user_id=? AND status='rejected'")->execute([$this->user_id]);
104        Database::prepare("UPDATE `##change` SET user_id=? WHERE user_id=?")->execute([
105            Auth::id(),
106            $this->user_id,
107        ]);
108        Database::prepare("DELETE `##block_setting` FROM `##block_setting` JOIN `##block` USING (block_id) WHERE user_id=?")->execute([$this->user_id]);
109        Database::prepare("DELETE FROM `##block` WHERE user_id=?")->execute([$this->user_id]);
110        Database::prepare("DELETE FROM `##user_gedcom_setting` WHERE user_id=?")->execute([$this->user_id]);
111        Database::prepare("DELETE FROM `##gedcom_setting` WHERE setting_value=? AND setting_name IN ('CONTACT_USER_ID', 'WEBMASTER_USER_ID')")->execute([(string) $this->user_id]);
112        Database::prepare("DELETE FROM `##user_setting` WHERE user_id=?")->execute([$this->user_id]);
113        Database::prepare("DELETE FROM `##message` WHERE user_id=?")->execute([$this->user_id]);
114        Database::prepare("DELETE FROM `##user` WHERE user_id=?")->execute([$this->user_id]);
115    }
116
117    /**
118     * Find the user with a specified user_id.
119     *
120     * @param int|null $user_id
121     *
122     * @return User|null
123     */
124    public static function find($user_id)
125    {
126        if (!array_key_exists($user_id, self::$cache)) {
127            $row = Database::prepare(
128                "SELECT user_id, user_name, real_name, email FROM `##user` WHERE user_id = ?"
129            )->execute([$user_id])->fetchOneRow();
130            if ($row) {
131                self::$cache[$user_id] = new self($row);
132            } else {
133                self::$cache[$user_id] = null;
134            }
135        }
136
137        return self::$cache[$user_id];
138    }
139
140    /**
141     * Find the user with a specified email address.
142     *
143     * @param string $email
144     *
145     * @return User|null
146     */
147    public static function findByEmail($email)
148    {
149        $user_id = (int) Database::prepare(
150            "SELECT user_id FROM `##user` WHERE email = :email"
151        )->execute([
152            'email' => $email,
153        ])->fetchOne();
154
155        return self::find($user_id);
156    }
157
158    /**
159     * Find the user with a specified user_name or email address.
160     *
161     * @param string $identifier
162     *
163     * @return User|null
164     */
165    public static function findByIdentifier($identifier)
166    {
167        $user_id = (int) Database::prepare(
168            "SELECT user_id FROM `##user` WHERE ? IN (user_name, email)"
169        )->execute([$identifier])->fetchOne();
170
171        return self::find($user_id);
172    }
173
174    /**
175     * Find the user with a specified genealogy record.
176     *
177     * @param Individual $individual
178     *
179     * @return User|null
180     */
181    public static function findByIndividual(Individual $individual)
182    {
183        $user_id = (int) Database::prepare(
184            "SELECT user_id" .
185            " FROM `##user_gedcom_setting`" .
186            " WHERE gedcom_id = :tree_id AND setting_name = 'gedcomid' AND setting_value = :xref"
187        )->execute([
188            'tree_id' => $individual->getTree()->getTreeId(),
189            'xref'    => $individual->getXref(),
190        ])->fetchOne();
191
192        return self::find($user_id);
193    }
194
195    /**
196     * Find the user with a specified user_name.
197     *
198     * @param string $user_name
199     *
200     * @return User|null
201     */
202    public static function findByUserName($user_name)
203    {
204        $user_id = (int) Database::prepare(
205            "SELECT user_id FROM `##user` WHERE user_name = :user_name"
206        )->execute([
207            'user_name' => $user_name,
208        ])->fetchOne();
209
210        return self::find($user_id);
211    }
212
213    /**
214     * Find the latest user to register.
215     *
216     * @return User|null
217     */
218    public static function findLatestToRegister()
219    {
220        $user_id = (int) Database::prepare(
221            "SELECT u.user_id" .
222            " FROM `##user` u" .
223            " LEFT JOIN `##user_setting` us ON (u.user_id=us.user_id AND us.setting_name='reg_timestamp') " .
224            " ORDER BY us.setting_value DESC LIMIT 1"
225        )->execute()->fetchOne();
226
227        return self::find($user_id);
228    }
229
230    /**
231     * Get a list of all users.
232     *
233     * @return User[]
234     */
235    public static function all(): array
236    {
237        $rows = Database::prepare(
238            "SELECT user_id, user_name, real_name, email" .
239            " FROM `##user`" .
240            " WHERE user_id > 0" .
241            " ORDER BY real_name"
242        )->fetchAll();
243
244        return array_map(function (stdClass $row): User {
245            return new static($row);
246        }, $rows);
247    }
248
249    /**
250     * Get a list of all administrators.
251     *
252     * @return User[]
253     */
254    public static function administrators(): array
255    {
256        $rows = Database::prepare(
257            "SELECT user_id, user_name, real_name, email" .
258            " FROM `##user`" .
259            " JOIN `##user_setting` USING (user_id)" .
260            " WHERE user_id > 0 AND setting_name = 'canadmin' AND setting_value = '1'" .
261            " ORDER BY real_name"
262        )->fetchAll();
263
264        return array_map(function (stdClass $row): User {
265            return new static($row);
266        }, $rows);
267    }
268
269    /**
270     * Validate a supplied password
271     *
272     * @param string $password
273     *
274     * @return bool
275     */
276    public function checkPassword(string $password): bool
277    {
278        $password_hash = Database::prepare(
279            "SELECT password FROM `##user` WHERE user_id = ?"
280        )->execute([$this->user_id])->fetchOne();
281
282        if (password_verify($password, $password_hash)) {
283            if (password_needs_rehash($password_hash, PASSWORD_DEFAULT)) {
284                $this->setPassword($password);
285            }
286
287            return true;
288        } else {
289            return false;
290        }
291    }
292
293    /**
294     * Get a list of all managers.
295     *
296     * @return User[]
297     */
298    public static function managers(): array
299    {
300        $rows = Database::prepare(
301            "SELECT user_id, user_name, real_name, email" .
302            " FROM `##user` JOIN `##user_gedcom_setting` USING (user_id)" .
303            " WHERE setting_name = 'canedit' AND setting_value='admin'" .
304            " GROUP BY user_id, real_name" .
305            " ORDER BY real_name"
306        )->fetchAll();
307
308        return array_map(function (stdClass $row): User {
309            return new static($row);
310        }, $rows);
311    }
312
313    /**
314     * Get a list of all moderators.
315     *
316     * @return User[]
317     */
318    public static function moderators(): array
319    {
320        $rows = Database::prepare(
321            "SELECT user_id, user_name, real_name, email" .
322            " FROM `##user` JOIN `##user_gedcom_setting` USING (user_id)" .
323            " WHERE setting_name = 'canedit' AND setting_value='accept'" .
324            " GROUP BY user_id, real_name" .
325            " ORDER BY real_name"
326        )->fetchAll();
327
328        return array_map(function (stdClass $row): User {
329            return new static($row);
330        }, $rows);
331    }
332
333    /**
334     * Get a list of all verified users.
335     *
336     * @return User[]
337     */
338    public static function unapproved(): array
339    {
340        $rows = Database::prepare(
341            "SELECT user_id, user_name, real_name, email" .
342            " FROM `##user` JOIN `##user_setting` USING (user_id)" .
343            " WHERE setting_name = 'verified_by_admin' AND setting_value = '0'" .
344            " ORDER BY real_name"
345        )->fetchAll();
346
347        return array_map(function (stdClass $row): User {
348            return new static($row);
349        }, $rows);
350    }
351
352    /**
353     * Get a list of all verified users.
354     *
355     * @return User[]
356     */
357    public static function unverified(): array
358    {
359        $rows = Database::prepare(
360            "SELECT user_id, user_name, real_name, email" .
361            " FROM `##user` JOIN `##user_setting` USING (user_id)" .
362            " WHERE setting_name = 'verified' AND setting_value = '0'" .
363            " ORDER BY real_name"
364        )->fetchAll();
365
366        return array_map(function (stdClass $row): User {
367            return new static($row);
368        }, $rows);
369    }
370
371    /**
372     * Get a list of all users who are currently logged in.
373     *
374     * @return User[]
375     */
376    public static function allLoggedIn(): array
377    {
378        $rows = Database::prepare(
379            "SELECT DISTINCT user_id, user_name, real_name, email" .
380            " FROM `##user`" .
381            " JOIN `##session` USING (user_id)"
382        )->fetchAll();
383
384        return array_map(function (stdClass $row): User {
385            return new static($row);
386        }, $rows);
387    }
388
389    /**
390     * Get the numeric ID for this user.
391     *
392     * @return int
393     */
394    public function getUserId(): int
395    {
396        return $this->user_id;
397    }
398
399    /**
400     * Get the login name for this user.
401     *
402     * @return string
403     */
404    public function getUserName(): string
405    {
406        return $this->user_name;
407    }
408
409    /**
410     * Set the login name for this user.
411     *
412     * @param string $user_name
413     *
414     * @return $this
415     */
416    public function setUserName($user_name): self
417    {
418        if ($this->user_name !== $user_name) {
419            $this->user_name = $user_name;
420            Database::prepare(
421                "UPDATE `##user` SET user_name = ? WHERE user_id = ?"
422            )->execute([
423                $user_name,
424                $this->user_id,
425            ]);
426        }
427
428        return $this;
429    }
430
431    /**
432     * Get the real name of this user.
433     *
434     * @return string
435     */
436    public function getRealName(): string
437    {
438        return $this->real_name;
439    }
440
441    /**
442     * Set the real name of this user.
443     *
444     * @param string $real_name
445     *
446     * @return User
447     */
448    public function setRealName($real_name): User
449    {
450        if ($this->real_name !== $real_name) {
451            $this->real_name = $real_name;
452            Database::prepare(
453                "UPDATE `##user` SET real_name = ? WHERE user_id = ?"
454            )->execute([
455                $real_name,
456                $this->user_id,
457            ]);
458        }
459
460        return $this;
461    }
462
463    /**
464     * Get the email address of this user.
465     *
466     * @return string
467     */
468    public function getEmail(): string
469    {
470        return $this->email;
471    }
472
473    /**
474     * Set the email address of this user.
475     *
476     * @param string $email
477     *
478     * @return User
479     */
480    public function setEmail($email): User
481    {
482        if ($this->email !== $email) {
483            $this->email = $email;
484            Database::prepare(
485                "UPDATE `##user` SET email = ? WHERE user_id = ?"
486            )->execute([
487                $email,
488                $this->user_id,
489            ]);
490        }
491
492        return $this;
493    }
494
495    /**
496     * Set the password of this user.
497     *
498     * @param string $password
499     *
500     * @return User
501     */
502    public function setPassword($password): User
503    {
504        Database::prepare(
505            "UPDATE `##user` SET password = :password WHERE user_id = :user_id"
506        )->execute([
507            'password' => password_hash($password, PASSWORD_DEFAULT),
508            'user_id'  => $this->user_id,
509        ]);
510
511        return $this;
512    }
513
514    /**
515     * Fetch a user option/setting from the wt_user_setting table.
516     * Since we'll fetch several settings for each user, and since there aren’t
517     * that many of them, fetch them all in one database query
518     *
519     * @param string $setting_name
520     * @param string $default
521     *
522     * @return string
523     */
524    public function getPreference($setting_name, $default = ''): string
525    {
526        if (empty($this->preferences) && $this->user_id !== 0) {
527            $this->preferences = Database::prepare(
528                "SELECT setting_name, setting_value" .
529                " FROM `##user_setting`" .
530                " WHERE user_id = :user_id"
531            )->execute([
532                'user_id' => $this->user_id,
533            ])->fetchAssoc();
534        }
535
536        if (!array_key_exists($setting_name, $this->preferences)) {
537            $this->preferences[$setting_name] = $default;
538        }
539
540        return $this->preferences[$setting_name];
541    }
542
543    /**
544     * Update a setting for the user.
545     *
546     * @param string $setting_name
547     * @param string $setting_value
548     *
549     * @return User
550     */
551    public function setPreference($setting_name, $setting_value): User
552    {
553        if ($this->user_id !== 0 && $this->getPreference($setting_name) !== $setting_value) {
554            Database::prepare(
555                "REPLACE INTO `##user_setting` (user_id, setting_name, setting_value) VALUES (?, ?, LEFT(?, 255))"
556            )->execute([
557                $this->user_id,
558                $setting_name,
559                $setting_value,
560            ]);
561
562            $this->preferences[$setting_name] = $setting_value;
563        }
564
565        return $this;
566    }
567}
568