]> git.mxchange.org Git - friendica.git/blobdiff - src/Model/User.php
Merge pull request #12547 from MrPetovan/bug/12545-plink-zindex
[friendica.git] / src / Model / User.php
index 07f2dfe8de003b21680cc906a3d4e3f8cfad73b0..916844251e5c69710faa69890c52f826d0ef56f4 100644 (file)
@@ -117,16 +117,18 @@ class User
                switch ($accounttype) {
                        case 'person':
                                return User::ACCOUNT_TYPE_PERSON;
+
                        case 'organisation':
                                return User::ACCOUNT_TYPE_ORGANISATION;
+
                        case 'news':
                                return User::ACCOUNT_TYPE_NEWS;
+
                        case 'community':
                                return User::ACCOUNT_TYPE_COMMUNITY;
-                       default:
-                               return null;
-                       break;
+
                }
+               return null;
        }
 
        /**
@@ -156,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'] = '';
@@ -210,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);
        }
 
@@ -262,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;
@@ -274,7 +278,8 @@ class User
        /**
         * Returns true if a user record exists with the provided id
         *
-        * @param  integer $uid
+        * @param  int $uid
+        *
         * @return boolean
         * @throws Exception
         */
@@ -362,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]);
        }
@@ -379,17 +382,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] ?? [];
        }
 
        /**
@@ -412,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])) {
@@ -512,7 +513,7 @@ class User
         * @throws HTTPException\ForbiddenException
         * @throws HTTPException\NotFoundException
         */
-       public static function getIdFromPasswordAuthentication($user_info, string $password, bool $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
@@ -665,6 +666,28 @@ 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
         *
@@ -733,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
         *
@@ -753,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 (:)'));
                }
 
@@ -782,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
         *
@@ -967,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.') . '<br />' . DI::l10n()->t('The error message was:') . $e->getMessage(), 0, $e);
                                }
                                System::externalRedirect($authurl);
                                // NOTREACHED
@@ -1027,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);
@@ -1164,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;
@@ -1290,7 +1351,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']);
@@ -1342,7 +1403,6 @@ 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
@@ -1392,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.'));
 
@@ -1496,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'],
@@ -1541,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;
        }
@@ -1688,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)) {
@@ -1703,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']++;
                        }
@@ -1731,7 +1791,6 @@ 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|bool The list of the users
         * @throws Exception
         */
@@ -1744,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;
@@ -1756,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;
+               });
+       }
 }