Friendica Communications Platform (please note that this is a clone of the repository at github, issues are handled there) https://friendi.ca
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.
 
 
 
 
 
 

1147 lines
33 KiB

  1. <?php
  2. /**
  3. * @file src/Model/User.php
  4. * @brief This file includes the User class with user related database functions
  5. */
  6. namespace Friendica\Model;
  7. use DivineOmega\PasswordExposed;
  8. use Exception;
  9. use Friendica\Core\Config;
  10. use Friendica\Core\Hook;
  11. use Friendica\Core\L10n;
  12. use Friendica\Core\Logger;
  13. use Friendica\Core\Protocol;
  14. use Friendica\Core\System;
  15. use Friendica\Core\Worker;
  16. use Friendica\Database\DBA;
  17. use Friendica\DI;
  18. use Friendica\Model\TwoFactor\AppSpecificPassword;
  19. use Friendica\Object\Image;
  20. use Friendica\Util\Crypto;
  21. use Friendica\Util\DateTimeFormat;
  22. use Friendica\Util\Images;
  23. use Friendica\Util\Network;
  24. use Friendica\Util\Strings;
  25. use Friendica\Worker\Delivery;
  26. use LightOpenID;
  27. /**
  28. * @brief This class handles User related functions
  29. */
  30. class User
  31. {
  32. /**
  33. * Page/profile types
  34. *
  35. * PAGE_FLAGS_NORMAL is a typical personal profile account
  36. * PAGE_FLAGS_SOAPBOX automatically approves all friend requests as Contact::SHARING, (readonly)
  37. * PAGE_FLAGS_COMMUNITY automatically approves all friend requests as Contact::SHARING, but with
  38. * write access to wall and comments (no email and not included in page owner's ACL lists)
  39. * PAGE_FLAGS_FREELOVE automatically approves all friend requests as full friends (Contact::FRIEND).
  40. *
  41. * @{
  42. */
  43. const PAGE_FLAGS_NORMAL = 0;
  44. const PAGE_FLAGS_SOAPBOX = 1;
  45. const PAGE_FLAGS_COMMUNITY = 2;
  46. const PAGE_FLAGS_FREELOVE = 3;
  47. const PAGE_FLAGS_BLOG = 4;
  48. const PAGE_FLAGS_PRVGROUP = 5;
  49. /**
  50. * @}
  51. */
  52. /**
  53. * Account types
  54. *
  55. * ACCOUNT_TYPE_PERSON - the account belongs to a person
  56. * Associated page types: PAGE_FLAGS_NORMAL, PAGE_FLAGS_SOAPBOX, PAGE_FLAGS_FREELOVE
  57. *
  58. * ACCOUNT_TYPE_ORGANISATION - the account belongs to an organisation
  59. * Associated page type: PAGE_FLAGS_SOAPBOX
  60. *
  61. * ACCOUNT_TYPE_NEWS - the account is a news reflector
  62. * Associated page type: PAGE_FLAGS_SOAPBOX
  63. *
  64. * ACCOUNT_TYPE_COMMUNITY - the account is community forum
  65. * Associated page types: PAGE_COMMUNITY, PAGE_FLAGS_PRVGROUP
  66. *
  67. * ACCOUNT_TYPE_RELAY - the account is a relay
  68. * This will only be assigned to contacts, not to user accounts
  69. * @{
  70. */
  71. const ACCOUNT_TYPE_PERSON = 0;
  72. const ACCOUNT_TYPE_ORGANISATION = 1;
  73. const ACCOUNT_TYPE_NEWS = 2;
  74. const ACCOUNT_TYPE_COMMUNITY = 3;
  75. const ACCOUNT_TYPE_RELAY = 4;
  76. /**
  77. * @}
  78. */
  79. /**
  80. * Returns true if a user record exists with the provided id
  81. *
  82. * @param integer $uid
  83. * @return boolean
  84. * @throws Exception
  85. */
  86. public static function exists($uid)
  87. {
  88. return DBA::exists('user', ['uid' => $uid]);
  89. }
  90. /**
  91. * @param integer $uid
  92. * @param array $fields
  93. * @return array|boolean User record if it exists, false otherwise
  94. * @throws Exception
  95. */
  96. public static function getById($uid, array $fields = [])
  97. {
  98. return DBA::selectFirst('user', $fields, ['uid' => $uid]);
  99. }
  100. /**
  101. * Returns a user record based on it's GUID
  102. *
  103. * @param string $guid The guid of the user
  104. * @param array $fields The fields to retrieve
  105. * @param bool $active True, if only active records are searched
  106. *
  107. * @return array|boolean User record if it exists, false otherwise
  108. * @throws Exception
  109. */
  110. public static function getByGuid(string $guid, array $fields = [], bool $active = true)
  111. {
  112. if ($active) {
  113. $cond = ['guid' => $guid, 'account_expired' => false, 'account_removed' => false];
  114. } else {
  115. $cond = ['guid' => $guid];
  116. }
  117. return DBA::selectFirst('user', $fields, $cond);
  118. }
  119. /**
  120. * @param string $nickname
  121. * @param array $fields
  122. * @return array|boolean User record if it exists, false otherwise
  123. * @throws Exception
  124. */
  125. public static function getByNickname($nickname, array $fields = [])
  126. {
  127. return DBA::selectFirst('user', $fields, ['nickname' => $nickname]);
  128. }
  129. /**
  130. * @brief Returns the user id of a given profile URL
  131. *
  132. * @param string $url
  133. *
  134. * @return integer user id
  135. * @throws Exception
  136. */
  137. public static function getIdForURL($url)
  138. {
  139. $self = DBA::selectFirst('contact', ['uid'], ['nurl' => Strings::normaliseLink($url), 'self' => true]);
  140. if (!DBA::isResult($self)) {
  141. return false;
  142. } else {
  143. return $self['uid'];
  144. }
  145. }
  146. /**
  147. * Get a user based on its email
  148. *
  149. * @param string $email
  150. * @param array $fields
  151. *
  152. * @return array|boolean User record if it exists, false otherwise
  153. *
  154. * @throws Exception
  155. */
  156. public static function getByEmail($email, array $fields = [])
  157. {
  158. return DBA::selectFirst('user', $fields, ['email' => $email]);
  159. }
  160. /**
  161. * @brief Get owner data by user id
  162. *
  163. * @param int $uid
  164. * @param boolean $check_valid Test if data is invalid and correct it
  165. * @return boolean|array
  166. * @throws Exception
  167. */
  168. public static function getOwnerDataById($uid, $check_valid = true)
  169. {
  170. $r = DBA::fetchFirst(
  171. "SELECT
  172. `contact`.*,
  173. `user`.`prvkey` AS `uprvkey`,
  174. `user`.`timezone`,
  175. `user`.`nickname`,
  176. `user`.`sprvkey`,
  177. `user`.`spubkey`,
  178. `user`.`page-flags`,
  179. `user`.`account-type`,
  180. `user`.`prvnets`,
  181. `user`.`account_removed`,
  182. `user`.`hidewall`
  183. FROM `contact`
  184. INNER JOIN `user`
  185. ON `user`.`uid` = `contact`.`uid`
  186. WHERE `contact`.`uid` = ?
  187. AND `contact`.`self`
  188. LIMIT 1",
  189. $uid
  190. );
  191. if (!DBA::isResult($r)) {
  192. return false;
  193. }
  194. if (empty($r['nickname'])) {
  195. return false;
  196. }
  197. if (!$check_valid) {
  198. return $r;
  199. }
  200. // Check if the returned data is valid, otherwise fix it. See issue #6122
  201. // Check for correct url and normalised nurl
  202. $url = DI::baseUrl() . '/profile/' . $r['nickname'];
  203. $repair = ($r['url'] != $url) || ($r['nurl'] != Strings::normaliseLink($r['url']));
  204. if (!$repair) {
  205. // Check if "addr" is present and correct
  206. $addr = $r['nickname'] . '@' . substr(DI::baseUrl(), strpos(DI::baseUrl(), '://') + 3);
  207. $repair = ($addr != $r['addr']);
  208. }
  209. if (!$repair) {
  210. // Check if the avatar field is filled and the photo directs to the correct path
  211. $avatar = Photo::selectFirst(['resource-id'], ['uid' => $uid, 'profile' => true]);
  212. if (DBA::isResult($avatar)) {
  213. $repair = empty($r['avatar']) || !strpos($r['photo'], $avatar['resource-id']);
  214. }
  215. }
  216. if ($repair) {
  217. Contact::updateSelfFromUserID($uid);
  218. // Return the corrected data and avoid a loop
  219. $r = self::getOwnerDataById($uid, false);
  220. }
  221. return $r;
  222. }
  223. /**
  224. * @brief Get owner data by nick name
  225. *
  226. * @param int $nick
  227. * @return boolean|array
  228. * @throws Exception
  229. */
  230. public static function getOwnerDataByNick($nick)
  231. {
  232. $user = DBA::selectFirst('user', ['uid'], ['nickname' => $nick]);
  233. if (!DBA::isResult($user)) {
  234. return false;
  235. }
  236. return self::getOwnerDataById($user['uid']);
  237. }
  238. /**
  239. * @brief Returns the default group for a given user and network
  240. *
  241. * @param int $uid User id
  242. * @param string $network network name
  243. *
  244. * @return int group id
  245. * @throws \Friendica\Network\HTTPException\InternalServerErrorException
  246. */
  247. public static function getDefaultGroup($uid, $network = '')
  248. {
  249. $default_group = 0;
  250. if ($network == Protocol::OSTATUS) {
  251. $default_group = DI::pConfig()->get($uid, "ostatus", "default_group");
  252. }
  253. if ($default_group != 0) {
  254. return $default_group;
  255. }
  256. $user = DBA::selectFirst('user', ['def_gid'], ['uid' => $uid]);
  257. if (DBA::isResult($user)) {
  258. $default_group = $user["def_gid"];
  259. }
  260. return $default_group;
  261. }
  262. /**
  263. * Authenticate a user with a clear text password
  264. *
  265. * @brief Authenticate a user with a clear text password
  266. * @param mixed $user_info
  267. * @param string $password
  268. * @param bool $third_party
  269. * @return int|boolean
  270. * @deprecated since version 3.6
  271. * @see User::getIdFromPasswordAuthentication()
  272. */
  273. public static function authenticate($user_info, $password, $third_party = false)
  274. {
  275. try {
  276. return self::getIdFromPasswordAuthentication($user_info, $password, $third_party);
  277. } catch (Exception $ex) {
  278. return false;
  279. }
  280. }
  281. /**
  282. * Returns the user id associated with a successful password authentication
  283. *
  284. * @brief Authenticate a user with a clear text password
  285. * @param mixed $user_info
  286. * @param string $password
  287. * @param bool $third_party
  288. * @return int User Id if authentication is successful
  289. * @throws Exception
  290. */
  291. public static function getIdFromPasswordAuthentication($user_info, $password, $third_party = false)
  292. {
  293. $user = self::getAuthenticationInfo($user_info);
  294. if ($third_party && DI::pConfig()->get($user['uid'], '2fa', 'verified')) {
  295. // Third-party apps can't verify two-factor authentication, we use app-specific passwords instead
  296. if (AppSpecificPassword::authenticateUser($user['uid'], $password)) {
  297. return $user['uid'];
  298. }
  299. } elseif (strpos($user['password'], '$') === false) {
  300. //Legacy hash that has not been replaced by a new hash yet
  301. if (self::hashPasswordLegacy($password) === $user['password']) {
  302. self::updatePasswordHashed($user['uid'], self::hashPassword($password));
  303. return $user['uid'];
  304. }
  305. } elseif (!empty($user['legacy_password'])) {
  306. //Legacy hash that has been double-hashed and not replaced by a new hash yet
  307. //Warning: `legacy_password` is not necessary in sync with the content of `password`
  308. if (password_verify(self::hashPasswordLegacy($password), $user['password'])) {
  309. self::updatePasswordHashed($user['uid'], self::hashPassword($password));
  310. return $user['uid'];
  311. }
  312. } elseif (password_verify($password, $user['password'])) {
  313. //New password hash
  314. if (password_needs_rehash($user['password'], PASSWORD_DEFAULT)) {
  315. self::updatePasswordHashed($user['uid'], self::hashPassword($password));
  316. }
  317. return $user['uid'];
  318. }
  319. throw new Exception(L10n::t('Login failed'));
  320. }
  321. /**
  322. * Returns authentication info from various parameters types
  323. *
  324. * User info can be any of the following:
  325. * - User DB object
  326. * - User Id
  327. * - User email or username or nickname
  328. * - User array with at least the uid and the hashed password
  329. *
  330. * @param mixed $user_info
  331. * @return array
  332. * @throws Exception
  333. */
  334. private static function getAuthenticationInfo($user_info)
  335. {
  336. $user = null;
  337. if (is_object($user_info) || is_array($user_info)) {
  338. if (is_object($user_info)) {
  339. $user = (array) $user_info;
  340. } else {
  341. $user = $user_info;
  342. }
  343. if (
  344. !isset($user['uid'])
  345. || !isset($user['password'])
  346. || !isset($user['legacy_password'])
  347. ) {
  348. throw new Exception(L10n::t('Not enough information to authenticate'));
  349. }
  350. } elseif (is_int($user_info) || is_string($user_info)) {
  351. if (is_int($user_info)) {
  352. $user = DBA::selectFirst(
  353. 'user',
  354. ['uid', 'password', 'legacy_password'],
  355. [
  356. 'uid' => $user_info,
  357. 'blocked' => 0,
  358. 'account_expired' => 0,
  359. 'account_removed' => 0,
  360. 'verified' => 1
  361. ]
  362. );
  363. } else {
  364. $fields = ['uid', 'password', 'legacy_password'];
  365. $condition = [
  366. "(`email` = ? OR `username` = ? OR `nickname` = ?)
  367. AND NOT `blocked` AND NOT `account_expired` AND NOT `account_removed` AND `verified`",
  368. $user_info, $user_info, $user_info
  369. ];
  370. $user = DBA::selectFirst('user', $fields, $condition);
  371. }
  372. if (!DBA::isResult($user)) {
  373. throw new Exception(L10n::t('User not found'));
  374. }
  375. }
  376. return $user;
  377. }
  378. /**
  379. * Generates a human-readable random password
  380. *
  381. * @return string
  382. */
  383. public static function generateNewPassword()
  384. {
  385. return ucfirst(Strings::getRandomName(8)) . random_int(1000, 9999);
  386. }
  387. /**
  388. * Checks if the provided plaintext password has been exposed or not
  389. *
  390. * @param string $password
  391. * @return bool
  392. * @throws Exception
  393. */
  394. public static function isPasswordExposed($password)
  395. {
  396. $cache = new \DivineOmega\DOFileCachePSR6\CacheItemPool();
  397. $cache->changeConfig([
  398. 'cacheDirectory' => get_temppath() . '/password-exposed-cache/',
  399. ]);
  400. try {
  401. $passwordExposedChecker = new PasswordExposed\PasswordExposedChecker(null, $cache);
  402. return $passwordExposedChecker->passwordExposed($password) === PasswordExposed\PasswordStatus::EXPOSED;
  403. } catch (\Exception $e) {
  404. Logger::error('Password Exposed Exception: ' . $e->getMessage(), [
  405. 'code' => $e->getCode(),
  406. 'file' => $e->getFile(),
  407. 'line' => $e->getLine(),
  408. 'trace' => $e->getTraceAsString()
  409. ]);
  410. return false;
  411. }
  412. }
  413. /**
  414. * Legacy hashing function, kept for password migration purposes
  415. *
  416. * @param string $password
  417. * @return string
  418. */
  419. private static function hashPasswordLegacy($password)
  420. {
  421. return hash('whirlpool', $password);
  422. }
  423. /**
  424. * Global user password hashing function
  425. *
  426. * @param string $password
  427. * @return string
  428. * @throws Exception
  429. */
  430. public static function hashPassword($password)
  431. {
  432. if (!trim($password)) {
  433. throw new Exception(L10n::t('Password can\'t be empty'));
  434. }
  435. return password_hash($password, PASSWORD_DEFAULT);
  436. }
  437. /**
  438. * Updates a user row with a new plaintext password
  439. *
  440. * @param int $uid
  441. * @param string $password
  442. * @return bool
  443. * @throws Exception
  444. */
  445. public static function updatePassword($uid, $password)
  446. {
  447. $password = trim($password);
  448. if (empty($password)) {
  449. throw new Exception(L10n::t('Empty passwords are not allowed.'));
  450. }
  451. if (!Config::get('system', 'disable_password_exposed', false) && self::isPasswordExposed($password)) {
  452. throw new Exception(L10n::t('The new password has been exposed in a public data dump, please choose another.'));
  453. }
  454. $allowed_characters = '!"#$%&\'()*+,-./;<=>?@[\]^_`{|}~';
  455. if (!preg_match('/^[a-z0-9' . preg_quote($allowed_characters, '/') . ']+$/i', $password)) {
  456. throw new Exception(L10n::t('The password can\'t contain accentuated letters, white spaces or colons (:)'));
  457. }
  458. return self::updatePasswordHashed($uid, self::hashPassword($password));
  459. }
  460. /**
  461. * Updates a user row with a new hashed password.
  462. * Empties the password reset token field just in case.
  463. *
  464. * @param int $uid
  465. * @param string $pasword_hashed
  466. * @return bool
  467. * @throws Exception
  468. */
  469. private static function updatePasswordHashed($uid, $pasword_hashed)
  470. {
  471. $fields = [
  472. 'password' => $pasword_hashed,
  473. 'pwdreset' => null,
  474. 'pwdreset_time' => null,
  475. 'legacy_password' => false
  476. ];
  477. return DBA::update('user', $fields, ['uid' => $uid]);
  478. }
  479. /**
  480. * @brief Checks if a nickname is in the list of the forbidden nicknames
  481. *
  482. * Check if a nickname is forbidden from registration on the node by the
  483. * admin. Forbidden nicknames (e.g. role namess) can be configured in the
  484. * admin panel.
  485. *
  486. * @param string $nickname The nickname that should be checked
  487. * @return boolean True is the nickname is blocked on the node
  488. * @throws \Friendica\Network\HTTPException\InternalServerErrorException
  489. */
  490. public static function isNicknameBlocked($nickname)
  491. {
  492. $forbidden_nicknames = Config::get('system', 'forbidden_nicknames', '');
  493. // if the config variable is empty return false
  494. if (empty($forbidden_nicknames)) {
  495. return false;
  496. }
  497. // check if the nickname is in the list of blocked nicknames
  498. $forbidden = explode(',', $forbidden_nicknames);
  499. $forbidden = array_map('trim', $forbidden);
  500. if (in_array(strtolower($nickname), $forbidden)) {
  501. return true;
  502. }
  503. // else return false
  504. return false;
  505. }
  506. /**
  507. * @brief Catch-all user creation function
  508. *
  509. * Creates a user from the provided data array, either form fields or OpenID.
  510. * Required: { username, nickname, email } or { openid_url }
  511. *
  512. * Performs the following:
  513. * - Sends to the OpenId auth URL (if relevant)
  514. * - Creates new key pairs for crypto
  515. * - Create self-contact
  516. * - Create profile image
  517. *
  518. * @param array $data
  519. * @return array
  520. * @throws \ErrorException
  521. * @throws \Friendica\Network\HTTPException\InternalServerErrorException
  522. * @throws \ImagickException
  523. * @throws Exception
  524. */
  525. public static function create(array $data)
  526. {
  527. $return = ['user' => null, 'password' => ''];
  528. $using_invites = Config::get('system', 'invitation_only');
  529. $invite_id = !empty($data['invite_id']) ? Strings::escapeTags(trim($data['invite_id'])) : '';
  530. $username = !empty($data['username']) ? Strings::escapeTags(trim($data['username'])) : '';
  531. $nickname = !empty($data['nickname']) ? Strings::escapeTags(trim($data['nickname'])) : '';
  532. $email = !empty($data['email']) ? Strings::escapeTags(trim($data['email'])) : '';
  533. $openid_url = !empty($data['openid_url']) ? Strings::escapeTags(trim($data['openid_url'])) : '';
  534. $photo = !empty($data['photo']) ? Strings::escapeTags(trim($data['photo'])) : '';
  535. $password = !empty($data['password']) ? trim($data['password']) : '';
  536. $password1 = !empty($data['password1']) ? trim($data['password1']) : '';
  537. $confirm = !empty($data['confirm']) ? trim($data['confirm']) : '';
  538. $blocked = !empty($data['blocked']);
  539. $verified = !empty($data['verified']);
  540. $language = !empty($data['language']) ? Strings::escapeTags(trim($data['language'])) : 'en';
  541. $publish = !empty($data['profile_publish_reg']);
  542. $netpublish = $publish && Config::get('system', 'directory');
  543. if ($password1 != $confirm) {
  544. throw new Exception(L10n::t('Passwords do not match. Password unchanged.'));
  545. } elseif ($password1 != '') {
  546. $password = $password1;
  547. }
  548. if ($using_invites) {
  549. if (!$invite_id) {
  550. throw new Exception(L10n::t('An invitation is required.'));
  551. }
  552. if (!Register::existsByHash($invite_id)) {
  553. throw new Exception(L10n::t('Invitation could not be verified.'));
  554. }
  555. }
  556. /// @todo Check if this part is really needed. We should have fetched all this data in advance
  557. if (empty($username) || empty($email) || empty($nickname)) {
  558. if ($openid_url) {
  559. if (!Network::isUrlValid($openid_url)) {
  560. throw new Exception(L10n::t('Invalid OpenID url'));
  561. }
  562. $_SESSION['register'] = 1;
  563. $_SESSION['openid'] = $openid_url;
  564. $openid = new LightOpenID(DI::baseUrl()->getHostname());
  565. $openid->identity = $openid_url;
  566. $openid->returnUrl = DI::baseUrl() . '/openid';
  567. $openid->required = ['namePerson/friendly', 'contact/email', 'namePerson'];
  568. $openid->optional = ['namePerson/first', 'media/image/aspect11', 'media/image/default'];
  569. try {
  570. $authurl = $openid->authUrl();
  571. } catch (Exception $e) {
  572. throw new Exception(L10n::t('We encountered a problem while logging in with the OpenID you provided. Please check the correct spelling of the ID.') . EOL . EOL . L10n::t('The error message was:') . $e->getMessage(), 0, $e);
  573. }
  574. System::externalRedirect($authurl);
  575. // NOTREACHED
  576. }
  577. throw new Exception(L10n::t('Please enter the required information.'));
  578. }
  579. if (!Network::isUrlValid($openid_url)) {
  580. $openid_url = '';
  581. }
  582. // collapse multiple spaces in name
  583. $username = preg_replace('/ +/', ' ', $username);
  584. $username_min_length = max(1, min(64, intval(Config::get('system', 'username_min_length', 3))));
  585. $username_max_length = max(1, min(64, intval(Config::get('system', 'username_max_length', 48))));
  586. if ($username_min_length > $username_max_length) {
  587. Logger::log(L10n::t('system.username_min_length (%s) and system.username_max_length (%s) are excluding each other, swapping values.', $username_min_length, $username_max_length), Logger::WARNING);
  588. $tmp = $username_min_length;
  589. $username_min_length = $username_max_length;
  590. $username_max_length = $tmp;
  591. }
  592. if (mb_strlen($username) < $username_min_length) {
  593. throw new Exception(L10n::tt('Username should be at least %s character.', 'Username should be at least %s characters.', $username_min_length));
  594. }
  595. if (mb_strlen($username) > $username_max_length) {
  596. throw new Exception(L10n::tt('Username should be at most %s character.', 'Username should be at most %s characters.', $username_max_length));
  597. }
  598. // So now we are just looking for a space in the full name.
  599. $loose_reg = Config::get('system', 'no_regfullname');
  600. if (!$loose_reg) {
  601. $username = mb_convert_case($username, MB_CASE_TITLE, 'UTF-8');
  602. if (strpos($username, ' ') === false) {
  603. throw new Exception(L10n::t("That doesn't appear to be your full (First Last) name."));
  604. }
  605. }
  606. if (!Network::isEmailDomainAllowed($email)) {
  607. throw new Exception(L10n::t('Your email domain is not among those allowed on this site.'));
  608. }
  609. if (!filter_var($email, FILTER_VALIDATE_EMAIL) || !Network::isEmailDomainValid($email)) {
  610. throw new Exception(L10n::t('Not a valid email address.'));
  611. }
  612. if (self::isNicknameBlocked($nickname)) {
  613. throw new Exception(L10n::t('The nickname was blocked from registration by the nodes admin.'));
  614. }
  615. if (Config::get('system', 'block_extended_register', false) && DBA::exists('user', ['email' => $email])) {
  616. throw new Exception(L10n::t('Cannot use that email.'));
  617. }
  618. // Disallow somebody creating an account using openid that uses the admin email address,
  619. // since openid bypasses email verification. We'll allow it if there is not yet an admin account.
  620. if (Config::get('config', 'admin_email') && strlen($openid_url)) {
  621. $adminlist = explode(',', str_replace(' ', '', strtolower(Config::get('config', 'admin_email'))));
  622. if (in_array(strtolower($email), $adminlist)) {
  623. throw new Exception(L10n::t('Cannot use that email.'));
  624. }
  625. }
  626. $nickname = $data['nickname'] = strtolower($nickname);
  627. if (!preg_match('/^[a-z0-9][a-z0-9\_]*$/', $nickname)) {
  628. throw new Exception(L10n::t('Your nickname can only contain a-z, 0-9 and _.'));
  629. }
  630. // Check existing and deleted accounts for this nickname.
  631. if (
  632. DBA::exists('user', ['nickname' => $nickname])
  633. || DBA::exists('userd', ['username' => $nickname])
  634. ) {
  635. throw new Exception(L10n::t('Nickname is already registered. Please choose another.'));
  636. }
  637. $new_password = strlen($password) ? $password : User::generateNewPassword();
  638. $new_password_encoded = self::hashPassword($new_password);
  639. $return['password'] = $new_password;
  640. $keys = Crypto::newKeypair(4096);
  641. if ($keys === false) {
  642. throw new Exception(L10n::t('SERIOUS ERROR: Generation of security keys failed.'));
  643. }
  644. $prvkey = $keys['prvkey'];
  645. $pubkey = $keys['pubkey'];
  646. // Create another keypair for signing/verifying salmon protocol messages.
  647. $sres = Crypto::newKeypair(512);
  648. $sprvkey = $sres['prvkey'];
  649. $spubkey = $sres['pubkey'];
  650. $insert_result = DBA::insert('user', [
  651. 'guid' => System::createUUID(),
  652. 'username' => $username,
  653. 'password' => $new_password_encoded,
  654. 'email' => $email,
  655. 'openid' => $openid_url,
  656. 'nickname' => $nickname,
  657. 'pubkey' => $pubkey,
  658. 'prvkey' => $prvkey,
  659. 'spubkey' => $spubkey,
  660. 'sprvkey' => $sprvkey,
  661. 'verified' => $verified,
  662. 'blocked' => $blocked,
  663. 'language' => $language,
  664. 'timezone' => 'UTC',
  665. 'register_date' => DateTimeFormat::utcNow(),
  666. 'default-location' => ''
  667. ]);
  668. if ($insert_result) {
  669. $uid = DBA::lastInsertId();
  670. $user = DBA::selectFirst('user', [], ['uid' => $uid]);
  671. } else {
  672. throw new Exception(L10n::t('An error occurred during registration. Please try again.'));
  673. }
  674. if (!$uid) {
  675. throw new Exception(L10n::t('An error occurred during registration. Please try again.'));
  676. }
  677. // if somebody clicked submit twice very quickly, they could end up with two accounts
  678. // due to race condition. Remove this one.
  679. $user_count = DBA::count('user', ['nickname' => $nickname]);
  680. if ($user_count > 1) {
  681. DBA::delete('user', ['uid' => $uid]);
  682. throw new Exception(L10n::t('Nickname is already registered. Please choose another.'));
  683. }
  684. $insert_result = DBA::insert('profile', [
  685. 'uid' => $uid,
  686. 'name' => $username,
  687. 'photo' => DI::baseUrl() . "/photo/profile/{$uid}.jpg",
  688. 'thumb' => DI::baseUrl() . "/photo/avatar/{$uid}.jpg",
  689. 'publish' => $publish,
  690. 'is-default' => 1,
  691. 'net-publish' => $netpublish,
  692. 'profile-name' => L10n::t('default')
  693. ]);
  694. if (!$insert_result) {
  695. DBA::delete('user', ['uid' => $uid]);
  696. throw new Exception(L10n::t('An error occurred creating your default profile. Please try again.'));
  697. }
  698. // Create the self contact
  699. if (!Contact::createSelfFromUserId($uid)) {
  700. DBA::delete('user', ['uid' => $uid]);
  701. throw new Exception(L10n::t('An error occurred creating your self contact. Please try again.'));
  702. }
  703. // Create a group with no members. This allows somebody to use it
  704. // right away as a default group for new contacts.
  705. $def_gid = Group::create($uid, L10n::t('Friends'));
  706. if (!$def_gid) {
  707. DBA::delete('user', ['uid' => $uid]);
  708. throw new Exception(L10n::t('An error occurred creating your default contact group. Please try again.'));
  709. }
  710. $fields = ['def_gid' => $def_gid];
  711. if (Config::get('system', 'newuser_private') && $def_gid) {
  712. $fields['allow_gid'] = '<' . $def_gid . '>';
  713. }
  714. DBA::update('user', $fields, ['uid' => $uid]);
  715. // if we have no OpenID photo try to look up an avatar
  716. if (!strlen($photo)) {
  717. $photo = Network::lookupAvatarByEmail($email);
  718. }
  719. // unless there is no avatar-addon loaded
  720. if (strlen($photo)) {
  721. $photo_failure = false;
  722. $filename = basename($photo);
  723. $img_str = Network::fetchUrl($photo, true);
  724. // guess mimetype from headers or filename
  725. $type = Images::guessType($photo, true);
  726. $Image = new Image($img_str, $type);
  727. if ($Image->isValid()) {
  728. $Image->scaleToSquare(300);
  729. $resource_id = Photo::newResource();
  730. $r = Photo::store($Image, $uid, 0, $resource_id, $filename, L10n::t('Profile Photos'), 4);
  731. if ($r === false) {
  732. $photo_failure = true;
  733. }
  734. $Image->scaleDown(80);
  735. $r = Photo::store($Image, $uid, 0, $resource_id, $filename, L10n::t('Profile Photos'), 5);
  736. if ($r === false) {
  737. $photo_failure = true;
  738. }
  739. $Image->scaleDown(48);
  740. $r = Photo::store($Image, $uid, 0, $resource_id, $filename, L10n::t('Profile Photos'), 6);
  741. if ($r === false) {
  742. $photo_failure = true;
  743. }
  744. if (!$photo_failure) {
  745. Photo::update(['profile' => 1], ['resource-id' => $resource_id]);
  746. }
  747. }
  748. }
  749. Hook::callAll('register_account', $uid);
  750. $return['user'] = $user;
  751. return $return;
  752. }
  753. /**
  754. * @brief Sends pending registration confirmation email
  755. *
  756. * @param array $user User record array
  757. * @param string $sitename
  758. * @param string $siteurl
  759. * @param string $password Plaintext password
  760. * @return NULL|boolean from notification() and email() inherited
  761. * @throws \Friendica\Network\HTTPException\InternalServerErrorException
  762. */
  763. public static function sendRegisterPendingEmail($user, $sitename, $siteurl, $password)
  764. {
  765. $body = Strings::deindent(L10n::t(
  766. '
  767. Dear %1$s,
  768. Thank you for registering at %2$s. Your account is pending for approval by the administrator.
  769. Your login details are as follows:
  770. Site Location: %3$s
  771. Login Name: %4$s
  772. Password: %5$s
  773. ',
  774. $user['username'],
  775. $sitename,
  776. $siteurl,
  777. $user['nickname'],
  778. $password
  779. ));
  780. return notification([
  781. 'type' => SYSTEM_EMAIL,
  782. 'uid' => $user['uid'],
  783. 'to_email' => $user['email'],
  784. 'subject' => L10n::t('Registration at %s', $sitename),
  785. 'body' => $body
  786. ]);
  787. }
  788. /**
  789. * @brief Sends registration confirmation
  790. *
  791. * It's here as a function because the mail is sent from different parts
  792. *
  793. * @param L10n\L10n $l10n The used language
  794. * @param array $user User record array
  795. * @param string $sitename
  796. * @param string $siteurl
  797. * @param string $password Plaintext password
  798. * @return NULL|boolean from notification() and email() inherited
  799. * @throws \Friendica\Network\HTTPException\InternalServerErrorException
  800. */
  801. public static function sendRegisterOpenEmail(L10n\L10n $l10n, $user, $sitename, $siteurl, $password)
  802. {
  803. $preamble = Strings::deindent($l10n->t(
  804. '
  805. Dear %1$s,
  806. Thank you for registering at %2$s. Your account has been created.
  807. ',
  808. $user['username'],
  809. $sitename
  810. ));
  811. $body = Strings::deindent($l10n->t(
  812. '
  813. The login details are as follows:
  814. Site Location: %3$s
  815. Login Name: %1$s
  816. Password: %5$s
  817. You may change your password from your account "Settings" page after logging
  818. in.
  819. Please take a few moments to review the other account settings on that page.
  820. You may also wish to add some basic information to your default profile
  821. ' . "\x28" . 'on the "Profiles" page' . "\x29" . ' so that other people can easily find you.
  822. We recommend setting your full name, adding a profile photo,
  823. adding some profile "keywords" ' . "\x28" . 'very useful in making new friends' . "\x29" . ' - and
  824. perhaps what country you live in; if you do not wish to be more specific
  825. than that.
  826. We fully respect your right to privacy, and none of these items are necessary.
  827. If you are new and do not know anybody here, they may help
  828. you to make some new and interesting friends.
  829. If you ever want to delete your account, you can do so at %3$s/removeme
  830. Thank you and welcome to %2$s.',
  831. $user['nickname'],
  832. $sitename,
  833. $siteurl,
  834. $user['username'],
  835. $password
  836. ));
  837. return notification([
  838. 'uid' => $user['uid'],
  839. 'language' => $user['language'],
  840. 'type' => SYSTEM_EMAIL,
  841. 'to_email' => $user['email'],
  842. 'subject' => L10n::t('Registration details for %s', $sitename),
  843. 'preamble' => $preamble,
  844. 'body' => $body
  845. ]);
  846. }
  847. /**
  848. * @param object $uid user to remove
  849. * @return bool
  850. * @throws \Friendica\Network\HTTPException\InternalServerErrorException
  851. */
  852. public static function remove($uid)
  853. {
  854. if (!$uid) {
  855. return false;
  856. }
  857. Logger::log('Removing user: ' . $uid);
  858. $user = DBA::selectFirst('user', [], ['uid' => $uid]);
  859. Hook::callAll('remove_user', $user);
  860. // save username (actually the nickname as it is guaranteed
  861. // unique), so it cannot be re-registered in the future.
  862. DBA::insert('userd', ['username' => $user['nickname']]);
  863. // The user and related data will be deleted in "cron_expire_and_remove_users" (cronjobs.php)
  864. DBA::update('user', ['account_removed' => true, 'account_expires_on' => DateTimeFormat::utc('now + 7 day')], ['uid' => $uid]);
  865. Worker::add(PRIORITY_HIGH, 'Notifier', Delivery::REMOVAL, $uid);
  866. // Send an update to the directory
  867. $self = DBA::selectFirst('contact', ['url'], ['uid' => $uid, 'self' => true]);
  868. Worker::add(PRIORITY_LOW, 'Directory', $self['url']);
  869. // Remove the user relevant data
  870. Worker::add(PRIORITY_NEGLIGIBLE, 'RemoveUser', $uid);
  871. return true;
  872. }
  873. /**
  874. * Return all identities to a user
  875. *
  876. * @param int $uid The user id
  877. * @return array All identities for this user
  878. *
  879. * Example for a return:
  880. * [
  881. * [
  882. * 'uid' => 1,
  883. * 'username' => 'maxmuster',
  884. * 'nickname' => 'Max Mustermann'
  885. * ],
  886. * [
  887. * 'uid' => 2,
  888. * 'username' => 'johndoe',
  889. * 'nickname' => 'John Doe'
  890. * ]
  891. * ]
  892. * @throws Exception
  893. */
  894. public static function identities($uid)
  895. {
  896. $identities = [];
  897. $user = DBA::selectFirst('user', ['uid', 'nickname', 'username', 'parent-uid'], ['uid' => $uid]);
  898. if (!DBA::isResult($user)) {
  899. return $identities;
  900. }
  901. if ($user['parent-uid'] == 0) {
  902. // First add our own entry
  903. $identities = [[
  904. 'uid' => $user['uid'],
  905. 'username' => $user['username'],
  906. 'nickname' => $user['nickname']
  907. ]];
  908. // Then add all the children
  909. $r = DBA::select(
  910. 'user',
  911. ['uid', 'username', 'nickname'],
  912. ['parent-uid' => $user['uid'], 'account_removed' => false]
  913. );
  914. if (DBA::isResult($r)) {
  915. $identities = array_merge($identities, DBA::toArray($r));
  916. }
  917. } else {
  918. // First entry is our parent
  919. $r = DBA::select(
  920. 'user',
  921. ['uid', 'username', 'nickname'],
  922. ['uid' => $user['parent-uid'], 'account_removed' => false]
  923. );
  924. if (DBA::isResult($r)) {
  925. $identities = DBA::toArray($r);
  926. }
  927. // Then add all siblings
  928. $r = DBA::select(
  929. 'user',
  930. ['uid', 'username', 'nickname'],
  931. ['parent-uid' => $user['parent-uid'], 'account_removed' => false]
  932. );
  933. if (DBA::isResult($r)) {
  934. $identities = array_merge($identities, DBA::toArray($r));
  935. }
  936. }
  937. $r = DBA::p(
  938. "SELECT `user`.`uid`, `user`.`username`, `user`.`nickname`
  939. FROM `manage`
  940. INNER JOIN `user` ON `manage`.`mid` = `user`.`uid`
  941. WHERE `user`.`account_removed` = 0 AND `manage`.`uid` = ?",
  942. $user['uid']
  943. );
  944. if (DBA::isResult($r)) {
  945. $identities = array_merge($identities, DBA::toArray($r));
  946. }
  947. return $identities;
  948. }
  949. /**
  950. * Returns statistical information about the current users of this node
  951. *
  952. * @return array
  953. *
  954. * @throws Exception
  955. */
  956. public static function getStatistics()
  957. {
  958. $statistics = [
  959. 'total_users' => 0,
  960. 'active_users_halfyear' => 0,
  961. 'active_users_monthly' => 0,
  962. ];
  963. $userStmt = DBA::p("SELECT `user`.`uid`, `user`.`login_date`, `contact`.`last-item`
  964. FROM `user`
  965. INNER JOIN `profile` ON `profile`.`uid` = `user`.`uid` AND `profile`.`is-default`
  966. INNER JOIN `contact` ON `contact`.`uid` = `user`.`uid` AND `contact`.`self`
  967. WHERE (`profile`.`publish` OR `profile`.`net-publish`) AND `user`.`verified`
  968. AND NOT `user`.`blocked` AND NOT `user`.`account_removed`
  969. AND NOT `user`.`account_expired`");
  970. if (!DBA::isResult($userStmt)) {
  971. return $statistics;
  972. }
  973. $halfyear = time() - (180 * 24 * 60 * 60);
  974. $month = time() - (30 * 24 * 60 * 60);
  975. while ($user = DBA::fetch($userStmt)) {
  976. $statistics['total_users']++;
  977. if ((strtotime($user['login_date']) > $halfyear) || (strtotime($user['last-item']) > $halfyear)
  978. ) {
  979. $statistics['active_users_halfyear']++;
  980. }
  981. if ((strtotime($user['login_date']) > $month) || (strtotime($user['last-item']) > $month)
  982. ) {
  983. $statistics['active_users_monthly']++;
  984. }
  985. }
  986. return $statistics;
  987. }
  988. }