PluginProbe
ZIP AI – AI Website Builder & AI Agent / 0.0.14
ZIP AI – AI Website Builder & AI Agent v0.0.14
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.14, at inc/services/media-service.php

168 lines 4.8 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 // The bytes must be the declared image type; fails closed when they are not an image.
91 if ( wp_get_image_mime( $temp_file ) !== $mime_type ) {
92 wp_delete_file( $temp_file );
93 return new \WP_Error( 'mime_mismatch', 'File content does not match the declared MIME type.' );
94 }
95
96 $file_array = array(
97 'name' => $filename,
98 'tmp_name' => $temp_file,
99 );
100
101 $attachment_id = media_handle_sideload( $file_array, 0 );
102
103 if ( file_exists( $temp_file ) ) {
104 wp_delete_file( $temp_file );
105 }
106
107 if ( is_wp_error( $attachment_id ) ) {
108 return $attachment_id;
109 }
110
111 $this->update_attachment_metadata( $attachment_id, $args );
112
113 return $attachment_id;
114 }
115
116 /**
117 * Update attachment metadata.
118 *
119 * @param int $attachment_id Attachment ID.
120 * @param array<string,mixed> $args Metadata arguments.
121 * @return void
122 */
123 public function update_attachment_metadata( $attachment_id, $args ) {
124 $post_data = array( 'ID' => $attachment_id );
125 $updated = false;
126
127 if ( ! empty( $args['title'] ) ) {
128 $post_data['post_title'] = sanitize_text_field( Utils::to_str( $args['title'] ) );
129 $updated = true;
130 }
131
132 if ( ! empty( $args['caption'] ) ) {
133 $post_data['post_excerpt'] = sanitize_textarea_field( Utils::to_str( $args['caption'] ) );
134 $updated = true;
135 }
136
137 if ( ! empty( $args['description'] ) ) {
138 $post_data['post_content'] = sanitize_textarea_field( Utils::to_str( $args['description'] ) );
139 $updated = true;
140 }
141
142 if ( $updated ) {
143 wp_update_post( $post_data );
144 }
145
146 if ( ! empty( $args['alt_text'] ) ) {
147 update_post_meta( $attachment_id, '_wp_attachment_image_alt', sanitize_text_field( Utils::to_str( $args['alt_text'] ) ) );
148 }
149 }
150
151 /**
152 * Get extension from mime type.
153 *
154 * @param string $mime Mime type.
155 * @return string Extension.
156 */
157 private function get_extension_from_mime( $mime ) {
158 $map = array(
159 'image/jpeg' => 'jpg',
160 'image/png' => 'png',
161 'image/gif' => 'gif',
162 'image/webp' => 'webp',
163 'application/pdf' => 'pdf',
164 );
165 return $map[ $mime ] ?? 'bin';
166 }
167 }
168