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__