]> git.mxchange.org Git - friendica.git/blobdiff - src/Model/User.php
Merge pull request #13238 from annando/issue-13221
[friendica.git] / src / Model / User.php
index 005e3bbf35ccc6213dc7d098ab8deaea1a90b15b..854961154b5dfe56772dc11c70c7d4314a1eaae0 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /**
- * @copyright Copyright (C) 2010-2022, the Friendica project
+ * @copyright Copyright (C) 2010-2023, the Friendica project
  *
  * @license GNU AGPL version 3 or any later version
  *
@@ -35,17 +35,18 @@ use Friendica\Core\System;
 use Friendica\Core\Worker;
 use Friendica\Database\DBA;
 use Friendica\DI;
+use Friendica\Module;
 use Friendica\Network\HTTPClient\Client\HttpClientAccept;
 use Friendica\Security\TwoFactor\Model\AppSpecificPassword;
 use Friendica\Network\HTTPException;
 use Friendica\Object\Image;
+use Friendica\Protocol\Delivery;
 use Friendica\Util\Crypto;
 use Friendica\Util\DateTimeFormat;
 use Friendica\Util\Images;
 use Friendica\Util\Network;
 use Friendica\Util\Proxy;
 use Friendica\Util\Strings;
-use Friendica\Worker\Delivery;
 use ImagickException;
 use LightOpenID;
 
@@ -87,7 +88,7 @@ class User
         * ACCOUNT_TYPE_NEWS - the account is a news reflector
         *      Associated page type: PAGE_FLAGS_SOAPBOX
         *
-        * ACCOUNT_TYPE_COMMUNITY - the account is community forum
+        * ACCOUNT_TYPE_COMMUNITY - the account is community group
         *      Associated page types: PAGE_COMMUNITY, PAGE_FLAGS_PRVGROUP
         *
         * ACCOUNT_TYPE_RELAY - the account is a relay
@@ -158,6 +159,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'] = '';
@@ -165,7 +167,7 @@ class User
                $system['region'] = '';
                $system['postal-code'] = '';
                $system['country-name'] = '';
-               $system['homepage'] = DI::baseUrl()->get();
+               $system['homepage'] = (string)DI::baseUrl();
                $system['dob'] = '0000-00-00';
 
                // Ensure that the user contains data
@@ -218,7 +220,7 @@ class User
                        'self'         => true,
                        'network'      => Protocol::ACTIVITYPUB,
                        'name'         => 'System Account',
-                       'addr'         => $system_actor_name . '@' . DI::baseUrl()->getHostname(),
+                       'addr'         => $system_actor_name . '@' . DI::baseUrl()->getHost(),
                        'nick'         => $system_actor_name,
                        'url'          => DI::baseUrl() . '/friendica',
                        'pubkey'       => $keys['pubkey'],
@@ -265,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;
@@ -381,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] ?? [];
        }
 
        /**
@@ -483,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;
        }
 
        /**
@@ -529,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;
@@ -667,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
         *
@@ -736,7 +780,7 @@ class User
        }
 
        /**
-        * Allowed characters are a-z, A-Z, 0-9 and special characters except white spaces, accentuated letters and colon (:).
+        * 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
@@ -749,13 +793,13 @@ class User
         */
        public static function getPasswordRegExp(string $delimiter = null): string
        {
-               $allowed_characters = '!"#$%&\'()*+,-./;<=>?@[\]^_`{|}~';
+               $allowed_characters = ':!"#$%&\'()*+,-./;<=>?@[\]^_`{|}~';
 
                if ($delimiter) {
                        $allowed_characters = preg_quote($allowed_characters, $delimiter);
                }
 
-               return '^[a-zA-Z0-9' . $allowed_characters . ']' . (PASSWORD_DEFAULT !== PASSWORD_BCRYPT ? '{1,72}' : '+') . '$';
+               return '^[a-zA-Z0-9' . $allowed_characters . ']' . (PASSWORD_DEFAULT === PASSWORD_BCRYPT ? '{1,72}' : '+') . '$';
        }
 
        /**
@@ -783,7 +827,7 @@ class User
                }
 
                if (!preg_match('/' . self::getPasswordRegExp('/') . '/', $password)) {
-                       throw new Exception(DI::l10n()->t('The password can\'t contain accentuated letters, white spaces or colons (:)'));
+                       throw new Exception(DI::l10n()->t("The password can't contain white spaces nor accentuated letters"));
                }
 
                return self::updatePasswordHashed($uid, self::hashPassword($password));
@@ -794,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
@@ -809,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
@@ -986,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'];
@@ -1054,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);
@@ -1153,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];
@@ -1169,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);
@@ -1197,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);
@@ -1232,6 +1294,8 @@ class User
 
                Hook::callAll('register_account', $uid);
 
+               self::setRegisterMethodByUserCount();
+
                $return['user'] = $user;
                return $return;
        }
@@ -1326,7 +1390,7 @@ class User
                        $l10n,
                        $user,
                        DI::config()->get('config', 'sitename'),
-                       DI::baseUrl()->get(),
+                       DI::baseUrl(),
                        ($register['password'] ?? '') ?: 'Sent in a previous email'
                );
        }
@@ -1340,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
@@ -1418,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()
@@ -1522,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'],
@@ -1576,6 +1640,7 @@ class User
                // Remove the user relevant data
                Worker::add(Worker::PRIORITY_NEGLIGIBLE, 'RemoveUser', $uid);
 
+               self::setRegisterMethodByUserCount();
                return true;
        }
 
@@ -1602,7 +1667,7 @@ class User
         */
        public static function identities(int $uid): array
        {
-               if (empty($uid)) {
+               if (!$uid) {
                        return [];
                }
 
@@ -1613,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'],
@@ -1674,7 +1739,7 @@ class User
         */
        public static function hasIdentities(int $uid): bool
        {
-               if (empty($uid)) {
+               if (!$uid) {
                        return false;
                }
 
@@ -1683,7 +1748,7 @@ class User
                        return false;
                }
 
-               if ($user['parent-uid'] != 0) {
+               if ($user['parent-uid']) {
                        return true;
                }
 
@@ -1714,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)) {
@@ -1729,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']++;
                        }
@@ -1754,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
@@ -1783,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]);
+               }
+       }
 }