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\PConfig;
12 use Friendica\Core\System;
13 use Friendica\Core\Worker;
14 use Friendica\Database\DBM;
15 use Friendica\Model\Contact;
16 use Friendica\Model\Group;
17 use Friendica\Model\Photo;
18 use Friendica\Object\Image;
22 require_once 'boot.php';
23 require_once 'include/crypto.php';
24 require_once 'include/enotify.php';
25 require_once 'include/network.php';
26 require_once 'library/openid.php';
27 require_once 'include/pgettext.php';
28 require_once 'include/plugin.php';
29 require_once 'include/text.php';
31 * @brief This class handles User related functions
36 * @brief Returns the default group for a given user and network
38 * @param int $uid User id
39 * @param string $network network name
41 * @return int group id
43 public static function getDefaultGroup($uid, $network = '')
47 if ($network == NETWORK_OSTATUS) {
48 $default_group = PConfig::get($uid, "ostatus", "default_group");
51 if ($default_group != 0) {
52 return $default_group;
55 $user = dba::select('user', ['def_gid'], ['uid' => $uid], ['limit' => 1]);
57 if (DBM::is_result($user)) {
58 $default_group = $user["def_gid"];
61 return $default_group;
66 * @brief Authenticate a user with a clear text password
68 * User info can be any of the following:
71 * - User email or username or nickname
72 * - User array with at least the uid and the hashed password
74 * @param mixed $user_info
75 * @param string $password
78 public static function authenticate($user_info, $password)
80 if (is_object($user_info)) {
81 $user = (array) $user_info;
82 } elseif (is_int($user_info)) {
83 $user = dba::select('user',
88 'account_expired' => 0,
89 'account_removed' => 0,
94 } elseif (is_string($user_info)) {
95 $user = dba::fetch_first('SELECT `uid`, `password`
97 WHERE (`email` = ? OR `username` = ? OR `nickname` = ?)
99 AND `account_expired` = 0
100 AND `account_removed` = 0
111 if (!DBM::is_result($user) || !isset($user['uid']) || !isset($user['password'])) {
115 $password_hashed = hash('whirlpool', $password);
117 if ($password_hashed !== $user['password']) {
125 * @brief Catch-all user creation function
127 * Creates a user from the provided data array, either form fields or OpenID.
128 * Required: { username, nickname, email } or { openid_url }
130 * Performs the following:
131 * - Sends to the OpenId auth URL (if relevant)
132 * - Creates new key pairs for crypto
133 * - Create self-contact
134 * - Create profile image
140 public static function create(array $data)
143 $return = ['user' => null, 'password' => ''];
145 $using_invites = Config::get('system', 'invitation_only');
146 $num_invites = Config::get('system', 'number_invites');
148 $invite_id = x($data, 'invite_id') ? notags(trim($data['invite_id'])) : '';
149 $username = x($data, 'username') ? notags(trim($data['username'])) : '';
150 $nickname = x($data, 'nickname') ? notags(trim($data['nickname'])) : '';
151 $email = x($data, 'email') ? notags(trim($data['email'])) : '';
152 $openid_url = x($data, 'openid_url') ? notags(trim($data['openid_url'])) : '';
153 $photo = x($data, 'photo') ? notags(trim($data['photo'])) : '';
154 $password = x($data, 'password') ? trim($data['password']) : '';
155 $password1 = x($data, 'password1') ? trim($data['password1']) : '';
156 $confirm = x($data, 'confirm') ? trim($data['confirm']) : '';
157 $blocked = x($data, 'blocked') ? intval($data['blocked']) : 0;
158 $verified = x($data, 'verified') ? intval($data['verified']) : 0;
160 $publish = x($data, 'profile_publish_reg') && intval($data['profile_publish_reg']) ? 1 : 0;
161 $netpublish = strlen(Config::get('system', 'directory')) ? $publish : 0;
163 if ($password1 != $confirm) {
164 throw new Exception(t('Passwords do not match. Password unchanged.'));
165 } elseif ($password1 != "") {
166 $password = $password1;
169 $tmp_str = $openid_url;
171 if ($using_invites) {
173 throw new Exception(t('An invitation is required.'));
175 $r = q("SELECT * FROM `register` WHERE `hash` = '%s' LIMIT 1", dbesc($invite_id));
177 throw new Exception(t('Invitation could not be verified.'));
181 if (!x($username) || !x($email) || !x($nickname)) {
183 if (!validate_url($tmp_str)) {
184 throw new Exception(t('Invalid OpenID url'));
186 $_SESSION['register'] = 1;
187 $_SESSION['openid'] = $openid_url;
189 $openid = new LightOpenID;
190 $openid->identity = $openid_url;
191 $openid->returnUrl = System::baseUrl() . '/openid';
192 $openid->required = array('namePerson/friendly', 'contact/email', 'namePerson');
193 $openid->optional = array('namePerson/first', 'media/image/aspect11', 'media/image/default');
195 $authurl = $openid->authUrl();
196 } catch (Exception $e) {
197 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);
203 throw new Exception(t('Please enter the required information.'));
206 if (!validate_url($tmp_str)) {
212 // collapse multiple spaces in name
213 $username = preg_replace('/ +/', ' ', $username);
215 if (mb_strlen($username) > 48) {
216 throw new Exception(t('Please use a shorter name.'));
218 if (mb_strlen($username) < 3) {
219 throw new Exception(t('Name too short.'));
222 // So now we are just looking for a space in the full name.
223 $loose_reg = Config::get('system', 'no_regfullname');
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."));
231 if (!allowed_email($email)) {
232 throw new Exception(t('Your email domain is not among those allowed on this site.'));
235 if (!valid_email($email) || !validate_email($email)) {
236 throw new Exception(t('Not a valid email address.'));
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.
242 $adminlist = explode(",", str_replace(" ", "", strtolower($a->config['admin_email'])));
244 //if((x($a->config,'admin_email')) && (strcasecmp($email,$a->config['admin_email']) == 0) && strlen($openid_url)) {
245 if (x($a->config, 'admin_email') && in_array(strtolower($email), $adminlist) && strlen($openid_url)) {
246 $r = q("SELECT * FROM `user` WHERE `email` = '%s' LIMIT 1",
249 if (DBM::is_result($r)) {
250 $result['message'] .= t('Cannot use that email.') . EOL;
254 $nickname = $data['nickname'] = strtolower($nickname);
256 if (!preg_match("/^[a-z0-9][a-z0-9\_]*$/", $nickname)) {
257 throw new Exception(t('Your "nickname" can only contain "a-z", "0-9" and "_".'));
260 $r = q("SELECT `uid` FROM `user`
261 WHERE `nickname` = '%s' LIMIT 1",
264 if (DBM::is_result($r)) {
265 throw new Exception(t('Nickname is already registered. Please choose another.'));
268 // Check deleted accounts that had this nickname. Doesn't matter to us,
269 // but could be a security issue for federated platforms.
270 $r = q("SELECT * FROM `userd`
271 WHERE `username` = '%s' LIMIT 1",
274 if (DBM::is_result($r)) {
275 throw new Exception(t('Nickname was once registered here and may not be re-used. Please choose another.'));
278 $new_password = strlen($password) ? $password : autoname(6) . mt_rand(100, 9999);
279 $new_password_encoded = hash('whirlpool', $new_password);
281 $return['password'] = $new_password;
283 $keys = new_keypair(4096);
284 if ($keys === false) {
285 throw new Exception(t('SERIOUS ERROR: Generation of security keys failed.'));
288 $prvkey = $keys['prvkey'];
289 $pubkey = $keys['pubkey'];
291 // Create another keypair for signing/verifying salmon protocol messages.
292 $sres = new_keypair(512);
293 $sprvkey = $sres['prvkey'];
294 $spubkey = $sres['pubkey'];
296 $r = q("INSERT INTO `user` (`guid`, `username`, `password`, `email`, `openid`, `nickname`,
297 `pubkey`, `prvkey`, `spubkey`, `sprvkey`, `register_date`, `verified`, `blocked`, `timezone`, `default-location`)
298 VALUES ('%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', %d, %d, 'UTC', '')",
299 dbesc(generate_user_guid()),
301 dbesc($new_password_encoded),
309 dbesc(datetime_convert()),
315 $r = q("SELECT * FROM `user`
316 WHERE `username` = '%s' AND `password` = '%s' LIMIT 1",
318 dbesc($new_password_encoded)
320 if (DBM::is_result($r)) {
322 $newuid = intval($r[0]['uid']);
325 throw new Exception(t('An error occurred during registration. Please try again.'));
329 * if somebody clicked submit twice very quickly, they could end up with two accounts
330 * due to race condition. Remove this one.
332 $r = q("SELECT `uid` FROM `user`
333 WHERE `nickname` = '%s' ",
336 if (DBM::is_result($r) && count($r) > 1 && $newuid) {
337 dba::delete('user', array('uid' => $newuid));
339 throw new Exception(t('Nickname is already registered. Please choose another.'));
342 if (x($newuid) !== false) {
343 $r = q("INSERT INTO `profile` ( `uid`, `profile-name`, `is-default`, `name`, `photo`, `thumb`, `publish`, `net-publish` )
344 VALUES ( %d, '%s', %d, '%s', '%s', '%s', %d, %d ) ",
349 dbesc(System::baseUrl() . "/photo/profile/{$newuid}.jpg"),
350 dbesc(System::baseUrl() . "/photo/avatar/{$newuid}.jpg"),
355 dba::delete('user', array('uid' => $newuid));
357 throw new Exception(t('An error occurred creating your default profile. Please try again.'));
360 // Create the self contact
361 if (!Contact::createSelfFromUserId($newuid)) {
362 dba::delete('user', array('uid' => $newuid));
364 throw new Exception(t('An error occurred creating your self contact. Please try again.'));
367 // Create a group with no members. This allows somebody to use it
368 // right away as a default group for new contacts.
369 if (!Group::create($newuid, t('Friends'))) {
370 dba::delete('user', array('uid' => $newuid));
372 throw new Exception(t('An error occurred creating your default contact group. Please try again.'));
375 $r = q("SELECT `id` FROM `group` WHERE `uid` = %d AND `name` = '%s'",
379 if (DBM::is_result($r)) {
380 $def_gid = $r[0]['id'];
382 q("UPDATE `user` SET `def_gid` = %d WHERE `uid` = %d",
388 if (Config::get('system', 'newuser_private') && $def_gid) {
389 q("UPDATE `user` SET `allow_gid` = '%s' WHERE `uid` = %d",
390 dbesc("<" . $def_gid . ">"),
396 // if we have no OpenID photo try to look up an avatar
397 if (!strlen($photo)) {
398 $photo = avatar_img($email);
401 // unless there is no avatar-plugin loaded
402 if (strlen($photo)) {
403 $photo_failure = false;
405 $filename = basename($photo);
406 $img_str = fetch_url($photo, true);
407 // guess mimetype from headers or filename
408 $type = Image::guessType($photo, true);
411 $Image = new Image($img_str, $type);
412 if ($Image->isValid()) {
413 $Image->scaleToSquare(175);
415 $hash = photo_new_resource();
417 $r = Photo::store($Image, $newuid, 0, $hash, $filename, t('Profile Photos'), 4);
420 $photo_failure = true;
423 $Image->scaleDown(80);
425 $r = Photo::store($Image, $newuid, 0, $hash, $filename, t('Profile Photos'), 5);
428 $photo_failure = true;
431 $Image->scaleDown(48);
433 $r = Photo::store($Image, $newuid, 0, $hash, $filename, t('Profile Photos'), 6);
436 $photo_failure = true;
439 if (!$photo_failure) {
440 q("UPDATE `photo` SET `profile` = 1 WHERE `resource-id` = '%s' ",
447 call_hooks('register_account', $newuid);
449 $return['user'] = $u;
454 * @brief Sends pending registration confiĆmation email
456 * @param string $email
457 * @param string $sitename
458 * @param string $username
459 * @return NULL|boolean from notification() and email() inherited
461 public static function sendRegisterPendingEmail($email, $sitename, $username)
465 Thank you for registering at %2$s. Your account is pending for approval by the administrator.
468 $body = sprintf($body, $username, $sitename);
470 return notification(array(
471 'type' => SYSTEM_EMAIL,
472 'to_email' => $email,
473 'subject'=> sprintf( t('Registration at %s'), $sitename),
478 * @brief Sends registration confirmation
480 * It's here as a function because the mail is sent from different parts
482 * @param string $email
483 * @param string $sitename
484 * @param string $siteurl
485 * @param string $username
486 * @param string $password
487 * @return NULL|boolean from notification() and email() inherited
489 public static function sendRegisterOpenEmail($email, $sitename, $siteurl, $username, $password)
491 $preamble = deindent(t('
493 Thank you for registering at %2$s. Your account has been created.
496 The login details are as follows:
501 You may change your password from your account "Settings" page after logging
504 Please take a few moments to review the other account settings on that page.
506 You may also wish to add some basic information to your default profile
507 (on the "Profiles" page) so that other people can easily find you.
509 We recommend setting your full name, adding a profile photo,
510 adding some profile "keywords" (very useful in making new friends) - and
511 perhaps what country you live in; if you do not wish to be more specific
514 We fully respect your right to privacy, and none of these items are necessary.
515 If you are new and do not know anybody here, they may help
516 you to make some new and interesting friends.
519 Thank you and welcome to %2$s.'));
521 $preamble = sprintf($preamble, $username, $sitename);
522 $body = sprintf($body, $email, $sitename, $siteurl, $username, $password);
524 return notification(array(
525 'type' => SYSTEM_EMAIL,
526 'to_email' => $email,
527 'subject'=> sprintf( t('Registration details for %s'), $sitename),
528 'preamble'=> $preamble,
533 * @param object $uid user to remove
536 public static function remove($uid)
542 logger('Removing user: ' . $uid);
544 $user = dba::select('user', [], ['uid' => $uid], ['limit' => 1]);
546 call_hooks('remove_user', $user);
548 // save username (actually the nickname as it is guaranteed
549 // unique), so it cannot be re-registered in the future.
550 dba::insert('userd', ['username' => $user['nickname']]);
552 // The user and related data will be deleted in "cron_expire_and_remove_users" (cronjobs.php)
553 dba::update('user', ['account_removed' => true, 'account_expires_on' => datetime_convert()], ['uid' => $uid]);
554 Worker::add(PRIORITY_HIGH, "Notifier", "removeme", $uid);
556 // Send an update to the directory
557 Worker::add(PRIORITY_LOW, "Directory", $user['url']);
559 if ($uid == local_user()) {
560 unset($_SESSION['authenticated']);
561 unset($_SESSION['uid']);
562 goaway(System::baseUrl());