From a03c7e7319d97c9b8db6ee354acfe447c1467ef3 Mon Sep 17 00:00:00 2001 From: Clemens Schwaighofer Date: Fri, 13 Dec 2024 10:54:20 +0900 Subject: [PATCH] Class ACL Login and Session update Session: - can recreate session id periodic (Default never) - options are set via array like in other classes - checks for strict session settings on default ACL Login: - remove all DEBUG/DB_DEBUG variables, calls, etc - removed from the EditBase/EditUsers classes too - switch to UUIDv4 as the session lookup variable - all session vars are prefixed with "LOGIN_" - the charset ones are left as DEFAULT_CHARSET, DEFAULT_LOCALE, DEFAULT_LANG - the old LOGIN_LANG has been removed (deprecated) - TEMPLATE session has been removed, there is no template data in the edit class - session is resynced (ACL lookup), default 5min, adjustable via option - sets strict header options as default - moves several methods parts into their own classes - plan to split up class into sub classes for certain actions - new force logout counter in DB - edit logger is moved into this class - plan to move logging into sub class - all SQL calls user heredoc and params - update login/change password to new layout for pc/smartphone compatible - change password will be replaced with reset password in future - last login success is now set as timestamp - all old PK lookups for edit access etc are deprecated and replaced with cuid lookups ArrayHandling: - add array return matching key Give any array with key values and a list of keys and only return matching keys Wrapper for array_filter call --- src/ACL/Login.php | 2196 +++++++++++------ src/ACL/LoginUserStatus.php | 68 + src/Admin/EditBase.php | 2 - src/Combined/ArrayHandler.php | 24 + src/Create/Session.php | 169 +- src/Output/Form/TableArrays/EditUsers.php | 24 - test/phpunit/ACL/CoreLibsACLLoginTest.php | 178 +- .../CoreLibsACLLogin_database_create_data.sql | 65 +- .../CoreLibsCombinedArrayHandlerTest.php | 85 + .../Create/CoreLibsCreateSessionTest.php | 21 +- .../Debug/CoreLibsDebugSupportTest.php | 3 + .../CoreLibsLanguageGetLocaleTest.php | 335 --- .../CoreLibsLoggingErrorMessagesTest.php | 2 +- 13 files changed, 1935 insertions(+), 1237 deletions(-) create mode 100644 src/ACL/LoginUserStatus.php diff --git a/src/ACL/Login.php b/src/ACL/Login.php index 691b03d..365f5e6 100644 --- a/src/ACL/Login.php +++ b/src/ACL/Login.php @@ -14,13 +14,14 @@ * will be a class one day * * descrption of session_vars -* DEBUG_ALL - set to one, prints out error_msg var at end of php execution -* DB_DEBUG - prints out database debugs (query, etc) -* GROUP_LEVEL - the level he can access (numeric) -* USER_NAME - login name from user -* LANG - lang to show edit interface (not yet used) +* TODO: Update session var info +* [DEPRECATED] DEBUG_ALL - set to one, prints out error_msg var at end of php execution +* [DEPRECATED] DB_DEBUG - prints out database debugs (query, etc) +* [REMOVED] LOGIN_GROUP_LEVEL - the level he can access (numeric) +* LOGIN_USER_NAME - login name from user +* [DEPRECATED] LANG - lang to show edit interface (not yet used) * DEFAULT_CHARSET - in connection with LANG (not yet used) -* PAGES - array of hashes +* LOGIN_PAGES - array of hashes * edit_page_id - ID from the edit_pages table * filename - name of the file * page_name - name in menu @@ -75,18 +76,18 @@ use CoreLibs\Convert\Json; class Login { /** @var ?int the user id var*/ - private ?int $euid; + private ?int $edit_user_id; /** @var ?string the user cuid (note will be super seeded with uuid v4 later) */ - private ?string $ecuid; + private ?string $edit_user_cuid; /** @var ?string UUIDv4, will superseed the ecuid and replace euid as login id */ - private ?string $ecuuid; + private ?string $edit_user_cuuid; /** @var string _GET/_POST loginUserId parameter for non password login */ private string $login_user_id = ''; /** @var string source, either _GET or _POST or empty */ private string $login_user_id_source = ''; /** @var bool set to true if illegal characters where found in the login user id string */ private bool $login_user_id_unclear = false; - // is set to one if login okay, or EUID is set and user is okay to access this page + // is set to one if login okay, or EUCUUID is set and user is okay to access this page /** @var bool */ private bool $permission_okay = false; /** @var string pressed login */ @@ -180,6 +181,7 @@ class Login private array $login_template = [ 'strings' => [], 'password_change' => '', + 'password_forgot' => '', 'template' => '' ]; @@ -215,6 +217,16 @@ class Login 'path' => '', ]; + /** @var int resync interval time in minutes */ + private const DEFAULT_AUTH_RESYNC_INTERVAL = 5 * 60; + /** @var int the session max garbage collection life time */ + // private const DEFAULT_SESSION_GC_MAXLIFETIME = ; + private int $default_session_gc_maxlifetime; + /** @var int in how many minutes an auth resync is done */ + private int $auth_resync_interval; + /** @var bool set the enhanced header security */ + private bool $header_enhance_security = false; + /** @var \CoreLibs\Logging\Logging logger */ public \CoreLibs\Logging\Logging $log; /** @var \CoreLibs\DB\IO database */ @@ -228,17 +240,16 @@ class Login * constructor, does ALL, opens db, works through connection checks, * finishes itself * - * @param \CoreLibs\DB\IO $db Database connection class - * @param \CoreLibs\Logging\Logging $log Logging class - * @param \CoreLibs\Create\Session $session Session interface class - * @param array $options Login ACL settings - * $auto_login [default true] DEPRECATED, moved into options + * @param \CoreLibs\DB\IO $db Database connection class + * @param \CoreLibs\Logging\Logging $log Logging class + * @param \CoreLibs\Create\Session $session Session interface class + * @param array $options Login ACL settings */ public function __construct( \CoreLibs\DB\IO $db, \CoreLibs\Logging\Logging $log, \CoreLibs\Create\Session $session, - array $options = [] + array $options = [], ) { // attach db class $this->db = $db; @@ -247,148 +258,19 @@ class Login // attach session class $this->session = $session; + $this->default_session_gc_maxlifetime = (int)ini_get("session.gc_maxlifetime"); + // set and check options if (false === $this->loginSetOptions($options)) { // on failure, exit echo "Could not set options"; $this->loginTerminate('Could not set options', 3000); } - - // string key, msg: string, flag: e (error), o (ok) - $this->login_error_msg = [ - '0' => [ - 'msg' => 'No error', - 'flag' => 'o' - ], - // actually obsolete - '100' => [ - 'msg' => '[EUID] came in as GET/POST!', - 'flag' => 'e', - ], - // query errors - '1009' => [ - 'msg' => 'Login query reading failed', - 'flag' => 'e', - ], - // user not found - '1010' => [ - 'msg' => 'Login Failed - Wrong Username or Password', - 'flag' => 'e' - ], - // blowfish password wrong - '1011' => [ - 'msg' => 'Login Failed - Wrong Username or Password', - 'flag' => 'e' - ], - // fallback md5 password wrong - '1012' => [ - 'msg' => 'Login Failed - Wrong Username or Password', - 'flag' => 'e' - ], - // new password_hash wrong - '1013' => [ - 'msg' => 'Login Failed - Wrong Username or Password', - 'flag' => 'e' - ], - '1101' => [ - 'msg' => 'Login Failed - Login User ID must be validated', - 'flag' => 'e' - ], - '1102' => [ - 'msg' => 'Login Failed - Login User ID is outside valid date range', - 'flag' => 'e' - ], - '102' => [ - 'msg' => 'Login Failed - Please enter username and password', - 'flag' => 'e' - ], - '103' => [ - 'msg' => 'You do not have the rights to access this Page', - 'flag' => 'e' - ], - '104' => [ - 'msg' => 'Login Failed - User not enabled', - 'flag' => 'e' - ], - '105' => [ - 'msg' => 'Login Failed - User is locked', - 'flag' => 'e' - ], - '106' => [ - 'msg' => 'Login Failed - User is deleted', - 'flag' => 'e' - ], - '107' => [ - 'msg' => 'Login Failed - User in locked via date period', - 'flag' => 'e' - ], - '108' => [ - 'msg' => 'Login Failed - User is locked via Login User ID', - 'flag' => 'e' - ], - '109' => [ - 'msg' => 'Check permission query reading failed', - 'flag' => 'e' - ], - // actually this is an illegal user, but I mask it - '220' => [ - 'msg' => 'Password change - The user could not be found', - 'flag' => 'e' - ], - '200' => [ - 'msg' => 'Password change - Please enter username and old password', - 'flag' => 'e' - ], - '201' => [ - 'msg' => 'Password change - The user could not be found', - 'flag' => 'e' - ], - '202' => [ - 'msg' => 'Password change - The old password is not correct', - 'flag' => 'e' - ], - '203' => [ - 'msg' => 'Password change - Please fill out both new password fields', - 'flag' => 'e' - ], - '204' => [ - 'msg' => 'Password change - The new passwords do not match', - 'flag' => 'e' - ], - // we should also not here WHAT is valid - '205' => [ - 'msg' => 'Password change - The new password is not in a valid format', - 'flag' => 'e' - ], - // for OK password change - '300' => [ - 'msg' => 'Password change successful', - 'flag' => 'o' - ], - // this is bad bad error - '9999' => [ - 'msg' => 'Necessary crypt engine could not be found. Login is impossible', - 'flag' => 'e' - ], - ]; - - // read the current edit_access_right list into an array - $q = "SELECT level, type, name FROM edit_access_right " - . "WHERE level >= 0 ORDER BY level"; - while (is_array($res = $this->db->dbReturn($q))) { - // level to description format (numeric) - $this->default_acl_list[$res['level']] = [ - 'type' => $res['type'], - 'name' => $res['name'] - ]; - $this->default_acl_list_type[(string)$res['type']] = (int)$res['level']; - } - // write that into the session - $this->session->setMany([ - 'DEFAULT_ACL_LIST' => $this->default_acl_list, - 'DEFAULT_ACL_LIST_TYPE' => $this->default_acl_list_type, - ]); - + // init error array + $this->loginInitErrorMessages(); + // acess right list + $this->loginLoadAccessRightList(); + // log allowed write flags $this->loginSetEditLogWriteTypeAvailable(); // this will be deprecated @@ -398,7 +280,7 @@ class Login } // ************************************************************************* - // **** PROTECTED INTERNAL + // **** MARK: PROTECTED INTERNAL // ************************************************************************* /** @@ -416,6 +298,7 @@ class Login } else { $this->log->critical($message, ['code' => $code]); } + // TODO throw error and not exit exit($code); } @@ -441,7 +324,7 @@ class Login } // ************************************************************************* - // **** PRIVATE INTERNAL + // **** MARK: PRIVATE INTERNAL // ************************************************************************* /** @@ -568,6 +451,20 @@ class Login } $this->password_forgot = $options['forgot_flow']; + // sync _SESSION acl settings + if ( + !isset($options['auth_resync_interval']) || + !is_numeric($options['auth_resync_interval']) || + $options['auth_resync_interval'] < 0 || + $options['auth_resync_interval'] > $this->default_session_gc_maxlifetime + ) { + // default 5 minutues + $options['auth_resync_interval'] = self::DEFAULT_AUTH_RESYNC_INTERVAL; + } else { + $options['auth_resync_interval'] = (int)$options['auth_resync_interval']; + } + $this->auth_resync_interval = $options['auth_resync_interval']; + // *** LANGUAGE // LANG: LOCALE PATH if (empty($options['locale_path'])) { @@ -579,7 +476,6 @@ class Login // set path $options['locale_path'] = BASE . INCLUDES . LOCALE; } - $this->session->set('LOCALE_PATH', $options['locale_path']); // LANG: LOCALE if (empty($options['site_locale'])) { trigger_error( @@ -614,7 +510,6 @@ class Login $options['set_domain'] = str_replace(DIRECTORY_SEPARATOR, '', CONTENT_PATH); } } - $this->session->set('DEFAULT_DOMAIN', $options['site_domain']); // LANG: ENCODING if (empty($options['site_encoding'])) { trigger_error( @@ -624,12 +519,212 @@ class Login $options['site_encoding'] = defined('SITE_ENCODING') && !empty(SITE_ENCODING) ? SITE_ENCODING : 'UTF-8'; } + // set enhancded security flag + if ( + empty($options['enhanced_security']) || + !is_bool($options['enhanced_security']) + ) { + $options['enhanced_security'] = true; + } + $this->header_enhance_security = $options['enhanced_security']; // write array to options $this->options = $options; return true; } + /** + * sets the login error message array + * + * @return void + */ + private function loginInitErrorMessages() + { + // string key, msg: string, flag: e (error), o (ok) + $this->login_error_msg = [ + '0' => [ + 'msg' => 'No error', + 'flag' => 'o' + ], + // actually obsolete + '100' => [ + 'msg' => '[EUCUUID] set from GET/POST!', + 'flag' => 'e', + ], + // query errors + '1009' => [ + 'msg' => 'Login query reading failed', + 'flag' => 'e', + ], + // user not found + '1010' => [ + 'msg' => 'Login Failed - Wrong Username or Password', + 'flag' => 'e' + ], + // general login error + '1011' => [ + 'msg' => 'Login Failed - General authentication error', + 'flag' => 'e' + ], + // fallback md5 password wrong + '1012' => [ + 'msg' => 'Login Failed - Wrong Username or Password', + 'flag' => 'e' + ], + // new password_hash wrong + '1013' => [ + 'msg' => 'Login Failed - Wrong Username or Password', + 'flag' => 'e' + ], + '1101' => [ + 'msg' => 'Login Failed - Login User ID must be validated', + 'flag' => 'e' + ], + '1102' => [ + 'msg' => 'Login Failed - Login User ID is outside valid date range', + 'flag' => 'e' + ], + '102' => [ + 'msg' => 'Login Failed - Please enter username and password', + 'flag' => 'e' + ], + '103' => [ + 'msg' => 'You do not have the rights to access this Page', + 'flag' => 'e' + ], + '104' => [ + 'msg' => 'Login Failed - User not enabled', + 'flag' => 'e' + ], + '105' => [ + 'msg' => 'Login Failed - User is locked', + 'flag' => 'e' + ], + '106' => [ + 'msg' => 'Login Failed - User is deleted', + 'flag' => 'e' + ], + '107' => [ + 'msg' => 'Login Failed - User in locked via date period', + 'flag' => 'e' + ], + '108' => [ + 'msg' => 'Login Failed - User is locked via Login User ID', + 'flag' => 'e' + ], + '109' => [ + 'msg' => 'Check permission query reading failed', + 'flag' => 'e' + ], + '110' => [ + 'msg' => 'Forced logout', + 'flag' => '', + ], + // actually this is an illegal user, but I mask it + '220' => [ + 'msg' => 'Password change - The user could not be found', + 'flag' => 'e' + ], + '200' => [ + 'msg' => 'Password change - Please enter username and old password', + 'flag' => 'e' + ], + '201' => [ + 'msg' => 'Password change - The user could not be found', + 'flag' => 'e' + ], + '202' => [ + 'msg' => 'Password change - The old password is not correct', + 'flag' => 'e' + ], + '203' => [ + 'msg' => 'Password change - Please fill out both new password fields', + 'flag' => 'e' + ], + '204' => [ + 'msg' => 'Password change - The new passwords do not match', + 'flag' => 'e' + ], + // we should also not here WHAT is valid + '205' => [ + 'msg' => 'Password change - The new password is not in a valid format', + 'flag' => 'e' + ], + // for OK password change + '300' => [ + 'msg' => 'Password change successful', + 'flag' => 'o' + ], + // this is bad bad error + '9999' => [ + 'msg' => 'Necessary crypt engine could not be found. Login is impossible', + 'flag' => 'e' + ], + ]; + } + + /** + * loads the access right list from the database + * + * @return void + */ + private function loginLoadAccessRightList(): void + { + // read the current edit_access_right list into an array + $q = <<= 0 + ORDER BY + level + SQL; + while (is_array($res = $this->db->dbReturn($q))) { + // level to description format (numeric) + $this->default_acl_list[$res['level']] = [ + 'type' => $res['type'], + 'name' => $res['name'] + ]; + $this->default_acl_list_type[(string)$res['type']] = (int)$res['level']; + } + // write that into the session + $this->session->setMany([ + 'LOGIN_DEFAULT_ACL_LIST' => $this->default_acl_list, + 'LOGIN_DEFAULT_ACL_LIST_TYPE' => $this->default_acl_list_type, + ]); + } + + /** + * Improves the application's security over HTTP(S) by setting specific headers + * + * @return void + */ + protected function loginEnhanceHttpSecurity(): void + { + // skip if not wanted + if (!$this->header_enhance_security) { + return; + } + // remove exposure of PHP version (at least where possible) + header_remove('X-Powered-By'); + // if the user is signed in + if ($this->permission_okay) { + // prevent clickjacking + header('X-Frame-Options: sameorigin'); + // prevent content sniffing (MIME sniffing) + header('X-Content-Type-Options: nosniff'); + + // disable caching of potentially sensitive data + header('Cache-Control: no-store, no-cache, must-revalidate', true); + header('Expires: Thu, 19 Nov 1981 00:00:00 GMT', true); + header('Pragma: no-cache', true); + } + } + + // MARK: validation checks + /** * Checks for all flags and sets error codes for each * In order: @@ -640,6 +735,7 @@ class Login * @param int $locked Locked because of too many invalid passwords * @param int $locked_period Locked because of time period set * @param int $login_user_id_locked Locked from using Login User Id + * @param int $force_logout Force logout counter, if higher than session, permission is false * @return bool */ private function loginValidationCheck( @@ -647,7 +743,8 @@ class Login int $enabled, int $locked, int $locked_period, - int $login_user_id_locked + int $login_user_id_locked, + int $force_logout ): bool { $validation = false; if ($deleted) { @@ -665,6 +762,8 @@ class Login } elseif ($login_user_id_locked) { // user is locked, either set or auto set $this->login_error = 108; + } elseif ($force_logout > $this->session->get('LOGIN_FORCE_LOGOUT')) { + $this->login_error = 110; } else { $validation = true; } @@ -748,6 +847,113 @@ class Login return $login_id_ok; } + /** + * write error data for login errors + * + * @param array $res + * @return void + */ + private function loginWriteLoginError(array $res) + { + if (!$this->login_error) { + return; + } + $login_error_date_first = ''; + if ($res['login_error_count'] == 0) { + $login_error_date_first = ", login_error_date_first = NOW()"; + } + // update login error count for this user + $q = <<db->dbExecParams( + str_replace('{LOGIN_ERROR_SQL}', $login_error_date_first, $q), + [$res['edit_user_id']] + ); + // totally lock the user if error max is reached + if ( + $this->max_login_error_count != -1 && + $res['login_error_count'] + 1 > $this->max_login_error_count + ) { + // do some alert reporting in case this error is too big + // if strict is set, lock this user + // this needs manual unlocking by an admin user + if ($res['strict'] && !in_array($this->username, $this->lock_deny_users)) { + $q = << $res + * @return void + */ + private function loginSetEditUserUidData(array $res) + { + // normal user processing + // set class var and session var + $this->edit_user_id = (int)$res['edit_user_id']; + $this->edit_user_cuid = (string)$res['cuid']; + $this->edit_user_cuuid = (string)$res['cuuid']; + $this->session->setMany([ + 'LOGIN_EUID' => $this->edit_user_id, + 'LOGIN_EUCUID' => $this->edit_user_cuid, + 'LOGIN_EUCUUID' => $this->edit_user_cuuid, + ]); + } + + /** + * check for re-loading of ACL data after a period of time + * or if any of the core session vars is not set + * + * @return void + */ + private function loginAuthResync() + { + if (!$this->session->get('LOGIN_LAST_AUTH_RESYNC')) { + $this->session->set('LOGIN_LAST_AUTH_RESYNC', 0); + } + // reauth on missing session vars and timed out re-sync interval + $mandatory_session_vars = [ + 'LOGIN_USER_NAME', 'LOGIN_GROUP_NAME', 'LOGIN_EUCUID', 'LOGIN_EUCUUID', + 'LOGIN_USER_ADDITIONAL_ACL', 'LOGIN_GROUP_ADDITIONAL_ACL', + 'LOGIN_ADMIN', 'LOGIN_GROUP_ACL_LEVEL', 'LOGIN_PAGES_ACL_LEVEL', 'LOGIN_USER_ACL_LEVEL', + 'LOGIN_UNIT', 'LOGIN_UNIT_DEFAULT_EACUID' + ]; + $force_reauth = false; + foreach ($mandatory_session_vars as $_session_var) { + if (!isset($_SESSION[$_session_var])) { + $force_reauth = true; + break; + } + } + if ( + $this->session->get('LOGIN_LAST_AUTH_RESYNC') + $this->auth_resync_interval <= time() && + $force_reauth == false + ) { + return; + } + if (($res = $this->loginLoadUserData($this->edit_user_cuuid)) === false) { + return; + } + // set the session vars + $this->loginSetSession($res); + } + + // MARK: MAIN LOGIN ACTION + /** * if user pressed login button this script is called, * but only if there is no preview euid set @@ -757,7 +963,11 @@ class Login private function loginLoginUser(): void { // if pressed login at least and is not yet loggined in - if ($this->euid || (!$this->login && !$this->login_user_id)) { + if ($this->edit_user_cuuid || (!$this->login && !$this->login_user_id)) { + // run reload user data based on re-auth timeout, but only if we got a set cuuid + if ($this->edit_user_cuuid) { + $this->loginAuthResync(); + } return; } // if not username AND password where given @@ -767,86 +977,8 @@ class Login $this->permission_okay = false; return; } - // have to get the global stuff here for setting it later - // we have to get the themes in here too - $q = "SELECT eu.edit_user_id, eu.cuid, eu.cuuid, eu.username, eu.password, " - . "eu.edit_group_id, " - . "eg.name AS edit_group_name, eu.admin, " - // additinal acl lists - . "eu.additional_acl AS user_additional_acl, " - . "eg.additional_acl AS group_additional_acl, " - // login error + locked - . "eu.login_error_count, eu.login_error_date_last, " - . "eu.login_error_date_first, eu.strict, eu.locked, " - // date based lock - . "CASE WHEN (" - . "(eu.lock_until IS NULL " - . "OR (eu.lock_until IS NOT NULL AND NOW() >= eu.lock_until)) " - . "AND (eu.lock_after IS NULL " - . "OR (eu.lock_after IS NOT NULL AND NOW() <= eu.lock_after))" - . ") THEN 0::INT ELSE 1::INT END locked_period, " - // debug (legacy) - . "eu.debug, eu.db_debug, " - // enabled - . "eu.enabled, eu.deleted, " - // for checks only - . "eu.login_user_id, " - // login id validation - . "CASE WHEN (" - . "(eu.login_user_id_valid_from IS NULL " - . "OR (eu.login_user_id_valid_from IS NOT NULL AND NOW() >= eu.login_user_id_valid_from)) " - . "AND (eu.login_user_id_valid_until IS NULL " - . "OR (eu.login_user_id_valid_until IS NOT NULL AND NOW() <= eu.login_user_id_valid_until))" - . ") THEN 1::INT ELSE 0::INT END AS login_user_id_valid_date, " - // check if user must login - . "CASE WHEN eu.login_user_id_revalidate_after IS NOT NULL " - . "AND eu.login_user_id_revalidate_after > '0 days'::INTERVAL " - . "AND (eu.login_user_id_last_revalidate + eu.login_user_id_revalidate_after)::DATE " - . "<= NOW()::DATE " - . "THEN 1::INT ELSE 0::INT END AS login_user_id_revalidate, " - . "eu.login_user_id_locked, " - // language - . "el.short_name AS locale, el.iso_name AS encoding, " - // levels - . "eareu.level AS user_level, eareu.type AS user_type, " - . "eareg.level AS group_level, eareg.type AS group_type, " - // colors - . "first.header_color AS first_header_color, " - . "second.header_color AS second_header_color, second.template " - . "FROM edit_user eu " - . "LEFT JOIN edit_scheme second ON " - . "(second.edit_scheme_id = eu.edit_scheme_id AND second.enabled = 1), " - . "edit_language el, edit_group eg, " - . "edit_access_right eareu, " - . "edit_access_right eareg, " - . "edit_scheme first " - . "WHERE first.edit_scheme_id = eg.edit_scheme_id " - . "AND eu.edit_group_id = eg.edit_group_id " - . "AND eu.edit_language_id = el.edit_language_id " - . "AND eu.edit_access_right_id = eareu.edit_access_right_id " - . "AND eg.edit_access_right_id = eareg.edit_access_right_id " - . "AND " - // either login_user_id OR password must be given - . (!empty($this->login_user_id && empty($this->username)) ? - // check with login id if set and NO username - "eu.login_user_id = " . $this->db->dbEscapeLiteral($this->login_user_id) . " " : - // password match is done in script, against old plain or new blowfish encypted - "LOWER(username) = " . $this->db->dbEscapeLiteral(strtolower($this->username)) . " " - ); - // reset any query data that might exist - $this->db->dbCacheReset($q); - // never cache return data - $res = $this->db->dbReturn($q, $this->db::NO_CACHE); - // query was not run successful - if (!empty($this->db->dbGetLastError())) { - $this->login_error = 1009; - $this->permission_okay = false; - return; - } elseif (!is_array($res)) { - // username is wrong, but we throw for wrong username - // and wrong password the same error - $this->login_error = 1010; - $this->permission_okay = false; + // load user data, abort on error + if (($res = $this->loginLoadUserData()) === false) { return; } // if login errors is half of max errors and the last login error @@ -864,7 +996,8 @@ class Login (int)$res['enabled'], (int)$res['locked'], (int)$res['locked_period'], - (int)$res['login_user_id_locked'] + (int)$res['login_user_id_locked'], + (int)$res['force_logout'] ) ) { // error set in method (104, 105, 106, 107, 108) @@ -893,241 +1026,31 @@ class Login // .' => HASH: '.(Password::passwordRehashCheck($res['password']) ? 'NEW NEEDED' : 'OK')); if (Password::passwordRehashCheck($res['password'])) { // update password hash to new one now - $q = "UPDATE edit_user " - . "SET password = '" . $this->db->dbEscapeString(Password::passwordSet($this->password)) - . "' WHERE edit_user_id = " . $res['edit_user_id']; - $this->db->dbExec($q); + $q = <<db->dbExecParams($q, [ + Password::passwordSet($this->password), + $res['edit_user_id'] + ]); } // normal user processing // set class var and session var - $this->euid = (int)$res['edit_user_id']; - $this->ecuid = (string)$res['cuid']; - $this->ecuuid = (string)$res['cuuid']; - $this->session->setMany([ - 'EUID' => $this->euid, - 'ECUID' => $this->ecuid, - 'ECUUID' => $this->ecuuid, - ]); - // check if user is okay - $this->loginCheckPermissions(); - if ($this->login_error == 0) { - if ( - !empty($res['login_user_id']) && - !empty($this->username) && !empty($this->password) - ) { - $q = "UPDATE edit_user SET " - . "login_user_id_last_revalidate = NOW() " - . "WHERE edit_user_id = " . $this->euid; - $this->db->dbExec($q); - } - $locale = $res['locale'] ?? 'en'; - $encoding = $res['encoding'] ?? 'UTF-8'; - $this->session->setMany([ - // now set all session vars and read page permissions - 'DEBUG_ALL' => $this->db->dbBoolean($res['debug']), - 'DB_DEBUG' => $this->db->dbBoolean($res['db_debug']), - // general info for user logged in - 'USER_NAME' => $res['username'], - 'ADMIN' => $res['admin'], - 'GROUP_NAME' => $res['edit_group_name'], - 'USER_ACL_LEVEL' => $res['user_level'], - 'USER_ACL_TYPE' => $res['user_type'], - 'USER_ADDITIONAL_ACL' => Json::jsonConvertToArray($res['user_additional_acl']), - 'GROUP_ACL_LEVEL' => $res['group_level'], - 'GROUP_ACL_TYPE' => $res['group_type'], - 'GROUP_ADDITIONAL_ACL' => Json::jsonConvertToArray($res['group_additional_acl']), - // deprecated TEMPLATE setting - 'TEMPLATE' => $res['template'] ? $res['template'] : '', - 'HEADER_COLOR' => !empty($res['second_header_color']) ? - $res['second_header_color'] : - $res['first_header_color'], - // LANGUAGE/LOCALE/ENCODING: - 'LANG' => $locale, - 'DEFAULT_CHARSET' => $encoding, - 'DEFAULT_LOCALE' => $locale . '.' . strtoupper($encoding), - 'DEFAULT_LANG' => $locale . '_' . strtolower(str_replace('-', '', $encoding)) - ]); - // missing # before, this is for legacy data, will be deprecated - if ( - !empty($this->session->get('HEADER_COLOR')) && - preg_match("/^[\dA-Fa-f]{6,8}$/", $this->session->get('HEADER_COLOR')) - ) { - $this->session->set('HEADER_COLOR', '#' . $this->session->get('HEADER_COLOR')); - } - // TODO: make sure that header color is valid: - // # + 6 hex - // # + 8 hex (alpha) - // rgb(), rgba(), hsl(), hsla() - // rgb: nnn.n for each - // hsl: nnn.n for first, nnn.n% for 2nd, 3rd - // Check\Colors::validateColor() - // reset any login error count for this user - if ($res['login_error_count'] > 0) { - $q = "UPDATE edit_user " - . "SET login_error_count = 0, login_error_date_last = NULL, " - . "login_error_date_first = NULL " - . "WHERE edit_user_id = " . $res['edit_user_id']; - $this->db->dbExec($q); - } - $edit_page_ids = []; - $pages = []; - $pages_acl = []; - // set pages access - $q = "SELECT ep.edit_page_id, ep.cuid, epca.cuid AS content_alias_uid, " - . "ep.hostname, ep.filename, ep.name AS edit_page_name, " - . "ep.order_number AS edit_page_order, ep.menu, " - . "ep.popup, ep.popup_x, ep.popup_y, ep.online, ear.level, ear.type " - . "FROM edit_page ep " - . "LEFT JOIN edit_page epca ON (epca.edit_page_id = ep.content_alias_edit_page_id)" - . ", edit_page_access epa, edit_access_right ear " - . "WHERE ep.edit_page_id = epa.edit_page_id " - . "AND ear.edit_access_right_id = epa.edit_access_right_id " - . "AND epa.enabled = 1 AND epa.edit_group_id = " . $res["edit_group_id"] . " " - . "ORDER BY ep.order_number"; - while (is_array($res = $this->db->dbReturn($q))) { - // page id array for sub data readout - $edit_page_ids[$res['edit_page_id']] = $res['cuid']; - // create the array for pages - $pages[$res['cuid']] = [ - 'edit_page_id' => $res['edit_page_id'], - 'cuid' => $res['cuid'], - // for reference of content data on a differen page - 'content_alias_uid' => $res['content_alias_uid'], - 'hostname' => $res['hostname'], - 'filename' => $res['filename'], - 'page_name' => $res['edit_page_name'], - 'order' => $res['edit_page_order'], - 'menu' => $res['menu'], - 'popup' => $res['popup'], - 'popup_x' => $res['popup_x'], - 'popup_y' => $res['popup_y'], - 'online' => $res['online'], - 'acl_level' => $res['level'], - 'acl_type' => $res['type'], - 'query' => [], - 'visible' => [] - ]; - // make reference filename -> level - $pages_acl[$res['filename']] = $res['level']; - } // for each page - // get the visible groups for all pages and write them to the pages - $q = "SELECT epvg.edit_page_id, name, flag " - . "FROM edit_visible_group evp, edit_page_visible_group epvg " - . "WHERE evp.edit_visible_group_id = epvg.edit_visible_group_id " - . "AND epvg.edit_page_id IN (" . join(', ', array_keys($edit_page_ids)) . ") " - . "ORDER BY epvg.edit_page_id"; - while (is_array($res = $this->db->dbReturn($q))) { - $pages[$edit_page_ids[$res['edit_page_id']]]['visible'][$res['name']] = $res['flag']; - } - // get the same for the query strings - $q = "SELECT eqs.edit_page_id, name, value, dynamic FROM edit_query_string eqs " - . "WHERE enabled = 1 AND edit_page_id " - . "IN (" . join(', ', array_keys($edit_page_ids)) . ") " - . "ORDER BY eqs.edit_page_id"; - while (is_array($res = $this->db->dbReturn($q))) { - $pages[$edit_page_ids[$res['edit_page_id']]]['query'][] = [ - 'name' => $res['name'], - 'value' => $res['value'], - 'dynamic' => $res['dynamic'] - ]; - } - // get the page content and add them to the page - $q = "SELECT epc.edit_page_id, epc.name, epc.uid, epc.order_number, " - . "epc.online, ear.level, ear.type " - . "FROM edit_page_content epc, edit_access_right ear " - . "WHERE epc.edit_access_right_id = ear.edit_access_right_id AND " - . "epc.edit_page_id IN (" . join(', ', array_keys($edit_page_ids)) . ") " - . "ORDER BY epc.order_number"; - while (is_array($res = $this->db->dbReturn($q))) { - $pages[$edit_page_ids[$res['edit_page_id']]]['content'][$res['uid']] = [ - 'name' => $res['name'], - 'uid' => $res['uid'], - 'online' => $res['online'], - 'order' => $res['order_number'], - // access name and level - 'acl_type' => $res['type'], - 'acl_level' => $res['level'] - ]; - } - // write back the pages data to the output array - $this->session->setMany([ - 'PAGES' => $pages, - 'PAGES_ACL_LEVEL' => $pages_acl, - ]); - // load the edit_access user rights - $q = "SELECT ea.edit_access_id, level, type, ea.name, " - . "ea.color, ea.uid, edit_default, ea.additional_acl " - . "FROM edit_access_user eau, edit_access_right ear, edit_access ea " - . "WHERE eau.edit_access_id = ea.edit_access_id " - . "AND eau.edit_access_right_id = ear.edit_access_right_id " - . "AND eau.enabled = 1 AND edit_user_id = " . $this->euid . " " - . "ORDER BY ea.name"; - $unit_access = []; - $eauid = []; - $unit_acl = []; - $unit_uid = []; - while (is_array($res = $this->db->dbReturn($q))) { - // read edit access data fields and drop them into the unit access array - $q_sub = "SELECT name, value " - . "FROM edit_access_data " - . "WHERE enabled = 1 AND edit_access_id = " . $res['edit_access_id']; - $ea_data = []; - while (is_array($res_sub = $this->db->dbReturn($q_sub))) { - $ea_data[$res_sub['name']] = $res_sub['value']; - } - // build master unit array - $unit_access[$res['edit_access_id']] = [ - 'id' => (int)$res['edit_access_id'], - 'acl_level' => $res['level'], - 'acl_type' => $res['type'], - 'name' => $res['name'], - 'uid' => $res['uid'], - 'color' => $res['color'], - 'default' => $res['edit_default'], - 'additional_acl' => Json::jsonConvertToArray($res['additional_acl']), - 'data' => $ea_data - ]; - // set the default unit - if ($res['edit_default']) { - $this->session->set('UNIT_DEFAULT', (int)$res['edit_access_id']); - } - $unit_uid[$res['uid']] = (int)$res['edit_access_id']; - // sub arrays for simple access - array_push($eauid, $res['edit_access_id']); - $unit_acl[$res['edit_access_id']] = $res['level']; - } - $this->session->setMany([ - 'UNIT_UID' => $unit_uid, - 'UNIT' => $unit_access, - 'UNIT_ACL_LEVEL' => $unit_acl, - 'EAID' => $eauid, - ]); - } // user has permission to THIS page + $this->loginSetEditUserUidData($res); + // set the last login time stamp for normal login only (not for reauthenticate) + $this->db->dbExecParams(<<edit_user_id]); + // set the session vars + $this->loginSetSession($res); } // user was not enabled or other login error - if ($this->login_error && is_array($res)) { - $login_error_date_first = ''; - if ($res['login_error_count'] == 0) { - $login_error_date_first = ", login_error_date_first = NOW()"; - } - // update login error count for this user - $q = "UPDATE edit_user " - . "SET login_error_count = login_error_count + 1, " - . "login_error_date_last = NOW() " . $login_error_date_first . " " - . "WHERE edit_user_id = " . $res['edit_user_id']; - $this->db->dbExec($q); - // totally lock the user if error max is reached - if ( - $this->max_login_error_count != -1 && - $res['login_error_count'] + 1 > $this->max_login_error_count - ) { - // do some alert reporting in case this error is too big - // if strict is set, lock this user - // this needs manual unlocking by an admin user - if ($res['strict'] && !in_array($this->username, $this->lock_deny_users)) { - $q = "UPDATE edit_user SET locked = 1 WHERE edit_user_id = " . $res['edit_user_id']; - } - } - } + // check for login error and write to the user + $this->loginWriteLoginError($res); // if there was an login error, show login screen if ($this->login_error) { // reset the perm var, to confirm logout @@ -1135,6 +1058,398 @@ class Login } } + /** + * load user data and all connect4ed settings + * + * @param ?string $edit_user_cuuid for re-auth + * @return array|false + */ + private function loginLoadUserData(?string $edit_user_cuuid = null): array|false + { + $q = <<= eu.lock_until) + ) + AND ( + eu.lock_after IS NULL + OR (eu.lock_after IS NOT NULL AND NOW() <= eu.lock_after) + ) + ) THEN 0::INT ELSE 1::INT END locked_period, + -- enabled + eu.enabled, eu.deleted, + -- for checks only + eu.login_user_id, + -- login id validation + CASE WHEN ( + ( + eu.login_user_id_valid_from IS NULL + OR (eu.login_user_id_valid_from IS NOT NULL AND NOW() >= eu.login_user_id_valid_from) + ) + AND ( + eu.login_user_id_valid_until IS NULL + OR (eu.login_user_id_valid_until IS NOT NULL AND NOW() <= eu.login_user_id_valid_until) + ) + ) THEN 1::INT ELSE 0::INT END AS login_user_id_valid_date, + -- check if user must login + CASE WHEN + eu.login_user_id_revalidate_after IS NOT NULL + AND eu.login_user_id_revalidate_after > '0 days'::INTERVAL + AND (eu.login_user_id_last_revalidate + eu.login_user_id_revalidate_after)::DATE + <= NOW()::DATE + THEN 1::INT ELSE 0::INT END AS login_user_id_revalidate, + eu.login_user_id_locked, + -- language + el.short_name AS locale, el.iso_name AS encoding, + -- levels + eareu.level AS user_level, eareu.type AS user_type, + eareg.level AS group_level, eareg.type AS group_type, + -- colors + first.header_color AS first_header_color, + second.header_color AS second_header_color, second.template + FROM edit_user eu + LEFT JOIN edit_scheme second ON + (second.edit_scheme_id = eu.edit_scheme_id AND second.enabled = 1), + edit_language el, edit_group eg, + edit_access_right eareu, + edit_access_right eareg, + edit_scheme first + WHERE first.edit_scheme_id = eg.edit_scheme_id + AND eu.edit_group_id = eg.edit_group_id + AND eu.edit_language_id = el.edit_language_id + AND eu.edit_access_right_id = eareu.edit_access_right_id + AND eg.edit_access_right_id = eareg.edit_access_right_id + AND {SEARCH_QUERY} + SQL; + $params = []; + $replace_string = ''; + // if login is OK and we have edit_user_cuuid as parameter, then this is internal re-auth + // else login_user_id OR password must be given + if (!empty($edit_user_cuuid)) { + $replace_string = 'eu.cuuid = $1'; + $params = [$this->edit_user_cuuid]; + } elseif (!empty($this->login_user_id) && empty($this->username)) { + // check with login id if set and NO username + $replace_string = 'eu.login_user_id = $1'; + $params = [$this->login_user_id]; + } else { + // password match is done in script, against old plain or new blowfish encypted + $replace_string = 'LOWER(username) = $1'; + $params = [strtolower($this->username)]; + } + $q = str_replace( + '{SEARCH_QUERY}', + $replace_string, + $q + ); + // reset any query data that might exist + $this->db->dbCacheReset($q, $params); + // never cache return data + $res = $this->db->dbReturnParams($q, $params, $this->db::NO_CACHE); + // query was not run successful + if (!empty($this->db->dbGetLastError())) { + $this->login_error = 1009; + $this->permission_okay = false; + return false; + } elseif (!is_array($res)) { + // username is wrong, but we throw for wrong username + // and wrong password the same error + // unless with have edit user cuuid set then we run an general ACL error + if (empty($edit_user_cuuid)) { + $this->login_error = 1010; + } else { + $this->login_error = 1011; + } + $this->permission_okay = false; + return false; + } + return $res; + } + + // MARK: login set all session variables + + /** + * set all the _SESSION variables + * + * @param array $res user data loaded query result + * @return void + */ + private function loginSetSession(array $res): void + { + // user has permission to THIS page + if ($this->login_error != 0) { + return; + } + // set the dit group id + $edit_group_id = $res["edit_group_id"]; + $edit_user_id = (int)$res['edit_user_id']; + // update last revalidate flag + if ( + !empty($res['login_user_id']) && + !empty($this->username) && !empty($this->password) + ) { + $q = <<db->dbExecParams($q, [$edit_user_id]); + } + $locale = $res['locale'] ?? 'en'; + $encoding = $res['encoding'] ?? 'UTF-8'; + $this->session->setMany([ + // now set all session vars and read page permissions + // DEBUG flag is deprecated + // 'DEBUG_ALL' => $this->db->dbBoolean($res['debug']), + // 'DB_DEBUG' => $this->db->dbBoolean($res['db_debug']), + // login timestamp + 'LOGIN_LAST_AUTH_RESYNC' => time(), + // current forced logout counter + 'LOGIN_FORCE_LOGOUT' => $res['force_logout'], + // general info for user logged in + 'LOGIN_USER_NAME' => $res['username'], + 'LOGIN_EMAIL' => $res['email'], + 'LOGIN_ADMIN' => $res['admin'], + 'LOGIN_GROUP_NAME' => $res['edit_group_name'], + 'LOGIN_USER_ACL_LEVEL' => $res['user_level'], + 'LOGIN_USER_ACL_TYPE' => $res['user_type'], + 'LOGIN_USER_ADDITIONAL_ACL' => Json::jsonConvertToArray($res['user_additional_acl']), + 'LOGIN_GROUP_ACL_LEVEL' => $res['group_level'], + 'LOGIN_GROUP_ACL_TYPE' => $res['group_type'], + 'LOGIN_GROUP_ADDITIONAL_ACL' => Json::jsonConvertToArray($res['group_additional_acl']), + // deprecated TEMPLATE setting + // 'TEMPLATE' => $res['template'] ? $res['template'] : '', + 'LOGIN_HEADER_COLOR' => !empty($res['second_header_color']) ? + $res['second_header_color'] : + $res['first_header_color'], + // LANGUAGE/LOCALE/ENCODING: + // 'LOGIN_LANG' => $locale, + 'DEFAULT_CHARSET' => $encoding, + 'DEFAULT_LOCALE' => $locale . '.' . strtoupper($encoding), + 'DEFAULT_LANG' => $locale . '_' . strtolower(str_replace('-', '', $encoding)) + ]); + // missing # before, this is for legacy data, will be deprecated + if ( + !empty($this->session->get('LOGIN_HEADER_COLOR')) && + preg_match("/^[\dA-Fa-f]{6,8}$/", $this->session->get('LOGIN_HEADER_COLOR')) + ) { + $this->session->set('LOGIN_HEADER_COLOR', '#' . $this->session->get('LOGIN_HEADER_COLOR')); + } + // TODO: make sure that header color is valid: + // # + 6 hex + // # + 8 hex (alpha) + // rgb(), rgba(), hsl(), hsla() + // rgb: nnn.n for each + // hsl: nnn.n for first, nnn.n% for 2nd, 3rd + // Check\Colors::validateColor() + // reset any login error count for this user + if ($res['login_error_count'] > 0) { + $q = <<db->dbExecParams($q, [$edit_user_id]); + } + $edit_page_ids = []; + $pages = []; + $pages_acl = []; + // set pages access + $q = <<db->dbReturnParams($q, [$edit_group_id]))) { + // page id array for sub data readout + $edit_page_ids[$res['edit_page_id']] = $res['cuid']; + // create the array for pages + $pages[$res['cuid']] = [ + 'edit_page_id' => $res['edit_page_id'], + 'cuid' => $res['cuid'], + 'cuuid' => $res['cuuid'], + // for reference of content data on a differen page + 'content_alias_uid' => $res['content_alias_uid'], + 'hostname' => $res['hostname'], + 'filename' => $res['filename'], + 'page_name' => $res['edit_page_name'], + 'order' => $res['edit_page_order'], + 'menu' => $res['menu'], + 'popup' => $res['popup'], + 'popup_x' => $res['popup_x'], + 'popup_y' => $res['popup_y'], + 'online' => $res['online'], + 'acl_level' => $res['level'], + 'acl_type' => $res['type'], + 'query' => [], + 'visible' => [] + ]; + // make reference filename -> level + $pages_acl[$res['filename']] = $res['level']; + } // for each page + // edit page id params + $params = ['{' . join(',', array_keys($edit_page_ids)) . '}']; + // get the visible groups for all pages and write them to the pages + $q = <<db->dbReturnParams($q, $params))) { + $pages[$edit_page_ids[$res['edit_page_id']]]['visible'][$res['name']] = $res['flag']; + } + // get the same for the query strings + $q = <<db->dbReturnParams($q, $params))) { + $pages[$edit_page_ids[$res['edit_page_id']]]['query'][] = [ + 'name' => $res['name'], + 'value' => $res['value'], + 'dynamic' => $res['dynamic'] + ]; + } + // get the page content and add them to the page + $q = <<db->dbReturnParams($q, $params))) { + $pages[$edit_page_ids[$res['edit_page_id']]]['content'][$res['uid']] = [ + 'name' => $res['name'], + 'uid' => $res['uid'], + 'cuid' => $res['cuid'], + 'cuuid' => $res['cuuid'], + 'online' => $res['online'], + 'order' => $res['order_number'], + // access name and level + 'acl_type' => $res['type'], + 'acl_level' => $res['level'] + ]; + } + // write back the pages data to the output array + $this->session->setMany([ + 'LOGIN_PAGES' => $pages, + 'LOGIN_PAGES_ACL_LEVEL' => $pages_acl, + ]); + // load the edit_access user rights + $q = <<db->dbReturnParams($q, [$edit_user_id]))) { + // read edit access data fields and drop them into the unit access array + $q_sub = <<db->dbReturnParams($q_sub, [$res['edit_access_id']]))) { + $ea_data[$res_sub['name']] = $res_sub['value']; + } + // build master unit array + $unit_access_cuid[$res['cuid']] = [ + 'id' => (int)$res['edit_access_id'], // DEPRECATED + 'cuuid' => $res['cuuid'], + 'acl_level' => $res['level'], + 'acl_type' => $res['type'], + 'name' => $res['name'], + 'uid' => $res['uid'], + 'color' => $res['color'], + 'default' => $res['edit_default'], + 'additional_acl' => Json::jsonConvertToArray($res['additional_acl']), + 'data' => $ea_data + ]; + $unit_access_eaid[$res['edit_access_id']] = [ + 'cuid' => $res['cuid'], + ]; + // set the default unit + $this->session->setMany([ + 'LOGIN_UNIT_DEFAULT_EAID' => null, + 'LOGIN_UNIT_DEFAULT_EACUID' => null, + ]); + if ($res['edit_default']) { + $this->session->set('LOGIN_UNIT_DEFAULT_EAID', (int)$res['edit_access_id']); // DEPRECATED + $this->session->set('LOGIN_UNIT_DEFAULT_EACUID', (int)$res['cuid']); + } + $unit_uid_lookup[$res['uid']] = $res['edit_access_id']; // DEPRECATED + $unit_cuid_lookup[$res['uid']] = $res['cuid']; + // sub arrays for simple access + array_push($eaid, $res['edit_access_id']); + array_push($eacuid, $res['cuid']); + $unit_acl[$res['cuid']] = $res['level']; + } + $this->session->setMany([ + 'LOGIN_UNIT_UID' => $unit_uid_lookup, // DEPRECATED + 'LOGIN_UNIT_CUID' => $unit_cuid_lookup, + 'LOGIN_UNIT' => $unit_access_cuid, + 'LOGIN_UNIT_LEGACY' => $unit_access_eaid, // DEPRECATED + 'LOGIN_UNIT_ACL_LEVEL' => $unit_acl, + 'LOGIN_EAID' => $eaid, // DEPRECATED + 'LOGIN_EACUID' => $eacuid, + ]); + } + + // MARK: login set ACL + /** * sets all the basic ACLs * init set the basic acl the user has, based on the following rules @@ -1160,21 +1475,21 @@ class Login return; } // username (login), group name - $this->acl['user_name'] = $_SESSION['USER_NAME']; - $this->acl['group_name'] = $_SESSION['GROUP_NAME']; + $this->acl['user_name'] = $_SESSION['LOGIN_USER_NAME']; + $this->acl['group_name'] = $_SESSION['LOGIN_GROUP_NAME']; // edit user cuid - $this->acl['ecuid'] = $_SESSION['ECUID']; - $this->acl['ecuuid'] = $_SESSION['ECUUID']; + $this->acl['eucuid'] = $_SESSION['LOGIN_EUCUID']; + $this->acl['eucuuid'] = $_SESSION['LOGIN_EUCUUID']; // set additional acl $this->acl['additional_acl'] = [ - 'user' => $_SESSION['USER_ADDITIONAL_ACL'], - 'group' => $_SESSION['GROUP_ADDITIONAL_ACL'], + 'user' => $_SESSION['LOGIN_USER_ADDITIONAL_ACL'], + 'group' => $_SESSION['LOGIN_GROUP_ADDITIONAL_ACL'], ]; // we start with the default acl $this->acl['base'] = $this->default_acl_level; // set admin flag and base to 100 - if (!empty($_SESSION['ADMIN'])) { + if (!empty($_SESSION['LOGIN_ADMIN'])) { $this->acl['admin'] = 1; $this->acl['base'] = 100; } else { @@ -1182,80 +1497,83 @@ class Login // now go throw the flow and set the correct ACL // user > page > group // group ACL 0 - if ($_SESSION['GROUP_ACL_LEVEL'] != -1) { - $this->acl['base'] = (int)$_SESSION['GROUP_ACL_LEVEL']; + if ($_SESSION['LOGIN_GROUP_ACL_LEVEL'] != -1) { + $this->acl['base'] = (int)$_SESSION['LOGIN_GROUP_ACL_LEVEL']; } // page ACL 1 if ( - isset($_SESSION['PAGES_ACL_LEVEL'][$this->page_name]) && - $_SESSION['PAGES_ACL_LEVEL'][$this->page_name] != -1 + isset($_SESSION['LOGIN_PAGES_ACL_LEVEL'][$this->page_name]) && + $_SESSION['LOGIN_PAGES_ACL_LEVEL'][$this->page_name] != -1 ) { - $this->acl['base'] = (int)$_SESSION['PAGES_ACL_LEVEL'][$this->page_name]; + $this->acl['base'] = (int)$_SESSION['LOGIN_PAGES_ACL_LEVEL'][$this->page_name]; } // user ACL 2 - if ($_SESSION['USER_ACL_LEVEL'] != -1) { - $this->acl['base'] = (int)$_SESSION['USER_ACL_LEVEL']; + if ($_SESSION['LOGIN_USER_ACL_LEVEL'] != -1) { + $this->acl['base'] = (int)$_SESSION['LOGIN_USER_ACL_LEVEL']; } } - $this->session->set('BASE_ACL_LEVEL', $this->acl['base']); + $this->session->set('LOGIN_BASE_ACL_LEVEL', $this->acl['base']); // set the current page acl // start with base acl // set group if not -1, overrides default // set page if not -1, overrides group set $this->acl['page'] = $this->acl['base']; - if ($_SESSION['GROUP_ACL_LEVEL'] != -1) { - $this->acl['page'] = $_SESSION['GROUP_ACL_LEVEL']; + if ($_SESSION['LOGIN_GROUP_ACL_LEVEL'] != -1) { + $this->acl['page'] = $_SESSION['LOGIN_GROUP_ACL_LEVEL']; } if ( - isset($_SESSION['PAGES_ACL_LEVEL'][$this->page_name]) && - $_SESSION['PAGES_ACL_LEVEL'][$this->page_name] != -1 + isset($_SESSION['LOGIN_PAGES_ACL_LEVEL'][$this->page_name]) && + $_SESSION['LOGIN_PAGES_ACL_LEVEL'][$this->page_name] != -1 ) { - $this->acl['page'] = $_SESSION['PAGES_ACL_LEVEL'][$this->page_name]; + $this->acl['page'] = $_SESSION['LOGIN_PAGES_ACL_LEVEL'][$this->page_name]; } $this->acl['unit_id'] = null; $this->acl['unit_name'] = null; $this->acl['unit_uid'] = null; $this->acl['unit'] = []; + $this->acl['unit_legacy'] = []; $this->acl['unit_detail'] = []; // PER ACCOUNT (UNIT/edit access)-> - foreach ($_SESSION['UNIT'] as $ea_id => $unit) { + foreach ($_SESSION['LOGIN_UNIT'] as $ea_cuid => $unit) { // if admin flag is set, all units are set to 100 if (!empty($this->acl['admin'])) { - $this->acl['unit'][$ea_id] = $this->acl['base']; + $this->acl['unit'][$ea_cuid] = $this->acl['base']; } else { if ($unit['acl_level'] != -1) { - $this->acl['unit'][$ea_id] = $unit['acl_level']; + $this->acl['unit'][$ea_cuid] = $unit['acl_level']; } else { - $this->acl['unit'][$ea_id] = $this->acl['base']; + $this->acl['unit'][$ea_cuid] = $this->acl['base']; } } + // legacy + $this->acl['unit_legacy'][$unit['id']] = $this->acl['unit'][$ea_cuid]; // detail name/level set - $this->acl['unit_detail'][$ea_id] = [ + $this->acl['unit_detail'][$ea_cuid] = [ 'name' => $unit['name'], 'uid' => $unit['uid'], - 'level' => $this->default_acl_list[$this->acl['unit'][$ea_id]]['name'] ?? -1, + 'level' => $this->default_acl_list[$this->acl['unit'][$ea_cuid]]['name'] ?? -1, 'default' => $unit['default'], 'data' => $unit['data'], 'additional_acl' => $unit['additional_acl'] ]; // set default if (!empty($unit['default'])) { - $this->acl['unit_id'] = $unit['id']; + $this->acl['unit_cuid'] = $ea_cuid; $this->acl['unit_name'] = $unit['name']; $this->acl['unit_uid'] = $unit['uid']; } } // flag if to show extra edit access drop downs (because user has multiple groups assigned) - if (count($_SESSION['UNIT']) > 1) { + if (count($_SESSION['LOGIN_UNIT']) > 1) { $this->acl['show_ea_extra'] = true; } else { $this->acl['show_ea_extra'] = false; } // set the default edit access - $this->acl['default_edit_access'] = $_SESSION['UNIT_DEFAULT'] ?? null; + $this->acl['default_edit_access'] = $_SESSION['LOGIN_UNIT_DEFAULT_EACUID']; // integrate the type acl list, but only for the keyword -> level $this->acl['min'] = $this->default_acl_list_type; // set the full acl list too (lookup level number and get level data) @@ -1264,6 +1582,8 @@ class Login // $this->debug('ACL', $this->print_ar($this->acl)); } + // MARK: login set locale + /** * set locale * if invalid, set to empty string @@ -1323,6 +1643,8 @@ class Login ]; } + // MARK: password handling + /** * checks if the password is in a valid format * @@ -1461,6 +1783,8 @@ class Login $this->writeEditLog($event, $data, $this->login_error, $this->pw_username); } + // MARK: set HTML login page + /** * creates the login html part if no permission (error) is set * this does not print anything yet @@ -1489,6 +1813,10 @@ class Login $this->login_template['strings']['LANGUAGE'] = $locales['lang'] ?? 'en'; // if password change is okay + // TODO: this should be a "forgot" password + // -> input email + // -> send to user with link + token + // -> validate token -> show change password if ($this->password_change) { $html_string_password_change = $this->login_template['password_change']; @@ -1505,14 +1833,14 @@ class Login // print error messagae if ($this->login_error) { $html_string_password_change = str_replace( - '{ERROR_MSG}', - $this->loginGetErrorMsg($this->login_error) . '
', + ['{ERROR_VISIBLE}', '{ERROR_MSG}'], + ['login-visible', $this->loginGetErrorMsg($this->login_error)], $html_string_password_change ); } else { $html_string_password_change = str_replace( - '{ERROR_MSG}', - '
', + ['{ERROR_VISIBLE}', '{ERROR_MSG}'], + ['login-hidden', ''], $html_string_password_change ); } @@ -1520,8 +1848,11 @@ class Login if ($this->change_password && !$this->password_change_ok) { $html_string_password_change = str_replace( '{PASSWORD_CHANGE_SHOW}', - '', + << + ShowHideDiv('login_pw_change_div'); + + HTML, $html_string_password_change ); } else { @@ -1548,18 +1879,22 @@ class Login // print error messagae if ($this->login_error) { $html_string = str_replace( - '{ERROR_MSG}', - $this->loginGetErrorMsg($this->login_error) . '
', + ['{ERROR_VISIBLE}', '{ERROR_MSG}'], + ['login-visible', $this->loginGetErrorMsg($this->login_error)], $html_string ); } elseif ($this->password_change_ok && $this->password_change) { $html_string = str_replace( - '{ERROR_MSG}', - $this->loginGetErrorMsg(300) . '
', + ['{ERROR_VISIBLE}', '{ERROR_MSG}'], + ['login-visible', $this->loginGetErrorMsg(300)], $html_string ); } else { - $html_string = str_replace('{ERROR_MSG}', '
', $html_string); + $html_string = str_replace( + ['{ERROR_VISIBLE}', '{ERROR_MSG}'], + ['login-hidden', ''], + $html_string + ); } // create the replace array context @@ -1570,6 +1905,8 @@ class Login return $html_string; } + // MARK: logout call + /** * last function called, writes log and prints out error msg and * exists script if permission 0 @@ -1591,11 +1928,15 @@ class Login $event = 'No Permission'; } // prepare for log - if ($this->euid) { + if ($this->edit_user_cuuid) { // get user from user table - $q = "SELECT username FROM edit_user WHERE edit_user_id = " . $this->euid; + $q = <<db->dbReturnRow($q))) { + if (is_array($res = $this->db->dbReturnRowParams($q, [$this->edit_user_cuuid]))) { $username = $res['username']; } } // if euid is set, get username (or try) @@ -1610,6 +1951,8 @@ class Login } } + // MARK: set template for login page + /** * checks if there are external templates, if not uses internal fallback ones * @@ -1636,39 +1979,93 @@ class Login 'NEW_PASSWORD' => $this->l->__('New Password'), 'NEW_PASSWORD_CONFIRM' => $this->l->__('New Password confirm'), 'CLOSE' => $this->l->__('Close'), - 'JS_SHOW_HIDE' => "function ShowHideDiv(id) { " - . "element = document.getElementById(id); " - . "if (element.className == 'visible' || !element.className) element.className = 'hidden'; " - . "else element.className = 'visible'; }", - 'PASSWORD_CHANGE_BUTTON' => '' + 'JS_SHOW_HIDE' => << str_replace( + '{PASSWORD_CHANGE_BUTTON_VALUE}', + $strings['PASSWORD_CHANGE_BUTTON_VALUE'], + // phpcs:disable Generic.Files.LineLength + << + HTML + // phpcs:enable Generic.Files.LineLength + ), ]); - // TODO: submit or JS to set target page as ajax call - // NOTE: for the HTML block I ignore line lengths - // phpcs:disable + // phpcs:disable Generic.Files.LineLength $this->login_template['password_change'] = << - - - - - - - - - - - - -

{TITLE_PASSWORD_CHANGE}

{ERROR_MSG}
{USERNAME}
{OLD_PASSWORD}
{NEW_PASSWORD}
{NEW_PASSWORD_CONFIRM}
-
+ {PASSWORD_CHANGE_SHOW} HTML; - // phpcs:enable + // phpcs:enable Generic.Files.LineLength } if ($this->password_forgot) { + // TODO: create a password forget request flow } if (!$this->password_change && !$this->password_forgot) { $strings = array_merge($strings, [ @@ -1687,71 +2084,162 @@ HTML; } // now check templates - // TODO: submit or JS to set target page as ajax call if (!$this->login_template['template']) { + // phpcs:disable Generic.Files.LineLength $this->login_template['template'] = << -{HTML_TITLE} - - + {LOGOUT_TARGET} -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - -
-

{TITLE}

-
 
- {ERROR_MSG} -
{USERNAME}
{PASSWORD}
- - {PASSWORD_CHANGE_BUTTON} -
-

-
 
-{PASSWORD_CHANGE_DIV} +
HTML; + // phpcs:enable Generic.Files.LineLength } } @@ -1884,16 +2372,18 @@ HTML; $q = <<db->dbExecParams( @@ -1904,13 +2394,14 @@ HTML; ), [ // row 1 - empty($username) ? $this->session->get('USER_NAME') ?? '' : $username, - is_numeric($this->session->get('EUID')) ? - $this->session->get('EUID') : null, - is_string($this->session->get('ECUID')) ? - $this->session->get('ECUID') : null, - !empty($this->session->get('ECUUID')) && Uids::validateUuuidv4($this->session->get('ECUUID')) ? - $this->session->get('ECUUID') : null, + empty($username) ? $this->session->get('LOGIN_USER_NAME') ?? '' : $username, + is_numeric($this->session->get('LOGIN_EUID')) ? + $this->session->get('LOGIN_EUID') : null, + is_string($this->session->get('LOGIN_EUCUID')) ? + $this->session->get('LOGIN_EUCUID') : null, + !empty($this->session->get('LOGIN_EUCUUID')) && + Uids::validateUuuidv4($this->session->get('LOGIN_EUCUUID')) ? + $this->session->get('LOGIN_EUCUUID') : null, (string)$event, (string)$error, $data_write, @@ -1918,29 +2409,45 @@ HTML; (string)$this->page_name, // row 2 $_SERVER["REMOTE_ADDR"] ?? null, + Json::jsonConvertArrayTo([ + 'REMOTE_ADDR' => $_SERVER["REMOTE_ADDR"] ?? null, + 'HTTP_X_FORWARDED_FOR' => !empty($_SERVER['HTTP_X_FORWARDED_FOR']) ? + explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']) + : [], + 'CLIENT_IP' => !empty($_SERVER['CLIENT_IP']) ? + explode(',', $_SERVER['CLIENT_IP']) + : [], + ]), $_SERVER['HTTP_USER_AGENT'] ?? null, $_SERVER['HTTP_REFERER'] ?? null, $_SERVER['SCRIPT_FILENAME'] ?? null, $_SERVER['QUERY_STRING'] ?? null, + $_SERVER['REQUEST_SCHEME'] ?? null, $_SERVER['SERVER_NAME'] ?? null, - $_SERVER['HTTP_HOST'] ?? null, // row 3 - $_SERVER['HTTP_ACCEPT'] ?? null, - $_SERVER['HTTP_ACCEPT_CHARSET'] ?? null, - $_SERVER['HTTP_ACCEPT_ENCODING'] ?? null, + $_SERVER['HTTP_HOST'] ?? null, + Json::jsonConvertArrayTo([ + 'HTTP_ACCEPT' => $_SERVER['HTTP_ACCEPT'] ?? null, + 'HTTP_ACCEPT_CHARSET' => $_SERVER['HTTP_ACCEPT_CHARSET'] ?? null, + 'HTTP_ACCEPT_LANGUAGE' => $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? null, + 'HTTP_ACCEPT_ENCODING' => $_SERVER['HTTP_ACCEPT_ENCODING'] ?? null, + ]), $this->session->getSessionId() !== '' ? $this->session->getSessionId() : null, // row 4 - $action_set['action'] ?? null, - $action_set['action_id'] ?? null, - $action_set['action_sub_id'] ?? null, - $action_set['action_yes'] ?? null, - $action_set['action_flag'] ?? null, - $action_set['action_menu'] ?? null, - $action_set['action_loaded'] ?? null, - $action_set['action_value'] ?? null, - $action_set['action_type'] ?? null, - $action_set['action_error'] ?? null, + // action data as JSONB + Json::jsonConvertArrayTo([ + 'action' => $action_set['action'] ?? null, + 'action_id' => $action_set['action_id'] ?? null, + 'action_sub_id' => $action_set['action_sub_id'] ?? null, + 'action_yes' => $action_set['action_yes'] ?? null, + 'action_flag' => $action_set['action_flag'] ?? null, + 'action_menu' => $action_set['action_menu'] ?? null, + 'action_loaded' => $action_set['action_loaded'] ?? null, + 'action_value' => $action_set['action_value'] ?? null, + 'action_type' => $action_set['action_type'] ?? null, + 'action_error' => $action_set['action_error'] ?? null, + ]) ], 'NULL' ); @@ -1967,7 +2474,7 @@ HTML; // **** PUBLIC INTERNAL // ************************************************************************* - // MARK: LOGIN CALL + // MARK: MASTER PUBLIC LOGIN CALL /** * Main call that needs to be run to actaully check for login @@ -2037,10 +2544,7 @@ HTML; } } // if there is none, there is none, saves me POST/GET check - $this->euid = (int)($this->session->get('EUID') ?? 0); - // TODO: allow load from cuid - // $this->ecuid = (string)($this->session->get('ECUID') ?? ''); - // $this->ecuuid = (string)($this->session->get('ECUUID') ?? ''); + $this->edit_user_cuuid = (string)($this->session->get('LOGIN_EUCUUID') ?? ''); // get login vars, are so, can't be changed // prepare // pass on vars to Object vars @@ -2049,11 +2553,11 @@ HTML; $this->password = $_POST['login_password'] ?? ''; $this->logout = $_POST['login_logout'] ?? ''; // password change vars - $this->change_password = $_POST['change_password'] ?? ''; - $this->pw_username = $_POST['pw_username'] ?? ''; - $this->pw_old_password = $_POST['pw_old_password'] ?? ''; - $this->pw_new_password = $_POST['pw_new_password'] ?? ''; - $this->pw_new_password_confirm = $_POST['pw_new_password_confirm'] ?? ''; + $this->change_password = $_POST['login_change_password'] ?? ''; + $this->pw_username = $_POST['login_pw_username'] ?? ''; + $this->pw_old_password = $_POST['login_pw_old_password'] ?? ''; + $this->pw_new_password = $_POST['login_pw_new_password'] ?? ''; + $this->pw_new_password_confirm = $_POST['login_pw_new_password_confirm'] ?? ''; // disallow user list for password change $this->pw_change_deny_users = ['admin']; // max login counts before error reporting @@ -2063,10 +2567,12 @@ HTML; // if username & password & !$euid start login $this->loginLoginUser(); - // checks if $euid given check if user is okay for that side + // checks if $euid given check if user is okay for that site $this->loginCheckPermissions(); - // logsout user + // logout user $this->loginLogoutUser(); + // set headers for enhanced security + $this->loginEnhanceHttpSecurity(); // ** LANGUAGE SET AFTER LOGIN ** $this->loginSetLocale(); // load translator @@ -2153,6 +2659,119 @@ HTML; return $this->login_is_ajax_page; } + /** + * Returns current set loginUserId or empty if unset + * + * @return string loginUserId or empty string for not set + */ + public function loginGetLoginUserId(): string + { + return $this->login_user_id; + } + + /** + * Returns GET/POST for where the loginUserId was set + * + * @return string GET or POST or empty string for not set + */ + public function loginGetLoginUserIdSource(): string + { + return $this->login_user_id_source; + } + + /** + * Returns unclear login user id state. If true then illegal characters + * where present in the loginUserId parameter + * + * @return bool False for clear, True if illegal characters found + */ + public function loginGetLoginUserIdUnclean(): bool + { + return $this->login_user_id_unclear; + } + + /** + * Return locale settings with + * locale + * domain + * encoding + * path + * + * empty string if not set + * + * @return array Locale settings + */ + public function loginGetLocale(): array + { + return $this->locale; + } + + /** + * return header color or null for not set + * + * @return string|null Header color in RGB hex with leading sharp + */ + public function loginGetHeaderColor(): ?string + { + return $this->session->get('LOGIN_HEADER_COLOR'); + } + + /** + * Return the current loaded list of pages the user can access + * + * @return array + */ + public function loginGetPages(): array + { + + return $this->session->get('LOGIN_PAGES'); + } + + // MARK: logged in uid(pk)/cuid/ecuuid + + /** + * Get the current set EUID (edit user id) + * + * @return string EUID as string + */ + public function loginGetEuid(): string + { + return (string)$this->edit_user_id; + } + + /** + * Get the current set EUCUID (edit user cuid) + * + * @return string EUCUID as string + */ + public function loginGetEuCuid(): string + { + return (string)$this->edit_user_cuid; + } + + /** + * Get the current set EUCUUID (edit user cuuid) + * + * @return string EUCUUID as string + * @deprecated Wrong name, use ->loginGetEuCuuid + */ + public function loginGetEcuuid(): string + { + return (string)$this->edit_user_cuuid; + } + + /** + * Get the current set EUCUUID (edit user cuuid) + * + * @return string EUCUUID as string + */ + public function loginGetEuCuuid(): string + { + return (string)$this->edit_user_cuuid; + } + + // MARK: get error messages + /** * returns the last set error code * @@ -2199,6 +2818,8 @@ HTML; return $string; } + // MARK: password checks + /** * Sets the minium length and checks on valid. * Current max length is 255 characters @@ -2251,6 +2872,8 @@ HTML; return $value; } + // MARK: max login count + /** * Set the maximum login errors a user can have before getting locked * if the user has the strict lock setting turned on @@ -2277,6 +2900,8 @@ HTML; return $this->max_login_error_count; } + // MARK: LGOUT USER + /** * if a user pressed on logout, destroyes session and unsets all global vars * @@ -2291,13 +2916,15 @@ HTML; // unset session vars set/used in this login $this->session->sessionDestroy(); // unset euid - $this->euid = null; - $this->ecuid = null; - $this->ecuuid = null; + $this->edit_user_id = null; + $this->edit_user_cuid = null; + $this->edit_user_cuuid = null; // then prints the login screen again $this->permission_okay = false; } + // MARK: logged in user permssion check + /** * for every page the user access this script checks if he is allowed to do so * @@ -2308,7 +2935,7 @@ HTML; // start with not allowed $this->permission_okay = false; // bail for no euid (no login) - if (empty($this->euid)) { + if (empty($this->edit_user_cuuid)) { return $this->permission_okay; } // euid must match ecuid and ecuuid @@ -2316,39 +2943,53 @@ HTML; if ($this->login_error == 103) { return $this->permission_okay; } - $q = "SELECT ep.filename, eu.cuid, eu.cuuid, " - // base lock flags - . "eu.deleted, eu.enabled, eu.locked, " - // date based lock - . "CASE WHEN (" - . "(eu.lock_until IS NULL " - . "OR (eu.lock_until IS NOT NULL AND NOW() >= eu.lock_until)) " - . "AND (eu.lock_after IS NULL " - . "OR (eu.lock_after IS NOT NULL AND NOW() <= eu.lock_after))" - . ") THEN 0::INT ELSE 1::INT END locked_period, " - // login id validation - . "login_user_id, " - . "CASE WHEN (" - . "(eu.login_user_id_valid_from IS NULL " - . "OR (eu.login_user_id_valid_from IS NOT NULL AND NOW() >= eu.login_user_id_valid_from)) " - . "AND (eu.login_user_id_valid_until IS NULL " - . "OR (eu.login_user_id_valid_until IS NOT NULL AND NOW() <= eu.login_user_id_valid_until))" - . ") THEN 1::INT ELSE 0::INT END AS login_user_id_valid_date, " - // check if user must login - . "CASE WHEN eu.login_user_id_revalidate_after IS NOT NULL " - . "AND eu.login_user_id_revalidate_after > '0 days'::INTERVAL " - . "AND eu.login_user_id_last_revalidate + eu.login_user_id_revalidate_after <= NOW()::DATE " - . "THEN 1::INT ELSE 0::INT END AS login_user_id_revalidate, " - . "eu.login_user_id_locked " - // - . "FROM edit_page ep, edit_page_access epa, edit_group eg, edit_user eu " - . "WHERE ep.edit_page_id = epa.edit_page_id " - . "AND eg.edit_group_id = epa.edit_group_id " - . "AND eg.edit_group_id = eu.edit_group_id " - . "AND eu.edit_user_id = " . $this->euid . " " - . "AND ep.filename = '" . $this->page_name . "' " - . "AND eg.enabled = 1 AND epa.enabled = 1"; - $res = $this->db->dbReturnRow($q); + $q = <<= eu.lock_until) + ) + AND ( + eu.lock_after IS NULL + OR (eu.lock_after IS NOT NULL AND NOW() <= eu.lock_after) + ) + ) THEN 0::INT ELSE 1::INT END locked_period, + -- login id validation + login_user_id, + CASE WHEN ( + ( + eu.login_user_id_valid_from IS NULL + OR (eu.login_user_id_valid_from IS NOT NULL AND NOW() >= eu.login_user_id_valid_from) + ) + AND ( + eu.login_user_id_valid_until IS NULL + OR (eu.login_user_id_valid_until IS NOT NULL AND NOW() <= eu.login_user_id_valid_until) + ) + ) THEN 1::INT ELSE 0::INT END AS login_user_id_valid_date, + -- check if user must login + CASE WHEN + eu.login_user_id_revalidate_after IS NOT NULL + AND eu.login_user_id_revalidate_after > '0 days'::INTERVAL + AND eu.login_user_id_last_revalidate + eu.login_user_id_revalidate_after <= NOW()::DATE + THEN 1::INT ELSE 0::INT END AS login_user_id_revalidate, + eu.login_user_id_locked + -- + FROM + edit_page ep, edit_page_access epa, edit_group eg, edit_user eu + WHERE + ep.edit_page_id = epa.edit_page_id + AND eg.edit_group_id = epa.edit_group_id + AND eg.edit_group_id = eu.edit_group_id + AND eg.enabled = 1 AND epa.enabled = 1 + AND eu.cuuid = $1 + AND ep.filename = $2 + SQL; + $res = $this->db->dbReturnRowParams($q, [$this->edit_user_cuuid, $this->page_name]); if (!is_array($res)) { $this->login_error = 109; return $this->permission_okay; @@ -2359,7 +3000,8 @@ HTML; (int)$res['enabled'], (int)$res['locked'], (int)$res['locked_period'], - (int)$res['login_user_id_locked'] + (int)$res['login_user_id_locked'], + (int)$res['force_logout'] ) ) { // errors set in method @@ -2382,13 +3024,8 @@ HTML; } else { $this->login_error = 103; } - // set ECUID - $this->ecuid = (string)$res['cuid']; - $this->ecuuid = (string)$res['cuuid']; - $this->session->setMany([ - 'ECUID' => $this->ecuid, - 'ECUUID' => $this->ecuuid, - ]); + // set all the internal vars + $this->loginSetEditUserUidData($res); // if called from public, so we can check if the permissions are ok return $this->permission_okay; } @@ -2403,6 +3040,8 @@ HTML; return $this->permission_okay; } + // MARK: ACL acess check + /** * Check if source (page, base) is matching to the given min access string * min access string must be valid access level string (eg read, mod, write) @@ -2503,24 +3142,68 @@ HTML; return (int)$this->default_acl_list_type[$type]; } + // MARK: edit access helpers + /** * checks if this edit access id is valid * * @param int|null $edit_access_id access id pk to check * @return bool true/false: if the edit access is not * in the valid list: false + * @deprecated Please switch to using edit access cuid check with ->loginCheckEditAccessCuid() */ public function loginCheckEditAccess(?int $edit_access_id): bool { if ($edit_access_id === null) { return false; } - if (array_key_exists($edit_access_id, $this->acl['unit'])) { + if (array_key_exists($edit_access_id, $this->acl['unit_legacy'])) { return true; } return false; } + /** + * check if this edit access cuid is valid + * + * @param string|null $cuid + * @return bool + */ + public function loginCheckEditAccessCuid(?string $cuid): bool + { + if ($cuid === null) { + return false; + } + if (array_key_exists($cuid, $this->acl['unit'])) { + return true; + } + return false; + } + + /** + * checks that the given edit access id is valid for this user + * return null if nothing set, or the edit access id + * + * @param string|null $cuid edit access cuid to check + * @return string|null same edit access cuid if ok + * or the default edit access id + * if given one is not valid + */ + public function loginCheckEditAccessValidCuid(?string $cuid): ?string + { + if ( + $cuid !== null && + is_array($this->session->get('LOGIN_UNIT')) && + !array_key_exists($cuid, $this->session->get('LOGIN_UNIT')) + ) { + $cuid = null; + if (!empty($this->session->get('LOGIN_UNIT_DEFAULT_EACUID'))) { + $cuid = $this->session->get('LOGIN_UNIT_DEFAULT_EACUID'); + } + } + return $cuid; + } + /** * checks that the given edit access id is valid for this user * return null if nothing set, or the edit access id @@ -2529,38 +3212,39 @@ HTML; * @return int|null same edit access id if ok * or the default edit access id * if given one is not valid + * @deprecated Please switch to using edit access cuid check with ->loginCheckEditAccessValidCuid() */ public function loginCheckEditAccessId(?int $edit_access_id): ?int { if ( $edit_access_id !== null && - is_array($this->session->get('UNIT')) && - !array_key_exists($edit_access_id, $this->session->get('UNIT')) + is_array($this->session->get('LOGIN_UNIT_LEGACY')) && + !array_key_exists($edit_access_id, $this->session->get('LOGIN_UNIT_LEGACY')) ) { $edit_access_id = null; - if (is_numeric($this->session->get('UNIT_DEFAULT'))) { - $edit_access_id = (int)$this->session->get('UNIT_DEFAULT'); + if (!empty($this->session->get('LOGIN_UNIT_DEFAULT_EAID'))) { + $edit_access_id = (int)$this->session->get('LOGIN_UNIT_DEFAULT_EAID'); } } return $edit_access_id; } /** - * return a set entry from the UNIT session for an edit access_id + * return a set entry from the UNIT session for an edit access cuid * if not found return false * - * @param int $edit_access_id edit access id - * @param string|int $data_key key value to search for - * @return bool|string false for not found or string for found data + * @param string $cuid edit access cuid + * @param string|int $data_key key value to search for + * @return false|string false for not found or string for found data */ public function loginGetEditAccessData( - int $edit_access_id, + string $cuid, string|int $data_key - ): bool|string { - if (!isset($_SESSION['UNIT'][$edit_access_id]['data'][$data_key])) { + ): false|string { + if (!isset($_SESSION['LOGIN_UNIT'][$cuid]['data'][$data_key])) { return false; } - return $_SESSION['UNIT'][$edit_access_id]['data'][$data_key]; + return $_SESSION['LOGIN_UNIT'][$cuid]['data'][$data_key]; } /** @@ -2568,14 +3252,29 @@ HTML; * false on not found * * @param string $uid Edit Access UID to look for - * @return int|bool Either primary key in int or false in bool for not found + * @return int|false Either primary key in int or false in bool for not found + * @deprecated use loginGetEditAccessCuidFromUid */ - public function loginGetEditAccessIdFromUid(string $uid): int|bool + public function loginGetEditAccessIdFromUid(string $uid): int|false { - if (!isset($_SESSION['UNIT_UID'][$uid])) { + if (!isset($_SESSION['LOGIN_UNIT_UID'][$uid])) { return false; } - return (int)$_SESSION['UNIT_UID'][$uid]; + return (int)$_SESSION['LOGIN_UNIT_UID'][$uid]; + } + + /** + * Get the edit access UID from the edit access CUID + * + * @param string $uid + * @return int|false + */ + public function loginGetEditAccessCuidFromUid(string $uid): int|false + { + if (!isset($_SESSION['LOGIN_UNIT_CUID'][$uid])) { + return false; + } + return (int)$_SESSION['LOGIN_UNIT_CUID'][$uid]; } /** @@ -2591,6 +3290,8 @@ HTML; return false; } + // MARK: various basic login id checks + /** * Returns true if login button was pressed * @@ -2600,119 +3301,6 @@ HTML; { return empty($this->login) ? false : true; } - - /** - * Returns current set loginUserId or empty if unset - * - * @return string loginUserId or empty string for not set - */ - public function loginGetLoginUserId(): string - { - return $this->login_user_id; - } - - /** - * Returns GET/POST for where the loginUserId was set - * - * @return string GET or POST or empty string for not set - */ - public function loginGetLoginUserIdSource(): string - { - return $this->login_user_id_source; - } - - /** - * Returns unclear login user id state. If true then illegal characters - * where present in the loginUserId parameter - * - * @return bool False for clear, True if illegal characters found - */ - public function loginGetLoginUserIdUnclean(): bool - { - return $this->login_user_id_unclear; - } - - /** - * old name for loginGetEditAccessData - * - * @deprecated Use $login->loginGetEditAccessData() - * @param int $edit_access_id - * @param string|int $data_key - * @return bool|string - */ - public function loginSetEditAccessData( - int $edit_access_id, - string|int $data_key - ): bool|string { - return $this->loginGetEditAccessData($edit_access_id, $data_key); - } - - /** - * Return locale settings with - * locale - * domain - * encoding - * path - * - * empty string if not set - * - * @return array Locale settings - */ - public function loginGetLocale(): array - { - return $this->locale; - } - - /** - * return header color or null for not set - * - * @return string|null Header color in RGB hex with leading sharp - */ - public function loginGetHeaderColor(): ?string - { - return $this->session->get('HEADER_COLOR'); - } - - /** - * Return the current loaded list of pages the user can access - * - * @return array - */ - public function loginGetPages(): array - { - - return $this->session->get('PAGES'); - } - - /** - * Get the current set EUID (edit user id) - * - * @return string EUID as string - */ - public function loginGetEuid(): string - { - return (string)$this->euid; - } - - /** - * Get the current set ECUID (edit user cuid) - * - * @return string ECUID as string - */ - public function loginGetEcuid(): string - { - return (string)$this->ecuid; - } - - /** - * Get the current set ECUUID (edit user cuuid) - * - * @return string ECUUID as string - */ - public function loginGetEcuuid(): string - { - return (string)$this->ecuuid; - } } // __END__ diff --git a/src/ACL/LoginUserStatus.php b/src/ACL/LoginUserStatus.php new file mode 100644 index 0000000..ddc5a84 --- /dev/null +++ b/src/ACL/LoginUserStatus.php @@ -0,0 +1,68 @@ + + */ + public static function getMap() + { + return array_flip((new \ReflectionClass(static::class))->getConstants()); + } + + /** + * Returns the descriptive role names + * + * @return string[] + */ + public static function getNames() + { + + return array_keys((new \ReflectionClass(static::class))->getConstants()); + } + + /** + * Returns the numerical role values + * + * @return int[] + */ + public static function getValues() + { + return array_values((new \ReflectionClass(static::class))->getConstants()); + } +} + +// __END__ diff --git a/src/Admin/EditBase.php b/src/Admin/EditBase.php index f740b53..27d4a16 100644 --- a/src/Admin/EditBase.php +++ b/src/Admin/EditBase.php @@ -415,8 +415,6 @@ class EditBase $elements[] = $this->form->formCreateElement('lock_until'); $elements[] = $this->form->formCreateElement('lock_after'); $elements[] = $this->form->formCreateElement('admin'); - $elements[] = $this->form->formCreateElement('debug'); - $elements[] = $this->form->formCreateElement('db_debug'); $elements[] = $this->form->formCreateElement('edit_language_id'); $elements[] = $this->form->formCreateElement('edit_scheme_id'); $elements[] = $this->form->formCreateElementListTable('edit_access_user'); diff --git a/src/Combined/ArrayHandler.php b/src/Combined/ArrayHandler.php index b100294..38727c1 100644 --- a/src/Combined/ArrayHandler.php +++ b/src/Combined/ArrayHandler.php @@ -525,6 +525,30 @@ class ArrayHandler { return array_diff($array, $remove); } + + /** + * From the array with key -> anything values return only the matching entries from key list + * key list is a list[string] + * if key list is empty, return array as is + * + * @param array $array + * @param array $key_list + * @return array + */ + public static function arrayReturnMatchingKeyOnly( + array $array, + array $key_list + ): array { + // on empty return as is + if (empty($key_list)) { + return $array; + } + return array_filter( + $array, + fn($key) => in_array($key, $key_list), + ARRAY_FILTER_USE_KEY + ); + } } // __END__ diff --git a/src/Create/Session.php b/src/Create/Session.php index 6873ee2..b3f9f77 100644 --- a/src/Create/Session.php +++ b/src/Create/Session.php @@ -21,21 +21,107 @@ class Session private string $session_id = ''; /** @var bool flag auto write close */ private bool $auto_write_close = false; + /** @var string regenerate option, default never */ + private string $regenerate = 'never'; + /** @var int regenerate interval either 1 to 100 for random or 0 to 3600 for interval */ + private int $regenerate_interval = 0; + + /** @var array allowed session id regenerate (rotate) options */ + private const ALLOWED_REGENERATE_OPTIONS = ['none', 'random', 'interval']; + /** @var int default random interval */ + public const DEFAULT_REGENERATE_RANDOM = 100; + /** @var int default rotate internval in minutes */ + public const DEFAULT_REGENERATE_INTERVAL = 5 * 60; + /** @var int maximum time for regenerate interval is one hour */ + public const MAX_REGENERATE_INTERAL = 60 * 60; /** * init a session, if array is empty or array does not have session_name set * then no auto init is run * * @param string $session_name if set and not empty, will start session + * @param array{auto_write_close?:bool,session_strict?:bool,regenerate?:string,regenerate_interval?:int} $options */ - public function __construct(string $session_name, bool $auto_write_close = false) - { + public function __construct( + string $session_name, + array $options = [] + ) { + $this->setOptions($options); $this->initSession($session_name); - $this->auto_write_close = $auto_write_close; } // MARK: private methods + /** + * set session class options + * + * @param array{auto_write_close?:bool,session_strict?:bool,regenerate?:string,regenerate_interval?:int} $options + * @return void + */ + private function setOptions(array $options): void + { + if ( + !isset($options['auto_write_close']) || + !is_bool($options['auto_write_close']) + ) { + $options['auto_write_close'] = false; + } + $this->auto_write_close = $options['auto_write_close']; + if ( + !isset($options['session_strict']) || + !is_bool($options['session_strict']) + ) { + $options['session_strict'] = true; + } + // set strict options, on not started sessiononly + if ( + $options['session_strict'] && + $this->getSessionStatus() === PHP_SESSION_NONE + ) { + // use cookies to store session IDs + ini_set('session.use_cookies', 1); + // use cookies only (do not send session IDs in URLs) + ini_set('session.use_only_cookies', 1); + // do not send session IDs in URLs + ini_set('session.use_trans_sid', 0); + } + // session regenerate id options + if ( + empty($options['regenerate']) || + !in_array($options['regenerate'], self::ALLOWED_REGENERATE_OPTIONS) + ) { + $options['regenerate'] = 'never'; + } + $this->regenerate = (string)$options['regenerate']; + // for regenerate: 'random' (default 100) + // regenerate_interval must be between (1 = always) and 100 (1 in 100) + // for regenerate: 'interval' (default 5min) + // regenerate_interval must be 0 = always, to 3600 (every hour) + if ( + $options['regenerate'] == 'random' && + ( + !isset($options['regenerate_interval']) || + !is_numeric($options['regenerate_interval']) || + $options['regenerate_interval'] < 0 || + $options['regenerate_interval'] > 100 + ) + ) { + $options['regenerate_interval'] = self::DEFAULT_REGENERATE_RANDOM; + } + if ( + $options['regenerate'] == 'interval' && + ( + !isset($options['regenerate_interval']) || + !is_numeric($options['regenerate_interval']) || + $options['regenerate_interval'] < 1 || + $options['regenerate_interval'] > self::MAX_REGENERATE_INTERAL + ) + ) { + $options['regenerate_interval'] = self::DEFAULT_REGENERATE_INTERVAL; + } + $this->regenerate_interval = (int)($options['regenerate_interval'] ?? 0); + } + /** * Start session * startSession should be called for complete check @@ -72,6 +158,72 @@ class Session return false; } + // MARK: regenerate session + + /** + * auto rotate session id + * + * @return void + * @throws \RuntimeException failure to regenerate session id + * @throws \UnexpectedValueException failed to get new session id + * @throws \RuntimeException failed to set new sesson id + * @throws \UnexpectedValueException new session id generated does not match the new set one + */ + private function sessionRegenerateSessionId() + { + // never + if ($this->regenerate == 'never') { + return; + } + // regenerate + if ( + !( + // is not session obsolete + empty($_SESSION['SESSION_REGENERATE_OBSOLETE']) && + ( + ( + // random + $this->regenerate == 'random' && + mt_rand(1, $this->regenerate_interval) == 1 + ) || ( + // interval type + $this->regenerate == 'interval' && + ($_SESSION['SESSION_REGENERATE_TIMESTAMP'] ?? 0) + $this->regenerate_interval < time() + ) + ) + ) + ) { + return; + } + // Set current session to expire in 1 minute + $_SESSION['SESSION_REGENERATE_OBSOLETE'] = true; + $_SESSION['SESSION_REGENERATE_EXPIRES'] = time() + 60; + $_SESSION['SESSION_REGENERATE_TIMESTAMP'] = time(); + // Create new session without destroying the old one + if (session_regenerate_id(false) === false) { + throw new \RuntimeException('[SESSION] Session id regeneration failed', 1); + } + // Grab current session ID and close both sessions to allow other scripts to use them + if (false === ($new_session_id = $this->getSessionIdCall())) { + throw new \UnexpectedValueException('[SESSION] getSessionIdCall did not return a session id', 2); + } + $this->writeClose(); + // Set session ID to the new one, and start it back up again + if (($get_new_session_id = session_id($new_session_id)) === false) { + throw new \RuntimeException('[SESSION] set session_id failed', 3); + } + if ($get_new_session_id != $new_session_id) { + throw new \UnexpectedValueException('[SESSION] new session id does not match the new set one', 4); + } + $this->session_id = $new_session_id; + $this->startSessionCall(); + // Don't want this one to expire + unset($_SESSION['SESSION_REGENERATE_OBSOLETE']); + unset($_SESSION['SESSION_REGENERATE_EXPIRES']); + } + + // MARK: session validation + /** * check if session name is valid * @@ -151,6 +303,13 @@ class Session if (!$this->checkActiveSession()) { throw new \RuntimeException('[SESSION] Failed to activate session', 5); } + if ( + !empty($_SESSION['SESSION_REGENERATE_OBSOLETE']) && + !empty($_SESSION['SESSION_REGENERATE_EXPIRES']) && $_SESSION['SESSION_REGENERATE_EXPIRES'] < time() + ) { + $this->sessionDestroy(); + throw new \RuntimeException('[SESSION] Expired session found', 6); + } } elseif ($session_name != $this->getSessionName()) { throw new \UnexpectedValueException( '[SESSION] Another session exists with a different name: ' . $this->getSessionName(), @@ -159,10 +318,12 @@ class Session } // check session id if (false === ($session_id = $this->getSessionIdCall())) { - throw new \UnexpectedValueException('[SESSION] getSessionId did not return a session id', 6); + throw new \UnexpectedValueException('[SESSION] getSessionIdCall did not return a session id', 7); } // set session id $this->session_id = $session_id; + // run session id re-create from time to time + $this->sessionRegenerateSessionId(); // if flagged auto close, write close session if ($this->auto_write_close) { $this->writeClose(); diff --git a/src/Output/Form/TableArrays/EditUsers.php b/src/Output/Form/TableArrays/EditUsers.php index 6d87753..c51dbd7 100644 --- a/src/Output/Form/TableArrays/EditUsers.php +++ b/src/Output/Form/TableArrays/EditUsers.php @@ -135,30 +135,6 @@ class EditUsers implements Interface\TableArraysInterface 'min_edit_acl' => '100', 'min_show_acl' => '100', ], - 'debug' => [ - 'value' => $_POST['debug'] ?? '', - 'output_name' => 'Debug', - 'type' => 'binary', - 'int' => 1, - 'element_list' => [ - '1' => 'Yes', - '0' => 'No' - ], - 'min_edit_acl' => '100', - 'min_show_acl' => '100', - ], - 'db_debug' => [ - 'value' => $_POST['db_debug'] ?? '', - 'output_name' => 'DB Debug', - 'type' => 'binary', - 'int' => 1, - 'element_list' => [ - '1' => 'Yes', - '0' => 'No' - ], - 'min_edit_acl' => '100', - 'min_show_acl' => '100', - ], 'email' => [ 'value' => $_POST['email'] ?? '', 'output_name' => 'E-Mail', diff --git a/test/phpunit/ACL/CoreLibsACLLoginTest.php b/test/phpunit/ACL/CoreLibsACLLoginTest.php index 92d3d97..1582a1e 100644 --- a/test/phpunit/ACL/CoreLibsACLLoginTest.php +++ b/test/phpunit/ACL/CoreLibsACLLoginTest.php @@ -22,8 +22,12 @@ Not yet covered tests: */ final class CoreLibsACLLoginTest extends TestCase { - private static $db; - private static $log; + private static \CoreLibs\DB\IO $db; + private static \CoreLibs\Logging\Logging $log; + + private static string $edit_access_cuid; + private static string $edit_user_cuid; + private static string $edit_user_cuuid; /** * start DB conneciton, setup DB, etc @@ -108,14 +112,40 @@ final class CoreLibsACLLoginTest extends TestCase self::$db->dbSetMaxQueryCall(-1); // insert additional content for testing (locked user, etc) $queries = [ - "INSERT INTO edit_access_data " - . "(edit_access_id, name, value, enabled) VALUES " - . "((SELECT edit_access_id FROM edit_access WHERE uid = 'AdminAccess'), " - . "'test', 'value', 1)" + <<dbExec($query); } + // read edit access cuid, edit user cuid and edit user cuuid + $row = self::$db->dbReturnRowParams( + "SELECT cuid FROM edit_access WHERE uid = $1", + ["AdminAccess"] + ); + self::$edit_access_cuid = $row['cuid'] ?? ''; + if (empty(self::$edit_access_cuid)) { + self::markTestIncomplete( + 'Cannot read edit access cuid for "AdminAccess".' + ); + } + $row = self::$db->dbReturnRowParams( + "SELECT cuid, cuuid FROM edit_user WHERE username = $1", + ["admin"] + ); + self::$edit_user_cuid = $row['cuid'] ?? ''; + self::$edit_user_cuuid = $row['cuuid'] ?? ''; + if (empty(self::$edit_user_cuid) || empty(self::$edit_user_cuuid)) { + self::markTestIncomplete( + 'Cannot read edit user cuid or cuuid for "admin".' + ); + } // define mandatory constant // must set @@ -235,24 +265,25 @@ final class CoreLibsACLLoginTest extends TestCase 'ajax_post_action' => 'login', ], ], - 'load, session euid set only, php error' => [ + 'load, session eucuuid set only, php error' => [ [ 'page_name' => 'edit_users.php', ], [], [], [ - 'EUID' => 1, - 'ECUID' => 'abc', - 'ECUUID' => '1233456-1234-1234-1234-123456789012', + 'LOGIN_EUID' => 1, + 'LOGIN_EUCUID' => 'abc', + 'LOGIN_EUCUUID' => '1233456-1234-1234-1234-123456789012', ], 2, [], ], - 'load, session euid set, all set' => [ + 'load, session eucuuid set, all set' => [ [ 'page_name' => 'edit_users.php', 'edit_access_id' => 1, + 'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST', 'edit_access_uid' => 'AdminAccess', 'edit_access_data' => 'test', 'base_access' => 'list', @@ -261,22 +292,23 @@ final class CoreLibsACLLoginTest extends TestCase [], [], [ - 'EUID' => 1, - 'ECUID' => 'abc', - 'ECUUID' => '1233456-1234-1234-1234-123456789012', - 'USER_NAME' => '', - 'GROUP_NAME' => '', - 'ADMIN' => 1, - 'GROUP_ACL_LEVEL' => -1, - 'PAGES_ACL_LEVEL' => [], - 'USER_ACL_LEVEL' => -1, - 'USER_ADDITIONAL_ACL' => [], - 'GROUP_ADDITIONAL_ACL' => [], - 'UNIT_UID' => [ - 'AdminAccess' => 1, + 'LOGIN_EUID' => 1, + 'LOGIN_EUCUID' => 'abc', + 'LOGIN_EUCUUID' => 'SET_EUCUUID_IN_TEST', + 'LOGIN_USER_NAME' => '', + 'LOGIN_GROUP_NAME' => '', + 'LOGIN_ADMIN' => 1, + 'LOGIN_GROUP_ACL_LEVEL' => -1, + 'LOGIN_PAGES_ACL_LEVEL' => [], + 'LOGIN_USER_ACL_LEVEL' => -1, + 'LOGIN_USER_ADDITIONAL_ACL' => [], + 'LOGIN_GROUP_ADDITIONAL_ACL' => [], + 'LOGIN_UNIT_UID' => [ + 'AdminAccess' => '123456789012', ], - 'UNIT' => [ - 1 => [ + 'LOGIN_UNIT' => [ + '123456789012' => [ + 'id' => 1, 'acl_level' => 80, 'name' => 'Admin Access', 'uid' => 'AdminAccess', @@ -288,8 +320,8 @@ final class CoreLibsACLLoginTest extends TestCase 'additional_acl' => [] ], ], - // 'UNIT_DEFAULT' => '', - // 'DEFAULT_ACL_LIST' => [], + // 'LOGIN_UNIT_DEFAULT' => '', + // 'LOGIN_DEFAULT_ACL_LIST' => [], ], 0, [ @@ -297,6 +329,7 @@ final class CoreLibsACLLoginTest extends TestCase 'admin_flag' => true, 'check_access' => true, 'check_access_id' => 1, + 'check_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST', 'check_access_data' => 'value', 'base_access' => true, 'page_access' => true, @@ -416,6 +449,7 @@ final class CoreLibsACLLoginTest extends TestCase [ 'page_name' => 'edit_users.php', 'edit_access_id' => 1, + 'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST', 'base_access' => 'list', 'page_access' => 'list', 'test_deleted' => true @@ -441,6 +475,7 @@ final class CoreLibsACLLoginTest extends TestCase [ 'page_name' => 'edit_users.php', 'edit_access_id' => 1, + 'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST', 'base_access' => 'list', 'page_access' => 'list', 'test_enabled' => true @@ -466,6 +501,7 @@ final class CoreLibsACLLoginTest extends TestCase [ 'page_name' => 'edit_users.php', 'edit_access_id' => 1, + 'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST', 'base_access' => 'list', 'page_access' => 'list', 'test_locked' => true @@ -491,6 +527,7 @@ final class CoreLibsACLLoginTest extends TestCase [ 'page_name' => 'edit_users.php', 'edit_access_id' => 1, + 'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST', 'base_access' => 'list', 'page_access' => 'list', 'test_get_locked' => true, @@ -515,6 +552,7 @@ final class CoreLibsACLLoginTest extends TestCase [ 'page_name' => 'edit_users.php', 'edit_access_id' => 1, + 'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST', 'base_access' => 'list', 'page_access' => 'list', 'test_locked_period_until' => 'on' @@ -540,6 +578,7 @@ final class CoreLibsACLLoginTest extends TestCase [ 'page_name' => 'edit_users.php', 'edit_access_id' => 1, + 'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST', 'edit_access_uid' => 'AdminAccess', 'edit_access_data' => 'test', 'base_access' => 'list', @@ -559,6 +598,7 @@ final class CoreLibsACLLoginTest extends TestCase 'admin_flag' => true, 'check_access' => true, 'check_access_id' => 1, + 'check_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST', 'check_access_data' => 'value', 'base_access' => true, 'page_access' => true, @@ -569,6 +609,7 @@ final class CoreLibsACLLoginTest extends TestCase [ 'page_name' => 'edit_users.php', 'edit_access_id' => 1, + 'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST', 'base_access' => 'list', 'page_access' => 'list', 'test_locked_period_after' => 'on' @@ -594,6 +635,7 @@ final class CoreLibsACLLoginTest extends TestCase [ 'page_name' => 'edit_users.php', 'edit_access_id' => 1, + 'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST', 'base_access' => 'list', 'page_access' => 'list', 'test_locked_period_until' => 'on', @@ -620,6 +662,7 @@ final class CoreLibsACLLoginTest extends TestCase [ 'page_name' => 'edit_users.php', 'edit_access_id' => 1, + 'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST', 'base_access' => 'list', 'page_access' => 'list', 'test_login_user_id_locked' => true @@ -645,6 +688,7 @@ final class CoreLibsACLLoginTest extends TestCase [ 'page_name' => 'edit_users.php', 'edit_access_id' => 1, + 'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST', 'edit_access_uid' => 'AdminAccess', 'edit_access_data' => 'test', 'base_access' => 'list', @@ -663,6 +707,7 @@ final class CoreLibsACLLoginTest extends TestCase 'admin_flag' => true, 'check_access' => true, 'check_access_id' => 1, + 'check_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST', 'check_access_data' => 'value', 'base_access' => true, 'page_access' => true, @@ -673,6 +718,7 @@ final class CoreLibsACLLoginTest extends TestCase [ 'page_name' => 'edit_users.php', 'edit_access_id' => 1, + 'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST', 'edit_access_uid' => 'AdminAccess', 'edit_access_data' => 'test', 'base_access' => 'list', @@ -692,6 +738,7 @@ final class CoreLibsACLLoginTest extends TestCase 'admin_flag' => true, 'check_access' => true, 'check_access_id' => 1, + 'check_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST', 'check_access_data' => 'value', 'base_access' => true, 'page_access' => true, @@ -702,6 +749,7 @@ final class CoreLibsACLLoginTest extends TestCase [ 'page_name' => 'edit_users.php', 'edit_access_id' => 1, + 'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST', 'edit_access_uid' => 'AdminAccess', 'edit_access_data' => 'test', 'base_access' => 'list', @@ -721,6 +769,7 @@ final class CoreLibsACLLoginTest extends TestCase 'admin_flag' => true, 'check_access' => true, 'check_access_id' => 1, + 'check_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST', 'check_access_data' => 'value', 'base_access' => true, 'page_access' => true, @@ -731,6 +780,7 @@ final class CoreLibsACLLoginTest extends TestCase [ 'page_name' => 'edit_users.php', 'edit_access_id' => 1, + 'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST', 'edit_access_uid' => 'AdminAccess', 'edit_access_data' => 'test', 'base_access' => 'list', @@ -750,6 +800,7 @@ final class CoreLibsACLLoginTest extends TestCase 'admin_flag' => true, 'check_access' => true, 'check_access_id' => 1, + 'check_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST', 'check_access_data' => 'value', 'base_access' => true, 'page_access' => true, @@ -781,6 +832,7 @@ final class CoreLibsACLLoginTest extends TestCase [ 'page_name' => 'edit_users.php', 'edit_access_id' => 1, + 'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST', 'edit_access_uid' => 'AdminAccess', 'edit_access_data' => 'test', 'base_access' => 'list', @@ -804,6 +856,7 @@ final class CoreLibsACLLoginTest extends TestCase 'admin_flag' => true, 'check_access' => true, 'check_access_id' => 1, + 'check_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST', 'check_access_data' => 'value', 'base_access' => true, 'page_access' => true, @@ -814,6 +867,7 @@ final class CoreLibsACLLoginTest extends TestCase [ 'page_name' => 'edit_users.php', 'edit_access_id' => 1, + 'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST', 'edit_access_uid' => 'AdminAccess', 'edit_access_data' => 'test', 'base_access' => 'list', @@ -837,6 +891,7 @@ final class CoreLibsACLLoginTest extends TestCase 'admin_flag' => true, 'check_access' => true, 'check_access_id' => 1, + 'check_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST', 'check_access_data' => 'value', 'base_access' => true, 'page_access' => true, @@ -847,6 +902,7 @@ final class CoreLibsACLLoginTest extends TestCase [ 'page_name' => 'edit_users.php', 'edit_access_id' => 1, + 'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST', 'base_access' => 'list', 'page_access' => 'list', 'test_login_user_id_revalidate_after' => 'on', @@ -873,6 +929,7 @@ final class CoreLibsACLLoginTest extends TestCase [ 'page_name' => 'edit_users.php', 'edit_access_id' => 1, + 'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST', 'edit_access_uid' => 'AdminAccess', 'edit_access_data' => 'test', 'base_access' => 'list', @@ -893,6 +950,7 @@ final class CoreLibsACLLoginTest extends TestCase 'admin_flag' => true, 'check_access' => true, 'check_access_id' => 1, + 'check_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST', 'check_access_data' => 'value', 'base_access' => true, 'page_access' => true, @@ -903,6 +961,7 @@ final class CoreLibsACLLoginTest extends TestCase [ 'page_name' => 'edit_users.php', 'edit_access_id' => 1, + 'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST', 'base_access' => 'list', 'page_access' => 'list', 'test_login_user_id_valid_from' => 'on', @@ -929,6 +988,7 @@ final class CoreLibsACLLoginTest extends TestCase [ 'page_name' => 'edit_users.php', 'edit_access_id' => 1, + 'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST', 'edit_access_uid' => 'AdminAccess', 'edit_access_data' => 'test', 'base_access' => 'list', @@ -949,6 +1009,7 @@ final class CoreLibsACLLoginTest extends TestCase 'admin_flag' => true, 'check_access' => true, 'check_access_id' => 1, + 'check_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST', 'check_access_data' => 'value', 'base_access' => true, 'page_access' => true, @@ -959,6 +1020,7 @@ final class CoreLibsACLLoginTest extends TestCase [ 'page_name' => 'edit_users.php', 'edit_access_id' => 1, + 'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST', 'base_access' => 'list', 'page_access' => 'list', 'test_login_user_id_valid_until' => 'on', @@ -985,6 +1047,7 @@ final class CoreLibsACLLoginTest extends TestCase [ 'page_name' => 'edit_users.php', 'edit_access_id' => 1, + 'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST', 'base_access' => 'list', 'page_access' => 'list', 'test_login_user_id_valid_from' => 'on', @@ -1012,6 +1075,7 @@ final class CoreLibsACLLoginTest extends TestCase [ 'page_name' => 'edit_users.php', 'edit_access_id' => 1, + 'edit_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST', 'edit_access_uid' => 'AdminAccess', 'edit_access_data' => 'test', 'base_access' => 'list', @@ -1042,6 +1106,7 @@ final class CoreLibsACLLoginTest extends TestCase 'admin_flag' => true, 'check_access' => true, 'check_access_id' => 1, + 'check_access_cuid' => 'SET_EDIT_ACCESS_CUID_IN_TEST', 'check_access_data' => 'value', 'base_access' => true, 'page_access' => true, @@ -1111,11 +1176,15 @@ final class CoreLibsACLLoginTest extends TestCase $_POST[$post_var] = $post_value; } + // set ingoing session cuuid if requested + if (isset($session['LOGIN_EUCUUID']) && $session['LOGIN_EUCUUID'] == 'SET_EUCUUID_IN_TEST') { + $session['LOGIN_EUCUUID'] = self::$edit_user_cuuid; + } + // set _SESSION data foreach ($session as $session_var => $session_value) { $_SESSION[$session_var] = $session_value; } - /** @var \CoreLibs\ACL\Login&MockObject */ $login_mock = $this->getMockBuilder(\CoreLibs\ACL\Login::class) ->setConstructorArgs([ @@ -1134,7 +1203,7 @@ final class CoreLibsACLLoginTest extends TestCase . 'locale' . DIRECTORY_SEPARATOR, ] ]) - ->onlyMethods(['loginTerminate', 'loginReadPageName', 'loginPrintLogin']) + ->onlyMethods(['loginTerminate', 'loginReadPageName', 'loginPrintLogin', 'loginEnhanceHttpSecurity']) ->getMock(); $login_mock->expects($this->any()) ->method('loginTerminate') @@ -1152,6 +1221,10 @@ final class CoreLibsACLLoginTest extends TestCase ->method('loginPrintLogin') ->willReturnCallback(function () { }); + $login_mock->expects($this->any()) + ->method('loginEnhanceHttpSecurity') + ->willReturnCallback(function () { + }); // if mock_settings: enabled OFF // run DB update and set off @@ -1369,6 +1442,19 @@ final class CoreLibsACLLoginTest extends TestCase // run test try { + // preset, we cannot set that in the provider + if ( + isset($expected['check_access_cuid']) && + $expected['check_access_cuid'] == 'SET_EDIT_ACCESS_CUID_IN_TEST' + ) { + $expected['check_access_cuid'] = self::$edit_access_cuid; + } + if ( + isset($mock_settings['edit_access_cuid']) && + $mock_settings['edit_access_cuid'] == 'SET_EDIT_ACCESS_CUID_IN_TEST' + ) { + $mock_settings['edit_access_cuid'] = self::$edit_access_cuid; + } // if ajax call // check if parameter, or globals (old type) // else normal call @@ -1427,6 +1513,25 @@ final class CoreLibsACLLoginTest extends TestCase $login_mock->loginCheckAccessPage($mock_settings['page_access']), 'Assert page access' ); + // - loginCheckEditAccessCuid + $this->assertEquals( + $expected['check_access'], + $login_mock->loginCheckEditAccessCuid($mock_settings['edit_access_cuid']), + 'Assert check access' + ); + // - loginCheckEditAccessValidCuid + $this->assertEquals( + $expected['check_access_cuid'], + $login_mock->loginCheckEditAccessValidCuid($mock_settings['edit_access_cuid']), + 'Assert check access cuid valid' + ); + // - loginGetEditAccessCuidFromUid + $this->assertEquals( + $expected['check_access_cuid'], + $login_mock->loginGetEditAccessCuidFromUid($mock_settings['edit_access_uid']), + 'Assert check access uid to cuid valid' + ); + // Deprecated // - loginCheckEditAccess $this->assertEquals( $expected['check_access'], @@ -1449,7 +1554,7 @@ final class CoreLibsACLLoginTest extends TestCase $this->assertEquals( $expected['check_access_data'], $login_mock->loginGetEditAccessData( - $mock_settings['edit_access_id'], + $mock_settings['edit_access_uid'], $mock_settings['edit_access_data'] ), 'Assert check access id data value valid' @@ -1480,11 +1585,12 @@ final class CoreLibsACLLoginTest extends TestCase // - loginCheckPermissions // - loginGetPermissionOkay } catch (\Exception $e) { - // print "[E]: " . $e->getCode() . ", ERROR: " . $login_mock->loginGetLastErrorCode() . "/" - // . ($expected['login_error'] ?? 0) . "\n"; - // print "AJAX: " . $login_mock->loginGetAjaxFlag() . "\n"; - // print "AJAX GLOBAL: " . ($GLOBALS['AJAX_PAGE'] ?? '{f}') . "\n"; - // print "Login error expext: " . ($expected['login_error'] ?? '{0}') . "\n"; + /* print "[E]: " . $e->getCode() . ", ERROR: " . $login_mock->loginGetLastErrorCode() . "/" + . ($expected['login_error'] ?? 0) . "\n"; + print "AJAX: " . $login_mock->loginGetAjaxFlag() . "\n"; + print "AJAX GLOBAL: " . ($GLOBALS['AJAX_PAGE'] ?? '{f}') . "\n"; + print "Login error expext: " . ($expected['login_error'] ?? '{0}') . "\n"; + print "POST exit: " . ($_POST['login_exit'] ?? '{0}') . "\n"; */ // if this is 100, then we do further error checks if ( $e->getCode() == 100 || diff --git a/test/phpunit/ACL/database/CoreLibsACLLogin_database_create_data.sql b/test/phpunit/ACL/database/CoreLibsACLLogin_database_create_data.sql index 3d4a54b..7caaf1c 100644 --- a/test/phpunit/ACL/database/CoreLibsACLLogin_database_create_data.sql +++ b/test/phpunit/ACL/database/CoreLibsACLLogin_database_create_data.sql @@ -30,11 +30,11 @@ DECLARE random_length INT = 12; -- that should be long enough BEGIN IF TG_OP = 'INSERT' THEN - NEW.date_created := 'now'; + NEW.date_created := clock_timestamp(); NEW.cuid := random_string(random_length); NEW.cuuid := gen_random_uuid(); ELSIF TG_OP = 'UPDATE' THEN - NEW.date_updated := 'now'; + NEW.date_updated := clock_timestamp(); END IF; RETURN NEW; END; @@ -579,11 +579,10 @@ CREATE TABLE edit_user ( strict SMALLINT DEFAULT 0, locked SMALLINT DEFAULT 0, protected SMALLINT NOT NULL DEFAULT 0, - -- legacy, debug flags - debug SMALLINT NOT NULL DEFAULT 0, - db_debug SMALLINT NOT NULL DEFAULT 0, -- is admin user admin SMALLINT NOT NULL DEFAULT 0, + -- forced logout counter + force_logout INT DEFAULT 0, -- last login log last_login TIMESTAMP WITHOUT TIME ZONE, -- login error @@ -620,8 +619,6 @@ COMMENT ON COLUMN edit_user.deleted IS 'Login is deleted (master switch), overri COMMENT ON COLUMN edit_user.strict IS 'If too many failed logins user will be locked, default off'; COMMENT ON COLUMN edit_user.locked IS 'Locked from too many wrong password logins'; COMMENT ON COLUMN edit_user.protected IS 'User can only be chnaged by admin user'; -COMMENT ON COLUMN edit_user.debug IS 'Turn debug flag on (legacy)'; -COMMENT ON COLUMN edit_user.db_debug IS 'Turn DB debug flag on (legacy)'; COMMENT ON COLUMN edit_user.admin IS 'If set, this user is SUPER admin'; COMMENT ON COLUMN edit_user.last_login IS 'Last succesfull login tiemstamp'; COMMENT ON COLUMN edit_user.login_error_count IS 'Number of failed logins, reset on successful login'; @@ -656,36 +653,52 @@ CREATE TABLE edit_log ( euid INT, -- this is a foreign key, but I don't nedd to reference to it FOREIGN KEY (euid) REFERENCES edit_user (edit_user_id) MATCH FULL ON UPDATE CASCADE ON DELETE SET NULL, ecuid VARCHAR, - ecuuid UUID, - username VARCHAR, - password VARCHAR, + ecuuid UUID, -- this is the one we want to use, full UUIDv4 from the edit user table + -- date_created equal, but can be overridden event_date TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP, - ip VARCHAR, + -- session ID if set + session_id VARCHAR, + -- username + username VARCHAR, + -- DEPRECATED [password] + password VARCHAR, + ip_address JSONB, -- REMOTE_IP and all other IPs (X_FORWARD, etc) as JSON block + -- DEPRECATED [ip] + ip VARCHAR, -- just the REMOTE_IP, full set see ip_address + -- string blocks, general error TEXT, event TEXT, + -- bytea or string type storage of any data data_binary BYTEA, data TEXT, + -- set page name only page VARCHAR, - action VARCHAR, - action_id VARCHAR, - action_sub_id VARCHAR, - action_yes VARCHAR, - action_flag VARCHAR, - action_menu VARCHAR, - action_loaded VARCHAR, - action_value VARCHAR, - action_type VARCHAR, - action_error VARCHAR, + -- various info data sets user_agent VARCHAR, referer VARCHAR, script_name VARCHAR, query_string VARCHAR, + request_scheme VARCHAR, -- http or https server_name VARCHAR, http_host VARCHAR, - http_accept VARCHAR, - http_accept_charset VARCHAR, - http_accept_encoding VARCHAR, - session_id VARCHAR + http_data JSONB, + -- DEPRECATED [http*] + http_accept VARCHAR, -- in http_data + http_accept_charset VARCHAR, -- in http_data + http_accept_encoding VARCHAR, -- in http_data + -- any action var, -> same set in action_data as JSON + action_data JSONB, + -- DEPRECATED [action*] + action VARCHAR, -- in action_data + action_id VARCHAR, -- in action_data + action_sub_id VARCHAR, -- in action_data + action_yes VARCHAR, -- in action_data + action_flag VARCHAR, -- in action_data + action_menu VARCHAR, -- in action_data + action_loaded VARCHAR, -- in action_data + action_value VARCHAR, -- in action_data + action_type VARCHAR, -- in action_data + action_error VARCHAR -- in action_data ) INHERITS (edit_generic) WITHOUT OIDS; -- END: table/edit_log.sql -- START: table/edit_log_overflow.sql @@ -1015,7 +1028,7 @@ INSERT INTO edit_page_access (enabled, edit_group_id, edit_page_id, edit_access_ -- edit user -- inserts admin user so basic users can be created DELETE FROM edit_user; -INSERT INTO edit_user (username, password, enabled, debug, db_debug, email, protected, admin, edit_language_id, edit_group_id, edit_scheme_id, edit_access_right_id) VALUES ('admin', 'admin', 1, 1, 1, '', 1, 1, +INSERT INTO edit_user (username, password, enabled, email, protected, admin, edit_language_id, edit_group_id, edit_scheme_id, edit_access_right_id) VALUES ('admin', 'admin', 1, 'test@tequila.jp', 1, 1, (SELECT edit_language_id FROM edit_language WHERE short_name = 'en_US'), (SELECT edit_group_id FROM edit_group WHERE name = 'Admin'), (SELECT edit_scheme_id FROM edit_scheme WHERE name = 'Admin'), diff --git a/test/phpunit/Combined/CoreLibsCombinedArrayHandlerTest.php b/test/phpunit/Combined/CoreLibsCombinedArrayHandlerTest.php index 8dc5729..d320b84 100644 --- a/test/phpunit/Combined/CoreLibsCombinedArrayHandlerTest.php +++ b/test/phpunit/Combined/CoreLibsCombinedArrayHandlerTest.php @@ -1201,6 +1201,91 @@ final class CoreLibsCombinedArrayHandlerTest extends TestCase 'Find next key in array' ); } + + public function providerReturnMatchingKeyOnley(): array + { + return [ + 'limited entries' => [ + [ + 'a' => 'foo', + 'b' => 'bar', + 'c' => 'foobar' + ], + [ + 'a', 'b' + ], + [ + 'a' => 'foo', + 'b' => 'bar', + ], + ], + 'limited entries, with one wrong key' => [ + [ + 'a' => 'foo', + 'b' => 'bar', + 'c' => 'foobar' + ], + [ + 'a', 'b', 'f' + ], + [ + 'a' => 'foo', + 'b' => 'bar', + ], + ], + 'wrong keys only' => [ + [ + 'a' => 'foo', + 'b' => 'bar', + 'c' => 'foobar' + ], + [ + 'f', 'f' + ], + [ + ], + ], + 'empty keys' => [ + [ + 'a' => 'foo', + 'b' => 'bar', + 'c' => 'foobar' + ], + [], + [ + 'a' => 'foo', + 'b' => 'bar', + 'c' => 'foobar' + ], + ], + ]; + } + + /** + * Undocumented function + * + * @covers ::arrayReturnMatchingKeyOnly + * @dataProvider providerReturnMatchingKeyOnley + * @testdox arrayReturnMatchingKeyOnly get only selected key entries from array [$_dataName] + * + * @param array $input + * @param array $key_list + * @param array $expected + * @return void + */ + public function testArrayReturnMatchingKeyOnly( + array $input, + array $key_list, + array $expected + ): void { + $this->assertEquals( + $expected, + \CoreLibs\Combined\ArrayHandler::arrayReturnMatchingKeyOnly( + $input, + $key_list + ) + ); + } } // __END__ diff --git a/test/phpunit/Create/CoreLibsCreateSessionTest.php b/test/phpunit/Create/CoreLibsCreateSessionTest.php index 2ac833f..411b610 100644 --- a/test/phpunit/Create/CoreLibsCreateSessionTest.php +++ b/test/phpunit/Create/CoreLibsCreateSessionTest.php @@ -54,7 +54,9 @@ final class CoreLibsCreateSessionTest extends TestCase 'getSessionId' => '1234abcd4567' ], 'sessionNameGlobals', - false, + [ + 'auto_write_close' => false, + ], ], 'auto write close' => [ 'sessionNameAutoWriteClose', @@ -66,7 +68,9 @@ final class CoreLibsCreateSessionTest extends TestCase 'getSessionId' => '1234abcd4567' ], 'sessionNameAutoWriteClose', - true, + [ + 'auto_write_close' => true, + ], ], ]; } @@ -81,13 +85,14 @@ final class CoreLibsCreateSessionTest extends TestCase * @param string $input * @param array $mock_data * @param string $expected + * @param array $options * @return void */ public function testStartSession( string $input, array $mock_data, string $expected, - ?bool $auto_write_close, + ?array $options, ): void { /** @var \CoreLibs\Create\Session&MockObject $session_mock */ $session_mock = $this->createPartialMock( @@ -174,9 +179,14 @@ final class CoreLibsCreateSessionTest extends TestCase 4, '/^\[SESSION\] Failed to activate session/' ], + 'expired session' => [ + \RuntimeException::class, + 5, + '/^\[SESSION\] Expired session found/' + ], 'not a valid session id returned' => [ \UnexpectedValueException::class, - 5, + 6, '/^\[SESSION\] getSessionId did not return a session id/' ], */ ]; @@ -206,7 +216,8 @@ final class CoreLibsCreateSessionTest extends TestCase $this->expectException($exception); $this->expectExceptionCode($exception_code); $this->expectExceptionMessageMatches($expected_error); - new \CoreLibs\Create\Session($session_name); + // cannot set ini after header sent, plus we are on command line there are no headers + new \CoreLibs\Create\Session($session_name, ['session_strict' => false]); } /** diff --git a/test/phpunit/Debug/CoreLibsDebugSupportTest.php b/test/phpunit/Debug/CoreLibsDebugSupportTest.php index 74ed67e..7b6ed19 100644 --- a/test/phpunit/Debug/CoreLibsDebugSupportTest.php +++ b/test/phpunit/Debug/CoreLibsDebugSupportTest.php @@ -568,6 +568,9 @@ final class CoreLibsDebugSupportTest extends TestCase 'assert expected 12' ); break; + default: + $this->assertTrue(true, 'Default fallback as true'); + break; } } diff --git a/test/phpunit/Language/CoreLibsLanguageGetLocaleTest.php b/test/phpunit/Language/CoreLibsLanguageGetLocaleTest.php index fa05341..afd71a4 100644 --- a/test/phpunit/Language/CoreLibsLanguageGetLocaleTest.php +++ b/test/phpunit/Language/CoreLibsLanguageGetLocaleTest.php @@ -21,341 +21,6 @@ final class CoreLibsLanguageGetLocaleTest extends TestCase . 'includes' . DIRECTORY_SEPARATOR . 'locale' . DIRECTORY_SEPARATOR; - /** - * set all constant variables that must be set before call - * - * @return void - */ - public static function setUpBeforeClass(): void - { - // default web page encoding setting - /* if (!defined('DEFAULT_ENCODING')) { - define('DEFAULT_ENCODING', 'UTF-8'); - } - if (!defined('DEFAULT_LOCALE')) { - // default lang + encoding - define('DEFAULT_LOCALE', 'en_US.UTF-8'); - } - // site - if (!defined('SITE_ENCODING')) { - define('SITE_ENCODING', DEFAULT_ENCODING); - } - if (!defined('SITE_LOCALE')) { - define('SITE_LOCALE', DEFAULT_LOCALE); - } */ - // just set - /* if (!defined('BASE')) { - define('BASE', str_replace('/configs', '', __DIR__) . DIRECTORY_SEPARATOR); - } - if (!defined('INCLUDES')) { - define('INCLUDES', 'includes' . DIRECTORY_SEPARATOR); - } - if (!defined('LANG')) { - define('LANG', 'lang' . DIRECTORY_SEPARATOR); - } - if (!defined('LOCALE')) { - define('LOCALE', 'locale' . DIRECTORY_SEPARATOR); - } - if (!defined('CONTENT_PATH')) { - define('CONTENT_PATH', 'frontend' . DIRECTORY_SEPARATOR); - } */ - // array session - $_SESSION = []; - global $_SESSION; - } - - /** - * all the test data - * - * @return array - */ - /* public function setLocaleProvider(): array - { - return [ - // 0: locale - // 1: domain - // 2: encoding - // 3: path - // 4: SESSION: DEFAULT_LOCALE - // 5: SESSION: DEFAULT_CHARSET - // 6: expected array - // 7: deprecation message - 'no params, all default constants' => [ - // lang, domain, encoding, path - null, null, null, null, - // SESSION DEFAULT_LOCALE, SESSION: DEFAULT_CHARSET - null, null, - // return array - [ - 'locale' => 'en_US.UTF-8', - 'lang' => 'en_US', - 'domain' => 'frontend', - 'encoding' => 'UTF-8', - 'path' => "/^\/(.*\/)?includes\/locale\/$/", - ], - 'setLocale: Unset $locale or unset SESSION locale is deprecated', - ], - 'no params, session charset and lang' => [ - // lang, domain, encoding, path - null, null, null, null, - // SESSION DEFAULT_LOCALE, SESSION: DEFAULT_CHARSET - 'ja_JP', 'UTF-8', - // return array - [ - 'locale' => 'ja_JP', - 'lang' => 'ja_JP', - 'domain' => 'frontend', - 'encoding' => 'UTF-8', - 'path' => "/^\/(.*\/)?includes\/locale\/$/", - ], - 'setLocale: Unset $domain is deprecated' - ], - 'no params, session charset and lang short' => [ - // lang, domain, encoding, path - null, null, null, null, - // SESSION DEFAULT_LOCALE, SESSION: DEFAULT_CHARSET - 'ja', 'UTF-8', - // return array - [ - 'locale' => 'ja', - 'lang' => 'ja', - 'domain' => 'frontend', - 'encoding' => 'UTF-8', - 'path' => "/^\/(.*\/)?includes\/locale\/$/", - ], - 'setLocale: Unset $domain is deprecated', - ], - // param lang (no sessions) - 'locale param only, no sessions' => [ - // lang, domain, encoding, path - 'ja.UTF-8', null, null, null, - // SESSION DEFAULT_LOCALE, SESSION: DEFAULT_CHARSET - null, null, - // return array - [ - 'locale' => 'ja.UTF-8', - 'lang' => 'ja', - 'domain' => 'frontend', - 'encoding' => 'UTF-8', - 'path' => "/^\/(.*\/)?includes\/locale\/$/", - ], - 'setLocale: Unset $domain is deprecated', - ], - // different locale setting - 'locale complex param only, no sessions' => [ - // lang, domain, encoding, path - 'ja_JP.SJIS', null, null, null, - // SESSION DEFAULT_LOCALE, SESSION: DEFAULT_CHARSET - null, null, - // return array - [ - 'locale' => 'ja_JP.SJIS', - 'lang' => 'ja_JP', - 'domain' => 'frontend', - 'encoding' => 'SJIS', - 'path' => "/^\/(.*\/)?includes\/locale\/$/", - ], - 'setLocale: Unset $domain is deprecated', - ], - // param lang and domain (no override) - 'locale, domain params, no sessions' => [ - // lang, domain, encoding, path - 'ja.UTF-8', 'admin', null, null, - // SESSION DEFAULT_LOCALE, SESSION: DEFAULT_CHARSET - null, null, - // return array - [ - 'locale' => 'ja.UTF-8', - 'lang' => 'ja', - 'domain' => 'admin', - 'encoding' => 'UTF-8', - 'path' => "/^\/(.*\/)?includes\/locale\/$/", - ], - 'setLocale: Unset $path is deprecated', - ], - // param lang and domain (no override) - 'locale, domain, encoding params, no sessions' => [ - // lang, domain, encoding, path - 'ja.UTF-8', 'admin', 'UTF-8', null, - // SESSION DEFAULT_LOCALE, SESSION: DEFAULT_CHARSET - null, null, - // return array - [ - 'locale' => 'ja.UTF-8', - 'lang' => 'ja', - 'domain' => 'admin', - 'encoding' => 'UTF-8', - 'path' => "/^\/(.*\/)?includes\/locale\/$/", - ], - 'setLocale: Unset $path is deprecated' - ], - // lang, domain, path (no override) - 'locale, domain and path, no sessions' => [ - // lang, domain, encoding, path - 'ja.UTF-8', 'admin', '', __DIR__ . '/locale_other/', - // SESSION DEFAULT_LOCALE, SESSION: DEFAULT_CHARSET - null, null, - // return array - [ - 'locale' => 'ja.UTF-8', - 'lang' => 'ja', - 'domain' => 'admin', - 'encoding' => 'UTF-8', - 'path' => "/^\/(.*\/)?locale_other\/$/", - ], - null - ], - // all params set (no override) - 'all parameter, no sessions' => [ - // lang, domain, encoding, path - 'ja', 'admin', 'UTF-8', __DIR__ . '/locale_other/', - // SESSION DEFAULT_LOCALE, SESSION: DEFAULT_CHARSET - null, null, - // return array - [ - 'locale' => 'ja', - 'lang' => 'ja', - 'domain' => 'admin', - 'encoding' => 'UTF-8', - 'path' => "/^\/(.*\/)?locale_other\/$/", - ], - null - ], - // param lang and domain (no override) - 'long locale, domain, encoding params, no sessions' => [ - // lang, domain, encoding, path - 'de_CH.UTF-8@euro', 'admin', 'UTF-8', null, - // SESSION DEFAULT_LOCALE, SESSION: DEFAULT_CHARSET - null, null, - // return array - [ - 'locale' => 'de_CH.UTF-8@euro', - 'lang' => 'de_CH', - 'domain' => 'admin', - 'encoding' => 'UTF-8', - 'path' => "/^\/(.*\/)?includes\/locale\/$/", - ], - 'setLocale: Unset $path is deprecated', - ], - // TODO invalid params (bad path) (no override) - // TODO param calls, but with override set - ]; - } */ - - /** - * Undocumented function - * - * @covers ::setLocale - * @dataProvider setLocaleProvider - * @testdox lang settings lang $language, domain $domain, encoding $encoding, path $path; session lang: $SESSION_DEFAULT_LOCALE, session char: $SESSION_DEFAULT_CHARSET [$_dataName] - * - * @param string|null $language - * @param string|null $domain - * @param string|null $encoding - * @param string|null $path - * @param string|null $SESSION_DEFAULT_LOCALE - * @param string|null $SESSION_DEFAULT_CHARSET - * @param array $expected - * @param string|null $deprecation_message - * @return void - */ - /* public function testsetLocale( - ?string $language, - ?string $domain, - ?string $encoding, - ?string $path, - ?string $SESSION_DEFAULT_LOCALE, - ?string $SESSION_DEFAULT_CHARSET, - array $expected, - ?string $deprecation_message - ): void { - $return_lang_settings = []; - global $_SESSION; - // set override - if ($SESSION_DEFAULT_LOCALE !== null) { - $_SESSION['DEFAULT_LOCALE'] = $SESSION_DEFAULT_LOCALE; - } - if ($SESSION_DEFAULT_CHARSET !== null) { - $_SESSION['DEFAULT_CHARSET'] = $SESSION_DEFAULT_CHARSET; - } - if ($deprecation_message !== null) { - set_error_handler( - static function (int $errno, string $errstr): never { - throw new \Exception($errstr, $errno); - }, - E_USER_DEPRECATED - ); - // catch this with the message - $this->expectExceptionMessage($deprecation_message); - } - // function call - if ( - $language === null && $domain === null && - $encoding === null && $path === null - ) { - $return_lang_settings = \CoreLibs\Language\GetLocale::setLocale(); - } elseif ( - $language !== null && $domain === null && - $encoding === null && $path === null - ) { - $return_lang_settings = \CoreLibs\Language\GetLocale::setLocale( - $language - ); - } elseif ( - $language !== null && $domain !== null && - $encoding === null && $path === null - ) { - $return_lang_settings = \CoreLibs\Language\GetLocale::setLocale( - $language, - $domain - ); - } elseif ( - $language !== null && $domain !== null && - $encoding !== null && $path === null - ) { - $return_lang_settings = \CoreLibs\Language\GetLocale::setLocale( - $language, - $domain, - $encoding - ); - } else { - $return_lang_settings = \CoreLibs\Language\GetLocale::setLocale( - $language, - $domain, - $encoding, - $path - ); - } - restore_error_handler(); - // print "RETURN: " . print_r($return_lang_settings, true) . "\n"; - - foreach ( - [ - 'locale', 'lang', 'domain', 'encoding', 'path' - ] as $key - ) { - $value = $expected[$key]; - if (strpos($value, "/") === 0) { - // this is regex - $this->assertMatchesRegularExpression( - $value, - $return_lang_settings[$key], - 'assert regex failed for ' . $key - ); - } else { - // assert equal - $this->assertEquals( - $value, - $return_lang_settings[$key], - 'assert equal failed for ' . $key - ); - } - } - // unset all vars - $_SESSION = []; - unset($GLOBALS['OVERRIDE_LANG']); - } */ - /** * all the test data * diff --git a/test/phpunit/Logging/CoreLibsLoggingErrorMessagesTest.php b/test/phpunit/Logging/CoreLibsLoggingErrorMessagesTest.php index ad2d606..5e9c3ac 100644 --- a/test/phpunit/Logging/CoreLibsLoggingErrorMessagesTest.php +++ b/test/phpunit/Logging/CoreLibsLoggingErrorMessagesTest.php @@ -10,7 +10,7 @@ use CoreLibs\Logging\Logger\Level; /** * Test class for Logging * @coversDefaultClass \CoreLibs\Logging\ErrorMessages - * @testdox \CoreLibs\Logging\ErrorMEssages method tests + * @testdox \CoreLibs\Logging\ErrorMessages method tests */ final class CoreLibsLoggingErrorMessagesTest extends TestCase {