upstream V3 development https://www.gnusocial.rocks/v3
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

425 lines
13 KiB

  1. <?php
  2. declare(strict_types = 1);
  3. // {{{ License
  4. // This file is part of GNU social - https://www.gnu.org/software/social
  5. //
  6. // GNU social is free software: you can redistribute it and/or modify
  7. // it under the terms of the GNU Affero 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. //
  11. // GNU social is distributed in the hope that it will be useful,
  12. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. // GNU Affero General Public License for more details.
  15. //
  16. // You should have received a copy of the GNU Affero General Public License
  17. // along with GNU social. If not, see <http://www.gnu.org/licenses/>.
  18. // }}}
  19. namespace App\Entity;
  20. use App\Core\Cache;
  21. use App\Core\DB\DB;
  22. use App\Core\Entity;
  23. use App\Core\UserRoles;
  24. use App\Util\Common;
  25. use DateTimeInterface;
  26. use Exception;
  27. use libphonenumber\PhoneNumber;
  28. use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
  29. use Symfony\Component\Security\Core\User\UserInterface;
  30. /**
  31. * Entity for users
  32. *
  33. * @category DB
  34. * @package GNUsocial
  35. *
  36. * @author Zach Copley <zach@status.net>
  37. * @copyright 2010 StatusNet Inc.
  38. * @author Mikael Nordfeldth <mmn@hethane.se>
  39. * @copyright 2009-2014 Free Software Foundation, Inc http://www.fsf.org
  40. * @author Hugo Sales <hugo@hsal.es>
  41. * @copyright 2020-2021 Free Software Foundation, Inc http://www.fsf.org
  42. * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
  43. */
  44. class LocalUser extends Entity implements UserInterface, PasswordAuthenticatedUserInterface
  45. {
  46. // {{{ Autocode
  47. // @codeCoverageIgnoreStart
  48. private int $id;
  49. private string $nickname;
  50. private ?string $password;
  51. private ?string $outgoing_email;
  52. private ?string $incoming_email;
  53. private ?bool $is_email_verified;
  54. private ?string $timezone;
  55. private ?PhoneNumber $phone_number;
  56. private ?int $sms_carrier;
  57. private ?string $sms_email;
  58. private ?string $uri;
  59. private ?bool $auto_subscribe_back;
  60. private ?int $subscription_policy;
  61. private ?bool $is_stream_private;
  62. private DateTimeInterface $created;
  63. private DateTimeInterface $modified;
  64. public function setId(int $id): self
  65. {
  66. $this->id = $id;
  67. return $this;
  68. }
  69. public function getId(): int
  70. {
  71. return $this->id;
  72. }
  73. public function setNickname(string $nickname): self
  74. {
  75. $this->nickname = $nickname;
  76. return $this;
  77. }
  78. public function getNickname(): string
  79. {
  80. return $this->nickname;
  81. }
  82. public function setPassword(?string $password): self
  83. {
  84. $this->password = $password;
  85. return $this;
  86. }
  87. public function getPassword(): ?string
  88. {
  89. return $this->password;
  90. }
  91. public function setOutgoingEmail(?string $outgoing_email): self
  92. {
  93. $this->outgoing_email = $outgoing_email;
  94. return $this;
  95. }
  96. public function getOutgoingEmail(): ?string
  97. {
  98. return $this->outgoing_email;
  99. }
  100. public function setIncomingEmail(?string $incoming_email): self
  101. {
  102. $this->incoming_email = $incoming_email;
  103. return $this;
  104. }
  105. public function getIncomingEmail(): ?string
  106. {
  107. return $this->incoming_email;
  108. }
  109. public function setIsEmailVerified(?bool $is_email_verified): self
  110. {
  111. $this->is_email_verified = $is_email_verified;
  112. return $this;
  113. }
  114. public function getIsEmailVerified(): ?bool
  115. {
  116. return $this->is_email_verified;
  117. }
  118. public function setTimezone(?string $timezone): self
  119. {
  120. $this->timezone = $timezone;
  121. return $this;
  122. }
  123. public function getTimezone(): ?string
  124. {
  125. return $this->timezone;
  126. }
  127. public function setPhoneNumber(?PhoneNumber $phone_number): self
  128. {
  129. $this->phone_number = $phone_number;
  130. return $this;
  131. }
  132. public function getPhoneNumber(): ?PhoneNumber
  133. {
  134. return $this->phone_number;
  135. }
  136. public function setSmsCarrier(?int $sms_carrier): self
  137. {
  138. $this->sms_carrier = $sms_carrier;
  139. return $this;
  140. }
  141. public function getSmsCarrier(): ?int
  142. {
  143. return $this->sms_carrier;
  144. }
  145. public function setSmsEmail(?string $sms_email): self
  146. {
  147. $this->sms_email = $sms_email;
  148. return $this;
  149. }
  150. public function getSmsEmail(): ?string
  151. {
  152. return $this->sms_email;
  153. }
  154. public function setUri(?string $uri): self
  155. {
  156. $this->uri = $uri;
  157. return $this;
  158. }
  159. public function getUri(): ?string
  160. {
  161. return $this->uri;
  162. }
  163. public function setAutoSubscribeBack(?bool $auto_subscribe_back): self
  164. {
  165. $this->auto_subscribe_back = $auto_subscribe_back;
  166. return $this;
  167. }
  168. public function getAutoSubscribeBack(): ?bool
  169. {
  170. return $this->auto_subscribe_back;
  171. }
  172. public function setSubscriptionPolicy(?int $subscription_policy): self
  173. {
  174. $this->subscription_policy = $subscription_policy;
  175. return $this;
  176. }
  177. public function getSubscriptionPolicy(): ?int
  178. {
  179. return $this->subscription_policy;
  180. }
  181. public function setIsStreamPrivate(?bool $is_stream_private): self
  182. {
  183. $this->is_stream_private = $is_stream_private;
  184. return $this;
  185. }
  186. public function getIsStreamPrivate(): ?bool
  187. {
  188. return $this->is_stream_private;
  189. }
  190. public function setCreated(DateTimeInterface $created): self
  191. {
  192. $this->created = $created;
  193. return $this;
  194. }
  195. public function getCreated(): DateTimeInterface
  196. {
  197. return $this->created;
  198. }
  199. public function setModified(DateTimeInterface $modified): self
  200. {
  201. $this->modified = $modified;
  202. return $this;
  203. }
  204. public function getModified(): DateTimeInterface
  205. {
  206. return $this->modified;
  207. }
  208. // @codeCoverageIgnoreEnd
  209. // }}} Autocode
  210. // {{{ Authentication
  211. /**
  212. * Returns the salt that was originally used to encode the password.
  213. * BCrypt and Argon2 generate their own salts
  214. */
  215. public function getSalt(): ?string
  216. {
  217. return null;
  218. }
  219. /**
  220. * Removes sensitive data from the user.
  221. *
  222. * This is important if, at any given point, sensitive information like
  223. * the plain-text password is stored on this object.
  224. */
  225. public function eraseCredentials()
  226. {
  227. }
  228. /**
  229. * When authenticating, check a user's password in a timing safe
  230. * way. Will update the password by rehashing if deemed necessary
  231. */
  232. public function checkPassword(string $password_plain_text): bool
  233. {
  234. // Timing safe password verification
  235. if (password_verify($password_plain_text, $this->password)) {
  236. // Update old formats
  237. if (password_needs_rehash(
  238. $this->password,
  239. self::algoNameToConstant(Common::config('security', 'algorithm')),
  240. Common::config('security', 'options'),
  241. )
  242. ) {
  243. // @codeCoverageIgnoreStart
  244. $this->changePassword(null, $password_plain_text, override: true);
  245. // @codeCoverageIgnoreEnd
  246. }
  247. return true;
  248. }
  249. return false;
  250. }
  251. public function changePassword(?string $old_password_plain_text, string $new_password_plain_text, bool $override = false): bool
  252. {
  253. if ($override || $this->checkPassword($old_password_plain_text)) {
  254. $this->setPassword(self::hashPassword($new_password_plain_text));
  255. DB::flush();
  256. return true;
  257. }
  258. return false;
  259. }
  260. public static function hashPassword(string $password): string
  261. {
  262. $algorithm = self::algoNameToConstant(Common::config('security', 'algorithm'));
  263. $options = Common::config('security', 'options');
  264. return password_hash($password, $algorithm, $options);
  265. }
  266. /**
  267. * Public for testing
  268. */
  269. public static function algoNameToConstant(string $algo)
  270. {
  271. switch ($algo) {
  272. case 'bcrypt':
  273. case 'argon2i':
  274. case 'argon2d':
  275. case 'argon2id':
  276. $c = 'PASSWORD_' . mb_strtoupper($algo);
  277. if (\defined($c)) {
  278. return \constant($c);
  279. }
  280. // fallthrough
  281. // no break
  282. default:
  283. throw new Exception('Unsupported or unsafe hashing algorithm requested');
  284. }
  285. }
  286. /**
  287. * Returns the username used to authenticate the user.
  288. * Part of the Symfony UserInterface
  289. *
  290. * @return string
  291. *
  292. * @deprecated since Symfony 5.3, use getUserIdentifier() instead
  293. */
  294. public function getUsername(): string
  295. {
  296. return $this->getUserIdentifier();
  297. }
  298. /**
  299. * returns the identifier for this user (e.g. its nickname)
  300. * Part of the Symfony UserInterface
  301. * @return string
  302. */
  303. public function getUserIdentifier(): string
  304. {
  305. return $this->getNickname();
  306. }
  307. // }}} Authentication
  308. public function getActor()
  309. {
  310. return DB::find('actor', ['id' => $this->id]);
  311. }
  312. /**
  313. * Returns the roles granted to the user
  314. */
  315. public function getRoles()
  316. {
  317. return UserRoles::toArray($this->getActor()->getRoles());
  318. }
  319. public static function getByNickname(string $nickname): ?self
  320. {
  321. $key = str_replace('_', '-', $nickname);
  322. return Cache::get("user-nickname-{$key}", fn () => DB::findOneBy('local_user', ['nickname' => $nickname]));
  323. }
  324. /**
  325. * @return self Returns self if email found
  326. */
  327. public static function getByEmail(string $email): ?self
  328. {
  329. $key = str_replace('@', '-', $email);
  330. return Cache::get("user-email-{$key}", fn () => DB::findOneBy('local_user', ['or' => ['outgoing_email' => $email, 'incoming_email' => $email]]));
  331. }
  332. public static function schemaDef(): array
  333. {
  334. return [
  335. 'name' => 'local_user',
  336. 'description' => 'local users, bots, etc',
  337. 'fields' => [
  338. 'id' => ['type' => 'int', 'foreign key' => true, 'target' => 'Actor.id', 'multiplicity' => 'one to one', 'not null' => true, 'description' => 'foreign key to actor table'],
  339. 'nickname' => ['type' => 'varchar', 'not null' => true, 'length' => 64, 'description' => 'nickname or username, foreign key to actor'],
  340. 'password' => ['type' => 'varchar', 'length' => 191, 'description' => 'salted password, can be null for users with federated authentication'],
  341. 'outgoing_email' => ['type' => 'varchar', 'length' => 191, 'description' => 'email address for password recovery, notifications, etc.'],
  342. 'incoming_email' => ['type' => 'varchar', 'length' => 191, 'description' => 'email address for post-by-email'],
  343. 'is_email_verified' => ['type' => 'bool', 'default' => false, 'description' => 'Whether the user opened the comfirmation email'],
  344. 'timezone' => ['type' => 'varchar', 'length' => 50, 'description' => 'timezone'],
  345. 'phone_number' => ['type' => 'phone_number', 'description' => 'phone number'],
  346. 'sms_carrier' => ['type' => 'int', 'foreign key' => true, 'target' => 'SmsCarrier.id', 'multiplicity' => 'one to one', 'description' => 'foreign key to sms_carrier'],
  347. 'sms_email' => ['type' => 'varchar', 'length' => 191, 'description' => 'built from sms and carrier (see sms_carrier)'],
  348. 'uri' => ['type' => 'varchar', 'length' => 191, 'description' => 'universally unique identifier, usually a tag URI'],
  349. 'auto_subscribe_back' => ['type' => 'bool', 'default' => false, 'description' => 'automatically subscribe to users who subscribed us'],
  350. 'subscription_policy' => ['type' => 'int', 'size' => 'tiny', 'default' => 0, 'description' => '0 = anybody can subscribe; 1 = require approval'],
  351. 'is_stream_private' => ['type' => 'bool', 'default' => false, 'description' => 'whether to limit all notices to subscribers only'],
  352. 'created' => ['type' => 'datetime', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was created'],
  353. 'modified' => ['type' => 'timestamp', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was modified'],
  354. ],
  355. 'primary key' => ['id'],
  356. 'unique keys' => [
  357. 'user_nickname_key' => ['nickname'],
  358. 'user_outgoing_email_key' => ['outgoing_email'],
  359. 'user_incoming_email_key' => ['incoming_email'],
  360. 'user_phone_number_key' => ['phone_number'],
  361. 'user_uri_key' => ['uri'],
  362. ],
  363. 'indexes' => [
  364. 'user_nickname_idx' => ['nickname'],
  365. 'user_created_idx' => ['created'],
  366. 'user_sms_email_idx' => ['sms_email'],
  367. ],
  368. ];
  369. }
  370. }