From 828cdafe85b088970fc64b390461bd994feff69b Mon Sep 17 00:00:00 2001 From: YvesCesar Date: Thu, 1 Oct 2026 18:08:57 -0400 Subject: [PATCH 1/2] fix: keep the wordpress hash when it already matches the new password Signed-off-by: YvesCesar --- .../BeforePasswordUpdatedListener.php | 12 ++- .../BeforePasswordUpdatedListenerTest.php | 81 +++++++++++++++++++ tests/bootstrap.php | 44 ++++++++++ 3 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 tests/Unit/Listener/BeforePasswordUpdatedListenerTest.php diff --git a/lib/Listener/BeforePasswordUpdatedListener.php b/lib/Listener/BeforePasswordUpdatedListener.php index 7a465b7..80f11ef 100644 --- a/lib/Listener/BeforePasswordUpdatedListener.php +++ b/lib/Listener/BeforePasswordUpdatedListener.php @@ -39,14 +39,24 @@ public function handle(Event $event): void { return; } $hashPassword = new HashPassword(); - $hash = $hashPassword->hashPassword($event->getPassword()); $username = $event->getUser()->getUID(); + if ($hashPassword->validate($event->getPassword(), $this->getWordPressHash($db, $username))) { + return; + } + $hash = $hashPassword->hashPassword($event->getPassword()); $statement = $db->prepare($this->queryConfig->setWordPressPasswordQuery()); $statement->bindValue(':username', $username); $statement->bindValue(':hash', $hash); $statement->execute(); } + private function getWordPressHash(PDO $db, string $username): string { + $statement = $db->prepare($this->queryConfig->getWordPressUserQuery()); + $statement->execute(['username' => $username]); + $row = $statement->fetch(PDO::FETCH_ASSOC); + return $row === false ? '' : (string)$row['password']; + } + private function getDatabase(): ?PDO { if (!$this->dsn) { return null; diff --git a/tests/Unit/Listener/BeforePasswordUpdatedListenerTest.php b/tests/Unit/Listener/BeforePasswordUpdatedListenerTest.php new file mode 100644 index 0000000..82118db --- /dev/null +++ b/tests/Unit/Listener/BeforePasswordUpdatedListenerTest.php @@ -0,0 +1,81 @@ +databaseFile = tempnam(sys_get_temp_dir(), 'wordpress'); + $this->database = new PDO('sqlite:' . $this->databaseFile); + $this->database->exec('CREATE TABLE wp_users (ID INTEGER PRIMARY KEY, user_login TEXT, user_email TEXT, user_pass TEXT, display_name TEXT)'); + $this->database->exec('CREATE TABLE wp_wc_orders (customer_id INTEGER, status TEXT, type TEXT)'); + } + + protected function tearDown(): void { + unlink($this->databaseFile); + } + + public function testWritesTheNewPasswordToWordPress(): void { + $this->createWordPressUser('ana', (new HashPassword())->hashPassword('old password')); + + $this->changePassword('ana', 'new password'); + + $this->assertTrue((new HashPassword())->validate('new password', $this->wordPressHashOf('ana'))); + } + + public function testKeepsTheWordPressHashThatAlreadyMatchesTheNewPassword(): void { + $hash = (new HashPassword())->hashPassword('new password'); + $this->createWordPressUser('ana', $hash); + + $this->changePassword('ana', 'new password'); + + $this->assertSame($hash, $this->wordPressHashOf('ana')); + } + + public function testIgnoresAUserMissingFromWordPress(): void { + $this->createWordPressUser('ana', 'hash of ana'); + + $this->changePassword('bruno', 'new password'); + + $this->assertSame('hash of ana', $this->wordPressHashOf('ana')); + } + + private function createWordPressUser(string $login, string $hash): void { + $statement = $this->database->prepare('INSERT INTO wp_users (user_login, user_email, user_pass, display_name) VALUES (:login, :email, :hash, :login)'); + $statement->execute(['login' => $login, 'email' => $login . '@example.org', 'hash' => $hash]); + } + + private function wordPressHashOf(string $login): string { + $statement = $this->database->prepare('SELECT user_pass FROM wp_users WHERE user_login = :login'); + $statement->execute(['login' => $login]); + return (string)$statement->fetchColumn(); + } + + private function changePassword(string $uid, string $password): void { + $config = $this->createMock(IConfig::class); + $config->method('getSystemValue')->willReturnMap([ + ['wordpress_dsn', '', 'sqlite:' . $this->databaseFile], + ['wordpress_queries', [], []], + ]); + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn($uid); + + (new BeforePasswordUpdatedListener($config))->handle(new BeforePasswordUpdatedEvent($user, $password)); + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 8943993..f362aa5 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -17,3 +17,47 @@ public function getSystemValue($key, $default = ''); } } } + +namespace OCP { + if (!interface_exists(IUser::class)) { + interface IUser { + public function getUID(); + } + } +} + +namespace OCP\EventDispatcher { + if (!class_exists(Event::class)) { + class Event { + } + } + + if (!interface_exists(IEventListener::class)) { + interface IEventListener { + public function handle(Event $event): void; + } + } +} + +namespace OCP\User\Events { + use OCP\EventDispatcher\Event; + use OCP\IUser; + + if (!class_exists(BeforePasswordUpdatedEvent::class)) { + class BeforePasswordUpdatedEvent extends Event { + public function __construct( + private IUser $user, + private string $password, + ) { + } + + public function getUser(): IUser { + return $this->user; + } + + public function getPassword(): string { + return $this->password; + } + } + } +} From 9cd488d174ab245da4db694d2068a422789852b5 Mon Sep 17 00:00:00 2001 From: YvesCesar Date: Thu, 1 Oct 2026 18:15:39 -0400 Subject: [PATCH 2/2] fix: compare the new password with the hash of the user that is updated Signed-off-by: YvesCesar --- lib/Listener/BeforePasswordUpdatedListener.php | 8 ++++++-- .../Listener/BeforePasswordUpdatedListenerTest.php | 13 +++++++++++-- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/lib/Listener/BeforePasswordUpdatedListener.php b/lib/Listener/BeforePasswordUpdatedListener.php index 80f11ef..6a5b338 100644 --- a/lib/Listener/BeforePasswordUpdatedListener.php +++ b/lib/Listener/BeforePasswordUpdatedListener.php @@ -53,8 +53,12 @@ public function handle(Event $event): void { private function getWordPressHash(PDO $db, string $username): string { $statement = $db->prepare($this->queryConfig->getWordPressUserQuery()); $statement->execute(['username' => $username]); - $row = $statement->fetch(PDO::FETCH_ASSOC); - return $row === false ? '' : (string)$row['password']; + foreach ($statement->fetchAll(PDO::FETCH_ASSOC) as $row) { + if ((string)$row['uid'] === $username) { + return (string)$row['password']; + } + } + return ''; } private function getDatabase(): ?PDO { diff --git a/tests/Unit/Listener/BeforePasswordUpdatedListenerTest.php b/tests/Unit/Listener/BeforePasswordUpdatedListenerTest.php index 82118db..99b26ec 100644 --- a/tests/Unit/Listener/BeforePasswordUpdatedListenerTest.php +++ b/tests/Unit/Listener/BeforePasswordUpdatedListenerTest.php @@ -48,6 +48,15 @@ public function testKeepsTheWordPressHashThatAlreadyMatchesTheNewPassword(): voi $this->assertSame($hash, $this->wordPressHashOf('ana')); } + public function testWritesTheNewPasswordWhenAnotherUserHasTheLoginAsEmail(): void { + $this->createWordPressUser('bruno', (new HashPassword())->hashPassword('new password'), 'ana@example.org'); + $this->createWordPressUser('ana@example.org', (new HashPassword())->hashPassword('old password'), 'ana@example.net'); + + $this->changePassword('ana@example.org', 'new password'); + + $this->assertTrue((new HashPassword())->validate('new password', $this->wordPressHashOf('ana@example.org'))); + } + public function testIgnoresAUserMissingFromWordPress(): void { $this->createWordPressUser('ana', 'hash of ana'); @@ -56,9 +65,9 @@ public function testIgnoresAUserMissingFromWordPress(): void { $this->assertSame('hash of ana', $this->wordPressHashOf('ana')); } - private function createWordPressUser(string $login, string $hash): void { + private function createWordPressUser(string $login, string $hash, ?string $email = null): void { $statement = $this->database->prepare('INSERT INTO wp_users (user_login, user_email, user_pass, display_name) VALUES (:login, :email, :hash, :login)'); - $statement->execute(['login' => $login, 'email' => $login . '@example.org', 'hash' => $hash]); + $statement->execute(['login' => $login, 'email' => $email ?? $login . '@example.org', 'hash' => $hash]); } private function wordPressHashOf(string $login): string {