TimeTrex/classes/modules/api/unauthenticated/APIAuthentication.class.php

1647 lines
78 KiB
PHP
Raw Normal View History

2022-12-13 07:10:06 +01:00
<?php
/*********************************************************************************
*
* TimeTrex is a Workforce Management program developed by
* TimeTrex Software Inc. Copyright (C) 2003 - 2021 TimeTrex Software Inc.
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License version 3 as published by
* the Free Software Foundation with the addition of the following permission
* added to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED
* WORK IN WHICH THE COPYRIGHT IS OWNED BY TIMETREX, TIMETREX DISCLAIMS THE
* WARRANTY OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
*
* You should have received a copy of the GNU Affero General Public License along
* with this program; if not, see http://www.gnu.org/licenses or write to the Free
* Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301 USA.
*
*
* You can contact TimeTrex headquarters at Unit 22 - 2475 Dobbin Rd. Suite
* #292 West Kelowna, BC V4T 2E9, Canada or at email address info@timetrex.com.
*
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
*
* In accordance with Section 7(b) of the GNU Affero General Public License
* version 3, these Appropriate Legal Notices must retain the display of the
* "Powered by TimeTrex" logo. If the display of the logo is not reasonably
* feasible for technical reasons, the Appropriate Legal Notices must display
* the words "Powered by TimeTrex".
*
********************************************************************************/
/**
* @package API\UnAuthenticated
*/
class APIAuthentication extends APIFactory {
protected $main_class = 'Authentication';
/**
* APIAuthentication constructor.
*/
public function __construct() {
parent::__construct(); //Make sure parent constructor is always called.
return true;
}
/**
* @param null $user_name
* @param null $password
* @return array
*/
function PunchLogin( $user_name = null, $password = null ) {
global $config_vars, $authentication;
Debug::Text( 'Quick Punch ID: ' . $user_name, __FILE__, __LINE__, __METHOD__, 10 );
if ( ( isset( $config_vars['other']['installer_enabled'] ) && $config_vars['other']['installer_enabled'] == 1 ) || ( isset( $config_vars['other']['down_for_maintenance'] ) && $config_vars['other']['down_for_maintenance'] == 1 ) ) {
Debug::text( 'WARNING: Installer is enabled... Normal logins are disabled!', __FILE__, __LINE__, __METHOD__, 10 );
//When installer is enabled, just display down for maintenance message to user if they try to login.
$error_message = TTi18n::gettext( '%1 is currently undergoing maintenance. We apologize for any inconvenience this may cause, please try again later. (%2)', [ APPLICATION_NAME, ( ( isset( $config_vars['other']['installer_enabled'] ) && $config_vars['other']['installer_enabled'] == 1 ) ? 'INSTALL' : 'MAINT' ) ] );
$validator_obj = new Validator();
$validator_stats = [ 'total_records' => 1, 'valid_records' => 0 ];
$validator_obj->isTrue( 'user_name', false, $error_message );
$validator = [];
$validator[0] = $validator_obj->getErrorsArray();
return $this->returnHandler( false, 'VALIDATION', TTi18n::getText( 'INVALID DATA' ), $validator, $validator_stats );
}
if ( isset( $config_vars['other']['web_session_expire'] ) && $config_vars['other']['web_session_expire'] != '' ) {
$authentication->setEnableExpireSession( (int)$config_vars['other']['web_session_expire'] );
}
$clf = TTnew( 'CompanyListFactory' ); /** @var CompanyListFactory $clf */
$clf->getByPhoneID( $user_name );
if ( $clf->getRecordCount() == 1 ) {
$c_obj = $clf->getCurrent();
} else {
$c_obj = false;
}
//Checks user_name/password
$password_result = false;
$user_name = trim( $user_name );
if ( $user_name != '' && $password != '' && ( is_object( $c_obj ) && in_array( $c_obj->getStatus(), [ 10, 20 ] ) && getTTProductEdition() >= TT_PRODUCT_PROFESSIONAL && $c_obj->getProductEdition() >= TT_PRODUCT_PROFESSIONAL ) ) { //Allow QuickPunch logins when company is on hold so employees can still punch in/out.
$password_result = $authentication->Login( $user_name, $password, 'QUICK_PUNCH_ID' );
}
if ( $password_result === true ) {
$clf = TTnew( 'CompanyListFactory' ); /** @var CompanyListFactory $clf */
$clf->getByID( $authentication->getObject()->getCompany() );
$current_company = $clf->getCurrent();
unset( $clf );
$create_new_station = false;
//If this is a new station, insert it now.
if ( isset( $_COOKIE['StationID'] ) ) {
Debug::text( 'Station ID Cookie found! ' . $_COOKIE['StationID'], __FILE__, __LINE__, __METHOD__, 10 );
$slf = TTnew( 'StationListFactory' ); /** @var StationListFactory $slf */
$slf->getByStationIdandCompanyId( $_COOKIE['StationID'], $current_company->getId() );
$current_station = $slf->getCurrent();
unset( $slf );
if ( $current_station->isNew() ) {
Debug::text( 'Station ID is NOT IN DB!! ' . $_COOKIE['StationID'], __FILE__, __LINE__, __METHOD__, 10 );
$create_new_station = true;
}
} else {
$create_new_station = true;
}
if ( $create_new_station == true ) {
//Insert new station
$sf = TTnew( 'StationFactory' ); /** @var StationFactory $sf */
$sf->setCompany( $current_company->getId() );
$sf->setStatus( 20 ); //Enabled
if ( Misc::detectMobileBrowser() == false ) {
Debug::text( 'PC Station device...', __FILE__, __LINE__, __METHOD__, 10 );
$sf->setType( 10 ); //PC
} else {
$sf->setType( 26 ); //Mobile device web browser
Debug::text( 'Mobile Station device...', __FILE__, __LINE__, __METHOD__, 10 );
}
$sf->setSource( Misc::getRemoteIPAddress() );
$sf->setStation();
$sf->setDescription( substr( $_SERVER['HTTP_USER_AGENT'], 0, 250 ) );
if ( $sf->isValid() ) { //Standard Edition can't save mobile stations.
if ( $sf->Save( false ) ) {
$sf->setCookie();
}
}
}
return [ 'SessionID' => $authentication->getSessionId() ];
} else {
$validator_obj = new Validator();
$validator_stats = [ 'total_records' => 1, 'valid_records' => 0 ];
$error_column = 'quick_punch_id'; // match the correct input field in the html
$error_message = TTi18n::gettext( 'Quick Punch ID or Password is incorrect' );
//Get company status from user_name, so we can display messages for ONHOLD/Cancelled accounts.
if ( is_object( $c_obj ) ) {
//Allow QuickPunch when company is ON HOLD.
// if ( $c_obj->getStatus() == 20 ) {
// $error_message = TTi18n::gettext('Sorry, your company\'s account has been placed ON HOLD, please contact customer support immediately');
// } else
if ( $c_obj->getStatus() == 23 ) {
$error_message = TTi18n::gettext( 'Sorry, your trial period has expired, please contact our sales department to reactivate your account' );
} else if ( $c_obj->getStatus() == 28 ) {
if ( $c_obj->getMigrateURL() != '' ) {
$error_message = TTi18n::gettext( 'To better serve our customers your account has been migrated, please update your bookmarks to use the following URL from now on' ) . ': ' . 'http://' . $c_obj->getMigrateURL();
} else {
$error_message = TTi18n::gettext( 'To better serve our customers your account has been migrated, please contact customer support immediately.' );
}
} else if ( $c_obj->getStatus() == 30 ) {
$error_message = TTi18n::gettext( 'Sorry, your company\'s account has been CANCELLED, please contact customer support if you believe this is an error' );
}
}
$validator_obj->isTrue( $error_column, false, $error_message );
$validator = [];
$validator[0] = $validator_obj->getErrorsArray();
return $this->returnHandler( false, 'VALIDATION', TTi18n::getText( 'INVALID DATA' ), $validator, $validator_stats );
}
}
/**
* @param $user_name
* @return array|bool
*/
function getSessionTypeForLogin( $user_name ) {
global $config_vars;
$retval = [
'session_type' => 'user_name',
'mfa_type_id' => 0,
];
$ulf = TTnew( 'UserListFactory' ); /** @var UserListFactory $ulf */
$ulf->getByUserName( $user_name );
if ( $ulf->getRecordCount() == 1 && $ulf->getCurrent()->getMultiFactorType() > 0 && getTTProductEdition() >= TT_PRODUCT_PROFESSIONAL ) { //0 = Disabled
if ( isset( $config_vars['other']['disable_mfa'] ) && $config_vars['other']['disable_mfa'] == true ) {
//Login is forced to use user_name/password only when MFA is disabled.
Debug::Text( 'MFA is disabled. User has MFA enabled but the session has been forced to user_name (800)', __FILE__, __LINE__, __METHOD__, 10 );
} else {
$retval['session_type'] = 'user_name_multi_factor';
$retval['mfa_type_id'] = $ulf->getCurrent()->getMultiFactorType();
}
}
return $this->returnHandler( $retval );
}
/**
* @return array|bool
*/
function sendMultiFactorNotification() {
$authentication = new Authentication();
$authentication->setAndReadForMultiFactor();
$multi_factor_data = $authentication->getMultiFactorData();
if ( $multi_factor_data == false ) {
Debug::Text( 'No multifactor data found for session ID: ' . $authentication->getSessionId(), __FILE__, __LINE__, __METHOD__, 10 );
return $this->returnHandler( false );
}
if ( $multi_factor_data['time'] === '' || ( TTDate::getTime() - $multi_factor_data['time'] ) > $authentication->mfa_timeout_seconds ) {
Debug::Text( 'Multifactor data is expired, not sending notification', __FILE__, __LINE__, __METHOD__, 10 );
return $this->returnHandler( false );
}
$user_agent = null;
$user_agent_display = null;
if ( isset( $_SERVER['HTTP_USER_AGENT'] ) ) {
require_once( Environment::getBasePath() . 'vendor' . DIRECTORY_SEPARATOR . 'cbschuld' . DIRECTORY_SEPARATOR . 'browser.php' . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'Browser.php' );
$browser = new Browser( $_SERVER['HTTP_USER_AGENT'] );
$user_agent = $browser->getBrowser();
if ( $user_agent === 'unknown' ) {
$user_agent_display = $_SERVER['HTTP_USER_AGENT'];
} else {
$agent_arr = [];
if ( $browser->getBrowser() !== 'unknown' ) {
$agent_arr[] = $browser->getBrowser();
}
if ( $browser->getVersion() !== 'unknown' ) {
$agent_arr[] = 'v' . $browser->getVersion();
}
if ( $browser->getPlatform() !== 'unknown' ) {
$agent_arr[] = TTi18n::getText( 'on ' ) . $browser->getPlatform();
}
$user_agent_display = implode( ' ', $agent_arr );
}
unset( $browser );
}
$payload = [
'timetrex' => [
'event' => [
[
'type' => 'multi_factor_authenticate',
'data' => [
'user_id' => $multi_factor_data['user_id'],
'session_id' => $multi_factor_data['session_id'],
'key' => $multi_factor_data['key'],
'login_timestamp' => TTDate::getTime(),
'login_timestamp_display' => TTDate::getDate( 'DATE+TIME', TTDate::getTime() ),
'user_agent' => $user_agent,
'user_agent_display' => $user_agent_display,
'ip_address' => Misc::getRemoteIPAddress(),
'location' => Misc::getLocationOfIPAddress(),
],
],
],
],
'uri' => 'multi_factor_authenticate', //Required for app to trigger proper event on its side.
];
Debug::Arr( $payload, ' Sending background notification to users app to authenticate multifactor...', __FILE__, __LINE__, __METHOD__, 10 );
$notification_data = [
'object_id' => $authentication->getSessionId(),
'user_id' => $multi_factor_data['user_id'],
'type_id' => 'system',
'object_type_id' => 0,
'priority' => 1, //1=Highest
'title_short' => TTi18n::getText( 'Verify your identity' ),
'body_short' => TTi18n::getText( 'Are you logging into TimeTrex currently?' ),
'payload' => $payload,
'time_to_live' => $authentication->mfa_timeout_seconds,
'device_id' => [ 32768 ], //App Only.
'save_notification' => false,
];
Notification::sendNotification( $notification_data );
return $this->returnHandler( true );
}
/**
* Starts a 30 second listen loop waiting for a multifactor authentication response from validateMultiFactor().
* This loop runs for the device trying to log and that waits for a multifactor approval from another device or API request.
* @return bool
* @throws DBError
*/
public function listenForMultiFactorAuthentication( $enable_trusted_device = false ) {
global $config_vars, $db, $cache;
// When using memoryCaching, this long-running process will never get an updated cache from a different process during the listen loop.
// But if technically caching is *always* enabled, even when disabled we switch to memoryCaching only mode.
// So when caching is disabled in the .ini, disable it fully.
// When caching is enabled, just disable memoryCaching, so cache invalidations can be picked up in this long-running process.
// Note: $authentication->setAndReadForMultiFactor() already skips cache. But keeping this here as a safety net.
if ( !isset( $config_vars['cache']['enable'] ) || $config_vars['cache']['enable'] == false ) {
$cache->_caching = false;
}
$cache->_memoryCaching = false;
Debug::Text( 'Multifactor enabled! Listening for auth response.', __FILE__, __LINE__, __METHOD__, 10 );
$config_vars['database']['persistent_connections'] = true; //Force persistent connections so LISTEN/NOTIFY works properly.
$authentication = new Authentication();
$authentication->setAndReadForMultiFactor();
$multi_factor_data = $authentication->getMultiFactorData();
if ( $multi_factor_data == false ) {
Debug::Text( 'No multifactor data found for session ID: ' . $authentication->getSessionID(), __FILE__, __LINE__, __METHOD__, 10 );
return $this->returnHandler( [ 'status' => 'cancelled' ] );
} else {
Debug::Arr( $multi_factor_data, ' Initial Multifactor data for Session ID: ' . $authentication->getSessionID(), __FILE__, __LINE__, __METHOD__, 10 );
}
if ( isset( $multi_factor_data['time'] ) && ( TTDate::getTime() - $multi_factor_data['time'] < $authentication->mfa_timeout_seconds ) ) {
//If current multifactor data is less than X minutes old and already authenticated, we not need to start a listen.
if ( isset( $multi_factor_data['login_approved'] ) == true && $multi_factor_data['login_approved'] == true ) {
Debug::Text( 'Multifactor data is already authenticated, not starting listen', __FILE__, __LINE__, __METHOD__, 10 );
//login_approved is a one time flag to know if this session has already been authenticated, and if it has, we don't need to start a listen. The flag can now be cleared so future listen requests can start a new listen.
$authentication->clearMultiFactorDataFlags( [ 'login_approved' ] );
return $this->returnHandler( [ 'status' => 'completed' ] );
}
}
Debug::text( ' Listening for Multifactor authentication. Started at: ' . TTDate::getTime(), __FILE__, __LINE__, __METHOD__, 10 );
$db->Execute( 'LISTEN "multi_auth:' . $db->qStr( TTUUID::castUUID( $multi_factor_data['user_id'] ) ) . '"' );
$i = 0;
while ( $i < 30 ) {
$_SERVER['REQUEST_TIME_FLOAT'] = microtime( true ); //This restarts the DEBUG time on each loop as if its a separate request. This helps prevent long request WARNING from being triggered like in reports.
$auth_status = pg_get_notify( $db->_connectionID );
if ( isset( $auth_status['payload'] ) && $auth_status['payload'] != '' ) {
$mfa_response_payload = json_decode( $auth_status['payload'], true );
if ( $mfa_response_payload['success'] == true ) {
Debug::Text( ' PG Notify Auth Payload Received: ' . $auth_status['payload'], __FILE__, __LINE__, __METHOD__, 10 );
if ( $mfa_response_payload['key'] == $multi_factor_data['expected_key'] ) {
//Can continue with login.
Debug::text( ' SUCCESS: Notify auth request accepted, login allowed. Received at: ' . TTDate::getTime(), __FILE__, __LINE__, __METHOD__, 10 );
//Authenticaiton may have outdated data depending on how long listen has been running. Need to re-read it.
$authentication = new Authentication();
$authentication->setAndReadForMultiFactor();
//Authentication may have outdated data depending on how long listen has been running. We may need to re-read it.
// **NOTE: When using database replication, its possible this data could still be out-dated. So we need to make sure we aren't writing any data to the authentication table here.
// If we do, then it should be wrapped in a transaction so its always reads/writes to/from the master database node.
if ( $enable_trusted_device == true ) {
$authentication->setTrustedDevice();
}
return $this->returnHandler( [ 'status' => 'completed' ] );
} else {
Debug::Text( ' FAILURE: Notify auth request failed as keys did not match, login denied', __FILE__, __LINE__, __METHOD__, 10 );
return $this->returnHandler( [ 'status' => 'cancelled' ] );
}
} else {
if ( isset( $mfa_response_payload['other_data']['restart_listen'] ) && $mfa_response_payload['other_data']['restart_listen'] == true ) {
Debug::text( ' Listen cancelled early as a restart listen request was received...', __FILE__, __LINE__, __METHOD__, 10 );
return $this->returnHandler( [ 'status' => 'restart_listen' ] );
}
Debug::Text( ' FAILURE: Notify auth request denied, login denied', __FILE__, __LINE__, __METHOD__, 10 );
return $this->returnHandler( [ 'status' => 'cancelled' ] );
}
}
$i++;
sleep( 1 );
}
return $this->returnHandler( [ 'status' => 'restart_listen' ] );
}
/**
* This is called by the app/device that approves or denies the multi-factor authentication request.
* It then sends a SQL Notify message to listenForMultiFactorAuthentication() to let it know if the request was approved or denied.
* @param $validation_allowed bool
* @param $session_id string
* @param $key string
* @param null $other_data
* @return array|bool
* @throws DBError
*/
public function validateMultiFactor( $validation_allowed, $session_id, $key, $other_data = null ) {
global $db;
Debug::Text( 'Attempting to validate multifactor.', __FILE__, __LINE__, __METHOD__, 10 );
Debug::Text( 'App sent a validation allowed response of: ' . Misc::HumanBoolean( $validation_allowed ), __FILE__, __LINE__, __METHOD__, 10 );
$authentication = new Authentication();
$authentication->setAndReadForMultiFactor( $session_id );
$multi_factor_data = $authentication->getMultiFactorData();
$other_json = $authentication->getOtherJSON();
if ( $multi_factor_data !== false ) { //There is a pending multinfactor auth request.
Debug::Arr( $multi_factor_data, 'Multifactor data: ', __FILE__, __LINE__, __METHOD__, 10 );
Debug::Arr( $other_json, 'Other JSON data: ', __FILE__, __LINE__, __METHOD__, 10 );
$error_message = null;
//App neede to be logged in to validate a session and the user_id needs to match for both sessions.
if ( ( $this->isLoggedIn() == false || $this->getCurrentUserObject()->getId() != $multi_factor_data['user_id'] ) ) {
Debug::Text( ' FAILURE: Session is not allowed to be validated, as the session attempting to validate is not logged in or user mismatch.', __FILE__, __LINE__, __METHOD__, 10 );
$validation_allowed = false; //Not allowed to validate if the app is not logged in or users mismatch.
//If we are requesting listen to be restarted for any reason, we do not need to send a validation error for user not being logged in.
//This is because there is a legitimate scenario where the user is not yet logged in but the listen needs to be restarted.
if ( isset( $other_data['restart_listen'] ) == false || $other_data['restart_listen'] == false ) {
//Restart listen was not requested or is false, so send a validation error that the user is not logged in.
$error_message = TTi18n::getText( 'Unable to validate identity, please login and try again.' ); //Session is not allowed to be validated, as the session attempting to validate is not logged in or user mismatch.
}
}
if ( ( $key !== $multi_factor_data['expected_key'] || $multi_factor_data['expected_key'] == false ) ) {
Debug::Text( ' FAILURE: Session is not allowed to be validated, as the key does not match or is invalid.', __FILE__, __LINE__, __METHOD__, 10 );
$error_message = TTi18n::getText( 'Unable to validate identity due to key mismatch, please try again.' ); //Session is not allowed to be validated, there was a authentication mismatch.
$validation_allowed = false;
}
if ( ( $multi_factor_data['time'] == false || ( TTDate::getTime() - $multi_factor_data['time'] ) > $authentication->mfa_timeout_seconds ) ) {
Debug::Text( ' FAILURE: Session is not allowed to be validated, as the the multifactor attempt has expired.', __FILE__, __LINE__, __METHOD__, 10 );
$error_message = TTi18n::getText( 'Identity validation request has expired, please login again.' ); //Session is not allowed to be validated, as the the multifactor attempt has expired.
$validation_allowed = false;
}
if ( $validation_allowed == true ) {
//If login is already approved we do not reauthenticate or modify the session as it already has been authenticated.
if ( ( isset( $other_json['mfa']['login_approved'] ) == false || $other_json['mfa']['login_approved'] == false ) ) {
Debug::text( 'Sending multifactor response: SUCCESS', __FILE__, __LINE__, __METHOD__, 10 );
$other_json['mfa']['login_approved'] = true; //One time flag for API to check before starting listen, incase it was already approved.
$other_json['mfa']['recent_authenticated_device_session_id'] = $authentication->encryptSessionID( $authentication->getCurrentSessionID( 800 ) ); //Store the session ID of the device that just authenticated this request.
$authentication->setOtherJSON( $other_json );
if ( $multi_factor_data['is_reauthentication_request'] === true ) { //This may just be a setup test or reauthentication, we do not want to upgrade the session type.
$authentication->setReauthenticatedSession();
TTLog::addEntry( $authentication->getObjectID(), 102, TTi18n::getText( 'From' ) . ': ' . Misc::getLocationOfIPAddress( $authentication->getIPAddress() ) . ' (' . $authentication->getIPAddress() . ') ' . TTi18n::getText( 'Type' ) . ': user_name_multi_factor ' . TTi18n::getText( 'SessionID' ) . ': ' . $authentication->getSecureSessionID() . ' ' . TTi18n::getText( 'Client' ) . ': ' . $authentication->getClientID() . ' ' . TTi18n::getText( 'End Point' ) . ': ' . $authentication->getEndPointID() . ' ' . TTi18n::getText( 'ObjectID' ) . ': ' . $authentication->getObjectID(), $authentication->getObjectID(), 'authentication' ); //ReAuthenticate
} else if ( $multi_factor_data['type_id'] == 0 || $multi_factor_data['type_id'] == 800 ) {
$authentication->setType( 810 );
//This updates the user table and not authentication table.
$authentication->UpdateLastLoginDate( $multi_factor_data['user_id'] );
//Login is fully successful, so add "Login" audit log entry.
TTLog::addEntry( $authentication->getObjectID(), 100, TTi18n::getText( 'From' ) . ': ' . Misc::getLocationOfIPAddress( $authentication->getIPAddress() ) . ' (' . $authentication->getIPAddress() . ') ' . TTi18n::getText( 'Type' ) . ': user_name_multi_factor ' . TTi18n::getText( 'SessionID' ) . ': ' . $authentication->getSecureSessionID() . ' ' . TTi18n::getText( 'Client' ) . ': ' . $authentication->getClientID() . ' ' . TTi18n::getText( 'End Point' ) . ': ' . $authentication->getEndPointID() . ' ' . TTi18n::getText( 'ObjectID' ) . ': ' . $authentication->getObjectID(), $authentication->getObjectID(), 'authentication' ); //Login
} else {
Debug::text( 'Not modifying session, as not reauthenticating and not valid Type ID to promote: ' . $multi_factor_data['type_id'], __FILE__, __LINE__, __METHOD__, 10 );
}
$authentication->clearMultiFactorDataFlags( [ 'key' ] ); //Key has been consumed and is no longer valid and cannot be re-used.
} else {
Debug::text( 'Login has alrady been approved, not updating session...', __FILE__, __LINE__, __METHOD__, 10 );
}
} else {
Debug::text( 'NOTICE: $validation_allowed is false, not updating session!', __FILE__, __LINE__, __METHOD__, 10 );
}
$response = [
'success' => (bool)$validation_allowed,
'key' => preg_replace( '/[^a-zA-Z0-9]/', '', (string)$key ), //Sanitizing the key to only contain alphanumeric characters to help prevent SQL injection.
'other_data' => [],
];
//Other data is optional but can contain further information/commands for various multifactor types.
//It contains only specific data and does not allow arbitrary data to be passed in.
if ( isset( $other_data['restart_listen'] ) && $other_data['restart_listen'] == true ) {
$response['other_data']['restart_listen'] = true;
}
Debug::Arr( $response, 'Sending multifactor notify message.', __FILE__, __LINE__, __METHOD__, 10 );
$db->Execute( 'NOTIFY "multi_auth:' . $db->qStr( TTUUID::castUUID( $multi_factor_data['user_id'] ) ) . '", ' . $db->qStr( json_encode( $response ) ) );
if ( $error_message == null ) {
return $this->returnHandler( true );
} else {
return $this->returnHandler( false, 'VALIDATION', $error_message );
}
} else {
Debug::text( 'No multifactor data for session...', __FILE__, __LINE__, __METHOD__, 10 );
}
return $this->returnHandler( false, 'VALIDATION', TTi18n::getText( 'Failed to validate multifactor as the request may have expired.' ) );
}
/**
* Default username=NULL to prevent argument warnings messages if its not passed from the API.
* @param null $user_name
* @param null $password
* @param string $type
* @param bool $reauthenticate_only
* @return array|bool
*/
function Login( $user_name = null, $password = null, $type = 'USER_NAME', $reauthenticate_only = false ) {
global $config_vars, $authentication;
//Prevent: NOTICE(8): Array to string conversion below.
if ( is_array( $user_name ) ) {
$user_name = '';
}
if ( is_array( $password ) ) {
$password = '';
}
Debug::text( 'User Name: ' . $user_name . ' Password Length: ' . strlen( $password ) . ' Type: ' . $type, __FILE__, __LINE__, __METHOD__, 10 );
if ( ( isset( $config_vars['other']['installer_enabled'] ) && $config_vars['other']['installer_enabled'] == 1 ) || ( isset( $config_vars['other']['down_for_maintenance'] ) && $config_vars['other']['down_for_maintenance'] == 1 ) ) {
Debug::text( 'WARNING: Installer is enabled... Normal logins are disabled!', __FILE__, __LINE__, __METHOD__, 10 );
//When installer is enabled, just display down for maintenance message to user if they try to login.
$error_message = TTi18n::gettext( '%1 is currently undergoing maintenance. We apologize for any inconvenience this may cause, please try again later. (%2)', [ APPLICATION_NAME, ( ( isset( $config_vars['other']['installer_enabled'] ) && $config_vars['other']['installer_enabled'] == 1 ) ? 'INSTALL' : 'MAINT' ) ] );
$validator_obj = new Validator();
$validator_stats = [ 'total_records' => 1, 'valid_records' => 0 ];
$validator_obj->isTrue( 'user_name', false, $error_message );
$validator = [];
$validator[0] = $validator_obj->getErrorsArray();
return $this->returnHandler( false, 'VALIDATION', TTi18n::getText( 'INVALID DATA' ), $validator, $validator_stats );
}
if ( isset( $config_vars['other']['web_session_expire'] ) && $config_vars['other']['web_session_expire'] != '' ) {
$authentication->setEnableExpireSession( (int)$config_vars['other']['web_session_expire'] );
}
$login_method = $this->stripReturnHandler( $this->getSessionTypeForLogin( $user_name ) );
if ( strtolower( $type ) === 'user_name' ) {
//Don't allow users to circumvent the multifactor auth process, by forcing 'user_name' login type when MFA is enabled.
$type = $login_method['session_type'];
}
if ( strtolower( $type ) === 'user_name_multi_factor' && $authentication->getClientIDHeader() == 'App-TimeTrex' && Misc::getMobileAppClientVersion() != '' && version_compare( Misc::getMobileAppClientVersion(), '5.1.10', '<' ) == true ) {
return $this->returnHandler( false, 'VALIDATION', TTi18n::getText( 'Please upgrade to a newer version of the app that supports multifactor authentication.' ) );
}
//Only allow reauthentication for the currently logged-in user.
if ( $reauthenticate_only && ( $this->isLoggedIn() === false || $this->getCurrentUserObject()->checkUsername( $user_name ) === false ) ) {
$validator_obj = new Validator();
$validator_obj->isTrue( 'user_name', false, TTi18n::gettext( 'Authenticating for invalid employee.' ) );
$validator_stats = [ 'total_records' => 1, 'valid_records' => 0 ];
$validator[0] = $validator_obj->getErrorsArray();
return $this->returnHandler( false, 'VALIDATION', TTi18n::getText( 'INVALID DATA' ), $validator, $validator_stats );
}
if ( $authentication->Login( $user_name, $password, $type, true, $reauthenticate_only, $login_method['mfa_type_id'] ) === true ) {
if ( strtolower( $type ) === 'user_name_multi_factor' ) {
Debug::text( 'MFA Login Success, Session ID: ' . $authentication->getSessionId(), __FILE__, __LINE__, __METHOD__, 10 );
$authentication->generateMultiFactorAuthKey( $reauthenticate_only );
return $this->returnHandler( $authentication->getAuthenticationResponseData( TTi18n::getText( 'Please check your device to verify your identity' ) ) );
} else {
Debug::text( 'Login Success, Session ID: ' . $authentication->getSessionId(), __FILE__, __LINE__, __METHOD__, 10 );
//Return different data stucture to end-points that support MFA. Only Browser and Mobile App >=5.1.10 for now.
if ( $authentication->getClientIDHeader() == 'Browser-TimeTrex' || ( $authentication->getClientIDHeader() == 'App-TimeTrex' && Misc::getMobileAppClientVersion() != '' && version_compare( Misc::getMobileAppClientVersion(), '5.1.10', '>=' ) == true ) ) {
if ( $reauthenticate_only == true ) {
$authentication->setReauthenticatedSession( true );
}
return $this->returnHandler( $authentication->getAuthenticationResponseData() );
} else {
Debug::text( ' Returning raw Session ID to legacy API/App: ' . $authentication->getSessionId(), __FILE__, __LINE__, __METHOD__, 10 );
return $authentication->getSessionId(); //Legacy app versions don't support MFA, so just return the session ID.
}
}
} else {
$validator_obj = new Validator();
$validator_stats = [ 'total_records' => 1, 'valid_records' => 0 ];
$error_column = 'user_name';
if ( $reauthenticate_only == true ) {
$error_message = TTi18n::gettext( 'Password is incorrect' );
} else {
$error_message = TTi18n::gettext( 'User Name or Password is incorrect' );
}
//Get company status from user_name, so we can display messages for ONHOLD/Cancelled accounts.
$clf = TTnew( 'CompanyListFactory' ); /** @var CompanyListFactory $clf */
$clf->getByUserName( $user_name );
if ( $clf->getRecordCount() > 0 ) {
$c_obj = $clf->getCurrent();
if ( $c_obj->getStatus() == 20 ) {
$error_message = TTi18n::gettext( 'Sorry, your company\'s account has been placed ON HOLD, please contact customer support immediately' );
} else if ( $c_obj->getStatus() == 23 ) {
$error_message = TTi18n::gettext( 'Sorry, your trial period has expired, please contact our sales department to reactivate your account' );
} else if ( $c_obj->getStatus() == 28 ) {
if ( $c_obj->getMigrateURL() != '' ) {
$migrate_url = ( Misc::isSSL() == true ) ? 'https://' . $c_obj->getMigrateURL() : 'http://' . $c_obj->getMigrateURL();
$error_message = TTi18n::gettext( 'To better serve our customers your account has been migrated, please update your bookmarks to use the following URL from now on' ) . ': ' . '<a href="' . $migrate_url . '">' . $migrate_url . '</a>';
} else {
$error_message = TTi18n::gettext( 'To better serve our customers your account has been migrated, please contact customer support immediately.' );
}
} else if ( $c_obj->getStatus() == 30 ) {
$error_message = TTi18n::gettext( 'Sorry, your company\'s account has been CANCELLED, please contact customer support if you believe this is an error' );
} else {
$ulf = TTnew( 'UserListFactory' ); /** @var UserListFactory $ulf */
$ulf->getByUserName( $user_name );
if ( $ulf->getRecordCount() == 1 ) {
$u_obj = $ulf->getCurrent(); /** @var UserFactory $u_obj */
if ( $u_obj->checkPassword( $password, false, false ) == true ) {
if ( $u_obj->getEnableLogin() == false ) {
$error_message = TTi18n::gettext( 'Sorry, your login is currently disabled, please contact your supervisor or manager to request access.' );
} else {
if ( $u_obj->isFirstLogin() == true && $u_obj->isCompromisedPassword() == true ) {
$error_message = TTi18n::gettext( 'Welcome to %1, since this is your first time logging in, we ask that you change your password to something more secure', [ APPLICATION_NAME ] );
$error_column = 'password';
} else if ( $u_obj->isCompromisedPassword() == true && $u_obj->isFirstLogin() == false ) {
$error_message = TTi18n::gettext( 'Due to your company\'s password policy, your password must be changed immediately' );
$error_column = 'password';
} else if ( $u_obj->isPasswordPolicyEnabled() == true ) {
if ( $u_obj->checkPasswordAge() == false ) {
//Password policy is enabled, confirm users password has not exceeded maximum age.
//Make sure we confirm that the password is in fact correct, but just expired.
$error_message = TTi18n::gettext( 'Your password has exceeded its maximum age specified by your company\'s password policy and must be changed immediately' );
$error_column = 'password';
}
} else {
Debug::text( ' Password matches, but other criteria denied...', __FILE__, __LINE__, __METHOD__, 10 );
}
}
} else {
if ( $u_obj->checkLoginPermissions() == false ) {
$error_message = TTi18n::gettext( 'Sorry, you don\'t have permission to login, please contact your supervisor or manager to request access.' );
}
}
} else {
Debug::text( ' User Name: ' . $user_name .' record count: '. $ulf->getRecordCount(), __FILE__, __LINE__, __METHOD__, 10 );
}
unset( $ulf, $u_obj );
}
} else {
Debug::text( ' User Name: ' . $user_name .' linked to company, record count: '. $clf->getRecordCount(), __FILE__, __LINE__, __METHOD__, 10 );
}
$validator_obj->isTrue( $error_column, false, $error_message );
$validator[0] = $validator_obj->getErrorsArray();
return $this->returnHandler( false, 'VALIDATION', TTi18n::getText( 'INVALID DATA' ), $validator, $validator_stats );
}
}
/**
* Register permanent API key Session ID to be used for all subsequent API calls without needing a username/password.
* @param string $user_name
* @param string $password
* @return bool|string
*/
function registerAPIKey( $user_name, $password, $end_point = null ) {
$session_type = $this->stripReturnHandler( $this->getSessionTypeForLogin( $user_name ) );
if ( $session_type['mfa_type_id'] > 0 ) {
//This function is not available for MFA enabled users.
return false;
}
//Always require UserName/Password when registering an API key, as from a security stand-point this is similar to changing passwords.
// If its a master administrator, only need to register an API key for the master administrator employee, then they can switchUser() to any other user as needed with that same key.
if ( $user_name != '' && $password != '' ) {
$authentication = new Authentication();
$api_key = $authentication->registerAPIKey( $user_name, $password, $end_point );
Debug::text( 'User Name: ' . $user_name .' API Key: '. $api_key .' End Point: '. $end_point, __FILE__, __LINE__, __METHOD__, 10 );
return $api_key; //Don't wrap in return handler.
}
return false;
}
/**
* @param $end_point
* @return array|bool
*/
function registerAPIKeyForCurrentUser( $end_point = 'json/api' ) {
global $authentication;
if ( $this->isLoggedIn() == false ) {
return $this->returnHandler( false );
}
if ( $authentication->isSessionReauthenticated() === false ) {
return $this->getPermissionObject()->ReauthenticationRequired( $this->getCurrentUserObject() );
}
$api_key = $authentication->registerAPIKeyForCurrentUser( $end_point );
//One time auth is used to verify single actions that require re-authentication and needs to be removed after used.
$authentication->reauthenticationActionCompleted();
return $this->returnHandler( $api_key );
}
/**
* @param string $user_id UUID
* @param string $invoice_invoice_client_id UUID
* @param string $ip_address
* @param string $user_agent
* @param string $client_id UUID
* @param string $end_point_id
* @param null $type_id
* @return array|bool
* @throws DBError
* @throws ReflectionException
*/
function newSession( $user_id, $invoice_invoice_client_id = null, $ip_address = null, $user_agent = null, $client_id = null, $end_point_id = null, $type_id = null ) {
global $authentication;
if ( is_object( $authentication ) && $authentication->getSessionID() != '' ) {
Debug::text( 'Session ID: ' . $authentication->getSessionID() .' Encrypted: '. $authentication->encryptSessionID( $authentication->getSessionID() ) .' Type ID: '. $authentication->getType(), __FILE__, __LINE__, __METHOD__, 10 );
if ( $this->getPermissionObject()->checkAuthenticationType( 700 ) == false ) { //700=API Key
return $this->getPermissionObject()->AuthenticationTypeDenied();
}
if ( $this->getPermissionObject()->Check( 'company', 'view' ) && $this->getPermissionObject()->Check( 'company', 'login_other_user' ) ) {
if ( TTUUID::isUUID( $user_id ) == false ) { //If username is used, lookup user_id
Debug::Text( 'Lookup User ID by UserName: ' . $user_id, __FILE__, __LINE__, __METHOD__, 10 );
$ulf = TTnew( 'UserListFactory' ); /** @var UserListFactory $ulf */
$ulf->getByUserName( trim( $user_id ) );
if ( $ulf->getRecordCount() == 1 ) {
$user_id = $ulf->getCurrent()->getID();
}
}
$ulf = TTnew( 'UserListFactory' ); /** @var UserListFactory $ulf */
$ulf->getByIdAndStatus( TTUUID::castUUID( $user_id ), 10 ); //Can only switch to Active employees
if ( $ulf->getRecordCount() == 1 ) {
$new_session_user_obj = $ulf->getCurrent();
if ( $client_id == '' ) {
$client_id = 'browser-timetrex';
}
if ( $end_point_id == '' ) {
$end_point_id = 'json/api';
}
if ( $user_agent == '' ) {
$user_agent = isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : null;
}
Debug::Text( 'Login as different user: ' . $user_id . ' IP Address: ' . $ip_address . ' Client ID: ' . $client_id . ' End Point ID: ' . $end_point_id . ' Type ID: '. $type_id .' User Agent: ' . $user_agent, __FILE__, __LINE__, __METHOD__, 10 );
$new_session_id = $authentication->newSession( $user_id, $ip_address, $user_agent, $client_id, $end_point_id, $type_id );
$retarr = [
'session_id' => $new_session_id,
'url' => Misc::getHostName( false ) . Environment::getBaseURL(), //Don't include the port in the hostname, otherwise it can cause problems when forcing port 443 but not using 'https'.
'cookie_base_url' => Environment::getCookieBaseURL(),
];
//Add entry in source *AND* destination user log describing who logged in.
//Source user log, showing that the source user logged in as someone else.
TTLog::addEntry( $this->getCurrentUserObject()->getId(), 100, TTi18n::getText( 'Override Login' ) . ': ' . TTi18n::getText( 'SourceIP' ) . ': ' . $authentication->getIPAddress() . ' ' . TTi18n::getText( 'SessionID' ) . ': ' . $authentication->getSecureSessionID() . ' ' . TTi18n::getText( 'To Employee' ) . ': ' . $new_session_user_obj->getFullName() . ' (' . $user_id . ')', $this->getCurrentUserObject()->getId(), 'authentication' );
//Destination user log, showing the destination user was logged in *by* someone else.
TTLog::addEntry( $user_id, 100, TTi18n::getText( 'Override Login' ) . ': ' . TTi18n::getText( 'SourceIP' ) . ': ' . $authentication->getIPAddress() . ' ' . TTi18n::getText( 'SessionID' ) . ': ' . $authentication->getSecureSessionID() . ' ' . TTi18n::getText( 'By Employee' ) . ': ' . $this->getCurrentUserObject()->getFullName() . ' (' . $user_id . ')', $user_id, 'authentication' );
return $this->returnHandler( $retarr );
}
} else {
Debug::text( ' ERROR: Permission check failed for logging in as another user...', __FILE__, __LINE__, __METHOD__, 10 );
}
}
return false;
}
/**
* Accepts user_id or user_name.
* @param string $user_id UUID
* @return bool
*/
function switchUser( $user_id ) {
global $authentication;
if ( is_object( $authentication ) && $authentication->getSessionID() != '' ) {
Debug::text( 'Session ID: ' . $authentication->getSessionID(), __FILE__, __LINE__, __METHOD__, 10 );
if ( $this->getPermissionObject()->checkAuthenticationType( 700 ) == false ) { //700=API Key
return $this->getPermissionObject()->AuthenticationTypeDenied();
}
if ( $this->getPermissionObject()->Check( 'company', 'view' ) && $this->getPermissionObject()->Check( 'company', 'login_other_user' ) ) {
if ( TTUUID::isUUID( $user_id ) == false ) { //If username is used, lookup user_id
Debug::Text( 'Lookup User ID by UserName: ' . $user_id, __FILE__, __LINE__, __METHOD__, 10 );
$ulf = TTnew( 'UserListFactory' ); /** @var UserListFactory $ulf */
$ulf->getByUserName( trim( $user_id ) );
if ( $ulf->getRecordCount() == 1 ) {
$user_id = $ulf->getCurrent()->getID();
}
}
$ulf = TTnew( 'UserListFactory' ); /** @var UserListFactory $ulf */
$ulf->getByIdAndStatus( TTUUID::castUUID( $user_id ), 10 ); //Can only switch to Active employees
if ( $ulf->getRecordCount() == 1 ) {
Debug::Text( 'Login as different user: ' . $user_id, __FILE__, __LINE__, __METHOD__, 10 );
$authentication->changeObject( $user_id );
//Add entry in source *AND* destination user log describing who logged in.
//Source user log, showing that the source user logged in as someone else.
TTLog::addEntry( $this->getCurrentUserObject()->getId(), 100, TTi18n::getText( 'Override Login' ) . ': ' . TTi18n::getText( 'SourceIP' ) . ': ' . $authentication->getIPAddress() . ' ' . TTi18n::getText( 'SessionID' ) . ': ' . $authentication->getSecureSessionID() . ' ' . TTi18n::getText( 'To Employee' ) . ': ' . $authentication->getObject()->getFullName() . ' (' . $user_id . ')', $this->getCurrentUserObject()->getId(), 'authentication' );
//Destination user log, showing the destination user was logged in *by* someone else.
TTLog::addEntry( $user_id, 100, TTi18n::getText( 'Override Login' ) . ': ' . TTi18n::getText( 'SourceIP' ) . ': ' . $authentication->getIPAddress() . ' ' . TTi18n::getText( 'SessionID' ) . ': ' . $authentication->getSecureSessionID() . ' ' . TTi18n::getText( 'By Employee' ) . ': ' . $this->getCurrentUserObject()->getFullName() . ' (' . $user_id . ')', $user_id, 'authentication' );
return true;
} else {
Debug::Text( 'User is likely not active: ' . $user_id, __FILE__, __LINE__, __METHOD__, 10 );
}
} else {
Debug::text( ' ERROR: Permission check failed for switching users...', __FILE__, __LINE__, __METHOD__, 10 );
}
}
return false;
}
/**
* @return bool
*/
function Logout() {
global $authentication;
if ( is_object( $authentication ) && $authentication->getSessionID() != '' ) {
Debug::text( 'Logging out session ID: ' . $authentication->getSessionID(), __FILE__, __LINE__, __METHOD__, 10 );
return $authentication->Logout();
}
return false;
}
/**
* @return int
*/
function getSessionIdle() {
global $authentication;
if ( !is_object( $authentication ) ) {
$authentication = new Authentication();
}
return $authentication->getIdleTimeout();
}
/**
* @param bool $touch_updated_date
* @param string|array $type
* @return bool
*/
function isLoggedIn( $touch_updated_date = true, $type = [ 'USER_NAME', 'USER_NAME_MULTI_FACTOR' ] ) {
global $authentication;
if ( is_array( $type ) == false ) {
$type = [ $type ];
}
$session_id = getSessionID( $type[0] );
if ( $session_id != '' ) {
$authentication = new Authentication();
Debug::text( 'Session ID: ' . $session_id . ' Source IP: ' . Misc::getRemoteIPAddress() . ' Touch Updated Date: ' . (int)$touch_updated_date, __FILE__, __LINE__, __METHOD__, 10 );
if ( $authentication->Check( $session_id, $type, $touch_updated_date ) === true ) {
return true;
}
}
return false;
}
/**
* @return array
*/
function getCurrentUserName() {
if ( is_object( $this->getCurrentUserObject() ) ) {
return $this->returnHandler( $this->getCurrentUserObject()->getUserName() );
}
return $this->returnHandler( false );
}
/**
* @return array
*/
function getCurrentUser() {
if ( is_object( $this->getCurrentUserObject() ) ) {
return $this->returnHandler( $this->getCurrentUserObject()->getObjectAsArray( [ 'id' => true, 'company_id' => true, 'currency_id' => true, 'permission_control_id' => true, 'pay_period_schedule_id' => true, 'policy_group_id' => true, 'default_branch_id' => true, 'default_department_id' => true, 'default_job_id' => true, 'default_job_item_id' => true, 'employee_number' => true, 'user_name' => true, 'phone_id' => true, 'first_name' => true, 'middle_name' => true, 'last_name' => true, 'full_name' => true, 'city' => true, 'province' => true, 'country' => true, 'longitude' => true, 'latitude' => true, 'work_phone' => true, 'home_phone' => true, 'work_email' => true, 'home_email' => true, 'feedback_rating' => true, 'prompt_for_feedback' => true, 'last_login_date' => true, 'created_date' => true, 'is_owner' => true, 'is_child' => true ] ) );
}
return $this->returnHandler( false );
}
/**
* @return array
*/
function getCurrentCompany() {
if ( is_object( $this->getCurrentCompanyObject() ) ) {
return $this->returnHandler( $this->getCurrentCompanyObject()->getObjectAsArray( [ 'id' => true, 'product_edition_id' => true, 'name' => true, 'short_name' => true, 'industry' => true, 'city' => true, 'province' => true, 'country' => true, 'work_phone' => true, 'application_build' => true, 'is_setup_complete' => true, 'total_active_days' => true, 'created_date' => true, 'latitude' => true, 'longitude' => true ] ) );
}
return $this->returnHandler( false );
}
/**
* @return array
*/
function getCustomFieldData() {
$cflf = TTNew( 'CustomFieldListFactory' ); /** @var CustomFieldListFactory $cflf */
$retarr = [
'parent_tables' => $cflf->getUniqueParentTableByCompanyId( $this->getCurrentCompanyObject()->getId() ),
'conversion_field_types' => $cflf->getOptions( 'conversion_field_types' ),
];
return $this->returnHandler( $retarr );
}
/**
* @return array
*/
function getCurrentUserPreference() {
if ( is_object( $this->getCurrentUserObject() ) && is_object( $this->getCurrentUserObject()->getUserPreferenceObject() ) ) {
return $this->returnHandler( $this->getCurrentUserObject()->getUserPreferenceObject()->getObjectAsArray() );
}
return $this->returnHandler( false );
}
/**
* Functions that can be called before the API client is logged in.
* Mainly so the proper loading/login page can be displayed.
* @return bool
*/
function getProduction() {
return PRODUCTION;
}
/**
* @return string
*/
function getApplicationName() {
return APPLICATION_NAME;
}
/**
* @return string
*/
function getApplicationVersion() {
return APPLICATION_VERSION;
}
/**
* @return int
*/
function getApplicationVersionDate() {
return APPLICATION_VERSION_DATE;
}
/**
* @return string
*/
function getApplicationBuild() {
return APPLICATION_BUILD;
}
/**
* @return string
*/
function getOrganizationName() {
return ORGANIZATION_NAME;
}
/**
* @return string
*/
function getOrganizationURL() {
return ORGANIZATION_URL;
}
/**
* @return bool
*/
function isApplicationBranded() {
global $config_vars;
if ( isset( $config_vars['branding']['application_name'] ) ) {
return true;
}
return false;
}
/**
* @return bool
*/
function isPoweredByLogoEnabled() {
global $config_vars;
if ( isset( $config_vars['branding']['disable_powered_by_logo'] ) && $config_vars['branding']['disable_powered_by_logo'] == true ) {
return false;
}
return true;
}
/**
* @return bool
*/
function isAnalyticsEnabled() {
global $config_vars;
if ( isset( $config_vars['other']['disable_google_analytics'] ) && $config_vars['other']['disable_google_analytics'] == true ) {
return false;
}
return true;
}
/**
* @return string
*/
function getAnalyticsTrackingCode() {
global $config_vars;
if ( isset( $config_vars['other']['analytics_tracking_code'] ) && $config_vars['other']['analytics_tracking_code'] != '' ) {
return $config_vars['other']['analytics_tracking_code'];
}
return 'G-4MSFN7PM0H'; //GA4 - OnSite
}
/**
* @param bool $name
* @return int|string
*/
function getTTProductEdition( $name = false ) {
if ( $name == true ) {
$edition = getTTProductEditionName();
} else {
$edition = getTTProductEdition();
}
Debug::text( 'Edition: ' . $edition, __FILE__, __LINE__, __METHOD__, 10 );
return $edition;
}
/**
* @return bool
*/
function getDeploymentOnDemand() {
return DEPLOYMENT_ON_DEMAND;
}
/**
* @return bool
*/
function getRegistrationKey() {
return SystemSettingFactory::getSystemSettingValueByKey( 'registration_key' );
}
/**
* @param null $language
* @param null $country
* @return null
*/
function getLocale( $language = null, $country = null ) {
$language = Misc::trimSortPrefix( $language );
if ( $language == '' && is_object( $this->getCurrentUserObject() ) && is_object( $this->getCurrentUserObject()->getUserPreferenceObject() ) ) {
$language = $this->getCurrentUserObject()->getUserPreferenceObject()->getLanguage();
}
if ( $country == '' && is_object( $this->getCurrentUserObject() ) ) {
$country = $this->getCurrentUserObject()->getCountry();
}
if ( $language != '' ) {
TTi18n::setLanguage( $language );
}
if ( $country != '' ) {
TTi18n::setCountry( $country );
}
TTi18n::setLocale(); //Sets master locale
//$retval = str_replace('.UTF-8', '', TTi18n::getLocale() );
$retval = TTi18n::getNormalizedLocale();
Debug::text( 'Locale: ' . $retval . ' Language: ' . $language, __FILE__, __LINE__, __METHOD__, 10 );
return $retval;
}
/**
* @return int|mixed
*/
function getSystemLoad() {
return Misc::getSystemLoad();
}
/**
* @return mixed
*/
function getHTTPHost() {
return $_SERVER['HTTP_HOST'];
}
/**
* @return bool
*/
function getCompanyName() {
//Get primary company data needs to be used when user isn't logged in as well.
$clf = TTnew( 'CompanyListFactory' ); /** @var CompanyListFactory $clf */
$clf->getByID( PRIMARY_COMPANY_ID );
Debug::text( 'Primary Company ID: ' . PRIMARY_COMPANY_ID, __FILE__, __LINE__, __METHOD__, 10 );
if ( $clf->getRecordCount() == 1 ) {
return $clf->getCurrent()->getName();
}
Debug::text( ' ERROR: Primary Company does not exist!', __FILE__, __LINE__, __METHOD__, 10 );
return false;
}
/**
* Returns all login data required in a single call for optimization purposes.
* @param null $api
* @return array
*/
function getPreLoginData( $api = null, $authentication_type = [ 'USER_NAME', 'USER_NAME_MULTI_FACTOR'] ) {
global $config_vars;
//Get browser information from user agent. Used below to get vendor and if browser is mobil or not.
$browser = new Browser( ( ( isset( $_SERVER['HTTP_USER_AGENT'] ) ) ? $_SERVER['HTTP_USER_AGENT'] : null ) );
//Basic settings that *do not* require a DB connection.
$retarr = [
'primary_company_id' => PRIMARY_COMPANY_ID, //Needed for some branded checks.
'primary_company_name' => null, //Requires DB connection.
'base_url' => Environment::getBaseURL(),
'cookie_base_url' => Environment::getCookieBaseURL( 'json' ),
'api_url' => Environment::getAPIURL( $api ),
'api_base_url' => Environment::getAPIBaseURL( $api ),
'api_json_url' => Environment::getAPIURL( 'json' ),
'images_url' => Environment::getImagesURL(),
'application_name' => $this->getApplicationName(),
'organization_name' => $this->getOrganizationName(),
'organization_url' => $this->getOrganizationURL(),
'copyright_notice' => COPYRIGHT_NOTICE,
'product_edition' => $this->getTTProductEdition( false ),
'product_edition_name' => $this->getTTProductEdition( true ),
'deployment_on_demand' => $this->getDeploymentOnDemand(),
'web_session_expire' => ( isset( $config_vars['other']['web_session_expire'] ) && $config_vars['other']['web_session_expire'] != '' ) ? (bool)$config_vars['other']['web_session_expire'] : false, //If TRUE then session expires when browser closes.
'analytics_enabled' => $this->isAnalyticsEnabled(),
'analytics_tracking_code' => $this->getAnalyticsTrackingCode(),
'registration_key' => null, //Requires DB connection.
'http_host' => $this->getHTTPHost(),
'is_ssl' => Misc::isSSL(),
'production' => $this->getProduction(),
'demo_mode' => DEMO_MODE,
'application_version' => $this->getApplicationVersion(),
'application_version_date' => $this->getApplicationVersionDate(),
'application_build' => $this->getApplicationBuild(),
'installer_enabled' => ( isset( $config_vars['other']['installer_enabled'] ) && $config_vars['other']['installer_enabled'] != '' ) ? (bool)$config_vars['other']['installer_enabled'] : false,
'is_logged_in' => false, //Requires DB connection.
'session_idle_timeout' => $this->getSessionIdle(),
'footer_left_html' => ( isset( $config_vars['other']['footer_left_html'] ) && $config_vars['other']['footer_left_html'] != '' ) ? $config_vars['other']['footer_left_html'] : false,
'footer_right_html' => ( isset( $config_vars['other']['footer_right_html'] ) && $config_vars['other']['footer_right_html'] != '' ) ? $config_vars['other']['footer_right_html'] : false,
'disable_feedback' => ( isset( $config_vars['other']['disable_feedback'] ) && $config_vars['other']['disable_feedback'] != '' ) ? (bool)$config_vars['other']['disable_feedback'] : false,
'support_email' => ( isset( $config_vars['other']['support_email'] ) ) ? $config_vars['other']['support_email'] : 'support@timetrex.com', //Allow this to be defined as empty to disable the Email Support icon.
'language_options' => Misc::addSortPrefix( TTi18n::getLanguageArray() ),
//Make sure locale is set properly before this function is called, either in api.php or APIGlobal.js.php for example.
'enable_default_language_translation' => ( isset( $config_vars['other']['enable_default_language_translation'] ) ) ? $config_vars['other']['enable_default_language_translation'] : false,
'language' => TTi18n::getLanguage(),
'locale' => TTi18n::getNormalizedLocale(), //Needed for HTML5 interface to load proper translation file.
'map_provider' => isset( $config_vars['map']['provider'] ) ? $config_vars['map']['provider'] : 'mapbox',
'map_api_key' => ( isset( $config_vars['map']['api_key'] ) && $config_vars['map']['api_key'] != '' ) ? $config_vars['map']['api_key'] : 'pk.eyJ1IjoidGltZXRyZXgiLCJhIjoiY2t1OHBxejEyNXI3ajJwcGk5d3cxNGJkeSJ9.4O1x-ULp4DuSRbeXJTIL3w', //On-Site.
/* @formatter:off */ 'product_edition_match' => (new Install())->checkProductEditionMatch(), /* @formatter:on */
//Registration key for the map servers must be added in JS because of the url formats
'map_tile_url' => isset( $config_vars['map']['tile_url'] ) ? rtrim( $config_vars['map']['tile_url'], '/' ) : 'https://map-tiles.timetrex.com',
'map_routing_url' => isset( $config_vars['map']['routing_url'] ) ? rtrim( $config_vars['map']['routing_url'], '/' ) : 'https://map-routing.timetrex.com',
'map_geocode_url' => isset( $config_vars['map']['geocode_url'] ) ? rtrim( $config_vars['map']['geocode_url'], '/' ) : 'https://map-geocode.timetrex.com',
'sandbox_url' => isset( $config_vars['other']['sandbox_url'] ) ? $config_vars['other']['sandbox_url'] : false,
'sandbox' => isset( $config_vars['other']['sandbox'] ) ? $config_vars['other']['sandbox'] : false,
'uuid_seed' => TTUUID::getSeed( true ),
'user_agent_data' => [ 'browser' => $browser->getBrowser(), 'is_mobile' => $browser->isMobile() ],
'feature_flags' => Misc::parseFeatureFlags(),
];
if ( ( isset( $_GET['disable_db'] ) && $_GET['disable_db'] == 1 )
|| ( isset( $config_vars['other']['installer_enabled'] ) && $config_vars['other']['installer_enabled'] == 1 )
|| ( isset( $config_vars['other']['down_for_maintenance'] ) && $config_vars['other']['down_for_maintenance'] == true ) ) {
Debug::text( 'WARNING: Installer/Down For Maintenance is enabled... Normal logins are disabled!', __FILE__, __LINE__, __METHOD__, 10 );
} else {
//Only data that requires a DB connection to obtain here.
$retarr['company_name'] = $this->getCompanyName();
if ( $retarr['company_name'] == '' ) {
$retarr['company_name'] = 'N/A';
}
$retarr['registration_key'] = $this->getRegistrationKey();
$retarr['is_logged_in'] = $this->isLoggedIn( true, $authentication_type );
}
return $retarr;
}
/**
* Returns all post login data required in a single call for optimization purposes.
* @param array $data
* @return array | bool
*/
function getPostLoginData( $data = [] ) {
if ( !is_object( $this->getCurrentUserObject() ) ) {
return false;
}
$retarr = [];
$retarr['user_data'] = $this->stripReturnHandler( $this->getCurrentUser() );
$retarr['user_preference'] = $this->stripReturnHandler( $this->getCurrentUserPreference() );
$retarr['company_data'] = $this->stripReturnHandler( $this->getCurrentCompany() );
$retarr['locale'] = $this->getLocale( $data['selected_language'] ?? null );
$retarr['custom_field_data'] = $this->stripReturnHandler( $this->getCustomFieldData() );
$permission = new Permission();
$retarr['permissions'] = $permission->getPermissions( $this->getCurrentUserObject()->getId(), $this->getCurrentCompanyObject()->getId() );
$clf = TTnew( 'CurrencyListFactory' );
$clf->getByCompanyIdAndUserId( $this->getCurrentCompanyObject()->getId(), $this->getCurrentUserObject()->getId() );
if ( $clf->getRecordCount() > 0 ) {
$c_obj = $clf->getCurrent();
$retarr['currency_symbol'] = $c_obj->getSymbol();
} else {
$retarr['currency_symbol'] = '$';
}
$ulf = TTNew( 'UserListFactory' ); /** @var UserListFactory $ulf */
$retarr['unique_country'] = $ulf->getUniqueCountryByCompanyId( $this->getCurrentCompanyObject()->getId() );
$api_user_preference = new APIUserPreference();
$retarr['moment_date_format'] = $api_user_preference->getOptions( 'moment_date_format' );
$retarr['moment_time_format'] = $api_user_preference->getOptions( 'moment_time_format' );
if ( !$retarr['user_preference'] || !isset( $retarr['user_preference']['date_format'] ) ) {
//Get default user preferences if user does not have any set.
$retarr['user_preference'] = $this->stripReturnHandler( $api_user_preference->getUserPreferenceDefaultData() );
}
$retarr['feature_flags'] = Misc::parseFeatureFlags();
return $retarr;
}
/**
* Function that HTML5 interface can call when an irrecoverable error or uncaught exception is triggered.
* @param null $data
* @param null $screenshot
* @return string
*/
function sendErrorReport( $data = null, $screenshot = null ) {
$rl = TTNew( 'RateLimit' ); /** @var RateLimit $rl */
$rl->setID( 'error_report_' . Misc::getRemoteIPAddress() );
$rl->setAllowedCalls( 20 );
$rl->setTimeFrame( 900 ); //15 minutes
if ( $rl->check() == false ) {
Debug::Text( 'Excessive error reports... Preventing error reports from: ' . Misc::getRemoteIPAddress() . ' for up to 15 minutes...', __FILE__, __LINE__, __METHOD__, 10 );
return APPLICATION_BUILD;
}
$attachments = null;
if ( $screenshot != '' ) {
$attachments[] = [ 'file_name' => 'screenshot.png', 'mime_type' => 'image/png', 'data' => base64_decode( $screenshot ) ];
}
$subject = 'HTML5 Error Report'; //Don't translate this, as it breaks filters.
$data = 'IP Address: ' . Misc::getRemoteIPAddress() . "\nServer Version: " . APPLICATION_BUILD . "\nUser Agent: " . ( isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : 'N/A' ) . "\n\n" . $data;
Misc::sendSystemMail( $subject, $data, $attachments ); //Do not send if PRODUCTION=FALSE.
//return APPLICATION_BUILD so JS can check if its correct and notify the user to refresh/clear cache.
return APPLICATION_BUILD;
}
/**
* Allows user who isn't logged in to change their password.
* @param string $user_name
* @param string $current_password
* @param string $new_password
* @param string $new_password2
* @return array|bool
* @internal param string $type
*/
function changePassword( $user_name, $current_password = null, $new_password = null, $new_password2 = null ) {
$rl = TTNew( 'RateLimit' ); /** @var RateLimit $rl */
$rl->setID( 'authentication_' . Misc::getRemoteIPAddress() );
$rl->setAllowedCalls( 20 );
$rl->setTimeFrame( 900 ); //15 minutes
if ( $rl->check() == false ) {
Debug::Text( 'Excessive failed password attempts... Preventing password change from: ' . Misc::getRemoteIPAddress() . ' for up to 15 minutes...', __FILE__, __LINE__, __METHOD__, 10 );
sleep( 5 ); //Excessive password attempts, sleep longer.
$u_obj = TTnew( 'UserListFactory' ); /** @var UserListFactory $u_obj */
$u_obj->Validator->isTrue( 'current_password', false, TTi18n::gettext( 'Current User Name or Password is incorrect' ) );
return $this->returnHandler( false, 'VALIDATION', TTi18n::getText( 'INVALID DATA' ), $u_obj->Validator->getErrorsArray(), [ 'total_records' => 1, 'valid_records' => 0 ] );
}
$ulf = TTnew( 'UserListFactory' ); /** @var UserListFactory $ulf */
$ulf->getByUserName( $user_name );
if ( $ulf->getRecordCount() == 1 ) {
$u_obj = $ulf->getCurrent();
if ( is_object( $u_obj->getCompanyObject() ) && $u_obj->getCompanyObject()->getStatus() == 10 ) {
Debug::text( 'Attempting to change password for: ' . $user_name, __FILE__, __LINE__, __METHOD__, 10 );
$u_obj->setIsRequiredCurrentPassword( true );
$u_obj->setCurrentPassword( $current_password );
if ( $current_password != '' ) {
if ( $u_obj->checkPassword( $current_password, false ) !== true ) { //Disable password policy checking on current password.
Debug::text( 'Password check failed! Attempt: ' . $rl->getAttempts(), __FILE__, __LINE__, __METHOD__, 10 );
sleep( ( $rl->getAttempts() * 0.5 ) ); //If password is incorrect, sleep for some time to slow down brute force attacks.
//setCurrentPassword() above handles this validation error message now.
// $u_obj->Validator->isTrue( 'current_password',
// FALSE,
// TTi18n::gettext( 'Current User Name or Password is incorrect' ) );
}
} else {
Debug::Text( 'Current password not specified', __FILE__, __LINE__, __METHOD__, 10 );
$u_obj->Validator->isTrue( 'current_password',
false,
TTi18n::gettext( 'Current User Name or Password is incorrect' ) );
}
if ( $current_password == $new_password ) {
$u_obj->Validator->isTrue( 'password',
false,
TTi18n::gettext( 'New password must be different than current password' ) );
} else {
if ( $new_password != '' || $new_password2 != '' ) {
if ( $new_password == $new_password2 ) {
$u_obj->setPassword( $new_password );
} else {
$u_obj->Validator->isTrue( 'password',
false,
TTi18n::gettext( 'Passwords don\'t match' ) );
}
} else {
$u_obj->Validator->isTrue( 'password',
false,
TTi18n::gettext( 'Passwords don\'t match' ) );
}
}
//This should force the updated_by field to match the user changing their password,
// so we know not to ask the user to change their password again, since they were the last ones to do so.
//$current_user must be set above $u_obj->isValid() so it can properly validate things like hierarchy and such in UserFactory.
global $current_user;
$current_user = $u_obj;
if ( $u_obj->isValid() ) {
if ( DEMO_MODE == true ) {
//Return TRUE even in demo mode, but nothing happens.
return $this->returnHandler( true );
} else {
TTLog::addEntry( $u_obj->getID(), 20, TTi18n::getText( 'Password - Web (Password Policy)' ), null, $u_obj->getTable() );
$rl->delete(); //Clear failed password rate limit upon successful login.
$u_obj->setPasswordUpdatedDate( time() ); //Since the user isn't logged in, we have to manually force the PasswordUpdatedDate here to ensure its no longer -1 (compromised)
$retval = $u_obj->Save( false ); //UserID is needed below.
//Logout all other sessions for this user.
$authentication = TTNew( 'Authentication' ); /** @var Authentication $authentication */
$authentication->logoutUser( $u_obj->getID() );
$current_user = null; //unset( $current_user ); -- unset() doesn't work on global variables.
return $this->returnHandler( $retval ); //Single valid record
}
}
} else {
$u_obj = TTnew( 'UserListFactory' ); /** @var UserListFactory $u_obj */
$u_obj->Validator->isTrue( 'current_password', false, TTi18n::gettext( 'Sorry, your company\'s account is not currently ACTIVE, please contact customer support' ) );
}
} else {
//Issue #2225 - Be sure to return the same error message even if username is not valid to avoid user enumeration attacks.
$u_obj = TTnew( 'UserListFactory' ); /** @var UserListFactory $u_obj */
$u_obj->Validator->isTrue( 'current_password', false, TTi18n::gettext( 'Current User Name or Password is incorrect' ) );
}
sleep( ( $rl->getAttempts() * 0.5 ) ); //If password is incorrect, sleep for some time to slow down brute force attacks.
Debug::Text( 'Failed username/password... Attempt: ' . $rl->getAttempts() . ' Sleeping...', __FILE__, __LINE__, __METHOD__, 10 );
return $this->returnHandler( false, 'VALIDATION', TTi18n::getText( 'INVALID DATA' ), $u_obj->Validator->getErrorsArray(), [ 'total_records' => 1, 'valid_records' => 0 ] );
}
/**
* @param $email
* @return array
*/
function resetPassword( $email ) {
//Debug::setVerbosity( 11 );
$rl = TTNew( 'RateLimit' ); /** @var RateLimit $rl */
$rl->setID( 'password_reset_' . Misc::getRemoteIPAddress() );
$rl->setAllowedCalls( 10 );
$rl->setTimeFrame( 900 ); //15 minutes
$validator = new Validator();
Debug::Text( 'Email: ' . $email, __FILE__, __LINE__, __METHOD__, 10 );
if ( $rl->check() == false ) {
Debug::Text( 'Excessive reset password attempts... Preventing resets from: ' . Misc::getRemoteIPAddress() . ' for up to 15 minutes...', __FILE__, __LINE__, __METHOD__, 10 );
sleep( 5 ); //Excessive password attempts, sleep longer.
$validator->isTrue( 'email', false, TTi18n::getText( 'Email address was not found in our database (z)' ) );
} else {
$ulf = TTnew( 'UserListFactory' ); /** @var UserListFactory $ulf */
$ulf->getByHomeEmailOrWorkEmail( $email );
if ( $ulf->getRecordCount() == 1 ) {
$user_obj = $ulf->getCurrent();
if ( $user_obj->getEnableLogin() == true ) { //Only allow password resets when logins are enabled.
//Check if company is using LDAP authentication, if so deny password reset.
if ( $user_obj->getCompanyObject()->getLDAPAuthenticationType() == 0 ) {
if ( $user_obj->sendPasswordResetEmail() == true ) {
Debug::Text( 'Found USER! ', __FILE__, __LINE__, __METHOD__, 10 );
//Logout *current* session is they are currently logged in. Its a rare case of a user trying to reset their password while already logged in,
// but it has happened because they don't remember their password stored in a password manager.
$authentication = TTNew( 'Authentication' ); /** @var Authentication $authentication */
$authentication->setSessionID( getSessionID() );
$authentication->logout();
$rl->delete(); //Clear password reset rate limit upon successful login.
return $this->returnHandler( [ 'email_sent' => 1, 'email' => $email ] );
} else {
Debug::Text( 'ERROR: Unable to send password reset email, perhaps user record is invalid, or production mode is disabled?', __FILE__, __LINE__, __METHOD__, 10 );
$validator->isTrue( 'email', false, TTi18n::getText( 'Unable to reset password, please contact your administrator for more information' ) );
}
} else {
Debug::Text( 'LDAP Authentication is enabled, password reset is disabled! ', __FILE__, __LINE__, __METHOD__, 10 );
$validator->isTrue( 'email', false, TTi18n::getText( 'Please contact your administrator for instructions on changing your password' ) . ' (LDAP)' );
}
} else {
$validator->isTrue( 'email', false, TTi18n::getText( 'Email address was not found in our database (b)' ) );
}
} else {
//Error
Debug::Text( 'DID NOT FIND USER! Returned: ' . $ulf->getRecordCount(), __FILE__, __LINE__, __METHOD__, 10 );
$validator->isTrue( 'email', false, TTi18n::getText( 'Email address was not found in our database (a)' ) );
//If password was incorrect, sleep for some specified period of time to help delay brute force attacks.
if ( PRODUCTION == true ) {
Debug::Text( 'Email address for password reset was incorrect, sleeping for random amount of time...', __FILE__, __LINE__, __METHOD__, 10 );
usleep( rand( 750000, 1500000 ) );
}
}
Debug::text( 'Reset Password Failed! Attempt: ' . $rl->getAttempts(), __FILE__, __LINE__, __METHOD__, 10 );
sleep( ( $rl->getAttempts() * 0.5 ) ); //If email is incorrect, sleep for some time to slow down brute force attacks.
}
return $this->returnHandler( false, 'VALIDATION', TTi18n::getText( 'INVALID DATA' ), [ 'error' => $validator->getErrorsArray() ], [ 'total_records' => 1, 'valid_records' => 0 ] );
}
/**
* Reset the password if users forgotten their password
* @param $key
* @param $password
* @param $password2
* @return array
*/
function passwordReset( $key, $password, $password2 ) {
$rl = TTNew( 'RateLimit' ); /** @var RateLimit $rl */
$rl->setID( 'password_reset_' . Misc::getRemoteIPAddress() );
$rl->setAllowedCalls( 10 );
$rl->setTimeFrame( 900 ); //15 minutes
$validator = new Validator();
if ( $rl->check() == false ) {
Debug::Text( 'Excessive password reset attempts... Preventing resets from: ' . Misc::getRemoteIPAddress() . ' for up to 15 minutes...', __FILE__, __LINE__, __METHOD__, 10 );
sleep( 5 ); //Excessive password attempts, sleep longer.
} else {
$ulf = TTnew( 'UserListFactory' ); /** @var UserListFactory $ulf */
Debug::Text( 'Key: ' . $key, __FILE__, __LINE__, __METHOD__, 10 );
$ulf->getByPasswordResetKey( $key );
if ( $ulf->getRecordCount() == 1 ) {
Debug::Text( 'FOUND Password reset key! ', __FILE__, __LINE__, __METHOD__, 10 );
$user_obj = $ulf->getCurrent(); /** @var UserFactory $user_obj */
if ( $user_obj->checkPasswordResetKey( $key ) == true ) {
//Logout *current* session is they are currently logged in. Its a rare case of a user trying to reset their password while already logged in,
// but it has happened because they don't remember their password stored in a password manager.
$authentication = TTNew( 'Authentication' ); /** @var Authentication $authentication */
$authentication->setSessionID( getSessionID() );
$authentication->logout();
//Make sure passwords match
Debug::Text( 'Change Password Key: ' . $key, __FILE__, __LINE__, __METHOD__, 10 );
if ( $password != '' && trim( $password ) === trim( $password2 ) ) {
//Change password
$user_obj->setPassword( $password ); //Password reset key is cleared when password is changed.
$user_obj->setPasswordUpdatedDate( time() ); //Since the user isn't logged in, we have to manually force the PasswordUpdatedDate here to ensure its no longer -1 (compromised)
$user_obj->setEnableClearPasswordResetData( true ); //Clear any outstanding password reset key now that it has been used successfully.
$user_obj->setMultiFactorType( 0 ); //Disable MFA for this user when resetting password.
if ( $user_obj->isValid() ) {
$user_obj->Save( false );
Debug::Text( 'Password Change succesful!', __FILE__, __LINE__, __METHOD__, 10 );
TTLog::addEntry( $user_obj->getID(), 20, TTi18n::getText( 'Password Reset - Web (Completed)' ), $user_obj->getID(), $user_obj->getTable() );
//Logout all sessions for this user when password is successfully reset.
$authentication = TTNew( 'Authentication' ); /** @var Authentication $authentication */
$authentication->logoutUser( $user_obj->getId() );
unset( $user_obj );
return $this->returnHandler( true );
} else {
$validator->merge( $user_obj->Validator ); //Make sure we display any validation errors like password too weak.
}
} else {
$validator->isTrue( 'password', false, TTi18n::getText( 'Passwords do not match' ) );
}
//Do this once a successful key is found, so the user can get as many password change attempts as needed.
$rl->delete(); //Clear password reset rate limit upon successful reset.
} else {
Debug::Text( 'DID NOT FIND Valid Password reset key!', __FILE__, __LINE__, __METHOD__, 10 );
$validator->isTrue( 'password', false, TTi18n::getText( 'Password reset key is invalid, please try resetting your password again.' ) );
}
} else {
Debug::Text( 'DID NOT FIND Valid Password reset key! (b)', __FILE__, __LINE__, __METHOD__, 10 );
$validator->isTrue( 'password', false, TTi18n::getText( 'Password reset key is invalid, please try resetting your password again.' ) . ' (b)' );
}
Debug::text( 'Password Reset Failed! Attempt: ' . $rl->getAttempts(), __FILE__, __LINE__, __METHOD__, 10 );
sleep( ( $rl->getAttempts() * 0.5 ) ); //If email is incorrect, sleep for some time to slow down brute force attacks.
}
return $this->returnHandler( false, 'VALIDATION', TTi18n::getText( 'INVALID DATA' ), [ 'error' => $validator->getErrorsArray() ], [ 'total_records' => 1, 'valid_records' => 0 ] );
}
/**
* Sends a refreshed CSRF token cookie in case it expires prior to the user clicking the login button. This helps avoid showing an error message and triggering a full browser refresh.
*/
function sendCSRFTokenCookie() {
return $this->returnHandler( sendCSRFTokenCookie() );
}
/**
* Ping function is also in APIMisc for when the session timesout is valid.
* Ping no longer can tell if the session is timed-out, must use "isLoggedIn(FALSE)" instead.
* @return bool
*/
function Ping() {
return true;
}
/**
* Used to mark notifications as read from things like tracking pixels in notification emails.
* Authentication is not required for this, but the user_id, type_id and object_id must all match and would be very unlikely to guess. Plus its pretty benign even if they do.
* @param string $user_id UUID
* @param int $type_id INT
* @param string $object_id UUID
* @return bool
* @throws DBError
*/
function markNotificationAsRead( $user_id, $type_id, $object_id ) {
if ( TTUUID::isUUID( $user_id ) == false || TTUUID::isUUID( $object_id ) == false || empty( $type_id ) ) {
return false;
}
return NotificationFactory::updateStatusByObjectIdAndObjectTypeId( (int)$type_id, TTUUID::castUUID( $object_id ), TTUUID::castUUID( $user_id ) );
}
/**
* @return array|bool
*/
function removeAllTrustedDevices() {
if ( $this->isLoggedIn() == false ) {
return $this->returnHandler( false );
}
$atdlf = TTnew( 'AuthenticationTrustedDeviceListFactory' ); /** @var AuthenticationTrustedDeviceListFactory $atdlf */
$atdlf->getByUserId( $this->getCurrentUserObject()->getId() );
if ( $atdlf->getRecordCount() > 0 ) {
foreach ( $atdlf as $atd_obj ) { /** @var AuthenticationTrustedDeviceFactory $atd_obj */
$atd_obj->setDeleted( true );
if ( $atd_obj->isValid() ) {
$atd_obj->Save();
}
}
}
if ( isset( $_COOKIE['TrustedDevice'] ) ) {
//Destroy the current TrustedDevice cookie.
setcookie( 'TrustedDevice', '', ( time() - ( 3600 * 34 ) ), Environment::getCookieBaseURL() );
}
return $this->returnHandler( true );
}
/**
* @param $delete_all_sessions
* @return array|bool
*/
function logoutAllSessions( $delete_all_sessions = false ) {
if ( $this->isLoggedIn() == false ) {
return $this->returnHandler( false );
}
global $authentication;
if ( $delete_all_sessions === true ) {
$authentication->logoutUser( $this->getCurrentUserObject()->getId(), null, null );
} else {
$authentication->logoutUser( $this->getCurrentUserObject()->getId() );
}
return $this->returnHandler( true );
}
}
?>