Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion lib/Listener/BeforePasswordUpdatedListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,28 @@ 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]);
foreach ($statement->fetchAll(PDO::FETCH_ASSOC) as $row) {
if ((string)$row['uid'] === $username) {
return (string)$row['password'];
}
}
return '';
}

private function getDatabase(): ?PDO {
if (!$this->dsn) {
return null;
Expand Down
90 changes: 90 additions & 0 deletions tests/Unit/Listener/BeforePasswordUpdatedListenerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<?php

declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\WordPressLoginBackend\Tests\Unit\Listener;

use OCA\WordPressLoginBackend\Helper\HashPassword;
use OCA\WordPressLoginBackend\Listener\BeforePasswordUpdatedListener;
use OCP\IConfig;
use OCP\IUser;
use OCP\User\Events\BeforePasswordUpdatedEvent;
use PDO;
use PHPUnit\Framework\TestCase;

class BeforePasswordUpdatedListenerTest extends TestCase {
private string $databaseFile;
private PDO $database;

protected function setUp(): void {
$this->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 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');

$this->changePassword('bruno', 'new password');

$this->assertSame('hash of ana', $this->wordPressHashOf('ana'));
}

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' => $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));
}
}
44 changes: 44 additions & 0 deletions tests/bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}
}
Loading