PluginProbe
ZIP AI – AI Website Builder & AI Agent / 0.0.12
ZIP AI – AI Website Builder & AI Agent v0.0.12
0.0.14 0.0.13 0.0.12 0.0.11 0.0.10 0.0.9 trunk 0.0.4 0.0.5 0.0.6 0.0.7 0.0.8
zip-ai / inc / services / media-service.php

media-service.php in ZIP AI – AI Website Builder & AI Agent 0.0.12, at inc/services/media-service.php

169 lines 4.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Media Service - Logic for handling WordPress media
4 *
5 * @package zip-ai
6 */
7
8 namespace ZipAI\MCP\Classes\Services;
9
10 use ZipAI\MCP\Classes\Core\Response;
11 use ZipAI\MCP\Classes\Core\Utils;
12
13 // Exit if accessed directly.
14 if ( ! defined( 'ABSPATH' ) ) {
15 exit;
16 }
17
18 /**
19 * Class Media_Service
20 */
21 class Media_Service {
22
23 /**
24 * Upload media from a base64 data URL string.
25 *
26 * @param string $base64_data Raw base64 string (without data: prefix).
27 * @param string $mime_type MIME type (e.g. image/jpeg).
28 * @param array<string,mixed> $args Optional metadata (title, alt_text, etc.).
29 * @return int|\WP_Error Attachment ID on success, WP_Error on failure.
30 */
31 public function upload_from_base64( $base64_data, $mime_type, $args = array() ) {
32 if ( empty( $base64_data ) || empty( $mime_type ) ) {
33 return new \WP_Error( 'invalid_data', 'Base64 data and MIME type are required.' );
34 }
35
36 // Only allow image MIME types.
37 $allowed_mimes = array( 'image/jpeg', 'image/png', 'image/gif', 'image/webp' );
38 if ( ! in_array( $mime_type, $allowed_mimes, true ) ) {
39 return new \WP_Error( 'invalid_mime', 'Unsupported file type. Only JPEG, PNG, GIF, and WebP images are allowed.' );
40 }
41
42 $decoded = base64_decode( $base64_data, true );
43 if ( false === $decoded ) {
44 return new \WP_Error( 'decode_failed', 'Failed to decode base64 data.' );
45 }
46
47 // 5 MB limit.
48 if ( strlen( $decoded ) > 5 * 1024 * 1024 ) {
49 return new \WP_Error( 'file_too_large', 'File exceeds the 5 MB limit.' );
50 }
51
52 require_once ABSPATH . 'wp-admin/includes/file.php';
53 require_once ABSPATH . 'wp-admin/includes/media.php';
54 require_once ABSPATH . 'wp-admin/includes/image.php';
55
56 $ext = $this->get_extension_from_mime( $mime_type );
57 $filename = ! empty( $args['filename'] )
58 ? sanitize_file_name( Utils::to_str( $args['filename'] ) )
59 : 'upload-' . time() . '.' . $ext;
60
61 // Ensure extension.
62 if ( ! pathinfo( $filename, PATHINFO_EXTENSION ) ) {
63 $filename .= '.' . $ext;
64 }
65
66 // Write to temp file.
67 $temp_file = wp_tempnam( $filename );
68 if ( ! $temp_file ) {
69 return new \WP_Error( 'temp_failed', 'Failed to create temporary file.' );
70 }
71
72 // Write the decoded bytes via WP_Filesystem (no advisory locking needed —
73 // $temp_file is a fresh wp_tempnam() path with no concurrent writers).
74 /**
75 * Narrowed type for `$wp_filesystem`.
76 *
77 * @var \WP_Filesystem_Base|null $wp_filesystem
78 */
79 global $wp_filesystem;
80 if ( ! $wp_filesystem ) {
81 WP_Filesystem();
82 }
83 if ( ! $wp_filesystem || ! $wp_filesystem->put_contents( $temp_file, $decoded, FS_CHMOD_FILE ) ) {
84 if ( file_exists( $temp_file ) ) {
85 wp_delete_file( $temp_file );
86 }
87 return new \WP_Error( 'write_failed', 'Failed to write the uploaded file.' );
88 }
89
90 // Verify actual file content matches declared MIME type.
91 $detected_mime = function_exists( 'mime_content_type' ) ? mime_content_type( $temp_file ) : null;
92 if ( $detected_mime && $detected_mime !== $mime_type ) {
93 wp_delete_file( $temp_file );
94 return new \WP_Error( 'mime_mismatch', 'File content does not match the declared MIME type.' );
95 }
96
97 $file_array = array(
98 'name' => $filename,
99 'tmp_name' => $temp_file,
100 );
101
102 $attachment_id = media_handle_sideload( $file_array, 0 );
103
104 if ( file_exists( $temp_file ) ) {
105 wp_delete_file( $temp_file );
106 }
107
108 if ( is_wp_error( $attachment_id ) ) {
109 return $attachment_id;
110 }
111
112 $this->update_attachment_metadata( $attachment_id, $args );
113
114 return $attachment_id;
115 }
116
117 /**
118 * Update attachment metadata.
119 *
120 * @param int $attachment_id Attachment ID.
121 * @param array<string,mixed> $args Metadata arguments.
122 * @return void
123 */
124 public function update_attachment_metadata( $attachment_id, $args ) {
125 $post_data = array( 'ID' => $attachment_id );
126 $updated = false;
127
128 if ( ! empty( $args['title'] ) ) {
129 $post_data['post_title'] = sanitize_text_field( Utils::to_str( $args['title'] ) );
130 $updated = true;
131 }
132
133 if ( ! empty( $args['caption'] ) ) {
134 $post_data['post_excerpt'] = sanitize_textarea_field( Utils::to_str( $args['caption'] ) );
135 $updated = true;
136 }
137
138 if ( ! empty( $args['description'] ) ) {
139 $post_data['post_content'] = sanitize_textarea_field( Utils::to_str( $args['description'] ) );
140 $updated = true;
141 }
142
143 if ( $updated ) {
144 wp_update_post( $post_data );
145 }
146
147 if ( ! empty( $args['alt_text'] ) ) {
148 update_post_meta( $attachment_id, '_wp_attachment_image_alt', sanitize_text_field( Utils::to_str( $args['alt_text'] ) ) );
149 }
150 }
151
152 /**
153 * Get extension from mime type.
154 *
155 * @param string $mime Mime type.
156 * @return string Extension.
157 */
158 private function get_extension_from_mime( $mime ) {
159 $map = array(
160 'image/jpeg' => 'jpg',
161 'image/png' => 'png',
162 'image/gif' => 'gif',
163 'image/webp' => 'webp',
164 'application/pdf' => 'pdf',
165 );
166 return $map[ $mime ] ?? 'bin';
167 }
168 }
169