diff --git a/src/Integrations/Media/MediaImporter.php b/src/Integrations/Media/MediaImporter.php index 8472791..08b49a3 100644 --- a/src/Integrations/Media/MediaImporter.php +++ b/src/Integrations/Media/MediaImporter.php @@ -188,11 +188,21 @@ class MediaImporter } $title = isset($meta['title']) ? sanitize_text_field((string) $meta['title']) : null; - $attachmentId = $this->sideload($name, $tmp, (int) ($meta['post_id'] ?? 0), $title); + // media_handle_sideload() and update_post_meta() expect slashed data (they unslash). + $attachmentId = $this->sideload( + $name, + $tmp, + (int) ($meta['post_id'] ?? 0), + $title === null ? null : wp_slash($title) + ); if ($attachmentId instanceof WP_Error) { - $reason = 'WordPress hat die Datei nicht übernommen: ' . $attachmentId->get_error_message(); + // WordPress' message can contain server paths: only for the log, not for the client. + $message = sprintf('[wp-lit-mcp] media sideload failed: %s', $attachmentId->get_error_message()); + error_log($message); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log - return Errors::writeFailed($reason); + return Errors::writeFailed( + 'WordPress hat die Datei nicht übernommen (z. B. Upload-Verzeichnis nicht beschreibbar).' + ); } if (isset($meta['alt']) && $meta['alt'] !== '') { @@ -235,7 +245,7 @@ class MediaImporter private function saveAlt(int $attachmentId, string $alt): void { // update_post_meta() is also false when the value is already stored (alt from IPTC data). - $stored = update_post_meta($attachmentId, self::ALT_KEY, $alt) !== false + $stored = update_post_meta($attachmentId, self::ALT_KEY, wp_slash($alt)) !== false || (string) get_post_meta($attachmentId, self::ALT_KEY, true) === $alt; if ($stored) { return; diff --git a/tests/Unit/Integrations/Media/MediaImporterTest.php b/tests/Unit/Integrations/Media/MediaImporterTest.php index 22f9c3f..6724102 100644 --- a/tests/Unit/Integrations/Media/MediaImporterTest.php +++ b/tests/Unit/Integrations/Media/MediaImporterTest.php @@ -32,9 +32,15 @@ final class MediaImporterTest extends TestCase /** @var list */ private array $tempFiles = []; + /** error_log() is a PHP internal and cannot be stubbed; it is redirected to this file. */ + private string $errorLog = ''; + private string|false $previousErrorLog = false; + protected function setUp(): void { parent::setUp(); + $this->errorLog = (string) tempnam(sys_get_temp_dir(), 'lit-mcp-log'); + $this->previousErrorLog = ini_set('error_log', $this->errorLog); Functions\when('get_option')->justReturn(['upload_mb' => 1]); Functions\when('wp_tempnam')->alias(function (): string { $file = (string) tempnam(sys_get_temp_dir(), 'lit-up'); @@ -63,6 +69,10 @@ final class MediaImporterTest extends TestCase protected function tearDown(): void { + ini_set('error_log', (string) $this->previousErrorLog); + if (is_file($this->errorLog)) { + unlink($this->errorLog); + } foreach ($this->tempFiles as $file) { if (is_file($file)) { unlink($file); @@ -181,13 +191,40 @@ final class MediaImporterTest extends TestCase Functions\when('wp_check_filetype_and_ext')->justReturn(['ext' => 'png', 'type' => 'image/png', 'proper_filename' => false]); Functions\when('media_handle_sideload')->justReturn(new WP_Error('upload_error', 'Verzeichnis nicht beschreibbar')); + Functions\when('media_handle_sideload')->justReturn( + new WP_Error('upload_error', 'Verzeichnis /var/www/html/wp-content/uploads/2026/09 nicht beschreibbar') + ); + $result = $this->importer()->fromBase64(base64_encode('PNGDATA'), 'logo.png', []); + $logged = (string) file_get_contents($this->errorLog); self::assertSame('lit_write_failed', $result->get_error_code()); - self::assertStringContainsString('Verzeichnis nicht beschreibbar', $result->get_error_message()); + self::assertStringNotContainsString('/var/www', $result->get_error_message()); + self::assertStringNotContainsString('/var/www', (string) json_encode($result->get_error_data())); + self::assertStringContainsString('WordPress hat die Datei nicht übernommen', $result->get_error_message()); + self::assertStringContainsString('/var/www/html/wp-content/uploads/2026/09 nicht beschreibbar', $logged); $this->assertTempFilesRemoved(); } + public function testTitleAndAltTextAreSlashedForWordPress(): void + { + Functions\when('wp_slash')->alias('addslashes'); + Functions\when('wp_check_filetype_and_ext')->justReturn(['ext' => 'png', 'type' => 'image/png', 'proper_filename' => false]); + Functions\expect('media_handle_sideload')->once() + ->with(Mockery::type('array'), 0, 'C:\\\\Pfad') + ->andReturn(55); + Functions\expect('update_post_meta')->once() + ->with(55, '_wp_attachment_image_alt', 'Back\\\\slash') + ->andReturn(true); + + $id = $this->importer()->fromBase64(base64_encode('PNGDATA'), 'logo.png', [ + 'title' => 'C:\\Pfad', + 'alt' => 'Back\\slash', + ]); + + self::assertSame(55, $id); + } + public function testFailedAltTextDeletesTheNewAttachmentAgain(): void { Functions\when('wp_check_filetype_and_ext')->justReturn(['ext' => 'png', 'type' => 'image/png', 'proper_filename' => false]);