X-Git-Url: https://git.mxchange.org/?a=blobdiff_plain;f=src%2FModel%2FUser.php;h=854961154b5dfe56772dc11c70c7d4314a1eaae0;hb=302619a5de7e694acc2d6883af77fa9b051bb974;hp=a05d8a27703454d4574f12237ef37ea86b990163;hpb=78343599571fb42eb75ef63af13909fa34e50998;p=friendica.git diff --git a/src/Model/User.php b/src/Model/User.php index a05d8a2770..854961154b 100644 --- a/src/Model/User.php +++ b/src/Model/User.php @@ -1,6 +1,6 @@ get(); + $system['homepage'] = (string)DI::baseUrl(); $system['dob'] = '0000-00-00'; // Ensure that the user contains data @@ -212,32 +214,33 @@ class User throw new Exception(DI::l10n()->t('SERIOUS ERROR: Generation of security keys failed.')); } - $system = []; - $system['uid'] = 0; - $system['created'] = DateTimeFormat::utcNow(); - $system['self'] = true; - $system['network'] = Protocol::ACTIVITYPUB; - $system['name'] = 'System Account'; - $system['addr'] = $system_actor_name . '@' . DI::baseUrl()->getHostname(); - $system['nick'] = $system_actor_name; - $system['url'] = DI::baseUrl() . '/friendica'; + $system = [ + 'uid' => 0, + 'created' => DateTimeFormat::utcNow(), + 'self' => true, + 'network' => Protocol::ACTIVITYPUB, + 'name' => 'System Account', + 'addr' => $system_actor_name . '@' . DI::baseUrl()->getHost(), + 'nick' => $system_actor_name, + 'url' => DI::baseUrl() . '/friendica', + 'pubkey' => $keys['pubkey'], + 'prvkey' => $keys['prvkey'], + 'blocked' => 0, + 'pending' => 0, + 'contact-type' => Contact::TYPE_RELAY, // In AP this is translated to 'Application' + 'name-date' => DateTimeFormat::utcNow(), + 'uri-date' => DateTimeFormat::utcNow(), + 'avatar-date' => DateTimeFormat::utcNow(), + 'closeness' => 0, + 'baseurl' => DI::baseUrl(), + ]; $system['avatar'] = $system['photo'] = Contact::getDefaultAvatar($system, Proxy::SIZE_SMALL); - $system['thumb'] = Contact::getDefaultAvatar($system, Proxy::SIZE_THUMB); - $system['micro'] = Contact::getDefaultAvatar($system, Proxy::SIZE_MICRO); - - $system['nurl'] = Strings::normaliseLink($system['url']); - $system['pubkey'] = $keys['pubkey']; - $system['prvkey'] = $keys['prvkey']; - $system['blocked'] = 0; - $system['pending'] = 0; - $system['contact-type'] = Contact::TYPE_RELAY; // In AP this is translated to 'Application' - $system['name-date'] = DateTimeFormat::utcNow(); - $system['uri-date'] = DateTimeFormat::utcNow(); - $system['avatar-date'] = DateTimeFormat::utcNow(); - $system['closeness'] = 0; - $system['baseurl'] = DI::baseUrl(); - $system['gsid'] = GServer::getID($system['baseurl']); + $system['thumb'] = Contact::getDefaultAvatar($system, Proxy::SIZE_THUMB); + $system['micro'] = Contact::getDefaultAvatar($system, Proxy::SIZE_MICRO); + $system['nurl'] = Strings::normaliseLink($system['url']); + $system['gsid'] = GServer::getID($system['baseurl']); + Contact::insert($system); } @@ -264,7 +267,7 @@ class User // List of possible actor names $possible_accounts = ['friendica', 'actor', 'system', 'internal']; foreach ($possible_accounts as $name) { - if (!DBA::exists('user', ['nickname' => $name, 'account_removed' => false, 'expire' => false]) && + if (!DBA::exists('user', ['nickname' => $name, 'account_removed' => false, 'account_expired' => false]) && !DBA::exists('userd', ['username' => $name])) { DI::config()->set('system', 'actor_name', $name); return $name; @@ -380,17 +383,15 @@ class User * * @param array $fields * @return array user + * @throws Exception */ public static function getFirstAdmin(array $fields = []) : array { if (!empty(DI::config()->get('config', 'admin_nickname'))) { return self::getByNickname(DI::config()->get('config', 'admin_nickname'), $fields); - } elseif (!empty(DI::config()->get('config', 'admin_email'))) { - $adminList = explode(',', str_replace(' ', '', DI::config()->get('config', 'admin_email'))); - return self::getByEmail($adminList[0], $fields); - } else { - return []; } + + return self::getAdminList()[0] ?? []; } /** @@ -482,23 +483,41 @@ class User } /** - * Returns the default group for a given user and network + * Returns the default circle for a given user * * @param int $uid User id * - * @return int group id + * @return int circle id * @throws Exception */ - public static function getDefaultGroup(int $uid): int + public static function getDefaultCircle(int $uid): int { $user = DBA::selectFirst('user', ['def_gid'], ['uid' => $uid]); if (DBA::isResult($user)) { - $default_group = $user["def_gid"]; + $default_circle = $user['def_gid']; } else { - $default_group = 0; + $default_circle = 0; } - return $default_group; + return $default_circle; + } + + /** + * Returns the default circle for groups for a given user + * + * @param int $uid User id + * + * @return int circle id + * @throws Exception + */ + public static function getDefaultGroupCircle(int $uid): int + { + $default_circle = DI::pConfig()->get($uid, 'system', 'default-group-gid'); + if (empty($default_circle)) { + $default_circle = self::getDefaultCircle($uid); + } + + return $default_circle; } /** @@ -528,7 +547,7 @@ class User // Addons can create users, and since this 'catch' branch should only // execute if getAuthenticationInfo can't find an existing user, that's // exactly what will happen here. Creating a numeric username would create - // abiguity with user IDs, possibly opening up an attack vector. + // ambiguity with user IDs, possibly opening up an attack vector. // So let's be very careful about that. if (empty($username) || is_numeric($username)) { throw $e; @@ -666,6 +685,32 @@ class User return $user; } + /** + * Update the day of the last activity of the given user + * + * @param integer $uid + * @return void + */ + public static function updateLastActivity(int $uid) + { + if (!$uid) { + return; + } + + $user = User::getById($uid, ['last-activity']); + if (empty($user)) { + return; + } + + $current_day = DateTimeFormat::utcNow('Y-m-d'); + + if ($user['last-activity'] != $current_day) { + User::update(['last-activity' => $current_day], $uid); + // Set the last activity for all identities of the user + DBA::update('user', ['last-activity' => $current_day], ['parent-uid' => $uid, 'account_removed' => false]); + } + } + /** * Generates a human-readable random password * @@ -734,6 +779,29 @@ class User return password_hash($password, PASSWORD_DEFAULT); } + /** + * Allowed characters are a-z, A-Z, 0-9 and special characters except white spaces and accentuated letters. + * + * Password length is limited to 72 characters if the current default password hashing algorithm is Blowfish. + * From the manual: "Using the PASSWORD_BCRYPT as the algorithm, will result in the password parameter being + * truncated to a maximum length of 72 bytes." + * + * @see https://www.php.net/manual/en/function.password-hash.php#refsect1-function.password-hash-parameters + * + * @param string|null $delimiter Whether the regular expression is meant to be wrapper in delimiter characters + * @return string + */ + public static function getPasswordRegExp(string $delimiter = null): string + { + $allowed_characters = ':!"#$%&\'()*+,-./;<=>?@[\]^_`{|}~'; + + if ($delimiter) { + $allowed_characters = preg_quote($allowed_characters, $delimiter); + } + + return '^[a-zA-Z0-9' . $allowed_characters . ']' . (PASSWORD_DEFAULT === PASSWORD_BCRYPT ? '{1,72}' : '+') . '$'; + } + /** * Updates a user row with a new plaintext password * @@ -754,10 +822,12 @@ class User throw new Exception(DI::l10n()->t('The new password has been exposed in a public data dump, please choose another.')); } - $allowed_characters = '!"#$%&\'()*+,-./;<=>?@[\]^_`{|}~'; + if (PASSWORD_DEFAULT === PASSWORD_BCRYPT && strlen($password) > 72) { + throw new Exception(DI::l10n()->t('The password length is limited to 72 characters.')); + } - if (!preg_match('/^[a-z0-9' . preg_quote($allowed_characters, '/') . ']+$/i', $password)) { - throw new Exception(DI::l10n()->t('The password can\'t contain accentuated letters, white spaces or colons (:)')); + if (!preg_match('/' . self::getPasswordRegExp('/') . '/', $password)) { + throw new Exception(DI::l10n()->t("The password can't contain white spaces nor accentuated letters")); } return self::updatePasswordHashed($uid, self::hashPassword($password)); @@ -768,14 +838,14 @@ class User * Empties the password reset token field just in case. * * @param int $uid - * @param string $pasword_hashed + * @param string $password_hashed * @return bool * @throws Exception */ - private static function updatePasswordHashed(int $uid, string $pasword_hashed): bool + private static function updatePasswordHashed(int $uid, string $password_hashed): bool { $fields = [ - 'password' => $pasword_hashed, + 'password' => $password_hashed, 'pwdreset' => null, 'pwdreset_time' => null, 'legacy_password' => false @@ -783,11 +853,27 @@ class User return DBA::update('user', $fields, ['uid' => $uid]); } + /** + * Returns if the given uid is valid and in the admin list + * + * @param int $uid + * + * @return bool + * @throws Exception + */ + public static function isSiteAdmin(int $uid): bool + { + return DBA::exists('user', [ + 'uid' => $uid, + 'email' => self::getAdminEmailList() + ]); + } + /** * Checks if a nickname is in the list of the forbidden nicknames * * Check if a nickname is forbidden from registration on the node by the - * admin. Forbidden nicknames (e.g. role namess) can be configured in the + * admin. Forbidden nicknames (e.g. role names) can be configured in the * admin panel. * * @param string $nickname The nickname that should be checked @@ -960,7 +1046,7 @@ class User $_SESSION['register'] = 1; $_SESSION['openid'] = $openid_url; - $openid = new LightOpenID(DI::baseUrl()->getHostname()); + $openid = new LightOpenID(DI::baseUrl()->getHost()); $openid->identity = $openid_url; $openid->returnUrl = DI::baseUrl() . '/openid'; $openid->required = ['namePerson/friendly', 'contact/email', 'namePerson']; @@ -968,7 +1054,7 @@ class User try { $authurl = $openid->authUrl(); } catch (Exception $e) { - throw new Exception(DI::l10n()->t('We encountered a problem while logging in with the OpenID you provided. Please check the correct spelling of the ID.') . EOL . EOL . DI::l10n()->t('The error message was:') . $e->getMessage(), 0, $e); + throw new Exception(DI::l10n()->t('We encountered a problem while logging in with the OpenID you provided. Please check the correct spelling of the ID.') . '
' . DI::l10n()->t('The error message was:') . $e->getMessage(), 0, $e); } System::externalRedirect($authurl); // NOTREACHED @@ -1028,11 +1114,8 @@ class User // Disallow somebody creating an account using openid that uses the admin email address, // since openid bypasses email verification. We'll allow it if there is not yet an admin account. - if (DI::config()->get('config', 'admin_email') && strlen($openid_url)) { - $adminlist = explode(',', str_replace(' ', '', strtolower(DI::config()->get('config', 'admin_email')))); - if (in_array(strtolower($email), $adminlist)) { - throw new Exception(DI::l10n()->t('Cannot use that email.')); - } + if (strlen($openid_url) && in_array(strtolower($email), self::getAdminEmailList())) { + throw new Exception(DI::l10n()->t('Cannot use that email.')); } $nickname = $data['nickname'] = strtolower($nickname); @@ -1127,13 +1210,13 @@ class User throw new Exception(DI::l10n()->t('An error occurred creating your self contact. Please try again.')); } - // Create a group with no members. This allows somebody to use it - // right away as a default group for new contacts. - $def_gid = Group::create($uid, DI::l10n()->t('Friends')); + // Create a circle with no members. This allows somebody to use it + // right away as a default circle for new contacts. + $def_gid = Circle::create($uid, DI::l10n()->t('Friends')); if (!$def_gid) { DBA::delete('user', ['uid' => $uid]); - throw new Exception(DI::l10n()->t('An error occurred creating your default contact group. Please try again.')); + throw new Exception(DI::l10n()->t('An error occurred creating your default contact circle. Please try again.')); } $fields = ['def_gid' => $def_gid]; @@ -1143,6 +1226,11 @@ class User DBA::update('user', $fields, ['uid' => $uid]); + $def_gid_groups = Circle::create($uid, DI::l10n()->t('Groups')); + if ($def_gid_groups) { + DI::pConfig()->set($uid, 'system', 'default-group-gid', $def_gid_groups); + } + // if we have no OpenID photo try to look up an avatar if (!strlen($photo)) { $photo = Network::lookupAvatarByEmail($email); @@ -1171,7 +1259,7 @@ class User $resource_id = Photo::newResource(); - // Not using Photo::PROFILE_PHOTOS here, so that it is discovered as translateble string + // Not using Photo::PROFILE_PHOTOS here, so that it is discovered as translatable string $profile_album = DI::l10n()->t('Profile Photos'); $r = Photo::store($image, $uid, 0, $resource_id, $filename, $profile_album, 4); @@ -1206,6 +1294,8 @@ class User Hook::callAll('register_account', $uid); + self::setRegisterMethodByUserCount(); + $return['user'] = $user; return $return; } @@ -1291,7 +1381,7 @@ class User if (DBA::isResult($profile) && $profile['net-publish'] && Search::getGlobalDirectory()) { $url = DI::baseUrl() . '/profile/' . $user['nickname']; - Worker::add(PRIORITY_LOW, "Directory", $url); + Worker::add(Worker::PRIORITY_LOW, "Directory", $url); } $l10n = DI::l10n()->withLang($register['language']); @@ -1300,7 +1390,7 @@ class User $l10n, $user, DI::config()->get('config', 'sitename'), - DI::baseUrl()->get(), + DI::baseUrl(), ($register['password'] ?? '') ?: 'Sent in a previous email' ); } @@ -1314,7 +1404,7 @@ class User * permanently against re-registration, as the person was not yet * allowed to have friends on this system * - * @return bool True, if the deny was successfull + * @return bool True, if the deny was successful * @throws Exception */ public static function deny(string $hash): bool @@ -1392,12 +1482,12 @@ class User If you are new and do not know anybody here, they may help you to make some new and interesting friends. - If you ever want to delete your account, you can do so at %1$s/removeme + If you ever want to delete your account, you can do so at %1$s/settings/removeme Thank you and welcome to %4$s.')); $preamble = sprintf($preamble, $user['username'], DI::config()->get('config', 'sitename')); - $body = sprintf($body, DI::baseUrl()->get(), $user['nickname'], $result['password'], DI::config()->get('config', 'sitename')); + $body = sprintf($body, DI::baseUrl(), $user['nickname'], $result['password'], DI::config()->get('config', 'sitename')); $email = DI::emailer() ->newSystemMail() @@ -1496,7 +1586,7 @@ class User If you are new and do not know anybody here, they may help you to make some new and interesting friends. - If you ever want to delete your account, you can do so at %3$s/removeme + If you ever want to delete your account, you can do so at %3$s/settings/removeme Thank you and welcome to %2$s.', $user['nickname'], @@ -1541,15 +1631,16 @@ class User // The user and related data will be deleted in Friendica\Worker\ExpireAndRemoveUsers DBA::update('user', ['account_removed' => true, 'account_expires_on' => DateTimeFormat::utc('now + 7 day')], ['uid' => $uid]); - Worker::add(PRIORITY_HIGH, 'Notifier', Delivery::REMOVAL, $uid); + Worker::add(Worker::PRIORITY_HIGH, 'Notifier', Delivery::REMOVAL, $uid); // Send an update to the directory $self = DBA::selectFirst('contact', ['url'], ['uid' => $uid, 'self' => true]); - Worker::add(PRIORITY_LOW, 'Directory', $self['url']); + Worker::add(Worker::PRIORITY_LOW, 'Directory', $self['url']); // Remove the user relevant data - Worker::add(PRIORITY_NEGLIGIBLE, 'RemoveUser', $uid); + Worker::add(Worker::PRIORITY_NEGLIGIBLE, 'RemoveUser', $uid); + self::setRegisterMethodByUserCount(); return true; } @@ -1576,7 +1667,7 @@ class User */ public static function identities(int $uid): array { - if (empty($uid)) { + if (!$uid) { return []; } @@ -1587,7 +1678,7 @@ class User return $identities; } - if ($user['parent-uid'] == 0) { + if (!$user['parent-uid']) { // First add our own entry $identities = [[ 'uid' => $user['uid'], @@ -1648,7 +1739,7 @@ class User */ public static function hasIdentities(int $uid): bool { - if (empty($uid)) { + if (!$uid) { return false; } @@ -1657,7 +1748,7 @@ class User return false; } - if ($user['parent-uid'] != 0) { + if ($user['parent-uid']) { return true; } @@ -1688,8 +1779,8 @@ class User 'active_users_weekly' => 0, ]; - $userStmt = DBA::select('owner-view', ['uid', 'login_date', 'last-item'], - ["`verified` AND `login_date` > ? AND NOT `blocked` + $userStmt = DBA::select('owner-view', ['uid', 'last-activity', 'last-item'], + ["`verified` AND `last-activity` > ? AND NOT `blocked` AND NOT `account_removed` AND NOT `account_expired`", DBA::NULL_DATETIME]); if (!DBA::isResult($userStmt)) { @@ -1703,17 +1794,17 @@ class User while ($user = DBA::fetch($userStmt)) { $statistics['total_users']++; - if ((strtotime($user['login_date']) > $halfyear) || (strtotime($user['last-item']) > $halfyear) + if ((strtotime($user['last-activity']) > $halfyear) || (strtotime($user['last-item']) > $halfyear) ) { $statistics['active_users_halfyear']++; } - if ((strtotime($user['login_date']) > $month) || (strtotime($user['last-item']) > $month) + if ((strtotime($user['last-activity']) > $month) || (strtotime($user['last-item']) > $month) ) { $statistics['active_users_monthly']++; } - if ((strtotime($user['login_date']) > $week) || (strtotime($user['last-item']) > $week) + if ((strtotime($user['last-activity']) > $week) || (strtotime($user['last-item']) > $week) ) { $statistics['active_users_weekly']++; } @@ -1728,7 +1819,7 @@ class User * * @param int $start Start count (Default is 0) * @param int $count Count of the items per page (Default is @see Pager::ITEMS_PER_PAGE) - * @param string $type The type of users, which should get (all, bocked, removed) + * @param string $type The type of users, which should get (all, blocked, removed) * @param string $order Order of the user list (Default is 'contact.name') * @param bool $descending Order direction (Default is ascending) * @return array|bool The list of the users @@ -1757,4 +1848,89 @@ class User return DBA::selectToArray('owner-view', [], $condition, $param); } + + /** + * Returns a list of lowercase admin email addresses from the comma-separated list in the config + * + * @return array + */ + public static function getAdminEmailList(): array + { + $adminEmails = strtolower(str_replace(' ', '', DI::config()->get('config', 'admin_email'))); + if (!$adminEmails) { + return []; + } + + return explode(',', $adminEmails); + } + + /** + * Returns the complete list of admin user accounts + * + * @param array $fields + * @return array + * @throws Exception + */ + public static function getAdminList(array $fields = []): array + { + $condition = [ + 'email' => self::getAdminEmailList(), + 'parent-uid' => null, + 'blocked' => false, + 'verified' => true, + 'account_removed' => false, + 'account_expired' => false, + ]; + + return DBA::selectToArray('user', $fields, $condition, ['order' => ['uid']]); + } + + /** + * Return a list of admin user accounts where each unique email address appears only once. + * + * This method is meant for admin notifications that do not need to be sent multiple times to the same email address. + * + * @param array $fields + * @return array + * @throws Exception + */ + public static function getAdminListForEmailing(array $fields = []): array + { + return array_filter(self::getAdminList($fields), function ($user) { + static $emails = []; + + if (in_array($user['email'], $emails)) { + return false; + } + + $emails[] = $user['email']; + + return true; + }); + } + + public static function setRegisterMethodByUserCount() + { + $max_registered_users = DI::config()->get('config', 'max_registered_users'); + if ($max_registered_users <= 0) { + return; + } + + $register_policy = DI::config()->get('config', 'register_policy'); + if (!in_array($register_policy, [Module\Register::OPEN, Module\Register::CLOSED])) { + Logger::debug('Unsupported register policy.', ['policy' => $register_policy]); + return; + } + + $users = DBA::count('user', ['blocked' => false, 'account_removed' => false, 'account_expired' => false]); + if (($users >= $max_registered_users) && ($register_policy == Module\Register::OPEN)) { + DI::config()->set('config', 'register_policy', Module\Register::CLOSED); + Logger::notice('Max users reached, registration is closed.', ['users' => $users, 'max' => $max_registered_users]); + } elseif (($users < $max_registered_users) && ($register_policy == Module\Register::CLOSED)) { + DI::config()->set('config', 'register_policy', Module\Register::OPEN); + Logger::notice('Below maximum users, registration is opened.', ['users' => $users, 'max' => $max_registered_users]); + } else { + Logger::debug('Unchanged register policy', ['policy' => $register_policy, 'users' => $users, 'max' => $max_registered_users]); + } + } }