* @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', '' ], [ 'auth_token', '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', ], ], ]; 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->getpaginated( $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; } return true; } /** * Return the most recent database data. * * @return {array} - An array of the user data. */ public function data() { return $this->data; } public function findByToken( $token ) { $data = self::$db->get( $this->tableName, [ 'auth_token', '=', $token ] ); if ( ! $data->count() ) { return false; } return $data->first(); } public function addAccessToken( $id, $length = 64 ) { if ( ! Check::id( $id ) ) { return false; } $fields = [ 'auth_token' => $this->generateRandomString( $length ) ]; if ( !self::$db->update( $this->tableName, $id, $fields ) ) { Debug::error( "User: $id not updated." ); return false; } return true; } private function generateRandomString( $length = 10 ) { $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; $charactersLength = strlen( $characters ); $randomString = ''; for ($i = 0; $i < $length; $i++) { $randomString .= $characters[random_int(0, $charactersLength - 1)]; } return $randomString; } }