4 * @file src/Model/User.php
5 * @brief This file includes the User class with user related database functions
8 namespace Friendica\Model;
10 use Friendica\Core\Config;
11 use Friendica\Core\System;
12 use Friendica\Core\Worker;
13 use Friendica\Database\DBM;
14 use Friendica\Model\Contact;
15 use Friendica\Model\Photo;
16 use Friendica\Object\Image;
19 require_once 'boot.php';
20 require_once 'include/crypto.php';
21 require_once 'include/enotify.php';
22 require_once 'include/group.php';
23 require_once 'include/network.php';
24 require_once 'library/openid.php';
25 require_once 'include/pgettext.php';
26 require_once 'include/plugin.php';
27 require_once 'include/text.php';
29 * @brief This class handles User related functions
34 * @brief Authenticate a user with a clear text password
36 * User info can be any of the following:
39 * - User email or username or nickname
40 * - User array with at least the uid and the hashed password
42 * @param mixed $user_info
43 * @param string $password
46 public static function authenticate($user_info, $password)
48 if (is_object($user_info)) {
49 $user = (array) $user_info;
50 } elseif (is_int($user_info)) {
51 $user = dba::select('user',
56 'account_expired' => 0,
57 'account_removed' => 0,
62 } elseif (is_string($user_info)) {
63 $user = dba::fetch_first('SELECT `uid`, `password`
65 WHERE (`email` = ? OR `username` = ? OR `nickname` = ?)
67 AND `account_expired` = 0
68 AND `account_removed` = 0
79 if (!DBM::is_result($user) || !isset($user['uid']) || !isset($user['password'])) {
83 $password_hashed = hash('whirlpool', $password);
85 if ($password_hashed !== $user['password']) {
93 * @brief Catch-all user creation function
95 * Creates a user from the provided data array, either form fields or OpenID.
96 * Required: { username, nickname, email } or { openid_url }
98 * Performs the following:
99 * - Sends to the OpenId auth URL (if relevant)
100 * - Creates new key pairs for crypto
101 * - Create self-contact
102 * - Create profile image
107 public static function create(array $data)
110 $result = array('success' => false, 'user' => null, 'password' => '', 'message' => '');
112 $using_invites = Config::get('system', 'invitation_only');
113 $num_invites = Config::get('system', 'number_invites');
115 $invite_id = x($data, 'invite_id') ? notags(trim($data['invite_id'])) : '';
116 $username = x($data, 'username') ? notags(trim($data['username'])) : '';
117 $nickname = x($data, 'nickname') ? notags(trim($data['nickname'])) : '';
118 $email = x($data, 'email') ? notags(trim($data['email'])) : '';
119 $openid_url = x($data, 'openid_url') ? notags(trim($data['openid_url'])) : '';
120 $photo = x($data, 'photo') ? notags(trim($data['photo'])) : '';
121 $password = x($data, 'password') ? trim($data['password']) : '';
122 $password1 = x($data, 'password1') ? trim($data['password1']) : '';
123 $confirm = x($data, 'confirm') ? trim($data['confirm']) : '';
124 $blocked = x($data, 'blocked') ? intval($data['blocked']) : 0;
125 $verified = x($data, 'verified') ? intval($data['verified']) : 0;
127 $publish = x($data, 'profile_publish_reg') && intval($data['profile_publish_reg']) ? 1 : 0;
128 $netpublish = strlen(Config::get('system', 'directory')) ? $publish : 0;
130 if ($password1 != $confirm) {
131 $result['message'] .= t('Passwords do not match. Password unchanged.') . EOL;
133 } elseif ($password1 != "") {
134 $password = $password1;
137 $tmp_str = $openid_url;
139 if ($using_invites) {
141 $result['message'] .= t('An invitation is required.') . EOL;
144 $r = q("SELECT * FROM `register` WHERE `hash` = '%s' LIMIT 1", dbesc($invite_id));
146 $result['message'] .= t('Invitation could not be verified.') . EOL;
151 if (!x($username) || !x($email) || !x($nickname)) {
153 if (!validate_url($tmp_str)) {
154 $result['message'] .= t('Invalid OpenID url') . EOL;
157 $_SESSION['register'] = 1;
158 $_SESSION['openid'] = $openid_url;
160 $openid = new LightOpenID;
161 $openid->identity = $openid_url;
162 $openid->returnUrl = System::baseUrl() . '/openid';
163 $openid->required = array('namePerson/friendly', 'contact/email', 'namePerson');
164 $openid->optional = array('namePerson/first', 'media/image/aspect11', 'media/image/default');
166 $authurl = $openid->authUrl();
167 } catch (Exception $e) {
168 $result['message'] .= 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() . EOL;
175 notice(t('Please enter the required information.') . EOL);
179 if (!validate_url($tmp_str)) {
185 // collapse multiple spaces in name
186 $username = preg_replace('/ +/', ' ', $username);
188 if (mb_strlen($username) > 48) {
189 $result['message'] .= t('Please use a shorter name.') . EOL;
191 if (mb_strlen($username) < 3) {
192 $result['message'] .= t('Name too short.') . EOL;
195 // So now we are just looking for a space in the full name.
197 $loose_reg = Config::get('system', 'no_regfullname');
199 $username = mb_convert_case($username, MB_CASE_TITLE, 'UTF-8');
200 if (!strpos($username, ' ')) {
201 $result['message'] .= t("That doesn't appear to be your full \x28First Last\x29 name.") . EOL;
206 if (!allowed_email($email)) {
207 $result['message'] .= t('Your email domain is not among those allowed on this site.') . EOL;
210 if (!valid_email($email) || !validate_email($email)) {
211 $result['message'] .= t('Not a valid email address.') . EOL;
214 // Disallow somebody creating an account using openid that uses the admin email address,
215 // since openid bypasses email verification. We'll allow it if there is not yet an admin account.
217 $adminlist = explode(",", str_replace(" ", "", strtolower($a->config['admin_email'])));
219 //if((x($a->config,'admin_email')) && (strcasecmp($email,$a->config['admin_email']) == 0) && strlen($openid_url)) {
220 if (x($a->config, 'admin_email') && in_array(strtolower($email), $adminlist) && strlen($openid_url)) {
221 $r = q("SELECT * FROM `user` WHERE `email` = '%s' LIMIT 1",
224 if (DBM::is_result($r)) {
225 $result['message'] .= t('Cannot use that email.') . EOL;
229 $nickname = $data['nickname'] = strtolower($nickname);
231 if (!preg_match("/^[a-z0-9][a-z0-9\_]*$/", $nickname)) {
232 $result['message'] .= t('Your "nickname" can only contain "a-z", "0-9" and "_".') . EOL;
235 $r = q("SELECT `uid` FROM `user`
236 WHERE `nickname` = '%s' LIMIT 1",
239 if (DBM::is_result($r)) {
240 $result['message'] .= t('Nickname is already registered. Please choose another.') . EOL;
243 // Check deleted accounts that had this nickname. Doesn't matter to us,
244 // but could be a security issue for federated platforms.
246 $r = q("SELECT * FROM `userd`
247 WHERE `username` = '%s' LIMIT 1",
250 if (DBM::is_result($r)) {
251 $result['message'] .= t('Nickname was once registered here and may not be re-used. Please choose another.') . EOL;
254 if (strlen($result['message'])) {
258 $new_password = strlen($password) ? $password : autoname(6) . mt_rand(100, 9999);
259 $new_password_encoded = hash('whirlpool', $new_password);
261 $result['password'] = $new_password;
263 $keys = new_keypair(4096);
265 if ($keys === false) {
266 $result['message'] .= t('SERIOUS ERROR: Generation of security keys failed.') . EOL;
270 $prvkey = $keys['prvkey'];
271 $pubkey = $keys['pubkey'];
273 // Create another keypair for signing/verifying salmon protocol messages.
274 $sres = new_keypair(512);
275 $sprvkey = $sres['prvkey'];
276 $spubkey = $sres['pubkey'];
278 $r = q("INSERT INTO `user` (`guid`, `username`, `password`, `email`, `openid`, `nickname`,
279 `pubkey`, `prvkey`, `spubkey`, `sprvkey`, `register_date`, `verified`, `blocked`, `timezone`, `default-location`)
280 VALUES ('%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', %d, %d, 'UTC', '')",
281 dbesc(generate_user_guid()),
283 dbesc($new_password_encoded),
291 dbesc(datetime_convert()),
297 $r = q("SELECT * FROM `user`
298 WHERE `username` = '%s' AND `password` = '%s' LIMIT 1",
300 dbesc($new_password_encoded)
302 if (DBM::is_result($r)) {
304 $newuid = intval($r[0]['uid']);
307 $result['message'] .= t('An error occurred during registration. Please try again.') . EOL;
312 * if somebody clicked submit twice very quickly, they could end up with two accounts
313 * due to race condition. Remove this one.
315 $r = q("SELECT `uid` FROM `user`
316 WHERE `nickname` = '%s' ",
319 if (DBM::is_result($r) && count($r) > 1 && $newuid) {
320 $result['message'] .= t('Nickname is already registered. Please choose another.') . EOL;
321 dba::delete('user', array('uid' => $newuid));
325 if (x($newuid) !== false) {
326 $r = q("INSERT INTO `profile` ( `uid`, `profile-name`, `is-default`, `name`, `photo`, `thumb`, `publish`, `net-publish` )
327 VALUES ( %d, '%s', %d, '%s', '%s', '%s', %d, %d ) ",
332 dbesc(System::baseUrl() . "/photo/profile/{$newuid}.jpg"),
333 dbesc(System::baseUrl() . "/photo/avatar/{$newuid}.jpg"),
338 $result['message'] .= t('An error occurred creating your default profile. Please try again.') . EOL;
339 // Start fresh next time.
340 dba::delete('user', array('uid' => $newuid));
344 // Create the self contact
345 Contact::createSelfFromUserId($newuid);
347 // Create a group with no members. This allows somebody to use it
348 // right away as a default group for new contacts.
350 group_add($newuid, t('Friends'));
352 $r = q("SELECT `id` FROM `group` WHERE `uid` = %d AND `name` = '%s'",
356 if (DBM::is_result($r)) {
357 $def_gid = $r[0]['id'];
359 q("UPDATE `user` SET `def_gid` = %d WHERE `uid` = %d",
365 if (Config::get('system', 'newuser_private') && $def_gid) {
366 q("UPDATE `user` SET `allow_gid` = '%s' WHERE `uid` = %d",
367 dbesc("<" . $def_gid . ">"),
373 // if we have no OpenID photo try to look up an avatar
374 if (!strlen($photo)) {
375 $photo = avatar_img($email);
378 // unless there is no avatar-plugin loaded
379 if (strlen($photo)) {
380 $photo_failure = false;
382 $filename = basename($photo);
383 $img_str = fetch_url($photo, true);
384 // guess mimetype from headers or filename
385 $type = Image::guessType($photo, true);
388 $Image = new Image($img_str, $type);
389 if ($Image->isValid()) {
390 $Image->scaleToSquare(175);
392 $hash = photo_new_resource();
394 $r = Photo::store($Image, $newuid, 0, $hash, $filename, t('Profile Photos'), 4);
397 $photo_failure = true;
400 $Image->scaleDown(80);
402 $r = Photo::store($Image, $newuid, 0, $hash, $filename, t('Profile Photos'), 5);
405 $photo_failure = true;
408 $Image->scaleDown(48);
410 $r = Photo::store($Image, $newuid, 0, $hash, $filename, t('Profile Photos'), 6);
413 $photo_failure = true;
416 if (!$photo_failure) {
417 q("UPDATE `photo` SET `profile` = 1 WHERE `resource-id` = '%s' ",
424 call_hooks('register_account', $newuid);
426 $result['success'] = true;
427 $result['user'] = $u;
432 * @brief Sends pending registration confiĆmation email
434 * @param string $email
435 * @param string $sitename
436 * @param string $username
437 * @return NULL|boolean from notification() and email() inherited
439 public static function sendRegisterPendingEmail($email, $sitename, $username)
443 Thank you for registering at %2$s. Your account is pending for approval by the administrator.
446 $body = sprintf($body, $username, $sitename);
448 return notification(array(
449 'type' => SYSTEM_EMAIL,
450 'to_email' => $email,
451 'subject'=> sprintf( t('Registration at %s'), $sitename),
456 * @brief Sends registration confirmation
458 * It's here as a function because the mail is sent from different parts
460 * @param string $email
461 * @param string $sitename
462 * @param string $siteurl
463 * @param string $username
464 * @param string $password
465 * @return NULL|boolean from notification() and email() inherited
467 public static function sendRegisterOpenEmail($email, $sitename, $siteurl, $username, $password)
469 $preamble = deindent(t('
471 Thank you for registering at %2$s. Your account has been created.
474 The login details are as follows:
479 You may change your password from your account "Settings" page after logging
482 Please take a few moments to review the other account settings on that page.
484 You may also wish to add some basic information to your default profile
485 (on the "Profiles" page) so that other people can easily find you.
487 We recommend setting your full name, adding a profile photo,
488 adding some profile "keywords" (very useful in making new friends) - and
489 perhaps what country you live in; if you do not wish to be more specific
492 We fully respect your right to privacy, and none of these items are necessary.
493 If you are new and do not know anybody here, they may help
494 you to make some new and interesting friends.
497 Thank you and welcome to %2$s.'));
499 $preamble = sprintf($preamble, $username, $sitename);
500 $body = sprintf($body, $email, $sitename, $siteurl, $username, $password);
502 return notification(array(
503 'type' => SYSTEM_EMAIL,
504 'to_email' => $email,
505 'subject'=> sprintf( t('Registration details for %s'), $sitename),
506 'preamble'=> $preamble,
511 * @param object $uid user to remove
514 public static function remove($uid)
520 logger('Removing user: ' . $uid);
522 $user = dba::select('user', [], ['uid' => $uid], ['limit' => 1]);
524 call_hooks('remove_user', $user);
526 // save username (actually the nickname as it is guaranteed
527 // unique), so it cannot be re-registered in the future.
528 dba::insert('userd', ['username' => $user['nickname']]);
530 // The user and related data will be deleted in "cron_expire_and_remove_users" (cronjobs.php)
531 dba::update('user', ['account_removed' => true, 'account_expires_on' => datetime_convert()], ['uid' => $uid]);
532 Worker::add(PRIORITY_HIGH, "Notifier", "removeme", $uid);
534 // Send an update to the directory
535 Worker::add(PRIORITY_LOW, "Directory", $user['url']);
537 if ($uid == local_user()) {
538 unset($_SESSION['authenticated']);
539 unset($_SESSION['uid']);
540 goaway(System::baseUrl());