fix: allow remote icon paths in generals

This commit is contained in:
2026-08-07 16:01:32 +00:00
parent f3bf490df3
commit db9c7ea828
4 changed files with 160 additions and 1 deletions
+1 -1
View File
@@ -11,7 +11,7 @@ CREATE TABLE `general` (
`bornyear` INT(3) NULL DEFAULT '180',
`deadyear` INT(3) NULL DEFAULT '300',
`newmsg` INT(1) NULL DEFAULT '0',
`picture` VARCHAR(40) NOT NULL,
`picture` VARCHAR(64) NOT NULL,
`imgsvr` INT(1) NOT NULL DEFAULT '0',
`name` VARCHAR(32) NOT NULL COLLATE 'utf8mb4_bin',
`owner_name` VARCHAR(32) NULL DEFAULT NULL COLLATE 'utf8mb4_bin',
+6
View File
@@ -0,0 +1,6 @@
<IfModule mod_authz_core.c>
Require all denied
</IfModule>
<IfModule !mod_authz_core.c>
Deny from all
</IfModule>
+111
View File
@@ -0,0 +1,111 @@
#!/usr/bin/env php
<?php
declare(strict_types=1);
use sammo\DB;
if (PHP_SAPI !== 'cli') {
http_response_code(404);
exit(1);
}
$_SERVER['REMOTE_ADDR'] ??= '127.0.0.1';
$_SERVER['REQUEST_URI'] ??= '/cli/migrate-general-picture';
require dirname(__DIR__) . '/hwe/lib.php';
require dirname(__DIR__) . '/hwe/func.php';
/** @return never */
function pictureMigrationUsage(int $exitCode = 0): void
{
$stream = $exitCode === 0 ? STDOUT : STDERR;
fwrite($stream, <<<'TEXT'
Usage:
php scripts/migrate-general-picture.php --status
php scripts/migrate-general-picture.php --apply --backup=/absolute/path/to/pre-migration.sql
--status is read-only. --apply widens general.picture from VARCHAR(40) to
VARCHAR(64), and requires a pre-existing, non-empty SQL backup. Stop web and
daemon traffic before applying; MariaDB/Aria DDL is not transactional.
TEXT);
exit($exitCode);
}
function pictureColumnCapacity(\MeekroDB $db): ?int
{
$column = $db->queryFirstRow('SHOW COLUMNS FROM general WHERE Field = %s', 'picture');
if (!$column || !is_string($column['Type'] ?? null)) {
return null;
}
if (preg_match('/^varchar\((\d+)\)$/i', $column['Type'], $matches) !== 1) {
return null;
}
return (int)$matches[1];
}
function pictureMigrationState(\MeekroDB $db): string
{
$capacity = pictureColumnCapacity($db);
if ($capacity === 40) {
return 'legacy';
}
if ($capacity !== null && $capacity >= 64) {
return 'ready';
}
return 'unsupported';
}
function printPictureMigrationStatus(\MeekroDB $db): string
{
$state = pictureMigrationState($db);
$capacity = pictureColumnCapacity($db);
printf("schema_state=%s\npicture_capacity=%s\n", $state, $capacity ?? 'unknown');
return $state;
}
$options = getopt('', ['help', 'status', 'apply', 'backup:']);
if (isset($options['help'])) {
pictureMigrationUsage();
}
if (isset($options['status']) === isset($options['apply'])) {
pictureMigrationUsage(2);
}
$db = DB::db();
if (isset($options['status'])) {
exit(printPictureMigrationStatus($db) === 'unsupported' ? 2 : 0);
}
$state = pictureMigrationState($db);
if ($state === 'ready') {
fwrite(STDOUT, "general.picture is already VARCHAR(64) or wider; nothing to do.\n");
exit(0);
}
if ($state !== 'legacy') {
fwrite(STDERR, "general.picture is not the supported VARCHAR(40) schema; inspect --status first.\n");
exit(2);
}
$backup = $options['backup'] ?? null;
if (!is_string($backup) || $backup === '' || $backup[0] !== '/' || !is_file($backup) || filesize($backup) === 0) {
fwrite(STDERR, "--backup must name a pre-existing, non-empty absolute SQL backup made immediately before migration.\n");
exit(2);
}
if (!\sammo\tryLock()) {
fwrite(STDERR, "Unable to acquire the GAME lock.\n");
exit(3);
}
try {
$db->query('ALTER TABLE general MODIFY picture VARCHAR(64) NOT NULL');
} finally {
\sammo\unlock();
}
if (printPictureMigrationStatus($db) !== 'ready') {
fwrite(STDERR, "general.picture migration verification failed; restore the supplied backup.\n");
exit(4);
}
fwrite(STDOUT, "general.picture migration completed.\n");
+42
View File
@@ -0,0 +1,42 @@
<?php
namespace sammo;
use PHPUnit\Framework\TestCase;
final class GeneralPictureSchemaTest extends TestCase
{
public function testGameAndAccountSchemasAcceptRemoteUserIconPaths(): void
{
$gameSchema = file_get_contents(__DIR__ . '/../hwe/sql/schema.sql');
$accountSchema = file_get_contents(__DIR__ . '/../f_install/sql/common_schema.sql');
self::assertIsString($gameSchema);
self::assertIsString($accountSchema);
self::assertMatchesRegularExpression('/`picture`\s+VARCHAR\(64\)\s+NOT NULL/i', $gameSchema);
self::assertMatchesRegularExpression('/`PICTURE`\s+VARCHAR\(64\)/i', $accountSchema);
$longestRemotePath = 'users/core/' . str_repeat('a', 32) . '.jpeg?=20260807';
self::assertGreaterThan(40, strlen($longestRemotePath));
self::assertLessThanOrEqual(64, strlen($longestRemotePath));
}
public function testExistingGameMigrationWidensOnlyThePictureColumn(): void
{
$migration = file_get_contents(__DIR__ . '/../scripts/migrate-general-picture.php');
self::assertIsString($migration);
self::assertStringContainsString(
'ALTER TABLE general MODIFY picture VARCHAR(64) NOT NULL',
$migration,
);
self::assertStringNotContainsString('UPDATE general', $migration);
self::assertStringContainsString("if (\$state === 'ready')", $migration);
}
public function testScriptsDirectoryIsDeniedOverApache(): void
{
$accessRules = file_get_contents(__DIR__ . '/../scripts/.htaccess');
self::assertIsString($accessRules);
self::assertStringContainsString('Require all denied', $accessRules);
self::assertStringContainsString('Deny from all', $accessRules);
}
}