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' ) . ': ' . '' . $migrate_url . ''; } 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 ); } } ?>