2017-11-19 16:55:28 -05:00
< ? php
/**
2021-03-29 02:40:20 -04:00
* @ copyright Copyright ( C ) 2010 - 2021 , the Friendica project
2020-02-09 09:45:36 -05: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 16:55:28 -05:00
*/
2019-10-10 19:21:41 -04:00
2017-11-19 16:55:28 -05:00
namespace Friendica\Model ;
2020-07-21 02:27:05 -04:00
use DivineOmega\DOFileCachePSR6\CacheItemPool ;
2018-10-19 11:26:48 -04:00
use DivineOmega\PasswordExposed ;
2020-07-21 02:27:05 -04:00
use ErrorException ;
2018-07-19 22:15:21 -04:00
use Exception ;
2020-02-25 16:16:27 -05:00
use Friendica\Content\Pager ;
2018-11-24 20:56:38 -05:00
use Friendica\Core\Hook ;
2020-02-21 16:57:17 -05:00
use Friendica\Core\L10n ;
2018-10-29 17:20:46 -04:00
use Friendica\Core\Logger ;
2018-08-11 16:40:44 -04:00
use Friendica\Core\Protocol ;
2017-11-19 16:55:28 -05:00
use Friendica\Core\System ;
use Friendica\Core\Worker ;
2018-07-20 08:19:26 -04:00
use Friendica\Database\DBA ;
2019-12-15 18:47:24 -05:00
use Friendica\DI ;
2021-01-18 22:53:06 -05:00
use Friendica\Security\TwoFactor\Model\AppSpecificPassword ;
2020-07-21 02:27:05 -04:00
use Friendica\Network\HTTPException ;
2017-12-07 08:56:11 -05:00
use Friendica\Object\Image ;
2017-12-30 11:51:49 -05:00
use Friendica\Util\Crypto ;
2018-01-26 21:38:34 -05:00
use Friendica\Util\DateTimeFormat ;
2019-10-17 21:26:15 -04:00
use Friendica\Util\Images ;
2018-01-26 23:09:48 -05:00
use Friendica\Util\Network ;
2020-12-07 01:43:43 -05:00
use Friendica\Util\Proxy ;
2018-11-08 10:14:37 -05:00
use Friendica\Util\Strings ;
2019-06-10 10:19:24 -04:00
use Friendica\Worker\Delivery ;
2020-07-21 02:27:05 -04:00
use ImagickException ;
2018-01-24 21:08:45 -05:00
use LightOpenID ;
2017-11-19 16:55:28 -05:00
/**
2020-01-19 01:05:23 -05:00
* This class handles User related functions
2017-11-19 16:55:28 -05:00
*/
class User
{
2019-01-06 12:37:48 -05:00
/**
2019-01-14 08:45:37 -05:00
* Page / profile types
2019-01-06 12:37:48 -05:00
*
2019-01-14 08:45:37 -05: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 12:37:48 -05:00
* write access to wall and comments ( no email and not included in page owner ' s ACL lists )
2019-01-14 08:45:37 -05:00
* PAGE_FLAGS_FREELOVE automatically approves all friend requests as full friends ( Contact :: FRIEND ) .
2019-01-06 12:37:48 -05: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 17:08:35 -05: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
*
* ACCOUNT_TYPE_COMMUNITY - the account is community forum
* 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 17:56:46 -05:00
const ACCOUNT_TYPE_DELETED = 127 ;
2019-01-06 17:08:35 -05:00
/**
* @ }
*/
2020-08-16 07:57:56 -04:00
private static $owner ;
2020-10-09 15:08:50 -04:00
/**
* Returns the numeric account type by their string
*
* @ param string $accounttype as string constant
2020-10-10 03:14:43 -04:00
* @ return int | null Numeric account type - or null when not set
2020-10-09 15:08:50 -04:00
*/
public static function getAccountTypeByString ( string $accounttype )
{
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 ;
}
}
2020-08-22 10:48:09 -04:00
/**
* Fetch the system account
*
2020-08-22 14:52:37 -04:00
* @ return array system account
2020-08-22 10:48:09 -04:00
*/
public static function getSystemAccount ()
{
$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 03:29:56 -04:00
$system [ 'sprvkey' ] = $system [ 'uprvkey' ] = $system [ 'prvkey' ];
$system [ 'spubkey' ] = $system [ 'upubkey' ] = $system [ 'pubkey' ];
2020-08-22 10:48:09 -04:00
$system [ 'nickname' ] = $system [ 'nick' ];
2021-07-04 17:26:08 -04:00
$system [ 'page-flags' ] = User :: PAGE_FLAGS_SOAPBOX ;
$system [ 'account-type' ] = $system [ 'contact-type' ];
$system [ 'guid' ] = '' ;
2021-08-23 14:07:32 -04:00
$system [ 'picdate' ] = '' ;
$system [ 'theme' ] = '' ;
$system [ 'publish' ] = false ;
$system [ 'net-publish' ] = false ;
$system [ 'hide-friends' ] = true ;
$system [ 'prv_keywords' ] = '' ;
$system [ 'pub_keywords' ] = '' ;
$system [ 'address' ] = '' ;
2021-07-04 17:26:08 -04:00
$system [ 'locality' ] = '' ;
$system [ 'region' ] = '' ;
2021-08-23 14:07:32 -04:00
$system [ 'postal-code' ] = '' ;
2021-07-04 17:26:08 -04:00
$system [ 'country-name' ] = '' ;
2021-08-23 14:07:32 -04:00
$system [ 'homepage' ] = DI :: baseUrl () -> get ();
$system [ 'dob' ] = '0000-00-00' ;
2020-11-18 00:33:17 -05:00
// Ensure that the user contains data
2021-08-05 04:30:44 -04:00
$user = DBA :: selectFirst ( 'user' , [ 'prvkey' , 'guid' ], [ 'uid' => 0 ]);
if ( empty ( $user [ 'prvkey' ]) || empty ( $user [ 'guid' ])) {
2020-11-18 00:33:17 -05: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 04:30:44 -04:00
'guid' => System :: createUUID (),
2020-11-18 00:33:17 -05:00
'verified' => true ,
'page-flags' => User :: PAGE_FLAGS_SOAPBOX ,
'account-type' => User :: ACCOUNT_TYPE_RELAY ,
];
DBA :: update ( 'user' , $fields , [ 'uid' => 0 ]);
2021-08-05 04:30:44 -04:00
$system [ 'guid' ] = $fields [ 'guid' ];
} else {
$system [ 'guid' ] = $user [ 'guid' ];
2020-11-18 00:33:17 -05:00
}
2020-08-22 10:48:09 -04:00
return $system ;
}
/**
* Create the system account
*
* @ return void
*/
private static function createSystemAccount ()
{
2020-08-22 14:52:37 -04:00
$system_actor_name = self :: getActorName ();
2020-08-22 10:48:09 -04:00
if ( empty ( $system_actor_name )) {
2020-08-22 14:52:37 -04:00
return ;
2020-08-22 10:48:09 -04:00
}
$keys = Crypto :: newKeypair ( 4096 );
if ( $keys === false ) {
throw new Exception ( DI :: l10n () -> t ( 'SERIOUS ERROR: Generation of security keys failed.' ));
}
$system = [];
$system [ 'uid' ] = 0 ;
$system [ 'created' ] = DateTimeFormat :: utcNow ();
$system [ 'self' ] = true ;
$system [ 'network' ] = Protocol :: ACTIVITYPUB ;
$system [ 'name' ] = 'System Account' ;
$system [ 'addr' ] = $system_actor_name . '@' . DI :: baseUrl () -> getHostname ();
$system [ 'nick' ] = $system_actor_name ;
$system [ 'url' ] = DI :: baseUrl () . '/friendica' ;
2020-12-07 01:43:43 -05:00
$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 );
2020-08-22 10:48:09 -04:00
$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' ]);
2021-09-10 09:05:16 -04:00
Contact :: insert ( $system );
2020-08-22 10:48:09 -04:00
}
/**
* Detect a usable actor name
*
* @ return string actor account name
*/
public static function getActorName ()
{
2020-08-22 14:52:37 -04:00
$system_actor_name = DI :: config () -> get ( 'system' , 'actor_name' );
if ( ! empty ( $system_actor_name )) {
2020-08-22 15:40:31 -04: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 14:52:37 -04:00
return $system_actor_name ;
}
2020-08-22 10:48:09 -04:00
// List of possible actor names
$possible_accounts = [ 'friendica' , 'actor' , 'system' , 'internal' ];
foreach ( $possible_accounts as $name ) {
2020-11-17 18:59:14 -05:00
if ( ! DBA :: exists ( 'user' , [ 'nickname' => $name , 'account_removed' => false , 'expire' => false ]) &&
2020-08-22 10:48:09 -04:00
! DBA :: exists ( 'userd' , [ 'username' => $name ])) {
DI :: config () -> set ( 'system' , 'actor_name' , $name );
return $name ;
}
}
return '' ;
}
2018-10-14 11:34:34 -04:00
/**
* Returns true if a user record exists with the provided id
*
* @ param integer $uid
* @ return boolean
2019-01-06 16:06:53 -05:00
* @ throws Exception
2018-10-14 11:34:34 -04:00
*/
public static function exists ( $uid )
{
return DBA :: exists ( 'user' , [ 'uid' => $uid ]);
}
2018-09-27 23:56:41 -04:00
/**
2018-10-15 11:58:52 -04:00
* @ param integer $uid
2019-04-30 18:14:06 -04:00
* @ param array $fields
2018-10-15 11:58:52 -04:00
* @ return array | boolean User record if it exists , false otherwise
2019-01-06 16:06:53 -05:00
* @ throws Exception
2018-10-15 11:58:52 -04:00
*/
2019-04-30 18:14:06 -04:00
public static function getById ( $uid , array $fields = [])
2018-10-15 11:58:52 -04:00
{
2020-11-19 12:19:14 -05:00
return ! empty ( $uid ) ? DBA :: selectFirst ( 'user' , $fields , [ 'uid' => $uid ]) : [];
2019-04-30 18:14:06 -04:00
}
2019-10-20 07:00:08 -04: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 ) {
$cond = [ 'guid' => $guid , 'account_expired' => false , 'account_removed' => false ];
} else {
$cond = [ 'guid' => $guid ];
}
return DBA :: selectFirst ( 'user' , $fields , $cond );
}
2019-04-30 18:14:06 -04:00
/**
* @ param string $nickname
* @ param array $fields
* @ return array | boolean User record if it exists , false otherwise
* @ throws Exception
*/
public static function getByNickname ( $nickname , array $fields = [])
{
return DBA :: selectFirst ( 'user' , $fields , [ 'nickname' => $nickname ]);
2018-10-15 11:58:52 -04:00
}
/**
2020-01-19 01:05:23 -05:00
* Returns the user id of a given profile URL
2018-09-27 23:56:41 -04:00
*
2018-10-15 11:58:52 -04:00
* @ param string $url
2018-09-27 23:56:41 -04:00
*
* @ return integer user id
2019-01-06 16:06:53 -05:00
* @ throws Exception
2018-09-27 23:56:41 -04:00
*/
2020-08-18 15:45:01 -04:00
public static function getIdForURL ( string $url )
2018-09-27 23:56:41 -04:00
{
2021-05-26 14:15:07 -04:00
// Avoid database queries when the local node hostname isn't even part of the url.
2021-05-26 05:24:37 -04:00
if ( ! Contact :: isLocal ( $url )) {
2020-08-19 00:45:31 -04:00
return 0 ;
}
2020-08-18 15:45:01 -04: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-27 23:56:41 -04:00
return $self [ 'uid' ];
}
2020-08-18 15:45:01 -04:00
$self = Contact :: selectFirst ([ 'uid' ], [ 'self' => true , 'alias' => [ $url , Strings :: normaliseLink ( $url )]]);
if ( ! empty ( $self [ 'uid' ])) {
return $self [ 'uid' ];
}
return 0 ;
2018-09-27 23:56:41 -04:00
}
2019-05-04 04:05:21 -04:00
/**
2019-05-05 04:00:28 -04:00
* Get a user based on its email
2019-05-04 04:05:21 -04:00
*
* @ 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 = [])
{
return DBA :: selectFirst ( 'user' , $fields , [ 'email' => $email ]);
}
2020-07-29 01:12:16 -04:00
/**
* Fetch the user array of the administrator . The first one if there are several .
*
* @ param array $fields
* @ return array user
*/
public static function getFirstAdmin ( array $fields = [])
{
if ( ! empty ( DI :: config () -> get ( 'config' , 'admin_nickname' ))) {
2020-07-29 10:59:55 -04:00
return self :: getByNickname ( DI :: config () -> get ( 'config' , 'admin_nickname' ), $fields );
2020-07-29 10:55:55 -04:00
} elseif ( ! empty ( DI :: config () -> get ( 'config' , 'admin_email' ))) {
2020-07-29 01:12:16 -04:00
$adminList = explode ( ',' , str_replace ( ' ' , '' , DI :: config () -> get ( 'config' , 'admin_email' )));
2020-07-29 10:59:55 -04:00
return self :: getByEmail ( $adminList [ 0 ], $fields );
} else {
return [];
2020-07-29 01:12:16 -04:00
}
}
2017-12-17 16:22:39 -05:00
/**
2020-01-19 01:05:23 -05:00
* Get owner data by user id
2017-12-17 16:22:39 -05:00
*
2020-12-15 09:41:10 -05:00
* @ param int $uid
* @ param boolean $repairMissing Repair the owner data if it ' s missing
2017-12-17 16:22:39 -05:00
* @ return boolean | array
2019-01-06 16:06:53 -05:00
* @ throws Exception
2017-12-17 16:22:39 -05:00
*/
2020-12-15 09:41:10 -05:00
public static function getOwnerDataById ( int $uid , bool $repairMissing = true )
2019-10-10 19:21:41 -04:00
{
2020-08-23 03:29:56 -04:00
if ( $uid == 0 ) {
return self :: getSystemAccount ();
}
2020-08-16 08:51:15 -04:00
if ( ! empty ( self :: $owner [ $uid ])) {
return self :: $owner [ $uid ];
2020-08-16 07:57:56 -04:00
}
2020-04-24 07:04:50 -04:00
$owner = DBA :: selectFirst ( 'owner-view' , [], [ 'uid' => $uid ]);
if ( ! DBA :: isResult ( $owner )) {
2020-12-15 09:41:10 -05:00
if ( ! DBA :: exists ( 'user' , [ 'uid' => $uid ]) || ! $repairMissing ) {
2020-04-25 03:29:38 -04:00
return false ;
}
2021-06-15 17:39:28 -04:00
if ( ! DBA :: exists ( 'profile' , [ 'uid' => $uid ])) {
DBA :: insert ( 'profile' , [ 'uid' => $uid ]);
}
2021-06-15 18:01:30 -04:00
if ( ! DBA :: exists ( 'contact' , [ 'uid' => $uid , 'self' => true ])) {
Contact :: createSelfFromUserId ( $uid );
}
2020-04-25 03:29:38 -04:00
$owner = self :: getOwnerDataById ( $uid , false );
2017-12-17 16:22:39 -05:00
}
2018-12-22 15:12:32 -05:00
2020-04-24 07:04:50 -04:00
if ( empty ( $owner [ 'nickname' ])) {
2018-12-22 15:12:32 -05:00
return false ;
}
2021-03-20 05:56:35 -04:00
if ( ! $repairMissing || $owner [ 'account_expired' ]) {
2020-04-24 07:04:50 -04:00
return $owner ;
2019-06-19 13:05:29 -04:00
}
2018-12-22 15:12:32 -05:00
// Check if the returned data is valid, otherwise fix it. See issue #6122
2019-06-19 13:05:29 -04:00
// Check for correct url and normalised nurl
2020-04-24 07:04:50 -04:00
$url = DI :: baseUrl () . '/profile/' . $owner [ 'nickname' ];
2021-07-04 10:17:10 -04:00
$repair = empty ( $owner [ 'network' ]) || ( $owner [ 'url' ] != $url ) || ( $owner [ 'nurl' ] != Strings :: normaliseLink ( $owner [ 'url' ]));
2019-06-19 13:05:29 -04:00
if ( ! $repair ) {
// Check if "addr" is present and correct
2020-04-24 07:04:50 -04:00
$addr = $owner [ 'nickname' ] . '@' . substr ( DI :: baseUrl (), strpos ( DI :: baseUrl (), '://' ) + 3 );
2020-09-26 16:59:28 -04:00
$repair = ( $addr != $owner [ 'addr' ]) || empty ( $owner [ 'prvkey' ]) || empty ( $owner [ 'pubkey' ]);
2019-06-19 13:05:29 -04: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 07:04:50 -04:00
$repair = empty ( $owner [ 'avatar' ]) || ! strpos ( $owner [ 'photo' ], $avatar [ 'resource-id' ]);
2019-06-19 13:05:29 -04:00
}
}
2018-12-22 15:12:32 -05:00
2019-06-19 13:05:29 -04:00
if ( $repair ) {
2018-12-22 15:12:32 -05:00
Contact :: updateSelfFromUserID ( $uid );
2019-06-19 13:05:29 -04:00
// Return the corrected data and avoid a loop
2020-04-24 07:04:50 -04:00
$owner = self :: getOwnerDataById ( $uid , false );
2018-12-22 15:12:32 -05:00
}
2020-08-16 08:51:15 -04:00
self :: $owner [ $uid ] = $owner ;
2020-04-24 07:04:50 -04:00
return $owner ;
2017-12-17 16:22:39 -05:00
}
2018-06-18 16:36:34 -04:00
/**
2020-01-19 01:05:23 -05:00
* Get owner data by nick name
2018-06-18 16:36:34 -04:00
*
* @ param int $nick
* @ return boolean | array
2019-01-06 16:06:53 -05:00
* @ throws Exception
2018-06-18 16:36:34 -04:00
*/
public static function getOwnerDataByNick ( $nick )
{
2018-07-20 08:19:26 -04: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 01:05:49 -04:00
2018-07-21 08:46:04 -04:00
if ( ! DBA :: isResult ( $user )) {
2018-06-18 16:36:34 -04: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 01:05:49 -04:00
2018-06-18 16:36:34 -04:00
return self :: getOwnerDataById ( $user [ 'uid' ]);
}
2017-12-03 22:15:31 -05:00
/**
2020-01-19 01:05:23 -05:00
* Returns the default group for a given user and network
2017-12-09 13:31:00 -05:00
*
2020-07-21 02:27:05 -04:00
* @ param int $uid User id
2017-12-09 13:31:00 -05:00
* @ param string $network network name
*
* @ return int group id
2020-07-21 02:27:05 -04:00
* @ throws Exception
2017-12-09 13:31:00 -05:00
*/
public static function getDefaultGroup ( $uid , $network = '' )
{
2018-07-20 08:19:26 -04:00
$user = DBA :: selectFirst ( 'user' , [ 'def_gid' ], [ 'uid' => $uid ]);
2018-07-21 08:46:04 -04:00
if ( DBA :: isResult ( $user )) {
2017-12-09 13:31:00 -05:00
$default_group = $user [ " def_gid " ];
2021-07-13 02:06:08 -04:00
} else {
$default_group = 0 ;
2017-12-09 13:31:00 -05:00
}
return $default_group ;
}
/**
2018-02-09 00:08:01 -05:00
* Authenticate a user with a clear text password
2017-12-03 22:15:31 -05:00
*
2019-07-22 07:56:36 -04:00
* @ param mixed $user_info
2017-12-03 22:15:31 -05:00
* @ param string $password
2019-07-22 07:56:36 -04:00
* @ param bool $third_party
2018-02-09 00:08:01 -05:00
* @ return int | boolean
* @ deprecated since version 3.6
2019-07-22 07:56:36 -04:00
* @ see User :: getIdFromPasswordAuthentication ()
2017-12-03 22:15:31 -05:00
*/
2019-07-22 07:56:36 -04:00
public static function authenticate ( $user_info , $password , $third_party = false )
2017-11-26 14:25:25 -05:00
{
2018-02-09 00:08:01 -05:00
try {
2019-07-22 07:56:36 -04:00
return self :: getIdFromPasswordAuthentication ( $user_info , $password , $third_party );
2018-02-09 00:08:01 -05:00
} catch ( Exception $ex ) {
return false ;
2017-11-26 14:25:25 -05:00
}
2018-02-09 00:08:01 -05:00
}
2017-11-26 14:25:25 -05:00
2018-02-09 00:08:01 -05:00
/**
2020-01-19 01:05:23 -05:00
* Authenticate a user with a clear text password
*
2018-02-09 00:08:01 -05:00
* Returns the user id associated with a successful password authentication
*
2019-07-22 07:56:36 -04:00
* @ param mixed $user_info
2018-02-09 00:08:01 -05:00
* @ param string $password
2019-07-22 07:56:36 -04:00
* @ param bool $third_party
2018-02-09 00:08:01 -05:00
* @ return int User Id if authentication is successful
2019-07-27 18:14:39 -04:00
* @ throws HTTPException\ForbiddenException
* @ throws HTTPException\NotFoundException
2018-02-09 00:08:01 -05:00
*/
2019-07-22 07:56:36 -04:00
public static function getIdFromPasswordAuthentication ( $user_info , $password , $third_party = false )
2018-02-09 00:08:01 -05:00
{
2021-05-20 03:16:08 -04: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 14:05:48 -04: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
2021-05-20 03:19:09 -04:00
// abiguity with user IDs, possibly opening up an attack vector.
// So let's be very careful about that.
2021-05-20 14:54:30 -04:00
if ( empty ( $username ) || is_numeric ( $username )) {
2021-05-20 03:19:09 -04:00
throw $e ;
}
2021-05-20 14:05:48 -04:00
return self :: getIdFromAuthenticateHooks ( $username , $password );
2021-05-20 03:16:08 -04:00
}
2017-11-26 14:25:25 -05:00
2020-01-18 10:50:57 -05:00
if ( $third_party && DI :: pConfig () -> get ( $user [ 'uid' ], '2fa' , 'verified' )) {
2019-07-22 07:56:36 -04: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 05:12:32 -04:00
//Legacy hash that has not been replaced by a new hash yet
2018-04-08 06:28:04 -04:00
if ( self :: hashPasswordLegacy ( $password ) === $user [ 'password' ]) {
2019-01-01 01:06:28 -05:00
self :: updatePasswordHashed ( $user [ 'uid' ], self :: hashPassword ( $password ));
2018-04-08 06:28:04 -04:00
2018-04-15 05:12:32 -04: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 01:06:28 -05:00
self :: updatePasswordHashed ( $user [ 'uid' ], self :: hashPassword ( $password ));
2018-04-15 05:12:32 -04:00
2018-04-08 06:28:04 -04:00
return $user [ 'uid' ];
}
2018-04-08 10:02:25 -04:00
} elseif ( password_verify ( $password , $user [ 'password' ])) {
2018-04-15 05:12:32 -04:00
//New password hash
2018-04-08 10:02:25 -04:00
if ( password_needs_rehash ( $user [ 'password' ], PASSWORD_DEFAULT )) {
2019-01-01 01:06:28 -05:00
self :: updatePasswordHashed ( $user [ 'uid' ], self :: hashPassword ( $password ));
2018-04-08 10:02:25 -04:00
}
return $user [ 'uid' ];
2021-05-19 15:57:58 -04:00
} else {
2021-05-20 03:16:08 -04:00
return self :: getIdFromAuthenticateHooks ( $user [ 'nickname' ], $password ); // throws
}
2021-05-19 15:57:58 -04:00
2021-05-20 03:16:08 -04:00
throw new HTTPException\ForbiddenException ( DI :: l10n () -> t ( 'Login failed' ));
}
2021-05-19 15:57:58 -04:00
2021-05-20 03:16:08 -04: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
*/
2021-05-20 14:05:48 -04:00
public static function getIdFromAuthenticateHooks ( $username , $password )
{
2021-05-20 03:16:08 -04: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 03:19:09 -04:00
return $addon_auth [ 'user_record' ][ 'uid' ];
2017-11-26 14:25:25 -05:00
}
2019-07-27 18:14:39 -04:00
throw new HTTPException\ForbiddenException ( DI :: l10n () -> t ( 'Login failed' ));
2018-02-09 00:08:01 -05: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
* @ return array
2019-07-27 18:14:39 -04:00
* @ throws HTTPException\NotFoundException
2018-02-09 00:08:01 -05:00
*/
2020-11-19 11:20:17 -05:00
public static function getAuthenticationInfo ( $user_info )
2018-02-09 00:08:01 -05:00
{
2018-02-14 00:05:00 -05:00
$user = null ;
2018-02-09 00:08:01 -05: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 19:21:41 -04:00
if (
! isset ( $user [ 'uid' ])
2018-02-09 00:08:01 -05:00
|| ! isset ( $user [ 'password' ])
2018-04-15 04:51:22 -04:00
|| ! isset ( $user [ 'legacy_password' ])
2018-02-09 00:08:01 -05:00
) {
2020-01-18 14:52:34 -05:00
throw new Exception ( DI :: l10n () -> t ( 'Not enough information to authenticate' ));
2018-02-09 00:08:01 -05:00
}
} elseif ( is_int ( $user_info ) || is_string ( $user_info )) {
if ( is_int ( $user_info )) {
2019-10-10 19:21:41 -04:00
$user = DBA :: selectFirst (
'user' ,
2021-05-19 15:57:58 -04:00
[ 'uid' , 'nickname' , 'password' , 'legacy_password' ],
2018-02-09 00:08:01 -05:00
[
'uid' => $user_info ,
'blocked' => 0 ,
'account_expired' => 0 ,
'account_removed' => 0 ,
'verified' => 1
]
);
} else {
2021-05-19 15:57:58 -04:00
$fields = [ 'uid' , 'nickname' , 'password' , 'legacy_password' ];
2019-10-10 19:21:41 -04:00
$condition = [
" (`email` = ? OR `username` = ? OR `nickname` = ?)
2018-06-19 17:33:07 -04:00
AND NOT `blocked` AND NOT `account_expired` AND NOT `account_removed` AND `verified` " ,
2019-10-10 19:21:41 -04:00
$user_info , $user_info , $user_info
];
2018-07-20 08:19:26 -04:00
$user = DBA :: selectFirst ( 'user' , $fields , $condition );
2018-02-09 00:08:01 -05:00
}
2018-07-21 08:46:04 -04:00
if ( ! DBA :: isResult ( $user )) {
2019-07-27 18:14:39 -04:00
throw new HTTPException\NotFoundException ( DI :: l10n () -> t ( 'User not found' ));
2018-02-09 00:08:01 -05:00
}
}
return $user ;
2017-11-26 14:25:25 -05:00
}
2018-01-19 22:49:06 -05:00
/**
* Generates a human - readable random password
*
* @ return string
2020-07-21 02:27:05 -04:00
* @ throws Exception
2018-01-19 22:49:06 -05:00
*/
public static function generateNewPassword ()
{
2019-10-10 19:21:41 -04:00
return ucfirst ( Strings :: getRandomName ( 8 )) . random_int ( 1000 , 9999 );
2018-01-19 22:49:06 -05:00
}
2018-03-21 01:33:35 -04:00
/**
* Checks if the provided plaintext password has been exposed or not
*
* @ param string $password
* @ return bool
2019-10-12 21:06:47 -04:00
* @ throws Exception
2018-03-21 01:33:35 -04:00
*/
2018-03-21 02:14:43 -04:00
public static function isPasswordExposed ( $password )
2018-03-21 01:33:35 -04:00
{
2020-07-21 02:27:05 -04:00
$cache = new CacheItemPool ();
2018-10-19 11:26:48 -04:00
$cache -> changeConfig ([
'cacheDirectory' => get_temppath () . '/password-exposed-cache/' ,
]);
2019-10-12 21:06:47 -04:00
try {
$passwordExposedChecker = new PasswordExposed\PasswordExposedChecker ( null , $cache );
return $passwordExposedChecker -> passwordExposed ( $password ) === PasswordExposed\PasswordStatus :: EXPOSED ;
2020-07-21 02:27:05 -04:00
} catch ( Exception $e ) {
2019-10-12 21:06:47 -04:00
Logger :: error ( 'Password Exposed Exception: ' . $e -> getMessage (), [
'code' => $e -> getCode (),
'file' => $e -> getFile (),
'line' => $e -> getLine (),
'trace' => $e -> getTraceAsString ()
]);
2018-10-19 11:26:48 -04:00
2019-10-12 21:06:47 -04:00
return false ;
}
2018-03-21 01:33:35 -04:00
}
2018-01-19 22:49:06 -05:00
/**
2018-01-20 22:29:03 -05:00
* Legacy hashing function , kept for password migration purposes
2018-01-19 22:49:06 -05:00
*
* @ param string $password
* @ return string
*/
2018-01-20 22:29:03 -05:00
private static function hashPasswordLegacy ( $password )
2018-01-19 22:49:06 -05:00
{
return hash ( 'whirlpool' , $password );
}
2018-01-20 22:29:03 -05:00
/**
* Global user password hashing function
*
* @ param string $password
* @ return string
2019-01-01 01:08:55 -05:00
* @ throws Exception
2018-01-20 22:29:03 -05:00
*/
public static function hashPassword ( $password )
{
2018-04-18 22:49:14 -04:00
if ( ! trim ( $password )) {
2020-01-18 14:52:34 -05:00
throw new Exception ( DI :: l10n () -> t ( 'Password can\'t be empty' ));
2018-04-18 22:49:14 -04:00
}
2018-01-20 22:29:03 -05:00
return password_hash ( $password , PASSWORD_DEFAULT );
}
2018-01-19 22:49:06 -05:00
/**
* Updates a user row with a new plaintext password
*
* @ param int $uid
* @ param string $password
* @ return bool
2019-01-01 01:08:55 -05:00
* @ throws Exception
2018-01-19 22:49:06 -05:00
*/
public static function updatePassword ( $uid , $password )
{
2019-01-01 01:08:55 -05:00
$password = trim ( $password );
if ( empty ( $password )) {
2020-01-18 14:52:34 -05:00
throw new Exception ( DI :: l10n () -> t ( 'Empty passwords are not allowed.' ));
2019-01-01 01:08:55 -05:00
}
2020-01-19 15:21:13 -05:00
if ( ! DI :: config () -> get ( 'system' , 'disable_password_exposed' , false ) && self :: isPasswordExposed ( $password )) {
2020-01-18 14:52:34 -05:00
throw new Exception ( DI :: l10n () -> t ( 'The new password has been exposed in a public data dump, please choose another.' ));
2019-01-01 01:08:55 -05:00
}
$allowed_characters = '!"#$%&\'()*+,-./;<=>?@[\]^_`{|}~' ;
if ( ! preg_match ( '/^[a-z0-9' . preg_quote ( $allowed_characters , '/' ) . ']+$/i' , $password )) {
2020-01-18 14:52:34 -05:00
throw new Exception ( DI :: l10n () -> t ( 'The password can\'t contain accentuated letters, white spaces or colons (:)' ));
2019-01-01 01:08:55 -05:00
}
2018-01-19 22:49:06 -05: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
* @ param string $pasword_hashed
* @ return bool
2019-01-06 16:06:53 -05:00
* @ throws Exception
2018-01-19 22:49:06 -05:00
*/
private static function updatePasswordHashed ( $uid , $pasword_hashed )
{
2018-01-20 18:15:55 -05:00
$fields = [
'password' => $pasword_hashed ,
'pwdreset' => null ,
2018-01-20 22:29:03 -05:00
'pwdreset_time' => null ,
2018-04-15 04:51:22 -04:00
'legacy_password' => false
2018-01-20 18:15:55 -05:00
];
2018-07-20 08:19:26 -04:00
return DBA :: update ( 'user' , $fields , [ 'uid' => $uid ]);
2018-01-19 22:49:06 -05:00
}
2018-07-06 09:32:56 -04:00
/**
2020-01-19 01:05:23 -05:00
* Checks if a nickname is in the list of the forbidden nicknames
2018-07-06 09:32:56 -04:00
*
* Check if a nickname is forbidden from registration on the node by the
* admin . Forbidden nicknames ( e . g . role namess ) can be configured in the
* admin panel .
*
* @ param string $nickname The nickname that should be checked
* @ return boolean True is the nickname is blocked on the node
*/
public static function isNicknameBlocked ( $nickname )
{
2020-01-19 15:21:13 -05:00
$forbidden_nicknames = DI :: config () -> get ( 'system' , 'forbidden_nicknames' , '' );
2020-08-22 10:48:09 -04: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 14:52:37 -04:00
$actor_name = self :: getActorName ();
2020-08-22 10:48:09 -04:00
if ( ! empty ( $actor_name )) {
$forbidden [] = $actor_name ;
}
2018-07-22 21:18:21 -04:00
2020-08-22 10:48:09 -04:00
if ( empty ( $forbidden )) {
2018-07-06 09:32:56 -04:00
return false ;
}
2018-07-22 21:18:21 -04:00
2020-08-22 15:40:31 -04:00
// check if the nickname is in the list of blocked nicknames
2018-07-06 09:32:56 -04:00
if ( in_array ( strtolower ( $nickname ), $forbidden )) {
return true ;
}
2018-07-22 21:18:21 -04:00
2018-07-06 09:32:56 -04:00
// else return false
return false ;
}
2021-09-17 14:36:20 -04:00
/**
2021-10-02 17:28:29 -04:00
* Get avatar link for given user
2021-09-17 14:36:20 -04:00
*
2021-10-02 17:28:29 -04:00
* @ param array $user
2021-10-05 16:18:19 -04:00
* @ param string $size One of the Proxy :: SIZE_ * constants
2021-09-17 14:36:20 -04:00
* @ return string avatar link
2021-10-02 17:28:29 -04:00
* @ throws Exception
2021-09-17 14:36:20 -04:00
*/
2021-10-02 17:28:29 -04:00
public static function getAvatarUrl ( array $user , string $size = '' ) : string
2021-09-17 14:36:20 -04:00
{
2021-10-03 19:18:00 -04:00
if ( empty ( $user [ 'nickname' ])) {
DI :: logger () -> warning ( 'Missing user nickname key' , [ 'trace' => System :: callstack ( 20 )]);
}
2021-09-17 14:36:20 -04: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 ;
}
$updated = '' ;
$imagetype = IMAGETYPE_JPEG ;
2021-10-02 17:28:29 -04:00
$photo = Photo :: selectFirst ([ 'type' , 'created' , 'edited' , 'updated' ], [ " scale " => $scale , 'uid' => $user [ 'uid' ], 'profile' => true ]);
2021-09-17 14:36:20 -04:00
if ( ! empty ( $photo )) {
$updated = max ( $photo [ 'created' ], $photo [ 'edited' ], $photo [ 'updated' ]);
2021-10-02 17:28:29 -04:00
if ( in_array ( $photo [ 'type' ], [ 'image/png' , 'image/gif' ])) {
$imagetype = IMAGETYPE_PNG ;
2021-09-17 14:36:20 -04:00
}
}
2021-10-02 18:42:14 -04:00
return $url . $user [ 'nickname' ] . image_type_to_extension ( $imagetype ) . ( $updated ? '?ts=' . strtotime ( $updated ) : '' );
2021-09-17 14:36:20 -04:00
}
2017-12-03 22:27:49 -05:00
/**
2020-01-19 01:05:23 -05:00
* Catch - all user creation function
2017-12-03 22:27:49 -05: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 01:08:55 -05:00
* @ param array $data
* @ return array
2020-07-21 02:27:05 -04:00
* @ throws ErrorException
* @ throws HTTPException\InternalServerErrorException
* @ throws ImagickException
2019-01-01 01:08:55 -05:00
* @ throws Exception
2017-12-03 22:27:49 -05:00
*/
public static function create ( array $data )
{
2017-12-12 20:43:21 -05:00
$return = [ 'user' => null , 'password' => '' ];
2017-12-03 22:27:49 -05:00
2020-01-19 15:21:13 -05:00
$using_invites = DI :: config () -> get ( 'system' , 'invitation_only' );
2017-12-03 22:27:49 -05:00
2018-11-09 13:29:42 -05:00
$invite_id = ! empty ( $data [ 'invite_id' ]) ? Strings :: escapeTags ( trim ( $data [ 'invite_id' ])) : '' ;
$username = ! empty ( $data [ 'username' ]) ? Strings :: escapeTags ( trim ( $data [ 'username' ])) : '' ;
$nickname = ! empty ( $data [ 'nickname' ]) ? Strings :: escapeTags ( trim ( $data [ 'nickname' ])) : '' ;
$email = ! empty ( $data [ 'email' ]) ? Strings :: escapeTags ( trim ( $data [ 'email' ])) : '' ;
$openid_url = ! empty ( $data [ 'openid_url' ]) ? Strings :: escapeTags ( trim ( $data [ 'openid_url' ])) : '' ;
$photo = ! empty ( $data [ 'photo' ]) ? Strings :: escapeTags ( trim ( $data [ 'photo' ])) : '' ;
2018-07-22 21:18:21 -04:00
$password = ! empty ( $data [ 'password' ]) ? trim ( $data [ 'password' ]) : '' ;
$password1 = ! empty ( $data [ 'password1' ]) ? trim ( $data [ 'password1' ]) : '' ;
$confirm = ! empty ( $data [ 'confirm' ]) ? trim ( $data [ 'confirm' ]) : '' ;
2018-11-30 09:06:22 -05:00
$blocked = ! empty ( $data [ 'blocked' ]);
$verified = ! empty ( $data [ 'verified' ]);
2018-11-09 13:29:42 -05:00
$language = ! empty ( $data [ 'language' ]) ? Strings :: escapeTags ( trim ( $data [ 'language' ])) : 'en' ;
2018-07-22 21:18:21 -04:00
2020-02-16 10:39:44 -05:00
$netpublish = $publish = ! empty ( $data [ 'profile_publish_reg' ]);
2017-12-03 22:27:49 -05:00
if ( $password1 != $confirm ) {
2020-01-18 14:52:34 -05:00
throw new Exception ( DI :: l10n () -> t ( 'Passwords do not match. Password unchanged.' ));
2017-12-12 21:07:03 -05:00
} elseif ( $password1 != '' ) {
2017-12-03 22:27:49 -05:00
$password = $password1 ;
}
if ( $using_invites ) {
if ( ! $invite_id ) {
2020-01-18 14:52:34 -05:00
throw new Exception ( DI :: l10n () -> t ( 'An invitation is required.' ));
2017-12-03 22:27:49 -05:00
}
2017-12-12 21:07:03 -05:00
2018-10-14 11:57:28 -04:00
if ( ! Register :: existsByHash ( $invite_id )) {
2020-01-18 14:52:34 -05:00
throw new Exception ( DI :: l10n () -> t ( 'Invitation could not be verified.' ));
2017-12-03 22:27:49 -05:00
}
}
2019-10-24 16:23:26 -04:00
/// @todo Check if this part is really needed. We should have fetched all this data in advance
2018-07-22 21:18:21 -04:00
if ( empty ( $username ) || empty ( $email ) || empty ( $nickname )) {
2017-12-03 22:27:49 -05:00
if ( $openid_url ) {
2018-01-27 11:13:41 -05:00
if ( ! Network :: isUrlValid ( $openid_url )) {
2020-01-18 14:52:34 -05:00
throw new Exception ( DI :: l10n () -> t ( 'Invalid OpenID url' ));
2017-12-03 22:27:49 -05:00
}
$_SESSION [ 'register' ] = 1 ;
$_SESSION [ 'openid' ] = $openid_url ;
2019-12-15 18:47:24 -05:00
$openid = new LightOpenID ( DI :: baseUrl () -> getHostname ());
2017-12-03 22:27:49 -05:00
$openid -> identity = $openid_url ;
2019-12-30 17:00:08 -05:00
$openid -> returnUrl = DI :: baseUrl () . '/openid' ;
2018-01-15 08:05:12 -05:00
$openid -> required = [ 'namePerson/friendly' , 'contact/email' , 'namePerson' ];
$openid -> optional = [ 'namePerson/first' , 'media/image/aspect11' , 'media/image/default' ];
2017-12-03 22:27:49 -05:00
try {
$authurl = $openid -> authUrl ();
} catch ( Exception $e ) {
2020-01-18 14:52:34 -05: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.' ) . EOL . EOL . DI :: l10n () -> t ( 'The error message was:' ) . $e -> getMessage (), 0 , $e );
2017-12-03 22:27:49 -05:00
}
2018-10-19 14:11:27 -04:00
System :: externalRedirect ( $authurl );
2017-12-03 22:27:49 -05:00
// NOTREACHED
}
2020-01-18 14:52:34 -05:00
throw new Exception ( DI :: l10n () -> t ( 'Please enter the required information.' ));
2017-12-03 22:27:49 -05:00
}
2018-01-27 11:13:41 -05:00
if ( ! Network :: isUrlValid ( $openid_url )) {
2017-12-03 22:27:49 -05:00
$openid_url = '' ;
}
// collapse multiple spaces in name
$username = preg_replace ( '/ +/' , ' ' , $username );
2020-01-19 15:21:13 -05: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 16:33:54 -04:00
2018-10-21 08:28:24 -04:00
if ( $username_min_length > $username_max_length ) {
2020-01-18 14:52:34 -05:00
Logger :: log ( DI :: l10n () -> t ( 'system.username_min_length (%s) and system.username_max_length (%s) are excluding each other, swapping values.' , $username_min_length , $username_max_length ), Logger :: WARNING );
2018-10-21 08:28:24 -04:00
$tmp = $username_min_length ;
$username_min_length = $username_max_length ;
$username_max_length = $tmp ;
}
2018-10-20 16:33:54 -04:00
if ( mb_strlen ( $username ) < $username_min_length ) {
2020-01-18 14:53:01 -05: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-03 22:27:49 -05:00
}
2018-10-20 16:33:54 -04:00
if ( mb_strlen ( $username ) > $username_max_length ) {
2020-01-18 14:53:01 -05: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-03 22:27:49 -05:00
}
// So now we are just looking for a space in the full name.
2020-01-19 15:21:13 -05:00
$loose_reg = DI :: config () -> get ( 'system' , 'no_regfullname' );
2017-12-03 22:27:49 -05:00
if ( ! $loose_reg ) {
$username = mb_convert_case ( $username , MB_CASE_TITLE , 'UTF-8' );
2018-10-20 16:33:54 -04:00
if ( strpos ( $username , ' ' ) === false ) {
2020-01-18 14:52:34 -05:00
throw new Exception ( DI :: l10n () -> t ( " That doesn't appear to be your full (First Last) name. " ));
2017-12-03 22:27:49 -05:00
}
}
2018-01-27 11:13:41 -05:00
if ( ! Network :: isEmailDomainAllowed ( $email )) {
2020-01-18 14:52:34 -05:00
throw new Exception ( DI :: l10n () -> t ( 'Your email domain is not among those allowed on this site.' ));
2017-12-03 22:27:49 -05:00
}
2018-11-09 13:18:42 -05:00
if ( ! filter_var ( $email , FILTER_VALIDATE_EMAIL ) || ! Network :: isEmailDomainValid ( $email )) {
2020-01-18 14:52:34 -05:00
throw new Exception ( DI :: l10n () -> t ( 'Not a valid email address.' ));
2017-12-03 22:27:49 -05:00
}
2018-07-06 09:32:56 -04:00
if ( self :: isNicknameBlocked ( $nickname )) {
2020-01-18 14:52:34 -05:00
throw new Exception ( DI :: l10n () -> t ( 'The nickname was blocked from registration by the nodes admin.' ));
2018-07-06 09:32:56 -04:00
}
2017-12-03 22:27:49 -05:00
2020-01-19 15:21:13 -05:00
if ( DI :: config () -> get ( 'system' , 'block_extended_register' , false ) && DBA :: exists ( 'user' , [ 'email' => $email ])) {
2020-01-18 14:52:34 -05:00
throw new Exception ( DI :: l10n () -> t ( 'Cannot use that email.' ));
2017-12-12 21:07:03 -05:00
}
2017-12-03 22:27:49 -05: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.
2020-01-19 15:21:13 -05:00
if ( DI :: config () -> get ( 'config' , 'admin_email' ) && strlen ( $openid_url )) {
$adminlist = explode ( ',' , str_replace ( ' ' , '' , strtolower ( DI :: config () -> get ( 'config' , 'admin_email' ))));
2017-12-12 21:07:03 -05:00
if ( in_array ( strtolower ( $email ), $adminlist )) {
2020-01-18 14:52:34 -05:00
throw new Exception ( DI :: l10n () -> t ( 'Cannot use that email.' ));
2017-12-03 22:27:49 -05:00
}
}
$nickname = $data [ 'nickname' ] = strtolower ( $nickname );
2020-07-21 02:27:05 -04:00
if ( ! preg_match ( '/^[a-z0-9][a-z0-9_]*$/' , $nickname )) {
2020-01-18 14:52:34 -05:00
throw new Exception ( DI :: l10n () -> t ( 'Your nickname can only contain a-z, 0-9 and _.' ));
2017-12-03 22:27:49 -05:00
}
2017-12-12 21:07:03 -05:00
// Check existing and deleted accounts for this nickname.
2019-10-10 19:21:41 -04:00
if (
DBA :: exists ( 'user' , [ 'nickname' => $nickname ])
2018-07-20 08:19:26 -04:00
|| DBA :: exists ( 'userd' , [ 'username' => $nickname ])
2017-12-12 21:07:03 -05:00
) {
2020-01-18 14:52:34 -05:00
throw new Exception ( DI :: l10n () -> t ( 'Nickname is already registered. Please choose another.' ));
2017-12-03 22:27:49 -05:00
}
2018-01-19 22:49:06 -05:00
$new_password = strlen ( $password ) ? $password : User :: generateNewPassword ();
$new_password_encoded = self :: hashPassword ( $new_password );
2017-12-03 22:27:49 -05:00
2017-12-12 20:43:21 -05:00
$return [ 'password' ] = $new_password ;
2017-12-03 22:27:49 -05:00
2017-12-30 11:51:49 -05:00
$keys = Crypto :: newKeypair ( 4096 );
2017-12-03 22:27:49 -05:00
if ( $keys === false ) {
2020-01-18 14:52:34 -05:00
throw new Exception ( DI :: l10n () -> t ( 'SERIOUS ERROR: Generation of security keys failed.' ));
2017-12-03 22:27:49 -05:00
}
$prvkey = $keys [ 'prvkey' ];
$pubkey = $keys [ 'pubkey' ];
// Create another keypair for signing/verifying salmon protocol messages.
2017-12-30 11:51:49 -05:00
$sres = Crypto :: newKeypair ( 512 );
2017-12-03 22:27:49 -05:00
$sprvkey = $sres [ 'prvkey' ];
$spubkey = $sres [ 'pubkey' ];
2018-07-20 08:19:26 -04:00
$insert_result = DBA :: insert ( 'user' , [
2018-09-27 07:52:15 -04:00
'guid' => System :: createUUID (),
2017-12-12 21:07:03 -05: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 02:27:27 -04:00
'language' => $language ,
2017-12-12 21:07:03 -05:00
'timezone' => 'UTC' ,
2018-01-26 21:38:34 -05:00
'register_date' => DateTimeFormat :: utcNow (),
2017-12-12 21:07:03 -05:00
'default-location' => ''
]);
if ( $insert_result ) {
2018-07-20 08:19:26 -04:00
$uid = DBA :: lastInsertId ();
$user = DBA :: selectFirst ( 'user' , [], [ 'uid' => $uid ]);
2017-12-03 22:27:49 -05:00
} else {
2020-01-18 14:52:34 -05:00
throw new Exception ( DI :: l10n () -> t ( 'An error occurred during registration. Please try again.' ));
2017-12-03 22:27:49 -05:00
}
2017-12-12 21:07:03 -05:00
if ( ! $uid ) {
2020-01-18 14:52:34 -05:00
throw new Exception ( DI :: l10n () -> t ( 'An error occurred during registration. Please try again.' ));
2017-12-03 22:27:49 -05:00
}
2017-12-12 21:07:03 -05: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 08:19:26 -04:00
$user_count = DBA :: count ( 'user' , [ 'nickname' => $nickname ]);
2017-12-12 21:07:03 -05:00
if ( $user_count > 1 ) {
2018-07-20 08:19:26 -04:00
DBA :: delete ( 'user' , [ 'uid' => $uid ]);
2017-12-03 22:27:49 -05:00
2020-01-18 14:52:34 -05:00
throw new Exception ( DI :: l10n () -> t ( 'Nickname is already registered. Please choose another.' ));
2017-12-12 21:07:03 -05:00
}
2017-12-12 20:43:21 -05:00
2018-07-20 08:19:26 -04:00
$insert_result = DBA :: insert ( 'profile' , [
2017-12-12 21:07:03 -05:00
'uid' => $uid ,
'name' => $username ,
2021-10-02 17:28:29 -04:00
'photo' => self :: getAvatarUrl ( $user ),
'thumb' => self :: getAvatarUrl ( $user , Proxy :: SIZE_THUMB ),
2017-12-12 21:07:03 -05:00
'publish' => $publish ,
'net-publish' => $netpublish ,
]);
if ( ! $insert_result ) {
2018-07-20 08:19:26 -04:00
DBA :: delete ( 'user' , [ 'uid' => $uid ]);
2017-12-12 21:07:03 -05:00
2020-01-18 14:52:34 -05:00
throw new Exception ( DI :: l10n () -> t ( 'An error occurred creating your default profile. Please try again.' ));
2017-12-12 21:07:03 -05:00
}
2017-12-03 22:27:49 -05:00
2017-12-12 21:07:03 -05:00
// Create the self contact
if ( ! Contact :: createSelfFromUserId ( $uid )) {
2018-07-20 08:19:26 -04:00
DBA :: delete ( 'user' , [ 'uid' => $uid ]);
2017-12-12 20:43:21 -05:00
2020-01-18 14:52:34 -05:00
throw new Exception ( DI :: l10n () -> t ( 'An error occurred creating your self contact. Please try again.' ));
2017-12-12 21:07:03 -05:00
}
2017-12-03 22:27:49 -05:00
2017-12-12 21:07:03 -05:00
// Create a group with no members. This allows somebody to use it
// right away as a default group for new contacts.
2020-01-18 14:52:34 -05:00
$def_gid = Group :: create ( $uid , DI :: l10n () -> t ( 'Friends' ));
2017-12-12 21:07:03 -05:00
if ( ! $def_gid ) {
2018-07-20 08:19:26 -04:00
DBA :: delete ( 'user' , [ 'uid' => $uid ]);
2017-12-03 22:27:49 -05:00
2020-01-18 14:52:34 -05:00
throw new Exception ( DI :: l10n () -> t ( 'An error occurred creating your default contact group. Please try again.' ));
2017-12-12 21:07:03 -05:00
}
2017-12-03 22:27:49 -05:00
2017-12-12 21:07:03 -05:00
$fields = [ 'def_gid' => $def_gid ];
2020-01-19 15:21:13 -05:00
if ( DI :: config () -> get ( 'system' , 'newuser_private' ) && $def_gid ) {
2017-12-12 21:07:03 -05:00
$fields [ 'allow_gid' ] = '<' . $def_gid . '>' ;
2017-12-03 22:27:49 -05:00
}
2018-07-20 08:19:26 -04:00
DBA :: update ( 'user' , $fields , [ 'uid' => $uid ]);
2017-12-12 21:07:03 -05:00
2017-12-03 22:27:49 -05:00
// if we have no OpenID photo try to look up an avatar
if ( ! strlen ( $photo )) {
2018-01-27 11:13:41 -05:00
$photo = Network :: lookupAvatarByEmail ( $email );
2017-12-03 22:27:49 -05:00
}
2018-01-17 14:22:38 -05:00
// unless there is no avatar-addon loaded
2017-12-03 22:27:49 -05:00
if ( strlen ( $photo )) {
$photo_failure = false ;
$filename = basename ( $photo );
2021-08-25 15:54:54 -04:00
$curlResult = DI :: httpClient () -> get ( $photo );
2020-04-01 01:42:44 -04:00
if ( $curlResult -> isSuccess ()) {
$img_str = $curlResult -> getBody ();
2020-10-11 17:25:40 -04:00
$type = $curlResult -> getContentType ();
2020-04-01 01:42:44 -04:00
} else {
$img_str = '' ;
2020-10-11 17:25:40 -04:00
$type = '' ;
2020-04-01 01:42:44 -04:00
}
2020-10-11 17:25:40 -04:00
$type = Images :: getMimeTypeByData ( $img_str , $photo , $type );
2017-12-03 22:27:49 -05:00
2017-12-07 08:56:11 -05:00
$Image = new Image ( $img_str , $type );
if ( $Image -> isValid ()) {
2018-10-23 10:36:57 -04:00
$Image -> scaleToSquare ( 300 );
2017-12-03 22:27:49 -05:00
2019-10-26 09:05:35 -04:00
$resource_id = Photo :: newResource ();
2017-12-03 22:27:49 -05:00
2020-01-18 14:52:34 -05:00
$r = Photo :: store ( $Image , $uid , 0 , $resource_id , $filename , DI :: l10n () -> t ( 'Profile Photos' ), 4 );
2017-12-03 22:27:49 -05:00
if ( $r === false ) {
$photo_failure = true ;
}
2017-12-07 08:56:11 -05:00
$Image -> scaleDown ( 80 );
2017-12-03 22:27:49 -05:00
2020-01-18 14:52:34 -05:00
$r = Photo :: store ( $Image , $uid , 0 , $resource_id , $filename , DI :: l10n () -> t ( 'Profile Photos' ), 5 );
2017-12-03 22:27:49 -05:00
if ( $r === false ) {
$photo_failure = true ;
}
2017-12-07 08:56:11 -05:00
$Image -> scaleDown ( 48 );
2017-12-03 22:27:49 -05:00
2020-01-18 14:52:34 -05:00
$r = Photo :: store ( $Image , $uid , 0 , $resource_id , $filename , DI :: l10n () -> t ( 'Profile Photos' ), 6 );
2017-12-03 22:27:49 -05:00
if ( $r === false ) {
$photo_failure = true ;
}
if ( ! $photo_failure ) {
2019-10-26 09:05:35 -04:00
Photo :: update ([ 'profile' => 1 ], [ 'resource-id' => $resource_id ]);
2017-12-03 22:27:49 -05:00
}
}
2021-05-30 14:36:40 -04:00
Contact :: updateSelfFromUserID ( $uid , true );
2017-12-03 22:27:49 -05:00
}
2018-12-26 01:06:24 -05:00
Hook :: callAll ( 'register_account' , $uid );
2017-12-03 22:27:49 -05:00
2017-12-15 20:47:10 -05:00
$return [ 'user' ] = $user ;
return $return ;
2017-12-03 22:27:49 -05:00
}
2021-06-15 07:12:44 -04:00
/**
* Update a user entry and distribute the changes if needed
*
* @ param array $fields
* @ param integer $uid
* @ return boolean
*/
public static function update ( array $fields , int $uid ) : bool
{
$old_owner = self :: getOwnerDataById ( $uid );
if ( empty ( $old_owner )) {
return false ;
}
if ( ! DBA :: update ( 'user' , $fields , [ 'uid' => $uid ])) {
return false ;
}
$update = Contact :: updateSelfFromUserID ( $uid );
$owner = self :: getOwnerDataById ( $uid );
if ( empty ( $owner )) {
return false ;
}
if ( $old_owner [ 'name' ] != $owner [ 'name' ]) {
Profile :: update ([ 'name' => $owner [ 'name' ]], $uid );
}
if ( $update ) {
Profile :: publishUpdate ( $uid );
}
return true ;
}
2020-02-20 17:43:52 -05: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
*/
public static function block ( int $uid , bool $block = true )
{
2020-02-21 17:12:07 -05:00
return DBA :: update ( 'user' , [ 'blocked' => $block ], [ 'uid' => $uid ]);
2020-02-20 17:43:52 -05:00
}
2020-02-21 16:57:17 -05:00
/**
* Allows a registration based on a hash
*
* @ param string $hash
*
* @ return bool True , if the allow was successful
*
2020-07-21 02:27:05 -04:00
* @ throws HTTPException\InternalServerErrorException
2020-02-21 16:57:17 -05:00
* @ throws Exception
*/
public static function allow ( string $hash )
{
$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' ]]);
if ( DBA :: isResult ( $profile ) && $profile [ 'net-publish' ] && DI :: config () -> get ( 'system' , 'directory' )) {
$url = DI :: baseUrl () . '/profile/' . $user [ 'nickname' ];
Worker :: add ( PRIORITY_LOW , " Directory " , $url );
}
$l10n = DI :: l10n () -> withLang ( $register [ 'language' ]);
return User :: sendRegisterOpenEmail (
$l10n ,
$user ,
DI :: config () -> get ( 'config' , 'sitename' ),
DI :: baseUrl () -> get (),
( $register [ 'password' ] ? ? '' ) ? : 'Sent in a previous email'
);
}
2020-02-21 17:03:33 -05: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
*
* @ return bool True , if the deny was successfull
* @ throws Exception
*/
public static function deny ( string $hash )
{
$register = Register :: getByHash ( $hash );
if ( ! DBA :: isResult ( $register )) {
return false ;
}
$user = User :: getById ( $register [ 'uid' ]);
if ( ! DBA :: isResult ( $user )) {
return false ;
}
2021-01-02 04:11:38 -05:00
// Delete the avatar
Photo :: delete ([ 'uid' => $register [ 'uid' ]]);
2020-02-21 17:03:33 -05:00
return DBA :: delete ( 'user' , [ 'uid' => $register [ 'uid' ]]) &&
Register :: deleteByHash ( $register [ 'hash' ]);
}
2020-02-21 16:57:17 -05: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 02:27:05 -04:00
* @ throws HTTPException\InternalServerErrorException
* @ throws ErrorException
* @ throws ImagickException
2020-02-21 16:57:17 -05:00
*/
public static function createMinimal ( string $name , string $email , string $nick , string $lang = L10n :: DEFAULT )
{
if ( empty ( $name ) ||
empty ( $email ) ||
empty ( $nick )) {
2020-07-21 02:27:05 -04:00
throw new HTTPException\InternalServerErrorException ( 'Invalid arguments.' );
2020-02-21 16:57:17 -05: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 .
If you ever want to delete your account , you can do so at % 1 $s / removeme
Thank you and welcome to % 4 $s . ' ));
$preamble = sprintf ( $preamble , $user [ 'username' ], DI :: config () -> get ( 'config' , 'sitename' ));
$body = sprintf ( $body , DI :: baseUrl () -> get (), $user [ 'nickname' ], $result [ 'password' ], DI :: config () -> get ( 'config' , 'sitename' ));
$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-03 22:27:49 -05:00
/**
2020-01-19 01:05:23 -05:00
* Sends pending registration confirmation email
2017-12-03 22:27:49 -05:00
*
2018-10-15 11:58:52 -04:00
* @ param array $user User record array
2017-12-03 22:27:49 -05:00
* @ param string $sitename
2018-10-15 11:58:52 -04:00
* @ param string $siteurl
2018-10-14 11:57:28 -04:00
* @ param string $password Plaintext password
2017-12-03 22:27:49 -05:00
* @ return NULL | boolean from notification () and email () inherited
2020-07-21 02:27:05 -04:00
* @ throws HTTPException\InternalServerErrorException
2017-12-03 22:27:49 -05:00
*/
2018-10-15 11:58:52 -04:00
public static function sendRegisterPendingEmail ( $user , $sitename , $siteurl , $password )
2017-12-03 22:27:49 -05:00
{
2020-01-18 14:52:34 -05:00
$body = Strings :: deindent ( DI :: l10n () -> t (
2019-10-10 19:21:41 -04:00
'
2017-12-03 22:27:49 -05:00
Dear % 1 $s ,
Thank you for registering at % 2 $s . Your account is pending for approval by the administrator .
2018-10-14 11:57:28 -04:00
Your login details are as follows :
Site Location : % 3 $s
Login Name : % 4 $s
Password : % 5 $s
' ,
2019-10-10 19:21:41 -04:00
$user [ 'username' ],
$sitename ,
$siteurl ,
$user [ 'nickname' ],
$password
2018-10-14 11:57:28 -04:00
));
2017-12-03 22:27:49 -05:00
2020-02-01 14:08:54 -05:00
$email = DI :: emailer ()
2020-02-04 15:04:08 -05:00
-> newSystemMail ()
2020-02-02 03:22:30 -05:00
-> withMessage ( DI :: l10n () -> t ( 'Registration at %s' , $sitename ), $body )
2020-02-04 15:04:08 -05:00
-> forUser ( $user )
2020-02-02 03:22:30 -05:00
-> withRecipient ( $user [ 'email' ])
-> build ();
2020-02-01 14:08:54 -05:00
return DI :: emailer () -> send ( $email );
2017-12-03 22:27:49 -05:00
}
/**
2020-01-19 01:05:23 -05:00
* Sends registration confirmation
2017-12-03 22:27:49 -05:00
*
* It ' s here as a function because the mail is sent from different parts
*
2020-07-21 02:27:05 -04: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 14:59:39 -05:00
*
2017-12-03 22:27:49 -05:00
* @ return NULL | boolean from notification () and email () inherited
2020-07-21 02:27:05 -04:00
* @ throws HTTPException\InternalServerErrorException
2017-12-03 22:27:49 -05:00
*/
2020-07-21 02:27:05 -04:00
public static function sendRegisterOpenEmail ( L10n $l10n , $user , $sitename , $siteurl , $password )
2017-12-03 22:27:49 -05:00
{
2019-12-28 17:12:01 -05:00
$preamble = Strings :: deindent ( $l10n -> t (
2019-10-10 19:21:41 -04:00
'
Dear % 1 $s ,
2017-12-03 22:27:49 -05:00
Thank you for registering at % 2 $s . Your account has been created .
2019-10-10 19:21:41 -04:00
' ,
$user [ 'username' ],
$sitename
2018-10-14 11:57:28 -04:00
));
2019-12-28 17:12:01 -05:00
$body = Strings :: deindent ( $l10n -> t (
2019-10-10 19:21:41 -04:00
'
2017-12-03 22:27:49 -05:00
The login details are as follows :
2018-04-04 15:56:34 -04:00
Site Location : % 3 $s
Login Name : % 1 $s
Password : % 5 $s
2018-04-02 12:40:52 -04:00
You may change your password from your account " Settings " page after logging
2017-12-03 22:27:49 -05: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 16:51:32 -05:00
' . "\x28" . ' on the " Profiles " page ' . "\x29" . ' so that other people can easily find you .
2017-12-03 22:27:49 -05:00
We recommend setting your full name , adding a profile photo ,
2018-04-02 12:40:52 -04:00
adding some profile " keywords " ' . "\x28" . ' very useful in making new friends ' . "\x29" . ' - and
2017-12-03 22:27:49 -05: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 .
2018-04-04 15:56:34 -04:00
If you ever want to delete your account , you can do so at % 3 $s / removeme
2017-12-03 22:27:49 -05:00
2018-10-14 11:57:28 -04:00
Thank you and welcome to % 2 $s . ' ,
2019-10-10 19:21:41 -04:00
$user [ 'nickname' ],
$sitename ,
$siteurl ,
$user [ 'username' ],
$password
2018-10-14 11:57:28 -04:00
));
2017-12-03 22:27:49 -05:00
2020-02-01 14:08:54 -05:00
$email = DI :: emailer ()
2020-02-04 15:04:08 -05:00
-> newSystemMail ()
2020-02-02 03:22:30 -05:00
-> withMessage ( DI :: l10n () -> t ( 'Registration details for %s' , $sitename ), $preamble , $body )
2020-02-04 15:04:08 -05:00
-> forUser ( $user )
2020-02-02 03:22:30 -05:00
-> withRecipient ( $user [ 'email' ])
-> build ();
2020-02-01 14:08:54 -05:00
return DI :: emailer () -> send ( $email );
2017-12-03 22:27:49 -05:00
}
2017-11-20 11:14:35 -05:00
/**
2020-02-21 17:12:07 -05:00
* @ param int $uid user to remove
2019-01-06 16:06:53 -05:00
* @ return bool
2020-07-21 02:27:05 -04:00
* @ throws HTTPException\InternalServerErrorException
2017-11-20 11:14:35 -05:00
*/
2020-02-21 17:12:07 -05:00
public static function remove ( int $uid )
2017-11-19 16:55:28 -05:00
{
2021-01-29 18:41:42 -05:00
if ( empty ( $uid )) {
2018-11-24 20:58:41 -05:00
return false ;
2017-11-19 16:55:28 -05:00
}
2018-10-29 17:20:46 -04:00
Logger :: log ( 'Removing user: ' . $uid );
2017-11-19 16:55:28 -05:00
2018-07-20 08:19:26 -04:00
$user = DBA :: selectFirst ( 'user' , [], [ 'uid' => $uid ]);
2017-11-19 16:55:28 -05:00
2018-11-24 20:56:38 -05:00
Hook :: callAll ( 'remove_user' , $user );
2017-11-19 16:55:28 -05:00
// save username (actually the nickname as it is guaranteed
// unique), so it cannot be re-registered in the future.
2018-07-20 08:19:26 -04:00
DBA :: insert ( 'userd' , [ 'username' => $user [ 'nickname' ]]);
2017-11-19 16:55:28 -05:00
2021-03-20 05:56:35 -04:00
// Remove all personal settings, especially connector settings
DBA :: delete ( 'pconfig' , [ 'uid' => $uid ]);
2020-08-16 04:39:04 -04:00
// The user and related data will be deleted in Friendica\Worker\ExpireAndRemoveUsers
2018-11-24 20:59:18 -05:00
DBA :: update ( 'user' , [ 'account_removed' => true , 'account_expires_on' => DateTimeFormat :: utc ( 'now + 7 day' )], [ 'uid' => $uid ]);
2019-06-10 10:19:24 -04:00
Worker :: add ( PRIORITY_HIGH , 'Notifier' , Delivery :: REMOVAL , $uid );
2017-11-19 16:55:28 -05:00
// Send an update to the directory
2018-08-02 01:21:01 -04:00
$self = DBA :: selectFirst ( 'contact' , [ 'url' ], [ 'uid' => $uid , 'self' => true ]);
2018-11-24 20:58:11 -05:00
Worker :: add ( PRIORITY_LOW , 'Directory' , $self [ 'url' ]);
2017-11-19 16:55:28 -05:00
2018-08-25 17:48:50 -04:00
// Remove the user relevant data
2019-02-03 18:55:46 -05:00
Worker :: add ( PRIORITY_NEGLIGIBLE , 'RemoveUser' , $uid );
2018-08-25 17:48:50 -04:00
2018-11-24 20:58:41 -05:00
return true ;
2017-11-19 16:55:28 -05:00
}
2018-11-07 18:22:15 -05: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 16:06:53 -05:00
* [
* [
* 'uid' => 1 ,
* 'username' => 'maxmuster' ,
* 'nickname' => 'Max Mustermann'
* ],
* [
* 'uid' => 2 ,
* 'username' => 'johndoe' ,
* 'nickname' => 'John Doe'
* ]
* ]
* @ throws Exception
2018-11-07 18:22:15 -05:00
*/
public static function identities ( $uid )
{
2021-06-19 02:27:25 -04:00
if ( empty ( $uid )) {
return [];
}
2018-11-07 18:22:15 -05:00
$identities = [];
$user = DBA :: selectFirst ( 'user' , [ 'uid' , 'nickname' , 'username' , 'parent-uid' ], [ 'uid' => $uid ]);
if ( ! DBA :: isResult ( $user )) {
return $identities ;
}
if ( $user [ 'parent-uid' ] == 0 ) {
// First add our own entry
2019-10-10 19:21:41 -04:00
$identities = [[
'uid' => $user [ 'uid' ],
2018-11-07 18:22:15 -05:00
'username' => $user [ 'username' ],
2019-10-10 19:21:41 -04:00
'nickname' => $user [ 'nickname' ]
]];
2018-11-07 18:22:15 -05:00
// Then add all the children
2019-10-10 19:21:41 -04:00
$r = DBA :: select (
'user' ,
[ 'uid' , 'username' , 'nickname' ],
[ 'parent-uid' => $user [ 'uid' ], 'account_removed' => false ]
);
2018-11-07 18:22:15 -05:00
if ( DBA :: isResult ( $r )) {
$identities = array_merge ( $identities , DBA :: toArray ( $r ));
}
} else {
// First entry is our parent
2019-10-10 19:21:41 -04:00
$r = DBA :: select (
'user' ,
[ 'uid' , 'username' , 'nickname' ],
[ 'uid' => $user [ 'parent-uid' ], 'account_removed' => false ]
);
2018-11-07 18:22:15 -05:00
if ( DBA :: isResult ( $r )) {
$identities = DBA :: toArray ( $r );
}
// Then add all siblings
2019-10-10 19:21:41 -04:00
$r = DBA :: select (
'user' ,
[ 'uid' , 'username' , 'nickname' ],
[ 'parent-uid' => $user [ 'parent-uid' ], 'account_removed' => false ]
);
2018-11-07 18:22:15 -05:00
if ( DBA :: isResult ( $r )) {
$identities = array_merge ( $identities , DBA :: toArray ( $r ));
}
}
2019-10-10 19:21:41 -04:00
$r = DBA :: p (
" SELECT `user`.`uid`, `user`.`username`, `user`.`nickname`
2018-11-07 18:22:15 -05:00
FROM `manage`
INNER JOIN `user` ON `manage` . `mid` = `user` . `uid`
WHERE `user` . `account_removed` = 0 AND `manage` . `uid` = ? " ,
$user [ 'uid' ]
);
if ( DBA :: isResult ( $r )) {
$identities = array_merge ( $identities , DBA :: toArray ( $r ));
}
return $identities ;
}
2019-04-22 08:00:17 -04:00
2021-07-24 07:49:11 -04:00
/**
* Check if the given user id has delegations or is delegated
*
2021-09-10 09:05:16 -04:00
* @ param int $uid
* @ return bool
2021-07-24 07:49:11 -04:00
*/
public static function hasIdentities ( int $uid ) : bool
{
if ( empty ( $uid )) {
return false ;
}
$user = DBA :: selectFirst ( 'user' , [ 'parent-uid' ], [ 'uid' => $uid , 'account_removed' => false ]);
if ( ! DBA :: isResult ( $user )) {
return false ;
}
if ( $user [ 'parent-uid' ] != 0 ) {
return true ;
}
if ( DBA :: exists ( 'user' , [ 'parent-uid' => $uid , 'account_removed' => false ])) {
return true ;
}
2021-07-24 09:24:26 -04:00
if ( DBA :: exists ( 'manage' , [ 'uid' => $uid ])) {
2021-07-24 07:49:11 -04:00
return true ;
}
return false ;
}
2019-04-22 08:00:17 -04:00
/**
* Returns statistical information about the current users of this node
*
* @ return array
*
* @ throws Exception
*/
public static function getStatistics ()
{
$statistics = [
'total_users' => 0 ,
'active_users_halfyear' => 0 ,
'active_users_monthly' => 0 ,
2020-07-12 17:53:17 -04:00
'active_users_weekly' => 0 ,
2019-04-22 08:00:17 -04:00
];
2020-04-24 07:04:50 -04:00
$userStmt = DBA :: select ( 'owner-view' , [ 'uid' , 'login_date' , 'last-item' ],
[ " `verified` AND `login_date` > ? AND NOT `blocked`
AND NOT `account_removed` AND NOT `account_expired` " ,
DBA :: NULL_DATETIME ]);
2019-04-22 08:00:17 -04:00
if ( ! DBA :: isResult ( $userStmt )) {
return $statistics ;
}
$halfyear = time () - ( 180 * 24 * 60 * 60 );
$month = time () - ( 30 * 24 * 60 * 60 );
2020-07-12 17:53:17 -04:00
$week = time () - ( 7 * 24 * 60 * 60 );
2019-04-22 08:00:17 -04:00
while ( $user = DBA :: fetch ( $userStmt )) {
$statistics [ 'total_users' ] ++ ;
2019-10-10 19:21:41 -04:00
if (( strtotime ( $user [ 'login_date' ]) > $halfyear ) || ( strtotime ( $user [ 'last-item' ]) > $halfyear )
) {
2019-04-22 08:00:17 -04:00
$statistics [ 'active_users_halfyear' ] ++ ;
}
2019-10-10 19:21:41 -04:00
if (( strtotime ( $user [ 'login_date' ]) > $month ) || ( strtotime ( $user [ 'last-item' ]) > $month )
) {
2019-04-22 08:00:17 -04:00
$statistics [ 'active_users_monthly' ] ++ ;
}
2020-07-12 17:53:17 -04:00
if (( strtotime ( $user [ 'login_date' ]) > $week ) || ( strtotime ( $user [ 'last-item' ]) > $week )
) {
$statistics [ 'active_users_weekly' ] ++ ;
}
2019-04-22 08:00:17 -04:00
}
2020-04-28 01:55:17 -04:00
DBA :: close ( $userStmt );
2019-04-22 08:00:17 -04:00
return $statistics ;
}
2020-02-25 16:16:27 -05: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 )
2020-02-25 17:22:47 -05:00
* @ param string $type The type of users , which should get ( all , bocked , removed )
2020-02-25 16:16:27 -05:00
* @ param string $order Order of the user list ( Default is 'contact.name' )
2020-04-24 07:04:50 -04:00
* @ param bool $descending Order direction ( Default is ascending )
2020-02-25 16:16:27 -05:00
*
* @ return array The list of the users
* @ throws Exception
*/
2020-04-24 07:04:50 -04:00
public static function getList ( $start = 0 , $count = Pager :: ITEMS_PER_PAGE , $type = 'all' , $order = 'name' , bool $descending = false )
2020-02-25 16:16:27 -05:00
{
2020-04-24 07:04:50 -04:00
$param = [ 'limit' => [ $start , $count ], 'order' => [ $order => $descending ]];
$condition = [];
2020-02-25 17:22:47 -05:00
switch ( $type ) {
case 'active' :
2020-07-06 15:26:39 -04:00
$condition [ 'account_removed' ] = false ;
2020-04-24 07:04:50 -04:00
$condition [ 'blocked' ] = false ;
2020-02-25 17:22:47 -05:00
break ;
case 'blocked' :
2020-11-08 02:26:12 -05:00
$condition [ 'account_removed' ] = false ;
2020-04-24 07:04:50 -04:00
$condition [ 'blocked' ] = true ;
2020-11-08 02:26:12 -05:00
$condition [ 'verified' ] = true ;
2020-02-25 17:22:47 -05:00
break ;
case 'removed' :
2020-04-24 07:04:50 -04:00
$condition [ 'account_removed' ] = true ;
2020-02-25 17:22:47 -05:00
break ;
}
2020-04-24 07:04:50 -04:00
return DBA :: selectToArray ( 'owner-view' , [], $condition , $param );
2020-02-25 16:16:27 -05:00
}
2017-11-19 16:55:28 -05:00
}