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