Files
WP-LIT-MCP/src/Integrations/Media/MediaDownloadAbility.php
T
janniclas.loosenandClaude Opus 5.5 b24eb6c3e9 fix(security): no direct HTTP access to plugin PHP files (audit L2, cheap part)
Every file under src/ exits outside WordPress (defined('ABSPATH') ||
exit; after the use block, so a direct request no longer prints a fatal
error with the server path); divi-convert-fixture.php gets the WP_CLI
guard the other integration scripts have. A unit test keeps both true;
PSR1 side-effects warning excluded for the guard.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
2026-09-26 19:32:12 +02:00

139 lines
4.5 KiB
PHP

<?php
declare(strict_types=1);
namespace LIT\MCP\Integrations\Media;
use LIT\MCP\Abilities\Ability;
use LIT\MCP\Abilities\Annotations;
use LIT\MCP\Abilities\Errors;
use LIT\MCP\Limits;
use WP_Error;
defined('ABSPATH') || exit;
/**
* 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). 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
{
public function __construct(private Limits $limits)
{
}
public function name(): string
{
return 'lit/media-download';
}
public function label(): string
{
return 'Medium herunterladen';
}
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 (höchstens 10 MB) werden nicht übertragen; '
. 'dann die Datei-URL nutzen.';
}
public function inputSchema(): array
{
return [
'type' => 'object',
'properties' => [
'id' => ['type' => 'integer', 'minimum' => 1],
'size' => ['type' => 'string', 'description' => 'full (Standard) oder Name einer Bildgröße.'],
],
'required' => ['id'],
'additionalProperties' => false,
];
}
public function outputSchema(): ?array
{
return null;
}
public function annotations(): array
{
return Annotations::read();
}
public function isDirect(): bool
{
return true;
}
public function permission(array $input): bool|WP_Error
{
$attachment = MediaGuard::attachment($input['id'] ?? null);
return $attachment instanceof WP_Error ? $attachment : true;
}
public function execute(array $input): mixed
{
$attachment = MediaGuard::attachment($input['id'] ?? null);
if ($attachment instanceof WP_Error) {
return $attachment;
}
$id = (int) $attachment->ID;
$size = (string) ($input['size'] ?? 'full');
if ($size === 'full') {
$path = get_attached_file($id);
$url = (string) wp_get_attachment_url($id);
} else {
$data = image_get_intermediate_size($id, $size);
if (! is_array($data) || ! isset($data['path'])) {
return Errors::notFound(
sprintf('Die Größe "%s" gibt es für dieses Medium nicht.', $size),
'Verfügbare Größen stehen unter sizes in lit/media-get.'
);
}
$uploads = wp_upload_dir();
$path = (is_array($uploads) ? (string) ($uploads['basedir'] ?? '') : '') . '/' . $data['path'];
$url = (string) ($data['url'] ?? '');
}
if (! is_string($path) || ! is_readable($path) || ! MediaFiles::isWithinUploads($path)) {
return Errors::notFound('Die Datei fehlt auf dem Server.', 'Medium in der Mediathek prüfen.');
}
$bytesOnDisk = (int) filesize($path);
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->inlineMegabytes()
),
'Die Datei-URL (url aus lit/media-get) verwenden oder eine kleinere Bildgröße (size) wählen.'
);
}
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
$bytes = file_get_contents($path);
if ($bytes === false) {
return Errors::internal();
}
$type = wp_check_filetype(wp_basename($path));
$mime = is_string($type['type'] ?? null) && $type['type'] !== ''
? $type['type']
: (string) $attachment->post_mime_type;
return [
'type' => 'resource',
'resource' => ['uri' => $url, 'mimeType' => $mime, 'blob' => base64_encode($bytes)],
];
}
}