PluginProbe
ActivityPub / 7.7.0
ActivityPub v7.7.0
9.3.1 9.3.0 9.2.2 9.2.1 9.2.0 9.1.0 9.0.2 9.0.1 9.0.0 8.3.0 8.2.1 8.2.0 8.1.1 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.2.0 1.3.0 2.0.0 2.0.1 2.1.0 2.1.1 All 160 releases
← All changes | includes/class-attachments.php +389 -332 9.2.07.7.0 View file →
@@ -6,28 +6,58 @@
6 6 */
7 7
8 8 namespace Activitypub;
9 9
10 +use Activitypub\Collection\Posts;
11 +
10 12 /**
11 13 * Attachments processor class.
12 - *
13 - * Handles importing media attachments into the WordPress Media Library.
14 - * Creates full WordPress attachment posts that are searchable and manageable.
15 - *
16 - * For lightweight file caching without Media Library overhead, use the
17 - * Cache\Media, Cache\Avatar, and Cache\Emoji classes instead.
18 - *
19 - * @since 1.0.0
20 14 */
21 15 class Attachments {
22 16 /**
23 - * Maximum width for imported images into Media Library.
17 + * Directory for storing ap_post media files.
24 18 *
25 - * @var int
19 + * @var string
26 20 */
27 - const MAX_IMAGE_DIMENSION = 1200;
21 + public static $ap_posts_dir = '/activitypub/ap_posts/';
28 22
29 23 /**
24 + * Directory for storing comment media files.
25 + *
26 + * @var string
27 + */
28 + public static $comments_dir = '/activitypub/comments/';
29 +
30 + /**
31 + * Initialize the class and set up filters.
32 + */
33 + public static function init() {
34 + \add_action( 'before_delete_post', array( self::class, 'delete_ap_posts_directory' ) );
35 + }
36 +
37 + /**
38 + * Delete the activitypub files directory for a post.
39 + *
40 + * @param int $post_id The post ID.
41 + */
42 + public static function delete_ap_posts_directory( $post_id ) {
43 + if ( Posts::POST_TYPE !== \get_post_type( $post_id ) ) {
44 + return;
45 + }
46 +
47 + require_once ABSPATH . 'wp-admin/includes/file.php';
48 +
49 + \WP_Filesystem();
50 + global $wp_filesystem;
51 +
52 + $activitypub_dir = self::get_storage_paths( $post_id, 'post' )['basedir'];
53 +
54 + if ( $wp_filesystem->is_dir( $activitypub_dir ) ) {
55 + $wp_filesystem->delete( $activitypub_dir, true );
56 + }
57 + }
58 +
59 + /**
30 60 * Import attachments from an ActivityPub object and attach them to a post.
31 61 *
32 62 * Creates full WordPress attachment posts in the media library. Each attachment
33 63 * becomes a searchable, manageable attachment post that appears in the WordPress
@@ -48,9 +78,9 @@
48 78 public static function import( $attachments, $post_id, $author_id = 0 ) {
49 79 // First, import inline images from the post content.
50 80 $inline_mappings = self::import_inline_images( $post_id, $author_id );
51 81
52 - if ( empty( $attachments ) || ! \is_array( $attachments ) ) {
82 + if ( empty( $attachments ) || ! is_array( $attachments ) ) {
53 83 return array();
54 84 }
55 85
56 86 $attachment_ids = array();
@@ -81,8 +111,147 @@
81 111 return $attachment_ids;
82 112 }
83 113
84 114 /**
115 + * Import attachments as direct files for posts.
116 + *
117 + * Saves files directly to uploads/activitypub/ap_posts/{post_id}/ without creating
118 + * WordPress attachment posts. This lightweight approach is ideal for federated content
119 + * that doesn't require full WordPress media management.
120 + *
121 + * Files are stored in a dedicated directory structure and automatically cleaned up
122 + * when the parent post is deleted. Media URLs point directly to the stored files
123 + * rather than going through WordPress attachment APIs.
124 + *
125 + * Use this when:
126 + * - Processing ActivityPub Create/Update activities from the inbox.
127 + * - Handling federated content that won't be owned or edited by the user.
128 + * - You want lightweight storage without Media Library overhead.
129 + *
130 + * @param array $attachments Array of ActivityPub attachment objects.
131 + * @param int $post_id The post ID to attach files to.
132 + *
133 + * @return array[] Array of file data arrays.
134 + */
135 + public static function import_post_files( $attachments, $post_id ) {
136 + return self::import_files_for_object( $attachments, $post_id, 'post' );
137 + }
138 +
139 + /**
140 + * Import attachments as direct files for any object type.
141 + *
142 + * Saves files directly to uploads/activitypub/{type}/{id}/ without creating
143 + * WordPress attachment posts. This is the internal method that handles
144 + * the actual import logic for both posts and comments.
145 + *
146 + * @param array $attachments Array of ActivityPub attachment objects.
147 + * @param int $object_id The object ID (post or comment).
148 + * @param string $object_type The object type ('post' or 'comment').
149 + *
150 + * @return array[] Array of file data arrays.
151 + */
152 + private static function import_files_for_object( $attachments, $object_id, $object_type ) {
153 + // First, import inline images from the content.
154 + $inline_mappings = self::import_inline_files( $object_id, $object_type );
155 +
156 + if ( empty( $attachments ) || ! is_array( $attachments ) ) {
157 + return array();
158 + }
159 +
160 + $files = array();
161 + foreach ( $attachments as $attachment ) {
162 + $attachment_data = self::normalize_attachment( $attachment );
163 +
164 + if ( empty( $attachment_data['url'] ) ) {
165 + continue;
166 + }
167 +
168 + // Skip if this URL was already processed as an inline image.
169 + if ( isset( $inline_mappings[ $attachment_data['url'] ] ) ) {
170 + continue;
171 + }
172 +
173 + $file_data = self::save_file( $attachment_data, $object_id, $object_type );
174 +
175 + if ( ! \is_wp_error( $file_data ) ) {
176 + $files[] = $file_data;
177 + }
178 + }
179 +
180 + // Append media markup to content.
181 + if ( ! empty( $files ) ) {
182 + self::append_files_to_content( $object_id, $files, $object_type );
183 + }
184 +
185 + return $files;
186 + }
187 +
188 + /**
189 + * Get storage paths for an object based on its type.
190 + *
191 + * @param int $object_id The object ID (post or comment).
192 + * @param string $object_type The object type ('post' or 'comment').
193 + *
194 + * @return array {
195 + * Storage paths for the object.
196 + *
197 + * @type string $basedir Base directory path.
198 + * @type string $baseurl Base URL.
199 + * }
200 + */
201 + private static function get_storage_paths( $object_id, $object_type ) {
202 + $upload_dir = \wp_upload_dir();
203 + $sub_dir = 'comment' === $object_type ? self::$comments_dir : self::$ap_posts_dir;
204 +
205 + return array(
206 + 'basedir' => $upload_dir['basedir'] . $sub_dir . $object_id,
207 + 'baseurl' => $upload_dir['baseurl'] . $sub_dir . $object_id,
208 + );
209 + }
210 +
211 + /**
212 + * Get content for an object based on its type.
213 + *
214 + * @param int $object_id The object ID (post or comment).
215 + * @param string $object_type The object type ('post' or 'comment').
216 + *
217 + * @return string The content string or empty if not found.
218 + */
219 + private static function get_object_content( $object_id, $object_type ) {
220 + if ( 'comment' === $object_type ) {
221 + $comment = \get_comment( $object_id );
222 + return $comment ? $comment->comment_content : '';
223 + }
224 +
225 + return \get_post_field( 'post_content', $object_id );
226 + }
227 +
228 + /**
229 + * Update content for an object based on its type.
230 + *
231 + * @param int $object_id The object ID (post or comment).
232 + * @param string $object_type The object type ('post' or 'comment').
233 + * @param string $content The new content.
234 + */
235 + private static function update_object_content( $object_id, $object_type, $content ) {
236 + if ( 'comment' === $object_type ) {
237 + \wp_update_comment(
238 + array(
239 + 'comment_ID' => $object_id,
240 + 'comment_content' => $content,
241 + )
242 + );
243 + } else {
244 + \wp_update_post(
245 + array(
246 + 'ID' => $object_id,
247 + 'post_content' => $content,
248 + )
249 + );
250 + }
251 + }
252 +
253 + /**
85 254 * Check if an attachment with the same source URL already exists for a post.
86 255 *
87 256 * @param string $source_url The source URL to check.
88 257 * @param int $post_id The post ID to check attachments for.
@@ -113,9 +282,9 @@
113 282 return array();
114 283 }
115 284
116 285 // Find all img tags in the content.
117 - \preg_match_all( '/<img[^>]+src=["\']([^"\']+)["\'][^>]*>/i', $post->post_content, $matches );
286 + preg_match_all( '/<img[^>]+src=["\']([^"\']+)["\'][^>]*>/i', $post->post_content, $matches );
118 287
119 288 if ( empty( $matches[1] ) ) {
120 289 return array();
121 290 }
@@ -159,8 +328,58 @@
159 328 return $url_mappings;
160 329 }
161 330
162 331 /**
332 + * Process inline images from content (for direct file storage).
333 + *
334 + * @param int $object_id The post or comment ID.
335 + * @param string $object_type The object type ('post' or 'comment').
336 + *
337 + * @return array Array of URL mappings (old URL => new URL).
338 + */
339 + private static function import_inline_files( $object_id, $object_type ) {
340 + $content = self::get_object_content( $object_id, $object_type );
341 + if ( ! $content ) {
342 + return array();
343 + }
344 +
345 + // Find all img tags in the content.
346 + preg_match_all( '/<img[^>]+src=["\']([^"\']+)["\'][^>]*>/i', $content, $matches );
347 +
348 + if ( empty( $matches[1] ) ) {
349 + return array();
350 + }
351 +
352 + $url_mappings = array();
353 +
354 + foreach ( $matches[1] as $image_url ) {
355 + // Skip if already processed.
356 + if ( isset( $url_mappings[ $image_url ] ) ) {
357 + continue;
358 + }
359 +
360 + $file_data = self::save_file( array( 'url' => $image_url ), $object_id, $object_type );
361 +
362 + if ( \is_wp_error( $file_data ) ) {
363 + continue;
364 + }
365 +
366 + $new_url = $file_data['url'];
367 + if ( $new_url ) {
368 + $url_mappings[ $image_url ] = $new_url;
369 + $content = \str_replace( $image_url, $new_url, $content );
370 + }
371 + }
372 +
373 + // Update content if URLs were replaced.
374 + if ( ! empty( $url_mappings ) ) {
375 + self::update_object_content( $object_id, $object_type, $content );
376 + }
377 +
378 + return $url_mappings;
379 + }
380 +
381 + /**
163 382 * Normalize an ActivityPub attachment object to a standard format.
164 383 *
165 384 * @param mixed $attachment The attachment data (array or object).
166 385 *
@@ -171,9 +390,9 @@
171 390 if ( \is_object( $attachment ) ) {
172 391 $attachment = \get_object_vars( $attachment );
173 392 }
174 393
175 - if ( ! \is_array( $attachment ) || empty( $attachment['url'] ) ) {
394 + if ( ! is_array( $attachment ) || empty( $attachment['url'] ) ) {
176 395 return false;
177 396 }
178 397
179 398 return array(
@@ -200,38 +419,24 @@
200 419 require_once ABSPATH . 'wp-admin/includes/file.php';
201 420 require_once ABSPATH . 'wp-admin/includes/image.php';
202 421 }
203 422
204 - // Use WP_Filesystem_Direct explicitly to avoid FTP fallback from WP_Filesystem().
205 - require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-base.php';
206 - require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-direct.php';
423 + $is_local = ! preg_match( '#^https?://#i', $attachment_data['url'] );
207 424
208 - $filesystem = new \WP_Filesystem_Direct( null );
209 -
210 - $is_local = ! \preg_match( '#^https?://#i', $attachment_data['url'] );
211 -
212 425 if ( $is_local ) {
213 - // Validate local path is within allowed directories to prevent file disclosure.
214 - $allowed = self::is_allowed_local_path( $attachment_data['url'] );
215 - if ( ! $allowed ) {
216 - return new \WP_Error( 'invalid_path', \__( 'Local file path is not within allowed directories.', 'activitypub' ) );
217 - }
426 + // Read local file from disk.
427 + \WP_Filesystem();
428 + global $wp_filesystem;
218 429
219 - // Read local file from disk.
220 - if ( ! $filesystem->exists( $attachment_data['url'] ) ) {
430 + if ( ! $wp_filesystem->exists( $attachment_data['url'] ) ) {
221 431 /* translators: %s: file path */
222 - return new \WP_Error( 'file_not_found', \sprintf( \__( 'File not found: %s', 'activitypub' ), $attachment_data['url'] ) );
432 + return new \WP_Error( 'file_not_found', sprintf( \__( 'File not found: %s', 'activitypub' ), $attachment_data['url'] ) );
223 433 }
224 434
225 435 // Copy to temp file so media_handle_sideload doesn't move the original.
226 436 $tmp_file = \wp_tempnam( \basename( $attachment_data['url'] ) );
227 - $filesystem->copy( $attachment_data['url'], $tmp_file, true );
437 + $wp_filesystem->copy( $attachment_data['url'], $tmp_file, true );
228 438 } else {
229 - // Validate remote URL before downloading.
230 - if ( ! \wp_http_validate_url( $attachment_data['url'] ) ) {
231 - return new \WP_Error( 'invalid_url', \__( 'URL is not allowed.', 'activitypub' ) );
232 - }
233 -
234 439 // Download remote URL.
235 440 $tmp_file = \download_url( $attachment_data['url'] );
236 441
237 442 if ( \is_wp_error( $tmp_file ) ) {
@@ -238,41 +443,21 @@
238 443 return $tmp_file;
239 444 }
240 445 }
241 446
242 - // Get original filename from URL.
243 - $original_name = \basename( \wp_parse_url( $attachment_data['url'], PHP_URL_PATH ) );
244 -
245 - // Rename temp file to have proper extension for optimize_image to detect mime type.
246 - $original_ext = \pathinfo( $original_name, PATHINFO_EXTENSION );
247 - if ( $original_ext ) {
248 - $renamed_tmp = $tmp_file . '.' . $original_ext;
249 - if ( $filesystem->move( $tmp_file, $renamed_tmp, true ) ) {
250 - $tmp_file = $renamed_tmp;
251 - }
252 - }
253 -
254 - // Optimize images before sideloading (resize and convert to WebP).
255 - $tmp_file = self::optimize_image( $tmp_file, self::MAX_IMAGE_DIMENSION );
256 -
257 - // Update filename extension to match optimized file.
258 - $new_ext = \pathinfo( $tmp_file, PATHINFO_EXTENSION );
259 - if ( $new_ext ) {
260 - $original_name = \preg_replace( '/\.[^.]+$/', '.' . $new_ext, $original_name );
261 - }
262 -
447 + // Prepare file array for WordPress.
263 448 $file_array = array(
264 - 'name' => $original_name,
449 + 'name' => \basename( \wp_parse_url( $attachment_data['url'], PHP_URL_PATH ) ),
265 450 'tmp_name' => $tmp_file,
266 451 );
267 452
268 453 // Prepare attachment post data.
269 - // Let WordPress auto-detect the mime type from the file.
270 454 $post_data = array(
271 - 'post_title' => $attachment_data['name'] ?? '',
272 - 'post_content' => $attachment_data['name'] ?? '',
273 - 'post_author' => $author_id,
274 - 'meta_input' => array(
455 + 'post_mime_type' => $attachment_data['mediaType'] ?? '',
456 + 'post_title' => $attachment_data['name'] ?? '',
457 + 'post_content' => $attachment_data['name'] ?? '',
458 + 'post_author' => $author_id,
459 + 'meta_input' => array(
275 460 '_source_url' => $attachment_data['url'],
276 461 ),
277 462 );
278 463
@@ -277,10 +462,10 @@
277 462 );
278 463
279 464 // Add alt text for images.
280 465 if ( ! empty( $attachment_data['name'] ) ) {
281 - $original_mime = $attachment_data['mediaType'] ?? '';
282 - if ( 'image' === \strtok( $original_mime, '/' ) ) {
466 + $mime_type = $attachment_data['mediaType'] ?? '';
467 + if ( 'image' === strtok( $mime_type, '/' ) ) {
283 468 $post_data['meta_input']['_wp_attachment_image_alt'] = $attachment_data['name'];
284 469 }
285 470 }
286 471
@@ -295,156 +480,76 @@
295 480 return $attachment_id;
296 481 }
297 482
298 483 /**
299 - * Get a unique file path by appending a counter if the file already exists.
484 + * Save a file directly to uploads/activitypub/{type}/{id}/.
300 485 *
301 - * @param string $file_path The desired file path.
486 + * @param array $attachment_data The normalized attachment data.
487 + * @param int $object_id The post or comment ID to attach to.
488 + * @param string $object_type The object type ('post' or 'comment').
302 489 *
303 - * @return string A unique file path that doesn't exist.
304 - */
305 - private static function get_unique_path( $file_path ) {
306 - if ( ! \file_exists( $file_path ) ) {
307 - return $file_path;
308 - }
309 -
310 - $path_info = \pathinfo( $file_path );
311 - $dir = $path_info['dirname'];
312 - $base_name = $path_info['filename'];
313 - $extension = isset( $path_info['extension'] ) ? '.' . $path_info['extension'] : '';
314 - $counter = 1;
315 -
316 - do {
317 - $new_path = $dir . '/' . $base_name . '-' . $counter . $extension;
318 - ++$counter;
319 - } while ( \file_exists( $new_path ) );
320 -
321 - return $new_path;
322 - }
323 -
324 - /**
325 - * Check if a local file path is within allowed directories.
490 + * @return array|\WP_Error {
491 + * Array of file data on success, WP_Error on failure.
326 492 *
327 - * Prevents arbitrary file access by restricting local paths to known safe
328 - * directories like the uploads folder or WordPress temp directory.
329 - *
330 - * @param string $file_path The local file path to validate.
331 - *
332 - * @return bool True if the path is allowed, false otherwise.
493 + * @type string $url Full URL to the saved file.
494 + * @type string $mime_type MIME type of the file.
495 + * @type string $alt Alt text from attachment name field.
496 + * }
333 497 */
334 - private static function is_allowed_local_path( $file_path ) {
335 - // Normalize the path and resolve any relative components.
336 - $real_path = \realpath( $file_path );
337 - if ( false === $real_path ) {
338 - // If file doesn't exist yet, check the directory.
339 - $dir_path = \realpath( \dirname( $file_path ) );
340 - if ( false === $dir_path ) {
341 - return false;
342 - }
343 - $real_path = $dir_path . '/' . \basename( $file_path );
498 + private static function save_file( $attachment_data, $object_id, $object_type ) {
499 + if ( ! \function_exists( 'download_url' ) ) {
500 + require_once ABSPATH . 'wp-admin/includes/file.php';
344 501 }
345 502
346 - // Get allowed base directories.
347 - $upload_dir = \wp_upload_dir();
348 - $allowed_dirs = array(
349 - \realpath( $upload_dir['basedir'] ),
350 - \realpath( \get_temp_dir() ),
351 - \realpath( ABSPATH . 'wp-content' ),
352 - );
503 + // Download remote URL.
504 + $tmp_file = \download_url( $attachment_data['url'] );
353 505
354 - /**
355 - * Filters the allowed directories for local file imports.
356 - *
357 - * @since 5.6.0
358 - *
359 - * @param string[] $allowed_dirs Array of allowed directory paths.
360 - * @param string $file_path The file path being validated.
361 - */
362 - $allowed_dirs = \apply_filters( 'activitypub_allowed_import_directories', $allowed_dirs, $file_path );
363 -
364 - // Remove any false values from realpath failures.
365 - $allowed_dirs = \array_filter( $allowed_dirs );
366 -
367 - // Check if the file is within any allowed directory.
368 - foreach ( $allowed_dirs as $allowed_dir ) {
369 - if ( \str_starts_with( $real_path, $allowed_dir ) ) {
370 - return true;
371 - }
506 + if ( \is_wp_error( $tmp_file ) ) {
507 + return $tmp_file;
372 508 }
373 509
374 - return false;
375 - }
510 + // Get storage paths for this object.
511 + $paths = self::get_storage_paths( $object_id, $object_type );
376 512
377 - /**
378 - * Optimize an image file by resizing and converting to WebP.
379 - *
380 - * Uses WordPress image editor to resize large images and convert them
381 - * to WebP format for better compression while maintaining quality.
382 - *
383 - * @param string $file_path Path to the image file.
384 - * @param int $max_dimension Maximum width/height in pixels.
385 - *
386 - * @return string The optimized file path.
387 - */
388 - private static function optimize_image( $file_path, $max_dimension ) {
389 - // Check if it's an image.
390 - $mime_type = \wp_check_filetype( $file_path )['type'] ?? '';
391 - if ( ! $mime_type || ! \str_starts_with( $mime_type, 'image/' ) ) {
392 - return $file_path;
393 - }
513 + // Create directory if it doesn't exist.
514 + \wp_mkdir_p( $paths['basedir'] );
394 515
395 - // Skip SVG and GIF files (GIFs may be animated).
396 - if ( \in_array( $mime_type, array( 'image/svg+xml', 'image/gif' ), true ) ) {
397 - return $file_path;
398 - }
516 + // Generate unique file name.
517 + $url_path = \wp_parse_url( $attachment_data['url'], PHP_URL_PATH );
518 + $file_name = \sanitize_file_name( \basename( $url_path ) );
519 + $file_path = $paths['basedir'] . '/' . $file_name;
399 520
400 - $editor = \wp_get_image_editor( $file_path );
401 - if ( \is_wp_error( $editor ) ) {
402 - return $file_path;
403 - }
521 + // Initialize filesystem if needed.
522 + \WP_Filesystem();
523 + global $wp_filesystem;
404 524
405 - $size = $editor->get_size();
406 - $needs_resize = $size['width'] > $max_dimension || $size['height'] > $max_dimension;
407 -
408 - // Resize if needed.
409 - if ( $needs_resize ) {
410 - $editor->resize( $max_dimension, $max_dimension, false );
411 - }
412 -
413 - // Check if WebP is supported.
414 - $can_webp = $editor->supports_mime_type( 'image/webp' );
415 -
416 - // Determine output format and save.
417 - if ( $can_webp ) {
418 - // Convert to WebP.
419 - $new_path = self::get_unique_path( \preg_replace( '/\.[^.]+$/', '.webp', $file_path ) );
420 - $result = $editor->save( $new_path, 'image/webp' );
421 - } elseif ( \in_array( $mime_type, array( 'image/png', 'image/webp' ), true ) ) {
422 - // Keep original format for potentially transparent images when WebP not available.
423 - if ( ! $needs_resize ) {
424 - // No changes needed.
425 - return $file_path;
525 + // Make sure file name is unique.
526 + $counter = 1;
527 + while ( $wp_filesystem->exists( $file_path ) ) {
528 + $path_info = pathinfo( $file_name );
529 + $file_name = $path_info['filename'] . '-' . $counter;
530 + if ( ! empty( $path_info['extension'] ) ) {
531 + $file_name .= '.' . $path_info['extension'];
426 532 }
427 - $result = $editor->save( $file_path );
428 - } else {
429 - // Convert to JPEG when WebP not available.
430 - $new_path = self::get_unique_path( \preg_replace( '/\.[^.]+$/', '.jpg', $file_path ) );
431 - $result = $editor->save( $new_path, 'image/jpeg' );
533 + $file_path = $paths['basedir'] . '/' . $file_name;
534 + ++$counter;
432 535 }
433 536
434 - if ( \is_wp_error( $result ) ) {
435 - return $file_path;
537 + // Move file to destination.
538 + if ( ! $wp_filesystem->move( $tmp_file, $file_path, true ) ) {
539 + \wp_delete_file( $tmp_file );
540 + return new \WP_Error( 'file_move_failed', \__( 'Failed to move file to destination.', 'activitypub' ) );
436 541 }
437 542
438 - // Handle result - $result is always an array from $editor->save().
439 - $result_path = $result['path'] ?? $file_path;
543 + // Get mime type and validate file.
544 + $file_info = \wp_check_filetype_and_ext( $file_path, $file_name );
545 + $mime_type = $file_info['type'] ?? $attachment_data['mediaType'] ?? '';
440 546
441 - // If path changed (format conversion), delete the original file.
442 - if ( $result_path !== $file_path ) {
443 - \wp_delete_file( $file_path );
444 - }
445 -
446 - return $result_path;
547 + return array(
548 + 'url' => $paths['baseurl'] . '/' . $file_name,
549 + 'mime_type' => $mime_type,
550 + 'alt' => $attachment_data['name'] ?? '',
551 + );
447 552 }
448 553
449 554 /**
450 555 * Append media to post content.
@@ -458,9 +563,9 @@
458 563 return;
459 564 }
460 565
461 566 $media = self::generate_media_markup( $attachment_ids );
462 - $separator = empty( \trim( $post->post_content ) ) ? '' : "\n\n";
567 + $separator = empty( trim( $post->post_content ) ) ? '' : "\n\n";
463 568
464 569 \wp_update_post(
465 570 array(
466 571 'ID' => $post_id,
@@ -469,8 +574,27 @@
469 574 );
470 575 }
471 576
472 577 /**
578 + * Append file-based media to content.
579 + *
580 + * @param int $object_id The post or comment ID.
581 + * @param array[] $files Array of file data arrays.
582 + * @param string $object_type The object type ('post' or 'comment').
583 + */
584 + private static function append_files_to_content( $object_id, $files, $object_type ) {
585 + $content = self::get_object_content( $object_id, $object_type );
586 + if ( empty( $content ) ) {
587 + return;
588 + }
589 +
590 + $media = self::generate_files_markup( $files );
591 + $separator = empty( trim( $content ) ) ? '' : "\n\n";
592 +
593 + self::update_object_content( $object_id, $object_type, $content . $separator . $media );
594 + }
595 +
596 + /**
473 597 * Generate media markup for attachments.
474 598 *
475 599 * @param int[] $attachment_ids Array of attachment IDs.
476 600 *
@@ -497,13 +621,13 @@
497 621 return $custom_markup;
498 622 }
499 623
500 624 // Default to block markup.
501 - $type = \strtok( \get_post_mime_type( $attachment_ids[0] ), '/' );
625 + $type = strtok( \get_post_mime_type( $attachment_ids[0] ), '/' );
502 626
503 627 // Single video or audio file.
504 628 if ( 1 === \count( $attachment_ids ) && ( 'video' === $type || 'audio' === $type ) ) {
505 - return \sprintf(
629 + return sprintf(
506 630 '<!-- wp:%1$s {"id":"%2$s"} --><figure class="wp-block-%1$s"><%1$s controls src="%3$s"></%1$s></figure><!-- /wp:%1$s -->',
507 631 \esc_attr( $type ),
508 632 \esc_attr( $attachment_ids[0] ),
509 633 \esc_url( \wp_get_attachment_url( $attachment_ids[0] ) )
@@ -519,139 +643,10 @@
519 643 return self::get_gallery_block( $attachment_ids );
520 644 }
521 645
522 646 /**
523 - * Get standalone image block markup.
524 - *
525 - * @param int $attachment_id The attachment ID.
526 - *
527 - * @return string The image block markup.
528 - */
529 - private static function get_image_block( $attachment_id ) {
530 - $image_src = \wp_get_attachment_image_src( $attachment_id, 'large' );
531 - if ( ! $image_src ) {
532 - return '';
533 - }
534 -
535 - $alt = \get_post_meta( $attachment_id, '_wp_attachment_image_alt', true );
536 - if ( ! $alt ) {
537 - $alt = \get_post_field( 'post_excerpt', $attachment_id );
538 - }
539 -
540 - $block = '<!-- wp:image {"id":' . \esc_attr( $attachment_id ) . ',"sizeSlug":"large","linkDestination":"none"} -->' . "\n";
541 - $block .= '<figure class="wp-block-image size-large">';
542 - $block .= '<img src="' . \esc_url( $image_src[0] ) . '" alt="' . \esc_attr( $alt ) . '" class="' . \esc_attr( 'wp-image-' . $attachment_id ) . '"/>';
543 - $block .= '</figure>' . "\n";
544 - $block .= '<!-- /wp:image -->';
545 -
546 - return $block;
547 - }
548 -
549 - /**
550 - * Get gallery block markup.
551 - *
552 - * @param int[] $attachment_ids The attachment IDs to use.
553 - *
554 - * @return string The gallery block markup.
555 - */
556 - private static function get_gallery_block( $attachment_ids ) {
557 - $gallery = '<!-- wp:gallery {"columns":2,"linkTo":"none","sizeSlug":"large","imageCrop":true} -->' . "\n";
558 - $gallery .= '<figure class="wp-block-gallery has-nested-images columns-2 is-cropped">';
559 -
560 - foreach ( $attachment_ids as $id ) {
561 - $image_src = \wp_get_attachment_image_src( $id, 'large' );
562 - if ( ! $image_src ) {
563 - continue;
564 - }
565 -
566 - $alt = \get_post_meta( $id, '_wp_attachment_image_alt', true );
567 - if ( ! $alt ) {
568 - $alt = \get_post_field( 'post_excerpt', $id );
569 - }
570 -
571 - $gallery .= "\n" . '<!-- wp:image {"id":' . \esc_attr( $id ) . ',"sizeSlug":"large","linkDestination":"none"} -->' . "\n";
572 - $gallery .= '<figure class="wp-block-image size-large">';
573 - $gallery .= '<img src="' . \esc_url( $image_src[0] ) . '" alt="' . \esc_attr( $alt ) . '" class="' . \esc_attr( 'wp-image-' . $id ) . '"/>';
574 - $gallery .= '</figure>';
575 - $gallery .= "\n<!-- /wp:image -->\n";
576 - }
577 -
578 - $gallery .= "</figure>\n";
579 - $gallery .= '<!-- /wp:gallery -->';
580 -
581 - return $gallery;
582 - }
583 -
584 - /**
585 - * Get content from an object based on its type.
586 - *
587 - * @param int $object_id The object ID (post or comment).
588 - * @param string $object_type The object type ('post' or 'comment').
589 - *
590 - * @return string The object content.
591 - */
592 - private static function get_object_content( $object_id, $object_type ) {
593 - if ( 'comment' === $object_type ) {
594 - $comment = \get_comment( $object_id );
595 - return $comment ? $comment->comment_content : '';
596 - }
597 -
598 - return \get_post_field( 'post_content', $object_id );
599 - }
600 -
601 - /**
602 - * Update content for an object based on its type.
603 - *
604 - * @param int $object_id The object ID (post or comment).
605 - * @param string $object_type The object type ('post' or 'comment').
606 - * @param string $content The new content.
607 - */
608 - private static function update_object_content( $object_id, $object_type, $content ) {
609 - if ( 'comment' === $object_type ) {
610 - \wp_update_comment(
611 - array(
612 - 'comment_ID' => $object_id,
613 - 'comment_content' => $content,
614 - )
615 - );
616 - } else {
617 - \wp_update_post(
618 - array(
619 - 'ID' => $object_id,
620 - 'post_content' => $content,
621 - )
622 - );
623 - }
624 - }
625 -
626 - /**
627 - * Append file-based media markup to an object's content.
628 - *
629 - * Used for cached remote media (via Cache classes) that doesn't go through
630 - * the Media Library. Works with posts and comments.
631 - *
632 - * @param int $object_id The object ID (post or comment).
633 - * @param array $files Array of file data arrays with 'url', 'mime_type', and 'alt' keys.
634 - * @param string $object_type The object type ('post' or 'comment').
635 - */
636 - public static function append_files_to_content( $object_id, $files, $object_type = 'post' ) {
637 - $content = self::get_object_content( $object_id, $object_type );
638 - if ( empty( $content ) ) {
639 - return;
640 - }
641 -
642 - $media = self::generate_files_markup( $files );
643 - $separator = empty( \trim( $content ) ) ? '' : "\n\n";
644 -
645 - self::update_object_content( $object_id, $object_type, $content . $separator . $media );
646 - }
647 -
648 - /**
649 647 * Generate media markup for file-based attachments.
650 648 *
651 - * Creates WordPress block markup from file data arrays. Used for cached
652 - * remote media that doesn't have WordPress attachment posts.
653 - *
654 649 * @param array[] $files {
655 650 * Array of file data arrays.
656 651 *
657 652 * @type string $url Full URL to the file.
@@ -660,9 +655,9 @@
660 655 * }
661 656 *
662 657 * @return string The generated markup.
663 658 */
664 - public static function generate_files_markup( $files ) {
659 + private static function generate_files_markup( $files ) {
665 660 if ( empty( $files ) ) {
666 661 return '';
667 662 }
668 663
@@ -682,13 +677,13 @@
682 677 return $custom_markup;
683 678 }
684 679
685 680 // Default to block markup.
686 - $type = \strtok( $files[0]['mime_type'], '/' );
681 + $type = strtok( $files[0]['mime_type'], '/' );
687 682
688 683 // Single video or audio file.
689 684 if ( 1 === \count( $files ) && ( 'video' === $type || 'audio' === $type ) ) {
690 - return \sprintf(
685 + return sprintf(
691 686 '<!-- wp:%1$s --><figure class="wp-block-%1$s"><%1$s controls src="%2$s"></%1$s></figure><!-- /wp:%1$s -->',
692 687 \esc_attr( $type ),
693 688 \esc_url( $files[0]['url'] )
694 689 );
@@ -715,12 +710,12 @@
715 710 * }
716 711 *
717 712 * @return string The image block markup.
718 713 */
719 - public static function get_files_image_block( $file ) {
714 + private static function get_files_image_block( $file ) {
720 715 $block = '<!-- wp:image {"sizeSlug":"large","linkDestination":"none"} -->' . "\n";
721 716 $block .= '<figure class="wp-block-image size-large">';
722 - $block .= '<img src="' . \esc_url( $file['url'] ) . '" alt="' . \esc_attr( $file['alt'] ?? '' ) . '"/>';
717 + $block .= '<img src="' . \esc_url( $file['url'] ) . '" alt="' . \esc_attr( $file['alt'] ) . '"/>';
723 718 $block .= '</figure>' . "\n";
724 719 $block .= '<!-- /wp:image -->';
725 720
726 721 return $block;
@@ -726,8 +721,70 @@
726 721 return $block;
727 722 }
728 723
729 724 /**
725 + * Get standalone image block markup.
726 + *
727 + * @param int $attachment_id The attachment ID.
728 + *
729 + * @return string The image block markup.
730 + */
731 + private static function get_image_block( $attachment_id ) {
732 + $image_src = \wp_get_attachment_image_src( $attachment_id, 'large' );
733 + if ( ! $image_src ) {
734 + return '';
735 + }
736 +
737 + $alt = \get_post_meta( $attachment_id, '_wp_attachment_image_alt', true );
738 + if ( ! $alt ) {
739 + $alt = \get_post_field( 'post_excerpt', $attachment_id );
740 + }
741 +
742 + $block = '<!-- wp:image {"id":' . \esc_attr( $attachment_id ) . ',"sizeSlug":"large","linkDestination":"none"} -->' . "\n";
743 + $block .= '<figure class="wp-block-image size-large">';
744 + $block .= '<img src="' . \esc_url( $image_src[0] ) . '" alt="' . \esc_attr( $alt ) . '" class="' . \esc_attr( 'wp-image-' . $attachment_id ) . '"/>';
745 + $block .= '</figure>' . "\n";
746 + $block .= '<!-- /wp:image -->';
747 +
748 + return $block;
749 + }
750 +
751 + /**
752 + * Get gallery block markup.
753 + *
754 + * @param int[] $attachment_ids The attachment IDs to use.
755 + *
756 + * @return string The gallery block markup.
757 + */
758 + private static function get_gallery_block( $attachment_ids ) {
759 + $gallery = '<!-- wp:gallery {"columns":2,"linkTo":"none","sizeSlug":"large","imageCrop":true} -->' . "\n";
760 + $gallery .= '<figure class="wp-block-gallery has-nested-images columns-2 is-cropped">';
761 +
762 + foreach ( $attachment_ids as $id ) {
763 + $image_src = \wp_get_attachment_image_src( $id, 'large' );
764 + if ( ! $image_src ) {
765 + continue;
766 + }
767 +
768 + $alt = \get_post_meta( $id, '_wp_attachment_image_alt', true );
769 + if ( ! $alt ) {
770 + $alt = \get_post_field( 'post_excerpt', $id );
771 + }
772 +
773 + $gallery .= "\n" . '<!-- wp:image {"id":' . \esc_attr( $id ) . ',"sizeSlug":"large","linkDestination":"none"} -->' . "\n";
774 + $gallery .= '<figure class="wp-block-image size-large">';
775 + $gallery .= '<img src="' . \esc_url( $image_src[0] ) . '" alt="' . \esc_attr( $alt ) . '" class="' . \esc_attr( 'wp-image-' . $id ) . '"/>';
776 + $gallery .= '</figure>';
777 + $gallery .= "\n<!-- /wp:image -->\n";
778 + }
779 +
780 + $gallery .= "</figure>\n";
781 + $gallery .= '<!-- /wp:gallery -->';
782 +
783 + return $gallery;
784 + }
785 +
786 + /**
730 787 * Get gallery block markup for file-based attachments.
731 788 *
732 789 * @param array[] $files {
733 790 * Array of file data arrays.
@@ -738,9 +795,9 @@
738 795 * }
739 796 *
740 797 * @return string The gallery block markup.
741 798 */
742 - public static function get_files_gallery_block( $files ) {
799 + private static function get_files_gallery_block( $files ) {
743 800 $gallery = '<!-- wp:gallery {"columns":2,"linkTo":"none","imageCrop":true} -->' . "\n";
744 801 $gallery .= '<figure class="wp-block-gallery has-nested-images columns-2 is-cropped">';
745 802
746 803 foreach ( $files as $file ) {
@@ -745,9 +802,9 @@
745 802
746 803 foreach ( $files as $file ) {
747 804 $gallery .= "\n<!-- wp:image {\"sizeSlug\":\"large\",\"linkDestination\":\"none\"} -->\n";
748 805 $gallery .= '<figure class="wp-block-image size-large">';
749 - $gallery .= '<img src="' . \esc_url( $file['url'] ) . '" alt="' . \esc_attr( $file['alt'] ?? '' ) . '"/>';
806 + $gallery .= '<img src="' . \esc_url( $file['url'] ) . '" alt="' . \esc_attr( $file['alt'] ) . '"/>';
750 807 $gallery .= '</figure>';
751 808 $gallery .= "\n<!-- /wp:image -->\n";
752 809 }
753 810