diff --git a/includes/Tools/McpMediaTools.php b/includes/Tools/McpMediaTools.php index 7f5ba52..e1edc0a 100644 --- a/includes/Tools/McpMediaTools.php +++ b/includes/Tools/McpMediaTools.php @@ -315,15 +315,15 @@ public function get_media_file_permission_callback(): bool { */ public function wp_upload_media_pre_callback( $args ): array { $params = array( - 'args' => $args, + 'args' => array(), ); - if ( ! isset( $params['args']['file'] ) ) { - return $params; + if ( ! isset( $args['file'] ) ) { + return array( 'args' => $args ); } try { // Get the base64 data. - $base64_data = $params['args']['file']; + $base64_data = $args['file']; // Remove data URI prefix if present. if ( strpos( $base64_data, 'data:' ) === 0 ) { @@ -346,20 +346,47 @@ public function wp_upload_media_pre_callback( $args ): array { } // Generate a filename based on the title or use a default. - $filename = isset( $params['args']['title'] ) ? sanitize_file_name( $params['args']['title'] ) : 'upload'; + $filename = isset( $args['title'] ) ? sanitize_file_name( $args['title'] ) : 'upload'; $filename .= '.' . $this->get_extension_from_mime_type( $mime_type ); - // Set up the headers for the REST API. + // Create multipart boundary. + $boundary = wp_generate_password( 24, false ); + + // Build multipart body with file and metadata. + $body_parts = array(); + + // Add the file part. + $body_parts[] = '--' . $boundary; + $body_parts[] = 'Content-Disposition: form-data; name="file"; filename="' . $filename . '"'; + $body_parts[] = 'Content-Type: ' . $mime_type; + $body_parts[] = ''; + $body_parts[] = $file_data; + + // Add metadata fields if present. + $metadata_fields = array( 'title', 'caption', 'description', 'alt_text' ); + foreach ( $metadata_fields as $field ) { + if ( isset( $args[ $field ] ) && ! empty( $args[ $field ] ) ) { + $body_parts[] = '--' . $boundary; + $body_parts[] = 'Content-Disposition: form-data; name="' . $field . '"'; + $body_parts[] = ''; + $body_parts[] = $args[ $field ]; + } + } + + // Close the multipart body. + $body_parts[] = '--' . $boundary . '--'; + $body_parts[] = ''; + + // Set up the headers for multipart request. $params['headers'] = array( - 'content_type' => array( $mime_type ), - 'content_disposition' => array( 'attachment; filename="' . $filename . '"' ), + 'Content-Type' => array( 'multipart/form-data; boundary=' . $boundary ), ); - // Set the raw file data. - $params['body'] = $file_data; + // Set the multipart body. + $params['body'] = implode( "\r\n", $body_parts ); - // Remove the original file parameter to avoid confusion. - unset( $params['args']['file'] ); + // Clear args since everything is now in the body. + $params['args'] = array(); return $params; } catch ( \Exception $e ) { diff --git a/tests/phpunit/Tools/McpMediaToolsTest.php b/tests/phpunit/Tools/McpMediaToolsTest.php index 560aa1d..16f5863 100644 --- a/tests/phpunit/Tools/McpMediaToolsTest.php +++ b/tests/phpunit/Tools/McpMediaToolsTest.php @@ -253,6 +253,100 @@ public function test_wp_upload_media_tool(): void { $this->assertStringContainsString( 'Uploaded-Test-Image', $response->get_data()['content'][0]['text'] ); } + /** + * Test the wp_upload_media tool with complete metadata. + */ + public function test_wp_upload_media_tool_with_metadata(): void { + // Create a test image file. + $test_image_path = __DIR__ . '/../../assets/test-image.jpeg'; + $test_image_data = file_get_contents( $test_image_path ); + $base64_data = base64_encode( $test_image_data ); + + // Create a REST request. + $request = new WP_REST_Request( 'POST', '/wp/v2/wpmcp' ); + + // Set the request body as JSON with complete metadata. + $request->set_body( + wp_json_encode( + array( + 'method' => 'tools/call', + 'name' => 'wp_upload_media', + 'arguments' => array( + 'file' => $base64_data, + 'title' => 'Complete Metadata Test', + 'caption' => 'Test image caption', + 'description' => 'Test image description', + 'alt_text' => 'Test image alt text', + ), + ) + ) + ); + + // Set content type header. + $request->add_header( 'Content-Type', 'application/json' ); + + // Set the current user. + wp_set_current_user( $this->admin_user->ID ); + + // Dispatch the request. + $response = rest_do_request( $request ); + + // Get the uploaded attachment ID for cleanup. + $response_text_content = json_decode( $response->get_data()['content'][0]['text'], true ); + + // Delete image after test (avoid duplicate media). + if ( isset( $response_text_content['id'] ) ) { + wp_delete_attachment( $response_text_content['id'], true ); + } + + // Check the response. + $this->assertEquals( 200, $response->get_status() ); + $this->assertArrayHasKey( 'content', $response->get_data() ); + $this->assertIsArray( $response->get_data()['content'] ); + $this->assertCount( 1, $response->get_data()['content'] ); + $this->assertEquals( 'text', $response->get_data()['content'][0]['type'] ); + + // Verify metadata was preserved in the response. + $this->assertStringContainsString( 'Complete-Metadata-Test', $response->get_data()['content'][0]['text'] ); + } + + /** + * Test the wp_upload_media_pre_callback directly for multipart handling. + */ + public function test_wp_upload_media_pre_callback_multipart_format(): void { + $media_tools = new \Automattic\WordpressMcp\Tools\McpMediaTools(); + + // Create test data. + $test_image_data = 'fake-image-data-for-testing'; + $base64_data = base64_encode( $test_image_data ); + + $args = array( + 'file' => $base64_data, + 'title' => 'Test Title', + 'alt_text' => 'Test Alt Text', + 'caption' => 'Test Caption', + 'description' => 'Test Description', + ); + + // Call the pre-callback. + $result = $media_tools->wp_upload_media_pre_callback( $args ); + + // Verify multipart structure. + $this->assertArrayHasKey( 'headers', $result ); + $this->assertArrayHasKey( 'Content-Type', $result['headers'] ); + $this->assertStringContainsString( 'multipart/form-data; boundary=', $result['headers']['Content-Type'][0] ); + + $this->assertArrayHasKey( 'body', $result ); + $this->assertStringContainsString( 'Content-Disposition: form-data; name="file"', $result['body'] ); + $this->assertStringContainsString( 'Content-Disposition: form-data; name="title"', $result['body'] ); + $this->assertStringContainsString( 'Content-Disposition: form-data; name="alt_text"', $result['body'] ); + $this->assertStringContainsString( 'Content-Disposition: form-data; name="caption"', $result['body'] ); + $this->assertStringContainsString( 'Content-Disposition: form-data; name="description"', $result['body'] ); + + // Verify args are cleared. + $this->assertEmpty( $result['args'] ); + } + /** * Test the wp_update_media tool. */