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.

558 lines
16 KiB

4 years ago
4 years ago
4 years ago
  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 Friendica\Core\Config;
  8. use Friendica\Core\PConfig;
  9. use Friendica\Core\System;
  10. use Friendica\Core\Worker;
  11. use Friendica\Database\DBM;
  12. use Friendica\Model\Contact;
  13. use Friendica\Model\Group;
  14. use Friendica\Model\Photo;
  15. use Friendica\Object\Image;
  16. use dba;
  17. use Exception;
  18. require_once 'boot.php';
  19. require_once 'include/crypto.php';
  20. require_once 'include/dba.php';
  21. require_once 'include/enotify.php';
  22. require_once 'include/network.php';
  23. require_once 'library/openid.php';
  24. require_once 'include/pgettext.php';
  25. require_once 'include/plugin.php';
  26. require_once 'include/text.php';
  27. /**
  28. * @brief This class handles User related functions
  29. */
  30. class User
  31. {
  32. /**
  33. * @brief Get owner data by user id
  34. *
  35. * @param int $uid
  36. * @return boolean|array
  37. */
  38. public static function getOwnerDataById($uid) {
  39. $r = dba::fetch_first("SELECT
  40. `contact`.*,
  41. `user`.`prvkey` AS `uprvkey`,
  42. `user`.`timezone`,
  43. `user`.`nickname`,
  44. `user`.`sprvkey`,
  45. `user`.`spubkey`,
  46. `user`.`page-flags`,
  47. `user`.`account-type`,
  48. `user`.`prvnets`
  49. FROM `contact`
  50. INNER JOIN `user`
  51. ON `user`.`uid` = `contact`.`uid`
  52. WHERE `contact`.`uid` = ?
  53. AND `contact`.`self`
  54. LIMIT 1",
  55. $uid
  56. );
  57. if (!DBM::is_result($r)) {
  58. return false;
  59. }
  60. return $r;
  61. }
  62. /**
  63. * @brief Returns the default group for a given user and network
  64. *
  65. * @param int $uid User id
  66. * @param string $network network name
  67. *
  68. * @return int group id
  69. */
  70. public static function getDefaultGroup($uid, $network = '')
  71. {
  72. $default_group = 0;
  73. if ($network == NETWORK_OSTATUS) {
  74. $default_group = PConfig::get($uid, "ostatus", "default_group");
  75. }
  76. if ($default_group != 0) {
  77. return $default_group;
  78. }
  79. $user = dba::select('user', ['def_gid'], ['uid' => $uid], ['limit' => 1]);
  80. if (DBM::is_result($user)) {
  81. $default_group = $user["def_gid"];
  82. }
  83. return $default_group;
  84. }
  85. /**
  86. * @brief Authenticate a user with a clear text password
  87. *
  88. * User info can be any of the following:
  89. * - User DB object
  90. * - User Id
  91. * - User email or username or nickname
  92. * - User array with at least the uid and the hashed password
  93. *
  94. * @param mixed $user_info
  95. * @param string $password
  96. * @return boolean
  97. */
  98. public static function authenticate($user_info, $password)
  99. {
  100. if (is_object($user_info)) {
  101. $user = (array) $user_info;
  102. } elseif (is_int($user_info)) {
  103. $user = dba::select('user',
  104. ['uid', 'password'],
  105. [
  106. 'uid' => $user_info,
  107. 'blocked' => 0,
  108. 'account_expired' => 0,
  109. 'account_removed' => 0,
  110. 'verified' => 1
  111. ],
  112. ['limit' => 1]
  113. );
  114. } elseif (is_string($user_info)) {
  115. $user = dba::fetch_first('SELECT `uid`, `password`
  116. FROM `user`
  117. WHERE (`email` = ? OR `username` = ? OR `nickname` = ?)
  118. AND `blocked` = 0
  119. AND `account_expired` = 0
  120. AND `account_removed` = 0
  121. AND `verified` = 1
  122. LIMIT 1',
  123. $user_info,
  124. $user_info,
  125. $user_info
  126. );
  127. } else {
  128. $user = $user_info;
  129. }
  130. if (!DBM::is_result($user) || !isset($user['uid']) || !isset($user['password'])) {
  131. return false;
  132. }
  133. $password_hashed = hash('whirlpool', $password);
  134. if ($password_hashed !== $user['password']) {
  135. return false;
  136. }
  137. return $user['uid'];
  138. }
  139. /**
  140. * @brief Catch-all user creation function
  141. *
  142. * Creates a user from the provided data array, either form fields or OpenID.
  143. * Required: { username, nickname, email } or { openid_url }
  144. *
  145. * Performs the following:
  146. * - Sends to the OpenId auth URL (if relevant)
  147. * - Creates new key pairs for crypto
  148. * - Create self-contact
  149. * - Create profile image
  150. *
  151. * @param array $data
  152. * @return string
  153. * @throw Exception
  154. */
  155. public static function create(array $data)
  156. {
  157. $a = get_app();
  158. $return = ['user' => null, 'password' => ''];
  159. $using_invites = Config::get('system', 'invitation_only');
  160. $num_invites = Config::get('system', 'number_invites');
  161. $invite_id = x($data, 'invite_id') ? notags(trim($data['invite_id'])) : '';
  162. $username = x($data, 'username') ? notags(trim($data['username'])) : '';
  163. $nickname = x($data, 'nickname') ? notags(trim($data['nickname'])) : '';
  164. $email = x($data, 'email') ? notags(trim($data['email'])) : '';
  165. $openid_url = x($data, 'openid_url') ? notags(trim($data['openid_url'])) : '';
  166. $photo = x($data, 'photo') ? notags(trim($data['photo'])) : '';
  167. $password = x($data, 'password') ? trim($data['password']) : '';
  168. $password1 = x($data, 'password1') ? trim($data['password1']) : '';
  169. $confirm = x($data, 'confirm') ? trim($data['confirm']) : '';
  170. $blocked = x($data, 'blocked') ? intval($data['blocked']) : 0;
  171. $verified = x($data, 'verified') ? intval($data['verified']) : 0;
  172. $publish = x($data, 'profile_publish_reg') && intval($data['profile_publish_reg']) ? 1 : 0;
  173. $netpublish = strlen(Config::get('system', 'directory')) ? $publish : 0;
  174. if ($password1 != $confirm) {
  175. throw new Exception(t('Passwords do not match. Password unchanged.'));
  176. } elseif ($password1 != '') {
  177. $password = $password1;
  178. }
  179. $tmp_str = $openid_url;
  180. if ($using_invites) {
  181. if (!$invite_id) {
  182. throw new Exception(t('An invitation is required.'));
  183. }
  184. if (!dba::exists('register', ['hash' => $invite_id])) {
  185. throw new Exception(t('Invitation could not be verified.'));
  186. }
  187. }
  188. if (!x($username) || !x($email) || !x($nickname)) {
  189. if ($openid_url) {
  190. if (!validate_url($tmp_str)) {
  191. throw new Exception(t('Invalid OpenID url'));
  192. }
  193. $_SESSION['register'] = 1;
  194. $_SESSION['openid'] = $openid_url;
  195. $openid = new \LightOpenID;
  196. $openid->identity = $openid_url;
  197. $openid->returnUrl = System::baseUrl() . '/openid';
  198. $openid->required = array('namePerson/friendly', 'contact/email', 'namePerson');
  199. $openid->optional = array('namePerson/first', 'media/image/aspect11', 'media/image/default');
  200. try {
  201. $authurl = $openid->authUrl();
  202. } catch (Exception $e) {
  203. throw new Exception(t('We encountered a problem while logging in with the OpenID you provided. Please check the correct spelling of the ID.') . EOL . EOL . t('The error message was:') . $e->getMessage(), 0, $e);
  204. }
  205. goaway($authurl);
  206. // NOTREACHED
  207. }
  208. throw new Exception(t('Please enter the required information.'));
  209. }
  210. if (!validate_url($tmp_str)) {
  211. $openid_url = '';
  212. }
  213. $err = '';
  214. // collapse multiple spaces in name
  215. $username = preg_replace('/ +/', ' ', $username);
  216. if (mb_strlen($username) > 48) {
  217. throw new Exception(t('Please use a shorter name.'));
  218. }
  219. if (mb_strlen($username) < 3) {
  220. throw new Exception(t('Name too short.'));
  221. }
  222. // So now we are just looking for a space in the full name.
  223. $loose_reg = Config::get('system', 'no_regfullname');
  224. if (!$loose_reg) {
  225. $username = mb_convert_case($username, MB_CASE_TITLE, 'UTF-8');
  226. if (!strpos($username, ' ')) {
  227. throw new Exception(t("That doesn't appear to be your full \x28First Last\x29 name."));
  228. }
  229. }
  230. if (!allowed_email($email)) {
  231. throw new Exception(t('Your email domain is not among those allowed on this site.'));
  232. }
  233. if (!valid_email($email) || !validate_email($email)) {
  234. throw new Exception(t('Not a valid email address.'));
  235. }
  236. if (dba::exists('user', ['email' => $email])) {
  237. throw new Exception(t('Cannot use that email.'));
  238. }
  239. // Disallow somebody creating an account using openid that uses the admin email address,
  240. // since openid bypasses email verification. We'll allow it if there is not yet an admin account.
  241. if (x($a->config, 'admin_email') && strlen($openid_url)) {
  242. $adminlist = explode(',', str_replace(' ', '', strtolower($a->config['admin_email'])));
  243. if (in_array(strtolower($email), $adminlist)) {
  244. throw new Exception(t('Cannot use that email.'));
  245. }
  246. }
  247. $nickname = $data['nickname'] = strtolower($nickname);
  248. if (!preg_match('/^[a-z0-9][a-z0-9\_]*$/', $nickname)) {
  249. throw new Exception(t('Your "nickname" can only contain "a-z", "0-9" and "_".'));
  250. }
  251. // Check existing and deleted accounts for this nickname.
  252. if (dba::exists('user', ['nickname' => $nickname])
  253. || dba::exists('userd', ['username' => $nickname])
  254. ) {
  255. throw new Exception(t('Nickname is already registered. Please choose another.'));
  256. }
  257. $new_password = strlen($password) ? $password : autoname(6) . mt_rand(100, 9999);
  258. $new_password_encoded = hash('whirlpool', $new_password);
  259. $return['password'] = $new_password;
  260. $keys = new_keypair(4096);
  261. if ($keys === false) {
  262. throw new Exception(t('SERIOUS ERROR: Generation of security keys failed.'));
  263. }
  264. $prvkey = $keys['prvkey'];
  265. $pubkey = $keys['pubkey'];
  266. // Create another keypair for signing/verifying salmon protocol messages.
  267. $sres = new_keypair(512);
  268. $sprvkey = $sres['prvkey'];
  269. $spubkey = $sres['pubkey'];
  270. $insert_result = dba::insert('user', [
  271. 'guid' => generate_user_guid(),
  272. 'username' => $username,
  273. 'password' => $new_password_encoded,
  274. 'email' => $email,
  275. 'openid' => $openid_url,
  276. 'nickname' => $nickname,
  277. 'pubkey' => $pubkey,
  278. 'prvkey' => $prvkey,
  279. 'spubkey' => $spubkey,
  280. 'sprvkey' => $sprvkey,
  281. 'verified' => $verified,
  282. 'blocked' => $blocked,
  283. 'timezone' => 'UTC',
  284. 'register_date' => datetime_convert(),
  285. 'default-location' => ''
  286. ]);
  287. if ($insert_result) {
  288. $uid = dba::lastInsertId();
  289. $user = dba::select('user', [], ['uid' => $uid], ['limit' => 1]);
  290. } else {
  291. throw new Exception(t('An error occurred during registration. Please try again.'));
  292. }
  293. if (!$uid) {
  294. throw new Exception(t('An error occurred during registration. Please try again.'));
  295. }
  296. // if somebody clicked submit twice very quickly, they could end up with two accounts
  297. // due to race condition. Remove this one.
  298. $user_count = dba::count('user', ['nickname' => $nickname]);
  299. if ($user_count > 1) {
  300. dba::delete('user', ['uid' => $uid]);
  301. throw new Exception(t('Nickname is already registered. Please choose another.'));
  302. }
  303. $insert_result = dba::insert('profile', [
  304. 'uid' => $uid,
  305. 'name' => $username,
  306. 'photo' => System::baseUrl() . "/photo/profile/{$uid}.jpg",
  307. 'thumb' => System::baseUrl() . "/photo/avatar/{$uid}.jpg",
  308. 'publish' => $publish,
  309. 'is-default' => 1,
  310. 'net-publish' => $netpublish,
  311. 'profile-name' => t('default')
  312. ]);
  313. if (!$insert_result) {
  314. dba::delete('user', ['uid' => $uid]);
  315. throw new Exception(t('An error occurred creating your default profile. Please try again.'));
  316. }
  317. // Create the self contact
  318. if (!Contact::createSelfFromUserId($uid)) {
  319. dba::delete('user', ['uid' => $uid]);
  320. throw new Exception(t('An error occurred creating your self contact. Please try again.'));
  321. }
  322. // Create a group with no members. This allows somebody to use it
  323. // right away as a default group for new contacts.
  324. $def_gid = Group::create($uid, t('Friends'));
  325. if (!$def_gid) {
  326. dba::delete('user', ['uid' => $uid]);
  327. throw new Exception(t('An error occurred creating your default contact group. Please try again.'));
  328. }
  329. $fields = ['def_gid' => $def_gid];
  330. if (Config::get('system', 'newuser_private') && $def_gid) {
  331. $fields['allow_gid'] = '<' . $def_gid . '>';
  332. }
  333. dba::update('user', $fields, ['uid' => $uid]);
  334. // if we have no OpenID photo try to look up an avatar
  335. if (!strlen($photo)) {
  336. $photo = avatar_img($email);
  337. }
  338. // unless there is no avatar-plugin loaded
  339. if (strlen($photo)) {
  340. $photo_failure = false;
  341. $filename = basename($photo);
  342. $img_str = fetch_url($photo, true);
  343. // guess mimetype from headers or filename
  344. $type = Image::guessType($photo, true);
  345. $Image = new Image($img_str, $type);
  346. if ($Image->isValid()) {
  347. $Image->scaleToSquare(175);
  348. $hash = photo_new_resource();
  349. $r = Photo::store($Image, $uid, 0, $hash, $filename, t('Profile Photos'), 4);
  350. if ($r === false) {
  351. $photo_failure = true;
  352. }
  353. $Image->scaleDown(80);
  354. $r = Photo::store($Image, $uid, 0, $hash, $filename, t('Profile Photos'), 5);
  355. if ($r === false) {
  356. $photo_failure = true;
  357. }
  358. $Image->scaleDown(48);
  359. $r = Photo::store($Image, $uid, 0, $hash, $filename, t('Profile Photos'), 6);
  360. if ($r === false) {
  361. $photo_failure = true;
  362. }
  363. if (!$photo_failure) {
  364. dba::update('photo', ['profile' => 1], ['resource-id' => $hash]);
  365. }
  366. }
  367. }
  368. call_hooks('register_account', $uid);
  369. $return['user'] = $user;
  370. return $return;
  371. }
  372. /**
  373. * @brief Sends pending registration confiŕmation email
  374. *
  375. * @param string $email
  376. * @param string $sitename
  377. * @param string $username
  378. * @return NULL|boolean from notification() and email() inherited
  379. */
  380. public static function sendRegisterPendingEmail($email, $sitename, $username)
  381. {
  382. $body = deindent(t('
  383. Dear %1$s,
  384. Thank you for registering at %2$s. Your account is pending for approval by the administrator.
  385. '));
  386. $body = sprintf($body, $username, $sitename);
  387. return notification(array(
  388. 'type' => SYSTEM_EMAIL,
  389. 'to_email' => $email,
  390. 'subject'=> sprintf( t('Registration at %s'), $sitename),
  391. 'body' => $body));
  392. }
  393. /**
  394. * @brief Sends registration confirmation
  395. *
  396. * It's here as a function because the mail is sent from different parts
  397. *
  398. * @param string $email
  399. * @param string $sitename
  400. * @param string $siteurl
  401. * @param string $username
  402. * @param string $password
  403. * @return NULL|boolean from notification() and email() inherited
  404. */
  405. public static function sendRegisterOpenEmail($email, $sitename, $siteurl, $username, $password)
  406. {
  407. $preamble = deindent(t('
  408. Dear %1$s,
  409. Thank you for registering at %2$s. Your account has been created.
  410. '));
  411. $body = deindent(t('
  412. The login details are as follows:
  413. Site Location: %3$s
  414. Login Name: %1$s
  415. Password: %5$s
  416. You may change your password from your account "Settings" page after logging
  417. in.
  418. Please take a few moments to review the other account settings on that page.
  419. You may also wish to add some basic information to your default profile
  420. (on the "Profiles" page) so that other people can easily find you.
  421. We recommend setting your full name, adding a profile photo,
  422. adding some profile "keywords" (very useful in making new friends) - and
  423. perhaps what country you live in; if you do not wish to be more specific
  424. than that.
  425. We fully respect your right to privacy, and none of these items are necessary.
  426. If you are new and do not know anybody here, they may help
  427. you to make some new and interesting friends.
  428. Thank you and welcome to %2$s.'));
  429. $preamble = sprintf($preamble, $username, $sitename);
  430. $body = sprintf($body, $email, $sitename, $siteurl, $username, $password);
  431. return notification(array(
  432. 'type' => SYSTEM_EMAIL,
  433. 'to_email' => $email,
  434. 'subject'=> sprintf( t('Registration details for %s'), $sitename),
  435. 'preamble'=> $preamble,
  436. 'body' => $body));
  437. }
  438. /**
  439. * @param object $uid user to remove
  440. * @return void
  441. */
  442. public static function remove($uid)
  443. {
  444. if (!$uid) {
  445. return;
  446. }
  447. logger('Removing user: ' . $uid);
  448. $user = dba::select('user', [], ['uid' => $uid], ['limit' => 1]);
  449. call_hooks('remove_user', $user);
  450. // save username (actually the nickname as it is guaranteed
  451. // unique), so it cannot be re-registered in the future.
  452. dba::insert('userd', ['username' => $user['nickname']]);
  453. // The user and related data will be deleted in "cron_expire_and_remove_users" (cronjobs.php)
  454. dba::update('user', ['account_removed' => true, 'account_expires_on' => datetime_convert()], ['uid' => $uid]);
  455. Worker::add(PRIORITY_HIGH, "Notifier", "removeme", $uid);
  456. // Send an update to the directory
  457. Worker::add(PRIORITY_LOW, "Directory", $user['url']);
  458. if ($uid == local_user()) {
  459. unset($_SESSION['authenticated']);
  460. unset($_SESSION['uid']);
  461. goaway(System::baseUrl());
  462. }
  463. }
  464. }