2017-11-19 22:55:28 +01:00
< ? php
2019-10-11 01:21:41 +02:00
2017-11-19 22:55:28 +01:00
/**
* @ file src / Model / User . php
2020-01-19 07:05:23 +01:00
* This file includes the User class with user related database functions
2017-11-19 22:55:28 +01:00
*/
2019-10-11 01:21:41 +02:00
2017-11-19 22:55:28 +01:00
namespace Friendica\Model ;
2018-10-19 17:26:48 +02:00
use DivineOmega\PasswordExposed ;
2018-07-20 04:15:21 +02:00
use Exception ;
2018-11-25 02:56:38 +01:00
use Friendica\Core\Hook ;
2018-10-29 22:20:46 +01:00
use Friendica\Core\Logger ;
2018-08-11 22:40:44 +02:00
use Friendica\Core\Protocol ;
2017-11-19 22:55:28 +01:00
use Friendica\Core\System ;
use Friendica\Core\Worker ;
2018-07-20 14:19:26 +02:00
use Friendica\Database\DBA ;
2019-12-16 00:47:24 +01:00
use Friendica\DI ;
2019-07-22 13:56:36 +02:00
use Friendica\Model\TwoFactor\AppSpecificPassword ;
2017-12-07 14:56:11 +01:00
use Friendica\Object\Image ;
2017-12-30 17:51:49 +01:00
use Friendica\Util\Crypto ;
2018-01-27 03:38:34 +01:00
use Friendica\Util\DateTimeFormat ;
2019-10-18 03:26:15 +02:00
use Friendica\Util\Images ;
2018-01-27 05:09:48 +01:00
use Friendica\Util\Network ;
2018-11-08 16:14:37 +01:00
use Friendica\Util\Strings ;
2019-06-10 16:19:24 +02:00
use Friendica\Worker\Delivery ;
2018-01-25 03:08:45 +01:00
use LightOpenID ;
2017-11-19 22:55:28 +01:00
/**
2020-01-19 07:05:23 +01:00
* This class handles User related functions
2017-11-19 22:55:28 +01:00
*/
class User
{
2019-01-06 18:37:48 +01:00
/**
2019-01-14 14:45:37 +01:00
* Page / profile types
2019-01-06 18:37:48 +01:00
*
2019-01-14 14:45:37 +01: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 18:37:48 +01:00
* write access to wall and comments ( no email and not included in page owner ' s ACL lists )
2019-01-14 14:45:37 +01:00
* PAGE_FLAGS_FREELOVE automatically approves all friend requests as full friends ( Contact :: FRIEND ) .
2019-01-06 18:37:48 +01: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 23:08:35 +01: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 ;
/**
* @ }
*/
2018-10-14 17:34:34 +02:00
/**
* Returns true if a user record exists with the provided id
*
* @ param integer $uid
* @ return boolean
2019-01-06 22:06:53 +01:00
* @ throws Exception
2018-10-14 17:34:34 +02:00
*/
public static function exists ( $uid )
{
return DBA :: exists ( 'user' , [ 'uid' => $uid ]);
}
2018-09-28 05:56:41 +02:00
/**
2018-10-15 17:58:52 +02:00
* @ param integer $uid
2019-05-01 00:14:06 +02:00
* @ param array $fields
2018-10-15 17:58:52 +02:00
* @ return array | boolean User record if it exists , false otherwise
2019-01-06 22:06:53 +01:00
* @ throws Exception
2018-10-15 17:58:52 +02:00
*/
2019-05-01 00:14:06 +02:00
public static function getById ( $uid , array $fields = [])
2018-10-15 17:58:52 +02:00
{
2019-05-01 00:14:06 +02:00
return DBA :: selectFirst ( 'user' , $fields , [ 'uid' => $uid ]);
}
2019-10-20 13:00:08 +02: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-05-01 00:14:06 +02: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 17:58:52 +02:00
}
/**
2020-01-19 07:05:23 +01:00
* Returns the user id of a given profile URL
2018-09-28 05:56:41 +02:00
*
2018-10-15 17:58:52 +02:00
* @ param string $url
2018-09-28 05:56:41 +02:00
*
* @ return integer user id
2019-01-06 22:06:53 +01:00
* @ throws Exception
2018-09-28 05:56:41 +02:00
*/
public static function getIdForURL ( $url )
{
2018-11-08 17:28:29 +01:00
$self = DBA :: selectFirst ( 'contact' , [ 'uid' ], [ 'nurl' => Strings :: normaliseLink ( $url ), 'self' => true ]);
2018-09-28 05:56:41 +02:00
if ( ! DBA :: isResult ( $self )) {
return false ;
} else {
return $self [ 'uid' ];
}
}
2019-05-04 10:05:21 +02:00
/**
2019-05-05 10:00:28 +02:00
* Get a user based on its email
2019-05-04 10:05:21 +02: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 ]);
}
2017-12-17 22:22:39 +01:00
/**
2020-01-19 07:05:23 +01:00
* Get owner data by user id
2017-12-17 22:22:39 +01:00
*
* @ param int $uid
2019-06-19 19:05:29 +02:00
* @ param boolean $check_valid Test if data is invalid and correct it
2017-12-17 22:22:39 +01:00
* @ return boolean | array
2019-01-06 22:06:53 +01:00
* @ throws Exception
2017-12-17 22:22:39 +01:00
*/
2019-10-11 01:21:41 +02:00
public static function getOwnerDataById ( $uid , $check_valid = true )
{
$r = DBA :: fetchFirst (
" SELECT
2017-12-17 22:22:39 +01:00
`contact` .* ,
`user` . `prvkey` AS `uprvkey` ,
`user` . `timezone` ,
`user` . `nickname` ,
`user` . `sprvkey` ,
`user` . `spubkey` ,
`user` . `page-flags` ,
`user` . `account-type` ,
2019-02-11 21:30:08 +01:00
`user` . `prvnets` ,
2019-10-30 07:50:20 +01:00
`user` . `account_removed` ,
`user` . `hidewall`
2017-12-17 22:22:39 +01:00
FROM `contact`
INNER JOIN `user`
ON `user` . `uid` = `contact` . `uid`
WHERE `contact` . `uid` = ?
2017-12-18 10:21:47 +01:00
AND `contact` . `self`
2017-12-17 22:22:39 +01:00
LIMIT 1 " ,
$uid
);
2018-07-21 14:46:04 +02:00
if ( ! DBA :: isResult ( $r )) {
2017-12-17 22:22:39 +01:00
return false ;
}
2018-12-22 21:12:32 +01:00
if ( empty ( $r [ 'nickname' ])) {
return false ;
}
2019-06-19 19:05:29 +02:00
if ( ! $check_valid ) {
return $r ;
}
2018-12-22 21:12:32 +01:00
// Check if the returned data is valid, otherwise fix it. See issue #6122
2019-06-19 19:05:29 +02:00
// Check for correct url and normalised nurl
2019-12-30 23:00:08 +01:00
$url = DI :: baseUrl () . '/profile/' . $r [ 'nickname' ];
2019-06-19 19:05:29 +02:00
$repair = ( $r [ 'url' ] != $url ) || ( $r [ 'nurl' ] != Strings :: normaliseLink ( $r [ 'url' ]));
if ( ! $repair ) {
// Check if "addr" is present and correct
2019-12-30 23:00:08 +01:00
$addr = $r [ 'nickname' ] . '@' . substr ( DI :: baseUrl (), strpos ( DI :: baseUrl (), '://' ) + 3 );
2019-06-19 19:05:29 +02:00
$repair = ( $addr != $r [ 'addr' ]);
}
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 )) {
$repair = empty ( $r [ 'avatar' ]) || ! strpos ( $r [ 'photo' ], $avatar [ 'resource-id' ]);
}
}
2018-12-22 21:12:32 +01:00
2019-06-19 19:05:29 +02:00
if ( $repair ) {
2018-12-22 21:12:32 +01:00
Contact :: updateSelfFromUserID ( $uid );
2019-06-19 19:05:29 +02:00
// Return the corrected data and avoid a loop
$r = self :: getOwnerDataById ( $uid , false );
2018-12-22 21:12:32 +01:00
}
2017-12-18 10:21:47 +01:00
return $r ;
2017-12-17 22:22:39 +01:00
}
2018-06-18 22:36:34 +02:00
/**
2020-01-19 07:05:23 +01:00
* Get owner data by nick name
2018-06-18 22:36:34 +02:00
*
* @ param int $nick
* @ return boolean | array
2019-01-06 22:06:53 +01:00
* @ throws Exception
2018-06-18 22:36:34 +02:00
*/
public static function getOwnerDataByNick ( $nick )
{
2018-07-20 14:19:26 +02: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 07:05:49 +02:00
2018-07-21 14:46:04 +02:00
if ( ! DBA :: isResult ( $user )) {
2018-06-18 22:36:34 +02: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 07:05:49 +02:00
2018-06-18 22:36:34 +02:00
return self :: getOwnerDataById ( $user [ 'uid' ]);
}
2017-12-04 04:15:31 +01:00
/**
2020-01-19 07:05:23 +01:00
* Returns the default group for a given user and network
2017-12-09 19:31:00 +01:00
*
* @ param int $uid User id
* @ param string $network network name
*
* @ return int group id
2019-01-06 22:06:53 +01:00
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
2017-12-09 19:31:00 +01:00
*/
public static function getDefaultGroup ( $uid , $network = '' )
{
$default_group = 0 ;
2018-08-11 22:40:44 +02:00
if ( $network == Protocol :: OSTATUS ) {
2020-01-18 16:50:57 +01:00
$default_group = DI :: pConfig () -> get ( $uid , " ostatus " , " default_group " );
2017-12-09 19:31:00 +01:00
}
if ( $default_group != 0 ) {
return $default_group ;
}
2018-07-20 14:19:26 +02:00
$user = DBA :: selectFirst ( 'user' , [ 'def_gid' ], [ 'uid' => $uid ]);
2017-12-09 19:31:00 +01:00
2018-07-21 14:46:04 +02:00
if ( DBA :: isResult ( $user )) {
2017-12-09 19:31:00 +01:00
$default_group = $user [ " def_gid " ];
}
return $default_group ;
}
/**
2018-02-09 06:08:01 +01:00
* Authenticate a user with a clear text password
2017-12-04 04:15:31 +01:00
*
2019-07-22 13:56:36 +02:00
* @ param mixed $user_info
2017-12-04 04:15:31 +01:00
* @ param string $password
2019-07-22 13:56:36 +02:00
* @ param bool $third_party
2018-02-09 06:08:01 +01:00
* @ return int | boolean
* @ deprecated since version 3.6
2019-07-22 13:56:36 +02:00
* @ see User :: getIdFromPasswordAuthentication ()
2017-12-04 04:15:31 +01:00
*/
2019-07-22 13:56:36 +02:00
public static function authenticate ( $user_info , $password , $third_party = false )
2017-11-26 20:25:25 +01:00
{
2018-02-09 06:08:01 +01:00
try {
2019-07-22 13:56:36 +02:00
return self :: getIdFromPasswordAuthentication ( $user_info , $password , $third_party );
2018-02-09 06:08:01 +01:00
} catch ( Exception $ex ) {
return false ;
2017-11-26 20:25:25 +01:00
}
2018-02-09 06:08:01 +01:00
}
2017-11-26 20:25:25 +01:00
2018-02-09 06:08:01 +01:00
/**
2020-01-19 07:05:23 +01:00
* Authenticate a user with a clear text password
*
2018-02-09 06:08:01 +01:00
* Returns the user id associated with a successful password authentication
*
2019-07-22 13:56:36 +02:00
* @ param mixed $user_info
2018-02-09 06:08:01 +01:00
* @ param string $password
2019-07-22 13:56:36 +02:00
* @ param bool $third_party
2018-02-09 06:08:01 +01:00
* @ return int User Id if authentication is successful
* @ throws Exception
*/
2019-07-22 13:56:36 +02:00
public static function getIdFromPasswordAuthentication ( $user_info , $password , $third_party = false )
2018-02-09 06:08:01 +01:00
{
$user = self :: getAuthenticationInfo ( $user_info );
2017-11-26 20:25:25 +01:00
2020-01-18 16:50:57 +01:00
if ( $third_party && DI :: pConfig () -> get ( $user [ 'uid' ], '2fa' , 'verified' )) {
2019-07-22 13:56:36 +02: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 11:12:32 +02:00
//Legacy hash that has not been replaced by a new hash yet
2018-04-08 12:28:04 +02:00
if ( self :: hashPasswordLegacy ( $password ) === $user [ 'password' ]) {
2019-01-01 07:06:28 +01:00
self :: updatePasswordHashed ( $user [ 'uid' ], self :: hashPassword ( $password ));
2018-04-08 12:28:04 +02:00
2018-04-15 11:12:32 +02: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 07:06:28 +01:00
self :: updatePasswordHashed ( $user [ 'uid' ], self :: hashPassword ( $password ));
2018-04-15 11:12:32 +02:00
2018-04-08 12:28:04 +02:00
return $user [ 'uid' ];
}
2018-04-08 16:02:25 +02:00
} elseif ( password_verify ( $password , $user [ 'password' ])) {
2018-04-15 11:12:32 +02:00
//New password hash
2018-04-08 16:02:25 +02:00
if ( password_needs_rehash ( $user [ 'password' ], PASSWORD_DEFAULT )) {
2019-01-01 07:06:28 +01:00
self :: updatePasswordHashed ( $user [ 'uid' ], self :: hashPassword ( $password ));
2018-04-08 16:02:25 +02:00
}
return $user [ 'uid' ];
2017-11-26 20:25:25 +01:00
}
2020-01-18 20:52:34 +01:00
throw new Exception ( DI :: l10n () -> t ( 'Login failed' ));
2018-02-09 06:08:01 +01: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
* @ throws Exception
*/
private static function getAuthenticationInfo ( $user_info )
{
2018-02-14 06:05:00 +01:00
$user = null ;
2018-02-09 06:08:01 +01: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-11 01:21:41 +02:00
if (
! isset ( $user [ 'uid' ])
2018-02-09 06:08:01 +01:00
|| ! isset ( $user [ 'password' ])
2018-04-15 10:51:22 +02:00
|| ! isset ( $user [ 'legacy_password' ])
2018-02-09 06:08:01 +01:00
) {
2020-01-18 20:52:34 +01:00
throw new Exception ( DI :: l10n () -> t ( 'Not enough information to authenticate' ));
2018-02-09 06:08:01 +01:00
}
} elseif ( is_int ( $user_info ) || is_string ( $user_info )) {
if ( is_int ( $user_info )) {
2019-10-11 01:21:41 +02:00
$user = DBA :: selectFirst (
'user' ,
[ 'uid' , 'password' , 'legacy_password' ],
2018-02-09 06:08:01 +01:00
[
'uid' => $user_info ,
'blocked' => 0 ,
'account_expired' => 0 ,
'account_removed' => 0 ,
'verified' => 1
]
);
} else {
2018-06-19 23:33:07 +02:00
$fields = [ 'uid' , 'password' , 'legacy_password' ];
2019-10-11 01:21:41 +02:00
$condition = [
" (`email` = ? OR `username` = ? OR `nickname` = ?)
2018-06-19 23:33:07 +02:00
AND NOT `blocked` AND NOT `account_expired` AND NOT `account_removed` AND `verified` " ,
2019-10-11 01:21:41 +02:00
$user_info , $user_info , $user_info
];
2018-07-20 14:19:26 +02:00
$user = DBA :: selectFirst ( 'user' , $fields , $condition );
2018-02-09 06:08:01 +01:00
}
2018-07-21 14:46:04 +02:00
if ( ! DBA :: isResult ( $user )) {
2020-01-18 20:52:34 +01:00
throw new Exception ( DI :: l10n () -> t ( 'User not found' ));
2018-02-09 06:08:01 +01:00
}
}
return $user ;
2017-11-26 20:25:25 +01:00
}
2018-01-20 04:49:06 +01:00
/**
* Generates a human - readable random password
*
* @ return string
*/
public static function generateNewPassword ()
{
2019-10-11 01:21:41 +02:00
return ucfirst ( Strings :: getRandomName ( 8 )) . random_int ( 1000 , 9999 );
2018-01-20 04:49:06 +01:00
}
2018-03-21 06:33:35 +01:00
/**
* Checks if the provided plaintext password has been exposed or not
*
* @ param string $password
* @ return bool
2019-10-13 03:06:47 +02:00
* @ throws Exception
2018-03-21 06:33:35 +01:00
*/
2018-03-21 07:14:43 +01:00
public static function isPasswordExposed ( $password )
2018-03-21 06:33:35 +01:00
{
2018-10-19 17:26:48 +02:00
$cache = new \DivineOmega\DOFileCachePSR6\CacheItemPool ();
$cache -> changeConfig ([
'cacheDirectory' => get_temppath () . '/password-exposed-cache/' ,
]);
2019-10-13 03:06:47 +02:00
try {
$passwordExposedChecker = new PasswordExposed\PasswordExposedChecker ( null , $cache );
return $passwordExposedChecker -> passwordExposed ( $password ) === PasswordExposed\PasswordStatus :: EXPOSED ;
} catch ( \Exception $e ) {
Logger :: error ( 'Password Exposed Exception: ' . $e -> getMessage (), [
'code' => $e -> getCode (),
'file' => $e -> getFile (),
'line' => $e -> getLine (),
'trace' => $e -> getTraceAsString ()
]);
2018-10-19 17:26:48 +02:00
2019-10-13 03:06:47 +02:00
return false ;
}
2018-03-21 06:33:35 +01:00
}
2018-01-20 04:49:06 +01:00
/**
2018-01-21 04:29:03 +01:00
* Legacy hashing function , kept for password migration purposes
2018-01-20 04:49:06 +01:00
*
* @ param string $password
* @ return string
*/
2018-01-21 04:29:03 +01:00
private static function hashPasswordLegacy ( $password )
2018-01-20 04:49:06 +01:00
{
return hash ( 'whirlpool' , $password );
}
2018-01-21 04:29:03 +01:00
/**
* Global user password hashing function
*
* @ param string $password
* @ return string
2019-01-01 07:08:55 +01:00
* @ throws Exception
2018-01-21 04:29:03 +01:00
*/
public static function hashPassword ( $password )
{
2018-04-19 04:49:14 +02:00
if ( ! trim ( $password )) {
2020-01-18 20:52:34 +01:00
throw new Exception ( DI :: l10n () -> t ( 'Password can\'t be empty' ));
2018-04-19 04:49:14 +02:00
}
2018-01-21 04:29:03 +01:00
return password_hash ( $password , PASSWORD_DEFAULT );
}
2018-01-20 04:49:06 +01:00
/**
* Updates a user row with a new plaintext password
*
* @ param int $uid
* @ param string $password
* @ return bool
2019-01-01 07:08:55 +01:00
* @ throws Exception
2018-01-20 04:49:06 +01:00
*/
public static function updatePassword ( $uid , $password )
{
2019-01-01 07:08:55 +01:00
$password = trim ( $password );
if ( empty ( $password )) {
2020-01-18 20:52:34 +01:00
throw new Exception ( DI :: l10n () -> t ( 'Empty passwords are not allowed.' ));
2019-01-01 07:08:55 +01:00
}
2020-01-19 21:21:13 +01:00
if ( ! DI :: config () -> get ( 'system' , 'disable_password_exposed' , false ) && self :: isPasswordExposed ( $password )) {
2020-01-18 20:52:34 +01:00
throw new Exception ( DI :: l10n () -> t ( 'The new password has been exposed in a public data dump, please choose another.' ));
2019-01-01 07:08:55 +01:00
}
$allowed_characters = '!"#$%&\'()*+,-./;<=>?@[\]^_`{|}~' ;
if ( ! preg_match ( '/^[a-z0-9' . preg_quote ( $allowed_characters , '/' ) . ']+$/i' , $password )) {
2020-01-18 20:52:34 +01:00
throw new Exception ( DI :: l10n () -> t ( 'The password can\'t contain accentuated letters, white spaces or colons (:)' ));
2019-01-01 07:08:55 +01:00
}
2018-01-20 04:49:06 +01: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 22:06:53 +01:00
* @ throws Exception
2018-01-20 04:49:06 +01:00
*/
private static function updatePasswordHashed ( $uid , $pasword_hashed )
{
2018-01-21 00:15:55 +01:00
$fields = [
'password' => $pasword_hashed ,
'pwdreset' => null ,
2018-01-21 04:29:03 +01:00
'pwdreset_time' => null ,
2018-04-15 10:51:22 +02:00
'legacy_password' => false
2018-01-21 00:15:55 +01:00
];
2018-07-20 14:19:26 +02:00
return DBA :: update ( 'user' , $fields , [ 'uid' => $uid ]);
2018-01-20 04:49:06 +01:00
}
2018-07-06 15:32:56 +02:00
/**
2020-01-19 07:05:23 +01:00
* Checks if a nickname is in the list of the forbidden nicknames
2018-07-06 15:32:56 +02: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
2019-01-06 22:06:53 +01:00
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
2018-07-06 15:32:56 +02:00
*/
public static function isNicknameBlocked ( $nickname )
{
2020-01-19 21:21:13 +01:00
$forbidden_nicknames = DI :: config () -> get ( 'system' , 'forbidden_nicknames' , '' );
2018-07-23 03:18:21 +02:00
2018-07-06 15:32:56 +02:00
// if the config variable is empty return false
2018-07-23 03:18:21 +02:00
if ( empty ( $forbidden_nicknames )) {
2018-07-06 15:32:56 +02:00
return false ;
}
2018-07-23 03:18:21 +02:00
2018-07-06 15:32:56 +02:00
// check if the nickname is in the list of blocked nicknames
$forbidden = explode ( ',' , $forbidden_nicknames );
2018-07-06 15:49:27 +02:00
$forbidden = array_map ( 'trim' , $forbidden );
2018-07-06 15:32:56 +02:00
if ( in_array ( strtolower ( $nickname ), $forbidden )) {
return true ;
}
2018-07-23 03:18:21 +02:00
2018-07-06 15:32:56 +02:00
// else return false
return false ;
}
2017-12-04 04:27:49 +01:00
/**
2020-01-19 07:05:23 +01:00
* Catch - all user creation function
2017-12-04 04:27:49 +01: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 07:08:55 +01:00
* @ param array $data
* @ return array
* @ throws \ErrorException
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
2019-01-06 22:06:53 +01:00
* @ throws \ImagickException
2019-01-01 07:08:55 +01:00
* @ throws Exception
2017-12-04 04:27:49 +01:00
*/
public static function create ( array $data )
{
2017-12-13 02:43:21 +01:00
$return = [ 'user' => null , 'password' => '' ];
2017-12-04 04:27:49 +01:00
2020-01-19 21:21:13 +01:00
$using_invites = DI :: config () -> get ( 'system' , 'invitation_only' );
2017-12-04 04:27:49 +01:00
2018-11-09 19:29:42 +01: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-23 03:18:21 +02: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 15:06:22 +01:00
$blocked = ! empty ( $data [ 'blocked' ]);
$verified = ! empty ( $data [ 'verified' ]);
2018-11-09 19:29:42 +01:00
$language = ! empty ( $data [ 'language' ]) ? Strings :: escapeTags ( trim ( $data [ 'language' ])) : 'en' ;
2018-07-23 03:18:21 +02:00
2018-11-30 15:06:22 +01:00
$publish = ! empty ( $data [ 'profile_publish_reg' ]);
2020-01-19 21:21:13 +01:00
$netpublish = $publish && DI :: config () -> get ( 'system' , 'directory' );
2017-12-04 04:27:49 +01:00
if ( $password1 != $confirm ) {
2020-01-18 20:52:34 +01:00
throw new Exception ( DI :: l10n () -> t ( 'Passwords do not match. Password unchanged.' ));
2017-12-13 03:07:03 +01:00
} elseif ( $password1 != '' ) {
2017-12-04 04:27:49 +01:00
$password = $password1 ;
}
if ( $using_invites ) {
if ( ! $invite_id ) {
2020-01-18 20:52:34 +01:00
throw new Exception ( DI :: l10n () -> t ( 'An invitation is required.' ));
2017-12-04 04:27:49 +01:00
}
2017-12-13 03:07:03 +01:00
2018-10-14 17:57:28 +02:00
if ( ! Register :: existsByHash ( $invite_id )) {
2020-01-18 20:52:34 +01:00
throw new Exception ( DI :: l10n () -> t ( 'Invitation could not be verified.' ));
2017-12-04 04:27:49 +01:00
}
}
2019-10-24 22:23:26 +02:00
/// @todo Check if this part is really needed. We should have fetched all this data in advance
2018-07-23 03:18:21 +02:00
if ( empty ( $username ) || empty ( $email ) || empty ( $nickname )) {
2017-12-04 04:27:49 +01:00
if ( $openid_url ) {
2018-01-27 17:13:41 +01:00
if ( ! Network :: isUrlValid ( $openid_url )) {
2020-01-18 20:52:34 +01:00
throw new Exception ( DI :: l10n () -> t ( 'Invalid OpenID url' ));
2017-12-04 04:27:49 +01:00
}
$_SESSION [ 'register' ] = 1 ;
$_SESSION [ 'openid' ] = $openid_url ;
2019-12-16 00:47:24 +01:00
$openid = new LightOpenID ( DI :: baseUrl () -> getHostname ());
2017-12-04 04:27:49 +01:00
$openid -> identity = $openid_url ;
2019-12-30 23:00:08 +01:00
$openid -> returnUrl = DI :: baseUrl () . '/openid' ;
2018-01-15 14:05:12 +01:00
$openid -> required = [ 'namePerson/friendly' , 'contact/email' , 'namePerson' ];
$openid -> optional = [ 'namePerson/first' , 'media/image/aspect11' , 'media/image/default' ];
2017-12-04 04:27:49 +01:00
try {
$authurl = $openid -> authUrl ();
} catch ( Exception $e ) {
2020-01-18 20:52:34 +01: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-04 04:27:49 +01:00
}
2018-10-19 20:11:27 +02:00
System :: externalRedirect ( $authurl );
2017-12-04 04:27:49 +01:00
// NOTREACHED
}
2020-01-18 20:52:34 +01:00
throw new Exception ( DI :: l10n () -> t ( 'Please enter the required information.' ));
2017-12-04 04:27:49 +01:00
}
2018-01-27 17:13:41 +01:00
if ( ! Network :: isUrlValid ( $openid_url )) {
2017-12-04 04:27:49 +01:00
$openid_url = '' ;
}
// collapse multiple spaces in name
$username = preg_replace ( '/ +/' , ' ' , $username );
2020-01-19 21:21:13 +01: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 22:33:54 +02:00
2018-10-21 14:28:24 +02:00
if ( $username_min_length > $username_max_length ) {
2020-01-18 20:52:34 +01: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 14:28:24 +02:00
$tmp = $username_min_length ;
$username_min_length = $username_max_length ;
$username_max_length = $tmp ;
}
2018-10-20 22:33:54 +02:00
if ( mb_strlen ( $username ) < $username_min_length ) {
2020-01-18 20:53:01 +01: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 04:27:49 +01:00
}
2018-10-20 22:33:54 +02:00
if ( mb_strlen ( $username ) > $username_max_length ) {
2020-01-18 20:53:01 +01: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 04:27:49 +01:00
}
// So now we are just looking for a space in the full name.
2020-01-19 21:21:13 +01:00
$loose_reg = DI :: config () -> get ( 'system' , 'no_regfullname' );
2017-12-04 04:27:49 +01:00
if ( ! $loose_reg ) {
$username = mb_convert_case ( $username , MB_CASE_TITLE , 'UTF-8' );
2018-10-20 22:33:54 +02:00
if ( strpos ( $username , ' ' ) === false ) {
2020-01-18 20:52:34 +01:00
throw new Exception ( DI :: l10n () -> t ( " That doesn't appear to be your full (First Last) name. " ));
2017-12-04 04:27:49 +01:00
}
}
2018-01-27 17:13:41 +01:00
if ( ! Network :: isEmailDomainAllowed ( $email )) {
2020-01-18 20:52:34 +01:00
throw new Exception ( DI :: l10n () -> t ( 'Your email domain is not among those allowed on this site.' ));
2017-12-04 04:27:49 +01:00
}
2018-11-09 19:18:42 +01:00
if ( ! filter_var ( $email , FILTER_VALIDATE_EMAIL ) || ! Network :: isEmailDomainValid ( $email )) {
2020-01-18 20:52:34 +01:00
throw new Exception ( DI :: l10n () -> t ( 'Not a valid email address.' ));
2017-12-04 04:27:49 +01:00
}
2018-07-06 15:32:56 +02:00
if ( self :: isNicknameBlocked ( $nickname )) {
2020-01-18 20:52:34 +01:00
throw new Exception ( DI :: l10n () -> t ( 'The nickname was blocked from registration by the nodes admin.' ));
2018-07-06 15:32:56 +02:00
}
2017-12-04 04:27:49 +01:00
2020-01-19 21:21:13 +01:00
if ( DI :: config () -> get ( 'system' , 'block_extended_register' , false ) && DBA :: exists ( 'user' , [ 'email' => $email ])) {
2020-01-18 20:52:34 +01:00
throw new Exception ( DI :: l10n () -> t ( 'Cannot use that email.' ));
2017-12-13 03:07:03 +01:00
}
2017-12-04 04:27:49 +01: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 21:21:13 +01:00
if ( DI :: config () -> get ( 'config' , 'admin_email' ) && strlen ( $openid_url )) {
$adminlist = explode ( ',' , str_replace ( ' ' , '' , strtolower ( DI :: config () -> get ( 'config' , 'admin_email' ))));
2017-12-13 03:07:03 +01:00
if ( in_array ( strtolower ( $email ), $adminlist )) {
2020-01-18 20:52:34 +01:00
throw new Exception ( DI :: l10n () -> t ( 'Cannot use that email.' ));
2017-12-04 04:27:49 +01:00
}
}
$nickname = $data [ 'nickname' ] = strtolower ( $nickname );
2017-12-13 03:07:03 +01:00
if ( ! preg_match ( '/^[a-z0-9][a-z0-9\_]*$/' , $nickname )) {
2020-01-18 20:52:34 +01:00
throw new Exception ( DI :: l10n () -> t ( 'Your nickname can only contain a-z, 0-9 and _.' ));
2017-12-04 04:27:49 +01:00
}
2017-12-13 03:07:03 +01:00
// Check existing and deleted accounts for this nickname.
2019-10-11 01:21:41 +02:00
if (
DBA :: exists ( 'user' , [ 'nickname' => $nickname ])
2018-07-20 14:19:26 +02:00
|| DBA :: exists ( 'userd' , [ 'username' => $nickname ])
2017-12-13 03:07:03 +01:00
) {
2020-01-18 20:52:34 +01:00
throw new Exception ( DI :: l10n () -> t ( 'Nickname is already registered. Please choose another.' ));
2017-12-04 04:27:49 +01:00
}
2018-01-20 04:49:06 +01:00
$new_password = strlen ( $password ) ? $password : User :: generateNewPassword ();
$new_password_encoded = self :: hashPassword ( $new_password );
2017-12-04 04:27:49 +01:00
2017-12-13 02:43:21 +01:00
$return [ 'password' ] = $new_password ;
2017-12-04 04:27:49 +01:00
2017-12-30 17:51:49 +01:00
$keys = Crypto :: newKeypair ( 4096 );
2017-12-04 04:27:49 +01:00
if ( $keys === false ) {
2020-01-18 20:52:34 +01:00
throw new Exception ( DI :: l10n () -> t ( 'SERIOUS ERROR: Generation of security keys failed.' ));
2017-12-04 04:27:49 +01:00
}
$prvkey = $keys [ 'prvkey' ];
$pubkey = $keys [ 'pubkey' ];
// Create another keypair for signing/verifying salmon protocol messages.
2017-12-30 17:51:49 +01:00
$sres = Crypto :: newKeypair ( 512 );
2017-12-04 04:27:49 +01:00
$sprvkey = $sres [ 'prvkey' ];
$spubkey = $sres [ 'pubkey' ];
2018-07-20 14:19:26 +02:00
$insert_result = DBA :: insert ( 'user' , [
2018-09-27 13:52:15 +02:00
'guid' => System :: createUUID (),
2017-12-13 03:07:03 +01: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 08:27:27 +02:00
'language' => $language ,
2017-12-13 03:07:03 +01:00
'timezone' => 'UTC' ,
2018-01-27 03:38:34 +01:00
'register_date' => DateTimeFormat :: utcNow (),
2017-12-13 03:07:03 +01:00
'default-location' => ''
]);
if ( $insert_result ) {
2018-07-20 14:19:26 +02:00
$uid = DBA :: lastInsertId ();
$user = DBA :: selectFirst ( 'user' , [], [ 'uid' => $uid ]);
2017-12-04 04:27:49 +01:00
} else {
2020-01-18 20:52:34 +01:00
throw new Exception ( DI :: l10n () -> t ( 'An error occurred during registration. Please try again.' ));
2017-12-04 04:27:49 +01:00
}
2017-12-13 03:07:03 +01:00
if ( ! $uid ) {
2020-01-18 20:52:34 +01:00
throw new Exception ( DI :: l10n () -> t ( 'An error occurred during registration. Please try again.' ));
2017-12-04 04:27:49 +01:00
}
2017-12-13 03:07:03 +01: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 14:19:26 +02:00
$user_count = DBA :: count ( 'user' , [ 'nickname' => $nickname ]);
2017-12-13 03:07:03 +01:00
if ( $user_count > 1 ) {
2018-07-20 14:19:26 +02:00
DBA :: delete ( 'user' , [ 'uid' => $uid ]);
2017-12-04 04:27:49 +01:00
2020-01-18 20:52:34 +01:00
throw new Exception ( DI :: l10n () -> t ( 'Nickname is already registered. Please choose another.' ));
2017-12-13 03:07:03 +01:00
}
2017-12-13 02:43:21 +01:00
2018-07-20 14:19:26 +02:00
$insert_result = DBA :: insert ( 'profile' , [
2017-12-13 03:07:03 +01:00
'uid' => $uid ,
'name' => $username ,
2019-12-30 23:00:08 +01:00
'photo' => DI :: baseUrl () . " /photo/profile/ { $uid } .jpg " ,
'thumb' => DI :: baseUrl () . " /photo/avatar/ { $uid } .jpg " ,
2017-12-13 03:07:03 +01:00
'publish' => $publish ,
'net-publish' => $netpublish ,
]);
if ( ! $insert_result ) {
2018-07-20 14:19:26 +02:00
DBA :: delete ( 'user' , [ 'uid' => $uid ]);
2017-12-13 03:07:03 +01:00
2020-01-18 20:52:34 +01:00
throw new Exception ( DI :: l10n () -> t ( 'An error occurred creating your default profile. Please try again.' ));
2017-12-13 03:07:03 +01:00
}
2017-12-04 04:27:49 +01:00
2017-12-13 03:07:03 +01:00
// Create the self contact
if ( ! Contact :: createSelfFromUserId ( $uid )) {
2018-07-20 14:19:26 +02:00
DBA :: delete ( 'user' , [ 'uid' => $uid ]);
2017-12-13 02:43:21 +01:00
2020-01-18 20:52:34 +01:00
throw new Exception ( DI :: l10n () -> t ( 'An error occurred creating your self contact. Please try again.' ));
2017-12-13 03:07:03 +01:00
}
2017-12-04 04:27:49 +01:00
2017-12-13 03:07:03 +01: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 20:52:34 +01:00
$def_gid = Group :: create ( $uid , DI :: l10n () -> t ( 'Friends' ));
2017-12-13 03:07:03 +01:00
if ( ! $def_gid ) {
2018-07-20 14:19:26 +02:00
DBA :: delete ( 'user' , [ 'uid' => $uid ]);
2017-12-04 04:27:49 +01:00
2020-01-18 20:52:34 +01:00
throw new Exception ( DI :: l10n () -> t ( 'An error occurred creating your default contact group. Please try again.' ));
2017-12-13 03:07:03 +01:00
}
2017-12-04 04:27:49 +01:00
2017-12-13 03:07:03 +01:00
$fields = [ 'def_gid' => $def_gid ];
2020-01-19 21:21:13 +01:00
if ( DI :: config () -> get ( 'system' , 'newuser_private' ) && $def_gid ) {
2017-12-13 03:07:03 +01:00
$fields [ 'allow_gid' ] = '<' . $def_gid . '>' ;
2017-12-04 04:27:49 +01:00
}
2018-07-20 14:19:26 +02:00
DBA :: update ( 'user' , $fields , [ 'uid' => $uid ]);
2017-12-13 03:07:03 +01:00
2017-12-04 04:27:49 +01:00
// if we have no OpenID photo try to look up an avatar
if ( ! strlen ( $photo )) {
2018-01-27 17:13:41 +01:00
$photo = Network :: lookupAvatarByEmail ( $email );
2017-12-04 04:27:49 +01:00
}
2018-01-17 20:22:38 +01:00
// unless there is no avatar-addon loaded
2017-12-04 04:27:49 +01:00
if ( strlen ( $photo )) {
$photo_failure = false ;
$filename = basename ( $photo );
2018-01-27 17:13:41 +01:00
$img_str = Network :: fetchUrl ( $photo , true );
2017-12-04 04:27:49 +01:00
// guess mimetype from headers or filename
2019-10-18 03:26:15 +02:00
$type = Images :: guessType ( $photo , true );
2017-12-04 04:27:49 +01:00
2017-12-07 14:56:11 +01:00
$Image = new Image ( $img_str , $type );
if ( $Image -> isValid ()) {
2018-10-23 16:36:57 +02:00
$Image -> scaleToSquare ( 300 );
2017-12-04 04:27:49 +01:00
2019-10-26 15:05:35 +02:00
$resource_id = Photo :: newResource ();
2017-12-04 04:27:49 +01:00
2020-01-18 20:52:34 +01:00
$r = Photo :: store ( $Image , $uid , 0 , $resource_id , $filename , DI :: l10n () -> t ( 'Profile Photos' ), 4 );
2017-12-04 04:27:49 +01:00
if ( $r === false ) {
$photo_failure = true ;
}
2017-12-07 14:56:11 +01:00
$Image -> scaleDown ( 80 );
2017-12-04 04:27:49 +01:00
2020-01-18 20:52:34 +01:00
$r = Photo :: store ( $Image , $uid , 0 , $resource_id , $filename , DI :: l10n () -> t ( 'Profile Photos' ), 5 );
2017-12-04 04:27:49 +01:00
if ( $r === false ) {
$photo_failure = true ;
}
2017-12-07 14:56:11 +01:00
$Image -> scaleDown ( 48 );
2017-12-04 04:27:49 +01:00
2020-01-18 20:52:34 +01:00
$r = Photo :: store ( $Image , $uid , 0 , $resource_id , $filename , DI :: l10n () -> t ( 'Profile Photos' ), 6 );
2017-12-04 04:27:49 +01:00
if ( $r === false ) {
$photo_failure = true ;
}
if ( ! $photo_failure ) {
2019-10-26 15:05:35 +02:00
Photo :: update ([ 'profile' => 1 ], [ 'resource-id' => $resource_id ]);
2017-12-04 04:27:49 +01:00
}
}
}
2018-12-26 07:06:24 +01:00
Hook :: callAll ( 'register_account' , $uid );
2017-12-04 04:27:49 +01:00
2017-12-16 02:47:10 +01:00
$return [ 'user' ] = $user ;
return $return ;
2017-12-04 04:27:49 +01:00
}
/**
2020-01-19 07:05:23 +01:00
* Sends pending registration confirmation email
2017-12-04 04:27:49 +01:00
*
2018-10-15 17:58:52 +02:00
* @ param array $user User record array
2017-12-04 04:27:49 +01:00
* @ param string $sitename
2018-10-15 17:58:52 +02:00
* @ param string $siteurl
2018-10-14 17:57:28 +02:00
* @ param string $password Plaintext password
2017-12-04 04:27:49 +01:00
* @ return NULL | boolean from notification () and email () inherited
2019-01-07 16:24:06 +01:00
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
2017-12-04 04:27:49 +01:00
*/
2018-10-15 17:58:52 +02:00
public static function sendRegisterPendingEmail ( $user , $sitename , $siteurl , $password )
2017-12-04 04:27:49 +01:00
{
2020-01-18 20:52:34 +01:00
$body = Strings :: deindent ( DI :: l10n () -> t (
2019-10-11 01:21:41 +02:00
'
2017-12-04 04:27:49 +01:00
Dear % 1 $s ,
Thank you for registering at % 2 $s . Your account is pending for approval by the administrator .
2018-10-14 17:57:28 +02:00
Your login details are as follows :
Site Location : % 3 $s
Login Name : % 4 $s
Password : % 5 $s
' ,
2019-10-11 01:21:41 +02:00
$user [ 'username' ],
$sitename ,
$siteurl ,
$user [ 'nickname' ],
$password
2018-10-14 17:57:28 +02:00
));
2017-12-04 04:27:49 +01:00
2018-01-15 14:05:12 +01:00
return notification ([
2018-10-14 17:57:28 +02:00
'type' => SYSTEM_EMAIL ,
2018-10-15 17:58:52 +02:00
'uid' => $user [ 'uid' ],
'to_email' => $user [ 'email' ],
2020-01-18 20:52:34 +01:00
'subject' => DI :: l10n () -> t ( 'Registration at %s' , $sitename ),
2018-10-14 17:57:28 +02:00
'body' => $body
]);
2017-12-04 04:27:49 +01:00
}
/**
2020-01-19 07:05:23 +01:00
* Sends registration confirmation
2017-12-04 04:27:49 +01:00
*
* It ' s here as a function because the mail is sent from different parts
*
2020-01-18 20:59:39 +01:00
* @ param \Friendica\Core\L10n $l10n The used language
* @ param array $user User record array
* @ param string $sitename
* @ param string $siteurl
* @ param string $password Plaintext password
*
2017-12-04 04:27:49 +01:00
* @ return NULL | boolean from notification () and email () inherited
2019-01-07 16:24:06 +01:00
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
2017-12-04 04:27:49 +01:00
*/
2020-01-18 20:59:39 +01:00
public static function sendRegisterOpenEmail ( \Friendica\Core\L10n $l10n , $user , $sitename , $siteurl , $password )
2017-12-04 04:27:49 +01:00
{
2019-12-28 23:12:01 +01:00
$preamble = Strings :: deindent ( $l10n -> t (
2019-10-11 01:21:41 +02:00
'
Dear % 1 $s ,
2017-12-04 04:27:49 +01:00
Thank you for registering at % 2 $s . Your account has been created .
2019-10-11 01:21:41 +02:00
' ,
$user [ 'username' ],
$sitename
2018-10-14 17:57:28 +02:00
));
2019-12-28 23:12:01 +01:00
$body = Strings :: deindent ( $l10n -> t (
2019-10-11 01:21:41 +02:00
'
2017-12-04 04:27:49 +01:00
The login details are as follows :
2018-04-04 21:56:34 +02:00
Site Location : % 3 $s
Login Name : % 1 $s
Password : % 5 $s
2018-04-02 18:40:52 +02:00
You may change your password from your account " Settings " page after logging
2017-12-04 04:27:49 +01: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 22:51:32 +01:00
' . "\x28" . ' on the " Profiles " page ' . "\x29" . ' so that other people can easily find you .
2017-12-04 04:27:49 +01:00
We recommend setting your full name , adding a profile photo ,
2018-04-02 18:40:52 +02:00
adding some profile " keywords " ' . "\x28" . ' very useful in making new friends ' . "\x29" . ' - and
2017-12-04 04:27:49 +01: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 21:56:34 +02:00
If you ever want to delete your account , you can do so at % 3 $s / removeme
2017-12-04 04:27:49 +01:00
2018-10-14 17:57:28 +02:00
Thank you and welcome to % 2 $s . ' ,
2019-10-11 01:21:41 +02:00
$user [ 'nickname' ],
$sitename ,
$siteurl ,
$user [ 'username' ],
$password
2018-10-14 17:57:28 +02:00
));
2017-12-04 04:27:49 +01:00
2018-01-15 14:05:12 +01:00
return notification ([
2018-10-14 17:57:28 +02:00
'uid' => $user [ 'uid' ],
2018-08-10 21:39:43 +02:00
'language' => $user [ 'language' ],
2018-10-14 17:57:28 +02:00
'type' => SYSTEM_EMAIL ,
2018-10-15 17:58:52 +02:00
'to_email' => $user [ 'email' ],
2020-01-18 20:52:34 +01:00
'subject' => DI :: l10n () -> t ( 'Registration details for %s' , $sitename ),
2018-10-14 17:57:28 +02:00
'preamble' => $preamble ,
'body' => $body
]);
2017-12-04 04:27:49 +01:00
}
2017-11-20 17:14:35 +01:00
/**
* @ param object $uid user to remove
2019-01-06 22:06:53 +01:00
* @ return bool
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
2017-11-20 17:14:35 +01:00
*/
2017-11-19 22:55:28 +01:00
public static function remove ( $uid )
{
if ( ! $uid ) {
2018-11-25 02:58:41 +01:00
return false ;
2017-11-19 22:55:28 +01:00
}
2018-10-29 22:20:46 +01:00
Logger :: log ( 'Removing user: ' . $uid );
2017-11-19 22:55:28 +01:00
2018-07-20 14:19:26 +02:00
$user = DBA :: selectFirst ( 'user' , [], [ 'uid' => $uid ]);
2017-11-19 22:55:28 +01:00
2018-11-25 02:56:38 +01:00
Hook :: callAll ( 'remove_user' , $user );
2017-11-19 22:55:28 +01:00
// save username (actually the nickname as it is guaranteed
// unique), so it cannot be re-registered in the future.
2018-07-20 14:19:26 +02:00
DBA :: insert ( 'userd' , [ 'username' => $user [ 'nickname' ]]);
2017-11-19 22:55:28 +01:00
// The user and related data will be deleted in "cron_expire_and_remove_users" (cronjobs.php)
2018-11-25 02:59:18 +01:00
DBA :: update ( 'user' , [ 'account_removed' => true , 'account_expires_on' => DateTimeFormat :: utc ( 'now + 7 day' )], [ 'uid' => $uid ]);
2019-06-10 16:19:24 +02:00
Worker :: add ( PRIORITY_HIGH , 'Notifier' , Delivery :: REMOVAL , $uid );
2017-11-19 22:55:28 +01:00
// Send an update to the directory
2018-08-02 07:21:01 +02:00
$self = DBA :: selectFirst ( 'contact' , [ 'url' ], [ 'uid' => $uid , 'self' => true ]);
2018-11-25 02:58:11 +01:00
Worker :: add ( PRIORITY_LOW , 'Directory' , $self [ 'url' ]);
2017-11-19 22:55:28 +01:00
2018-08-25 23:48:50 +02:00
// Remove the user relevant data
2019-02-04 00:55:46 +01:00
Worker :: add ( PRIORITY_NEGLIGIBLE , 'RemoveUser' , $uid );
2018-08-25 23:48:50 +02:00
2018-11-25 02:58:41 +01:00
return true ;
2017-11-19 22:55:28 +01:00
}
2018-11-08 00:22:15 +01: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 22:06:53 +01:00
* [
* [
* 'uid' => 1 ,
* 'username' => 'maxmuster' ,
* 'nickname' => 'Max Mustermann'
* ],
* [
* 'uid' => 2 ,
* 'username' => 'johndoe' ,
* 'nickname' => 'John Doe'
* ]
* ]
* @ throws Exception
2018-11-08 00:22:15 +01:00
*/
public static function identities ( $uid )
{
$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-11 01:21:41 +02:00
$identities = [[
'uid' => $user [ 'uid' ],
2018-11-08 00:22:15 +01:00
'username' => $user [ 'username' ],
2019-10-11 01:21:41 +02:00
'nickname' => $user [ 'nickname' ]
]];
2018-11-08 00:22:15 +01:00
// Then add all the children
2019-10-11 01:21:41 +02:00
$r = DBA :: select (
'user' ,
[ 'uid' , 'username' , 'nickname' ],
[ 'parent-uid' => $user [ 'uid' ], 'account_removed' => false ]
);
2018-11-08 00:22:15 +01:00
if ( DBA :: isResult ( $r )) {
$identities = array_merge ( $identities , DBA :: toArray ( $r ));
}
} else {
// First entry is our parent
2019-10-11 01:21:41 +02:00
$r = DBA :: select (
'user' ,
[ 'uid' , 'username' , 'nickname' ],
[ 'uid' => $user [ 'parent-uid' ], 'account_removed' => false ]
);
2018-11-08 00:22:15 +01:00
if ( DBA :: isResult ( $r )) {
$identities = DBA :: toArray ( $r );
}
// Then add all siblings
2019-10-11 01:21:41 +02:00
$r = DBA :: select (
'user' ,
[ 'uid' , 'username' , 'nickname' ],
[ 'parent-uid' => $user [ 'parent-uid' ], 'account_removed' => false ]
);
2018-11-08 00:22:15 +01:00
if ( DBA :: isResult ( $r )) {
$identities = array_merge ( $identities , DBA :: toArray ( $r ));
}
}
2019-10-11 01:21:41 +02:00
$r = DBA :: p (
" SELECT `user`.`uid`, `user`.`username`, `user`.`nickname`
2018-11-08 00:22:15 +01: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 14:00:17 +02: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 ,
];
$userStmt = DBA :: p ( " SELECT `user`.`uid`, `user`.`login_date`, `contact`.`last-item`
FROM `user`
2020-01-23 01:34:15 +01:00
INNER JOIN `profile` ON `profile` . `uid` = `user` . `uid`
2019-04-22 14:00:17 +02:00
INNER JOIN `contact` ON `contact` . `uid` = `user` . `uid` AND `contact` . `self`
WHERE ( `profile` . `publish` OR `profile` . `net-publish` ) AND `user` . `verified`
AND NOT `user` . `blocked` AND NOT `user` . `account_removed`
AND NOT `user` . `account_expired` " );
if ( ! DBA :: isResult ( $userStmt )) {
return $statistics ;
}
$halfyear = time () - ( 180 * 24 * 60 * 60 );
$month = time () - ( 30 * 24 * 60 * 60 );
while ( $user = DBA :: fetch ( $userStmt )) {
$statistics [ 'total_users' ] ++ ;
2019-10-11 01:21:41 +02:00
if (( strtotime ( $user [ 'login_date' ]) > $halfyear ) || ( strtotime ( $user [ 'last-item' ]) > $halfyear )
) {
2019-04-22 14:00:17 +02:00
$statistics [ 'active_users_halfyear' ] ++ ;
}
2019-10-11 01:21:41 +02:00
if (( strtotime ( $user [ 'login_date' ]) > $month ) || ( strtotime ( $user [ 'last-item' ]) > $month )
) {
2019-04-22 14:00:17 +02:00
$statistics [ 'active_users_monthly' ] ++ ;
}
}
return $statistics ;
}
2017-11-19 22:55:28 +01:00
}