2017-11-19 16:55:28 -05:00
< ? php
/**
* @ file src / Model / User . php
* @ brief This file includes the User class with user related database functions
*/
namespace Friendica\Model ;
2018-10-19 11:26:48 -04:00
use DivineOmega\PasswordExposed ;
2018-07-19 22:15:21 -04:00
use Exception ;
2018-01-17 13:42:40 -05:00
use Friendica\Core\Addon ;
2017-12-03 22:27:49 -05:00
use Friendica\Core\Config ;
2018-01-22 09:54:13 -05:00
use Friendica\Core\L10n ;
2018-01-24 21:08:45 -05:00
use Friendica\Core\PConfig ;
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 ;
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 ;
2018-01-26 23:09:48 -05:00
use Friendica\Util\Network ;
2018-01-24 21:08:45 -05:00
use LightOpenID ;
2017-11-19 16:55:28 -05:00
require_once 'boot.php' ;
2017-12-17 15:24:57 -05:00
require_once 'include/dba.php' ;
2017-12-03 22:27:49 -05:00
require_once 'include/enotify.php' ;
require_once 'include/text.php' ;
2017-11-19 16:55:28 -05:00
/**
* @ brief This class handles User related functions
*/
class User
{
2018-10-14 11:34:34 -04:00
/**
* Returns true if a user record exists with the provided id
*
* @ param integer $uid
* @ return boolean
*/
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
* @ return array | boolean User record if it exists , false otherwise
*/
public static function getById ( $uid )
{
return DBA :: selectFirst ( 'user' , [], [ 'uid' => $uid ]);
}
/**
* @ brief 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
*/
public static function getIdForURL ( $url )
{
$self = DBA :: selectFirst ( 'contact' , [ 'uid' ], [ 'nurl' => normalise_link ( $url ), 'self' => true ]);
if ( ! DBA :: isResult ( $self )) {
return false ;
} else {
return $self [ 'uid' ];
}
}
2017-12-17 16:22:39 -05:00
/**
* @ brief Get owner data by user id
*
* @ param int $uid
* @ return boolean | array
*/
public static function getOwnerDataById ( $uid ) {
2018-07-20 22:01:53 -04:00
$r = DBA :: fetchFirst ( " SELECT
2017-12-17 16:22:39 -05:00
`contact` .* ,
`user` . `prvkey` AS `uprvkey` ,
`user` . `timezone` ,
`user` . `nickname` ,
`user` . `sprvkey` ,
`user` . `spubkey` ,
`user` . `page-flags` ,
`user` . `account-type` ,
`user` . `prvnets`
FROM `contact`
INNER JOIN `user`
ON `user` . `uid` = `contact` . `uid`
WHERE `contact` . `uid` = ?
2017-12-18 04:21:47 -05:00
AND `contact` . `self`
2017-12-17 16:22:39 -05:00
LIMIT 1 " ,
$uid
);
2018-07-21 08:46:04 -04:00
if ( ! DBA :: isResult ( $r )) {
2017-12-17 16:22:39 -05:00
return false ;
}
2017-12-18 04:21:47 -05:00
return $r ;
2017-12-17 16:22:39 -05:00
}
2018-06-18 16:36:34 -04:00
/**
* @ brief Get owner data by nick name
*
* @ param int $nick
* @ return boolean | array
*/
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
/**
2017-12-09 13:31:00 -05:00
* @ brief Returns the default group for a given user and network
*
* @ param int $uid User id
* @ param string $network network name
*
* @ return int group id
*/
public static function getDefaultGroup ( $uid , $network = '' )
{
$default_group = 0 ;
2018-08-11 16:40:44 -04:00
if ( $network == Protocol :: OSTATUS ) {
2017-12-09 13:31:00 -05:00
$default_group = PConfig :: get ( $uid , " ostatus " , " default_group " );
}
if ( $default_group != 0 ) {
return $default_group ;
}
2018-07-20 08:19:26 -04:00
$user = DBA :: selectFirst ( 'user' , [ 'def_gid' ], [ 'uid' => $uid ]);
2017-12-09 13:31:00 -05:00
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 " ];
}
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
*
2018-02-09 00:08:01 -05:00
* @ brief Authenticate a user with a clear text password
2017-12-03 22:15:31 -05:00
* @ param mixed $user_info
* @ param string $password
2018-02-09 00:08:01 -05:00
* @ return int | boolean
* @ deprecated since version 3.6
2018-03-21 01:33:35 -04:00
* @ see User :: getIdFromPasswordAuthentication ()
2017-12-03 22:15:31 -05:00
*/
2017-11-26 14:25:25 -05:00
public static function authenticate ( $user_info , $password )
{
2018-02-09 00:08:01 -05:00
try {
return self :: getIdFromPasswordAuthentication ( $user_info , $password );
} 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
/**
* Returns the user id associated with a successful password authentication
*
* @ brief Authenticate a user with a clear text password
* @ param mixed $user_info
* @ param string $password
* @ return int User Id if authentication is successful
* @ throws Exception
*/
public static function getIdFromPasswordAuthentication ( $user_info , $password )
{
$user = self :: getAuthenticationInfo ( $user_info );
2017-11-26 14:25:25 -05:00
2018-04-08 10:02:25 -04:00
if ( 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' ]) {
self :: updatePassword ( $user [ 'uid' ], $password );
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' ])) {
self :: updatePassword ( $user [ 'uid' ], $password );
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 )) {
self :: updatePassword ( $user [ 'uid' ], $password );
}
return $user [ 'uid' ];
2017-11-26 14:25:25 -05:00
}
2018-02-09 00:08:01 -05:00
throw new Exception ( L10n :: t ( 'Login failed' ));
}
/**
* 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 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 ;
}
if ( ! isset ( $user [ 'uid' ])
|| ! isset ( $user [ 'password' ])
2018-04-15 04:51:22 -04:00
|| ! isset ( $user [ 'legacy_password' ])
2018-02-09 00:08:01 -05:00
) {
throw new Exception ( L10n :: t ( 'Not enough information to authenticate' ));
}
} elseif ( is_int ( $user_info ) || is_string ( $user_info )) {
if ( is_int ( $user_info )) {
2018-07-20 08:19:26 -04:00
$user = DBA :: selectFirst ( 'user' , [ 'uid' , '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 {
2018-06-19 17:33:07 -04:00
$fields = [ 'uid' , 'password' , 'legacy_password' ];
$condition = [ " (`email` = ? OR `username` = ? OR `nickname` = ?)
AND NOT `blocked` AND NOT `account_expired` AND NOT `account_removed` AND `verified` " ,
$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 )) {
2018-02-09 00:08:01 -05:00
throw new Exception ( L10n :: t ( 'User not found' ));
}
}
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
*/
public static function generateNewPassword ()
{
return autoname ( 6 ) . mt_rand ( 100 , 9999 );
}
2018-03-21 01:33:35 -04:00
/**
* Checks if the provided plaintext password has been exposed or not
*
* @ param string $password
* @ return bool
*/
2018-03-21 02:14:43 -04:00
public static function isPasswordExposed ( $password )
2018-03-21 01:33:35 -04:00
{
2018-10-19 11:26:48 -04:00
$cache = new \DivineOmega\DOFileCachePSR6\CacheItemPool ();
$cache -> changeConfig ([
'cacheDirectory' => get_temppath () . '/password-exposed-cache/' ,
]);
$PasswordExposedCHecker = new PasswordExposed\PasswordExposedChecker ( null , $cache );
return $PasswordExposedCHecker -> passwordExposed ( $password ) === PasswordExposed\PasswordStatus :: EXPOSED ;
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
*/
public static function hashPassword ( $password )
{
2018-04-18 22:49:14 -04:00
if ( ! trim ( $password )) {
throw new Exception ( L10n :: t ( 'Password can\'t be empty' ));
}
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
*/
public static function updatePassword ( $uid , $password )
{
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
*/
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
/**
* @ brief Checks if a nickname is in the list of the forbidden nicknames
*
* Check if a nickname is forbidden from registration on the node by the
* admin . Forbidden nicknames ( e . g . role namess ) can be configured in the
* admin 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 )
{
$forbidden_nicknames = Config :: get ( 'system' , 'forbidden_nicknames' , '' );
2018-07-22 21:18:21 -04:00
2018-07-06 09:32:56 -04:00
// if the config variable is empty return false
2018-07-22 21:18:21 -04:00
if ( empty ( $forbidden_nicknames )) {
2018-07-06 09:32:56 -04:00
return false ;
}
2018-07-22 21:18:21 -04:00
2018-07-06 09:32:56 -04:00
// check if the nickname is in the list of blocked nicknames
$forbidden = explode ( ',' , $forbidden_nicknames );
2018-07-06 09:49:27 -04:00
$forbidden = array_map ( 'trim' , $forbidden );
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 ;
}
2017-12-03 22:27:49 -05:00
/**
* @ brief Catch - all user creation function
*
* 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
*
* @ param array $data
* @ return string
2017-12-12 20:43:21 -05:00
* @ throw Exception
2017-12-03 22:27:49 -05:00
*/
public static function create ( array $data )
{
$a = get_app ();
2017-12-12 20:43:21 -05:00
$return = [ 'user' => null , 'password' => '' ];
2017-12-03 22:27:49 -05:00
$using_invites = Config :: get ( 'system' , 'invitation_only' );
$num_invites = Config :: get ( 'system' , 'number_invites' );
2018-07-22 21:18:21 -04:00
$invite_id = ! empty ( $data [ 'invite_id' ]) ? notags ( trim ( $data [ 'invite_id' ])) : '' ;
$username = ! empty ( $data [ 'username' ]) ? notags ( trim ( $data [ 'username' ])) : '' ;
$nickname = ! empty ( $data [ 'nickname' ]) ? notags ( trim ( $data [ 'nickname' ])) : '' ;
$email = ! empty ( $data [ 'email' ]) ? notags ( trim ( $data [ 'email' ])) : '' ;
$openid_url = ! empty ( $data [ 'openid_url' ]) ? notags ( trim ( $data [ 'openid_url' ])) : '' ;
$photo = ! empty ( $data [ 'photo' ]) ? notags ( trim ( $data [ 'photo' ])) : '' ;
$password = ! empty ( $data [ 'password' ]) ? trim ( $data [ 'password' ]) : '' ;
$password1 = ! empty ( $data [ 'password1' ]) ? trim ( $data [ 'password1' ]) : '' ;
$confirm = ! empty ( $data [ 'confirm' ]) ? trim ( $data [ 'confirm' ]) : '' ;
$blocked = ! empty ( $data [ 'blocked' ]) ? intval ( $data [ 'blocked' ]) : 0 ;
$verified = ! empty ( $data [ 'verified' ]) ? intval ( $data [ 'verified' ]) : 0 ;
$language = ! empty ( $data [ 'language' ]) ? notags ( trim ( $data [ 'language' ])) : 'en' ;
$publish = ! empty ( $data [ 'profile_publish_reg' ]) && intval ( $data [ 'profile_publish_reg' ]) ? 1 : 0 ;
2017-12-03 22:27:49 -05:00
$netpublish = strlen ( Config :: get ( 'system' , 'directory' )) ? $publish : 0 ;
if ( $password1 != $confirm ) {
2018-01-22 09:54:13 -05:00
throw new Exception ( 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 ) {
2018-01-22 09:54:13 -05:00
throw new Exception ( 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 )) {
2018-01-22 09:54:13 -05:00
throw new Exception ( L10n :: t ( 'Invitation could not be verified.' ));
2017-12-03 22:27:49 -05:00
}
}
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 )) {
2018-01-22 09:54:13 -05:00
throw new Exception ( L10n :: t ( 'Invalid OpenID url' ));
2017-12-03 22:27:49 -05:00
}
$_SESSION [ 'register' ] = 1 ;
$_SESSION [ 'openid' ] = $openid_url ;
2018-10-09 13:58:58 -04:00
$openid = new LightOpenID ( $a -> getHostName ());
2017-12-03 22:27:49 -05:00
$openid -> identity = $openid_url ;
$openid -> returnUrl = System :: 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 ) {
2018-01-22 09:54:13 -05:00
throw new Exception ( L10n :: t ( 'We encountered a problem while logging in with the OpenID you provided. Please check the correct spelling of the ID.' ) . EOL . EOL . L10n :: t ( 'The error message was:' ) . $e -> getMessage (), 0 , $e );
2017-12-03 22:27:49 -05:00
}
2018-10-13 14:02:04 -04:00
$a -> redirect ( $authurl );
2017-12-03 22:27:49 -05:00
// NOTREACHED
}
2018-01-22 09:54:13 -05:00
throw new Exception ( 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 = '' ;
}
$err = '' ;
// collapse multiple spaces in name
$username = preg_replace ( '/ +/' , ' ' , $username );
2018-10-21 17:28:40 -04:00
$username_min_length = max ( 1 , min ( 64 , intval ( Config :: get ( 'system' , 'username_min_length' , 3 ))));
$username_max_length = max ( 1 , min ( 64 , intval ( 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 ) {
logger ( 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 );
$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 ) {
2018-10-21 08:28:24 -04:00
throw new Exception ( 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 ) {
throw new Exception ( 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.
$loose_reg = Config :: get ( 'system' , 'no_regfullname' );
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 ) {
throw new Exception ( 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 )) {
2018-01-22 09:54:13 -05:00
throw new Exception ( L10n :: t ( 'Your email domain is not among those allowed on this site.' ));
2017-12-03 22:27:49 -05:00
}
2018-01-27 11:13:41 -05:00
if ( ! valid_email ( $email ) || ! Network :: isEmailDomainValid ( $email )) {
2018-01-22 09:54:13 -05:00
throw new Exception ( 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 )) {
throw new Exception ( L10n :: t ( 'The nickname was blocked from registration by the nodes admin.' ));
}
2017-12-03 22:27:49 -05:00
2018-07-20 08:19:26 -04:00
if ( Config :: get ( 'system' , 'block_extended_register' , false ) && DBA :: exists ( 'user' , [ 'email' => $email ])) {
2018-01-22 09:54:13 -05:00
throw new Exception ( 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.
2018-07-07 17:46:30 -04:00
if ( Config :: get ( 'config' , 'admin_email' ) && strlen ( $openid_url )) {
$adminlist = explode ( ',' , str_replace ( ' ' , '' , strtolower ( Config :: get ( 'config' , 'admin_email' ))));
2017-12-12 21:07:03 -05:00
if ( in_array ( strtolower ( $email ), $adminlist )) {
2018-01-22 09:54:13 -05:00
throw new Exception ( L10n :: t ( 'Cannot use that email.' ));
2017-12-03 22:27:49 -05:00
}
}
$nickname = $data [ 'nickname' ] = strtolower ( $nickname );
2017-12-12 21:07:03 -05:00
if ( ! preg_match ( '/^[a-z0-9][a-z0-9\_]*$/' , $nickname )) {
2018-01-24 16:51:32 -05:00
throw new Exception ( 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.
2018-07-20 08:19:26 -04:00
if ( DBA :: exists ( 'user' , [ 'nickname' => $nickname ])
|| DBA :: exists ( 'userd' , [ 'username' => $nickname ])
2017-12-12 21:07:03 -05:00
) {
2018-01-22 09:54:13 -05:00
throw new Exception ( 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 ) {
2018-01-22 09:54:13 -05:00
throw new Exception ( 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 {
2018-01-22 09:54:13 -05:00
throw new Exception ( 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 ) {
2018-01-22 09:54:13 -05:00
throw new Exception ( 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
2018-01-22 09:54:13 -05:00
throw new Exception ( 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 ,
'photo' => System :: baseUrl () . " /photo/profile/ { $uid } .jpg " ,
'thumb' => System :: baseUrl () . " /photo/avatar/ { $uid } .jpg " ,
'publish' => $publish ,
'is-default' => 1 ,
'net-publish' => $netpublish ,
2018-01-22 09:54:13 -05:00
'profile-name' => L10n :: t ( 'default' )
2017-12-12 21:07:03 -05:00
]);
if ( ! $insert_result ) {
2018-07-20 08:19:26 -04:00
DBA :: delete ( 'user' , [ 'uid' => $uid ]);
2017-12-12 21:07:03 -05:00
2018-01-22 09:54:13 -05:00
throw new Exception ( 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
2018-01-22 09:54:13 -05:00
throw new Exception ( 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.
2018-01-22 09:54:13 -05:00
$def_gid = Group :: create ( $uid , 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
2018-01-22 09:54:13 -05:00
throw new Exception ( 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 ];
if ( Config :: get ( 'system' , 'newuser_private' ) && $def_gid ) {
$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 );
2018-01-27 11:13:41 -05:00
$img_str = Network :: fetchUrl ( $photo , true );
2017-12-03 22:27:49 -05:00
// guess mimetype from headers or filename
2017-12-07 08:56:11 -05:00
$type = Image :: guessType ( $photo , true );
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 ()) {
$Image -> scaleToSquare ( 175 );
2017-12-03 22:27:49 -05:00
2018-02-20 05:02:07 -05:00
$hash = Photo :: newResource ();
2017-12-03 22:27:49 -05:00
2018-01-22 09:54:13 -05:00
$r = Photo :: store ( $Image , $uid , 0 , $hash , $filename , 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
2018-01-22 09:54:13 -05:00
$r = Photo :: store ( $Image , $uid , 0 , $hash , $filename , 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
2018-01-22 09:54:13 -05:00
$r = Photo :: store ( $Image , $uid , 0 , $hash , $filename , L10n :: t ( 'Profile Photos' ), 6 );
2017-12-03 22:27:49 -05:00
if ( $r === false ) {
$photo_failure = true ;
}
if ( ! $photo_failure ) {
2018-07-20 08:19:26 -04:00
DBA :: update ( 'photo' , [ 'profile' => 1 ], [ 'resource-id' => $hash ]);
2017-12-03 22:27:49 -05:00
}
}
}
2018-01-17 13:42:40 -05:00
Addon :: callHooks ( '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
}
/**
2018-10-14 11:32:54 -04:00
* @ brief 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
*/
2018-10-15 11:58:52 -04:00
public static function sendRegisterPendingEmail ( $user , $sitename , $siteurl , $password )
2017-12-03 22:27:49 -05:00
{
2018-01-22 09:54:13 -05:00
$body = deindent ( L10n :: t ( '
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
' ,
2018-10-18 18:22:48 -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
2018-01-15 08:05:12 -05:00
return notification ([
2018-10-14 11:57:28 -04:00
'type' => SYSTEM_EMAIL ,
2018-10-15 11:58:52 -04:00
'uid' => $user [ 'uid' ],
'to_email' => $user [ 'email' ],
2018-10-14 11:57:28 -04:00
'subject' => L10n :: t ( 'Registration at %s' , $sitename ),
'body' => $body
]);
2017-12-03 22:27:49 -05:00
}
/**
* @ brief Sends registration confirmation
*
* It ' s here as a function because the mail is sent from different parts
*
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
* @ param string $siteurl
2018-10-15 11:58:52 -04:00
* @ param string $password Plaintext password
2017-12-03 22:27:49 -05:00
* @ return NULL | boolean from notification () and email () inherited
*/
2018-10-15 11:58:52 -04:00
public static function sendRegisterOpenEmail ( $user , $sitename , $siteurl , $password )
2017-12-03 22:27:49 -05:00
{
2018-01-22 09:54:13 -05:00
$preamble = deindent ( L10n :: t ( '
2017-12-03 22:27:49 -05:00
Dear % 1 $s ,
Thank you for registering at % 2 $s . Your account has been created .
2018-10-14 11:57:28 -04:00
' ,
2018-10-15 11:58:52 -04:00
$preamble , $user [ 'username' ], $sitename
2018-10-14 11:57:28 -04:00
));
2018-01-22 09:54:13 -05:00
$body = deindent ( L10n :: t ( '
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 . ' ,
2018-10-18 18:22:48 -04:00
$user [ 'email' ], $sitename , $siteurl , $user [ 'username' ], $password
2018-10-14 11:57:28 -04:00
));
2017-12-03 22:27:49 -05:00
2018-01-15 08:05:12 -05:00
return notification ([
2018-10-14 11:57:28 -04:00
'uid' => $user [ 'uid' ],
2018-08-10 15:39:43 -04:00
'language' => $user [ 'language' ],
2018-10-14 11:57:28 -04:00
'type' => SYSTEM_EMAIL ,
2018-10-15 11:58:52 -04:00
'to_email' => $user [ 'email' ],
2018-10-14 11:57:28 -04:00
'subject' => L10n :: t ( 'Registration details for %s' , $sitename ),
'preamble' => $preamble ,
'body' => $body
]);
2017-12-03 22:27:49 -05:00
}
2017-11-20 11:14:35 -05:00
/**
* @ param object $uid user to remove
* @ return void
*/
2017-11-19 16:55:28 -05:00
public static function remove ( $uid )
{
if ( ! $uid ) {
return ;
}
2018-10-13 14:02:04 -04:00
$a = get_app ();
2017-11-19 16:55:28 -05:00
logger ( 'Removing user: ' . $uid );
2018-07-20 08:19:26 -04:00
$user = DBA :: selectFirst ( 'user' , [], [ 'uid' => $uid ]);
2017-11-19 16:55:28 -05:00
2018-01-17 13:42:40 -05:00
Addon :: callHooks ( '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
// The user and related data will be deleted in "cron_expire_and_remove_users" (cronjobs.php)
2018-08-29 14:28:13 -04:00
DBA :: update ( 'user' , [ 'account_removed' => true , 'account_expires_on' => DateTimeFormat :: utc ( DateTimeFormat :: utcNow () . " + 7 day " )], [ 'uid' => $uid ]);
2017-11-19 16:55:28 -05:00
Worker :: add ( PRIORITY_HIGH , " Notifier " , " removeme " , $uid );
// Send an update to the directory
2018-08-02 01:21:01 -04:00
$self = DBA :: selectFirst ( 'contact' , [ 'url' ], [ 'uid' => $uid , 'self' => true ]);
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
Worker :: add ( PRIORITY_LOW , " RemoveUser " , $uid );
2017-11-19 16:55:28 -05:00
if ( $uid == local_user ()) {
unset ( $_SESSION [ 'authenticated' ]);
unset ( $_SESSION [ 'uid' ]);
2018-10-13 14:02:04 -04:00
$a -> redirect ();
2017-11-19 16:55:28 -05:00
}
}
}