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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ Legend:
? = Other stuff
```

(26/10/26): Version 2.2.3-srcdslab (**Run updater required**)
-----------------------
```
01. ! Upgraded installs missing the type_authid / type_ip indexes on
sb_bans get them back (updater migration 815); the ban check,
banlist and PruneBans() no longer scan the whole table there
```

(26/10/26): Version 2.2.2-srcdslab
-----------------------
```
Expand Down
87 changes: 87 additions & 0 deletions web/tests/integration/Updater815BansIndexesTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?php

declare(strict_types=1);

namespace Sbpp\Tests\Integration;

use Sbpp\Tests\ApiTestCase;
use Sbpp\Tests\Fixture;

/**
* Issue #1578: upgraded installs can lack the `type_authid` / `type_ip`
* indexes on `:prefix_bans`. `PruneBans()` must not fatal without them, and
* `web/updater/data/815.php` must (re)create them idempotently.
*/
final class Updater815BansIndexesTest extends ApiTestCase
{
private const INDEXES = ['type_authid', 'type_ip'];

protected function tearDown(): void
{
$table = DB_PREFIX . '_bans';
$pdo = Fixture::rawPdo();
if (!$this->indexExists('type_authid')) {
$pdo->exec("ALTER TABLE `{$table}` ADD INDEX `type_authid` (`type`, `authid`)");
}
if (!$this->indexExists('type_ip')) {
$pdo->exec("ALTER TABLE `{$table}` ADD INDEX `type_ip` (`type`, `ip`)");
}

parent::tearDown();
}

private function indexExists(string $index): bool
{
$stmt = Fixture::rawPdo()->prepare(
'SELECT COUNT(*) FROM information_schema.STATISTICS '
. 'WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND INDEX_NAME = ?'
);
$stmt->execute([DB_NAME, DB_PREFIX . '_bans', $index]);

return (int) $stmt->fetchColumn() > 0;
}

private function dropIndexes(): void
{
$table = DB_PREFIX . '_bans';
foreach (self::INDEXES as $index) {
Fixture::rawPdo()->exec("ALTER TABLE `{$table}` DROP INDEX `{$index}`");
$this->assertFalse($this->indexExists($index), "Pre-condition: {$index} was dropped.");
}
}

private function runMigration(): bool
{
$ctx = new class($GLOBALS['PDO']) {
public function __construct(public \Database $dbs) {}

public function run(string $path): mixed
{
return require $path;
}
};

return (bool) $ctx->run(ROOT . 'updater/data/815.php');
}

public function testPruneBansRunsWithoutCompositeIndexes(): void
{
$this->dropIndexes();

\PruneBans();

$this->addToAssertionCount(1);
}

public function testMigrationRestoresMissingIndexesAndIsIdempotent(): void
{
$this->dropIndexes();

$this->assertTrue($this->runMigration(), 'First run should succeed.');
foreach (self::INDEXES as $index) {
$this->assertTrue($this->indexExists($index), "Migration must add {$index}.");
}

$this->assertTrue($this->runMigration(), 'Second run must not fatal on duplicate index.');
}
}
4 changes: 2 additions & 2 deletions web/tests/integration/UpdaterMigrationPortableSqlTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,14 @@
*
* Scope (focused, per #1498)
* --------------------------
* Only migrations with version >= 800 (the 8xx v2 block, 801-810 today)
* Only migrations with version >= 800 (the 8xx v2 block, 801-815 today)
* are scanned. Two reasons:
*
* - Every supported upgrade path the issue names (v2 rc5 -> rc6,
* v1.8.x -> v2) starts at `config.version = 705`, so the updater only
* runs 801+. 801 is the first migration past 705 and the actual
* blocker. Future migrations are always numbered above the current
* max (811+), so a `>= 800` floor guards every migration that will
* max (816+), so a `>= 800` floor guards every migration that will
* ever be added going forward.
* - Ten pre-800 migrations carry the same MariaDB-only syntax
* (1, 112, 150, 153, 160, 241, 291, 295, 351, 355). They only run for
Expand Down
40 changes: 40 additions & 0 deletions web/updater/data/815.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

// Issue #1578: ensure the composite `type_authid` / `type_ip` indexes exist
// on `:prefix_bans`. `702.php` adds them, but some long-lived installs carry a
// `config.version` past 702 without the indexes, so 702 never runs for them.
// `PruneBans()` relies on these indexes on every banlist / home / servers render.
//
// Each ADD is guarded by a portable information_schema probe (no
// MariaDB-only `ADD INDEX IF NOT EXISTS`), so re-running is a no-op and the
// migration works on MySQL and MariaDB alike. Fresh installs get the indexes
// from install/includes/sql/struc.sql and never run the updater.
//
// `$this` is supplied by Updater::update(); PHPStan can't see that, so each
// `$this->dbs` read is suppressed like every sibling migration.

$ensureBansIndex = static function (\Database $dbs, string $index, string $alterSql): void {
$dbs->query(
'SELECT COUNT(*) AS c FROM information_schema.STATISTICS '
. 'WHERE TABLE_SCHEMA = DATABASE() '
. 'AND TABLE_NAME = :table '
. 'AND INDEX_NAME = :index'
);
$dbs->bind(':table', $dbs->getPrefix() . '_bans');
$dbs->bind(':index', $index);
$row = $dbs->single();

if (is_array($row) && (int) ($row['c'] ?? 0) > 0) {
return;
}

$dbs->query($alterSql);
$dbs->execute();
};

// @phpstan-ignore variable.undefined
$ensureBansIndex($this->dbs, 'type_authid', 'ALTER TABLE `:prefix_bans` ADD INDEX `type_authid` (`type`, `authid`)');
// @phpstan-ignore variable.undefined
$ensureBansIndex($this->dbs, 'type_ip', 'ALTER TABLE `:prefix_bans` ADD INDEX `type_ip` (`type`, `ip`)');

return true;
3 changes: 2 additions & 1 deletion web/updater/store.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,6 @@
"811": "811.php",
"812": "812.php",
"813": "813.php",
"814": "814.php"
"814": "814.php",
"815": "815.php"
}
Loading