X-Git-Url: https://git.mxchange.org/?a=blobdiff_plain;ds=sidebyside;f=src%2FModel%2FUser.php;h=916844251e5c69710faa69890c52f826d0ef56f4;hb=60551e6277a4e3a0901481e2a23b3963db6a1c2c;hp=263bba99c8355785e4d297df08fc45d089da4612;hpb=6f290607de7f10cea7429aacd0b394fd3f4c4e69;p=friendica.git diff --git a/src/Model/User.php b/src/Model/User.php index 263bba99c8..916844251e 100644 --- a/src/Model/User.php +++ b/src/Model/User.php @@ -1,6 +1,6 @@ true, 'uid' => 0]); if (!DBA::isResult($system)) { @@ -154,6 +158,7 @@ class User $system['publish'] = false; $system['net-publish'] = false; $system['hide-friends'] = true; + $system['hidewall'] = true; $system['prv_keywords'] = ''; $system['pub_keywords'] = ''; $system['address'] = ''; @@ -208,32 +213,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()->getHostname(), + '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); } @@ -242,7 +248,7 @@ class User * * @return string actor account name */ - public static function getActorName() + public static function getActorName(): string { $system_actor_name = DI::config()->get('system', 'actor_name'); if (!empty($system_actor_name)) { @@ -260,7 +266,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; @@ -272,11 +278,12 @@ class User /** * Returns true if a user record exists with the provided id * - * @param integer $uid + * @param int $uid + * * @return boolean * @throws Exception */ - public static function exists($uid) + public static function exists(int $uid): bool { return DBA::exists('user', ['uid' => $uid]); } @@ -287,7 +294,7 @@ class User * @return array|boolean User record if it exists, false otherwise * @throws Exception */ - public static function getById($uid, array $fields = []) + public static function getById(int $uid, array $fields = []) { return !empty($uid) ? DBA::selectFirst('user', $fields, ['uid' => $uid]) : []; } @@ -319,7 +326,7 @@ class User * @return array|boolean User record if it exists, false otherwise * @throws Exception */ - public static function getByNickname($nickname, array $fields = []) + public static function getByNickname(string $nickname, array $fields = []) { return DBA::selectFirst('user', $fields, ['nickname' => $nickname]); } @@ -332,7 +339,7 @@ class User * @return integer user id * @throws Exception */ - public static function getIdForURL(string $url) + public static function getIdForURL(string $url): int { // Avoid database queries when the local node hostname isn't even part of the url. if (!Contact::isLocal($url)) { @@ -360,14 +367,12 @@ class User /** * Get a user based on its email * - * @param string $email - * @param array $fields - * + * @param string $email + * @param array $fields * @return array|boolean User record if it exists, false otherwise - * * @throws Exception */ - public static function getByEmail($email, array $fields = []) + public static function getByEmail(string $email, array $fields = []) { return DBA::selectFirst('user', $fields, ['email' => $email]); } @@ -377,17 +382,15 @@ class User * * @param array $fields * @return array user + * @throws Exception */ - public static function getFirstAdmin(array $fields = []) + 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] ?? []; } /** @@ -410,7 +413,7 @@ class User $owner = DBA::selectFirst('owner-view', [], ['uid' => $uid]); if (!DBA::isResult($owner)) { - if (!DBA::exists('user', ['uid' => $uid]) || !$repairMissing) { + if (!self::exists($uid) || !$repairMissing) { return false; } if (!DBA::exists('profile', ['uid' => $uid])) { @@ -467,7 +470,7 @@ class User * @return boolean|array * @throws Exception */ - public static function getOwnerDataByNick($nick) + public static function getOwnerDataByNick(string $nick) { $user = DBA::selectFirst('user', ['uid'], ['nickname' => $nick]); @@ -481,13 +484,12 @@ class User /** * Returns the default group for a given user and network * - * @param int $uid User id - * @param string $network network name + * @param int $uid User id * * @return int group id * @throws Exception */ - public static function getDefaultGroup($uid, $network = '') + public static function getDefaultGroup(int $uid): int { $user = DBA::selectFirst('user', ['def_gid'], ['uid' => $uid]); if (DBA::isResult($user)) { @@ -499,26 +501,6 @@ class User return $default_group; } - - /** - * Authenticate a user with a clear text password - * - * @param mixed $user_info - * @param string $password - * @param bool $third_party - * @return int|boolean - * @deprecated since version 3.6 - * @see User::getIdFromPasswordAuthentication() - */ - public static function authenticate($user_info, $password, $third_party = false) - { - try { - return self::getIdFromPasswordAuthentication($user_info, $password, $third_party); - } catch (Exception $ex) { - return false; - } - } - /** * Authenticate a user with a clear text password * @@ -531,7 +513,7 @@ class User * @throws HTTPException\ForbiddenException * @throws HTTPException\NotFoundException */ - public static function getIdFromPasswordAuthentication($user_info, $password, $third_party = false) + public static function getIdFromPasswordAuthentication($user_info, string $password, bool $third_party = false): int { // Addons registered with the "authenticate" hook may create the user on the // fly. `getAuthenticationInfo` will fail if the user doesn't exist yet. If @@ -599,7 +581,7 @@ class User * @return int User Id if authentication is successful * @throws HTTPException\ForbiddenException */ - public static function getIdFromAuthenticateHooks($username, $password) + public static function getIdFromAuthenticateHooks(string $username, string $password): int { $addon_auth = [ 'username' => $username, @@ -632,7 +614,7 @@ class User * - User array with at least the uid and the hashed password * * @param mixed $user_info - * @return array + * @return array|null Null if not found/determined * @throws HTTPException\NotFoundException */ public static function getAuthenticationInfo($user_info) @@ -684,13 +666,35 @@ 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) + { + $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 actitivy 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 * * @return string * @throws Exception */ - public static function generateNewPassword() + public static function generateNewPassword(): string { return ucfirst(Strings::getRandomName(8)) . random_int(1000, 9999); } @@ -702,11 +706,11 @@ class User * @return bool * @throws Exception */ - public static function isPasswordExposed($password) + public static function isPasswordExposed(string $password): bool { $cache = new CacheItemPool(); $cache->changeConfig([ - 'cacheDirectory' => get_temppath() . '/password-exposed-cache/', + 'cacheDirectory' => System::getTempPath() . '/password-exposed-cache/', ]); try { @@ -731,7 +735,7 @@ class User * @param string $password * @return string */ - private static function hashPasswordLegacy($password) + private static function hashPasswordLegacy(string $password): string { return hash('whirlpool', $password); } @@ -743,7 +747,7 @@ class User * @return string * @throws Exception */ - public static function hashPassword($password) + public static function hashPassword(string $password): string { if (!trim($password)) { throw new Exception(DI::l10n()->t('Password can\'t be empty')); @@ -752,6 +756,29 @@ class User return password_hash($password, PASSWORD_DEFAULT); } + /** + * Allowed characters are a-z, A-Z, 0-9 and special characters except white spaces, accentuated letters and colon (:). + * + * 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 * @@ -760,7 +787,7 @@ class User * @return bool * @throws Exception */ - public static function updatePassword($uid, $password) + public static function updatePassword(int $uid, string $password): bool { $password = trim($password); @@ -772,9 +799,11 @@ 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)) { + if (!preg_match('/' . self::getPasswordRegExp('/') . '/', $password)) { throw new Exception(DI::l10n()->t('The password can\'t contain accentuated letters, white spaces or colons (:)')); } @@ -790,7 +819,7 @@ class User * @return bool * @throws Exception */ - private static function updatePasswordHashed($uid, $pasword_hashed) + private static function updatePasswordHashed(int $uid, string $pasword_hashed): bool { $fields = [ 'password' => $pasword_hashed, @@ -801,6 +830,22 @@ 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 * @@ -811,7 +856,7 @@ class User * @param string $nickname The nickname that should be checked * @return boolean True is the nickname is blocked on the node */ - public static function isNicknameBlocked($nickname) + public static function isNicknameBlocked(string $nickname): bool { $forbidden_nicknames = DI::config()->get('system', 'forbidden_nicknames', ''); if (!empty($forbidden_nicknames)) { @@ -848,7 +893,7 @@ class User * @return string avatar link * @throws Exception */ - public static function getAvatarUrl(array $user, string $size = ''):string + public static function getAvatarUrl(array $user, string $size = ''): string { if (empty($user['nickname'])) { DI::logger()->warning('Missing user nickname key', ['trace' => System::callstack(20)]); @@ -871,19 +916,46 @@ class User break; } - $updated = ''; - $imagetype = IMAGETYPE_JPEG; + $updated = ''; + $mimetype = ''; $photo = Photo::selectFirst(['type', 'created', 'edited', 'updated'], ["scale" => $scale, 'uid' => $user['uid'], 'profile' => true]); if (!empty($photo)) { - $updated = max($photo['created'], $photo['edited'], $photo['updated']); + $updated = max($photo['created'], $photo['edited'], $photo['updated']); + $mimetype = $photo['type']; + } - if (in_array($photo['type'], ['image/png', 'image/gif'])) { - $imagetype = IMAGETYPE_PNG; - } + return $url . $user['nickname'] . Images::getExtensionByMimeType($mimetype) . ($updated ? '?ts=' . strtotime($updated) : ''); + } + + /** + * Get banner link for given user + * + * @param array $user + * @return string banner link + * @throws Exception + */ + public static function getBannerUrl(array $user): string + { + if (empty($user['nickname'])) { + DI::logger()->warning('Missing user nickname key', ['trace' => System::callstack(20)]); } - return $url . $user['nickname'] . image_type_to_extension($imagetype) . ($updated ? '?ts=' . strtotime($updated) : ''); + $url = DI::baseUrl() . '/photo/banner/'; + + $updated = ''; + $mimetype = ''; + + $photo = Photo::selectFirst(['type', 'created', 'edited', 'updated'], ["scale" => 3, 'uid' => $user['uid'], 'photo-type' => Photo::USER_BANNER]); + if (!empty($photo)) { + $updated = max($photo['created'], $photo['edited'], $photo['updated']); + $mimetype = $photo['type']; + } else { + // Only for the RC phase: Don't return an image link for the default picture + return ''; + } + + return $url . $user['nickname'] . Images::getExtensionByMimeType($mimetype) . ($updated ? '?ts=' . strtotime($updated) : ''); } /** @@ -905,24 +977,24 @@ class User * @throws ImagickException * @throws Exception */ - public static function create(array $data) + public static function create(array $data): array { $return = ['user' => null, 'password' => '']; $using_invites = DI::config()->get('system', 'invitation_only'); - $invite_id = !empty($data['invite_id']) ? Strings::escapeTags(trim($data['invite_id'])) : ''; - $username = !empty($data['username']) ? Strings::escapeTags(trim($data['username'])) : ''; - $nickname = !empty($data['nickname']) ? Strings::escapeTags(trim($data['nickname'])) : ''; - $email = !empty($data['email']) ? Strings::escapeTags(trim($data['email'])) : ''; - $openid_url = !empty($data['openid_url']) ? Strings::escapeTags(trim($data['openid_url'])) : ''; - $photo = !empty($data['photo']) ? Strings::escapeTags(trim($data['photo'])) : ''; - $password = !empty($data['password']) ? trim($data['password']) : ''; - $password1 = !empty($data['password1']) ? trim($data['password1']) : ''; - $confirm = !empty($data['confirm']) ? trim($data['confirm']) : ''; + $invite_id = !empty($data['invite_id']) ? trim($data['invite_id']) : ''; + $username = !empty($data['username']) ? trim($data['username']) : ''; + $nickname = !empty($data['nickname']) ? trim($data['nickname']) : ''; + $email = !empty($data['email']) ? trim($data['email']) : ''; + $openid_url = !empty($data['openid_url']) ? trim($data['openid_url']) : ''; + $photo = !empty($data['photo']) ? trim($data['photo']) : ''; + $password = !empty($data['password']) ? trim($data['password']) : ''; + $password1 = !empty($data['password1']) ? trim($data['password1']) : ''; + $confirm = !empty($data['confirm']) ? trim($data['confirm']) : ''; $blocked = !empty($data['blocked']); $verified = !empty($data['verified']); - $language = !empty($data['language']) ? Strings::escapeTags(trim($data['language'])) : 'en'; + $language = !empty($data['language']) ? trim($data['language']) : 'en'; $netpublish = $publish = !empty($data['profile_publish_reg']); @@ -959,7 +1031,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 @@ -979,7 +1051,7 @@ class User $username_max_length = max(1, min(64, intval(DI::config()->get('system', 'username_max_length', 48)))); if ($username_min_length > $username_max_length) { - Logger::log(DI::l10n()->t('system.username_min_length (%s) and system.username_max_length (%s) are excluding each other, swapping values.', $username_min_length, $username_max_length), Logger::WARNING); + Logger::error(DI::l10n()->t('system.username_min_length (%s) and system.username_max_length (%s) are excluding each other, swapping values.', $username_min_length, $username_max_length)); $tmp = $username_min_length; $username_min_length = $username_max_length; $username_max_length = $tmp; @@ -1019,11 +1091,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); @@ -1144,8 +1213,9 @@ class User $photo_failure = false; $filename = basename($photo); - $curlResult = DI::httpClient()->get($photo); + $curlResult = DI::httpClient()->get($photo, HttpClientAccept::IMAGE); if ($curlResult->isSuccess()) { + Logger::debug('Got picture', ['Content-Type' => $curlResult->getHeader('Content-Type'), 'url' => $photo]); $img_str = $curlResult->getBody(); $type = $curlResult->getContentType(); } else { @@ -1155,32 +1225,32 @@ class User $type = Images::getMimeTypeByData($img_str, $photo, $type); - $Image = new Image($img_str, $type); - if ($Image->isValid()) { - $Image->scaleToSquare(300); + $image = new Image($img_str, $type); + if ($image->isValid()) { + $image->scaleToSquare(300); $resource_id = Photo::newResource(); // Not using Photo::PROFILE_PHOTOS here, so that it is discovered as translateble string $profile_album = DI::l10n()->t('Profile Photos'); - $r = Photo::store($Image, $uid, 0, $resource_id, $filename, $profile_album, 4); + $r = Photo::store($image, $uid, 0, $resource_id, $filename, $profile_album, 4); if ($r === false) { $photo_failure = true; } - $Image->scaleDown(80); + $image->scaleDown(80); - $r = Photo::store($Image, $uid, 0, $resource_id, $filename, $profile_album, 5); + $r = Photo::store($image, $uid, 0, $resource_id, $filename, $profile_album, 5); if ($r === false) { $photo_failure = true; } - $Image->scaleDown(48); + $image->scaleDown(48); - $r = Photo::store($Image, $uid, 0, $resource_id, $filename, $profile_album, 6); + $r = Photo::store($image, $uid, 0, $resource_id, $filename, $profile_album, 6); if ($r === false) { $photo_failure = true; @@ -1246,7 +1316,7 @@ class User * @throws Exception */ - public static function block(int $uid, bool $block = true) + public static function block(int $uid, bool $block = true): bool { return DBA::update('user', ['blocked' => $block], ['uid' => $uid]); } @@ -1261,7 +1331,7 @@ class User * @throws HTTPException\InternalServerErrorException * @throws Exception */ - public static function allow(string $hash) + public static function allow(string $hash): bool { $register = Register::getByHash($hash); if (!DBA::isResult($register)) { @@ -1279,9 +1349,9 @@ class User $profile = DBA::selectFirst('profile', ['net-publish'], ['uid' => $register['uid']]); - if (DBA::isResult($profile) && $profile['net-publish'] && DI::config()->get('system', 'directory')) { + 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']); @@ -1307,7 +1377,7 @@ class User * @return bool True, if the deny was successfull * @throws Exception */ - public static function deny(string $hash) + public static function deny(string $hash): bool { $register = Register::getByHash($hash); if (!DBA::isResult($register)) { @@ -1333,13 +1403,12 @@ class User * @param string $email The user's email address * @param string $nick The user's nick name * @param string $lang The user's language (default is english) - * * @return bool True, if the user was created successfully * @throws HTTPException\InternalServerErrorException * @throws ErrorException * @throws ImagickException */ - public static function createMinimal(string $name, string $email, string $nick, string $lang = L10n::DEFAULT) + public static function createMinimal(string $name, string $email, string $nick, string $lang = L10n::DEFAULT): bool { if (empty($name) || empty($email) || @@ -1383,7 +1452,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 %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.')); @@ -1409,7 +1478,7 @@ class User * @return NULL|boolean from notification() and email() inherited * @throws HTTPException\InternalServerErrorException */ - public static function sendRegisterPendingEmail($user, $sitename, $siteurl, $password) + public static function sendRegisterPendingEmail(array $user, string $sitename, string $siteurl, string $password) { $body = Strings::deindent(DI::l10n()->t( ' @@ -1452,7 +1521,7 @@ class User * @return NULL|boolean from notification() and email() inherited * @throws HTTPException\InternalServerErrorException */ - public static function sendRegisterOpenEmail(L10n $l10n, $user, $sitename, $siteurl, $password) + public static function sendRegisterOpenEmail(L10n $l10n, array $user, string $sitename, string $siteurl, string $password) { $preamble = Strings::deindent($l10n->t( ' @@ -1487,7 +1556,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'], @@ -1511,13 +1580,13 @@ class User * @return bool * @throws HTTPException\InternalServerErrorException */ - public static function remove(int $uid) + public static function remove(int $uid): bool { if (empty($uid)) { return false; } - Logger::log('Removing user: ' . $uid); + Logger::notice('Removing user', ['user' => $uid]); $user = DBA::selectFirst('user', [], ['uid' => $uid]); @@ -1532,14 +1601,14 @@ 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); return true; } @@ -1565,7 +1634,7 @@ class User * ] * @throws Exception */ - public static function identities($uid) + public static function identities(int $uid): array { if (empty($uid)) { return []; @@ -1637,7 +1706,7 @@ class User * @param int $uid * @return bool */ - public static function hasIdentities(int $uid):bool + public static function hasIdentities(int $uid): bool { if (empty($uid)) { return false; @@ -1670,7 +1739,7 @@ class User * * @throws Exception */ - public static function getStatistics() + public static function getStatistics(): array { $statistics = [ 'total_users' => 0, @@ -1679,8 +1748,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)) { @@ -1694,17 +1763,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']++; } @@ -1722,11 +1791,10 @@ class User * @param string $type The type of users, which should get (all, bocked, removed) * @param string $order Order of the user list (Default is 'contact.name') * @param bool $descending Order direction (Default is ascending) - * - * @return array The list of the users + * @return array|bool The list of the users * @throws Exception */ - public static function getList($start = 0, $count = Pager::ITEMS_PER_PAGE, $type = 'all', $order = 'name', bool $descending = false) + public static function getList(int $start = 0, int $count = Pager::ITEMS_PER_PAGE, string $type = 'all', string $order = 'name', bool $descending = false) { $param = ['limit' => [$start, $count], 'order' => [$order => $descending]]; $condition = []; @@ -1735,11 +1803,13 @@ class User $condition['account_removed'] = false; $condition['blocked'] = false; break; + case 'blocked': $condition['account_removed'] = false; $condition['blocked'] = true; $condition['verified'] = true; break; + case 'removed': $condition['account_removed'] = true; break; @@ -1747,4 +1817,64 @@ 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' => 0, + 'blocked' => 0, + '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; + }); + } }