2017-11-19 21:55:28 +00:00
< ? php
/**
2023-01-01 14:36:24 +00:00
* @ copyright Copyright ( C ) 2010 - 2023 , the Friendica project
2020-02-09 14:45:36 +00:00
*
* @ license GNU AGPL version 3 or any later version
*
* This program is free software : you can redistribute it and / or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation , either version 3 of the
* License , or ( at your option ) any later version .
*
* This program is distributed in the hope that it will be useful ,
* but WITHOUT ANY WARRANTY ; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
* GNU Affero General Public License for more details .
*
* You should have received a copy of the GNU Affero General Public License
* along with this program . If not , see < https :// www . gnu . org / licenses />.
*
2017-11-19 21:55:28 +00:00
*/
2019-10-10 23:21:41 +00:00
2017-11-19 21:55:28 +00:00
namespace Friendica\Model ;
2020-07-21 06:27:05 +00:00
use DivineOmega\DOFileCachePSR6\CacheItemPool ;
2018-10-19 15:26:48 +00:00
use DivineOmega\PasswordExposed ;
2020-07-21 06:27:05 +00:00
use ErrorException ;
2018-07-20 02:15:21 +00:00
use Exception ;
2020-02-25 21:16:27 +00:00
use Friendica\Content\Pager ;
2018-11-25 01:56:38 +00:00
use Friendica\Core\Hook ;
2020-02-21 21:57:17 +00:00
use Friendica\Core\L10n ;
2018-10-29 21:20:46 +00:00
use Friendica\Core\Logger ;
2018-08-11 20:40:44 +00:00
use Friendica\Core\Protocol ;
2022-04-26 18:33:58 +00:00
use Friendica\Core\Search ;
2017-11-19 21:55:28 +00:00
use Friendica\Core\System ;
use Friendica\Core\Worker ;
2018-07-20 12:19:26 +00:00
use Friendica\Database\DBA ;
2019-12-15 23:47:24 +00:00
use Friendica\DI ;
2023-04-01 12:07:48 +00:00
use Friendica\Module ;
2022-04-02 18:26:11 +00:00
use Friendica\Network\HTTPClient\Client\HttpClientAccept ;
2020-07-21 06:27:05 +00:00
use Friendica\Network\HTTPException ;
2017-12-07 13:56:11 +00:00
use Friendica\Object\Image ;
2022-12-30 21:20:28 +00:00
use Friendica\Protocol\Delivery ;
2023-07-27 15:12:05 +00:00
use Friendica\Security\TwoFactor\Model\AppSpecificPassword ;
2017-12-30 16:51:49 +00:00
use Friendica\Util\Crypto ;
2018-01-27 02:38:34 +00:00
use Friendica\Util\DateTimeFormat ;
2019-10-18 01:26:15 +00:00
use Friendica\Util\Images ;
2018-01-27 04:09:48 +00:00
use Friendica\Util\Network ;
2020-12-07 06:43:43 +00:00
use Friendica\Util\Proxy ;
2018-11-08 15:14:37 +00:00
use Friendica\Util\Strings ;
2020-07-21 06:27:05 +00:00
use ImagickException ;
2018-01-25 02:08:45 +00:00
use LightOpenID ;
2017-11-19 21:55:28 +00:00
/**
2020-01-19 06:05:23 +00:00
* This class handles User related functions
2017-11-19 21:55:28 +00:00
*/
class User
{
2019-01-06 17:37:48 +00:00
/**
2019-01-14 13:45:37 +00:00
* Page / profile types
2019-01-06 17:37:48 +00:00
*
2019-01-14 13:45:37 +00:00
* PAGE_FLAGS_NORMAL is a typical personal profile account
* PAGE_FLAGS_SOAPBOX automatically approves all friend requests as Contact :: SHARING , ( readonly )
* PAGE_FLAGS_COMMUNITY automatically approves all friend requests as Contact :: SHARING , but with
2019-01-06 17:37:48 +00:00
* write access to wall and comments ( no email and not included in page owner ' s ACL lists )
2019-01-14 13:45:37 +00:00
* PAGE_FLAGS_FREELOVE automatically approves all friend requests as full friends ( Contact :: FRIEND ) .
2019-01-06 17:37:48 +00:00
*
* @ {
*/
const PAGE_FLAGS_NORMAL = 0 ;
const PAGE_FLAGS_SOAPBOX = 1 ;
const PAGE_FLAGS_COMMUNITY = 2 ;
const PAGE_FLAGS_FREELOVE = 3 ;
const PAGE_FLAGS_BLOG = 4 ;
const PAGE_FLAGS_PRVGROUP = 5 ;
/**
* @ }
*/
2019-01-06 22:08:35 +00:00
/**
* Account types
*
* ACCOUNT_TYPE_PERSON - the account belongs to a person
* Associated page types : PAGE_FLAGS_NORMAL , PAGE_FLAGS_SOAPBOX , PAGE_FLAGS_FREELOVE
*
* ACCOUNT_TYPE_ORGANISATION - the account belongs to an organisation
* Associated page type : PAGE_FLAGS_SOAPBOX
*
* ACCOUNT_TYPE_NEWS - the account is a news reflector
* Associated page type : PAGE_FLAGS_SOAPBOX
*
2023-05-30 13:15:17 +00:00
* ACCOUNT_TYPE_COMMUNITY - the account is community group
2019-01-06 22:08:35 +00:00
* Associated page types : PAGE_COMMUNITY , PAGE_FLAGS_PRVGROUP
*
* ACCOUNT_TYPE_RELAY - the account is a relay
* This will only be assigned to contacts , not to user accounts
* @ {
*/
const ACCOUNT_TYPE_PERSON = 0 ;
const ACCOUNT_TYPE_ORGANISATION = 1 ;
const ACCOUNT_TYPE_NEWS = 2 ;
const ACCOUNT_TYPE_COMMUNITY = 3 ;
const ACCOUNT_TYPE_RELAY = 4 ;
2020-12-15 22:56:46 +00:00
const ACCOUNT_TYPE_DELETED = 127 ;
2019-01-06 22:08:35 +00:00
/**
* @ }
*/
2020-08-16 11:57:56 +00:00
private static $owner ;
2020-10-09 19:08:50 +00:00
/**
* Returns the numeric account type by their string
*
* @ param string $accounttype as string constant
2020-10-10 07:14:43 +00:00
* @ return int | null Numeric account type - or null when not set
2020-10-09 19:08:50 +00:00
*/
public static function getAccountTypeByString ( string $accounttype )
{
switch ( $accounttype ) {
case 'person' :
return User :: ACCOUNT_TYPE_PERSON ;
2022-06-23 08:56:37 +00:00
2020-10-09 19:08:50 +00:00
case 'organisation' :
return User :: ACCOUNT_TYPE_ORGANISATION ;
2022-06-23 08:56:37 +00:00
2020-10-09 19:08:50 +00:00
case 'news' :
return User :: ACCOUNT_TYPE_NEWS ;
2022-06-23 08:56:37 +00:00
2020-10-09 19:08:50 +00:00
case 'community' :
return User :: ACCOUNT_TYPE_COMMUNITY ;
2022-06-23 08:56:37 +00:00
2020-10-09 19:08:50 +00:00
}
2022-06-23 08:56:37 +00:00
return null ;
2020-10-09 19:08:50 +00:00
}
2023-07-15 20:12:08 +00:00
/**
* Get the Uri - Id of the system account
*
* @ return integer
*/
public static function getSystemUriId () : int
{
$system = self :: getSystemAccount ();
return $system [ 'uri-id' ] ? ? 0 ;
}
2020-08-22 14:48:09 +00:00
/**
* Fetch the system account
*
2020-08-22 18:52:37 +00:00
* @ return array system account
2020-08-22 14:48:09 +00:00
*/
2022-06-16 19:57:02 +00:00
public static function getSystemAccount () : array
2020-08-22 14:48:09 +00:00
{
$system = Contact :: selectFirst ([], [ 'self' => true , 'uid' => 0 ]);
if ( ! DBA :: isResult ( $system )) {
self :: createSystemAccount ();
$system = Contact :: selectFirst ([], [ 'self' => true , 'uid' => 0 ]);
if ( ! DBA :: isResult ( $system )) {
return [];
}
}
2020-08-23 07:29:56 +00:00
$system [ 'sprvkey' ] = $system [ 'uprvkey' ] = $system [ 'prvkey' ];
$system [ 'spubkey' ] = $system [ 'upubkey' ] = $system [ 'pubkey' ];
2020-08-22 14:48:09 +00:00
$system [ 'nickname' ] = $system [ 'nick' ];
2021-07-04 21:26:08 +00:00
$system [ 'page-flags' ] = User :: PAGE_FLAGS_SOAPBOX ;
$system [ 'account-type' ] = $system [ 'contact-type' ];
$system [ 'guid' ] = '' ;
2021-08-23 18:07:32 +00:00
$system [ 'picdate' ] = '' ;
$system [ 'theme' ] = '' ;
$system [ 'publish' ] = false ;
$system [ 'net-publish' ] = false ;
$system [ 'hide-friends' ] = true ;
2022-12-06 22:23:43 +00:00
$system [ 'hidewall' ] = true ;
2021-08-23 18:07:32 +00:00
$system [ 'prv_keywords' ] = '' ;
$system [ 'pub_keywords' ] = '' ;
$system [ 'address' ] = '' ;
2021-07-04 21:26:08 +00:00
$system [ 'locality' ] = '' ;
$system [ 'region' ] = '' ;
2021-08-23 18:07:32 +00:00
$system [ 'postal-code' ] = '' ;
2021-07-04 21:26:08 +00:00
$system [ 'country-name' ] = '' ;
2023-05-11 08:13:19 +00:00
$system [ 'homepage' ] = ( string ) DI :: baseUrl ();
2021-08-23 18:07:32 +00:00
$system [ 'dob' ] = '0000-00-00' ;
2020-11-18 05:33:17 +00:00
// Ensure that the user contains data
2023-09-03 08:44:17 +00:00
$user = DBA :: selectFirst ( 'user' , [ 'prvkey' , 'guid' , 'language' ], [ 'uid' => 0 ]);
2021-08-05 08:30:44 +00:00
if ( empty ( $user [ 'prvkey' ]) || empty ( $user [ 'guid' ])) {
2020-11-18 05:33:17 +00:00
$fields = [
'username' => $system [ 'name' ],
'nickname' => $system [ 'nick' ],
'register_date' => $system [ 'created' ],
'pubkey' => $system [ 'pubkey' ],
'prvkey' => $system [ 'prvkey' ],
'spubkey' => $system [ 'spubkey' ],
'sprvkey' => $system [ 'sprvkey' ],
2021-08-05 08:30:44 +00:00
'guid' => System :: createUUID (),
2020-11-18 05:33:17 +00:00
'verified' => true ,
'page-flags' => User :: PAGE_FLAGS_SOAPBOX ,
'account-type' => User :: ACCOUNT_TYPE_RELAY ,
];
DBA :: update ( 'user' , $fields , [ 'uid' => 0 ]);
2021-08-05 08:30:44 +00:00
$system [ 'guid' ] = $fields [ 'guid' ];
} else {
2023-09-03 08:44:17 +00:00
$system [ 'guid' ] = $user [ 'guid' ];
$system [ 'language' ] = $user [ 'language' ];
2020-11-18 05:33:17 +00:00
}
2020-08-22 14:48:09 +00:00
return $system ;
}
/**
* Create the system account
*
* @ return void
*/
private static function createSystemAccount ()
{
2020-08-22 18:52:37 +00:00
$system_actor_name = self :: getActorName ();
2020-08-22 14:48:09 +00:00
if ( empty ( $system_actor_name )) {
2020-08-22 18:52:37 +00:00
return ;
2020-08-22 14:48:09 +00:00
}
$keys = Crypto :: newKeypair ( 4096 );
if ( $keys === false ) {
throw new Exception ( DI :: l10n () -> t ( 'SERIOUS ERROR: Generation of security keys failed.' ));
}
2022-07-29 04:30:29 +00:00
$system = [
'uid' => 0 ,
'created' => DateTimeFormat :: utcNow (),
'self' => true ,
'network' => Protocol :: ACTIVITYPUB ,
'name' => 'System Account' ,
2023-02-18 19:57:30 +00:00
'addr' => $system_actor_name . '@' . DI :: baseUrl () -> getHost (),
2022-07-29 04:30:29 +00:00
'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 (),
];
2020-12-07 06:43:43 +00:00
$system [ 'avatar' ] = $system [ 'photo' ] = Contact :: getDefaultAvatar ( $system , Proxy :: SIZE_SMALL );
2022-07-29 04:30:29 +00:00
$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' ]);
2021-09-10 13:05:16 +00:00
Contact :: insert ( $system );
2020-08-22 14:48:09 +00:00
}
/**
* Detect a usable actor name
*
* @ return string actor account name
*/
2022-06-16 19:57:02 +00:00
public static function getActorName () : string
2020-08-22 14:48:09 +00:00
{
2020-08-22 18:52:37 +00:00
$system_actor_name = DI :: config () -> get ( 'system' , 'actor_name' );
if ( ! empty ( $system_actor_name )) {
2020-08-22 19:40:31 +00:00
$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' ];
}
}
2020-08-22 18:52:37 +00:00
return $system_actor_name ;
}
2020-08-22 14:48:09 +00:00
// List of possible actor names
$possible_accounts = [ 'friendica' , 'actor' , 'system' , 'internal' ];
foreach ( $possible_accounts as $name ) {
2023-09-08 15:15:38 +00:00
if ( ! DBA :: exists ( 'user' , [ 'nickname' => $name ]) && ! DBA :: exists ( 'userd' , [ 'username' => $name ])) {
2020-08-22 14:48:09 +00:00
DI :: config () -> set ( 'system' , 'actor_name' , $name );
return $name ;
}
}
return '' ;
}
2018-10-14 15:34:34 +00:00
/**
* Returns true if a user record exists with the provided id
*
2022-07-21 14:01:38 +00:00
* @ param int $uid
*
2018-10-14 15:34:34 +00:00
* @ return boolean
2019-01-06 21:06:53 +00:00
* @ throws Exception
2018-10-14 15:34:34 +00:00
*/
2022-06-16 19:57:02 +00:00
public static function exists ( int $uid ) : bool
2018-10-14 15:34:34 +00:00
{
return DBA :: exists ( 'user' , [ 'uid' => $uid ]);
}
2018-09-28 03:56:41 +00:00
/**
2018-10-15 15:58:52 +00:00
* @ param integer $uid
2019-04-30 22:14:06 +00:00
* @ param array $fields
2018-10-15 15:58:52 +00:00
* @ return array | boolean User record if it exists , false otherwise
2019-01-06 21:06:53 +00:00
* @ throws Exception
2018-10-15 15:58:52 +00:00
*/
2022-06-16 19:57:02 +00:00
public static function getById ( int $uid , array $fields = [])
2018-10-15 15:58:52 +00:00
{
2020-11-19 17:19:14 +00:00
return ! empty ( $uid ) ? DBA :: selectFirst ( 'user' , $fields , [ 'uid' => $uid ]) : [];
2019-04-30 22:14:06 +00:00
}
2019-10-20 11:00:08 +00:00
/**
* Returns a user record based on it ' s GUID
*
* @ param string $guid The guid of the user
* @ param array $fields The fields to retrieve
* @ param bool $active True , if only active records are searched
*
* @ return array | boolean User record if it exists , false otherwise
* @ throws Exception
*/
public static function getByGuid ( string $guid , array $fields = [], bool $active = true )
{
if ( $active ) {
2023-09-08 15:01:51 +00:00
$cond = [ 'guid' => $guid , 'verified' => true , 'blocked' => false , 'account_removed' => false , 'account_expired' => false ];
2019-10-20 11:00:08 +00:00
} else {
$cond = [ 'guid' => $guid ];
}
return DBA :: selectFirst ( 'user' , $fields , $cond );
}
2019-04-30 22:14:06 +00:00
/**
* @ param string $nickname
* @ param array $fields
* @ return array | boolean User record if it exists , false otherwise
* @ throws Exception
*/
2022-06-16 19:57:02 +00:00
public static function getByNickname ( string $nickname , array $fields = [])
2019-04-30 22:14:06 +00:00
{
return DBA :: selectFirst ( 'user' , $fields , [ 'nickname' => $nickname ]);
2018-10-15 15:58:52 +00:00
}
2023-10-29 19:43:44 +00:00
/**
* Set static settings for community user accounts
*
* @ param integer $uid
* @ return void
*/
public static function setCommunityUserSettings ( int $uid )
{
$user = self :: getById ( $uid , [ 'account-type' , 'page-flags' ]);
if ( $user [ 'account-type' ] != User :: ACCOUNT_TYPE_COMMUNITY ) {
return ;
}
DI :: pConfig () -> set ( $uid , 'system' , 'unlisted' , true );
$fields = [
'allow_cid' => '' ,
'allow_gid' => $user [ 'page-flags' ] == User :: PAGE_FLAGS_PRVGROUP ? '<' . Circle :: FOLLOWERS . '>' : '' ,
'deny_cid' => '' ,
'deny_gid' => '' ,
'blockwall' => true ,
'hidewall' => true ,
];
User :: update ( $fields , $uid );
Profile :: update ([ 'hide-friends' => true ], $uid );
}
2018-10-15 15:58:52 +00:00
/**
2020-01-19 06:05:23 +00:00
* Returns the user id of a given profile URL
2018-09-28 03:56:41 +00:00
*
2018-10-15 15:58:52 +00:00
* @ param string $url
2018-09-28 03:56:41 +00:00
*
* @ return integer user id
2019-01-06 21:06:53 +00:00
* @ throws Exception
2018-09-28 03:56:41 +00:00
*/
2022-06-16 19:57:02 +00:00
public static function getIdForURL ( string $url ) : int
2018-09-28 03:56:41 +00:00
{
2021-05-26 18:15:07 +00:00
// Avoid database queries when the local node hostname isn't even part of the url.
2021-05-26 09:24:37 +00:00
if ( ! Contact :: isLocal ( $url )) {
2020-08-19 04:45:31 +00:00
return 0 ;
}
2020-08-18 19:45:01 +00:00
$self = Contact :: selectFirst ([ 'uid' ], [ 'self' => true , 'nurl' => Strings :: normaliseLink ( $url )]);
if ( ! empty ( $self [ 'uid' ])) {
return $self [ 'uid' ];
}
$self = Contact :: selectFirst ([ 'uid' ], [ 'self' => true , 'addr' => $url ]);
if ( ! empty ( $self [ 'uid' ])) {
2018-09-28 03:56:41 +00:00
return $self [ 'uid' ];
}
2020-08-18 19:45:01 +00:00
$self = Contact :: selectFirst ([ 'uid' ], [ 'self' => true , 'alias' => [ $url , Strings :: normaliseLink ( $url )]]);
if ( ! empty ( $self [ 'uid' ])) {
return $self [ 'uid' ];
}
return 0 ;
2018-09-28 03:56:41 +00:00
}
2019-05-04 08:05:21 +00:00
/**
2019-05-05 08:00:28 +00:00
* Get a user based on its email
2019-05-04 08:05:21 +00:00
*
2022-06-23 08:56:37 +00:00
* @ param string $email
* @ param array $fields
2019-05-04 08:05:21 +00:00
* @ return array | boolean User record if it exists , false otherwise
* @ throws Exception
*/
2022-06-23 08:56:37 +00:00
public static function getByEmail ( string $email , array $fields = [])
2019-05-04 08:05:21 +00:00
{
return DBA :: selectFirst ( 'user' , $fields , [ 'email' => $email ]);
}
2020-07-29 05:12:16 +00:00
/**
* Fetch the user array of the administrator . The first one if there are several .
*
* @ param array $fields
* @ return array user
2022-11-12 17:01:22 +00:00
* @ throws Exception
2020-07-29 05:12:16 +00:00
*/
2022-06-16 19:57:02 +00:00
public static function getFirstAdmin ( array $fields = []) : array
2020-07-29 05:12:16 +00:00
{
if ( ! empty ( DI :: config () -> get ( 'config' , 'admin_nickname' ))) {
2020-07-29 14:59:55 +00:00
return self :: getByNickname ( DI :: config () -> get ( 'config' , 'admin_nickname' ), $fields );
2020-07-29 05:12:16 +00:00
}
2022-11-12 17:01:22 +00:00
return self :: getAdminList ()[ 0 ] ? ? [];
2020-07-29 05:12:16 +00:00
}
2017-12-17 21:22:39 +00:00
/**
2020-01-19 06:05:23 +00:00
* Get owner data by user id
2017-12-17 21:22:39 +00:00
*
2020-12-15 14:41:10 +00:00
* @ param int $uid
* @ param boolean $repairMissing Repair the owner data if it ' s missing
2017-12-17 21:22:39 +00:00
* @ return boolean | array
2019-01-06 21:06:53 +00:00
* @ throws Exception
2017-12-17 21:22:39 +00:00
*/
2020-12-15 14:41:10 +00:00
public static function getOwnerDataById ( int $uid , bool $repairMissing = true )
2019-10-10 23:21:41 +00:00
{
2020-08-23 07:29:56 +00:00
if ( $uid == 0 ) {
return self :: getSystemAccount ();
}
2020-08-16 12:51:15 +00:00
if ( ! empty ( self :: $owner [ $uid ])) {
return self :: $owner [ $uid ];
2020-08-16 11:57:56 +00:00
}
2020-04-24 11:04:50 +00:00
$owner = DBA :: selectFirst ( 'owner-view' , [], [ 'uid' => $uid ]);
if ( ! DBA :: isResult ( $owner )) {
2022-07-21 14:01:38 +00:00
if ( ! self :: exists ( $uid ) || ! $repairMissing ) {
2020-04-25 07:29:38 +00:00
return false ;
}
2021-06-15 21:39:28 +00:00
if ( ! DBA :: exists ( 'profile' , [ 'uid' => $uid ])) {
DBA :: insert ( 'profile' , [ 'uid' => $uid ]);
}
2021-06-15 22:01:30 +00:00
if ( ! DBA :: exists ( 'contact' , [ 'uid' => $uid , 'self' => true ])) {
Contact :: createSelfFromUserId ( $uid );
}
2020-04-25 07:29:38 +00:00
$owner = self :: getOwnerDataById ( $uid , false );
2017-12-17 21:22:39 +00:00
}
2018-12-22 20:12:32 +00:00
2020-04-24 11:04:50 +00:00
if ( empty ( $owner [ 'nickname' ])) {
2018-12-22 20:12:32 +00:00
return false ;
}
2021-03-20 09:56:35 +00:00
if ( ! $repairMissing || $owner [ 'account_expired' ]) {
2020-04-24 11:04:50 +00:00
return $owner ;
2019-06-19 17:05:29 +00:00
}
2018-12-22 20:12:32 +00:00
// Check if the returned data is valid, otherwise fix it. See issue #6122
2019-06-19 17:05:29 +00:00
// Check for correct url and normalised nurl
2020-04-24 11:04:50 +00:00
$url = DI :: baseUrl () . '/profile/' . $owner [ 'nickname' ];
2021-07-04 14:17:10 +00:00
$repair = empty ( $owner [ 'network' ]) || ( $owner [ 'url' ] != $url ) || ( $owner [ 'nurl' ] != Strings :: normaliseLink ( $owner [ 'url' ]));
2019-06-19 17:05:29 +00:00
if ( ! $repair ) {
// Check if "addr" is present and correct
2020-04-24 11:04:50 +00:00
$addr = $owner [ 'nickname' ] . '@' . substr ( DI :: baseUrl (), strpos ( DI :: baseUrl (), '://' ) + 3 );
2020-09-26 20:59:28 +00:00
$repair = ( $addr != $owner [ 'addr' ]) || empty ( $owner [ 'prvkey' ]) || empty ( $owner [ 'pubkey' ]);
2019-06-19 17:05:29 +00:00
}
if ( ! $repair ) {
// Check if the avatar field is filled and the photo directs to the correct path
$avatar = Photo :: selectFirst ([ 'resource-id' ], [ 'uid' => $uid , 'profile' => true ]);
if ( DBA :: isResult ( $avatar )) {
2020-04-24 11:04:50 +00:00
$repair = empty ( $owner [ 'avatar' ]) || ! strpos ( $owner [ 'photo' ], $avatar [ 'resource-id' ]);
2019-06-19 17:05:29 +00:00
}
}
2018-12-22 20:12:32 +00:00
2019-06-19 17:05:29 +00:00
if ( $repair ) {
2018-12-22 20:12:32 +00:00
Contact :: updateSelfFromUserID ( $uid );
2019-06-19 17:05:29 +00:00
// Return the corrected data and avoid a loop
2020-04-24 11:04:50 +00:00
$owner = self :: getOwnerDataById ( $uid , false );
2018-12-22 20:12:32 +00:00
}
2020-08-16 12:51:15 +00:00
self :: $owner [ $uid ] = $owner ;
2020-04-24 11:04:50 +00:00
return $owner ;
2017-12-17 21:22:39 +00:00
}
2018-06-18 20:36:34 +00:00
/**
2020-01-19 06:05:23 +00:00
* Get owner data by nick name
2018-06-18 20:36:34 +00:00
*
* @ param int $nick
* @ return boolean | array
2019-01-06 21:06:53 +00:00
* @ throws Exception
2018-06-18 20:36:34 +00:00
*/
2022-06-16 20:15:06 +00:00
public static function getOwnerDataByNick ( string $nick )
2018-06-18 20:36:34 +00:00
{
2018-07-20 12:19:26 +00:00
$user = DBA :: selectFirst ( 'user' , [ 'uid' ], [ 'nickname' => $nick ]);
Cleanups: isResult() more used, readability improved (#5608)
* [diaspora]: Maybe SimpleXMLElement is the right type-hint?
* Changes proposed + pre-renaming:
- pre-renamed $db -> $connection
- added TODOs for not allowing bad method invocations (there is a
BadMethodCallException in SPL)
* If no record is found, below $r[0] will fail with a E_NOTICE and the code
doesn't behave as expected.
* Ops, one more left ...
* Continued:
- added documentation for Contact::updateSslPolicy() method
- added type-hint for $contact of same method
- empty lines added + TODO where the bug origins that $item has no element 'body'
* Added empty lines for better readability
* Cleaned up:
- no more x() (deprecated) usage but empty() instead
- fixed mixing of space/tab indending
- merged else/if block goether in elseif() (lesser nested code blocks)
* Re-fixed DBM -> DBA switch
* Fixes/rewrites:
- use empty()/isset() instead of deprecated x()
- merged 2 nested if() blocks into one
- avoided nested if() block inside else block by rewriting it to elseif()
- $contact_id is an integer, let's test on > 0 here
- added a lot spaces and some empty lines for better readability
* Rewrite:
- moved all CONTACT_* constants from boot.php to Contact class
* CR request:
- renamed Contact::CONTACT_IS_* -> Contact::* ;-)
* Rewrites:
- moved PAGE_* to Friendica\Model\Profile class
- fixed mixure with "Contact::* rewrite"
* Ops, one still there (return is no function)
* Rewrite to Proxy class:
- introduced new Friendica\Network\Proxy class for in exchange of proxy_*()
functions
- moved also all PROXY_* constants there as Proxy::*
- removed now no longer needed mod/proxy.php loading as composer's auto-load
will do this for us
- renamed those proxy_*() functions to better names:
+ proxy_init() -> Proxy::init() (public)
+ proxy_url() -> Proxy::proxifyUrl() (public)
+ proxy_parse_html() -> Proxy::proxifyHtml() (public)
+ proxy_is_local_image() -> Proxy::isLocalImage() (private)
+ proxy_parse_query() -> Proxy::parseQuery() (private)
+ proxy_img_cb() -> Proxy::replaceUrl() (private)
* CR request:
- moved all PAGE_* constants to Friendica\Model\Contact class
- fixed all references of both classes
* Ops, need to set $a here ...
* CR request:
- moved Proxy class to Friendica\Module
- extended BaseModule
* Ops, no need for own instance of $a when self::getApp() is around.
* Proxy-rewrite:
- proxy_url() and proxy_parse_html() are both non-module functions (now
methods)
- so they must be splitted into a seperate class
- also the SIZE_* and DEFAULT_TIME constants are both not relevant to module
* No instances from utility classes
* Fixed error:
- proxify*() is now located in `Friendica\Util\ProxyUtils`
* Moved back to original place, ops? How did they move here? Well, it was not
intended by me.
* Removed duplicate (left-over from split) constants and static array. Thank to
MrPetovan finding it.
* Renamed ProxyUtils -> Proxy and aliased it back to ProxyUtils.
* Rewrite:
- stopped using deprecated NETWORK_* constants, now Protocol::* should be used
- still left them intact for slow/lazy developers ...
* Ops, was added accidentally ...
* Ops, why these wrong moves?
* Ops, one to much (thanks to MrPetovan)
* Ops, wrong moving ...
* moved back to original place ...
* spaces added
* empty lines add for better readability.
* convertered spaces -> tab for code indenting.
* CR request: Add space between if and brace.
* CR requests fixed + move reverted
- ops, src/Module/*.php has been moved to src/Network/ accidentally
- reverted some parts in src/Database/DBA.php as pointed out by Annando
- removed internal TODO items
- added some spaces for better readability
2018-08-24 05:05:49 +00:00
2018-07-21 12:46:04 +00:00
if ( ! DBA :: isResult ( $user )) {
2018-06-18 20:36:34 +00:00
return false ;
}
Cleanups: isResult() more used, readability improved (#5608)
* [diaspora]: Maybe SimpleXMLElement is the right type-hint?
* Changes proposed + pre-renaming:
- pre-renamed $db -> $connection
- added TODOs for not allowing bad method invocations (there is a
BadMethodCallException in SPL)
* If no record is found, below $r[0] will fail with a E_NOTICE and the code
doesn't behave as expected.
* Ops, one more left ...
* Continued:
- added documentation for Contact::updateSslPolicy() method
- added type-hint for $contact of same method
- empty lines added + TODO where the bug origins that $item has no element 'body'
* Added empty lines for better readability
* Cleaned up:
- no more x() (deprecated) usage but empty() instead
- fixed mixing of space/tab indending
- merged else/if block goether in elseif() (lesser nested code blocks)
* Re-fixed DBM -> DBA switch
* Fixes/rewrites:
- use empty()/isset() instead of deprecated x()
- merged 2 nested if() blocks into one
- avoided nested if() block inside else block by rewriting it to elseif()
- $contact_id is an integer, let's test on > 0 here
- added a lot spaces and some empty lines for better readability
* Rewrite:
- moved all CONTACT_* constants from boot.php to Contact class
* CR request:
- renamed Contact::CONTACT_IS_* -> Contact::* ;-)
* Rewrites:
- moved PAGE_* to Friendica\Model\Profile class
- fixed mixure with "Contact::* rewrite"
* Ops, one still there (return is no function)
* Rewrite to Proxy class:
- introduced new Friendica\Network\Proxy class for in exchange of proxy_*()
functions
- moved also all PROXY_* constants there as Proxy::*
- removed now no longer needed mod/proxy.php loading as composer's auto-load
will do this for us
- renamed those proxy_*() functions to better names:
+ proxy_init() -> Proxy::init() (public)
+ proxy_url() -> Proxy::proxifyUrl() (public)
+ proxy_parse_html() -> Proxy::proxifyHtml() (public)
+ proxy_is_local_image() -> Proxy::isLocalImage() (private)
+ proxy_parse_query() -> Proxy::parseQuery() (private)
+ proxy_img_cb() -> Proxy::replaceUrl() (private)
* CR request:
- moved all PAGE_* constants to Friendica\Model\Contact class
- fixed all references of both classes
* Ops, need to set $a here ...
* CR request:
- moved Proxy class to Friendica\Module
- extended BaseModule
* Ops, no need for own instance of $a when self::getApp() is around.
* Proxy-rewrite:
- proxy_url() and proxy_parse_html() are both non-module functions (now
methods)
- so they must be splitted into a seperate class
- also the SIZE_* and DEFAULT_TIME constants are both not relevant to module
* No instances from utility classes
* Fixed error:
- proxify*() is now located in `Friendica\Util\ProxyUtils`
* Moved back to original place, ops? How did they move here? Well, it was not
intended by me.
* Removed duplicate (left-over from split) constants and static array. Thank to
MrPetovan finding it.
* Renamed ProxyUtils -> Proxy and aliased it back to ProxyUtils.
* Rewrite:
- stopped using deprecated NETWORK_* constants, now Protocol::* should be used
- still left them intact for slow/lazy developers ...
* Ops, was added accidentally ...
* Ops, why these wrong moves?
* Ops, one to much (thanks to MrPetovan)
* Ops, wrong moving ...
* moved back to original place ...
* spaces added
* empty lines add for better readability.
* convertered spaces -> tab for code indenting.
* CR request: Add space between if and brace.
* CR requests fixed + move reverted
- ops, src/Module/*.php has been moved to src/Network/ accidentally
- reverted some parts in src/Database/DBA.php as pointed out by Annando
- removed internal TODO items
- added some spaces for better readability
2018-08-24 05:05:49 +00:00
2018-06-18 20:36:34 +00:00
return self :: getOwnerDataById ( $user [ 'uid' ]);
}
2017-12-04 03:15:31 +00:00
/**
2023-06-25 20:37:11 +00:00
* Returns the default circle for a given user
2017-12-09 18:31:00 +00:00
*
2021-11-06 04:07:07 +00:00
* @ param int $uid User id
2017-12-09 18:31:00 +00:00
*
2023-05-13 23:54:35 +00:00
* @ return int circle id
2020-07-21 06:27:05 +00:00
* @ throws Exception
2017-12-09 18:31:00 +00:00
*/
2023-05-13 23:54:35 +00:00
public static function getDefaultCircle ( int $uid ) : int
2017-12-09 18:31:00 +00:00
{
2018-07-20 12:19:26 +00:00
$user = DBA :: selectFirst ( 'user' , [ 'def_gid' ], [ 'uid' => $uid ]);
2018-07-21 12:46:04 +00:00
if ( DBA :: isResult ( $user )) {
2023-05-13 23:54:35 +00:00
$default_circle = $user [ 'def_gid' ];
2021-07-13 06:06:08 +00:00
} else {
2023-05-13 23:54:35 +00:00
$default_circle = 0 ;
2017-12-09 18:31:00 +00:00
}
2023-05-13 23:54:35 +00:00
return $default_circle ;
2017-12-09 18:31:00 +00:00
}
2023-06-25 20:37:11 +00:00
/**
* 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 ;
}
2023-09-03 08:44:17 +00:00
/**
* Fetch the language code from the given user . If the code is invalid , return the system language
*
2023-09-03 17:44:44 +00:00
* @ param integer $uid User - Id
2023-09-03 08:44:17 +00:00
* @ return string
*/
2023-09-03 17:44:44 +00:00
public static function getLanguageCode ( int $uid ) : string
2023-09-03 08:44:17 +00:00
{
$owner = self :: getOwnerDataById ( $uid );
2023-09-03 17:44:44 +00:00
$languages = DI :: l10n () -> getAvailableLanguages ( true );
2023-09-03 08:44:17 +00:00
if ( in_array ( $owner [ 'language' ], array_keys ( $languages ))) {
$language = $owner [ 'language' ];
} else {
$language = DI :: config () -> get ( 'system' , 'language' );
}
return $language ;
}
2018-02-09 05:08:01 +00:00
/**
2020-01-19 06:05:23 +00:00
* Authenticate a user with a clear text password
*
2018-02-09 05:08:01 +00:00
* Returns the user id associated with a successful password authentication
*
2019-07-22 11:56:36 +00:00
* @ param mixed $user_info
2018-02-09 05:08:01 +00:00
* @ param string $password
2019-07-22 11:56:36 +00:00
* @ param bool $third_party
2018-02-09 05:08:01 +00:00
* @ return int User Id if authentication is successful
2019-07-27 22:14:39 +00:00
* @ throws HTTPException\ForbiddenException
* @ throws HTTPException\NotFoundException
2018-02-09 05:08:01 +00:00
*/
2022-06-23 08:56:37 +00:00
public static function getIdFromPasswordAuthentication ( $user_info , string $password , bool $third_party = false ) : int
2018-02-09 05:08:01 +00:00
{
2021-05-20 07:16:08 +00:00
// 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 ) {
2021-05-20 18:05:48 +00:00
$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
2023-03-22 03:16:38 +00:00
// ambiguity with user IDs, possibly opening up an attack vector.
2021-05-20 07:19:09 +00:00
// So let's be very careful about that.
2021-05-20 18:54:30 +00:00
if ( empty ( $username ) || is_numeric ( $username )) {
2021-05-20 07:19:09 +00:00
throw $e ;
}
2021-05-20 18:05:48 +00:00
return self :: getIdFromAuthenticateHooks ( $username , $password );
2021-05-20 07:16:08 +00:00
}
2017-11-26 19:25:25 +00:00
2020-01-18 15:50:57 +00:00
if ( $third_party && DI :: pConfig () -> get ( $user [ 'uid' ], '2fa' , 'verified' )) {
2019-07-22 11:56:36 +00:00
// Third-party apps can't verify two-factor authentication, we use app-specific passwords instead
if ( AppSpecificPassword :: authenticateUser ( $user [ 'uid' ], $password )) {
return $user [ 'uid' ];
}
} elseif ( strpos ( $user [ 'password' ], '$' ) === false ) {
2018-04-15 09:12:32 +00:00
//Legacy hash that has not been replaced by a new hash yet
2018-04-08 10:28:04 +00:00
if ( self :: hashPasswordLegacy ( $password ) === $user [ 'password' ]) {
2019-01-01 06:06:28 +00:00
self :: updatePasswordHashed ( $user [ 'uid' ], self :: hashPassword ( $password ));
2018-04-08 10:28:04 +00:00
2018-04-15 09:12:32 +00:00
return $user [ 'uid' ];
}
} elseif ( ! empty ( $user [ 'legacy_password' ])) {
//Legacy hash that has been double-hashed and not replaced by a new hash yet
//Warning: `legacy_password` is not necessary in sync with the content of `password`
if ( password_verify ( self :: hashPasswordLegacy ( $password ), $user [ 'password' ])) {
2019-01-01 06:06:28 +00:00
self :: updatePasswordHashed ( $user [ 'uid' ], self :: hashPassword ( $password ));
2018-04-15 09:12:32 +00:00
2018-04-08 10:28:04 +00:00
return $user [ 'uid' ];
}
2018-04-08 14:02:25 +00:00
} elseif ( password_verify ( $password , $user [ 'password' ])) {
2018-04-15 09:12:32 +00:00
//New password hash
2018-04-08 14:02:25 +00:00
if ( password_needs_rehash ( $user [ 'password' ], PASSWORD_DEFAULT )) {
2019-01-01 06:06:28 +00:00
self :: updatePasswordHashed ( $user [ 'uid' ], self :: hashPassword ( $password ));
2018-04-08 14:02:25 +00:00
}
return $user [ 'uid' ];
2021-05-19 19:57:58 +00:00
} else {
2021-05-20 07:16:08 +00:00
return self :: getIdFromAuthenticateHooks ( $user [ 'nickname' ], $password ); // throws
}
2021-05-19 19:57:58 +00:00
2021-05-20 07:16:08 +00:00
throw new HTTPException\ForbiddenException ( DI :: l10n () -> t ( 'Login failed' ));
}
2021-05-19 19:57:58 +00:00
2021-05-20 07:16:08 +00:00
/**
* 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
*/
2022-06-16 19:57:02 +00:00
public static function getIdFromAuthenticateHooks ( string $username , string $password ) : int
2021-05-20 18:05:48 +00:00
{
2021-05-20 07:16:08 +00:00
$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' ]) {
2021-05-20 07:19:09 +00:00
return $addon_auth [ 'user_record' ][ 'uid' ];
2017-11-26 19:25:25 +00:00
}
2019-07-27 22:14:39 +00:00
throw new HTTPException\ForbiddenException ( DI :: l10n () -> t ( 'Login failed' ));
2018-02-09 05:08:01 +00:00
}
/**
* Returns authentication info from various parameters types
*
* User info can be any of the following :
* - User DB object
* - User Id
* - User email or username or nickname
* - User array with at least the uid and the hashed password
*
* @ param mixed $user_info
2022-06-16 19:57:02 +00:00
* @ return array | null Null if not found / determined
2019-07-27 22:14:39 +00:00
* @ throws HTTPException\NotFoundException
2018-02-09 05:08:01 +00:00
*/
2020-11-19 16:20:17 +00:00
public static function getAuthenticationInfo ( $user_info )
2018-02-09 05:08:01 +00:00
{
2018-02-14 05:05:00 +00:00
$user = null ;
2018-02-09 05:08:01 +00:00
if ( is_object ( $user_info ) || is_array ( $user_info )) {
if ( is_object ( $user_info )) {
$user = ( array ) $user_info ;
} else {
$user = $user_info ;
}
2019-10-10 23:21:41 +00:00
if (
! isset ( $user [ 'uid' ])
2018-02-09 05:08:01 +00:00
|| ! isset ( $user [ 'password' ])
2018-04-15 08:51:22 +00:00
|| ! isset ( $user [ 'legacy_password' ])
2018-02-09 05:08:01 +00:00
) {
2020-01-18 19:52:34 +00:00
throw new Exception ( DI :: l10n () -> t ( 'Not enough information to authenticate' ));
2018-02-09 05:08:01 +00:00
}
} elseif ( is_int ( $user_info ) || is_string ( $user_info )) {
if ( is_int ( $user_info )) {
2019-10-10 23:21:41 +00:00
$user = DBA :: selectFirst (
'user' ,
2021-05-19 19:57:58 +00:00
[ 'uid' , 'nickname' , 'password' , 'legacy_password' ],
2018-02-09 05:08:01 +00:00
[
'uid' => $user_info ,
'blocked' => 0 ,
'account_expired' => 0 ,
'account_removed' => 0 ,
'verified' => 1
]
);
} else {
2021-05-19 19:57:58 +00:00
$fields = [ 'uid' , 'nickname' , 'password' , 'legacy_password' ];
2019-10-10 23:21:41 +00:00
$condition = [
" (`email` = ? OR `username` = ? OR `nickname` = ?)
2023-09-08 15:01:51 +00:00
AND `verified` AND NOT `blocked` AND NOT `account_removed` AND NOT `account_expired` " ,
2019-10-10 23:21:41 +00:00
$user_info , $user_info , $user_info
];
2018-07-20 12:19:26 +00:00
$user = DBA :: selectFirst ( 'user' , $fields , $condition );
2018-02-09 05:08:01 +00:00
}
2018-07-21 12:46:04 +00:00
if ( ! DBA :: isResult ( $user )) {
2019-07-27 22:14:39 +00:00
throw new HTTPException\NotFoundException ( DI :: l10n () -> t ( 'User not found' ));
2018-02-09 05:08:01 +00:00
}
}
return $user ;
2017-11-26 19:25:25 +00:00
}
2022-11-30 22:34:50 +00:00
/**
* Update the day of the last activity of the given user
*
* @ param integer $uid
* @ return void
*/
public static function updateLastActivity ( int $uid )
{
2023-05-27 22:19:02 +00:00
if ( ! $uid ) {
return ;
}
2022-11-30 22:34:50 +00:00
$user = User :: getById ( $uid , [ 'last-activity' ]);
2022-12-01 06:12:13 +00:00
if ( empty ( $user )) {
return ;
}
2022-11-30 22:36:58 +00:00
$current_day = DateTimeFormat :: utcNow ( 'Y-m-d' );
2022-11-30 22:34:50 +00:00
if ( $user [ 'last-activity' ] != $current_day ) {
User :: update ([ 'last-activity' => $current_day ], $uid );
2023-03-22 03:16:39 +00:00
// Set the last activity for all identities of the user
2023-09-08 15:01:51 +00:00
DBA :: update ( 'user' , [ 'last-activity' => $current_day ], [ 'parent-uid' => $uid , 'verified' => true , 'blocked' => false , 'account_removed' => false , 'account_expired' => false ]);
2022-11-30 22:34:50 +00:00
}
}
2018-01-20 03:49:06 +00:00
/**
* Generates a human - readable random password
*
* @ return string
2020-07-21 06:27:05 +00:00
* @ throws Exception
2018-01-20 03:49:06 +00:00
*/
2022-06-16 19:57:02 +00:00
public static function generateNewPassword () : string
2018-01-20 03:49:06 +00:00
{
2019-10-10 23:21:41 +00:00
return ucfirst ( Strings :: getRandomName ( 8 )) . random_int ( 1000 , 9999 );
2018-01-20 03:49:06 +00:00
}
2018-03-21 05:33:35 +00:00
/**
* Checks if the provided plaintext password has been exposed or not
*
* @ param string $password
* @ return bool
2019-10-13 01:06:47 +00:00
* @ throws Exception
2018-03-21 05:33:35 +00:00
*/
2022-06-16 19:57:02 +00:00
public static function isPasswordExposed ( string $password ) : bool
2018-03-21 05:33:35 +00:00
{
2020-07-21 06:27:05 +00:00
$cache = new CacheItemPool ();
2018-10-19 15:26:48 +00:00
$cache -> changeConfig ([
2021-11-04 20:29:59 +00:00
'cacheDirectory' => System :: getTempPath () . '/password-exposed-cache/' ,
2018-10-19 15:26:48 +00:00
]);
2019-10-13 01:06:47 +00:00
try {
$passwordExposedChecker = new PasswordExposed\PasswordExposedChecker ( null , $cache );
return $passwordExposedChecker -> passwordExposed ( $password ) === PasswordExposed\PasswordStatus :: EXPOSED ;
2020-07-21 06:27:05 +00:00
} catch ( Exception $e ) {
2019-10-13 01:06:47 +00:00
Logger :: error ( 'Password Exposed Exception: ' . $e -> getMessage (), [
'code' => $e -> getCode (),
'file' => $e -> getFile (),
'line' => $e -> getLine (),
'trace' => $e -> getTraceAsString ()
]);
2018-10-19 15:26:48 +00:00
2019-10-13 01:06:47 +00:00
return false ;
}
2018-03-21 05:33:35 +00:00
}
2018-01-20 03:49:06 +00:00
/**
2018-01-21 03:29:03 +00:00
* Legacy hashing function , kept for password migration purposes
2018-01-20 03:49:06 +00:00
*
* @ param string $password
* @ return string
*/
2022-06-16 19:57:02 +00:00
private static function hashPasswordLegacy ( string $password ) : string
2018-01-20 03:49:06 +00:00
{
return hash ( 'whirlpool' , $password );
}
2018-01-21 03:29:03 +00:00
/**
* Global user password hashing function
*
* @ param string $password
* @ return string
2019-01-01 06:08:55 +00:00
* @ throws Exception
2018-01-21 03:29:03 +00:00
*/
2022-06-16 19:57:02 +00:00
public static function hashPassword ( string $password ) : string
2018-01-21 03:29:03 +00:00
{
2018-04-19 02:49:14 +00:00
if ( ! trim ( $password )) {
2020-01-18 19:52:34 +00:00
throw new Exception ( DI :: l10n () -> t ( 'Password can\'t be empty' ));
2018-04-19 02:49:14 +00:00
}
2018-01-21 03:29:03 +00:00
return password_hash ( $password , PASSWORD_DEFAULT );
}
2022-08-01 15:42:10 +00:00
/**
2023-01-19 01:33:39 +00:00
* Allowed characters are a - z , A - Z , 0 - 9 and special characters except white spaces and accentuated letters .
2022-08-01 15:42:10 +00:00
*
* 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
{
2023-01-19 01:33:39 +00:00
$allowed_characters = ':!"#$%&\'()*+,-./;<=>?@[\]^_`{|}~' ;
2022-08-01 15:42:10 +00:00
if ( $delimiter ) {
$allowed_characters = preg_quote ( $allowed_characters , $delimiter );
}
2023-01-19 01:27:29 +00:00
return '^[a-zA-Z0-9' . $allowed_characters . ']' . ( PASSWORD_DEFAULT === PASSWORD_BCRYPT ? '{1,72}' : '+' ) . '$' ;
2022-08-01 15:42:10 +00:00
}
2018-01-20 03:49:06 +00:00
/**
* Updates a user row with a new plaintext password
*
* @ param int $uid
* @ param string $password
* @ return bool
2019-01-01 06:08:55 +00:00
* @ throws Exception
2018-01-20 03:49:06 +00:00
*/
2022-06-16 19:57:02 +00:00
public static function updatePassword ( int $uid , string $password ) : bool
2018-01-20 03:49:06 +00:00
{
2019-01-01 06:08:55 +00:00
$password = trim ( $password );
if ( empty ( $password )) {
2020-01-18 19:52:34 +00:00
throw new Exception ( DI :: l10n () -> t ( 'Empty passwords are not allowed.' ));
2019-01-01 06:08:55 +00:00
}
2020-01-19 20:21:13 +00:00
if ( ! DI :: config () -> get ( 'system' , 'disable_password_exposed' , false ) && self :: isPasswordExposed ( $password )) {
2020-01-18 19:52:34 +00:00
throw new Exception ( DI :: l10n () -> t ( 'The new password has been exposed in a public data dump, please choose another.' ));
2019-01-01 06:08:55 +00:00
}
2022-08-01 15:42:10 +00:00
if ( PASSWORD_DEFAULT === PASSWORD_BCRYPT && strlen ( $password ) > 72 ) {
throw new Exception ( DI :: l10n () -> t ( 'The password length is limited to 72 characters.' ));
}
2019-01-01 06:08:55 +00:00
2022-08-01 15:42:10 +00:00
if ( ! preg_match ( '/' . self :: getPasswordRegExp ( '/' ) . '/' , $password )) {
2023-01-19 01:33:39 +00:00
throw new Exception ( DI :: l10n () -> t ( " The password can't contain white spaces nor accentuated letters " ));
2019-01-01 06:08:55 +00:00
}
2018-01-20 03:49:06 +00:00
return self :: updatePasswordHashed ( $uid , self :: hashPassword ( $password ));
}
/**
* Updates a user row with a new hashed password .
* Empties the password reset token field just in case .
*
* @ param int $uid
2023-03-26 22:36:31 +00:00
* @ param string $password_hashed
2018-01-20 03:49:06 +00:00
* @ return bool
2019-01-06 21:06:53 +00:00
* @ throws Exception
2018-01-20 03:49:06 +00:00
*/
2023-03-26 22:36:31 +00:00
private static function updatePasswordHashed ( int $uid , string $password_hashed ) : bool
2018-01-20 03:49:06 +00:00
{
2018-01-20 23:15:55 +00:00
$fields = [
2023-03-26 22:36:31 +00:00
'password' => $password_hashed ,
2018-01-20 23:15:55 +00:00
'pwdreset' => null ,
2018-01-21 03:29:03 +00:00
'pwdreset_time' => null ,
2018-04-15 08:51:22 +00:00
'legacy_password' => false
2018-01-20 23:15:55 +00:00
];
2018-07-20 12:19:26 +00:00
return DBA :: update ( 'user' , $fields , [ 'uid' => $uid ]);
2018-01-20 03:49:06 +00:00
}
2022-12-26 12:08:41 +00:00
/**
* 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 ()
]);
}
2023-08-10 21:06:08 +00:00
/**
* Returns if the given uid is valid and a moderator
*
* @ param int $uid
*
* @ return bool
* @ throws Exception
*/
public static function isModerator ( int $uid ) : bool
{
// @todo Replace with a moderator check in the future
return self :: isSiteAdmin ( $uid );
}
2018-07-06 13:32:56 +00:00
/**
2020-01-19 06:05:23 +00:00
* Checks if a nickname is in the list of the forbidden nicknames
2018-07-06 13:32:56 +00:00
*
* Check if a nickname is forbidden from registration on the node by the
2023-03-22 04:07:44 +00:00
* admin . Forbidden nicknames ( e . g . role names ) can be configured in the
2018-07-06 13:32:56 +00:00
* admin panel .
*
* @ param string $nickname The nickname that should be checked
* @ return boolean True is the nickname is blocked on the node
*/
2022-06-16 19:57:02 +00:00
public static function isNicknameBlocked ( string $nickname ) : bool
2018-07-06 13:32:56 +00:00
{
2020-01-19 20:21:13 +00:00
$forbidden_nicknames = DI :: config () -> get ( 'system' , 'forbidden_nicknames' , '' );
2020-08-22 14:48:09 +00:00
if ( ! empty ( $forbidden_nicknames )) {
$forbidden = explode ( ',' , $forbidden_nicknames );
$forbidden = array_map ( 'trim' , $forbidden );
} else {
$forbidden = [];
}
// Add the name of the internal actor to the "forbidden" list
2020-08-22 18:52:37 +00:00
$actor_name = self :: getActorName ();
2020-08-22 14:48:09 +00:00
if ( ! empty ( $actor_name )) {
$forbidden [] = $actor_name ;
}
2018-07-23 01:18:21 +00:00
2020-08-22 14:48:09 +00:00
if ( empty ( $forbidden )) {
2018-07-06 13:32:56 +00:00
return false ;
}
2018-07-23 01:18:21 +00:00
2020-08-22 19:40:31 +00:00
// check if the nickname is in the list of blocked nicknames
2018-07-06 13:32:56 +00:00
if ( in_array ( strtolower ( $nickname ), $forbidden )) {
return true ;
}
2018-07-23 01:18:21 +00:00
2018-07-06 13:32:56 +00:00
// else return false
return false ;
}
2021-09-17 18:36:20 +00:00
/**
2021-10-02 21:28:29 +00:00
* Get avatar link for given user
2021-09-17 18:36:20 +00:00
*
2021-10-02 21:28:29 +00:00
* @ param array $user
2021-10-05 20:18:19 +00:00
* @ param string $size One of the Proxy :: SIZE_ * constants
2021-09-17 18:36:20 +00:00
* @ return string avatar link
2021-10-02 21:28:29 +00:00
* @ throws Exception
2021-09-17 18:36:20 +00:00
*/
2022-06-16 19:57:02 +00:00
public static function getAvatarUrl ( array $user , string $size = '' ) : string
2021-09-17 18:36:20 +00:00
{
2021-10-03 23:18:00 +00:00
if ( empty ( $user [ 'nickname' ])) {
2023-10-18 19:55:15 +00:00
DI :: logger () -> warning ( 'Missing user nickname key' );
2021-10-03 23:18:00 +00:00
}
2021-09-17 18:36:20 +00:00
$url = DI :: baseUrl () . '/photo/' ;
switch ( $size ) {
case Proxy :: SIZE_MICRO :
$url .= 'micro/' ;
$scale = 6 ;
break ;
case Proxy :: SIZE_THUMB :
$url .= 'avatar/' ;
$scale = 5 ;
break ;
default :
$url .= 'profile/' ;
$scale = 4 ;
break ;
}
2022-01-09 16:06:00 +00:00
$updated = '' ;
$mimetype = '' ;
2021-09-17 18:36:20 +00:00
2021-10-02 21:28:29 +00:00
$photo = Photo :: selectFirst ([ 'type' , 'created' , 'edited' , 'updated' ], [ " scale " => $scale , 'uid' => $user [ 'uid' ], 'profile' => true ]);
2021-09-17 18:36:20 +00:00
if ( ! empty ( $photo )) {
2022-01-09 16:06:00 +00:00
$updated = max ( $photo [ 'created' ], $photo [ 'edited' ], $photo [ 'updated' ]);
$mimetype = $photo [ 'type' ];
2021-09-17 18:36:20 +00:00
}
2022-01-09 16:06:00 +00:00
return $url . $user [ 'nickname' ] . Images :: getExtensionByMimeType ( $mimetype ) . ( $updated ? '?ts=' . strtotime ( $updated ) : '' );
2021-09-17 18:36:20 +00:00
}
2022-01-08 22:43:11 +00:00
/**
* Get banner link for given user
*
* @ param array $user
* @ return string banner link
* @ throws Exception
*/
2022-06-16 19:57:02 +00:00
public static function getBannerUrl ( array $user ) : string
2022-01-08 22:43:11 +00:00
{
if ( empty ( $user [ 'nickname' ])) {
2023-10-18 19:55:15 +00:00
DI :: logger () -> warning ( 'Missing user nickname key' );
2022-01-08 22:43:11 +00:00
}
$url = DI :: baseUrl () . '/photo/banner/' ;
2022-01-09 16:06:00 +00:00
$updated = '' ;
$mimetype = '' ;
2022-01-08 22:43:11 +00:00
$photo = Photo :: selectFirst ([ 'type' , 'created' , 'edited' , 'updated' ], [ " scale " => 3 , 'uid' => $user [ 'uid' ], 'photo-type' => Photo :: USER_BANNER ]);
if ( ! empty ( $photo )) {
2022-01-09 16:06:00 +00:00
$updated = max ( $photo [ 'created' ], $photo [ 'edited' ], $photo [ 'updated' ]);
$mimetype = $photo [ 'type' ];
2022-01-09 15:45:14 +00:00
} else {
// Only for the RC phase: Don't return an image link for the default picture
return '' ;
2022-01-08 22:43:11 +00:00
}
2022-01-09 16:06:00 +00:00
return $url . $user [ 'nickname' ] . Images :: getExtensionByMimeType ( $mimetype ) . ( $updated ? '?ts=' . strtotime ( $updated ) : '' );
2022-01-08 22:43:11 +00:00
}
2017-12-04 03:27:49 +00:00
/**
2020-01-19 06:05:23 +00:00
* Catch - all user creation function
2017-12-04 03:27:49 +00:00
*
* Creates a user from the provided data array , either form fields or OpenID .
* Required : { username , nickname , email } or { openid_url }
*
* Performs the following :
* - Sends to the OpenId auth URL ( if relevant )
* - Creates new key pairs for crypto
* - Create self - contact
* - Create profile image
*
2019-01-01 06:08:55 +00:00
* @ param array $data
* @ return array
2020-07-21 06:27:05 +00:00
* @ throws ErrorException
* @ throws HTTPException\InternalServerErrorException
* @ throws ImagickException
2019-01-01 06:08:55 +00:00
* @ throws Exception
2017-12-04 03:27:49 +00:00
*/
2022-06-16 19:57:02 +00:00
public static function create ( array $data ) : array
2017-12-04 03:27:49 +00:00
{
2017-12-13 01:43:21 +00:00
$return = [ 'user' => null , 'password' => '' ];
2017-12-04 03:27:49 +00:00
2020-01-19 20:21:13 +00:00
$using_invites = DI :: config () -> get ( 'system' , 'invitation_only' );
2017-12-04 03:27:49 +00:00
2021-11-05 19:59:18 +00:00
$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' ]) : '' ;
2018-11-30 14:06:22 +00:00
$blocked = ! empty ( $data [ 'blocked' ]);
$verified = ! empty ( $data [ 'verified' ]);
2021-11-05 19:59:18 +00:00
$language = ! empty ( $data [ 'language' ]) ? trim ( $data [ 'language' ]) : 'en' ;
2018-07-23 01:18:21 +00:00
2020-02-16 15:39:44 +00:00
$netpublish = $publish = ! empty ( $data [ 'profile_publish_reg' ]);
2017-12-04 03:27:49 +00:00
if ( $password1 != $confirm ) {
2020-01-18 19:52:34 +00:00
throw new Exception ( DI :: l10n () -> t ( 'Passwords do not match. Password unchanged.' ));
2017-12-13 02:07:03 +00:00
} elseif ( $password1 != '' ) {
2017-12-04 03:27:49 +00:00
$password = $password1 ;
}
if ( $using_invites ) {
if ( ! $invite_id ) {
2020-01-18 19:52:34 +00:00
throw new Exception ( DI :: l10n () -> t ( 'An invitation is required.' ));
2017-12-04 03:27:49 +00:00
}
2017-12-13 02:07:03 +00:00
2018-10-14 15:57:28 +00:00
if ( ! Register :: existsByHash ( $invite_id )) {
2020-01-18 19:52:34 +00:00
throw new Exception ( DI :: l10n () -> t ( 'Invitation could not be verified.' ));
2017-12-04 03:27:49 +00:00
}
}
2019-10-24 20:23:26 +00:00
/// @todo Check if this part is really needed. We should have fetched all this data in advance
2018-07-23 01:18:21 +00:00
if ( empty ( $username ) || empty ( $email ) || empty ( $nickname )) {
2017-12-04 03:27:49 +00:00
if ( $openid_url ) {
2018-01-27 16:13:41 +00:00
if ( ! Network :: isUrlValid ( $openid_url )) {
2020-01-18 19:52:34 +00:00
throw new Exception ( DI :: l10n () -> t ( 'Invalid OpenID url' ));
2017-12-04 03:27:49 +00:00
}
$_SESSION [ 'register' ] = 1 ;
$_SESSION [ 'openid' ] = $openid_url ;
2023-02-18 19:57:30 +00:00
$openid = new LightOpenID ( DI :: baseUrl () -> getHost ());
2017-12-04 03:27:49 +00:00
$openid -> identity = $openid_url ;
2019-12-30 22:00:08 +00:00
$openid -> returnUrl = DI :: baseUrl () . '/openid' ;
2018-01-15 13:05:12 +00:00
$openid -> required = [ 'namePerson/friendly' , 'contact/email' , 'namePerson' ];
$openid -> optional = [ 'namePerson/first' , 'media/image/aspect11' , 'media/image/default' ];
2017-12-04 03:27:49 +00:00
try {
$authurl = $openid -> authUrl ();
} catch ( Exception $e ) {
2022-10-18 12:29:50 +00:00
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 );
2017-12-04 03:27:49 +00:00
}
2018-10-19 18:11:27 +00:00
System :: externalRedirect ( $authurl );
2017-12-04 03:27:49 +00:00
// NOTREACHED
}
2020-01-18 19:52:34 +00:00
throw new Exception ( DI :: l10n () -> t ( 'Please enter the required information.' ));
2017-12-04 03:27:49 +00:00
}
2018-01-27 16:13:41 +00:00
if ( ! Network :: isUrlValid ( $openid_url )) {
2017-12-04 03:27:49 +00:00
$openid_url = '' ;
}
// collapse multiple spaces in name
$username = preg_replace ( '/ +/' , ' ' , $username );
2020-01-19 20:21:13 +00:00
$username_min_length = max ( 1 , min ( 64 , intval ( DI :: config () -> get ( 'system' , 'username_min_length' , 3 ))));
$username_max_length = max ( 1 , min ( 64 , intval ( DI :: config () -> get ( 'system' , 'username_max_length' , 48 ))));
2018-10-20 20:33:54 +00:00
2018-10-21 12:28:24 +00:00
if ( $username_min_length > $username_max_length ) {
2021-11-03 23:19:24 +00:00
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 ));
2018-10-21 12:28:24 +00:00
$tmp = $username_min_length ;
$username_min_length = $username_max_length ;
$username_max_length = $tmp ;
}
2018-10-20 20:33:54 +00:00
if ( mb_strlen ( $username ) < $username_min_length ) {
2020-01-18 19:53:01 +00:00
throw new Exception ( DI :: l10n () -> tt ( 'Username should be at least %s character.' , 'Username should be at least %s characters.' , $username_min_length ));
2017-12-04 03:27:49 +00:00
}
2018-10-20 20:33:54 +00:00
if ( mb_strlen ( $username ) > $username_max_length ) {
2020-01-18 19:53:01 +00:00
throw new Exception ( DI :: l10n () -> tt ( 'Username should be at most %s character.' , 'Username should be at most %s characters.' , $username_max_length ));
2017-12-04 03:27:49 +00:00
}
// So now we are just looking for a space in the full name.
2020-01-19 20:21:13 +00:00
$loose_reg = DI :: config () -> get ( 'system' , 'no_regfullname' );
2017-12-04 03:27:49 +00:00
if ( ! $loose_reg ) {
$username = mb_convert_case ( $username , MB_CASE_TITLE , 'UTF-8' );
2018-10-20 20:33:54 +00:00
if ( strpos ( $username , ' ' ) === false ) {
2020-01-18 19:52:34 +00:00
throw new Exception ( DI :: l10n () -> t ( " That doesn't appear to be your full (First Last) name. " ));
2017-12-04 03:27:49 +00:00
}
}
2018-01-27 16:13:41 +00:00
if ( ! Network :: isEmailDomainAllowed ( $email )) {
2020-01-18 19:52:34 +00:00
throw new Exception ( DI :: l10n () -> t ( 'Your email domain is not among those allowed on this site.' ));
2017-12-04 03:27:49 +00:00
}
2018-11-09 18:18:42 +00:00
if ( ! filter_var ( $email , FILTER_VALIDATE_EMAIL ) || ! Network :: isEmailDomainValid ( $email )) {
2020-01-18 19:52:34 +00:00
throw new Exception ( DI :: l10n () -> t ( 'Not a valid email address.' ));
2017-12-04 03:27:49 +00:00
}
2018-07-06 13:32:56 +00:00
if ( self :: isNicknameBlocked ( $nickname )) {
2020-01-18 19:52:34 +00:00
throw new Exception ( DI :: l10n () -> t ( 'The nickname was blocked from registration by the nodes admin.' ));
2018-07-06 13:32:56 +00:00
}
2017-12-04 03:27:49 +00:00
2020-01-19 20:21:13 +00:00
if ( DI :: config () -> get ( 'system' , 'block_extended_register' , false ) && DBA :: exists ( 'user' , [ 'email' => $email ])) {
2020-01-18 19:52:34 +00:00
throw new Exception ( DI :: l10n () -> t ( 'Cannot use that email.' ));
2017-12-13 02:07:03 +00:00
}
2017-12-04 03:27:49 +00:00
// 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.
2022-11-12 17:01:22 +00:00
if ( strlen ( $openid_url ) && in_array ( strtolower ( $email ), self :: getAdminEmailList ())) {
throw new Exception ( DI :: l10n () -> t ( 'Cannot use that email.' ));
2017-12-04 03:27:49 +00:00
}
$nickname = $data [ 'nickname' ] = strtolower ( $nickname );
2020-07-21 06:27:05 +00:00
if ( ! preg_match ( '/^[a-z0-9][a-z0-9_]*$/' , $nickname )) {
2020-01-18 19:52:34 +00:00
throw new Exception ( DI :: l10n () -> t ( 'Your nickname can only contain a-z, 0-9 and _.' ));
2017-12-04 03:27:49 +00:00
}
2017-12-13 02:07:03 +00:00
// Check existing and deleted accounts for this nickname.
2019-10-10 23:21:41 +00:00
if (
DBA :: exists ( 'user' , [ 'nickname' => $nickname ])
2018-07-20 12:19:26 +00:00
|| DBA :: exists ( 'userd' , [ 'username' => $nickname ])
2017-12-13 02:07:03 +00:00
) {
2020-01-18 19:52:34 +00:00
throw new Exception ( DI :: l10n () -> t ( 'Nickname is already registered. Please choose another.' ));
2017-12-04 03:27:49 +00:00
}
2018-01-20 03:49:06 +00:00
$new_password = strlen ( $password ) ? $password : User :: generateNewPassword ();
$new_password_encoded = self :: hashPassword ( $new_password );
2017-12-04 03:27:49 +00:00
2017-12-13 01:43:21 +00:00
$return [ 'password' ] = $new_password ;
2017-12-04 03:27:49 +00:00
2017-12-30 16:51:49 +00:00
$keys = Crypto :: newKeypair ( 4096 );
2017-12-04 03:27:49 +00:00
if ( $keys === false ) {
2020-01-18 19:52:34 +00:00
throw new Exception ( DI :: l10n () -> t ( 'SERIOUS ERROR: Generation of security keys failed.' ));
2017-12-04 03:27:49 +00:00
}
$prvkey = $keys [ 'prvkey' ];
$pubkey = $keys [ 'pubkey' ];
// Create another keypair for signing/verifying salmon protocol messages.
2017-12-30 16:51:49 +00:00
$sres = Crypto :: newKeypair ( 512 );
2017-12-04 03:27:49 +00:00
$sprvkey = $sres [ 'prvkey' ];
$spubkey = $sres [ 'pubkey' ];
2018-07-20 12:19:26 +00:00
$insert_result = DBA :: insert ( 'user' , [
2018-09-27 11:52:15 +00:00
'guid' => System :: createUUID (),
2017-12-13 02:07:03 +00:00
'username' => $username ,
'password' => $new_password_encoded ,
'email' => $email ,
'openid' => $openid_url ,
'nickname' => $nickname ,
'pubkey' => $pubkey ,
'prvkey' => $prvkey ,
'spubkey' => $spubkey ,
'sprvkey' => $sprvkey ,
'verified' => $verified ,
'blocked' => $blocked ,
2018-05-31 06:27:27 +00:00
'language' => $language ,
2017-12-13 02:07:03 +00:00
'timezone' => 'UTC' ,
2018-01-27 02:38:34 +00:00
'register_date' => DateTimeFormat :: utcNow (),
2017-12-13 02:07:03 +00:00
'default-location' => ''
]);
if ( $insert_result ) {
2018-07-20 12:19:26 +00:00
$uid = DBA :: lastInsertId ();
$user = DBA :: selectFirst ( 'user' , [], [ 'uid' => $uid ]);
2017-12-04 03:27:49 +00:00
} else {
2020-01-18 19:52:34 +00:00
throw new Exception ( DI :: l10n () -> t ( 'An error occurred during registration. Please try again.' ));
2017-12-04 03:27:49 +00:00
}
2017-12-13 02:07:03 +00:00
if ( ! $uid ) {
2020-01-18 19:52:34 +00:00
throw new Exception ( DI :: l10n () -> t ( 'An error occurred during registration. Please try again.' ));
2017-12-04 03:27:49 +00:00
}
2017-12-13 02:07:03 +00:00
// if somebody clicked submit twice very quickly, they could end up with two accounts
// due to race condition. Remove this one.
2018-07-20 12:19:26 +00:00
$user_count = DBA :: count ( 'user' , [ 'nickname' => $nickname ]);
2017-12-13 02:07:03 +00:00
if ( $user_count > 1 ) {
2018-07-20 12:19:26 +00:00
DBA :: delete ( 'user' , [ 'uid' => $uid ]);
2017-12-04 03:27:49 +00:00
2020-01-18 19:52:34 +00:00
throw new Exception ( DI :: l10n () -> t ( 'Nickname is already registered. Please choose another.' ));
2017-12-13 02:07:03 +00:00
}
2017-12-13 01:43:21 +00:00
2018-07-20 12:19:26 +00:00
$insert_result = DBA :: insert ( 'profile' , [
2017-12-13 02:07:03 +00:00
'uid' => $uid ,
'name' => $username ,
2021-10-02 21:28:29 +00:00
'photo' => self :: getAvatarUrl ( $user ),
'thumb' => self :: getAvatarUrl ( $user , Proxy :: SIZE_THUMB ),
2017-12-13 02:07:03 +00:00
'publish' => $publish ,
'net-publish' => $netpublish ,
]);
if ( ! $insert_result ) {
2018-07-20 12:19:26 +00:00
DBA :: delete ( 'user' , [ 'uid' => $uid ]);
2017-12-13 02:07:03 +00:00
2020-01-18 19:52:34 +00:00
throw new Exception ( DI :: l10n () -> t ( 'An error occurred creating your default profile. Please try again.' ));
2017-12-13 02:07:03 +00:00
}
2017-12-04 03:27:49 +00:00
2017-12-13 02:07:03 +00:00
// Create the self contact
if ( ! Contact :: createSelfFromUserId ( $uid )) {
2018-07-20 12:19:26 +00:00
DBA :: delete ( 'user' , [ 'uid' => $uid ]);
2017-12-13 01:43:21 +00:00
2020-01-18 19:52:34 +00:00
throw new Exception ( DI :: l10n () -> t ( 'An error occurred creating your self contact. Please try again.' ));
2017-12-13 02:07:03 +00:00
}
2017-12-04 03:27:49 +00:00
2023-05-13 23:54:35 +00:00
// 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' ));
2017-12-13 02:07:03 +00:00
if ( ! $def_gid ) {
2018-07-20 12:19:26 +00:00
DBA :: delete ( 'user' , [ 'uid' => $uid ]);
2017-12-04 03:27:49 +00:00
2023-05-13 23:54:35 +00:00
throw new Exception ( DI :: l10n () -> t ( 'An error occurred creating your default contact circle. Please try again.' ));
2017-12-13 02:07:03 +00:00
}
2017-12-04 03:27:49 +00:00
2017-12-13 02:07:03 +00:00
$fields = [ 'def_gid' => $def_gid ];
2020-01-19 20:21:13 +00:00
if ( DI :: config () -> get ( 'system' , 'newuser_private' ) && $def_gid ) {
2017-12-13 02:07:03 +00:00
$fields [ 'allow_gid' ] = '<' . $def_gid . '>' ;
2017-12-04 03:27:49 +00:00
}
2018-07-20 12:19:26 +00:00
DBA :: update ( 'user' , $fields , [ 'uid' => $uid ]);
2017-12-13 02:07:03 +00:00
2023-06-25 20:37:11 +00:00
$def_gid_groups = Circle :: create ( $uid , DI :: l10n () -> t ( 'Groups' ));
2023-06-26 05:32:33 +00:00
if ( $def_gid_groups ) {
2023-06-25 20:37:11 +00:00
DI :: pConfig () -> set ( $uid , 'system' , 'default-group-gid' , $def_gid_groups );
}
2017-12-04 03:27:49 +00:00
// if we have no OpenID photo try to look up an avatar
if ( ! strlen ( $photo )) {
2018-01-27 16:13:41 +00:00
$photo = Network :: lookupAvatarByEmail ( $email );
2017-12-04 03:27:49 +00:00
}
2018-01-17 19:22:38 +00:00
// unless there is no avatar-addon loaded
2017-12-04 03:27:49 +00:00
if ( strlen ( $photo )) {
$photo_failure = false ;
$filename = basename ( $photo );
2022-04-02 19:16:22 +00:00
$curlResult = DI :: httpClient () -> get ( $photo , HttpClientAccept :: IMAGE );
2020-04-01 05:42:44 +00:00
if ( $curlResult -> isSuccess ()) {
2022-03-29 06:24:20 +00:00
Logger :: debug ( 'Got picture' , [ 'Content-Type' => $curlResult -> getHeader ( 'Content-Type' ), 'url' => $photo ]);
2020-04-01 05:42:44 +00:00
$img_str = $curlResult -> getBody ();
2020-10-11 21:25:40 +00:00
$type = $curlResult -> getContentType ();
2020-04-01 05:42:44 +00:00
} else {
$img_str = '' ;
2020-10-11 21:25:40 +00:00
$type = '' ;
2020-04-01 05:42:44 +00:00
}
2020-10-11 21:25:40 +00:00
$type = Images :: getMimeTypeByData ( $img_str , $photo , $type );
2017-12-04 03:27:49 +00:00
2022-06-23 08:56:37 +00:00
$image = new Image ( $img_str , $type );
if ( $image -> isValid ()) {
$image -> scaleToSquare ( 300 );
2017-12-04 03:27:49 +00:00
2019-10-26 13:05:35 +00:00
$resource_id = Photo :: newResource ();
2017-12-04 03:27:49 +00:00
2023-03-22 04:08:43 +00:00
// Not using Photo::PROFILE_PHOTOS here, so that it is discovered as translatable string
2021-10-14 06:22:47 +00:00
$profile_album = DI :: l10n () -> t ( 'Profile Photos' );
2022-06-23 08:56:37 +00:00
$r = Photo :: store ( $image , $uid , 0 , $resource_id , $filename , $profile_album , 4 );
2017-12-04 03:27:49 +00:00
if ( $r === false ) {
$photo_failure = true ;
}
2022-06-23 08:56:37 +00:00
$image -> scaleDown ( 80 );
2017-12-04 03:27:49 +00:00
2022-06-23 08:56:37 +00:00
$r = Photo :: store ( $image , $uid , 0 , $resource_id , $filename , $profile_album , 5 );
2017-12-04 03:27:49 +00:00
if ( $r === false ) {
$photo_failure = true ;
}
2022-06-23 08:56:37 +00:00
$image -> scaleDown ( 48 );
2017-12-04 03:27:49 +00:00
2022-06-23 08:56:37 +00:00
$r = Photo :: store ( $image , $uid , 0 , $resource_id , $filename , $profile_album , 6 );
2017-12-04 03:27:49 +00:00
if ( $r === false ) {
$photo_failure = true ;
}
if ( ! $photo_failure ) {
2021-10-11 14:21:10 +00:00
Photo :: update ([ 'profile' => true , 'photo-type' => Photo :: USER_AVATAR ], [ 'resource-id' => $resource_id ]);
2017-12-04 03:27:49 +00:00
}
}
2021-05-30 18:36:40 +00:00
Contact :: updateSelfFromUserID ( $uid , true );
2017-12-04 03:27:49 +00:00
}
2018-12-26 06:06:24 +00:00
Hook :: callAll ( 'register_account' , $uid );
2017-12-04 03:27:49 +00:00
2023-03-21 21:44:26 +00:00
self :: setRegisterMethodByUserCount ();
2017-12-16 01:47:10 +00:00
$return [ 'user' ] = $user ;
return $return ;
2017-12-04 03:27:49 +00:00
}
2021-06-15 11:12:44 +00:00
/**
* Update a user entry and distribute the changes if needed
*
2023-08-10 23:05:02 +00:00
* @ param array $fields
2021-06-15 11:12:44 +00:00
* @ param integer $uid
* @ return boolean
2023-08-10 23:05:02 +00:00
* @ throws Exception
2021-06-15 11:12:44 +00:00
*/
public static function update ( array $fields , int $uid ) : bool
{
if ( ! DBA :: update ( 'user' , $fields , [ 'uid' => $uid ])) {
return false ;
}
2023-08-10 23:05:02 +00:00
if ( Contact :: updateSelfFromUserID ( $uid )) {
2021-06-15 11:12:44 +00:00
Profile :: publishUpdate ( $uid );
}
return true ;
}
2020-02-20 22:43:52 +00:00
/**
* Sets block state for a given user
*
* @ param int $uid The user id
* @ param bool $block Block state ( default is true )
*
* @ return bool True , if successfully blocked
* @ throws Exception
*/
2022-06-16 19:57:02 +00:00
public static function block ( int $uid , bool $block = true ) : bool
2020-02-20 22:43:52 +00:00
{
2020-02-21 22:12:07 +00:00
return DBA :: update ( 'user' , [ 'blocked' => $block ], [ 'uid' => $uid ]);
2020-02-20 22:43:52 +00:00
}
2020-02-21 21:57:17 +00:00
/**
* Allows a registration based on a hash
*
* @ param string $hash
*
* @ return bool True , if the allow was successful
*
2020-07-21 06:27:05 +00:00
* @ throws HTTPException\InternalServerErrorException
2020-02-21 21:57:17 +00:00
* @ throws Exception
*/
2022-06-16 19:57:02 +00:00
public static function allow ( string $hash ) : bool
2020-02-21 21:57:17 +00:00
{
$register = Register :: getByHash ( $hash );
if ( ! DBA :: isResult ( $register )) {
return false ;
}
$user = User :: getById ( $register [ 'uid' ]);
if ( ! DBA :: isResult ( $user )) {
return false ;
}
Register :: deleteByHash ( $hash );
DBA :: update ( 'user' , [ 'blocked' => false , 'verified' => true ], [ 'uid' => $register [ 'uid' ]]);
$profile = DBA :: selectFirst ( 'profile' , [ 'net-publish' ], [ 'uid' => $register [ 'uid' ]]);
2022-04-26 18:33:58 +00:00
if ( DBA :: isResult ( $profile ) && $profile [ 'net-publish' ] && Search :: getGlobalDirectory ()) {
2020-02-21 21:57:17 +00:00
$url = DI :: baseUrl () . '/profile/' . $user [ 'nickname' ];
2022-10-17 05:49:55 +00:00
Worker :: add ( Worker :: PRIORITY_LOW , " Directory " , $url );
2020-02-21 21:57:17 +00:00
}
$l10n = DI :: l10n () -> withLang ( $register [ 'language' ]);
return User :: sendRegisterOpenEmail (
$l10n ,
$user ,
DI :: config () -> get ( 'config' , 'sitename' ),
2023-02-18 19:57:30 +00:00
DI :: baseUrl (),
2020-02-21 21:57:17 +00:00
( $register [ 'password' ] ? ? '' ) ? : 'Sent in a previous email'
);
}
2020-02-21 22:03:33 +00:00
/**
* Denys a pending registration
*
* @ param string $hash The hash of the pending user
*
* This does not have to go through user_remove () and save the nickname
* permanently against re - registration , as the person was not yet
* allowed to have friends on this system
*
2023-03-22 04:08:30 +00:00
* @ return bool True , if the deny was successful
2020-02-21 22:03:33 +00:00
* @ throws Exception
*/
2022-06-16 19:57:02 +00:00
public static function deny ( string $hash ) : bool
2020-02-21 22:03:33 +00:00
{
$register = Register :: getByHash ( $hash );
if ( ! DBA :: isResult ( $register )) {
return false ;
}
$user = User :: getById ( $register [ 'uid' ]);
if ( ! DBA :: isResult ( $user )) {
return false ;
}
2021-01-02 09:11:38 +00:00
// Delete the avatar
Photo :: delete ([ 'uid' => $register [ 'uid' ]]);
2020-02-21 22:03:33 +00:00
return DBA :: delete ( 'user' , [ 'uid' => $register [ 'uid' ]]) &&
Register :: deleteByHash ( $register [ 'hash' ]);
}
2020-02-21 21:57:17 +00:00
/**
* Creates a new user based on a minimal set and sends an email to this user
*
* @ param string $name The user ' s name
* @ 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
2020-07-21 06:27:05 +00:00
* @ throws HTTPException\InternalServerErrorException
* @ throws ErrorException
* @ throws ImagickException
2020-02-21 21:57:17 +00:00
*/
2022-06-16 19:57:02 +00:00
public static function createMinimal ( string $name , string $email , string $nick , string $lang = L10n :: DEFAULT ) : bool
2020-02-21 21:57:17 +00:00
{
if ( empty ( $name ) ||
empty ( $email ) ||
empty ( $nick )) {
2020-07-21 06:27:05 +00:00
throw new HTTPException\InternalServerErrorException ( 'Invalid arguments.' );
2020-02-21 21:57:17 +00:00
}
$result = self :: create ([
'username' => $name ,
'email' => $email ,
'nickname' => $nick ,
'verified' => 1 ,
'language' => $lang
]);
$user = $result [ 'user' ];
$preamble = Strings :: deindent ( DI :: l10n () -> t ( '
Dear % 1 $s ,
the administrator of % 2 $s has set up an account for you . ' ));
$body = Strings :: deindent ( DI :: l10n () -> t ( '
The login details are as follows :
Site Location : % 1 $s
Login Name : % 2 $s
Password : % 3 $s
You may change your password from your account " Settings " page after logging
in .
Please take a few moments to review the other account settings on that page .
You may also wish to add some basic information to your default profile
( on the " Profiles " page ) so that other people can easily find you .
We recommend setting your full name , adding a profile photo ,
adding some profile " keywords " ( very useful in making new friends ) - and
perhaps what country you live in ; if you do not wish to be more specific
than that .
We fully respect your right to privacy , and none of these items are necessary .
If you are new and do not know anybody here , they may help
you to make some new and interesting friends .
2022-11-08 04:58:21 +00:00
If you ever want to delete your account , you can do so at % 1 $s / settings / removeme
2020-02-21 21:57:17 +00:00
Thank you and welcome to % 4 $s . ' ));
$preamble = sprintf ( $preamble , $user [ 'username' ], DI :: config () -> get ( 'config' , 'sitename' ));
2023-02-18 19:57:30 +00:00
$body = sprintf ( $body , DI :: baseUrl (), $user [ 'nickname' ], $result [ 'password' ], DI :: config () -> get ( 'config' , 'sitename' ));
2020-02-21 21:57:17 +00:00
$email = DI :: emailer ()
-> newSystemMail ()
-> withMessage ( DI :: l10n () -> t ( 'Registration details for %s' , DI :: config () -> get ( 'config' , 'sitename' )), $preamble , $body )
-> forUser ( $user )
-> withRecipient ( $user [ 'email' ])
-> build ();
return DI :: emailer () -> send ( $email );
}
2017-12-04 03:27:49 +00:00
/**
2020-01-19 06:05:23 +00:00
* Sends pending registration confirmation email
2017-12-04 03:27:49 +00:00
*
2018-10-15 15:58:52 +00:00
* @ param array $user User record array
2017-12-04 03:27:49 +00:00
* @ param string $sitename
2018-10-15 15:58:52 +00:00
* @ param string $siteurl
2018-10-14 15:57:28 +00:00
* @ param string $password Plaintext password
2017-12-04 03:27:49 +00:00
* @ return NULL | boolean from notification () and email () inherited
2020-07-21 06:27:05 +00:00
* @ throws HTTPException\InternalServerErrorException
2017-12-04 03:27:49 +00:00
*/
2022-06-16 19:57:02 +00:00
public static function sendRegisterPendingEmail ( array $user , string $sitename , string $siteurl , string $password )
2017-12-04 03:27:49 +00:00
{
2020-01-18 19:52:34 +00:00
$body = Strings :: deindent ( DI :: l10n () -> t (
2019-10-10 23:21:41 +00:00
'
2017-12-04 03:27:49 +00:00
Dear % 1 $s ,
Thank you for registering at % 2 $s . Your account is pending for approval by the administrator .
2018-10-14 15:57:28 +00:00
Your login details are as follows :
Site Location : % 3 $s
Login Name : % 4 $s
Password : % 5 $s
' ,
2019-10-10 23:21:41 +00:00
$user [ 'username' ],
$sitename ,
$siteurl ,
$user [ 'nickname' ],
$password
2018-10-14 15:57:28 +00:00
));
2017-12-04 03:27:49 +00:00
2020-02-01 19:08:54 +00:00
$email = DI :: emailer ()
2020-02-04 20:04:08 +00:00
-> newSystemMail ()
2020-02-02 08:22:30 +00:00
-> withMessage ( DI :: l10n () -> t ( 'Registration at %s' , $sitename ), $body )
2020-02-04 20:04:08 +00:00
-> forUser ( $user )
2020-02-02 08:22:30 +00:00
-> withRecipient ( $user [ 'email' ])
-> build ();
2020-02-01 19:08:54 +00:00
return DI :: emailer () -> send ( $email );
2017-12-04 03:27:49 +00:00
}
/**
2020-01-19 06:05:23 +00:00
* Sends registration confirmation
2017-12-04 03:27:49 +00:00
*
* It ' s here as a function because the mail is sent from different parts
*
2020-07-21 06:27:05 +00:00
* @ param L10n $l10n The used language
* @ param array $user User record array
* @ param string $sitename
* @ param string $siteurl
* @ param string $password Plaintext password
2020-01-18 19:59:39 +00:00
*
2017-12-04 03:27:49 +00:00
* @ return NULL | boolean from notification () and email () inherited
2020-07-21 06:27:05 +00:00
* @ throws HTTPException\InternalServerErrorException
2017-12-04 03:27:49 +00:00
*/
2022-06-16 19:57:02 +00:00
public static function sendRegisterOpenEmail ( L10n $l10n , array $user , string $sitename , string $siteurl , string $password )
2017-12-04 03:27:49 +00:00
{
2019-12-28 22:12:01 +00:00
$preamble = Strings :: deindent ( $l10n -> t (
2019-10-10 23:21:41 +00:00
'
Dear % 1 $s ,
2017-12-04 03:27:49 +00:00
Thank you for registering at % 2 $s . Your account has been created .
2019-10-10 23:21:41 +00:00
' ,
$user [ 'username' ],
$sitename
2018-10-14 15:57:28 +00:00
));
2019-12-28 22:12:01 +00:00
$body = Strings :: deindent ( $l10n -> t (
2019-10-10 23:21:41 +00:00
'
2017-12-04 03:27:49 +00:00
The login details are as follows :
2018-04-04 19:56:34 +00:00
Site Location : % 3 $s
Login Name : % 1 $s
Password : % 5 $s
2018-04-02 16:40:52 +00:00
You may change your password from your account " Settings " page after logging
2017-12-04 03:27:49 +00:00
in .
Please take a few moments to review the other account settings on that page .
You may also wish to add some basic information to your default profile
2018-01-24 21:51:32 +00:00
' . "\x28" . ' on the " Profiles " page ' . "\x29" . ' so that other people can easily find you .
2017-12-04 03:27:49 +00:00
We recommend setting your full name , adding a profile photo ,
2018-04-02 16:40:52 +00:00
adding some profile " keywords " ' . "\x28" . ' very useful in making new friends ' . "\x29" . ' - and
2017-12-04 03:27:49 +00:00
perhaps what country you live in ; if you do not wish to be more specific
than that .
We fully respect your right to privacy , and none of these items are necessary .
If you are new and do not know anybody here , they may help
you to make some new and interesting friends .
2022-11-08 04:58:21 +00:00
If you ever want to delete your account , you can do so at % 3 $s / settings / removeme
2017-12-04 03:27:49 +00:00
2018-10-14 15:57:28 +00:00
Thank you and welcome to % 2 $s . ' ,
2019-10-10 23:21:41 +00:00
$user [ 'nickname' ],
$sitename ,
$siteurl ,
$user [ 'username' ],
$password
2018-10-14 15:57:28 +00:00
));
2017-12-04 03:27:49 +00:00
2020-02-01 19:08:54 +00:00
$email = DI :: emailer ()
2020-02-04 20:04:08 +00:00
-> newSystemMail ()
2020-02-02 08:22:30 +00:00
-> withMessage ( DI :: l10n () -> t ( 'Registration details for %s' , $sitename ), $preamble , $body )
2020-02-04 20:04:08 +00:00
-> forUser ( $user )
2020-02-02 08:22:30 +00:00
-> withRecipient ( $user [ 'email' ])
-> build ();
2020-02-01 19:08:54 +00:00
return DI :: emailer () -> send ( $email );
2017-12-04 03:27:49 +00:00
}
2017-11-20 16:14:35 +00:00
/**
2020-02-21 22:12:07 +00:00
* @ param int $uid user to remove
2019-01-06 21:06:53 +00:00
* @ return bool
2020-07-21 06:27:05 +00:00
* @ throws HTTPException\InternalServerErrorException
2023-07-27 15:12:05 +00:00
* @ throws HTTPException\NotFoundException
2017-11-20 16:14:35 +00:00
*/
2022-06-16 19:57:02 +00:00
public static function remove ( int $uid ) : bool
2017-11-19 21:55:28 +00:00
{
2021-01-29 23:41:42 +00:00
if ( empty ( $uid )) {
2023-07-27 15:12:05 +00:00
throw new \InvalidArgumentException ( 'uid needs to be greater than 0' );
2017-11-19 21:55:28 +00:00
}
2021-10-20 18:53:52 +00:00
Logger :: notice ( 'Removing user' , [ 'user' => $uid ]);
2017-11-19 21:55:28 +00:00
2023-07-27 15:12:05 +00:00
$user = self :: getById ( $uid );
if ( ! $user ) {
throw new HTTPException\NotFoundException ( 'User not found with uid: ' . $uid );
}
if ( DBA :: exists ( 'user' , [ 'parent-uid' => $uid ])) {
throw new \RuntimeException ( DI :: l10n () -> t ( " User with delegates can't be removed, please remove delegate users first " ));
}
2017-11-19 21:55:28 +00:00
2018-11-25 01:56:38 +00:00
Hook :: callAll ( 'remove_user' , $user );
2017-11-19 21:55:28 +00:00
// save username (actually the nickname as it is guaranteed
// unique), so it cannot be re-registered in the future.
2018-07-20 12:19:26 +00:00
DBA :: insert ( 'userd' , [ 'username' => $user [ 'nickname' ]]);
2017-11-19 21:55:28 +00:00
2021-03-20 09:56:35 +00:00
// Remove all personal settings, especially connector settings
DBA :: delete ( 'pconfig' , [ 'uid' => $uid ]);
2020-08-16 08:39:04 +00:00
// The user and related data will be deleted in Friendica\Worker\ExpireAndRemoveUsers
2018-11-25 01:59:18 +00:00
DBA :: update ( 'user' , [ 'account_removed' => true , 'account_expires_on' => DateTimeFormat :: utc ( 'now + 7 day' )], [ 'uid' => $uid ]);
2022-10-17 05:49:55 +00:00
Worker :: add ( Worker :: PRIORITY_HIGH , 'Notifier' , Delivery :: REMOVAL , $uid );
2017-11-19 21:55:28 +00:00
// Send an update to the directory
2018-08-02 05:21:01 +00:00
$self = DBA :: selectFirst ( 'contact' , [ 'url' ], [ 'uid' => $uid , 'self' => true ]);
2022-10-17 05:49:55 +00:00
Worker :: add ( Worker :: PRIORITY_LOW , 'Directory' , $self [ 'url' ]);
2017-11-19 21:55:28 +00:00
2018-08-25 21:48:50 +00:00
// Remove the user relevant data
2022-10-17 05:49:55 +00:00
Worker :: add ( Worker :: PRIORITY_NEGLIGIBLE , 'RemoveUser' , $uid );
2018-08-25 21:48:50 +00:00
2023-03-21 21:44:26 +00:00
self :: setRegisterMethodByUserCount ();
2018-11-25 01:58:41 +00:00
return true ;
2017-11-19 21:55:28 +00:00
}
2018-11-07 23:22:15 +00:00
/**
* Return all identities to a user
*
* @ param int $uid The user id
* @ return array All identities for this user
*
* Example for a return :
2019-01-06 21:06:53 +00:00
* [
* [
* 'uid' => 1 ,
* 'username' => 'maxmuster' ,
* 'nickname' => 'Max Mustermann'
* ],
* [
* 'uid' => 2 ,
* 'username' => 'johndoe' ,
* 'nickname' => 'John Doe'
* ]
* ]
* @ throws Exception
2018-11-07 23:22:15 +00:00
*/
2022-06-16 19:57:02 +00:00
public static function identities ( int $uid ) : array
2018-11-07 23:22:15 +00:00
{
2023-05-27 22:19:02 +00:00
if ( ! $uid ) {
2021-06-19 06:27:25 +00:00
return [];
}
2018-11-07 23:22:15 +00:00
$identities = [];
2023-09-08 15:01:51 +00:00
$user = DBA :: selectFirst ( 'user' , [ 'uid' , 'nickname' , 'username' , 'parent-uid' ], [ 'uid' => $uid , 'verified' => true , 'blocked' => false , 'account_removed' => false , 'account_expired' => false ]);
2018-11-07 23:22:15 +00:00
if ( ! DBA :: isResult ( $user )) {
return $identities ;
}
2023-05-27 22:19:02 +00:00
if ( ! $user [ 'parent-uid' ]) {
2018-11-07 23:22:15 +00:00
// First add our own entry
2019-10-10 23:21:41 +00:00
$identities = [[
'uid' => $user [ 'uid' ],
2018-11-07 23:22:15 +00:00
'username' => $user [ 'username' ],
2019-10-10 23:21:41 +00:00
'nickname' => $user [ 'nickname' ]
]];
2018-11-07 23:22:15 +00:00
// Then add all the children
2019-10-10 23:21:41 +00:00
$r = DBA :: select (
'user' ,
[ 'uid' , 'username' , 'nickname' ],
2023-09-08 15:01:51 +00:00
[ 'parent-uid' => $user [ 'uid' ], 'verified' => true , 'blocked' => false , 'account_removed' => false , 'account_expired' => false ]
2019-10-10 23:21:41 +00:00
);
2018-11-07 23:22:15 +00:00
if ( DBA :: isResult ( $r )) {
$identities = array_merge ( $identities , DBA :: toArray ( $r ));
}
} else {
// First entry is our parent
2019-10-10 23:21:41 +00:00
$r = DBA :: select (
'user' ,
[ 'uid' , 'username' , 'nickname' ],
2023-09-08 15:01:51 +00:00
[ 'uid' => $user [ 'parent-uid' ], 'verified' => true , 'blocked' => false , 'account_removed' => false , 'account_expired' => false ]
2019-10-10 23:21:41 +00:00
);
2018-11-07 23:22:15 +00:00
if ( DBA :: isResult ( $r )) {
$identities = DBA :: toArray ( $r );
}
// Then add all siblings
2019-10-10 23:21:41 +00:00
$r = DBA :: select (
'user' ,
[ 'uid' , 'username' , 'nickname' ],
2023-09-08 15:01:51 +00:00
[ 'parent-uid' => $user [ 'parent-uid' ], 'verified' => true , 'blocked' => false , 'account_removed' => false , 'account_expired' => false ]
2019-10-10 23:21:41 +00:00
);
2018-11-07 23:22:15 +00:00
if ( DBA :: isResult ( $r )) {
$identities = array_merge ( $identities , DBA :: toArray ( $r ));
}
}
2019-10-10 23:21:41 +00:00
$r = DBA :: p (
" SELECT `user`.`uid`, `user`.`username`, `user`.`nickname`
2018-11-07 23:22:15 +00:00
FROM `manage`
INNER JOIN `user` ON `manage` . `mid` = `user` . `uid`
2023-09-08 15:01:51 +00:00
WHERE NOT `user` . `account_removed` AND `manage` . `uid` = ? " ,
2018-11-07 23:22:15 +00:00
$user [ 'uid' ]
);
if ( DBA :: isResult ( $r )) {
$identities = array_merge ( $identities , DBA :: toArray ( $r ));
}
return $identities ;
}
2019-04-22 12:00:17 +00:00
2021-07-24 11:49:11 +00:00
/**
* Check if the given user id has delegations or is delegated
*
2021-09-10 13:05:16 +00:00
* @ param int $uid
* @ return bool
2021-07-24 11:49:11 +00:00
*/
2022-06-16 19:57:02 +00:00
public static function hasIdentities ( int $uid ) : bool
2021-07-24 11:49:11 +00:00
{
2023-05-27 22:19:02 +00:00
if ( ! $uid ) {
2021-07-24 11:49:11 +00:00
return false ;
}
2023-09-08 15:01:51 +00:00
$user = DBA :: selectFirst ( 'user' , [ 'parent-uid' ], [ 'uid' => $uid , 'verified' => true , 'blocked' => false , 'account_removed' => false , 'account_expired' => false ]);
2021-07-24 11:49:11 +00:00
if ( ! DBA :: isResult ( $user )) {
return false ;
}
2023-05-27 22:19:02 +00:00
if ( $user [ 'parent-uid' ]) {
2021-07-24 11:49:11 +00:00
return true ;
}
2023-09-08 15:01:51 +00:00
if ( DBA :: exists ( 'user' , [ 'parent-uid' => $uid , 'verified' => true , 'blocked' => false , 'account_removed' => false , 'account_expired' => false ])) {
2021-07-24 11:49:11 +00:00
return true ;
}
2021-07-24 13:24:26 +00:00
if ( DBA :: exists ( 'manage' , [ 'uid' => $uid ])) {
2021-07-24 11:49:11 +00:00
return true ;
}
return false ;
}
2019-04-22 12:00:17 +00:00
/**
* Returns statistical information about the current users of this node
*
* @ return array
*
* @ throws Exception
*/
2022-06-16 19:57:02 +00:00
public static function getStatistics () : array
2019-04-22 12:00:17 +00:00
{
$statistics = [
'total_users' => 0 ,
'active_users_halfyear' => 0 ,
'active_users_monthly' => 0 ,
2020-07-12 21:53:17 +00:00
'active_users_weekly' => 0 ,
2019-04-22 12:00:17 +00:00
];
2022-12-04 07:03:11 +00:00
$userStmt = DBA :: select ( 'owner-view' , [ 'uid' , 'last-activity' , 'last-item' ],
[ " `verified` AND `last-activity` > ? AND NOT `blocked`
2020-04-24 11:04:50 +00:00
AND NOT `account_removed` AND NOT `account_expired` " ,
DBA :: NULL_DATETIME ]);
2019-04-22 12:00:17 +00:00
if ( ! DBA :: isResult ( $userStmt )) {
return $statistics ;
}
$halfyear = time () - ( 180 * 24 * 60 * 60 );
$month = time () - ( 30 * 24 * 60 * 60 );
2020-07-12 21:53:17 +00:00
$week = time () - ( 7 * 24 * 60 * 60 );
2019-04-22 12:00:17 +00:00
while ( $user = DBA :: fetch ( $userStmt )) {
$statistics [ 'total_users' ] ++ ;
2022-12-04 07:03:11 +00:00
if (( strtotime ( $user [ 'last-activity' ]) > $halfyear ) || ( strtotime ( $user [ 'last-item' ]) > $halfyear )
2019-10-10 23:21:41 +00:00
) {
2019-04-22 12:00:17 +00:00
$statistics [ 'active_users_halfyear' ] ++ ;
}
2022-12-04 07:03:11 +00:00
if (( strtotime ( $user [ 'last-activity' ]) > $month ) || ( strtotime ( $user [ 'last-item' ]) > $month )
2019-10-10 23:21:41 +00:00
) {
2019-04-22 12:00:17 +00:00
$statistics [ 'active_users_monthly' ] ++ ;
}
2020-07-12 21:53:17 +00:00
2022-12-04 07:03:11 +00:00
if (( strtotime ( $user [ 'last-activity' ]) > $week ) || ( strtotime ( $user [ 'last-item' ]) > $week )
2020-07-12 21:53:17 +00:00
) {
$statistics [ 'active_users_weekly' ] ++ ;
}
2019-04-22 12:00:17 +00:00
}
2020-04-28 05:55:17 +00:00
DBA :: close ( $userStmt );
2019-04-22 12:00:17 +00:00
return $statistics ;
}
2020-02-25 21:16:27 +00:00
/**
* Get all users of the current node
*
* @ param int $start Start count ( Default is 0 )
* @ param int $count Count of the items per page ( Default is @ see Pager :: ITEMS_PER_PAGE )
2023-03-22 03:16:55 +00:00
* @ param string $type The type of users , which should get ( all , blocked , removed )
2020-02-25 21:16:27 +00:00
* @ param string $order Order of the user list ( Default is 'contact.name' )
2020-04-24 11:04:50 +00:00
* @ param bool $descending Order direction ( Default is ascending )
2022-06-16 19:57:02 +00:00
* @ return array | bool The list of the users
2020-02-25 21:16:27 +00:00
* @ throws Exception
*/
2022-06-16 19:57:02 +00:00
public static function getList ( int $start = 0 , int $count = Pager :: ITEMS_PER_PAGE , string $type = 'all' , string $order = 'name' , bool $descending = false )
2020-02-25 21:16:27 +00:00
{
2020-04-24 11:04:50 +00:00
$param = [ 'limit' => [ $start , $count ], 'order' => [ $order => $descending ]];
$condition = [];
2020-02-25 22:22:47 +00:00
switch ( $type ) {
case 'active' :
2020-07-06 19:26:39 +00:00
$condition [ 'account_removed' ] = false ;
2020-04-24 11:04:50 +00:00
$condition [ 'blocked' ] = false ;
2020-02-25 22:22:47 +00:00
break ;
2022-06-23 08:56:37 +00:00
2020-02-25 22:22:47 +00:00
case 'blocked' :
2020-11-08 07:26:12 +00:00
$condition [ 'account_removed' ] = false ;
2020-04-24 11:04:50 +00:00
$condition [ 'blocked' ] = true ;
2020-11-08 07:26:12 +00:00
$condition [ 'verified' ] = true ;
2020-02-25 22:22:47 +00:00
break ;
2022-06-23 08:56:37 +00:00
2020-02-25 22:22:47 +00:00
case 'removed' :
2020-04-24 11:04:50 +00:00
$condition [ 'account_removed' ] = true ;
2020-02-25 22:22:47 +00:00
break ;
}
2020-04-24 11:04:50 +00:00
return DBA :: selectToArray ( 'owner-view' , [], $condition , $param );
2020-02-25 21:16:27 +00:00
}
2022-11-12 17:01:22 +00:00
/**
* 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 (),
2023-05-27 22:19:02 +00:00
'parent-uid' => null ,
'blocked' => false ,
2022-11-12 17:01:22 +00:00
'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 ;
});
}
2023-03-21 21:44:26 +00:00
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' );
2023-04-01 12:07:48 +00:00
if ( ! in_array ( $register_policy , [ Module\Register :: OPEN , Module\Register :: CLOSED ])) {
2023-03-21 21:44:26 +00:00
Logger :: debug ( 'Unsupported register policy.' , [ 'policy' => $register_policy ]);
return ;
}
$users = DBA :: count ( 'user' , [ 'blocked' => false , 'account_removed' => false , 'account_expired' => false ]);
2023-04-01 12:07:48 +00:00
if (( $users >= $max_registered_users ) && ( $register_policy == Module\Register :: OPEN )) {
DI :: config () -> set ( 'config' , 'register_policy' , Module\Register :: CLOSED );
2023-03-21 21:44:26 +00:00
Logger :: notice ( 'Max users reached, registration is closed.' , [ 'users' => $users , 'max' => $max_registered_users ]);
2023-04-01 12:07:48 +00:00
} elseif (( $users < $max_registered_users ) && ( $register_policy == Module\Register :: CLOSED )) {
DI :: config () -> set ( 'config' , 'register_policy' , Module\Register :: OPEN );
2023-03-21 21:44:26 +00:00
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 ]);
}
}
2017-11-19 21:55:28 +00:00
}