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
activitypub / includes / class-attachments.php

class-attachments.php in ActivityPub 7.7.0, at includes/class-attachments.php

817 lines 24.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Attachments processing file.
4 *
5 * @package Activitypub
6 */
7
8 namespace Activitypub;
9
10 use Activitypub\Collection\Posts;
11
12 /**
13 * Attachments processor class.
14 */
15 class Attachments {
16 /**
17 * Directory for storing ap_post media files.
18 *
19 * @var string
20 */
21 public static $ap_posts_dir = '/activitypub/ap_posts/';
22
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 /**
60 * Import attachments from an ActivityPub object and attach them to a post.
61 *
62 * Creates full WordPress attachment posts in the media library. Each attachment
63 * becomes a searchable, manageable attachment post that appears in the WordPress
64 * Media Library and is part of the user's content.
65 *
66 * Use this when:
67 * - Importing content that will be owned and editable by the user.
68 * - You need WordPress attachment posts with full metadata support.
69 * - Media should be searchable and manageable in the Media Library.
70 * - Working with content that will be part of the user's site (e.g., importers).
71 *
72 * @param array $attachments Array of ActivityPub attachment objects.
73 * @param int $post_id The post ID to attach files to.
74 * @param int $author_id Optional. User ID to set as attachment author. Default 0.
75 *
76 * @return array Array of attachment IDs.
77 */
78 public static function import( $attachments, $post_id, $author_id = 0 ) {
79 // First, import inline images from the post content.
80 $inline_mappings = self::import_inline_images( $post_id, $author_id );
81
82 if ( empty( $attachments ) || ! is_array( $attachments ) ) {
83 return array();
84 }
85
86 $attachment_ids = array();
87 foreach ( $attachments as $attachment ) {
88 $attachment_data = self::normalize_attachment( $attachment );
89
90 if ( empty( $attachment_data['url'] ) ) {
91 continue;
92 }
93
94 // Skip if this URL was already processed as an inline image.
95 if ( isset( $inline_mappings[ $attachment_data['url'] ] ) ) {
96 continue;
97 }
98
99 $attachment_id = self::save_attachment( $attachment_data, $post_id, $author_id );
100
101 if ( ! \is_wp_error( $attachment_id ) ) {
102 $attachment_ids[] = $attachment_id;
103 }
104 }
105
106 // Append media markup to post content.
107 if ( ! empty( $attachment_ids ) ) {
108 self::append_media_to_post_content( $post_id, $attachment_ids );
109 }
110
111 return $attachment_ids;
112 }
113
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 /**
254 * Check if an attachment with the same source URL already exists for a post.
255 *
256 * @param string $source_url The source URL to check.
257 * @param int $post_id The post ID to check attachments for.
258 *
259 * @return int|false The existing attachment ID or false if not found.
260 */
261 private static function get_existing_attachment( $source_url, $post_id ) {
262 foreach ( \get_attached_media( '', $post_id ) as $attachment ) {
263 if ( \get_post_meta( $attachment->ID, '_source_url', true ) === $source_url ) {
264 return $attachment->ID;
265 }
266 }
267
268 return false;
269 }
270
271 /**
272 * Process inline images from post content.
273 *
274 * @param int $post_id The post ID.
275 * @param int $author_id Optional. User ID to set as attachment author. Default 0.
276 *
277 * @return array Array of URL mappings (old URL => new URL).
278 */
279 private static function import_inline_images( $post_id, $author_id = 0 ) {
280 $post = \get_post( $post_id );
281 if ( ! $post || empty( $post->post_content ) ) {
282 return array();
283 }
284
285 // Find all img tags in the content.
286 preg_match_all( '/<img[^>]+src=["\']([^"\']+)["\'][^>]*>/i', $post->post_content, $matches );
287
288 if ( empty( $matches[1] ) ) {
289 return array();
290 }
291
292 $url_mappings = array();
293 $content = $post->post_content;
294
295 foreach ( $matches[1] as $image_url ) {
296 // Skip if already processed or is a local URL.
297 if ( isset( $url_mappings[ $image_url ] ) ) {
298 continue;
299 }
300
301 // Check if this image was already processed as an attachment.
302 $attachment_id = self::get_existing_attachment( $image_url, $post_id );
303 if ( ! $attachment_id ) {
304 $attachment_id = self::save_attachment( array( 'url' => $image_url ), $post_id, $author_id );
305
306 if ( \is_wp_error( $attachment_id ) ) {
307 continue;
308 }
309 }
310
311 $new_url = \wp_get_attachment_url( $attachment_id );
312 if ( $new_url ) {
313 $url_mappings[ $image_url ] = $new_url;
314 $content = \str_replace( $image_url, $new_url, $content );
315 }
316 }
317
318 // Update post content if URLs were replaced.
319 if ( ! empty( $url_mappings ) ) {
320 \wp_update_post(
321 array(
322 'ID' => $post_id,
323 'post_content' => $content,
324 )
325 );
326 }
327
328 return $url_mappings;
329 }
330
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 /**
382 * Normalize an ActivityPub attachment object to a standard format.
383 *
384 * @param mixed $attachment The attachment data (array or object).
385 *
386 * @return array|false Normalized attachment data or false on failure.
387 */
388 private static function normalize_attachment( $attachment ) {
389 // Convert object to array if needed.
390 if ( \is_object( $attachment ) ) {
391 $attachment = \get_object_vars( $attachment );
392 }
393
394 if ( ! is_array( $attachment ) || empty( $attachment['url'] ) ) {
395 return false;
396 }
397
398 return array(
399 'url' => $attachment['url'],
400 'mediaType' => $attachment['mediaType'] ?? '',
401 'name' => $attachment['name'] ?? '',
402 'type' => $attachment['type'] ?? 'Document',
403 );
404 }
405
406 /**
407 * Save an attachment (local file or remote URL) to the media library.
408 *
409 * @param array $attachment_data The normalized attachment data.
410 * @param int $post_id The post ID to attach to.
411 * @param int $author_id Optional. User ID to set as attachment author. Default 0.
412 *
413 * @return int|\WP_Error The attachment ID or WP_Error on failure.
414 */
415 private static function save_attachment( $attachment_data, $post_id, $author_id = 0 ) {
416 // Ensure required WordPress functions are loaded.
417 if ( ! \function_exists( 'media_handle_sideload' ) || ! \function_exists( 'download_url' ) ) {
418 require_once ABSPATH . 'wp-admin/includes/media.php';
419 require_once ABSPATH . 'wp-admin/includes/file.php';
420 require_once ABSPATH . 'wp-admin/includes/image.php';
421 }
422
423 $is_local = ! preg_match( '#^https?://#i', $attachment_data['url'] );
424
425 if ( $is_local ) {
426 // Read local file from disk.
427 \WP_Filesystem();
428 global $wp_filesystem;
429
430 if ( ! $wp_filesystem->exists( $attachment_data['url'] ) ) {
431 /* translators: %s: file path */
432 return new \WP_Error( 'file_not_found', sprintf( \__( 'File not found: %s', 'activitypub' ), $attachment_data['url'] ) );
433 }
434
435 // Copy to temp file so media_handle_sideload doesn't move the original.
436 $tmp_file = \wp_tempnam( \basename( $attachment_data['url'] ) );
437 $wp_filesystem->copy( $attachment_data['url'], $tmp_file, true );
438 } else {
439 // Download remote URL.
440 $tmp_file = \download_url( $attachment_data['url'] );
441
442 if ( \is_wp_error( $tmp_file ) ) {
443 return $tmp_file;
444 }
445 }
446
447 // Prepare file array for WordPress.
448 $file_array = array(
449 'name' => \basename( \wp_parse_url( $attachment_data['url'], PHP_URL_PATH ) ),
450 'tmp_name' => $tmp_file,
451 );
452
453 // Prepare attachment post data.
454 $post_data = 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(
460 '_source_url' => $attachment_data['url'],
461 ),
462 );
463
464 // Add alt text for images.
465 if ( ! empty( $attachment_data['name'] ) ) {
466 $mime_type = $attachment_data['mediaType'] ?? '';
467 if ( 'image' === strtok( $mime_type, '/' ) ) {
468 $post_data['meta_input']['_wp_attachment_image_alt'] = $attachment_data['name'];
469 }
470 }
471
472 // Sideload the attachment into WordPress.
473 $attachment_id = \media_handle_sideload( $file_array, $post_id, '', $post_data );
474
475 // Clean up temp file if there was an error.
476 if ( \is_wp_error( $attachment_id ) ) {
477 \wp_delete_file( $tmp_file );
478 }
479
480 return $attachment_id;
481 }
482
483 /**
484 * Save a file directly to uploads/activitypub/{type}/{id}/.
485 *
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').
489 *
490 * @return array|\WP_Error {
491 * Array of file data on success, WP_Error on failure.
492 *
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 * }
497 */
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';
501 }
502
503 // Download remote URL.
504 $tmp_file = \download_url( $attachment_data['url'] );
505
506 if ( \is_wp_error( $tmp_file ) ) {
507 return $tmp_file;
508 }
509
510 // Get storage paths for this object.
511 $paths = self::get_storage_paths( $object_id, $object_type );
512
513 // Create directory if it doesn't exist.
514 \wp_mkdir_p( $paths['basedir'] );
515
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;
520
521 // Initialize filesystem if needed.
522 \WP_Filesystem();
523 global $wp_filesystem;
524
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'];
532 }
533 $file_path = $paths['basedir'] . '/' . $file_name;
534 ++$counter;
535 }
536
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' ) );
541 }
542
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'] ?? '';
546
547 return array(
548 'url' => $paths['baseurl'] . '/' . $file_name,
549 'mime_type' => $mime_type,
550 'alt' => $attachment_data['name'] ?? '',
551 );
552 }
553
554 /**
555 * Append media to post content.
556 *
557 * @param int $post_id The post ID.
558 * @param int[] $attachment_ids Array of attachment IDs.
559 */
560 private static function append_media_to_post_content( $post_id, $attachment_ids ) {
561 $post = \get_post( $post_id );
562 if ( ! $post ) {
563 return;
564 }
565
566 $media = self::generate_media_markup( $attachment_ids );
567 $separator = empty( trim( $post->post_content ) ) ? '' : "\n\n";
568
569 \wp_update_post(
570 array(
571 'ID' => $post_id,
572 'post_content' => $post->post_content . $separator . $media,
573 )
574 );
575 }
576
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 /**
597 * Generate media markup for attachments.
598 *
599 * @param int[] $attachment_ids Array of attachment IDs.
600 *
601 * @return string The generated markup.
602 */
603 private static function generate_media_markup( $attachment_ids ) {
604 if ( empty( $attachment_ids ) ) {
605 return '';
606 }
607
608 /**
609 * Filters the media markup for ActivityPub attachments.
610 *
611 * Allows plugins to provide custom markup for attachments.
612 * If this filter returns a non-empty string, it will be used instead of
613 * the default block markup.
614 *
615 * @param string $markup The custom markup. Default empty string.
616 * @param int[] $attachment_ids Array of attachment IDs.
617 */
618 $custom_markup = \apply_filters( 'activitypub_attachments_media_markup', '', $attachment_ids );
619
620 if ( ! empty( $custom_markup ) ) {
621 return $custom_markup;
622 }
623
624 // Default to block markup.
625 $type = strtok( \get_post_mime_type( $attachment_ids[0] ), '/' );
626
627 // Single video or audio file.
628 if ( 1 === \count( $attachment_ids ) && ( 'video' === $type || 'audio' === $type ) ) {
629 return sprintf(
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 -->',
631 \esc_attr( $type ),
632 \esc_attr( $attachment_ids[0] ),
633 \esc_url( \wp_get_attachment_url( $attachment_ids[0] ) )
634 );
635 }
636
637 // Single image: use standalone image block.
638 if ( 1 === \count( $attachment_ids ) && 'image' === $type ) {
639 return self::get_image_block( $attachment_ids[0] );
640 }
641
642 // Multiple attachments: use gallery block.
643 return self::get_gallery_block( $attachment_ids );
644 }
645
646 /**
647 * Generate media markup for file-based attachments.
648 *
649 * @param array[] $files {
650 * Array of file data arrays.
651 *
652 * @type string $url Full URL to the file.
653 * @type string $mime_type MIME type of the file.
654 * @type string $alt Alt text for the file.
655 * }
656 *
657 * @return string The generated markup.
658 */
659 private static function generate_files_markup( $files ) {
660 if ( empty( $files ) ) {
661 return '';
662 }
663
664 /**
665 * Filters the media markup for ActivityPub file-based attachments.
666 *
667 * Allows plugins to provide custom markup for file-based attachments.
668 * If this filter returns a non-empty string, it will be used instead of
669 * the default block markup.
670 *
671 * @param string $markup The custom markup. Default empty string.
672 * @param array $files Array of file data arrays.
673 */
674 $custom_markup = \apply_filters( 'activitypub_files_media_markup', '', $files );
675
676 if ( ! empty( $custom_markup ) ) {
677 return $custom_markup;
678 }
679
680 // Default to block markup.
681 $type = strtok( $files[0]['mime_type'], '/' );
682
683 // Single video or audio file.
684 if ( 1 === \count( $files ) && ( 'video' === $type || 'audio' === $type ) ) {
685 return sprintf(
686 '<!-- wp:%1$s --><figure class="wp-block-%1$s"><%1$s controls src="%2$s"></%1$s></figure><!-- /wp:%1$s -->',
687 \esc_attr( $type ),
688 \esc_url( $files[0]['url'] )
689 );
690 }
691
692 // Single image: use standalone image block.
693 if ( 1 === \count( $files ) && 'image' === $type ) {
694 return self::get_files_image_block( $files[0] );
695 }
696
697 // Multiple attachments: use gallery block.
698 return self::get_files_gallery_block( $files );
699 }
700
701 /**
702 * Get standalone image block markup for file-based attachments.
703 *
704 * @param array $file {
705 * File data array.
706 *
707 * @type string $url Full URL to the file.
708 * @type string $mime_type MIME type of the file.
709 * @type string $alt Alt text for the file.
710 * }
711 *
712 * @return string The image block markup.
713 */
714 private static function get_files_image_block( $file ) {
715 $block = '<!-- wp:image {"sizeSlug":"large","linkDestination":"none"} -->' . "\n";
716 $block .= '<figure class="wp-block-image size-large">';
717 $block .= '<img src="' . \esc_url( $file['url'] ) . '" alt="' . \esc_attr( $file['alt'] ) . '"/>';
718 $block .= '</figure>' . "\n";
719 $block .= '<!-- /wp:image -->';
720
721 return $block;
722 }
723
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 /**
787 * Get gallery block markup for file-based attachments.
788 *
789 * @param array[] $files {
790 * Array of file data arrays.
791 *
792 * @type string $url Full URL to the file.
793 * @type string $mime_type MIME type of the file.
794 * @type string $alt Alt text for the file.
795 * }
796 *
797 * @return string The gallery block markup.
798 */
799 private static function get_files_gallery_block( $files ) {
800 $gallery = '<!-- wp:gallery {"columns":2,"linkTo":"none","imageCrop":true} -->' . "\n";
801 $gallery .= '<figure class="wp-block-gallery has-nested-images columns-2 is-cropped">';
802
803 foreach ( $files as $file ) {
804 $gallery .= "\n<!-- wp:image {\"sizeSlug\":\"large\",\"linkDestination\":\"none\"} -->\n";
805 $gallery .= '<figure class="wp-block-image size-large">';
806 $gallery .= '<img src="' . \esc_url( $file['url'] ) . '" alt="' . \esc_attr( $file['alt'] ) . '"/>';
807 $gallery .= '</figure>';
808 $gallery .= "\n<!-- /wp:image -->\n";
809 }
810
811 $gallery .= "</figure>\n";
812 $gallery .= '<!-- /wp:gallery -->';
813
814 return $gallery;
815 }
816 }
817