fix: neutral hint form for remaining client-facing tool mentions, Limits bounds accessor

Fix round 1 (task 10 review):
- FieldValidator's "unknown field" validation error (reaches the
  client via validation_failed's fields) and PostPresenter's
  DIVI_HINT (surfaced in the result's hints array) still named
  lit-content-types/lit-content-update as plain lit-<name>; converted
  both to the neutral "lit/<name> (direkt: lit-<name>)" form like the
  other hints from task 10. Re-grepped src/ for any other client-facing
  error/field/hints text naming a direct-by-default tool as lit-<name>
  (descriptions/schemas excluded per Q11c) - none left.
- Added Limits::bounds() (field => [min, max]) and made LimitsTab use
  it for the number inputs' min/max instead of a second, hardcoded
  copy of the same bounds.
- LimitsTab now shows a description note when the configured upload_mb
  exceeds wp_max_upload_size() (PHP's upload_max_filesize/post_max_size),
  since WordPress would refuse a larger upload before this limit ever
  applies.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
This commit is contained in:
2026-09-26 03:57:15 +02:00
co-authored by Claude Opus 5.5
parent bca35f6166
commit 9104989c43
6 changed files with 98 additions and 7 deletions
+29 -5
View File
@@ -40,16 +40,18 @@ final class LimitsTab implements AdminTab
public function render(): void
{
$values = (new Limits())->all();
$fields = [
'upload_mb' => [__('Maximale Upload-/Download-Größe (MB)', 'wp-lit-mcp'), 1, 100],
'log_retention_days' => [__('Protokoll aufbewahren (Tage)', 'wp-lit-mcp'), 1, 3650],
'snapshot_count' => [__('Anzahl Designsystem-Snapshots (ab Etappe 4)', 'wp-lit-mcp'), 1, 100],
$bounds = Limits::bounds();
$labels = [
'upload_mb' => __('Maximale Upload-/Download-Größe (MB)', 'wp-lit-mcp'),
'log_retention_days' => __('Protokoll aufbewahren (Tage)', 'wp-lit-mcp'),
'snapshot_count' => __('Anzahl Designsystem-Snapshots (ab Etappe 4)', 'wp-lit-mcp'),
];
echo '<form method="post" action="options.php">';
settings_fields(self::GROUP);
echo '<table class="form-table"><tbody>';
foreach ($fields as $key => [$label, $min, $max]) {
foreach ($labels as $key => $label) {
[$min, $max] = $bounds[$key];
printf(
'<tr><th scope="row"><label for="lit-mcp-%1$s">%2$s</label></th><td>'
. '<input id="lit-mcp-%1$s" type="number" min="%3$d" max="%4$d" name="%5$s[%1$s]" value="%6$d">'
@@ -63,7 +65,29 @@ final class LimitsTab implements AdminTab
);
}
echo '</tbody></table>';
$this->maybeRenderUploadSizeNotice($values['upload_mb']);
submit_button();
echo '</form>';
}
/**
* PHP's own upload_max_filesize/post_max_size (via wp_max_upload_size()) can be lower than
* what is configured here – WordPress itself would then refuse a large upload/download before
* this limit ever applies. A warning only, since the two are independent knobs.
*/
private function maybeRenderUploadSizeNotice(int $uploadMb): void
{
$serverMaxMb = (int) floor(wp_max_upload_size() / (1024 * 1024));
if ($serverMaxMb <= 0 || $uploadMb <= $serverMaxMb) {
return;
}
$message = sprintf(
/* translators: 1: configured limit in MB, 2: server limit in MB */
__('%1$d MB übersteigt das Server-Limit von %2$d MB (upload_max_filesize/post_max_size).', 'wp-lit-mcp'),
$uploadMb,
$serverMaxMb
);
printf('<p class="description">%s</p>', esc_html($message));
}
}
@@ -19,7 +19,7 @@ class PostPresenter
public const DIVI_HINT = 'Dieser Inhalt ist mit Divi gebaut. Die Divi-Struktur wird über lit-divi-outline '
. 'gelesen und mit den lit-divi-*-Tools bearbeitet; fehlen diese Tools, kann der Divi-Inhalt hier nicht '
. 'bearbeitet werden. content wird deshalb nicht ausgegeben; Titel, Auszug, Felder und Begriffe sind '
. 'über lit-content-update änderbar.';
. 'über lit/content-update (direkt: lit-content-update) änderbar.';
public function __construct(private ContentSchema $schema)
{
@@ -28,7 +28,8 @@ final class FieldValidator
$key = (string) $key;
$field = $definitions[$key] ?? null;
if ($field === null) {
$errors[$key] = 'Unbekanntes Feld; verfügbare Felder über lit-content-types abrufen.';
$errors[$key] = 'Unbekanntes Feld; verfügbare Felder über lit/content-types (direkt: '
. 'lit-content-types) abrufen.';
continue;
}
+11
View File
@@ -39,6 +39,17 @@ final class Limits
return self::sanitize(get_option(self::OPTION, []));
}
/**
* The [min, max] each field is clamped to by sanitize() – for admin UI form attributes,
* so they can't drift from the actual bounds.
*
* @return array<string, array{int, int}>
*/
public static function bounds(): array
{
return self::BOUNDS;
}
/**
* Settings API sanitize callback: always returns all keys, clamped to their bounds.
*
+36
View File
@@ -31,6 +31,7 @@ final class LimitsTabTest extends TestCase
Functions\when('settings_fields')->justReturn(null);
Functions\when('submit_button')->justReturn(null);
Functions\when('get_option')->justReturn(['upload_mb' => 8]);
Functions\when('wp_max_upload_size')->justReturn(64 * 1024 * 1024);
ob_start();
(new LimitsTab())->render();
@@ -39,5 +40,40 @@ final class LimitsTabTest extends TestCase
self::assertSame(3, substr_count($html, 'type="number"'));
self::assertStringContainsString('name="lit_mcp_limits[upload_mb]" value="8"', $html);
self::assertStringContainsString('name="lit_mcp_limits[log_retention_days]" value="90"', $html);
self::assertStringContainsString('min="1" max="100"', $html);
self::assertStringContainsString('min="1" max="3650"', $html);
}
public function testWarnsWhenUploadMbExceedsTheServersOwnUploadLimit(): void
{
Functions\stubEscapeFunctions();
Functions\stubTranslationFunctions();
Functions\when('settings_fields')->justReturn(null);
Functions\when('submit_button')->justReturn(null);
Functions\when('get_option')->justReturn(['upload_mb' => 50]);
Functions\when('wp_max_upload_size')->justReturn(2 * 1024 * 1024);
ob_start();
(new LimitsTab())->render();
$html = (string) ob_get_clean();
self::assertStringContainsString('class="description"', $html);
self::assertMatchesRegularExpression('/50 MB.*2 MB/s', $html);
}
public function testNoWarningWhenUploadMbIsWithinTheServersOwnUploadLimit(): void
{
Functions\stubEscapeFunctions();
Functions\stubTranslationFunctions();
Functions\when('settings_fields')->justReturn(null);
Functions\when('submit_button')->justReturn(null);
Functions\when('get_option')->justReturn(['upload_mb' => 5]);
Functions\when('wp_max_upload_size')->justReturn(64 * 1024 * 1024);
ob_start();
(new LimitsTab())->render();
$html = (string) ob_get_clean();
self::assertStringNotContainsString('class="description"', $html);
}
}
+19
View File
@@ -42,4 +42,23 @@ final class LimitsTest extends TestCase
);
self::assertSame(Limits::DEFAULTS, Limits::sanitize(null));
}
public function testBoundsMatchesWhatSanitizeClampsTo(): void
{
self::assertSame([1, 100], Limits::bounds()['upload_mb']);
self::assertSame([1, 3650], Limits::bounds()['log_retention_days']);
self::assertSame([1, 100], Limits::bounds()['snapshot_count']);
// Cross-check against sanitize()'s actual clamping, so the two can't drift apart.
$tooLow = array_map(static fn (array $bound): int => $bound[0] - 1, Limits::bounds());
$tooHigh = array_map(static fn (array $bound): int => $bound[1] + 1, Limits::bounds());
self::assertSame(
array_map(static fn (array $bound): int => $bound[0], Limits::bounds()),
Limits::sanitize($tooLow)
);
self::assertSame(
array_map(static fn (array $bound): int => $bound[1], Limits::bounds()),
Limits::sanitize($tooHigh)
);
}
}