X-Git-Url: https://git.mxchange.org/?a=blobdiff_plain;f=src%2FModel%2FUser.php;h=2131406d4a5f8cf2be7432c66b57f2839936224c;hb=8c99d3acc1b65bd7645d69f430d5fd9604d6b3b5;hp=38fe3b0ec4798c4826680218b3e4b7042dca2598;hpb=5aba1df497379db0273249b8de5ca00aa4dc26bb;p=friendica.git diff --git a/src/Model/User.php b/src/Model/User.php index 38fe3b0ec4..2131406d4a 100644 --- a/src/Model/User.php +++ b/src/Model/User.php @@ -1,6 +1,6 @@ true, 'uid' => 0]); + if (!DBA::isResult($system)) { + self::createSystemAccount(); + $system = Contact::selectFirst([], ['self' => true, 'uid' => 0]); + if (!DBA::isResult($system)) { + return []; + } + } + + $system['sprvkey'] = $system['uprvkey'] = $system['prvkey']; + $system['spubkey'] = $system['upubkey'] = $system['pubkey']; + $system['nickname'] = $system['nick']; + + // Ensure that the user contains data + $user = DBA::selectFirst('user', ['prvkey'], ['uid' => 0]); + if (empty($user['prvkey'])) { + $fields = [ + 'username' => $system['name'], + 'nickname' => $system['nick'], + 'register_date' => $system['created'], + 'pubkey' => $system['pubkey'], + 'prvkey' => $system['prvkey'], + 'spubkey' => $system['spubkey'], + 'sprvkey' => $system['sprvkey'], + 'verified' => true, + 'page-flags' => User::PAGE_FLAGS_SOAPBOX, + 'account-type' => User::ACCOUNT_TYPE_RELAY, + ]; + + DBA::update('user', $fields, ['uid' => 0]); + } + + return $system; + } + + /** + * Create the system account + * + * @return void + */ + private static function createSystemAccount() + { + $system_actor_name = self::getActorName(); + if (empty($system_actor_name)) { + return; + } + + $keys = Crypto::newKeypair(4096); + if ($keys === false) { + 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['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']); + DBA::insert('contact', $system); + } + + /** + * Detect a usable actor name + * + * @return string actor account name + */ + public static function getActorName() + { + $system_actor_name = DI::config()->get('system', 'actor_name'); + if (!empty($system_actor_name)) { + $self = Contact::selectFirst(['nick'], ['uid' => 0, 'self' => true]); + if (!empty($self['nick'])) { + if ($self['nick'] != $system_actor_name) { + // Reset the actor name to the already used name + DI::config()->set('system', 'actor_name', $self['nick']); + $system_actor_name = $self['nick']; + } + } + return $system_actor_name; + } + + // 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]) && + !DBA::exists('userd', ['username' => $name])) { + DI::config()->set('system', 'actor_name', $name); + return $name; + } + } + return ''; + } + /** * Returns true if a user record exists with the provided id * @@ -119,7 +267,7 @@ class User */ public static function getById($uid, array $fields = []) { - return DBA::selectFirst('user', $fields, ['uid' => $uid]); + return !empty($uid) ? DBA::selectFirst('user', $fields, ['uid' => $uid]) : []; } /** @@ -164,6 +312,11 @@ class User */ public static function getIdForURL(string $url) { + // Avoid database queries when the local node hostname isn't even part of the url. + if (!Contact::isLocal($url)) { + return 0; + } + $self = Contact::selectFirst(['uid'], ['self' => true, 'nurl' => Strings::normaliseLink($url)]); if (!empty($self['uid'])) { return $self['uid']; @@ -218,20 +371,24 @@ class User /** * Get owner data by user id * - * @param int $uid - * @param boolean $check_valid Test if data is invalid and correct it + * @param int $uid + * @param boolean $repairMissing Repair the owner data if it's missing * @return boolean|array * @throws Exception */ - public static function getOwnerDataById(int $uid, bool $check_valid = true) + public static function getOwnerDataById(int $uid, bool $repairMissing = true) { + if ($uid == 0) { + return self::getSystemAccount(); + } + if (!empty(self::$owner[$uid])) { return self::$owner[$uid]; } $owner = DBA::selectFirst('owner-view', [], ['uid' => $uid]); if (!DBA::isResult($owner)) { - if (!DBA::exists('user', ['uid' => $uid]) || !$check_valid) { + if (!DBA::exists('user', ['uid' => $uid]) || !$repairMissing) { return false; } Contact::createSelfFromUserId($uid); @@ -242,7 +399,7 @@ class User return false; } - if (!$check_valid) { + if (!$repairMissing || $owner['account_expired']) { return $owner; } @@ -255,7 +412,7 @@ class User if (!$repair) { // Check if "addr" is present and correct $addr = $owner['nickname'] . '@' . substr(DI::baseUrl(), strpos(DI::baseUrl(), '://') + 3); - $repair = ($addr != $owner['addr']); + $repair = ($addr != $owner['addr']) || empty($owner['prvkey']) || empty($owner['pubkey']); } if (!$repair) { @@ -297,11 +454,11 @@ class User /** * Returns the default group for a given user and network * - * @param int $uid User id + * @param int $uid User id * @param string $network network name * * @return int group id - * @throws InternalServerErrorException + * @throws Exception */ public static function getDefaultGroup($uid, $network = '') { @@ -353,11 +510,32 @@ class User * @param string $password * @param bool $third_party * @return int User Id if authentication is successful - * @throws Exception + * @throws HTTPException\ForbiddenException + * @throws HTTPException\NotFoundException */ public static function getIdFromPasswordAuthentication($user_info, $password, $third_party = false) { - $user = self::getAuthenticationInfo($user_info); + // Addons registered with the "authenticate" hook may create the user on the + // fly. `getAuthenticationInfo` will fail if the user doesn't exist yet. If + // the user doesn't exist, we should give the addons a chance to create the + // user in our database, if applicable, before re-throwing the exception if + // they fail. + try { + $user = self::getAuthenticationInfo($user_info); + } catch (Exception $e) { + $username = (is_string($user_info) ? $user_info : $user_info['nickname'] ?? ''); + + // 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. + // So let's be very careful about that. + if (empty($username) || is_numeric($username)) { + throw $e; + } + + return self::getIdFromAuthenticateHooks($username, $password); + } if ($third_party && DI::pConfig()->get($user['uid'], '2fa', 'verified')) { // Third-party apps can't verify two-factor authentication, we use app-specific passwords instead @@ -386,9 +564,44 @@ class User } return $user['uid']; + } else { + return self::getIdFromAuthenticateHooks($user['nickname'], $password); // throws + } + + throw new HTTPException\ForbiddenException(DI::l10n()->t('Login failed')); + } + + /** + * Try to obtain a user ID via "authenticate" hook addons + * + * Returns the user id associated with a successful password authentication + * + * @param string $username + * @param string $password + * @return int User Id if authentication is successful + * @throws HTTPException\ForbiddenException + */ + public static function getIdFromAuthenticateHooks($username, $password) + { + $addon_auth = [ + 'username' => $username, + 'password' => $password, + 'authenticated' => 0, + 'user_record' => null + ]; + + /* + * An addon indicates successful login by setting 'authenticated' to non-zero value and returning a user record + * Addons should never set 'authenticated' except to indicate success - as hooks may be chained + * and later addons should not interfere with an earlier one that succeeded. + */ + Hook::callAll('authenticate', $addon_auth); + + if ($addon_auth['authenticated'] && $addon_auth['user_record']) { + return $addon_auth['user_record']['uid']; } - throw new Exception(DI::l10n()->t('Login failed')); + throw new HTTPException\ForbiddenException(DI::l10n()->t('Login failed')); } /** @@ -402,9 +615,9 @@ class User * * @param mixed $user_info * @return array - * @throws Exception + * @throws HTTPException\NotFoundException */ - private static function getAuthenticationInfo($user_info) + public static function getAuthenticationInfo($user_info) { $user = null; @@ -426,7 +639,7 @@ class User if (is_int($user_info)) { $user = DBA::selectFirst( 'user', - ['uid', 'password', 'legacy_password'], + ['uid', 'nickname', 'password', 'legacy_password'], [ 'uid' => $user_info, 'blocked' => 0, @@ -436,7 +649,7 @@ class User ] ); } else { - $fields = ['uid', 'password', 'legacy_password']; + $fields = ['uid', 'nickname', 'password', 'legacy_password']; $condition = [ "(`email` = ? OR `username` = ? OR `nickname` = ?) AND NOT `blocked` AND NOT `account_expired` AND NOT `account_removed` AND `verified`", @@ -446,7 +659,7 @@ class User } if (!DBA::isResult($user)) { - throw new Exception(DI::l10n()->t('User not found')); + throw new HTTPException\NotFoundException(DI::l10n()->t('User not found')); } } @@ -457,6 +670,7 @@ class User * Generates a human-readable random password * * @return string + * @throws Exception */ public static function generateNewPassword() { @@ -472,7 +686,7 @@ class User */ public static function isPasswordExposed($password) { - $cache = new \DivineOmega\DOFileCachePSR6\CacheItemPool(); + $cache = new CacheItemPool(); $cache->changeConfig([ 'cacheDirectory' => get_temppath() . '/password-exposed-cache/', ]); @@ -481,7 +695,7 @@ class User $passwordExposedChecker = new PasswordExposed\PasswordExposedChecker(null, $cache); return $passwordExposedChecker->passwordExposed($password) === PasswordExposed\PasswordStatus::EXPOSED; - } catch (\Exception $e) { + } catch (Exception $e) { Logger::error('Password Exposed Exception: ' . $e->getMessage(), [ 'code' => $e->getCode(), 'file' => $e->getFile(), @@ -578,20 +792,28 @@ class User * * @param string $nickname The nickname that should be checked * @return boolean True is the nickname is blocked on the node - * @throws InternalServerErrorException */ public static function isNicknameBlocked($nickname) { $forbidden_nicknames = DI::config()->get('system', 'forbidden_nicknames', ''); + if (!empty($forbidden_nicknames)) { + $forbidden = explode(',', $forbidden_nicknames); + $forbidden = array_map('trim', $forbidden); + } else { + $forbidden = []; + } - // if the config variable is empty return false - if (empty($forbidden_nicknames)) { + // Add the name of the internal actor to the "forbidden" list + $actor_name = self::getActorName(); + if (!empty($actor_name)) { + $forbidden[] = $actor_name; + } + + if (empty($forbidden)) { return false; } // check if the nickname is in the list of blocked nicknames - $forbidden = explode(',', $forbidden_nicknames); - $forbidden = array_map('trim', $forbidden); if (in_array(strtolower($nickname), $forbidden)) { return true; } @@ -614,9 +836,9 @@ class User * * @param array $data * @return array - * @throws \ErrorException - * @throws InternalServerErrorException - * @throws \ImagickException + * @throws ErrorException + * @throws HTTPException\InternalServerErrorException + * @throws ImagickException * @throws Exception */ public static function create(array $data) @@ -742,7 +964,7 @@ class User $nickname = $data['nickname'] = strtolower($nickname); - if (!preg_match('/^[a-z0-9][a-z0-9\_]*$/', $nickname)) { + if (!preg_match('/^[a-z0-9][a-z0-9_]*$/', $nickname)) { throw new Exception(DI::l10n()->t('Your nickname can only contain a-z, 0-9 and _.')); } @@ -858,7 +1080,7 @@ class User $photo_failure = false; $filename = basename($photo); - $curlResult = DI::httpRequest()->get($photo, true); + $curlResult = DI::httpRequest()->get($photo); if ($curlResult->isSuccess()) { $img_str = $curlResult->getBody(); $type = $curlResult->getContentType(); @@ -931,7 +1153,7 @@ class User * * @return bool True, if the allow was successful * - * @throws InternalServerErrorException + * @throws HTTPException\InternalServerErrorException * @throws Exception */ public static function allow(string $hash) @@ -992,6 +1214,9 @@ class User return false; } + // Delete the avatar + Photo::delete(['uid' => $register['uid']]); + return DBA::delete('user', ['uid' => $register['uid']]) && Register::deleteByHash($register['hash']); } @@ -1005,16 +1230,16 @@ class User * @param string $lang The user's language (default is english) * * @return bool True, if the user was created successfully - * @throws InternalServerErrorException - * @throws \ErrorException - * @throws \ImagickException + * @throws HTTPException\InternalServerErrorException + * @throws ErrorException + * @throws ImagickException */ public static function createMinimal(string $name, string $email, string $nick, string $lang = L10n::DEFAULT) { if (empty($name) || empty($email) || empty($nick)) { - throw new InternalServerErrorException('Invalid arguments.'); + throw new HTTPException\InternalServerErrorException('Invalid arguments.'); } $result = self::create([ @@ -1077,7 +1302,7 @@ class User * @param string $siteurl * @param string $password Plaintext password * @return NULL|boolean from notification() and email() inherited - * @throws InternalServerErrorException + * @throws HTTPException\InternalServerErrorException */ public static function sendRegisterPendingEmail($user, $sitename, $siteurl, $password) { @@ -1113,16 +1338,16 @@ class User * * It's here as a function because the mail is sent from different parts * - * @param \Friendica\Core\L10n $l10n The used language - * @param array $user User record array - * @param string $sitename - * @param string $siteurl - * @param string $password Plaintext password + * @param L10n $l10n The used language + * @param array $user User record array + * @param string $sitename + * @param string $siteurl + * @param string $password Plaintext password * * @return NULL|boolean from notification() and email() inherited - * @throws InternalServerErrorException + * @throws HTTPException\InternalServerErrorException */ - public static function sendRegisterOpenEmail(\Friendica\Core\L10n $l10n, $user, $sitename, $siteurl, $password) + public static function sendRegisterOpenEmail(L10n $l10n, $user, $sitename, $siteurl, $password) { $preamble = Strings::deindent($l10n->t( ' @@ -1179,11 +1404,11 @@ class User /** * @param int $uid user to remove * @return bool - * @throws InternalServerErrorException + * @throws HTTPException\InternalServerErrorException */ public static function remove(int $uid) { - if (!$uid) { + if (empty($uid)) { return false; } @@ -1197,6 +1422,9 @@ class User // unique), so it cannot be re-registered in the future. DBA::insert('userd', ['username' => $user['nickname']]); + // Remove all personal settings, especially connector settings + DBA::delete('pconfig', ['uid' => $uid]); + // 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); @@ -1367,7 +1595,9 @@ class User $condition['blocked'] = false; break; case 'blocked': + $condition['account_removed'] = false; $condition['blocked'] = true; + $condition['verified'] = true; break; case 'removed': $condition['account_removed'] = true;