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\Addon;
11 use Friendica\Core\Config;
12 use Friendica\Core\PConfig;
13 use Friendica\Core\System;
14 use Friendica\Core\Worker;
15 use Friendica\Database\DBM;
16 use Friendica\Model\Contact;
17 use Friendica\Model\Group;
18 use Friendica\Model\Photo;
19 use Friendica\Object\Image;
20 use Friendica\Util\Crypto;
24 require_once 'boot.php';
25 require_once 'include/dba.php';
26 require_once 'include/enotify.php';
27 require_once 'include/network.php';
28 require_once 'library/openid.php';
29 require_once 'include/pgettext.php';
30 require_once 'include/plugin.php';
31 require_once 'include/text.php';
33 * @brief This class handles User related functions
38 * @brief Get owner data by user id
41 * @return boolean|array
43 public static function getOwnerDataById($uid) {
44 $r = dba::fetch_first("SELECT
46 `user`.`prvkey` AS `uprvkey`,
52 `user`.`account-type`,
56 ON `user`.`uid` = `contact`.`uid`
57 WHERE `contact`.`uid` = ?
62 if (!DBM::is_result($r)) {
69 * @brief Returns the default group for a given user and network
71 * @param int $uid User id
72 * @param string $network network name
74 * @return int group id
76 public static function getDefaultGroup($uid, $network = '')
80 if ($network == NETWORK_OSTATUS) {
81 $default_group = PConfig::get($uid, "ostatus", "default_group");
84 if ($default_group != 0) {
85 return $default_group;
88 $user = dba::selectFirst('user', ['def_gid'], ['uid' => $uid]);
90 if (DBM::is_result($user)) {
91 $default_group = $user["def_gid"];
94 return $default_group;
99 * @brief Authenticate a user with a clear text password
101 * User info can be any of the following:
104 * - User email or username or nickname
105 * - User array with at least the uid and the hashed password
107 * @param mixed $user_info
108 * @param string $password
111 public static function authenticate($user_info, $password)
113 if (is_object($user_info)) {
114 $user = (array) $user_info;
115 } elseif (is_int($user_info)) {
116 $user = dba::selectFirst('user', ['uid', 'password'],
120 'account_expired' => 0,
121 'account_removed' => 0,
125 } elseif (is_string($user_info)) {
126 $user = dba::fetch_first('SELECT `uid`, `password`
128 WHERE (`email` = ? OR `username` = ? OR `nickname` = ?)
130 AND `account_expired` = 0
131 AND `account_removed` = 0
142 if (!DBM::is_result($user) || !isset($user['uid']) || !isset($user['password'])) {
146 $password_hashed = hash('whirlpool', $password);
148 if ($password_hashed !== $user['password']) {
156 * @brief Catch-all user creation function
158 * Creates a user from the provided data array, either form fields or OpenID.
159 * Required: { username, nickname, email } or { openid_url }
161 * Performs the following:
162 * - Sends to the OpenId auth URL (if relevant)
163 * - Creates new key pairs for crypto
164 * - Create self-contact
165 * - Create profile image
171 public static function create(array $data)
174 $return = ['user' => null, 'password' => ''];
176 $using_invites = Config::get('system', 'invitation_only');
177 $num_invites = Config::get('system', 'number_invites');
179 $invite_id = x($data, 'invite_id') ? notags(trim($data['invite_id'])) : '';
180 $username = x($data, 'username') ? notags(trim($data['username'])) : '';
181 $nickname = x($data, 'nickname') ? notags(trim($data['nickname'])) : '';
182 $email = x($data, 'email') ? notags(trim($data['email'])) : '';
183 $openid_url = x($data, 'openid_url') ? notags(trim($data['openid_url'])) : '';
184 $photo = x($data, 'photo') ? notags(trim($data['photo'])) : '';
185 $password = x($data, 'password') ? trim($data['password']) : '';
186 $password1 = x($data, 'password1') ? trim($data['password1']) : '';
187 $confirm = x($data, 'confirm') ? trim($data['confirm']) : '';
188 $blocked = x($data, 'blocked') ? intval($data['blocked']) : 0;
189 $verified = x($data, 'verified') ? intval($data['verified']) : 0;
191 $publish = x($data, 'profile_publish_reg') && intval($data['profile_publish_reg']) ? 1 : 0;
192 $netpublish = strlen(Config::get('system', 'directory')) ? $publish : 0;
194 if ($password1 != $confirm) {
195 throw new Exception(t('Passwords do not match. Password unchanged.'));
196 } elseif ($password1 != '') {
197 $password = $password1;
200 if ($using_invites) {
202 throw new Exception(t('An invitation is required.'));
205 if (!dba::exists('register', ['hash' => $invite_id])) {
206 throw new Exception(t('Invitation could not be verified.'));
210 if (!x($username) || !x($email) || !x($nickname)) {
212 if (!validate_url($openid_url)) {
213 throw new Exception(t('Invalid OpenID url'));
215 $_SESSION['register'] = 1;
216 $_SESSION['openid'] = $openid_url;
218 $openid = new \LightOpenID;
219 $openid->identity = $openid_url;
220 $openid->returnUrl = System::baseUrl() . '/openid';
221 $openid->required = ['namePerson/friendly', 'contact/email', 'namePerson'];
222 $openid->optional = ['namePerson/first', 'media/image/aspect11', 'media/image/default'];
224 $authurl = $openid->authUrl();
225 } catch (Exception $e) {
226 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);
232 throw new Exception(t('Please enter the required information.'));
235 if (!validate_url($openid_url)) {
241 // collapse multiple spaces in name
242 $username = preg_replace('/ +/', ' ', $username);
244 if (mb_strlen($username) > 48) {
245 throw new Exception(t('Please use a shorter name.'));
247 if (mb_strlen($username) < 3) {
248 throw new Exception(t('Name too short.'));
251 // So now we are just looking for a space in the full name.
252 $loose_reg = Config::get('system', 'no_regfullname');
254 $username = mb_convert_case($username, MB_CASE_TITLE, 'UTF-8');
255 if (!strpos($username, ' ')) {
256 throw new Exception(t("That doesn't appear to be your full \x28First Last\x29 name."));
260 if (!allowed_email($email)) {
261 throw new Exception(t('Your email domain is not among those allowed on this site.'));
264 if (!valid_email($email) || !validate_email($email)) {
265 throw new Exception(t('Not a valid email address.'));
268 if (dba::exists('user', ['email' => $email])) {
269 throw new Exception(t('Cannot use that email.'));
272 // Disallow somebody creating an account using openid that uses the admin email address,
273 // since openid bypasses email verification. We'll allow it if there is not yet an admin account.
274 if (x($a->config, 'admin_email') && strlen($openid_url)) {
275 $adminlist = explode(',', str_replace(' ', '', strtolower($a->config['admin_email'])));
276 if (in_array(strtolower($email), $adminlist)) {
277 throw new Exception(t('Cannot use that email.'));
281 $nickname = $data['nickname'] = strtolower($nickname);
283 if (!preg_match('/^[a-z0-9][a-z0-9\_]*$/', $nickname)) {
284 throw new Exception(t('Your "nickname" can only contain "a-z", "0-9" and "_".'));
287 // Check existing and deleted accounts for this nickname.
288 if (dba::exists('user', ['nickname' => $nickname])
289 || dba::exists('userd', ['username' => $nickname])
291 throw new Exception(t('Nickname is already registered. Please choose another.'));
294 $new_password = strlen($password) ? $password : autoname(6) . mt_rand(100, 9999);
295 $new_password_encoded = hash('whirlpool', $new_password);
297 $return['password'] = $new_password;
299 $keys = Crypto::newKeypair(4096);
300 if ($keys === false) {
301 throw new Exception(t('SERIOUS ERROR: Generation of security keys failed.'));
304 $prvkey = $keys['prvkey'];
305 $pubkey = $keys['pubkey'];
307 // Create another keypair for signing/verifying salmon protocol messages.
308 $sres = Crypto::newKeypair(512);
309 $sprvkey = $sres['prvkey'];
310 $spubkey = $sres['pubkey'];
312 $insert_result = dba::insert('user', [
313 'guid' => generate_user_guid(),
314 'username' => $username,
315 'password' => $new_password_encoded,
317 'openid' => $openid_url,
318 'nickname' => $nickname,
321 'spubkey' => $spubkey,
322 'sprvkey' => $sprvkey,
323 'verified' => $verified,
324 'blocked' => $blocked,
326 'register_date' => datetime_convert(),
327 'default-location' => ''
330 if ($insert_result) {
331 $uid = dba::lastInsertId();
332 $user = dba::selectFirst('user', [], ['uid' => $uid]);
334 throw new Exception(t('An error occurred during registration. Please try again.'));
338 throw new Exception(t('An error occurred during registration. Please try again.'));
341 // if somebody clicked submit twice very quickly, they could end up with two accounts
342 // due to race condition. Remove this one.
343 $user_count = dba::count('user', ['nickname' => $nickname]);
344 if ($user_count > 1) {
345 dba::delete('user', ['uid' => $uid]);
347 throw new Exception(t('Nickname is already registered. Please choose another.'));
350 $insert_result = dba::insert('profile', [
353 'photo' => System::baseUrl() . "/photo/profile/{$uid}.jpg",
354 'thumb' => System::baseUrl() . "/photo/avatar/{$uid}.jpg",
355 'publish' => $publish,
357 'net-publish' => $netpublish,
358 'profile-name' => t('default')
360 if (!$insert_result) {
361 dba::delete('user', ['uid' => $uid]);
363 throw new Exception(t('An error occurred creating your default profile. Please try again.'));
366 // Create the self contact
367 if (!Contact::createSelfFromUserId($uid)) {
368 dba::delete('user', ['uid' => $uid]);
370 throw new Exception(t('An error occurred creating your self contact. Please try again.'));
373 // Create a group with no members. This allows somebody to use it
374 // right away as a default group for new contacts.
375 $def_gid = Group::create($uid, t('Friends'));
377 dba::delete('user', ['uid' => $uid]);
379 throw new Exception(t('An error occurred creating your default contact group. Please try again.'));
382 $fields = ['def_gid' => $def_gid];
383 if (Config::get('system', 'newuser_private') && $def_gid) {
384 $fields['allow_gid'] = '<' . $def_gid . '>';
387 dba::update('user', $fields, ['uid' => $uid]);
389 // if we have no OpenID photo try to look up an avatar
390 if (!strlen($photo)) {
391 $photo = avatar_img($email);
394 // unless there is no avatar-plugin loaded
395 if (strlen($photo)) {
396 $photo_failure = false;
398 $filename = basename($photo);
399 $img_str = fetch_url($photo, true);
400 // guess mimetype from headers or filename
401 $type = Image::guessType($photo, true);
403 $Image = new Image($img_str, $type);
404 if ($Image->isValid()) {
405 $Image->scaleToSquare(175);
407 $hash = photo_new_resource();
409 $r = Photo::store($Image, $uid, 0, $hash, $filename, t('Profile Photos'), 4);
412 $photo_failure = true;
415 $Image->scaleDown(80);
417 $r = Photo::store($Image, $uid, 0, $hash, $filename, t('Profile Photos'), 5);
420 $photo_failure = true;
423 $Image->scaleDown(48);
425 $r = Photo::store($Image, $uid, 0, $hash, $filename, t('Profile Photos'), 6);
428 $photo_failure = true;
431 if (!$photo_failure) {
432 dba::update('photo', ['profile' => 1], ['resource-id' => $hash]);
437 Addon::callHooks('register_account', $uid);
439 $return['user'] = $user;
444 * @brief Sends pending registration confiĆmation email
446 * @param string $email
447 * @param string $sitename
448 * @param string $username
449 * @return NULL|boolean from notification() and email() inherited
451 public static function sendRegisterPendingEmail($email, $sitename, $username)
455 Thank you for registering at %2$s. Your account is pending for approval by the administrator.
458 $body = sprintf($body, $username, $sitename);
460 return notification([
461 'type' => SYSTEM_EMAIL,
462 'to_email' => $email,
463 'subject'=> sprintf( t('Registration at %s'), $sitename),
468 * @brief Sends registration confirmation
470 * It's here as a function because the mail is sent from different parts
472 * @param string $email
473 * @param string $sitename
474 * @param string $siteurl
475 * @param string $username
476 * @param string $password
477 * @return NULL|boolean from notification() and email() inherited
479 public static function sendRegisterOpenEmail($email, $sitename, $siteurl, $username, $password)
481 $preamble = deindent(t('
483 Thank you for registering at %2$s. Your account has been created.
486 The login details are as follows:
491 You may change your password from your account "Settings" page after logging
494 Please take a few moments to review the other account settings on that page.
496 You may also wish to add some basic information to your default profile
497 (on the "Profiles" page) so that other people can easily find you.
499 We recommend setting your full name, adding a profile photo,
500 adding some profile "keywords" (very useful in making new friends) - and
501 perhaps what country you live in; if you do not wish to be more specific
504 We fully respect your right to privacy, and none of these items are necessary.
505 If you are new and do not know anybody here, they may help
506 you to make some new and interesting friends.
509 Thank you and welcome to %2$s.'));
511 $preamble = sprintf($preamble, $username, $sitename);
512 $body = sprintf($body, $email, $sitename, $siteurl, $username, $password);
514 return notification([
515 'type' => SYSTEM_EMAIL,
516 'to_email' => $email,
517 'subject'=> sprintf( t('Registration details for %s'), $sitename),
518 'preamble'=> $preamble,
523 * @param object $uid user to remove
526 public static function remove($uid)
532 logger('Removing user: ' . $uid);
534 $user = dba::selectFirst('user', [], ['uid' => $uid]);
536 Addon::callHooks('remove_user', $user);
538 // save username (actually the nickname as it is guaranteed
539 // unique), so it cannot be re-registered in the future.
540 dba::insert('userd', ['username' => $user['nickname']]);
542 // The user and related data will be deleted in "cron_expire_and_remove_users" (cronjobs.php)
543 dba::update('user', ['account_removed' => true, 'account_expires_on' => datetime_convert()], ['uid' => $uid]);
544 Worker::add(PRIORITY_HIGH, "Notifier", "removeme", $uid);
546 // Send an update to the directory
547 Worker::add(PRIORITY_LOW, "Directory", $user['url']);
549 if ($uid == local_user()) {
550 unset($_SESSION['authenticated']);
551 unset($_SESSION['uid']);
552 goaway(System::baseUrl());