fix: cap Base64 media transfers at 10 MB, upload limit at most 32 MB
This commit is contained in:
@@ -42,7 +42,15 @@ final class LimitsTab implements AdminTab
|
||||
$values = (new Limits())->all();
|
||||
$bounds = Limits::bounds();
|
||||
$labels = [
|
||||
'upload_mb' => __('Maximale Upload-/Download-Größe (MB)', 'wp-lit-mcp'),
|
||||
'upload_mb' => sprintf(
|
||||
/* translators: 1: maximum value in MB, 2: cap for Base64 transfers in MB */
|
||||
__(
|
||||
'Maximale Upload-/Download-Größe (MB, höchstens %1$d; Base64-Upload/Download höchstens %2$d MB)',
|
||||
'wp-lit-mcp'
|
||||
),
|
||||
$bounds['upload_mb'][1],
|
||||
Limits::INLINE_MAX_MB
|
||||
),
|
||||
'log_retention_days' => __('Protokoll aufbewahren (Tage)', 'wp-lit-mcp'),
|
||||
'snapshot_count' => __('Anzahl Designsystem-Snapshots (ab Etappe 4)', 'wp-lit-mcp'),
|
||||
];
|
||||
|
||||
@@ -13,8 +13,9 @@ use WP_Error;
|
||||
* Delivers a media file (original or a named image size) as an MCP embedded resource
|
||||
* (spec §4.3). Direct tool: like ImageContent (lit-media-preview), an embedded resource
|
||||
* is only turned into a content block by mcp-adapter for a tool called directly, never
|
||||
* via mcp-adapter-execute-ability (ruling Q6). Files above the upload limit (Limits) are
|
||||
* refused; the caller falls back to the URL.
|
||||
* via mcp-adapter-execute-ability (ruling Q6). The whole file is read into memory and sent
|
||||
* as Base64, so files above Limits::inlineBytes() (upload limit, at most 10 MB) are refused;
|
||||
* the caller falls back to the file URL.
|
||||
*/
|
||||
final class MediaDownloadAbility implements Ability
|
||||
{
|
||||
@@ -35,7 +36,8 @@ final class MediaDownloadAbility implements Ability
|
||||
public function description(): string
|
||||
{
|
||||
return 'Liefert eine Datei der Mediathek (Original oder Bildgröße, siehe sizes in lit/media-get) als '
|
||||
. 'eingebettete Ressource. Dateien über dem Upload-Limit werden nicht übertragen; dann die URL nutzen.';
|
||||
. 'eingebettete Ressource. Dateien über dem Upload-Limit (höchstens 10 MB) werden nicht übertragen; '
|
||||
. 'dann die Datei-URL nutzen.';
|
||||
}
|
||||
|
||||
public function inputSchema(): array
|
||||
@@ -103,14 +105,14 @@ final class MediaDownloadAbility implements Ability
|
||||
}
|
||||
|
||||
$bytesOnDisk = (int) filesize($path);
|
||||
if ($bytesOnDisk > $this->limits->uploadBytes()) {
|
||||
if ($bytesOnDisk > $this->limits->inlineBytes()) {
|
||||
return Errors::unsupported(
|
||||
sprintf(
|
||||
'Die Datei ist zu groß für die Übertragung (%.1f MB, Limit %d MB).',
|
||||
$bytesOnDisk / 1048576,
|
||||
$this->limits->uploadMegabytes()
|
||||
$this->limits->inlineMegabytes()
|
||||
),
|
||||
'Kleinere Bildgröße (size) wählen oder die URL aus lit/media-get verwenden.'
|
||||
'Die Datei-URL (url aus lit/media-get) verwenden oder eine kleinere Bildgröße (size) wählen.'
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,8 @@ use WP_Error;
|
||||
* - URL: only http(s), checked with wp_http_validate_url() before any request and fetched
|
||||
* with wp_safe_remote_get() (rejects internal addresses, also after redirects), streamed
|
||||
* into a temp file and cut off one byte above the upload limit.
|
||||
* - Base64: decoded strictly; the size is checked before anything is decoded or written.
|
||||
* - Base64: decoded strictly; the size is checked against Limits::inlineBytes() (upload limit,
|
||||
* at most 10 MB – the whole file sits in memory) before anything is decoded or written.
|
||||
* - Type: whitelist (filter lit_mcp_upload_mimes) intersected with what WordPress allows the
|
||||
* current user (SVG etc. only if WordPress itself allows it), checked against the real
|
||||
* file content with wp_check_filetype_and_ext() – disguised files fail.
|
||||
@@ -83,8 +84,8 @@ class MediaImporter
|
||||
public function fromBase64(string $data, string $filename, array $meta): int|WP_Error
|
||||
{
|
||||
$estimated = intdiv(strlen($data) * 3, 4);
|
||||
if ($estimated > $this->limits->uploadBytes() + 2) {
|
||||
return $this->tooLarge($estimated);
|
||||
if ($estimated > $this->limits->inlineBytes() + 2) {
|
||||
return $this->tooLargeInline($estimated);
|
||||
}
|
||||
|
||||
$bytes = base64_decode($data, true);
|
||||
@@ -95,8 +96,8 @@ class MediaImporter
|
||||
);
|
||||
}
|
||||
|
||||
if (strlen($bytes) > $this->limits->uploadBytes()) {
|
||||
return $this->tooLarge(strlen($bytes));
|
||||
if (strlen($bytes) > $this->limits->inlineBytes()) {
|
||||
return $this->tooLargeInline(strlen($bytes));
|
||||
}
|
||||
|
||||
AdminIncludes::load();
|
||||
@@ -337,6 +338,20 @@ class MediaImporter
|
||||
);
|
||||
}
|
||||
|
||||
private function tooLargeInline(int $bytes): WP_Error
|
||||
{
|
||||
if ($this->limits->inlineMegabytes() === $this->limits->uploadMegabytes()) {
|
||||
return $this->tooLarge($bytes);
|
||||
}
|
||||
|
||||
return Errors::invalidInput(
|
||||
sprintf('Datei zu groß (%.1f MB, Limit %d MB).', $bytes / 1048576, $this->limits->inlineMegabytes()),
|
||||
['data' => 'über dem Limit für Base64-Uploads'],
|
||||
'Größere Dateien über eine öffentliche URL mit lit/media-upload-from-url (direkt: '
|
||||
. 'lit-media-upload-from-url) importieren.'
|
||||
);
|
||||
}
|
||||
|
||||
private function tooLarge(int $bytes, string $field = 'data'): WP_Error
|
||||
{
|
||||
return Errors::invalidInput(
|
||||
|
||||
@@ -10,7 +10,8 @@ use WP_Error;
|
||||
|
||||
/**
|
||||
* Uploads a Base64-encoded file into the media library (spec §6.3), up to the configured
|
||||
* upload limit. Checks and cleanup: MediaImporter; permission and result: MediaUploads.
|
||||
* upload limit (at most Limits::INLINE_MAX_MB, the request carries the whole file).
|
||||
* Checks and cleanup: MediaImporter; permission and result: MediaUploads.
|
||||
*/
|
||||
final class MediaUploadAbility implements Ability
|
||||
{
|
||||
@@ -36,8 +37,8 @@ final class MediaUploadAbility implements Ability
|
||||
return sprintf(
|
||||
'Lädt eine Datei (Bild, PDF, MP4, MP3) als Base64 in die Mediathek, höchstens %d MB; optional mit '
|
||||
. 'Titel, Alt-Text und Zuordnung zu einem Eintrag (post_id). Nicht idempotent: jeder Aufruf legt '
|
||||
. 'ein neues Medium an.',
|
||||
$this->limits->uploadMegabytes()
|
||||
. 'ein neues Medium an. Größere Dateien per URL mit lit/media-upload-from-url importieren.',
|
||||
$this->limits->inlineMegabytes()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+19
-1
@@ -11,7 +11,14 @@ final class Limits
|
||||
{
|
||||
public const OPTION = 'lit_mcp_limits';
|
||||
public const DEFAULTS = ['upload_mb' => 5, 'log_retention_days' => 90, 'snapshot_count' => 20];
|
||||
private const BOUNDS = ['upload_mb' => [1, 100], 'log_retention_days' => [1, 3650], 'snapshot_count' => [1, 100]];
|
||||
/**
|
||||
* Hard cap for transfers held in memory as a whole and passed through the MCP response or
|
||||
* request as Base64 (lit-media-download, lit/media-upload): above it, PHP memory and the
|
||||
* JSON payload become the problem, whatever upload_mb says. URL imports stream to disk
|
||||
* and only use upload_mb.
|
||||
*/
|
||||
public const INLINE_MAX_MB = 10;
|
||||
private const BOUNDS = ['upload_mb' => [1, 32], 'log_retention_days' => [1, 3650], 'snapshot_count' => [1, 100]];
|
||||
|
||||
public function uploadBytes(): int
|
||||
{
|
||||
@@ -23,6 +30,17 @@ final class Limits
|
||||
return $this->all()['upload_mb'];
|
||||
}
|
||||
|
||||
/** Limit for Base64 transfers: the upload limit, at most INLINE_MAX_MB. */
|
||||
public function inlineBytes(): int
|
||||
{
|
||||
return $this->inlineMegabytes() * 1024 * 1024;
|
||||
}
|
||||
|
||||
public function inlineMegabytes(): int
|
||||
{
|
||||
return min($this->uploadMegabytes(), self::INLINE_MAX_MB);
|
||||
}
|
||||
|
||||
public function logRetentionDays(): int
|
||||
{
|
||||
return $this->all()['log_retention_days'];
|
||||
|
||||
Reference in New Issue
Block a user