diff --git a/4dev/tests/CoreLibsACLLoginTest.php b/4dev/tests/CoreLibsACLLoginTest.php index 418fdbc6..ad82c043 100644 --- a/4dev/tests/CoreLibsACLLoginTest.php +++ b/4dev/tests/CoreLibsACLLoginTest.php @@ -101,6 +101,17 @@ final class CoreLibsACLLoginTest extends TestCase 'Cannot find edit_user table in ACL\Login database for testing' ); } + // 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)" + ]; + foreach ($queries as $query) { + self::$db->dbExec($query); + } + // define mandatory constant // must set // TARGET @@ -146,15 +157,15 @@ final class CoreLibsACLLoginTest extends TestCase */ public function loginProvider(): array { - // 0: mock settings + // 0: mock settings/override flag settings // 1: post array IN // login_login, login_username, login_password, login_logout // change_password, pw_username, pw_old_password, pw_new_password, // pw_new_password_confirm // 2: override session set - // 3: expected error code, 0 for all ok, 3 for login page view - // note that 1 (no db), 2 (no session) must be tested too - // 4: expected return on ok (error: 0) + // 3: expected error code, 0 for all ok, 3000 for login page view + // note that 1000 (no db), 2000 (no session) must be tested too + // 4: expected return array, eg login_error code, or other info data to match return [ 'load, no login' => [ // error code, only for exceptions @@ -165,7 +176,9 @@ final class CoreLibsACLLoginTest extends TestCase [], 3000, [ - 'login_error' => 0 + 'login_error' => 0, + 'error_string' => 'Success: No error', + 'error_string_text' => 'Success: No error', ], ], 'load, session euid set only, php error' => [ @@ -183,6 +196,8 @@ final class CoreLibsACLLoginTest extends TestCase [ 'page_name' => 'edit_users.php', 'edit_access_id' => 1, + 'edit_access_uid' => 'AdminAccess', + 'edit_access_data' => 'test', 'base_access' => 'list', 'page_access' => 'list', ], @@ -195,14 +210,19 @@ final class CoreLibsACLLoginTest extends TestCase 'GROUP_ACL_LEVEL' => -1, 'PAGES_ACL_LEVEL' => [], 'USER_ACL_LEVEL' => -1, + 'UNIT_UID' => [ + 'AdminAccess' => 1, + ], 'UNIT' => [ 1 => [ 'acl_level' => 80, 'name' => 'Admin Access', - 'uid' => '', + 'uid' => 'AdminAccess', 'level' => -1, 'default' => 0, - 'data' => [] + 'data' => [ + 'test' => 'value', + ], ], ], // 'UNIT_DEFAULT' => '', @@ -214,6 +234,7 @@ final class CoreLibsACLLoginTest extends TestCase 'admin_flag' => true, 'check_access' => true, 'check_access_id' => 1, + 'check_access_data' => 'value', 'base_access' => true, 'page_access' => true, ], @@ -231,7 +252,11 @@ final class CoreLibsACLLoginTest extends TestCase [], 3000, [ - 'login_error' => 102 + 'login_error' => 102, + 'error_string' => 'Fatal Error: ' + . 'Login Failed - Please enter username and password', + 'error_string_text' => 'Fatal Error: ' + . 'Login Failed - Please enter username and password' ] ], // login: missing username @@ -247,7 +272,11 @@ final class CoreLibsACLLoginTest extends TestCase [], 3000, [ - 'login_error' => 102 + 'login_error' => 102, + 'error_string' => 'Fatal Error: ' + . 'Login Failed - Please enter username and password', + 'error_string_text' => 'Fatal Error: ' + . 'Login Failed - Please enter username and password' ] ], // login: missing password @@ -263,7 +292,11 @@ final class CoreLibsACLLoginTest extends TestCase [], 3000, [ - 'login_error' => 102 + 'login_error' => 102, + 'error_string' => 'Fatal Error: ' + . 'Login Failed - Please enter username and password', + 'error_string_text' => 'Fatal Error: ' + . 'Login Failed - Please enter username and password' ] ], // login: user not found @@ -279,7 +312,11 @@ final class CoreLibsACLLoginTest extends TestCase [], 3000, [ - 'login_error' => 1010 + 'login_error' => 1010, + 'error_string' => 'Fatal Error: ' + . 'Login Failed - Wrong Username or Password', + 'error_string_text' => 'Fatal Error: ' + . 'Login Failed - Wrong Username or Password' ] ], // login: invalid password @@ -299,16 +336,91 @@ final class CoreLibsACLLoginTest extends TestCase 3000, [ // default password is plain text - 'login_error' => 1012 + 'login_error' => 1012, + 'error_string' => 'Fatal Error: ' + . 'Login Failed - Wrong Username or Password', + 'error_string_text' => 'Fatal Error: ' + . 'Login Failed - Wrong Username or Password' ] ], // login: ok (but not enabled) + 'login: ok, but not enabled' => [ + [ + 'page_name' => 'edit_users.php', + 'edit_access_id' => 1, + 'base_access' => 'list', + 'page_access' => 'list', + 'test_enabled' => true + ], + [ + 'login_login' => 'Login', + 'login_username' => 'admin', + 'login_password' => 'admin', + ], + [], + 3000, + [ + 'login_error' => 104, + 'error_string' => 'Fatal Error: ' + . 'Login Failed - User not enabled', + 'error_string_text' => 'Fatal Error: ' + . 'Login Failed - User not enabled' + ] + ], // login: ok (but locked) + 'login: ok, but locked' => [ + [ + 'page_name' => 'edit_users.php', + 'edit_access_id' => 1, + 'base_access' => 'list', + 'page_access' => 'list', + 'test_locked' => true + ], + [ + 'login_login' => 'Login', + 'login_username' => 'admin', + 'login_password' => 'admin', + ], + [], + 3000, + [ + 'login_error' => 105, + 'error_string' => 'Fatal Error: ' + . 'Login Failed - User is locked', + 'error_string_text' => 'Fatal Error: ' + . 'Login Failed - User is locked' + ] + ], + // login: make user get locked strict + 'login: ok, get locked, strict' => [ + [ + 'page_name' => 'edit_users.php', + 'edit_access_id' => 1, + 'base_access' => 'list', + 'page_access' => 'list', + 'test_get_locked' => true, + 'max_login_error_count' => 2, + 'test_locked_strict' => true, + ], + [ + 'login_login' => 'Login', + 'login_username' => 'admin', + 'login_password' => 'admin', + ], + [], + 0, + [ + 'lock_run_login_error' => 1012, + 'login_error' => 105, + ] + ], // login: ok 'login: ok' => [ [ 'page_name' => 'edit_users.php', 'edit_access_id' => 1, + 'edit_access_uid' => 'AdminAccess', + 'edit_access_data' => 'test', 'base_access' => 'list', 'page_access' => 'list', ], @@ -324,6 +436,7 @@ final class CoreLibsACLLoginTest extends TestCase 'admin_flag' => true, 'check_access' => true, 'check_access_id' => 1, + 'check_access_data' => 'value', 'base_access' => true, 'page_access' => true, ] @@ -340,7 +453,7 @@ final class CoreLibsACLLoginTest extends TestCase * @dataProvider loginProvider * @testdox ACL\Login Class tests [$_dataName] * - * @param array $mock_settings + * @param array $mock_settings * @param array $post * @param array $session * @param int $error @@ -354,9 +467,7 @@ final class CoreLibsACLLoginTest extends TestCase int $error, array $expected ): void { - // echo "ACL LOGIN TEST\n"; $_SESSION = []; - // init session (as MOCK) /** @var \CoreLibs\Create\Session&MockObject */ $session_mock = $this->createPartialMock( @@ -404,6 +515,77 @@ final class CoreLibsACLLoginTest extends TestCase ->method('loginPrintLogin') ->willReturnCallback(function () { }); + + // if mock_settings: enabled OFF + // run DB update and set off + if (!empty($mock_settings['test_enabled'])) { + self::$db->dbExec( + "UPDATE edit_user SET enabled = 0 WHERE LOWER(username) = " + . self::$db->dbEscapeLiteral($post['login_username']) + ); + } + // test locked already + if (!empty($mock_settings['test_locked'])) { + self::$db->dbExec( + "UPDATE edit_user SET locked = 1 WHERE LOWER(username) = " + . self::$db->dbEscapeLiteral($post['login_username']) + ); + } + // test get locked + if (!empty($mock_settings['test_get_locked'])) { + // enable strict if needed + if (!empty($mock_settings['test_locked_strict'])) { + self::$db->dbExec( + "UPDATE edit_user SET strict = 1 WHERE LOWER(username) = " + . self::$db->dbEscapeLiteral($post['login_username']) + ); + } + // reset any previous login error counts + self::$db->dbExec( + "UPDATE edit_user " + . "SET login_error_count = 0, login_error_date_last = NULL, " + . "login_error_date_first = NULL " + . "WHERE LOWER(username) = " + . self::$db->dbEscapeLiteral($post['login_username']) + ); + // check the max login error count and try login until one time before + // on each run, check that lock count is matching + // next run (run test) should then fail with user locked IF strict, + // else fail as normal login + $login_mock->loginSetMaxLoginErrorCount($mock_settings['max_login_error_count']); + // temporary wrong password + $_POST['login_password'] = 'wrong'; + for ($run = 1, $max_run = $login_mock->loginGetMaxLoginErrorCount(); $run <= $max_run; $run++) { + try { + $login_mock->loginMainCall(); + } catch (\Exception $e) { + // print 'Expected error code: ' . $e->getCode() + // . ', M:' . $e->getMessage() + // . ', L:' . $e->getLine() + // . ', E: ' . $login_mock->loginGetLastErrorCode() + // . "\n"; + $this->assertEquals( + $expected['lock_run_login_error'], + $login_mock->loginGetLastErrorCode(), + 'Assert login error code, exit on lock run' + ); + } + // check user error count + $res = self::$db->dbReturnRow( + "SELECT login_error_count FROM edit_user " + . "WHERE LOWER(username) = " + . self::$db->dbEscapeLiteral($post['login_username']) + ); + $this->assertEquals( + $res['login_error_count'], + $run, + 'Assert equal login error count' + ); + } + // set correct password next locked login + $_POST['login_password'] = $post['login_password']; + } + // run test try { $login_mock->loginMainCall(); @@ -421,7 +603,21 @@ final class CoreLibsACLLoginTest extends TestCase 'Assert page name' ); // - loginCheckPermissions [duplicated from loginrun] + $this->assertTrue( + $login_mock->loginCheckPermissions(), + 'Assert true for login permission ok' + ); // - loginCheckAccess [use Base, Page below] + $this->assertEquals( + $expected['base_access'], + $login_mock->loginCheckAccess('base', $mock_settings['base_access']), + 'Assert base access, via main method' + ); + $this->assertEquals( + $expected['base_access'], + $login_mock->loginCheckAccess('page', $mock_settings['base_access']), + 'Assert page access, via main method' + ); // - loginCheckAccessBase $this->assertEquals( $expected['base_access'], @@ -446,14 +642,39 @@ final class CoreLibsACLLoginTest extends TestCase $login_mock->loginCheckEditAccessId((int)$mock_settings['edit_access_id']), 'Assert check access id valid' ); - // - loginGetEditAccessData [test extra] + // - loginGetEditAccessIdFromUid + $this->assertEquals( + $expected['check_access_id'], + $login_mock->loginGetEditAccessIdFromUid($mock_settings['edit_access_uid']), + 'Assert check access uid to id valid' + ); + // - loginGetEditAccessData + $this->assertEquals( + $expected['check_access_data'], + $login_mock->loginGetEditAccessData( + $mock_settings['edit_access_id'], + $mock_settings['edit_access_data'] + ), + 'Assert check access id data value valid' + ); // - loginIsAdmin $this->assertEquals( $expected['admin_flag'], $login_mock->loginIsAdmin(), 'Assert admin flag set' ); + // - loginGetAcl + $this->assertIsArray( + $login_mock->loginGetAcl(), + 'Assert get acl is array' + ); + // TODO: detail match of ACL array (loginGetAcl) + // .. end with: loginLogoutUser + // _POST['login_logout'] = 'lgogout + // $login_mock->loginMainCall(); + // - loginCheckPermissions + // - loginGetPermissionOkay } catch (\Exception $e) { // print "[E]: " . $e->getCode() . ", ERROR: " . $login_mock->loginGetLastErrorCode() . "/" // . ($expected['login_error'] ?? 0) . "\n"; @@ -464,6 +685,37 @@ final class CoreLibsACLLoginTest extends TestCase $login_mock->loginGetLastErrorCode(), 'Assert login error code, exit' ); + // - loginGetErrorMsg + $this->assertEquals( + $expected['error_string'], + $login_mock->loginGetErrorMsg($login_mock->loginGetLastErrorCode()), + 'Assert error string, html' + ); + $this->assertEquals( + $expected['error_string_text'], + $login_mock->loginGetErrorMsg($login_mock->loginGetLastErrorCode(), true), + 'Assert error string, text' + ); + // - loginGetLoginHTML + $this->assertStringContainsString( + '', + $login_mock->loginGetLoginHTML(), + 'Assert login html string exits' + ); + // check that login script has above error message + if (!empty($login_mock->loginGetLastErrorCode())) { + $this->assertStringContainsString( + $login_mock->loginGetErrorMsg($login_mock->loginGetLastErrorCode()), + $login_mock->loginGetLoginHTML(), + 'Assert login error string exits' + ); + } else { + $this->assertStringNotContainsString( + $login_mock->loginGetErrorMsg($login_mock->loginGetLastErrorCode()), + $login_mock->loginGetLoginHTML(), + 'Assert login error string does not exit' + ); + } } // print "EXCEPTION: " . print_r($e, true) . "\n"; $this->assertEquals( @@ -474,38 +726,418 @@ final class CoreLibsACLLoginTest extends TestCase . ', ' . $e->getLine() ); } - // print "PAGENAME: " . $login_mock->loginGetPageName() . "\n"; - // $echo_string = $this->getActualOutput(); - // $this->setOutputCallback( - // function ($echo) { - // // echo "A"; - // echo "--" . $echo . "--\n"; - // } - // ); - - // $echo_string = $this->getActualOutput(); - // echo "~~~~~~~~~~~~~~~~\n"; - // print "ECHO: " . $echo_string . "\n"; - - // compare result to expected + // enable user again if flag set + if (!empty($mock_settings['test_enabled'])) { + self::$db->dbExec( + "UPDATE edit_user SET enabled = 1 " + . "WHERE LOWER(username) = " + . self::$db->dbEscapeLiteral($post['login_username']) + ); + } + // reset lock flag + if (!empty($mock_settings['test_locked'])) { + self::$db->dbExec( + "UPDATE edit_user SET locked = 0 " + . "WHERE LOWER(username) = " + . self::$db->dbEscapeLiteral($post['login_username']) + ); + } + // rest the get locked flow + if (!empty($mock_settings['test_get_locked'])) { + self::$db->dbExec( + "UPDATE edit_user " + . "SET login_error_count = 0, login_error_date_last = NULL, " + . "login_error_date_first = NULL, locked = 0, strict = 0 " + . "WHERE LOWER(username) = " + . self::$db->dbEscapeLiteral($post['login_username']) + ); + } } - // other tests - // - loginSetPasswordMinLength - // + // - loginGetAclList (null, invalid,) + + public function aclListProvider(): array + { + // 0: level (int|null) + // 2: type (string), null for skip (if 0 = null) + // 1: acl return from level (array) + // 2: level number to return (must match 0) + return [ + 'null, get full list' => [ + null, + null, + [ + 0 => [ + 'type' => 'none', + 'name' => 'No Access', + ], + 10 => [ + 'type' => 'list', + 'name' => 'List', + ], + 20 => [ + 'type' => 'read', + 'name' => 'Read', + ], + 30 => [ + 'type' => 'mod_trans', + 'name' => 'Translator', + ], + 40 => [ + 'type' => 'mod', + 'name' => 'Modify', + ], + 60 => [ + 'type' => 'write', + 'name' => 'Create/Write', + ], + 80 => [ + 'type' => 'del', + 'name' => 'Delete', + ], + 90 => [ + 'type' => 'siteadmin', + 'name' => 'Site Admin', + ], + 100 => [ + 'type' => 'admin', + 'name' => 'Admin', + ], + ], + null + ], + 'valid, search' => [ + 20, + 'read', + [ + 'type' => 'read', + 'name' => 'Read' + ], + 20 + ], + 'invalud search' => [ + 12, + 'foo', + [], + false, + ] + ]; + } /** * Undocumented function * - * @testdox ACL\Login Class empty void + * @dataProvider aclListProvider() + * @testdox ACL\Login list if $level and $type exepcted level is $expected_level [$_dataName] * + * @param int|null $level + * @param string|null $type + * @param array $expected_list + * @param int|null|bool $expected_level * @return void */ - // public function testOther(): void - // { - // echo "HERE EMPTY 1\n"; - // } + public function testAclLoginList( + ?int $level, + ?string $type, + array $expected_list, + $expected_level + ): void { + $_SESSION = []; + // init session (as MOCK) + /** @var \CoreLibs\Create\Session&MockObject */ + $session_mock = $this->createPartialMock( + \CoreLibs\Create\Session::class, + ['startSession', 'checkActiveSession', 'sessionDestroy'] + ); + $session_mock->method('startSession')->willReturn('ACLLOGINTEST34'); + $session_mock->method('checkActiveSession')->willReturn(true); + $session_mock->method('sessionDestroy')->will( + $this->returnCallback(function () { + global $_SESSION; + $_SESSION = []; + return true; + }) + ); + /** @var \CoreLibs\ACL\Login&MockObject */ + $login_mock = $this->getMockBuilder(\CoreLibs\ACL\Login::class) + ->setConstructorArgs([self::$db, self::$log, $session_mock, false]) + ->onlyMethods(['loginTerminate']) + ->getMock(); + $login_mock->expects($this->any()) + ->method('loginTerminate') + ->will( + $this->returnCallback(function ($code) { + throw new \Exception('', $code); + }) + ); + + $list = $login_mock->loginGetAclList($level); + $this->assertIsArray( + $list, + 'assert get acl list is array' + ); + $this->assertEquals( + $expected_list, + $list + ); + if ($type !== null) { + $this->assertEquals( + $expected_level, + $login_mock->loginGetAclListFromType($type), + 'assert type is level' + ); + // only back assert if found + if (isset($list['type'])) { + $this->assertEquals( + $list['type'], + $type, + 'assert level read type is type' + ); + } + } + } + + /** + * Undocumented function + * + * @return array + */ + public function minPasswordCheckProvider(): array + { + // 0: set length + // 1: expected return from set + // 2: expected set length + return [ + 'set new length' => [ + 12, + true, + 12, + ], + 'set new length, too short' => [ + 5, + false, + 9 + ], + 'set new length, too long' => [ + 500, + false, + 9 + ] + ]; + } + + /** + * check setting minimum password length + * + * @covers ::loginSetPasswordMinLength + * @covers ::loginGetPasswordLenght + * @dataProvider minPasswordCheckProvider() + * @testdox ACL\Login password min length set $input is $expected_return and matches $expected [$_dataName] + * + * @param int $input + * @param bool $expected_return + * @param int $expected + * @return void + */ + public function testACLLoginPasswordMinLenght(int $input, bool $expected_return, int $expected): void + { + $_SESSION = []; + // init session (as MOCK) + /** @var \CoreLibs\Create\Session&MockObject */ + $session_mock = $this->createPartialMock( + \CoreLibs\Create\Session::class, + ['startSession', 'checkActiveSession', 'sessionDestroy'] + ); + $session_mock->method('startSession')->willReturn('ACLLOGINTEST34'); + $session_mock->method('checkActiveSession')->willReturn(true); + $session_mock->method('sessionDestroy')->will( + $this->returnCallback(function () { + global $_SESSION; + $_SESSION = []; + return true; + }) + ); + /** @var \CoreLibs\ACL\Login&MockObject */ + $login_mock = $this->getMockBuilder(\CoreLibs\ACL\Login::class) + ->setConstructorArgs([self::$db, self::$log, $session_mock, false]) + ->onlyMethods(['loginTerminate']) + ->getMock(); + $login_mock->expects($this->any()) + ->method('loginTerminate') + ->will( + $this->returnCallback(function ($code) { + throw new \Exception('', $code); + }) + ); + + // set new min password length + $this->assertEquals( + $expected_return, + $login_mock->loginSetPasswordMinLength($input), + 'assert bool set password min length' + ); + // check value + $this->assertEquals( + $expected, + $login_mock->loginGetPasswordLenght('min'), + 'assert get password min length' + ); + } + + /** + * Undocumented function + * + * @return array + */ + public function getPasswordLengthProvider(): array + { + return [ + 'min' => ['min'], + 'lower' => ['lower'], + 'max' => ['max'], + 'upper' => ['upper'], + 'minimum_length' => ['minimum_length'], + 'min_length' => ['min_length'], + 'length' => ['length'], + ]; + } + + /** + * check all possible readable password length params + * + * @covers ::loginGetPasswordLenght + * @dataProvider getPasswordLengthProvider() + * @testdox ACL\Login get password length $input [$_dataName] + * + * @param string $input + * @return void + */ + public function testACLLoginGetPasswordLenght(string $input): void + { + $_SESSION = []; + // init session (as MOCK) + /** @var \CoreLibs\Create\Session&MockObject */ + $session_mock = $this->createPartialMock( + \CoreLibs\Create\Session::class, + ['startSession', 'checkActiveSession', 'sessionDestroy'] + ); + $session_mock->method('startSession')->willReturn('ACLLOGINTEST34'); + $session_mock->method('checkActiveSession')->willReturn(true); + $session_mock->method('sessionDestroy')->will( + $this->returnCallback(function () { + global $_SESSION; + $_SESSION = []; + return true; + }) + ); + /** @var \CoreLibs\ACL\Login&MockObject */ + $login_mock = $this->getMockBuilder(\CoreLibs\ACL\Login::class) + ->setConstructorArgs([self::$db, self::$log, $session_mock, false]) + ->onlyMethods(['loginTerminate']) + ->getMock(); + $login_mock->expects($this->any()) + ->method('loginTerminate') + ->will( + $this->returnCallback(function ($code) { + throw new \Exception('', $code); + }) + ); + + $this->assertMatchesRegularExpression( + "/^\d+$/", + (string)$login_mock->loginGetPasswordLenght($input) + ); + } + + /** + * Undocumented function + * + * @return array + */ + public function loginMaxErrorProvider(): array + { + return [ + 'set valid max failed login' => [ + 10, + true, + 10 + ], + 'set valid unlimted' => [ + -1, + true, + -1 + ], + 'set invalid 0' => [ + 0, + false, + -1 + ], + 'set invalid negative' => [ + -5, + false, + -1 + ], + ]; + } + + /** + * Undocumented function + * + * @covers ::loginSetMaxLoginErrorCount + * @covers ::loginGetMaxLoginErrorCount + * @dataProvider loginMaxErrorProvider() + * @testdox ACL\Login failed login set/get $input is $expected_return and matches $expected [$_dataName] + * + * @param int $input + * @param bool $expected_return + * @param int $expected + * @return void + */ + public function testACLLoginErrorCount(int $input, bool $expected_return, int $expected): void + { + $_SESSION = []; + // init session (as MOCK) + /** @var \CoreLibs\Create\Session&MockObject */ + $session_mock = $this->createPartialMock( + \CoreLibs\Create\Session::class, + ['startSession', 'checkActiveSession', 'sessionDestroy'] + ); + $session_mock->method('startSession')->willReturn('ACLLOGINTEST34'); + $session_mock->method('checkActiveSession')->willReturn(true); + $session_mock->method('sessionDestroy')->will( + $this->returnCallback(function () { + global $_SESSION; + $_SESSION = []; + return true; + }) + ); + /** @var \CoreLibs\ACL\Login&MockObject */ + $login_mock = $this->getMockBuilder(\CoreLibs\ACL\Login::class) + ->setConstructorArgs([self::$db, self::$log, $session_mock, false]) + ->onlyMethods(['loginTerminate']) + ->getMock(); + $login_mock->expects($this->any()) + ->method('loginTerminate') + ->will( + $this->returnCallback(function ($code) { + throw new \Exception('', $code); + }) + ); + + // set new min password length + $this->assertEquals( + $expected_return, + $login_mock->loginSetMaxLoginErrorCount($input), + 'assert bool set max login errors' + ); + // check value + $this->assertEquals( + $expected, + $login_mock->loginGetMaxLoginErrorCount(), + 'assert get max login errors' + ); + } } // __END__ diff --git a/www/lib/CoreLibs/ACL/Login.php b/www/lib/CoreLibs/ACL/Login.php index ab5eff13..aaed6143 100644 --- a/www/lib/CoreLibs/ACL/Login.php +++ b/www/lib/CoreLibs/ACL/Login.php @@ -87,9 +87,6 @@ class Login private $password; // login password /** @var string */ private $logout; // logout button - // login error code, can be matched to the array login_error_msg, which holds the string - /** @var int */ - private $login_error = 0; /** @var bool */ private $password_change = false; // if this is set to true, the user can change passwords /** @var bool */ @@ -112,20 +109,23 @@ class Login /** @var array */ private $pw_change_deny_users = []; // array of users for which the password change is forbidden /** @var string */ - private $logout_target; + private $logout_target = ''; /** @var int */ private $max_login_error_count = -1; /** @var array */ private $lock_deny_users = []; /** @var string */ - private $page_name; + private $page_name = ''; // if we have password change we need to define some rules /** @var int */ - private $password_min_length = PASSWORD_MIN_LENGTH; - // max length is fixed as 255 (for input type max), if set highter, it will be set back to 255 + private $password_min_length = 9; + /** @var int an true maxium min, can never be set below this */ + private $password_min_length_max = 9; + // max length is fixed as 255 (for input type max), if set highter + // it will be set back to 255 /** @var int */ - private $password_max_length = PASSWORD_MAX_LENGTH; + private $password_max_length = 255; // can have several regexes, if nothing set, all is ok /** @var array */ private $password_valid_chars = [ @@ -133,10 +133,14 @@ class Login // '^(?.*(\pL)u)(?=.*(\pN)u)(?=.*([^\pL\pN])u).{8,}', ]; - // all possible login error conditions - /** @var array */ + // login error code, can be matched to the array login_error_msg, + // which holds the string + /** @var int */ + private $login_error = 0; + /** @var array all possible login error conditions */ private $login_error_msg = []; - // this is an array holding all strings & templates passed from the outside (translation) + // this is an array holding all strings & templates passed + // rom the outside (translation) /** @var array */ private $login_template = [ 'strings' => [], @@ -146,9 +150,13 @@ class Login // acl vars /** @var array */ - public $acl = []; + private $acl = []; /** @var array */ - public $default_acl_list = []; + private $default_acl_list = []; + /** @var array Reverse list to lookup level from type */ + private $default_acl_list_type = []; + /** @var int default ACL level to be based on if nothing set */ + private $default_acl_level = 0; // login html, if we are on an ajax page /** @var string|null */ private $login_html = ''; @@ -171,11 +179,15 @@ class Login * @param \CoreLibs\DB\IO $db Database connection class * @param \CoreLibs\Debug\Logging $log Logging class * @param \CoreLibs\Create\Session $session Session interface class + * @param bool $auto_login [default true] Auto login flag, legacy + * If set to true will run login + * during construction */ public function __construct( \CoreLibs\DB\IO $db, \CoreLibs\Debug\Logging $log, - \CoreLibs\Create\Session $session + \CoreLibs\Create\Session $session, + bool $auto_login = true ) { // attach db class $this->db = $db; @@ -185,90 +197,108 @@ class Login $this->log = $log; // attach session class $this->session = $session; - // set internal page name - $this->page_name = \CoreLibs\Get\System::getPageName(); - // set db special errors - if (!$this->db->dbGetConnectionStatus()) { - echo 'Could not connect to DB
'; - // if I can't connect to the DB to auth exit hard. No access allowed - exit; - } - // initial the session if there is no session running already - // check if session exists and could be created - // TODO: move session creation and check to outside? - if ($this->session->checkActiveSession() === false) { - $this->login_error = 1; - echo 'No active session found'; - exit; - } - - // pre-check that password min/max lengths are inbetween 1 and 255; - if ($this->password_max_length > 255) { - $this->password_max_length = 255; - } - if ($this->password_min_length < 1) { - $this->password_min_length = 1; - } - - // set global is ajax page for if we show the data directly, - // or need to pass it back - // to the continue AJAX class for output back to the user - $this->login_is_ajax_page = isset($GLOBALS['AJAX_PAGE']) && $GLOBALS['AJAX_PAGE'] ? true : false; - - // if we have a search path we need to set it, to use the correct DB to login - // check what schema to use. if there is a login schema use this, else check - // if there is a schema set in the config, or fall back to DB_SCHEMA - // if this exists, if this also does not exists use public schema - /** @phpstan-ignore-next-line */ - if (!empty(LOGIN_DB_SCHEMA)) { - $SCHEMA = LOGIN_DB_SCHEMA; - } elseif (!empty($this->db->dbGetSchema(true))) { - $SCHEMA = $this->db->dbGetSchema(true); - } elseif (defined('PUBLIC_SCHEMA')) { - $SCHEMA = PUBLIC_SCHEMA; - } else { - $SCHEMA = 'public'; - } - // echo "

