From 5bcd2d2ac097491ea22765ce3a36bb9b0e38ae4c Mon Sep 17 00:00:00 2001 From: Yvo Brevoort Date: Sun, 6 Jul 2025 21:47:30 +0200 Subject: [PATCH 01/19] add first batch of unit tests --- tests/phpunit/.phpunit.result.cache | 1 + tests/phpunit/ClientRegistrationTest.php | 74 ++++++++++++++ tests/phpunit/IpAttemptsTest.php | 123 +++++++++++++++++++++++ tests/phpunit/JtiStoreTest.php | 87 ++++++++++++++++ tests/phpunit/PasswordValidatorTest.php | 79 +++++++++++++++ tests/phpunit/UtilTest.php | 14 +++ tests/phpunit/phpunit.xml | 7 ++ tests/phpunit/test-config.php | 5 + 8 files changed, 390 insertions(+) create mode 100644 tests/phpunit/.phpunit.result.cache create mode 100644 tests/phpunit/ClientRegistrationTest.php create mode 100644 tests/phpunit/IpAttemptsTest.php create mode 100644 tests/phpunit/JtiStoreTest.php create mode 100644 tests/phpunit/PasswordValidatorTest.php create mode 100644 tests/phpunit/UtilTest.php create mode 100644 tests/phpunit/phpunit.xml create mode 100644 tests/phpunit/test-config.php diff --git a/tests/phpunit/.phpunit.result.cache b/tests/phpunit/.phpunit.result.cache new file mode 100644 index 0000000..391632e --- /dev/null +++ b/tests/phpunit/.phpunit.result.cache @@ -0,0 +1 @@ +{"version":1,"defects":{"Pdsinterop\\PhpSolid\\Tests\\ClientRegistrationTest::testSimpleCreationg":8,"Pdsinterop\\PhpSolid\\ClientRegistrationTest::testSimpleCreationg":8,"Pdsinterop\\PhpSolid\\Tests\\ClientRegistrationTest::testSimpleCreation":5,"Pdsinterop\\PhpSolid\\Tests\\ClientRegistrationTest::testGetRegistration":8,"Pdsinterop\\PhpSolid\\Tests\\ClientRegistrationTest::testClientNameAutofill":7,"Pdsinterop\\PhpSolid\\Tests\\IpAttemptsTest::testGetAttemptsCountZero":8,"Pdsinterop\\PhpSolid\\Tests\\IpAttemptsTest::testGetAttemptsCountOne":8,"Pdsinterop\\PhpSolid\\Tests\\IpAttemptsTest::testGetAttemptsCountTwo":7,"Pdsinterop\\PhpSolid\\Tests\\IpAttemptsTest::testGetAttemptsCountExpired":7,"Pdsinterop\\PhpSolid\\Tests\\IpAttemptsTest::testCleanup":7,"Pdsinterop\\PhpSolid\\Tests\\UtilTest::testBase64EncodeDecode":7,"Pdsinterop\\PhpSolid\\Tests\\JtiTest::testNonExistingJti":8,"Pdsinterop\\PhpSolid\\Tests\\JtiTest::testExistingJti":8,"Pdsinterop\\PhpSolid\\Tests\\JtiTest::testExpiredJti":8,"Pdsinterop\\PhpSolid\\Tests\\JtiTest::testCleanup":8,"Pdsinterop\\PhpSolid\\Tests\\JtiStoreTest::testExpiredJti":8,"Pdsinterop\\PhpSolid\\Tests\\JtiStoreTest::testCleanup":7},"times":{"Pdsinterop\\PhpSolid\\Tests\\BaseTest::testBaseLower":0.001,"Pdsinterop\\PhpSolid\\Tests\\BaseTest::testBaseUpper":0,"Pdsinterop\\PhpSolid\\Tests\\BaseTest::testBaseNumbers":0,"Pdsinterop\\PhpSolid\\Tests\\BaseTest::testBaseSpecial":0,"Pdsinterop\\PhpSolid\\Tests\\BaseTest::testBaseUpperAndLower":0,"Pdsinterop\\PhpSolid\\Tests\\EntropyTest::testSimplyEntropy":0,"Pdsinterop\\PhpSolid\\Tests\\LengthTest::testLengthLower":0,"Pdsinterop\\PhpSolid\\Tests\\LengthTest::testLengthUpper":0,"Pdsinterop\\PhpSolid\\Tests\\LengthTest::testLengthNumbers":0,"Pdsinterop\\PhpSolid\\Tests\\LengthTest::testLengthSpecial":0,"Pdsinterop\\PhpSolid\\Tests\\LengthTest::testLengthUpperAndLower":0,"Pdsinterop\\PhpSolid\\Tests\\PasswordValidatorTest::testBaseLower":0,"Pdsinterop\\PhpSolid\\Tests\\PasswordValidatorTest::testBaseUpper":0,"Pdsinterop\\PhpSolid\\Tests\\PasswordValidatorTest::testBaseNumbers":0,"Pdsinterop\\PhpSolid\\Tests\\PasswordValidatorTest::testBaseSpecial":0,"Pdsinterop\\PhpSolid\\Tests\\PasswordValidatorTest::testBaseUpperAndLower":0,"Pdsinterop\\PhpSolid\\Tests\\PasswordValidatorTest::testLengthLower":0,"Pdsinterop\\PhpSolid\\Tests\\PasswordValidatorTest::testLengthUpper":0,"Pdsinterop\\PhpSolid\\Tests\\PasswordValidatorTest::testLengthNumbers":0,"Pdsinterop\\PhpSolid\\Tests\\PasswordValidatorTest::testLengthSpecial":0,"Pdsinterop\\PhpSolid\\Tests\\PasswordValidatorTest::testLengthUpperAndLower":0,"Pdsinterop\\PhpSolid\\Tests\\PasswordValidatorTest::testSimplyEntropy":0,"Pdsinterop\\PhpSolid\\Tests\\ClientRegistrationTest::testSimpleCreationg":0.002,"Pdsinterop\\PhpSolid\\ClientRegistrationTest::testSimpleCreationg":0.002,"Pdsinterop\\PhpSolid\\Tests\\ClientRegistrationTest::testSimpleCreation":0.001,"Pdsinterop\\PhpSolid\\Tests\\ClientRegistrationTest::testGetRegistration":0,"Pdsinterop\\PhpSolid\\Tests\\ClientRegistrationTest::testClientNameAutofill":0,"Pdsinterop\\PhpSolid\\Tests\\ClientRegistrationTest::testClientByOrigin":0,"Pdsinterop\\PhpSolid\\Tests\\ClientRegistrationTest::testClientByDuplicateOrigin":0,"Pdsinterop\\PhpSolid\\Tests\\IpAttemptsTest::testGetAttemptsCountZero":0.001,"Pdsinterop\\PhpSolid\\Tests\\IpAttemptsTest::testGetAttemptsCountOne":0,"Pdsinterop\\PhpSolid\\Tests\\IpAttemptsTest::testGetAttemptsCountTwo":0,"Pdsinterop\\PhpSolid\\Tests\\IpAttemptsTest::testGetAttemptsCountExpired":0,"Pdsinterop\\PhpSolid\\Tests\\IpAttemptsTest::testGetAttemptsCountOneExpired":0,"Pdsinterop\\PhpSolid\\Tests\\IpAttemptsTest::testCleanup":0,"Pdsinterop\\PhpSolid\\Tests\\IpAttemptsTest::testTrustedIpGetAttemptsCountTwo":0,"Pdsinterop\\PhpSolid\\Tests\\IpAttemptsTest::testTrustedIpGetAttempts":0,"Pdsinterop\\PhpSolid\\Tests\\IpAttemptsTest::testTrustedIpGetAttemptsSkipsDb":0,"Pdsinterop\\PhpSolid\\Tests\\UtilTest::testBase64EncodeDecode":0,"Pdsinterop\\PhpSolid\\Tests\\JtiTest::testNonExistingJti":0.002,"Pdsinterop\\PhpSolid\\Tests\\JtiTest::testExistingJti":0,"Pdsinterop\\PhpSolid\\Tests\\JtiTest::testExpiredJti":0,"Pdsinterop\\PhpSolid\\Tests\\JtiTest::testCleanup":0,"Pdsinterop\\PhpSolid\\Tests\\JtiStoreTest::testNonExistingJti":0,"Pdsinterop\\PhpSolid\\Tests\\JtiStoreTest::testExistingJti":0,"Pdsinterop\\PhpSolid\\Tests\\JtiStoreTest::testExpiredJti":0,"Pdsinterop\\PhpSolid\\Tests\\JtiStoreTest::testCleanup":0}} \ No newline at end of file diff --git a/tests/phpunit/ClientRegistrationTest.php b/tests/phpunit/ClientRegistrationTest.php new file mode 100644 index 0000000..8616fde --- /dev/null +++ b/tests/phpunit/ClientRegistrationTest.php @@ -0,0 +1,74 @@ +exec($statement); + } + } catch(\PDOException $e) { + echo $e->getMessage(); + } + + ClientRegistration::saveClientRegistration([ + "client_id" => "12345", + "origin" => "https://example.com", + "client_name" => "Client name" + ]); + + ClientRegistration::saveClientRegistration([ + "client_id" => "23456", + "origin" => "https://example2.com" + ]); + + ClientRegistration::saveClientRegistration([ + "client_id" => "34567", + "origin" => "https://example2.com" + ]); + } + + public function testGetRegistration() { + $storedData = ClientRegistration::getRegistration("12345"); + $this->assertEquals("12345", $storedData['client_id']); + $this->assertEquals("https://example.com", $storedData['origin']); + $this->assertEquals("Client name", $storedData['client_name']); + } + + public function testClientNameAutofill() { + $storedData = ClientRegistration::getRegistration("23456"); + $this->assertEquals("23456", $storedData['client_id']); + $this->assertEquals("https://example2.com", $storedData['origin']); + $this->assertEquals("https://example2.com", $storedData['client_name']); + } + + public function testClientByOrigin() { + $storedData = ClientRegistration::getClientByOrigin("https://example.com"); + $this->assertEquals("12345", $storedData['client_id']); + $this->assertEquals("https://example.com", $storedData['origin']); + $this->assertEquals("Client name", $storedData['client_name']); + } + + public function testClientByDuplicateOrigin() { + $storedData = ClientRegistration::getClientByOrigin("https://example2.com"); + $this->assertFalse($storedData); // false because we have 2 clients with the same origin + } +} diff --git a/tests/phpunit/IpAttemptsTest.php b/tests/phpunit/IpAttemptsTest.php new file mode 100644 index 0000000..df0373d --- /dev/null +++ b/tests/phpunit/IpAttemptsTest.php @@ -0,0 +1,123 @@ +exec($statement); + } + } catch(\PDOException $e) { + echo $e->getMessage(); + } + } + + public function testGetAttemptsCountZero() { + $ip = "10.0.0.1"; + $count = IpAttempts::getAttemptsCount($ip, "test"); + $this->assertEquals(0, $count); + } + + public function testGetAttemptsCountOne() { + $ip = "10.0.0.1"; + + IpAttempts::logFailedAttempt($ip, "test", time() + 3600); + $count = IpAttempts::getAttemptsCount($ip, "test"); + $this->assertEquals(1, $count); + } + + public function testGetAttemptsCountTwo() { + $ip = "10.0.0.1"; + + IpAttempts::logFailedAttempt($ip, "test", time() + 3600); + IpAttempts::logFailedAttempt($ip, "test", time() + 3600); + + $count = IpAttempts::getAttemptsCount($ip, "test"); + $this->assertEquals(2, $count); + } + + public function testGetAttemptsCountExpired() { + $ip = "10.0.0.1"; + + IpAttempts::logFailedAttempt($ip, "test", time() - 1); + IpAttempts::logFailedAttempt($ip, "test", time() - 1); + + $count = IpAttempts::getAttemptsCount($ip, "test"); + $this->assertEquals(0, $count); + } + + public function testGetAttemptsCountOneExpired() { + $ip = "10.0.0.1"; + + IpAttempts::logFailedAttempt($ip, "test", time() + 10); + IpAttempts::logFailedAttempt($ip, "test", time() - 1); + + $count = IpAttempts::getAttemptsCount($ip, "test"); + $this->assertEquals(1, $count); + } + + public function testCleanup() { + $ip = "10.0.0.1"; + + IpAttempts::logFailedAttempt($ip, "test", time() - 1); + IpAttempts::logFailedAttempt($ip, "test", time() - 1); + IpAttempts::logFailedAttempt($ip, "test", time() + 3600); + IpAttempts::logFailedAttempt($ip, "test", time() + 3600); + + $query = Db::$pdo->prepare('SELECT count(*) AS count FROM ipAttempts'); + $query->execute(); + $result = $query->fetchAll(); + $beforeCleanup = $result[0]['count']; + + $this->assertEquals(4, $beforeCleanup); + + IpAttempts::cleanupAttempts(); + $query = Db::$pdo->prepare('SELECT count(*) AS count FROM ipAttempts'); + $query->execute(); + $result = $query->fetchAll(); + $afterCleanup = $result[0]['count']; + + $this->assertEquals(2, $afterCleanup); + } + + public function testTrustedIpGetAttempts() { + $ip = "127.0.0.100"; // trusted IP + + IpAttempts::logFailedAttempt($ip, "test", time() + 3600); + IpAttempts::logFailedAttempt($ip, "test", time() + 3600); + + $count = IpAttempts::getAttemptsCount($ip, "test"); + $this->assertEquals(0, $count); + } + + public function testTrustedIpGetAttemptsSkipsDb() { + $ip = "127.0.0.100"; // trusted IP + + IpAttempts::logFailedAttempt($ip, "test", time() + 3600); + IpAttempts::logFailedAttempt($ip, "test", time() + 3600); + + $query = Db::$pdo->prepare('SELECT count(*) AS count FROM ipAttempts'); + $query->execute(); + $result = $query->fetchAll(); + $count = $result[0]['count']; + $this->assertEquals(0, $count); + } +} diff --git a/tests/phpunit/JtiStoreTest.php b/tests/phpunit/JtiStoreTest.php new file mode 100644 index 0000000..602afa4 --- /dev/null +++ b/tests/phpunit/JtiStoreTest.php @@ -0,0 +1,87 @@ +exec($statement); + } + } catch(\PDOException $e) { + echo $e->getMessage(); + } + } + + public function testNonExistingJti() { + $jti = "123"; + $found = JtiStore::hasJti($jti); + $this->assertFalse($found); + } + + public function testExistingJti() { + $jti = "123"; + JtiStore::saveJti($jti, time() + 3600); + $found = JtiStore::hasJti($jti); + $this->assertTrue($found); + } + + public function testExpiredJti() { + $jti = "123"; + JtiStore::saveJti($jti); + $query = Db::$pdo->prepare('UPDATE jti SET expires=:expires WHERE jti=:jti'); + $query->execute([ + 'expires' => time() - 10, + 'jti' => $jti + ]); + $found = JtiStore::hasJti($jti); + $this->assertFalse($found); + } + + public function testCleanup() { + JtiStore::saveJti("123"); + JtiStore::saveJti("234"); + + $query = Db::$pdo->prepare('UPDATE jti SET expires=:expires WHERE jti=:jti'); + $query->execute([ + 'expires' => time() - 10, + 'jti' => "123" + ]); + $query = Db::$pdo->prepare('UPDATE jti SET expires=:expires WHERE jti=:jti'); + $query->execute([ + 'expires' => time() - 10, + 'jti' => "234" + ]); + + $query = Db::$pdo->prepare('SELECT count(*) AS count FROM jti'); + $query->execute(); + $result = $query->fetchAll(); + $beforeCleanup = $result[0]['count']; + $this->assertEquals(2, $beforeCleanup); + + + JtiStore::cleanupJti(); + $query = Db::$pdo->prepare('SELECT count(*) AS count FROM jti'); + $query->execute(); + $result = $query->fetchAll(); + $afterCleanup = $result[0]['count']; + + $this->assertEquals(0, $afterCleanup); + } +} diff --git a/tests/phpunit/PasswordValidatorTest.php b/tests/phpunit/PasswordValidatorTest.php new file mode 100644 index 0000000..9930219 --- /dev/null +++ b/tests/phpunit/PasswordValidatorTest.php @@ -0,0 +1,79 @@ +assertEquals(26, PasswordValidator::getBase($password)); + } + + public function testBaseUpper() + { + $password = "AAA"; + $this->assertEquals(26, PasswordValidator::getBase($password)); + } + + public function testBaseNumbers() + { + $password = "123"; + $this->assertEquals(10, PasswordValidator::getBase($password)); + } + + public function testBaseSpecial() + { + $password = "!@#$"; + $this->assertEquals(32, PasswordValidator::getBase($password)); + } + + public function testBaseUpperAndLower() + { + $password = "aaaAAA"; + $this->assertEquals(52, PasswordValidator::getBase($password)); + } + + public function testLengthLower() + { + $password = "aaa"; + $this->assertEquals(2, PasswordValidator::getLength($password)); + } + + public function testLengthUpper() + { + $password = "AAA"; + $this->assertEquals(2, PasswordValidator::getLength($password)); + } + + public function testLengthNumbers() + { + $password = "123"; + $this->assertEquals(3, PasswordValidator::getLength($password)); + } + + public function testLengthSpecial() + { + $password = "!@#$"; + $this->assertEquals(4, PasswordValidator::getLength($password)); + } + + public function testLengthUpperAndLower() + { + $password = "aaaAAA"; + $this->assertEquals(4, PasswordValidator::getLength($password)); + } + + public function testSimplyEntropy() + { + $values = [ + ["password" => "aaa", "expected" => 6.52], + ["password" => "abc", "expected" => 9.77] + ]; + + foreach ($values as $value) { + $this->assertEquals($value['expected'], PasswordValidator::getEntropy($value['password'])); + } + } +} diff --git a/tests/phpunit/UtilTest.php b/tests/phpunit/UtilTest.php new file mode 100644 index 0000000..c92b71b --- /dev/null +++ b/tests/phpunit/UtilTest.php @@ -0,0 +1,14 @@ +assertEquals($string, $decoded); + } +} diff --git a/tests/phpunit/phpunit.xml b/tests/phpunit/phpunit.xml new file mode 100644 index 0000000..e883c5d --- /dev/null +++ b/tests/phpunit/phpunit.xml @@ -0,0 +1,7 @@ + + + + . + + + diff --git a/tests/phpunit/test-config.php b/tests/phpunit/test-config.php new file mode 100644 index 0000000..c917c6c --- /dev/null +++ b/tests/phpunit/test-config.php @@ -0,0 +1,5 @@ + Date: Sun, 6 Jul 2025 21:47:42 +0200 Subject: [PATCH 02/19] add Db class so we can reuse it in tests --- lib/Db.php | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 lib/Db.php diff --git a/lib/Db.php b/lib/Db.php new file mode 100644 index 0000000..8eb46e1 --- /dev/null +++ b/lib/Db.php @@ -0,0 +1,11 @@ + Date: Sun, 6 Jul 2025 21:48:00 +0200 Subject: [PATCH 03/19] refactor to use db class --- lib/ClientRegistration.php | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/lib/ClientRegistration.php b/lib/ClientRegistration.php index a0c354c..88d98a0 100644 --- a/lib/ClientRegistration.php +++ b/lib/ClientRegistration.php @@ -1,17 +1,12 @@ prepare( + Db::connect(); + $query = Db::$pdo->prepare( 'SELECT clientData FROM clients WHERE clientId=:clientId' ); $query->execute([ @@ -25,11 +20,11 @@ public static function getRegistration($clientId) { } public static function saveClientRegistration($clientData) { - self::connect(); + Db::connect(); if (!isset($clientData['client_name'])) { $clientData['client_name'] = $clientData['origin']; } - $query = self::$pdo->prepare( + $query = Db::$pdo->prepare( 'INSERT INTO clients VALUES(:clientId, :origin, :clientData)' ); $query->execute([ @@ -40,8 +35,8 @@ public static function saveClientRegistration($clientData) { } public static function getClientByOrigin($origin) { - self::connect(); - $query = self::$pdo->prepare( + Db::connect(); + $query = Db::$pdo->prepare( 'SELECT clientData FROM clients WHERE origin=:origin' ); $query->execute([ From 72a1dd038038be58eadb4576e2f224772f33ddfc Mon Sep 17 00:00:00 2001 From: Yvo Brevoort Date: Sun, 6 Jul 2025 21:48:14 +0200 Subject: [PATCH 04/19] refactor to use Db class --- lib/IpAttempts.php | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/lib/IpAttempts.php b/lib/IpAttempts.php index 09b5c1b..360031e 100644 --- a/lib/IpAttempts.php +++ b/lib/IpAttempts.php @@ -1,22 +1,17 @@ prepare( + $query = Db::$pdo->prepare( 'INSERT INTO ipAttempts VALUES(:ip, :type, :expires)' ); $query->execute([ @@ -31,10 +26,10 @@ public static function getAttemptsCount($ip, $type) { return 0; } - self::connect(); + Db::connect(); $now = new \DateTime(); - $query = self::$pdo->prepare( + $query = Db::$pdo->prepare( 'SELECT count(ip) as count FROM ipAttempts WHERE ip=:ip AND type=:type AND expires > :now' ); $query->execute([ @@ -49,10 +44,10 @@ public static function getAttemptsCount($ip, $type) { return 0; } public static function cleanupAttempts() { - self::connect(); + Db::connect(); $now = new \DateTime(); - $query = self::$pdo->prepare( + $query = Db::$pdo->prepare( 'DELETE FROM ipAttempts WHERE expires < :now' ); $query->execute([ From 6f9acd1ad656acf173e0cd433ef20b544cc61540 Mon Sep 17 00:00:00 2001 From: Yvo Brevoort Date: Sun, 6 Jul 2025 21:48:28 +0200 Subject: [PATCH 05/19] use db class, add cleanup --- lib/JtiStore.php | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/lib/JtiStore.php b/lib/JtiStore.php index bbf7861..b487059 100644 --- a/lib/JtiStore.php +++ b/lib/JtiStore.php @@ -1,19 +1,12 @@ prepare( + $query = Db::$pdo->prepare( 'SELECT jti FROM jti WHERE jti=:jti AND expires>:now' ); $query->execute([ @@ -28,8 +21,8 @@ public static function hasJti($jti) { } public static function saveJti($jti) { - self::connect(); - $query = self::$pdo->prepare( + Db::connect(); + $query = Db::$pdo->prepare( 'INSERT INTO jti VALUES(:jti, :expires)' ); $expires = new \DateTime(); @@ -39,4 +32,15 @@ public static function saveJti($jti) { ':expires' => $expires->getTimestamp() ]); } + + public static function cleanupJti() { + Db::connect(); + $now = new \DateTime(); + $query = Db::$pdo->prepare( + 'DELETE FROM jti WHERE expires < :now' + ); + $query->execute([ + ':now' => $now->getTimestamp() + ]); + } } \ No newline at end of file From 5ee939a077c790d4c8cf28968b5d2c0afd23e8d9 Mon Sep 17 00:00:00 2001 From: Yvo Brevoort Date: Sun, 6 Jul 2025 21:48:48 +0200 Subject: [PATCH 06/19] add jti cleanup --- www/idp/index.php | 1 + 1 file changed, 1 insertion(+) diff --git a/www/idp/index.php b/www/idp/index.php index dec7c29..311aecd 100644 --- a/www/idp/index.php +++ b/www/idp/index.php @@ -427,6 +427,7 @@ touch(CLEANUP_FILE, time() + 3600); User::cleanupTokens(); IpAttempts::cleanupAttempts(); + JtiStore::cleanupJti(); } break; case "OPTIONS": From 290e72242a7325c672dc0e769dbdbc7f68a30a7f Mon Sep 17 00:00:00 2001 From: Yvo Brevoort Date: Sun, 6 Jul 2025 22:19:18 +0200 Subject: [PATCH 07/19] add php CI workflow --- .github/workflows/php.yml | 134 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 .github/workflows/php.yml diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml new file mode 100644 index 0000000..f023969 --- /dev/null +++ b/.github/workflows/php.yml @@ -0,0 +1,134 @@ +--- +name: PHP Quality Assistance + +on: + # This event occurs when there is activity on a pull request. The workflow + # will be run against the commits, after merge to the target branch (main). + pull_request: + paths: + - '**.php' + - '.config/phpcs.xml.dist' + - 'tests/phpunit/phpunit.xml' + - '.github/workflows/php.yml' + - 'composer.json' + - 'composer.lock' + branches: [ main, feature/php-ci ] + types: [ opened, reopened, synchronize ] + # This event occurs when there is a push to the repository. + push: + paths: + - '**.php' + - '.config/phpcs.xml.dist' + - 'tests/phpunit/phpunit.xml' + - '.github/workflows/php.yml' + - 'composer.json' + - 'composer.lock' + # Allow manually triggering the workflow. + workflow_dispatch: + + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + # Needed to allow the "concurrency" section to cancel a workflow run. + actions: write + +jobs: + # 01.preflight.php.lint-syntax.yml + lint-php-syntax: + name: PHP Syntax Linting + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - uses: docker://pipelinecomponents/php-linter + with: + args: >- + parallel-lint + --exclude .git + --exclude vendor + --no-progress + . + # 01.quality.php.validate.dependencies-file.yml + validate-dependencies-file: + name: Validate dependencies file + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - run: >- + composer validate + --check-lock + --no-plugins + --no-scripts + --strict + # 02.test.php.test-unit.yml + php-unittest: + name: PHP Unit Tests + needs: + - lint-php-syntax + - validate-dependencies-file + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + php: + - '8.1' # from 2021-11 to 2023-11 (2025-12) + - '8.2' # from 2022-12 to 2024-12 (2026-12) + - '8.3' # from 2023-11 to 2025-12 (2027-12) + steps: + - uses: actions/checkout@v4 + - uses: shivammathur/setup-php@v2 + with: + coverage: xdebug + ini-values: error_reporting=E_ALL, display_errors=On + php-version: ${{ matrix.php }} + - name: Install and Cache Composer dependencies + uses: "ramsey/composer-install@v2" + env: + COMPOSER_AUTH: '{"github-oauth": {"github.com": "${{ secrets.GITHUB_TOKEN }}"}}' + - run: bin/phpunit --configuration tests/phpunit/phpunit.xml + # 03.quality.php.scan.dependencies-vulnerabilities.yml + scan-dependencies-vulnerabilities: + name: Scan Dependencies Vulnerabilities + needs: + - validate-dependencies-file + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - name: Install and Cache Composer dependencies + uses: "ramsey/composer-install@v2" + env: + COMPOSER_AUTH: '{"github-oauth": {"github.com": "${{ secrets.GITHUB_TOKEN }}"}}' + - run: >- + composer audit + --abandoned=report + --no-dev + --no-plugins + --no-scripts + # 03.quality.php.lint-version-compatibility.yml + php-check-version-compatibility: + name: PHP Version Compatibility + needs: + - lint-php-syntax + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + php: + - '8.1' # from 2021-11 to 2023-11 (2025-12) + - '8.2' # from 2022-12 to 2024-12 (2026-12) + - '8.3' # from 2023-11 to 2025-12 (2027-12) + steps: + - uses: actions/checkout@v4 + - uses: docker://pipelinecomponents/php-codesniffer + with: + args: >- + phpcs + -s + --extensions=php + --ignore='*vendor/*' + --runtime-set testVersion ${{ matrix.php }} + --standard=PHPCompatibility + . From 453d80e34a4d24862e60b628e9a901bc91a9dcea Mon Sep 17 00:00:00 2001 From: Yvo Brevoort Date: Sun, 6 Jul 2025 22:22:29 +0200 Subject: [PATCH 08/19] remove validate-deps (complains about the exact version number for easyrdf) --- .github/workflows/php.yml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index f023969..1532a9d 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -51,18 +51,18 @@ jobs: --exclude vendor --no-progress . - # 01.quality.php.validate.dependencies-file.yml - validate-dependencies-file: - name: Validate dependencies file - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@v4 - - run: >- - composer validate - --check-lock - --no-plugins - --no-scripts - --strict +# # 01.quality.php.validate.dependencies-file.yml +# validate-dependencies-file: +# name: Validate dependencies file +# runs-on: ubuntu-24.04 +# steps: +# - uses: actions/checkout@v4 +# - run: >- +# composer validate +# --check-lock +# --no-plugins +# --no-scripts +# --strict # 02.test.php.test-unit.yml php-unittest: name: PHP Unit Tests From 09560eb04dfe4fd98d327aae33d16a23e5f13b44 Mon Sep 17 00:00:00 2001 From: Yvo Brevoort Date: Sun, 6 Jul 2025 22:24:02 +0200 Subject: [PATCH 09/19] skip depending things --- .github/workflows/php.yml | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 1532a9d..3a3adfa 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -68,7 +68,7 @@ jobs: name: PHP Unit Tests needs: - lint-php-syntax - - validate-dependencies-file +# - validate-dependencies-file runs-on: ubuntu-24.04 strategy: fail-fast: false @@ -90,23 +90,23 @@ jobs: COMPOSER_AUTH: '{"github-oauth": {"github.com": "${{ secrets.GITHUB_TOKEN }}"}}' - run: bin/phpunit --configuration tests/phpunit/phpunit.xml # 03.quality.php.scan.dependencies-vulnerabilities.yml - scan-dependencies-vulnerabilities: - name: Scan Dependencies Vulnerabilities - needs: - - validate-dependencies-file - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@v4 - - name: Install and Cache Composer dependencies - uses: "ramsey/composer-install@v2" - env: - COMPOSER_AUTH: '{"github-oauth": {"github.com": "${{ secrets.GITHUB_TOKEN }}"}}' - - run: >- - composer audit - --abandoned=report - --no-dev - --no-plugins - --no-scripts +# scan-dependencies-vulnerabilities: +# name: Scan Dependencies Vulnerabilities +# needs: +# - validate-dependencies-file +# runs-on: ubuntu-24.04 +# steps: +# - uses: actions/checkout@v4 +# - name: Install and Cache Composer dependencies +# uses: "ramsey/composer-install@v2" +# env: +# COMPOSER_AUTH: '{"github-oauth": {"github.com": "${{ secrets.GITHUB_TOKEN }}"}}' +# - run: >- +# composer audit +# --abandoned=report +# --no-dev +# --no-plugins +# --no-scripts # 03.quality.php.lint-version-compatibility.yml php-check-version-compatibility: name: PHP Version Compatibility From 6bbd0164d0c6f526204cd2aded653262fbd81301 Mon Sep 17 00:00:00 2001 From: Yvo Brevoort Date: Sun, 6 Jul 2025 22:35:26 +0200 Subject: [PATCH 10/19] broader versions of phpunit --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index d0e1836..1ae808f 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,7 @@ "phrity/websocket": "^3.5" }, "require-dev": { - "phpunit/phpunit": "^12.2", + "phpunit/phpunit": "^9 || ^10 || ^11 || ^12", "phpstan/phpstan": "^2.1" } } From 4a05dde57b25c63474ddffbe5118b53e533613e9 Mon Sep 17 00:00:00 2001 From: Yvo Brevoort Date: Sun, 6 Jul 2025 22:37:55 +0200 Subject: [PATCH 11/19] prefix vendor --- .github/workflows/php.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 3a3adfa..544a6dd 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -88,7 +88,7 @@ jobs: uses: "ramsey/composer-install@v2" env: COMPOSER_AUTH: '{"github-oauth": {"github.com": "${{ secrets.GITHUB_TOKEN }}"}}' - - run: bin/phpunit --configuration tests/phpunit/phpunit.xml + - run: vendor/bin/phpunit --configuration tests/phpunit/phpunit.xml # 03.quality.php.scan.dependencies-vulnerabilities.yml # scan-dependencies-vulnerabilities: # name: Scan Dependencies Vulnerabilities From 313d7b21cbaa44e8d8b6b524a0a2e63272481c1a Mon Sep 17 00:00:00 2001 From: Yvo Brevoort Date: Sun, 6 Jul 2025 22:44:30 +0200 Subject: [PATCH 12/19] restore skipped checks --- .github/workflows/php.yml | 58 +++++++++++++++++++-------------------- composer.json | 3 +- 2 files changed, 31 insertions(+), 30 deletions(-) diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 544a6dd..85c820c 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -52,23 +52,23 @@ jobs: --no-progress . # # 01.quality.php.validate.dependencies-file.yml -# validate-dependencies-file: -# name: Validate dependencies file -# runs-on: ubuntu-24.04 -# steps: -# - uses: actions/checkout@v4 -# - run: >- -# composer validate -# --check-lock -# --no-plugins -# --no-scripts -# --strict + validate-dependencies-file: + name: Validate dependencies file + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - run: >- + composer validate + --check-lock + --no-plugins + --no-scripts + --strict # 02.test.php.test-unit.yml php-unittest: name: PHP Unit Tests needs: - lint-php-syntax -# - validate-dependencies-file + - validate-dependencies-file runs-on: ubuntu-24.04 strategy: fail-fast: false @@ -90,23 +90,23 @@ jobs: COMPOSER_AUTH: '{"github-oauth": {"github.com": "${{ secrets.GITHUB_TOKEN }}"}}' - run: vendor/bin/phpunit --configuration tests/phpunit/phpunit.xml # 03.quality.php.scan.dependencies-vulnerabilities.yml -# scan-dependencies-vulnerabilities: -# name: Scan Dependencies Vulnerabilities -# needs: -# - validate-dependencies-file -# runs-on: ubuntu-24.04 -# steps: -# - uses: actions/checkout@v4 -# - name: Install and Cache Composer dependencies -# uses: "ramsey/composer-install@v2" -# env: -# COMPOSER_AUTH: '{"github-oauth": {"github.com": "${{ secrets.GITHUB_TOKEN }}"}}' -# - run: >- -# composer audit -# --abandoned=report -# --no-dev -# --no-plugins -# --no-scripts + scan-dependencies-vulnerabilities: + name: Scan Dependencies Vulnerabilities + needs: + - validate-dependencies-file + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - name: Install and Cache Composer dependencies + uses: "ramsey/composer-install@v2" + env: + COMPOSER_AUTH: '{"github-oauth": {"github.com": "${{ secrets.GITHUB_TOKEN }}"}}' + - run: >- + composer audit + --abandoned=report + --no-dev + --no-plugins + --no-scripts # 03.quality.php.lint-version-compatibility.yml php-check-version-compatibility: name: PHP Version Compatibility diff --git a/composer.json b/composer.json index 1ae808f..1b4d9e0 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,6 @@ { "name": "pdsinterop/php-solid", + "description": "Multi-user Solid Server for PHP", "type": "project", "license": "MIT", "autoload": { @@ -17,7 +18,7 @@ "pdsinterop/solid-auth": "v0.13.0", "pdsinterop/solid-crud": "v0.8.1", "phpmailer/phpmailer": "^6.10", - "sweetrdf/easyrdf": "v1.15", + "sweetrdf/easyrdf": "~1.15.0", "phpseclib/bcmath_compat": "^2.0", "phrity/websocket": "^3.5" }, From 686b26c03458e04b5c97935fcbcddad3aeb5bc57 Mon Sep 17 00:00:00 2001 From: Yvo Brevoort Date: Sun, 6 Jul 2025 22:47:22 +0200 Subject: [PATCH 13/19] add workflows for json, markdown, yaml --- .github/workflows/json.yml | 46 ++++++++++++++++++++++++++++++++++ .github/workflows/markdown.yml | 42 +++++++++++++++++++++++++++++++ .github/workflows/yaml.yml | 42 +++++++++++++++++++++++++++++++ 3 files changed, 130 insertions(+) create mode 100644 .github/workflows/json.yml create mode 100644 .github/workflows/markdown.yml create mode 100644 .github/workflows/yaml.yml diff --git a/.github/workflows/json.yml b/.github/workflows/json.yml new file mode 100644 index 0000000..7e83269 --- /dev/null +++ b/.github/workflows/json.yml @@ -0,0 +1,46 @@ +--- +name: JSON Quality Assistance + +on: + # This event occurs when there is activity on a pull request. The workflow + # will be run against the commits, after merge to the target branch (main). + pull_request: + branches: [ main ] + paths: + - '**.json' + - '.github/workflows/json.yml' + types: [ opened, reopened, synchronize ] + # This event occurs when there is a push to the repository. + push: + paths: + - '**.json' + - '.github/workflows/json.yml' + # Allow manually triggering the workflow. + workflow_dispatch: + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + # Needed to allow the "concurrency" section to cancel a workflow run. + actions: write + +jobs: + # 01.preflight.json.lint-syntax.yml + lint-json-syntax: + name: JSON Syntax Linting + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - uses: docker://pipelinecomponents/jsonlint + with: + args: >- + find . + -not -path '*/.git/*' + -not -path '*/node_modules/*' + -not -path '*/vendor/*' + -name '*.json' + -type f + -exec jsonlint --quiet {} ; diff --git a/.github/workflows/markdown.yml b/.github/workflows/markdown.yml new file mode 100644 index 0000000..581b9c7 --- /dev/null +++ b/.github/workflows/markdown.yml @@ -0,0 +1,42 @@ +--- +name: Markdown Quality Assistance + +on: + # This event occurs when there is activity on a pull request. The workflow + # will be run against the commits, after merge to the target branch (main). + pull_request: + branches: [ main ] + paths: + - '**.md' + - '.github/workflows/markdown.yml' + types: [ opened, reopened, synchronize ] + # This event occurs when there is a push to the repository. + push: + paths: + - '**.md' + - '.github/workflows/markdown.yml' + # Allow manually triggering the workflow. + workflow_dispatch: + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + # Needed to allow the "concurrency" section to cancel a workflow run. + actions: write + +jobs: + # 01.quality.markdown.lint-syntax.yml + lint-markdown-syntax: + name: Markdown Linting + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - uses: docker://pipelinecomponents/remark-lint + with: + args: >- + remark + --rc-path=.config/.remarkrc + --ignore-pattern='*/vendor/*' diff --git a/.github/workflows/yaml.yml b/.github/workflows/yaml.yml new file mode 100644 index 0000000..ad8fb9d --- /dev/null +++ b/.github/workflows/yaml.yml @@ -0,0 +1,42 @@ +--- +name: YAML Quality Assistance + +on: + # This event occurs when there is activity on a pull request. The workflow + # will be run against the commits, after merge to the target branch (main). + pull_request: + branches: [ main ] + paths: + - '**.yml' + - '**.yaml' + types: [ opened, reopened, synchronize ] + # This event occurs when there is a push to the repository. + push: + paths: + - '**.yml' + - '**.yaml' + # Allow manually triggering the workflow. + workflow_dispatch: + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + # Needed to allow the "concurrency" section to cancel a workflow run. + actions: write + +jobs: + # 01.preflight.yaml.lint.yml + lint-yaml: + name: YAML Linting + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - uses: docker://pipelinecomponents/yamllint + with: + args: >- + yamllint + --config-file=.config/.yamllint + . From 3cab2208320363f726980ddac213d5ab4add6508 Mon Sep 17 00:00:00 2001 From: Yvo Brevoort Date: Sun, 6 Jul 2025 22:49:46 +0200 Subject: [PATCH 14/19] add configs --- .config/.remarkrc | 6 ++++++ .config/.yamllint | 14 ++++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 .config/.remarkrc create mode 100644 .config/.yamllint diff --git a/.config/.remarkrc b/.config/.remarkrc new file mode 100644 index 0000000..bfa065d --- /dev/null +++ b/.config/.remarkrc @@ -0,0 +1,6 @@ +{ + "plugins": [ + "remark-preset-lint-recommended", + ["remark-lint-list-item-indent", "space"] + ] +} diff --git a/.config/.yamllint b/.config/.yamllint new file mode 100644 index 0000000..7c1b6e4 --- /dev/null +++ b/.config/.yamllint @@ -0,0 +1,14 @@ +--- +extends: default + +ignore: | + vendor/ + +rules: + brackets: + max-spaces-inside: 1 + document-start: disable + line-length: + level: warning + max: 120 + truthy: {allowed-values: ["true", "false", "on"]} From 3e792bcc0948bd97981912ccf9002f9e9326174a Mon Sep 17 00:00:00 2001 From: Yvo Brevoort Date: Sun, 6 Jul 2025 23:23:22 +0200 Subject: [PATCH 15/19] use jtistore --- www/idp/index.php | 1 + 1 file changed, 1 insertion(+) diff --git a/www/idp/index.php b/www/idp/index.php index 311aecd..e8d3b94 100644 --- a/www/idp/index.php +++ b/www/idp/index.php @@ -12,6 +12,7 @@ use Pdsinterop\PhpSolid\User; use Pdsinterop\PhpSolid\Mailer; use Pdsinterop\PhpSolid\IpAttempts; + use Pdsinterop\PhpSolid\JtiStore; $request = explode("?", $_SERVER['REQUEST_URI'], 2)[0]; $method = $_SERVER['REQUEST_METHOD']; From 70598345c932d16d8ffa5ee94f68522a34d14143 Mon Sep 17 00:00:00 2001 From: Yvo Brevoort Date: Sun, 6 Jul 2025 23:28:21 +0200 Subject: [PATCH 16/19] refactor to use Db --- lib/User.php | 75 ++++++++++++++++++++++++---------------------------- 1 file changed, 35 insertions(+), 40 deletions(-) diff --git a/lib/User.php b/lib/User.php index 66c5505..22a2527 100644 --- a/lib/User.php +++ b/lib/User.php @@ -2,15 +2,9 @@ namespace Pdsinterop\PhpSolid; use Pdsinterop\PhpSolid\PasswordValidator; - + use Pdsinterop\PhpSolid\Db; + class User { - private static $pdo; - private static function connect() { - if (!isset(self::$pdo)) { - self::$pdo = new \PDO("sqlite:" . DBPATH); - } - } - private static function generateTokenCode() { $digits = 6; $code = random_int(0,1000000); @@ -42,8 +36,8 @@ public static function saveVerifyToken($tokenType, $tokenData) { break; } - self::connect(); - $query = self::$pdo->prepare( + Db::connect(); + $query = Db::$pdo->prepare( 'INSERT INTO verify VALUES(:code, :data)' ); $query->execute([ @@ -54,8 +48,8 @@ public static function saveVerifyToken($tokenType, $tokenData) { } public static function getVerifyToken($code) { - self::connect(); - $query = self::$pdo->prepare( + Db::connect(); + $query = Db::$pdo->prepare( 'SELECT data FROM verify WHERE code=:code' ); $query->execute([ @@ -84,13 +78,14 @@ public static function validatePasswordStrength($password) { $entropy = PasswordValidator::getEntropy($password, BANNED_PASSWORDS); $minimumEntropy = MINIMUM_PASSWORD_ENTROPY; if ($entropy < $minimumEntropy) { + error_log("Entered pasword does not satisfy minimum entropy"); return false; } return true; } public static function createUser($newUser) { - self::connect(); + Db::connect(); if (!self::validatePasswordStrength($newUser['password'])) { return false; } @@ -98,7 +93,7 @@ public static function createUser($newUser) { while (self::userIdExists($generatedUserId)) { $generatedUserId = md5(random_bytes(32)); } - $query = self::$pdo->prepare( + $query = Db::$pdo->prepare( 'INSERT INTO users VALUES (:userId, :email, :passwordHash, :data)' ); @@ -126,8 +121,8 @@ public static function setUserPassword($email, $newPassword) { if (!self::validatePasswordStrength($newPassword)) { return false; } - self::connect(); - $query = self::$pdo->prepare( + Db::connect(); + $query = Db::$pdo->prepare( 'UPDATE users SET password=:passwordHash WHERE email=:email' ); $queryParams = []; @@ -139,8 +134,8 @@ public static function setUserPassword($email, $newPassword) { } public static function allowClientForUser($clientId, $userId) { - self::connect(); - $query = self::$pdo->prepare( + Db::connect(); + $query = Db::$pdo->prepare( 'INSERT OR REPLACE INTO allowedClients VALUES(:userId, :clientId)' ); $query->execute([ @@ -151,8 +146,8 @@ public static function allowClientForUser($clientId, $userId) { } public static function getAllowedClients($userId) { - self::connect(); - $query = self::$pdo->prepare( + Db::connect(); + $query = Db::$pdo->prepare( 'SELECT clientId FROM allowedClients WHERE userId=:userId' ); $query->execute([ @@ -166,8 +161,8 @@ public static function getAllowedClients($userId) { } public static function getStorage($userId) { - self::connect(); - $query = self::$pdo->prepare( + Db::connect(); + $query = Db::$pdo->prepare( 'SELECT storageUrl FROM userStorage WHERE userId=:userId' ); $query->execute([ @@ -181,8 +176,8 @@ public static function getStorage($userId) { } public static function setStorage($userId, $storageUrl) { - self::connect(); - $query = self::$pdo->prepare( + Db::connect(); + $query = Db::$pdo->prepare( 'INSERT OR REPLACE INTO storage VALUES(:userId, :storageUrl)' ); $query->execute([ @@ -192,8 +187,8 @@ public static function setStorage($userId, $storageUrl) { } public static function getUser($email) { - self::connect(); - $query = self::$pdo->prepare( + Db::connect(); + $query = Db::$pdo->prepare( 'SELECT user_id, data FROM users WHERE email=:email' ); $query->execute([ @@ -217,8 +212,8 @@ public static function getUser($email) { } public static function getUserById($userId) { - self::connect(); - $query = self::$pdo->prepare( + Db::connect(); + $query = Db::$pdo->prepare( 'SELECT user_id, data FROM users WHERE user_id=:userId' ); $query->execute([ @@ -242,8 +237,8 @@ public static function getUserById($userId) { } public static function checkPassword($email, $password) { - self::connect(); - $query = self::$pdo->prepare( + Db::connect(); + $query = Db::$pdo->prepare( 'SELECT password FROM users WHERE email=:email' ); $query->execute([ @@ -271,8 +266,8 @@ public static function getLoggedInUser() { } public static function userIdExists($userId) { - self::connect(); - $query = self::$pdo->prepare( + Db::connect(); + $query = Db::$pdo->prepare( 'SELECT user_id FROM users WHERE user_id=:userId' ); $query->execute([ @@ -286,8 +281,8 @@ public static function userIdExists($userId) { } public static function userEmailExists($email) { - self::connect(); - $query = self::$pdo->prepare( + Db::connect(); + $query = Db::$pdo->prepare( 'SELECT user_id FROM users WHERE email=:email' ); $query->execute([ @@ -301,8 +296,8 @@ public static function userEmailExists($email) { } private static function deleteUser($email) { - self::connect(); - $query = self::$pdo->prepare( + Db::connect(); + $query = Db::$pdo->prepare( 'DELETE FROM users WHERE email=:email' ); $query->execute([ @@ -316,8 +311,8 @@ private static function deleteAllowedClients($email) { return; } - self::connect(); - $query = self::$pdo->prepare( + Db::connect(); + $query = Db::$pdo->prepare( 'DELETE FROM allowedClients WHERE userId=:userId' ); $query->execute([ @@ -335,10 +330,10 @@ public static function deleteAccount($email) { } public static function cleanupTokens() { - self::connect(); + Db::connect(); $now = new \DateTime(); - $query = self::$pdo->prepare( + $query = Db::$pdo->prepare( 'DELETE FROM verify WHERE json_extract(data, \'$.expires\') < :now' ); $query->execute([ From 8623b4f75f9d42069f19960145efd7534bf56adb Mon Sep 17 00:00:00 2001 From: Yvo Brevoort Date: Sun, 6 Jul 2025 23:28:51 +0200 Subject: [PATCH 17/19] set expires to text not null --- init.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/init.php b/init.php index 8bdba37..52d3d89 100644 --- a/init.php +++ b/init.php @@ -32,7 +32,7 @@ function initDatabase() { )', 'CREATE TABLE IF NOT EXISTS jti ( jti VARCHAR(255) NOT NULL PRIMARY KEY, - expires TEXT + expires TEXT NOT NULL )', 'CREATE TABLE IF NOT EXISTS users ( user_id VARCHAR(255) NOT NULL PRIMARY KEY, @@ -43,7 +43,7 @@ function initDatabase() { 'CREATE TABLE IF NOT EXISTS ipAttempts ( ip VARCHAR(255) NOT NULL, type VARCHAR(255) NOT NULL, - expires NOT NULL + expires TEXT NOT NULL )', ]; From 788ae5ab422fddc19e942cdf02d0107cf65bf02e Mon Sep 17 00:00:00 2001 From: Yvo Brevoort Date: Sun, 6 Jul 2025 23:29:42 +0200 Subject: [PATCH 18/19] update TODO, log reasons for bad request --- TODO | 19 ++++++++++++++++++- www/idp/index.php | 5 +++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/TODO b/TODO index fb86193..0e4659a 100644 --- a/TODO +++ b/TODO @@ -57,4 +57,21 @@ - [v] webid - [v] wac - [v] solid-crud -- [v] CI integration \ No newline at end of file +- [v] CI integration + +------ Unit tests ----- +- [v] ClientRegistration +- [v] JtiStore +- [v] IpAttempts +- [v] Util +- [v] PasswordValidator +- [ ] Mailer +- [ ] MailTemplateGenerator +- [ ] MailTemplates +- [ ] Server +- [ ] SolidNotifications +- [ ] SolidPubSub +- [ ] StorageServer +- [ ] User +- [-] Middleware +- [-] Db diff --git a/www/idp/index.php b/www/idp/index.php index e8d3b94..ca611d6 100644 --- a/www/idp/index.php +++ b/www/idp/index.php @@ -199,18 +199,22 @@ case "/api/accounts/new/": $verifyToken = User::getVerifyToken($_POST['confirm']); if (!$verifyToken) { + error_log("No verify token sent"); header("HTTP/1.1 400 Bad Request"); exit(); } if ($verifyToken['email'] !== $_POST['email']) { + error_log("Verify token does not match email"); header("HTTP/1.1 400 Bad Request"); exit(); } if (User::userEmailExists($_POST['email'])) { + error_log("Account already exists"); header("HTTP/1.1 400 Bad Request"); exit(); } if (!$_POST['password'] === $_POST['repeat_password']) { + error_log("Password repeat does not match"); header("HTTP/1.1 400 Bad Request"); exit(); } @@ -222,6 +226,7 @@ $createdUser = User::createUser($newUser); if (!$createdUser) { + error_log("Failed to create user"); header("HTTP/1.1 400 Bad Request"); exit(); } From afd0be24a6182c2376df2a54f35c239073a12570 Mon Sep 17 00:00:00 2001 From: Yvo Brevoort Date: Sun, 6 Jul 2025 23:36:08 +0200 Subject: [PATCH 19/19] remove cache file --- tests/phpunit/.phpunit.result.cache | 1 - 1 file changed, 1 deletion(-) delete mode 100644 tests/phpunit/.phpunit.result.cache diff --git a/tests/phpunit/.phpunit.result.cache b/tests/phpunit/.phpunit.result.cache deleted file mode 100644 index 391632e..0000000 --- a/tests/phpunit/.phpunit.result.cache +++ /dev/null @@ -1 +0,0 @@ -{"version":1,"defects":{"Pdsinterop\\PhpSolid\\Tests\\ClientRegistrationTest::testSimpleCreationg":8,"Pdsinterop\\PhpSolid\\ClientRegistrationTest::testSimpleCreationg":8,"Pdsinterop\\PhpSolid\\Tests\\ClientRegistrationTest::testSimpleCreation":5,"Pdsinterop\\PhpSolid\\Tests\\ClientRegistrationTest::testGetRegistration":8,"Pdsinterop\\PhpSolid\\Tests\\ClientRegistrationTest::testClientNameAutofill":7,"Pdsinterop\\PhpSolid\\Tests\\IpAttemptsTest::testGetAttemptsCountZero":8,"Pdsinterop\\PhpSolid\\Tests\\IpAttemptsTest::testGetAttemptsCountOne":8,"Pdsinterop\\PhpSolid\\Tests\\IpAttemptsTest::testGetAttemptsCountTwo":7,"Pdsinterop\\PhpSolid\\Tests\\IpAttemptsTest::testGetAttemptsCountExpired":7,"Pdsinterop\\PhpSolid\\Tests\\IpAttemptsTest::testCleanup":7,"Pdsinterop\\PhpSolid\\Tests\\UtilTest::testBase64EncodeDecode":7,"Pdsinterop\\PhpSolid\\Tests\\JtiTest::testNonExistingJti":8,"Pdsinterop\\PhpSolid\\Tests\\JtiTest::testExistingJti":8,"Pdsinterop\\PhpSolid\\Tests\\JtiTest::testExpiredJti":8,"Pdsinterop\\PhpSolid\\Tests\\JtiTest::testCleanup":8,"Pdsinterop\\PhpSolid\\Tests\\JtiStoreTest::testExpiredJti":8,"Pdsinterop\\PhpSolid\\Tests\\JtiStoreTest::testCleanup":7},"times":{"Pdsinterop\\PhpSolid\\Tests\\BaseTest::testBaseLower":0.001,"Pdsinterop\\PhpSolid\\Tests\\BaseTest::testBaseUpper":0,"Pdsinterop\\PhpSolid\\Tests\\BaseTest::testBaseNumbers":0,"Pdsinterop\\PhpSolid\\Tests\\BaseTest::testBaseSpecial":0,"Pdsinterop\\PhpSolid\\Tests\\BaseTest::testBaseUpperAndLower":0,"Pdsinterop\\PhpSolid\\Tests\\EntropyTest::testSimplyEntropy":0,"Pdsinterop\\PhpSolid\\Tests\\LengthTest::testLengthLower":0,"Pdsinterop\\PhpSolid\\Tests\\LengthTest::testLengthUpper":0,"Pdsinterop\\PhpSolid\\Tests\\LengthTest::testLengthNumbers":0,"Pdsinterop\\PhpSolid\\Tests\\LengthTest::testLengthSpecial":0,"Pdsinterop\\PhpSolid\\Tests\\LengthTest::testLengthUpperAndLower":0,"Pdsinterop\\PhpSolid\\Tests\\PasswordValidatorTest::testBaseLower":0,"Pdsinterop\\PhpSolid\\Tests\\PasswordValidatorTest::testBaseUpper":0,"Pdsinterop\\PhpSolid\\Tests\\PasswordValidatorTest::testBaseNumbers":0,"Pdsinterop\\PhpSolid\\Tests\\PasswordValidatorTest::testBaseSpecial":0,"Pdsinterop\\PhpSolid\\Tests\\PasswordValidatorTest::testBaseUpperAndLower":0,"Pdsinterop\\PhpSolid\\Tests\\PasswordValidatorTest::testLengthLower":0,"Pdsinterop\\PhpSolid\\Tests\\PasswordValidatorTest::testLengthUpper":0,"Pdsinterop\\PhpSolid\\Tests\\PasswordValidatorTest::testLengthNumbers":0,"Pdsinterop\\PhpSolid\\Tests\\PasswordValidatorTest::testLengthSpecial":0,"Pdsinterop\\PhpSolid\\Tests\\PasswordValidatorTest::testLengthUpperAndLower":0,"Pdsinterop\\PhpSolid\\Tests\\PasswordValidatorTest::testSimplyEntropy":0,"Pdsinterop\\PhpSolid\\Tests\\ClientRegistrationTest::testSimpleCreationg":0.002,"Pdsinterop\\PhpSolid\\ClientRegistrationTest::testSimpleCreationg":0.002,"Pdsinterop\\PhpSolid\\Tests\\ClientRegistrationTest::testSimpleCreation":0.001,"Pdsinterop\\PhpSolid\\Tests\\ClientRegistrationTest::testGetRegistration":0,"Pdsinterop\\PhpSolid\\Tests\\ClientRegistrationTest::testClientNameAutofill":0,"Pdsinterop\\PhpSolid\\Tests\\ClientRegistrationTest::testClientByOrigin":0,"Pdsinterop\\PhpSolid\\Tests\\ClientRegistrationTest::testClientByDuplicateOrigin":0,"Pdsinterop\\PhpSolid\\Tests\\IpAttemptsTest::testGetAttemptsCountZero":0.001,"Pdsinterop\\PhpSolid\\Tests\\IpAttemptsTest::testGetAttemptsCountOne":0,"Pdsinterop\\PhpSolid\\Tests\\IpAttemptsTest::testGetAttemptsCountTwo":0,"Pdsinterop\\PhpSolid\\Tests\\IpAttemptsTest::testGetAttemptsCountExpired":0,"Pdsinterop\\PhpSolid\\Tests\\IpAttemptsTest::testGetAttemptsCountOneExpired":0,"Pdsinterop\\PhpSolid\\Tests\\IpAttemptsTest::testCleanup":0,"Pdsinterop\\PhpSolid\\Tests\\IpAttemptsTest::testTrustedIpGetAttemptsCountTwo":0,"Pdsinterop\\PhpSolid\\Tests\\IpAttemptsTest::testTrustedIpGetAttempts":0,"Pdsinterop\\PhpSolid\\Tests\\IpAttemptsTest::testTrustedIpGetAttemptsSkipsDb":0,"Pdsinterop\\PhpSolid\\Tests\\UtilTest::testBase64EncodeDecode":0,"Pdsinterop\\PhpSolid\\Tests\\JtiTest::testNonExistingJti":0.002,"Pdsinterop\\PhpSolid\\Tests\\JtiTest::testExistingJti":0,"Pdsinterop\\PhpSolid\\Tests\\JtiTest::testExpiredJti":0,"Pdsinterop\\PhpSolid\\Tests\\JtiTest::testCleanup":0,"Pdsinterop\\PhpSolid\\Tests\\JtiStoreTest::testNonExistingJti":0,"Pdsinterop\\PhpSolid\\Tests\\JtiStoreTest::testExistingJti":0,"Pdsinterop\\PhpSolid\\Tests\\JtiStoreTest::testExpiredJti":0,"Pdsinterop\\PhpSolid\\Tests\\JtiStoreTest::testCleanup":0}} \ No newline at end of file