Files
thetempusproject/app/models/user.php
Joey Kimsey 32a9711ade wip from ATB
2025-01-21 19:19:06 -05:00

747 lines
23 KiB
PHP

<?php
/**
* app/models/user.php
*
* This class is used for the manipulation of the user database table.
*
* @todo needs a re-build
* @todo finish fixing the check functions that were migrated here
* These could go in the Forms class?
*
* @version 3.0
* @author Joey Kimsey <Joey@thetempusproject.com>
* @link https://TheTempusProject.com
* @license https://opensource.org/licenses/MIT [MIT LICENSE]
*/
namespace TheTempusProject\Models;
use TheTempusProject\Bedrock\Functions\Check;
use TheTempusProject\Canary\Bin\Canary as Debug;
use TheTempusProject\Bedrock\Functions\Hash;
use TheTempusProject\Bedrock\Functions\Session;
use TheTempusProject\Bedrock\Functions\Code;
use TheTempusProject\Bedrock\Classes\Config;
use TheTempusProject\Canary\Classes\CustomException;
use TheTempusProject\Classes\Email;
use TheTempusProject\Classes\DatabaseModel;
use TheTempusProject\Classes\Preferences;
use TheTempusProject\Classes\Forms;
use TheTempusProject\TheTempusProject as App;
class User extends DatabaseModel {
public $tableName = 'users';
public $modelVersion = '1.0';
public $databaseMatrix = [
[ 'registered', 'int', '10' ],
[ 'terms', 'int', '1' ],
[ 'confirmed', 'int', '1' ],
[ 'userGroup', 'int', '11' ],
[ 'lastLogin', 'int', '10' ],
[ 'username', 'varchar', '16' ],
[ 'password', 'varchar', '80' ],
[ 'email', 'varchar', '75' ],
[ 'name', 'varchar', '20' ],
[ 'confirmationCode', 'varchar', '80' ],
[ 'prefs', 'text', '' ],
];
public $permissionMatrix = [
'uploadImages' => [
'pretty' => 'Upload images (such as avatars)',
'default' => false,
],
];
public $preferenceMatrix = [
'gender' => [
'pretty' => 'Gender',
'type' => 'select',
'default' => 'unspecified',
'options' => [
'male',
'female',
'other',
'unspecified',
],
],
'newsletter' => [
'pretty' => 'Receive our Newsletter?',
'type' => 'checkbox',
'default' => 'true',
],
'avatar' => [
'pretty' => 'Avatar',
'type' => 'file',
'default' => 'images/defaultAvatar.png',
],
'timezone' => [
'pretty' => 'Timezone',
'type' => 'timezone',
'default' => 'America/New_York',
],
'dateFormat' => [
'pretty' => 'Date Format',
'type' => 'select',
'default' => 'F j, Y',
'options' => [
'1-8-1991' => 'n-j-Y',
'8-1-1991' => 'j-n-Y',
'01-08-1991' => 'm-d-Y',
'08-01-1991' => 'd-m-Y',
'January 8, 1991' => 'F-j-Y',
'8 January, 1991' => 'j-F-Y',
'January 08, 1991' => 'F-d-Y',
'08 January, 1991' => 'd-F-Y',
'Jan 8, 1991' => 'M-j-Y',
'8 Jan 1991' => 'j-M-Y',
'Jan 08, 1991' => 'M-d-Y',
'08 Jan 1991' => 'd-M-Y',
],
],
'timeFormat' => [
'pretty' => 'Time Format',
'type' => 'select',
'default' => 'g:i:s A',
'options' => [
'3:33:33 AM' => 'g:i:s A',
'03:33:33 AM' => 'h:i:s A',
'3:33:33 am' => 'g:i:s a',
'03:33:33 am' => 'h:i:s a',
'3:33:33 (military)' => 'G:i:s',
'03:33:33 (military)' => 'H:i:s',
],
],
'pageLimit' => [
'pretty' => 'Items Displayed Per Page',
'type' => 'select',
'default' => '10',
'options' => [
'10',
'15',
'20',
'25',
'50',
],
],
'darkMode' => [
'pretty' => 'Enable Dark-Mode viewing',
'type' => 'checkbox',
'default' => 'false',
],
];
protected static $avatars;
protected static $preferences;
protected static $group;
protected static $usernames;
protected $data;
public function __construct() {
parent::__construct();
self::$user = $this;
self::$preferences = new Preferences;
self::$group = new Group;
}
public function getPreferences( $id ) {
if ( !Check::id( $id ) ) {
return false;
}
$userData = $this->get( $id );
$prefs = json_decode( $userData->prefs, true );
return $prefs;
}
public function getPreferencesDelta() {
$defaults = $this->getDefaultPreferences();
foreach ( $defaults as $key => $value ) {
if ( isset( self::$preferences[ $key ] ) ) {
$defaults[ $key ] = self::$preferences[ $key ];
}
}
return $defaults;
}
public function getDefaultPreferences() {
return self::$preferences->getDefaultPreferencesArray();
}
/**
* Check the database for a user with the same email.
*
* @param {string} [$email] - The email being tested.
* @return {bool}
*/
public function noEmailExists( $email ) {
if ( Check::email( $email ) ) {
$emailQuery = self::$db->get( $this->tableName, [ 'email', '=', $email ] );
if ( $emailQuery->count() == 0 ) {
return true;
}
}
// self::addError("Email is already in use.", $email);
return false;
}
/**
* Check the database for a user with the same username.
*
* @param {string} [$data] - The string being tested.
* @return {bool}
*/
public function usernameExists( $data ) {
if ( Forms::checkUsername( $data ) ) {
$usernameResults = self::$db->get( $this->tableName, [ 'username', '=', $data ] );
if ( $usernameResults->count() ) {
return true;
}
}
// self::addError("No user exists in the DB.", $data);
return false;
}
/**
* Checks username formatting.
*
* Requirements:
* - 4 - 16 characters long
* - must only contain numbers and letters: [A - Z] , [a - z], [0 - 9]
*
* @param {string} [$data] - The string being tested.
* @return {bool}
*/
public function checkUsername( $data ) {
if ( strlen( $data ) > 16 ) {
// self::addError("Username must be be 4 to 16 numbers or letters.", $data);
return false;
}
if ( strlen( $data ) < 4 ) {
// self::addError("Username must be be 4 to 16 numbers or letters.", $data);
return false;
}
if ( !ctype_alnum( $data ) ) {
// self::addError("Username must be be 4 to 16 numbers or letters.", $data);
return false;
}
return true;
}
/**
* Find and define usernames by user ID.
*
* @param {int} [$id] - The ID of the user you are looking for.
* @return {string} - Either the username or 'unknown' will be returned.
*/
public function getUsername( $id ) {
if ( !Check::id( $id ) ) {
return false;
}
if ( !isset( self::$usernames[ $id ] ) ) {
$user = $this->get( $id );
if ( $user !== false ) {
self::$usernames[ $id ] = $user->username;
} else {
self::$usernames[ $id ] = 'Unknown';
}
}
return self::$usernames[ $id ];
}
/**
* Since we need a cache of the usernames, we use this function
* to find/return all usernames based on ID.
*
* @param {int} [$username] - The username of the user you are looking for.
* @return {int}
*/
public function getID( $username ) {
if ( !Forms::checkUsername( $username ) ) {
return false;
}
$user = $this->get( $username );
if ( $user !== false ) {
return $user->ID;
} else {
return 0;
}
}
/**
* Find and define user avatar image urls.
*
* @param {int} [$id] - The ID of the user you are looking for.
* @return {string} - Either the username or 'unknown' will be returned.
*/
public function getAvatar( $id ) {
if ( !Check::id( $id ) ) {
return false;
}
if ( !isset( self::$avatars[ $id ] ) ) {
if ( $this->get( $id ) ) {
self::$avatars[ $id ] = self::data()->avatar;
} else {
self::$avatars[ $id ] = '{BASE}images/defaultAvatar.png';
}
}
return self::$avatars[ $id ];
}
/**
* Delete the specified user(s).
*
* @param {int|array} [$data] - The log ID or array of ID's to be deleted.
* @return {bool}
*/
public function delete( $idArray ) {
if ( !is_array( $idArray ) ) {
$idArray = [ $idArray ];
}
foreach ( $idArray as $id ) {
if ( App::$activeUser->ID == $id ) {
Debug::info( 'Attempting to delete own account.' );
return false;
}
$user = $this->get( $id );
if (
'Super' == $user->groupName
&& 'Super' !== App::$activeGroup->name
) {
Debug::info( 'Attempting to delete superior account.' );
return false;
}
}
return parent::delete( $idArray );
}
/**
* Attempt to authenticate a user login and set them as the active user.
*
* @param {string} [$username] - The username being used to login.
* @param {string} [$password] - The un-hashed password.
* @param {bool} [$remember] - Whether the user wishes to be remembered or not.
* @return {bool}
*/
public function logIn( $username, $password, $remember = false ) {
if ( !isset( self::$session ) ) {
self::$session = new Sessions;
}
if ( !isset( self::$log ) ) {
self::$log = new Log;
}
Debug::group( 'login', 1 );
if ( !Forms::checkUsername( $username ) ) {
Debug::warn( 'Invalid Username.' );
return false;
}
if ( !$this->get( $username ) ) {
self::$log->login( 0, "User not found: $username" );
Debug::warn( "User not found: $username" );
return false;
}
// login attempts protection.
$timeLimit = ( time() - 3600 );
$limit = Config::getValue( 'main/loginLimit' );
$user = $this->data();
if ( $limit > 0 ) {
$limitCheck = self::$db->get(
'logs',
[
'source', '=', 'login',
'AND',
'userID', '=', $user->ID,
'AND',
'time', '>=', $timeLimit,
'AND',
'action', '!=', 'pass',
]
);
if ( $limitCheck->count() >= $limit ) {
Debug::info( 'login: Limit reached.', 1 );
self::$log->login( $user->ID, 'Too many failed attempts.' );
Debug::warn( 'Too many failed login attempts, please try again later.' );
return false;
}
}
if ( !Check::password( $password ) ) {
Debug::warn( 'Invalid password.' );
self::$log->login( $user->ID, 'Invalid Password.' );
return false;
}
if ( !Hash::check( $password, $user->password ) ) {
Debug::warn( 'Pass hash does not match.' );
self::$log->login( $user->ID, 'Wrong Password.' );
return false;
}
self::$session->newSession( null, true, $remember, $user->ID );
self::$log->login( $this->data()->ID, 'pass' );
$this->update( $this->data()->ID, [ 'lastLogin' => time() ] );
Debug::gend();
return true;
}
/**
* Log out the currently active user.
*/
public function logOut() {
if ( !isset( self::$session ) ) {
self::$session = new Sessions;
}
Debug::group( 'Logout', 1 );
self::$session->destroy( Session::get( 'SessionToken' ) );
App::$isLoggedIn = false;
App::$isMember = false;
App::$isMod = false;
App::$isAdmin = false;
App::$activeUser = null;
Debug::info( 'User has been logged out.' );
Debug::gend();
return null;
}
/**
* Change a user password.
*
* @param {string} [$code] - The confirmation code required from the password email.
* @param {string} [$password] - The new password for the user's account.
* @return {bool}
*/
public function changePassword( $code, $password ) {
if ( !Check::password( $password ) ) {
return false;
}
$data = self::$db->get( $this->tableName, [ 'confirmationCode', '=', $code ] );
if ( $data->count() ) {
$this->data = $data->first();
$this->update(
$this->data->ID,
[ 'password' => Hash::make( $password ), 'confirmationCode' => '', ],
);
return true;
}
return false;
}
/**
* Create a list of registered users.
*
* @param {array} [$filter] - A filter to be applied to the users list.
* @return {bool|object}
*/
public function userList( $filter = null ) {
if ( ! empty( $filter ) ) {
switch ( $filter ) {
case 'newsletter':
$data = self::$db->search( $this->tableName, 'prefs', 'newsletter":"true' );
break;
default:
$data = self::$db->get( $this->tableName, '*' );
break;
}
} else {
$data = self::$db->get( $this->tableName, '*' );
}
if ( ! $data->count() ) {
return false;
}
return (object) $data->results();
}
/**
* Create a list of recently registered users.
*
* @param {int} [$limit] - How many posts you would like returned.
* @return {bool|object}
*/
public function recent( $limit = null ) {
if ( empty( $limit ) ) {
$data = self::$db->get( $this->tableName, '*' );
} else {
$data = self::$db->get( $this->tableName, [ 'ID', '>', '0' ], 'ID', 'DESC', [ 0, $limit ] );
}
if ( !$data->count() ) {
return false;
}
return (object) $data->results();
}
/**
* Check the database for a user with the same confirmation code.
*
* @param {string} [$code] - The confirmation code being checked.
* @return {bool}
*/
public function checkCode( $code ) {
$data = self::$db->get( $this->tableName, [ 'confirmationCode', '=', $code ] );
if ( $data->count() > 0 ) {
return true;
}
Debug::error( 'User confirmation code not found.' );
return false;
}
/**
* Generate and save a new confirmation code for the user.
*
* @param {int} [$id] - The user ID to update the confirmation code for.
* @return {bool}
*/
public function newCode( $id ) {
$data = self::$db->get( $this->tableName, [ 'ID', '=', $id ] );
if ( $data->count() == 0 ) {
return false;
}
$this->data = $data->first();
$Ccode = md5( uniqid() );
$this->update(
$this->data->ID,
[ 'confirmationCode' => $Ccode ],
);
return true;
}
/**
* Finds and confirms a user by their confirmation code.
*
* @param {string} [$code] - The confirmation code sent to the user.
* @return {bool}
*/
public function confirm( $code ) {
$data = self::$db->get( $this->tableName, [ 'confirmationCode', '=', $code ] );
if ( $data->count() ) {
$this->data = $data->first();
$this->update(
$this->data->ID,
[ 'confirmed' => 1, 'confirmationCode' => '', ],
);
return true;
}
return false;
}
/**
* Check if the specified user exists or not.
*
* @return {bool}
* @todo this function should actually check for a user
*/
public function exists() {
return ( !empty( $this->data ) ) ? true : false;
}
public function filter( $data, $params = [] ) {
foreach ( $data as $instance ) {
if ( !is_object( $instance ) ) {
$instance = (object) $data;
$end = true;
}
if ( $instance->confirmed == 1 ) {
$instance->confirmedText = 'Yes';
} else {
$instance->confirmedText = 'No';
}
$group = self::$group->findById( $instance->userGroup );
if ( !empty( $group ) ) {
$instance->groupName = $group->name;
} else {
$instance->groupName = 'Unknown';
}
$instance->prefs = json_decode( $instance->prefs, true );
$instance->gender = $instance->prefs['gender'];
$instance->avatar = $instance->prefs['avatar'];
$out[] = $instance;
if ( !empty( $end ) ) {
$out = $out[0];
break;
}
}
return $out;
}
/**
* Get user data from an ID or username.
*
* @param {int|string} [$user] - Either the username or user ID being searched for.
* @return {bool|array}
*/
public function get( $user ) {
if ( empty( self::$group ) ) {
self::$group = new Group;
}
$user = (string) $user;
$field = ( ctype_digit( $user ) ) ? 'ID' : 'username';
if ( $field == 'username' ) {
if ( !Forms::checkUsername( $user ) ) {
Debug::info( 'modelUser->get Username improperly formatted.' );
return false;
}
} else {
if ( !Check::id( $user ) ) {
Debug::info( 'modelUser->get Invalid ID.' );
return false;
}
}
$data = self::$db->get( $this->tableName, [ $field, '=', $user ] );
if ( !$data->count() ) {
Debug::info( "modelUser->get User not found: $user" );
return false;
}
$this->data = $this->filter( $data->first() );
return $this->data;
}
/**
* Find a user by email address.
*
* @param {string} [$email] - The email being searched for.
* @return {bool}
*/
public function findByEmail( $email ) {
if ( Check::email( $email ) ) {
$data = self::$db->get( $this->tableName, [ 'email', '=', $email ] );
if ( $data->count() ) {
$this->data = $data->first();
return true;
}
}
Debug::error( "modelUser->findByEmail - User not found by email: $email" );
return false;
}
/**
* Create a new user.
*
* @param {array} [$fields] - The New User's data.
* @return {bool}
*/
public function create( $fields = [] ) {
if ( empty( $fields ) ) {
return false;
}
if ( !isset( $fields['email' ] ) ) {
return false;
}
if ( !isset( $fields['prefs' ] ) ) {
$fields['prefs'] = json_encode( $this->getDefaultPreferences() );
}
if ( !isset( $fields['userGroup' ] ) ) {
$fields['userGroup'] = Config::getValue( 'group/defaultGroup' );
} else {
if ( in_array( $fields['userGroup'], [ '1', 1 ] ) ) {
if ( App::$activeGroup && 'Super' !== App::$activeGroup->name ) {
Debug::error( 'You do not have permission to do this.' );
}
}
}
if ( !isset( $fields['registered' ] ) ) {
$fields['registered'] = time();
}
if ( !isset( $fields['confirmed' ] ) ) {
$code = Code::genConfirmation();
$fields['confirmed'] = 0;
$fields['confirmationCode'] = $code;
Email::send( $fields['email'], 'confirmation', $code, [ 'template' => true ] );
}
if ( !self::$db->insert( $this->tableName, $fields ) ) {
Debug::error( 'User not created.' );
return false;
}
return true;
}
/**
* Update a user database entry.
*
* @param {array} [$fields] - The fields to be updated.
* @param {int} [$id] - The user ID being updated.
* @return {bool}
*/
public function update( $id, $fields = [] ) {
if ( !Check::id( $id ) ) {
return false;
}
if ( !self::$db->update( $this->tableName, $id, $fields ) ) {
new CustomException( 'userUpdate' );
Debug::error( "User: $id not updated: $fields" );
return false;
}
return true;
}
/**
* Update a user's preferences.
*
* @param {array} [$fields] - The fields to be updated.
* @param {int} [$id] - The user ID being updated.
* @return {bool}
*/
public function updatePrefs( $fields, $id ) {
if ( !Check::id( $id ) ) {
return false;
}
$userData = $this->get( $id );
$prefsInput = $userData->prefs;
foreach ( $fields as $name => $value ) {
$prefsInput[$name] = $value;
}
$fields = [ 'prefs' => json_encode( $prefsInput ) ];
if ( !self::$db->update( $this->tableName, $id, $fields ) ) {
Debug::error( "User: $id not updated." );
return false;
}
if ( $id === App::$activeUser->ID ) {
$userData = $this->get( $id );
App::$activeUser = $userData;
}
return true;
}
/**
* Return the most recent database data.
*
* @return {array} - An array of the user data.
*/
public function data() {
return $this->data;
}
public function authorize( $username, $password ) {
if ( !isset( self::$log ) ) {
self::$log = new Log;
}
if ( !$this->get( $username ) ) {
self::$log->login( 0, "API: User not found: $username" );
return false;
}
// login attempts protection.
$timeLimit = ( time() - 3600 );
$limit = Config::getValue( 'main/loginLimit' );
$user = $this->data();
if ( $limit > 0 ) {
$limitCheck = self::$db->get(
'logs',
[
'source', '=', 'login',
'AND',
'userID', '=', $user->ID,
'AND',
'time', '>=', $timeLimit,
'AND',
'action', '!=', 'pass',
]
);
if ( $limitCheck->count() >= $limit ) {
self::$log->login( $user->ID, 'API: Too many failed attempts.' );
return false;
}
}
if ( !Check::password( $password ) ) {
self::$log->login( $user->ID, 'API: Invalid Password.' );
return false;
}
if ( !Hash::check( $password, $user->password ) ) {
self::$log->login( $user->ID, 'API: Wrong Password.' );
return false;
}
self::$log->login( $this->data()->ID, 'API: pass' );
return $user;
}
}