*****SCHEMA******

: $SCHEMA
"; - // set schema if schema differs to schema set in db conneciton - if ($this->db->dbGetSchema() != $SCHEMA) { - $this->db->dbExec("SET search_path TO " . $SCHEMA); - } - // if there is none, there is none, saves me POST/GET check - $this->euid = array_key_exists('EUID', $_SESSION) ? $_SESSION['EUID'] : 0; - // get login vars, are so, can't be changed - // prepare - // pass on vars to Object vars - $this->login = $_POST['login_login'] ?? ''; - $this->username = $_POST['login_username'] ?? ''; - $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'] ?? ''; - // logout target (from config) - $this->logout_target = LOGOUT_TARGET; - // disallow user list for password change - $this->pw_change_deny_users = ['admin']; - // set flag if password change is okay - if (defined('PASSWORD_CHANGE')) { - $this->password_change = PASSWORD_CHANGE; - } - // NOTE: forgot password flow with email - if (defined('PASSWORD_FORGOT')) { - $this->password_forgot = PASSWORD_FORGOT; - } - // max login counts before error reporting - $this->max_login_error_count = 10; - // users that never get locked, even if they are set strict - $this->lock_deny_users = ['admin']; + // 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' + ], + '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' + ], + '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' + ], + ]; // init default ACL list array $_SESSION['DEFAULT_ACL_LIST'] = []; + $_SESSION['DEFAULT_ACL_LIST_TYPE'] = []; // 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"; @@ -278,94 +308,52 @@ class Login 'type' => $res['type'], 'name' => $res['name'] ]; + $this->default_acl_list_type[$res['type']] = $res['level']; } // write that into the session $_SESSION['DEFAULT_ACL_LIST'] = $this->default_acl_list; + $_SESSION['DEFAULT_ACL_LIST_TYPE'] = $this->default_acl_list_type; - // if username & password & !$euid start login - $this->loginLoginUser(); - // checks if $euid given check if user is okay for that side - $this->loginCheckPermissions(); - // logsout user - $this->loginLogoutUser(); - // ** LANGUAGE SET AFTER LOGIN ** - // set the locale - if ( - $this->session->checkActiveSession() === true && - !empty($_SESSION['DEFAULT_LANG']) - ) { - $locale = $_SESSION['DEFAULT_LOCALE'] ?? ''; - } else { - $locale = !empty(SITE_LOCALE) ? - SITE_LOCALE : - /** @phpstan-ignore-next-line DEFAULT_LOCALE could be empty */ - (!empty(DEFAULT_LOCALE) ? - DEFAULT_LOCALE : 'en.UTF-8'); + // this will be deprecated + if ($auto_login === true) { + $this->loginMainCall(); } - // set domain - if (defined('CONTENT_PATH')) { - $domain = str_replace('/', '', CONTENT_PATH); - } else { - $domain = 'admin'; - } - $this->l = new \CoreLibs\Language\L10n($locale, $domain); - // if the password change flag is okay, run the password change method - if ($this->password_change) { - $this->loginPasswordChange(); - } - // password forgot - if ($this->password_forgot) { - $this->loginPasswordForgot(); - } - // if !$euid || permission not okay, print login screan - $this->login_html = $this->loginPrintLogin(); - // closing all connections, depending on error status, exit - if (!$this->loginCloseClass()) { - // if variable AJAX flag is not set, show output - // else pass through for ajax work - if ($this->login_is_ajax_page !== true) { - // the login screen if we hav no login permission & login screen html data - if ($this->login_html !== null) { - echo $this->login_html; - } - // do not go anywhere, quit processing here - // do something with possible debug data? - if (TARGET == 'live' || TARGET == 'remote') { - // login - $this->log->setLogLevelAll('debug', DEBUG ? true : false); - $this->log->setLogLevelAll('echo', false); - $this->log->setLogLevelAll('print', DEBUG ? true : false); - } - $status_msg = $this->log->printErrorMsg(); - // if ($this->echo_output_all) { - if ($this->log->getLogLevelAll('echo')) { - echo $status_msg; - } - // exit so we don't process anything further, at all - exit; - } else { - // if we are on an ajax page reset any POST/GET array data to avoid - // any accidentical processing going on - $_POST = []; - $_GET = []; - // set the action to login so we can trigger special login html return - $_POST['action'] = 'login'; - $_POST['login_html'] = $this->login_html; - // NOTE: this part needs to be catched by the frontend AJAX - // and some function needs to then set something like this - // document.getElementsByTagName('html')[0].innerHTML = data.content.login_html; - } - } - // set acls for this user/group and this page - $this->loginSetAcl(); + } + + // ************************************************************************* + // **** PROTECTED INTERNAL + // ************************************************************************* + + /** + * Wrapper for exit calls + * + * @param int $code + * @return void + */ + protected function loginTerminate($code = 0): void + { + exit($code); } /** - * deconstructory, called with the last function to close DB connection + * return current page name + * + * @return string Current page name */ - public function __destruct() + protected function loginReadPageName(): string { - // NO OP + // set internal page name as is + return \CoreLibs\Get\System::getPageName(); + } + + /** + * print out login HTML via echo + * + * @return void + */ + protected function loginPrintLogin(): void + { + echo $this->loginGetLoginHTML(); } // ************************************************************************* @@ -410,7 +398,7 @@ class Login preg_match("/^\\$2y\\$/", $hash) && !Password::passwordVerify($password, $hash) ) { - // this is the new password hash methid, is only $2y$ + // this is the new password hash method, is only $2y$ // all others are not valid anymore $this->login_error = 1013; $password_ok = false; @@ -439,8 +427,7 @@ class Login private function loginLoginUser(): void { // if pressed login at least and is not yet loggined in - // if (!(!$this->euid && $this->login)) { - if ($this->euid && !$this->login) { + if ($this->euid || !$this->login) { return; } // if not username AND password where given @@ -476,13 +463,16 @@ class Login . "eg.edit_access_right_id = eareg.edit_access_right_id AND " // password match is done in script, against old plain or new blowfish encypted . "(LOWER(username) = '" . $this->db->dbEscapeString(strtolower($this->username)) . "') "; - $res = $this->db->dbReturn($q); + // 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 (!is_array($res)) { + if (!empty($this->db->dbGetLastError())) { $this->login_error = 1009; $this->permission_okay = false; return; - } elseif (empty($this->db->dbGetCursorNumRows($q))) { + } elseif (!is_array($res)) { // username is wrong, but we throw for wrong username // and wrong password the same error $this->login_error = 1010; @@ -677,6 +667,7 @@ class Login if ($res['edit_default']) { $_SESSION['UNIT_DEFAULT'] = $res['edit_access_id']; } + $_SESSION['UNIT_UID'][$res['uid']] = $res['edit_access_id']; // sub arrays for simple access array_push($eauid, $res['edit_access_id']); $unit_acl[$res['edit_access_id']] = $res['level']; @@ -745,10 +736,10 @@ class Login $this->acl['user_name'] = $_SESSION['USER_NAME']; $this->acl['group_name'] = $_SESSION['GROUP_NAME']; // we start with the default acl - $this->acl['base'] = DEFAULT_ACL_LEVEL; + $this->acl['base'] = $this->default_acl_level; // set admin flag and base to 100 - if ($_SESSION['ADMIN']) { + if (!empty($_SESSION['ADMIN'])) { $this->acl['admin'] = 1; $this->acl['base'] = 100; } else { @@ -760,7 +751,10 @@ class Login $this->acl['base'] = $_SESSION['GROUP_ACL_LEVEL']; } // page ACL 1 - if ($_SESSION['PAGES_ACL_LEVEL'][$this->page_name] != -1) { + if ( + isset($_SESSION['PAGES_ACL_LEVEL'][$this->page_name]) && + $_SESSION['PAGES_ACL_LEVEL'][$this->page_name] != -1 + ) { $this->acl['base'] = $_SESSION['PAGES_ACL_LEVEL'][$this->page_name]; } // user ACL 2 @@ -788,7 +782,7 @@ class Login // PER ACCOUNT (UNIT/edit access)-> foreach ($_SESSION['UNIT'] as $ea_id => $unit) { // if admin flag is set, all units are set to 100 - if ($this->acl['admin']) { + if (!empty($this->acl['admin'])) { $this->acl['unit'][$ea_id] = $this->acl['base']; } else { if ($unit['acl_level'] != -1) { @@ -801,12 +795,12 @@ class Login $this->acl['unit_detail'][$ea_id] = [ 'name' => $unit['name'], 'uid' => $unit['uid'], - 'level' => $this->default_acl_list[$this->acl['unit'][$ea_id]]['name'], + 'level' => $this->default_acl_list[$this->acl['unit'][$ea_id]]['name'] ?? -1, 'default' => $unit['default'], 'data' => $unit['data'] ]; // set default - if ($unit['default']) { + if (!empty($unit['default'])) { $this->acl['unit_id'] = $unit['id']; $this->acl['unit_name'] = $unit['name']; $this->acl['unit_uid'] = $unit['uid']; @@ -820,43 +814,14 @@ class Login } // set the default edit access $this->acl['default_edit_access'] = $_SESSION['UNIT_DEFAULT'] ?? null; - $this->acl['min'] = []; // integrate the type acl list, but only for the keyword -> level - foreach ($this->default_acl_list as $level => $data) { - $this->acl['min'][$data['type']] = $level; - } - // set the full acl list too - $this->acl['acl_list'] = $_SESSION['DEFAULT_ACL_LIST'] ?? []; + $this->acl['min'] = $this->default_acl_list_type ?? []; + // set the full acl list too (lookup level number and get level data) + $this->acl['acl_list'] = $this->default_acl_list ?? []; // debug // $this->debug('ACL', $this->print_ar($this->acl)); } - /** - * 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) - * This does not take in account admin flag set - * - * @param string $source a valid base level string eg base, page - * @param string $min_access a valid min level string, eg read, mod, siteadmin - * @return bool True for valid access, False for invalid - */ - public function loginCheckAccess(string $source, string $min_access): bool - { - $source = 'base'; - if ( - empty($this->acl['min'][$min_access]) || - empty($this->acl[$source]) - ) { - return false; - } - // phan claims $this->acl['min'] can be null, but above should skip - /** @phan-suppress-next-line PhanTypeArraySuspiciousNullable */ - if ($this->acl[$source] >= $this->acl['min'][$min_access]) { - return true; - } - return false; - } - /** * checks if the password is in a valid format * @@ -875,7 +840,10 @@ class Login } } // check for min length - if (strlen($password) < $this->password_min_length || strlen($password) > $this->password_max_length) { + if ( + strlen($password) < $this->password_min_length || + strlen($password) > $this->password_max_length + ) { $is_valid_password = false; } return $is_valid_password; @@ -993,11 +961,12 @@ class Login } /** - * prints out login html part if no permission (error) is set + * creates the login html part if no permission (error) is set + * this does not print anything yet * * @return string|null html data for login page, or null for nothing */ - private function loginPrintLogin() + private function loginCreateLoginHTML() { $html_string = null; // if permission is ok, return null @@ -1007,10 +976,10 @@ class Login // set the templates now $this->loginSetTemplates(); // if there is a global logout target ... - if (file_exists($this->logout_target) && $this->logout_target) { + if (file_exists($this->logout_target)) { $LOGOUT_TARGET = $this->logout_target; } else { - $LOGOUT_TARGET = ""; + $LOGOUT_TARGET = ''; } $html_string = (string)$this->login_template['template']; @@ -1033,7 +1002,7 @@ class Login if ($this->login_error) { $html_string_password_change = str_replace( '{ERROR_MSG}', - $this->login_error_msg[$this->login_error] . '
', + $this->loginGetErrorMsg($this->login_error) . '
', $html_string_password_change ); } else { @@ -1076,13 +1045,13 @@ class Login if ($this->login_error) { $html_string = str_replace( '{ERROR_MSG}', - $this->login_error_msg[$this->login_error] . '
', + $this->loginGetErrorMsg($this->login_error) . '
', $html_string ); } elseif ($this->password_change_ok && $this->password_change) { $html_string = str_replace( '{ERROR_MSG}', - $this->login_error_msg[300] . '
', + $this->loginGetErrorMsg(300) . '
', $html_string ); } else { @@ -1155,40 +1124,6 @@ class Login 'PASSWORD_CHANGE_BUTTON_VALUE' => $this->l->__('Change Password') ]; - $error_msgs = [ - // actually obsolete - '100' => $this->l->__('Fatal Error: [EUID] came in as GET/POST!'), - // query errors - '1009' => $this->l->__('Fatal Error: Login query reading failed'), - // user not found - '1010' => $this->l->__('Fatal Error: Login Failed - Wrong Username or Password'), - // blowfish password wrong - '1011' => $this->l->__('Fatal Error: Login Failed - Wrong Username or Password'), - // fallback md5 password wrong - '1012' => $this->l->__('Fatal Error: Login Failed - Wrong Username or Password'), - // new password_hash wrong - '1013' => $this->l->__('Fatal Error: Login Failed - Wrong Username or Password'), - '102' => $this->l->__('Fatal Error: Login Failed - Please enter username and password'), - '103' => $this->l->__('Fatal Error: You do not have the rights to access this Page'), - '104' => $this->l->__('Fatal Error: Login Failed - User not enabled'), - '105' => $this->l->__('Fatal Error: Login Failed - User is locked'), - '109' => $this->l->__('Fatal Error: Check permission query reading failed'), - // actually this is an illegal user, but I mask it - '220' => $this->l->__('Fatal Error: Password change - The user could not be found'), - '200' => $this->l->__('Fatal Error: Password change - Please enter username and old password'), - '201' => $this->l->__('Fatal Error: Password change - The user could not be found'), - '202' => $this->l->__('Fatal Error: Password change - The old password is not correct'), - '203' => $this->l->__('Fatal Error: Password change - Please fill out both new password fields'), - '204' => $this->l->__('Fatal Error: Password change - The new passwords do not match'), - // we should also not here WHAT is valid - '205' => $this->l->__('Fatal Error: Password change - The new password is not in a valid format'), - // for OK password change - '300' => $this->l->__('Success: Password change successful'), - // this is bad bad error - '9999' => $this->l->__('Fatal Error: necessary crypt engine could not be found. ' - . 'Login is impossible'), - ]; - // if password change is okay if ($this->password_change) { $strings = array_merge($strings, [ @@ -1238,20 +1173,14 @@ EOM; ]); } - // first check if all strings are set from outside, if not, set with default ones + // first check if all strings are set from outside, + // if not, set with default ones foreach ($strings as $string => $data) { if (!array_key_exists($string, $this->login_template['strings'])) { $this->login_template['strings'][$string] = $data; } } - // error msgs the same - foreach ($error_msgs as $code => $data) { - if (!array_key_exists($code, $this->login_error_msg)) { - $this->login_error_msg[$code] = $data; - } - } - // now check templates if (!$this->login_template['template']) { $this->login_template['template'] = <<login_error = 0; + // set db special errors + if (!$this->db->dbGetConnectionStatus()) { + $this->login_error = 1; + echo 'Could not connect to DB
'; + // if I can't connect to the DB to auth exit hard. No access allowed + $this->loginTerminate(1000); + } + // initial the session if there is no session running already + // check if session exists and could be created + // TODO: move session creation and check to outside? + if ($this->session->checkActiveSession() === false) { + $this->login_error = 2; + echo 'No active session found'; + $this->loginTerminate(2000); + } + + // if we have a search path we need to set it, to use the correct DB to login + // check what schema to use. if there is a login schema use this, else check + // if there is a schema set in the config, or fall back to DB_SCHEMA + // if this exists, if this also does not exists use public schema + /** @phpstan-ignore-next-line */ + if (defined('LOGIN_DB_SCHEMA') && !empty(LOGIN_DB_SCHEMA)) { + $SCHEMA = LOGIN_DB_SCHEMA; + } elseif (!empty($this->db->dbGetSchema(true))) { + $SCHEMA = $this->db->dbGetSchema(true); + } elseif (defined('PUBLIC_SCHEMA')) { + $SCHEMA = PUBLIC_SCHEMA; + } else { + $SCHEMA = 'public'; + } + // set schema if schema differs to schema set in db conneciton + if ($this->db->dbGetSchema() != $SCHEMA) { + $this->db->dbExec("SET search_path TO " . $SCHEMA); + } + + // set internal page name + $this->page_name = $this->loginReadPageName(); + + // set default ACL Level + if (defined('DEFAULT_ACL_LEVEL')) { + $this->default_acl_level = DEFAULT_ACL_LEVEL; + } + + if (defined('PASSWORD_MIN_LENGTH')) { + $this->password_min_length = PASSWORD_MIN_LENGTH; + $this->password_min_length_max = PASSWORD_MIN_LENGTH; + } + if (defined('PASSWORD_MIN_LENGTH')) { + $this->password_max_length = PASSWORD_MAX_LENGTH; + } + + // pre-check that password min/max lengths are inbetween 1 and 255; + if ($this->password_max_length > 255) { + $this->password_max_length = 255; + } + if ($this->password_min_length < 1) { + $this->password_min_length = 1; + } + + // set global is ajax page for if we show the data directly, + // or need to pass it back + // to the continue AJAX class for output back to the user + $this->login_is_ajax_page = isset($GLOBALS['AJAX_PAGE']) && $GLOBALS['AJAX_PAGE'] ? true : false; + + // if there is none, there is none, saves me POST/GET check + $this->euid = array_key_exists('EUID', $_SESSION) ? $_SESSION['EUID'] : 0; + // get login vars, are so, can't be changed + // prepare + // pass on vars to Object vars + $this->login = $_POST['login_login'] ?? ''; + $this->username = $_POST['login_username'] ?? ''; + $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'] ?? ''; + // logout target (from config) + if (defined('LOGOUT_TARGET')) { + $this->logout_target = LOGOUT_TARGET; + } + // disallow user list for password change + $this->pw_change_deny_users = ['admin']; + // set flag if password change is okay + if (defined('PASSWORD_CHANGE')) { + $this->password_change = PASSWORD_CHANGE; + } + // NOTE: forgot password flow with email + if (defined('PASSWORD_FORGOT')) { + $this->password_forgot = PASSWORD_FORGOT; + } + // max login counts before error reporting + $this->max_login_error_count = 10; + // users that never get locked, even if they are set strict + $this->lock_deny_users = ['admin']; + + // if username & password & !$euid start login + $this->loginLoginUser(); + // checks if $euid given check if user is okay for that side + $this->loginCheckPermissions(); + // logsout user + $this->loginLogoutUser(); + // ** LANGUAGE SET AFTER LOGIN ** + // set the locale + if ( + $this->session->checkActiveSession() === true && + !empty($_SESSION['DEFAULT_LANG']) + ) { + $locale = $_SESSION['DEFAULT_LOCALE'] ?? ''; + } else { + $locale = (defined('SITE_LOCALE') && !empty(SITE_LOCALE)) ? + SITE_LOCALE : + /** @phpstan-ignore-next-line DEFAULT_LOCALE could be empty */ + ((defined('DEFAULT_LOCALE') && !empty(DEFAULT_LOCALE)) ? + DEFAULT_LOCALE : 'en.UTF-8'); + } + // set domain + if (defined('CONTENT_PATH')) { + $domain = str_replace('/', '', CONTENT_PATH); + } else { + $domain = 'admin'; + } + $this->l = new \CoreLibs\Language\L10n($locale, $domain); + // if the password change flag is okay, run the password change method + if ($this->password_change) { + $this->loginPasswordChange(); + } + // password forgot + if ($this->password_forgot) { + $this->loginPasswordForgot(); + } + // if !$euid || permission not okay, print login screan + $this->login_html = $this->loginCreateLoginHTML(); + // closing all connections, depending on error status, exit + if (!$this->loginCloseClass()) { + // if variable AJAX flag is not set, show output + // else pass through for ajax work + if ($this->login_is_ajax_page === false) { + // the login screen if we hav no login permission & login screen html data + if ($this->login_html !== null) { + // echo $this->login_html; + $this->loginPrintLogin(); + } + // do not go anywhere, quit processing here + // do something with possible debug data? + if (TARGET == 'live' || TARGET == 'remote') { + // login + $this->log->setLogLevelAll('debug', DEBUG ? true : false); + $this->log->setLogLevelAll('echo', false); + $this->log->setLogLevelAll('print', DEBUG ? true : false); + } + $status_msg = $this->log->printErrorMsg(); + // if ($this->echo_output_all) { + if ($this->log->getLogLevelAll('echo')) { + echo $status_msg; + } + // exit so we don't process anything further, at all + $this->loginTerminate(3000); + } else { + // if we are on an ajax page reset any POST/GET array data to avoid + // any accidentical processing going on + $_POST = []; + $_GET = []; + // set the action to login so we can trigger special login html return + $_POST['action'] = 'login'; + $_POST['login_html'] = $this->login_html; + // NOTE: this part needs to be catched by the frontend AJAX + // and some function needs to then set something like this + // document.getElementsByTagName('html')[0].innerHTML = data.content.login_html; + } + } + // set acls for this user/group and this page + $this->loginSetAcl(); + } + + /** + * Returns current set login_html content + * + * @return string login page html content, created, empty string if none + */ + public function loginGetLoginHTML(): string + { + return $this->login_html ?? ''; + } + + /** + * return the current set page name or empty string for nothing set + * + * @return string current page name set + */ + public function loginGetPageName(): string + { + return $this->page_name; + } + + /** + * returns the last set error code + * + * @return int Last set error code, 0 for no error + */ + public function loginGetLastErrorCode(): int + { + return $this->login_error; + } + + /** + * return set error message + * if nothing found for given code, return general error message + * + * @param int $code The error code for which we want the error string + * @param bool $text If set to true, do not use HTML code + * @return string Error string + */ + public function loginGetErrorMsg(int $code, bool $text = false): string + { + $string = ''; + if ( + !empty($this->login_error_msg[(string)$code]['msg']) && + !empty($this->login_error_msg[(string)$code]['flag']) + ) { + $error_str_prefix = ''; + switch ($this->login_error_msg[(string)$code]['flag']) { + case 'e': + $error_str_prefix = ($text ? '' : '') + . $this->l->__('Fatal Error:') + . ($text ? '' : ''); + break; + case 'o': + $error_str_prefix = $this->l->__('Success:'); + break; + } + $string = $error_str_prefix . ' ' + . ($text ? '' : '') + . $this->login_error_msg[(string)$code]['msg'] + . ($text ? '' : ''); + } elseif (!empty($code)) { + $string = $this->l->__('LOGIN: undefined error message'); + } + return $string; + } + + /** + * Sets the minium length and checks on valid. + * Current max length is 255 characters * * @param int $length set the minimum length * @return bool true/false on success @@ -1396,13 +1580,74 @@ EOM; { // check that numeric, positive numeric, not longer than max input string lenght // and not short than min password length - if (is_numeric($length) && $length >= PASSWORD_MIN_LENGTH && $length <= $this->password_max_length) { + if ( + is_numeric($length) && + $length >= $this->password_min_length_max && + $length <= $this->password_max_length && + $length <= 255 + ) { $this->password_min_length = $length; return true; } return false; } + /** + * return password min/max length values as selected + * min: return current minimum lenght + * max: return current set maximum length + * min_length: get the fixed minimum password length + * + * @param string $select Can be min/max or min_length + * @return int + */ + public function loginGetPasswordLenght(string $select): int + { + $value = 0; + switch (strtolower($select)) { + case 'min': + case 'lower': + $value = $this->password_min_length; + break; + case 'max': + case 'upper': + $value = $this->password_max_length; + break; + case 'minimum_length': + case 'min_length': + case 'length': + $value = $this->password_min_length_max; + break; + } + return $value; + } + + /** + * Set the maximum login errors a user can have before getting locked + * if the user has the strict lock setting turned on + * + * @param int $times Value can be -1 (no locking) or greater than 0 + * @return bool True on sueccess set, or false on error + */ + public function loginSetMaxLoginErrorCount(int $times): bool + { + if ($times == -1 || $times > 0) { + $this->max_login_error_count = $times; + return true; + } + return false; + } + + /** + * Get the current maximum login error count + * + * @return int Current set max login error count, Can be -1 or greater than 0 + */ + public function loginGetMaxLoginErrorCount(): int + { + return $this->max_login_error_count; + } + /** * if a user pressed on logout, destroyes session and unsets all global vars * @@ -1414,42 +1659,10 @@ EOM; if (!$this->logout && !$this->login_error) { return; } - // unregister and destroy session vars - foreach ( - // TODO move this into some global array for easier update - [ - 'ADMIN', - 'BASE_ACL_LEVEL', - 'DB_DEBUG', - 'DEBUG_ALL', - 'DEFAULT_ACL_LIST', - 'DEFAULT_CHARSET', - 'DEFAULT_LANG', - 'DEFAULT_LOCALE', - 'EAID', - 'EUID', - 'GROUP_ACL_LEVEL', - 'GROUP_ACL_TYPE', - 'GROUP_NAME', - 'HEADER_COLOR', - 'LANG', - 'PAGES_ACL_LEVEL', - 'PAGES', - 'TEMPLATE', - 'UNIT_ACL_LEVEL', - 'UNIT_DEFAULT', - 'UNIT', - 'USER_ACL_LEVEL', - 'USER_ACL_TYPE', - 'USER_NAME', - ] as $session_var - ) { - unset($_SESSION[$session_var]); - } - // final unset all - session_unset(); - // final destroy session - session_destroy(); + // unset session vars set/used in this login + $this->session->sessionDestroy(); + // unset euid + $this->euid = ''; // then prints the login screen again $this->permission_okay = false; } @@ -1464,7 +1677,7 @@ EOM; // start with not allowed $this->permission_okay = false; // bail for no euid (no login) - if (!$this->euid) { + if (empty($this->euid)) { return $this->permission_okay; } // bail for previous wrong page match, eg if method is called twice @@ -1472,13 +1685,13 @@ EOM; return $this->permission_okay; } // if ($this->euid && $this->login_error != 103) { - $q = "SELECT filename " + $q = "SELECT ep.filename " . "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 filename = '" . $this->page_name . "' " + . "AND ep.filename = '" . $this->page_name . "' " . "AND eg.enabled = 1 AND epa.enabled = 1"; $res = $this->db->dbReturnRow($q); if (!is_array($res)) { @@ -1495,41 +1708,38 @@ EOM; } /** - * Return ACL array as is + * Return current permission status; * - * @return array + * @return bool True for permission ok, False for not */ - public function loginGetAcl(): array + public function loginGetPermissionOkay(): bool { - return $this->acl; + return $this->permission_okay; } /** - * checks if this edit access id is valid + * 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) + * This does not take in account admin flag set * - * @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 + * @param string $source a valid base level string eg base, page + * @param string $min_access a valid min level string, eg read, mod, siteadmin + * @return bool True for valid access, False for invalid */ - public function loginCheckEditAccess($edit_access_id): bool + public function loginCheckAccess(string $source, string $min_access): bool { - if ($edit_access_id === null) { + if (!in_array($source, ['page', 'base'])) { + $source = 'base'; + } + if ( + empty($this->acl['min'][$min_access]) || + empty($this->acl[$source]) + ) { return false; } - if (array_key_exists($edit_access_id, $this->acl['unit'])) { - return true; - } - return false; - } - - /** - * Check if admin flag is set - * - * @return bool True if admin flag set - */ - public function loginIsAdmin(): bool - { - if (!empty($this->acl['admin'])) { + // phan claims $this->acl['min'] can be null, but above should skip + /** @phan-suppress-next-line PhanTypeArraySuspiciousNullable */ + if ($this->acl[$source] >= $this->acl['min'][$min_access]) { return true; } return false; @@ -1559,8 +1769,75 @@ EOM; return $this->loginCheckAccess('page', $min_access); } + /** + * Return ACL array as is + * + * @return array + */ + public function loginGetAcl(): array + { + return $this->acl; + } + + /** + * return full default acl list or a list entry if level is set and found + * for getting level from list type + * $login->loginGetAclList('list')['level'] ?? 0 + * + * @param int|null $level Level to get or null/empty for full list + * @return array Full default ACL level list or level entry if found + */ + public function loginGetAclList(?int $level = null): array + { + // if no level given, return full list + if (empty($level)) { + return $this->default_acl_list; + } + // if level given and exist return this array block (name/level) + if ( + !empty($level) && + !empty($this->default_acl_list[$level]) + ) { + return $this->default_acl_list[$level]; + } else { + // else return empty array + return []; + } + } + + /** + * return level number in int from acl list depending on level + * if not found return false + * + * @param string $type Type name to look in the acl list + * @return int|bool Either int level or false for not found + */ + public function loginGetAclListFromType(string $type) + { + return $this->default_acl_list_type[$type] ?? false; + } + + /** + * 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 + */ + public function loginCheckEditAccess($edit_access_id): bool + { + if ($edit_access_id === null) { + return false; + } + if (array_key_exists($edit_access_id, $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 int|null $edit_access_id edit access id to check * @return int|null same edit access id if ok @@ -1581,21 +1858,58 @@ EOM; } /** - * retunrn a set entry from the UNIT session for an edit access_id + * return a set entry from the UNIT session for an edit access_id * 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 */ - public function loginSetEditAccessData(int $edit_access_id, $data_key) + public function loginGetEditAccessData(int $edit_access_id, $data_key) { if (!isset($_SESSION['UNIT'][$edit_access_id]['data'][$data_key])) { return false; } return $_SESSION['UNIT'][$edit_access_id]['data'][$data_key]; } - // close class + + /** + * Return edit access primary key id from edit access uid + * 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 + */ + public function loginGetEditAccessIdFromUid(string $uid) + { + return $_SESSION['UNIT_UID'][$uid] ?? false; + } + + /** + * Check if admin flag is set + * + * @return bool True if admin flag set + */ + public function loginIsAdmin(): bool + { + if (!empty($this->acl['admin'])) { + return true; + } + return false; + } + + /** + * 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, $data_key) + { + return $this->loginGetEditAccessData($edit_access_id, $data_key); + } } // __END__