760 lines
24 KiB
PHP
760 lines
24 KiB
PHP
<?php
|
|
/**
|
|
* Attachments processing file.
|
|
*
|
|
* @package Activitypub
|
|
*/
|
|
|
|
namespace Activitypub;
|
|
|
|
/**
|
|
* Attachments processor class.
|
|
*
|
|
* Handles importing media attachments into the WordPress Media Library.
|
|
* Creates full WordPress attachment posts that are searchable and manageable.
|
|
*
|
|
* For lightweight file caching without Media Library overhead, use the
|
|
* Cache\Media, Cache\Avatar, and Cache\Emoji classes instead.
|
|
*
|
|
* @since 1.0.0
|
|
*/
|
|
class Attachments {
|
|
/**
|
|
* Maximum width for imported images into Media Library.
|
|
*
|
|
* @var int
|
|
*/
|
|
const MAX_IMAGE_DIMENSION = 1200;
|
|
|
|
/**
|
|
* Import attachments from an ActivityPub object and attach them to a post.
|
|
*
|
|
* Creates full WordPress attachment posts in the media library. Each attachment
|
|
* becomes a searchable, manageable attachment post that appears in the WordPress
|
|
* Media Library and is part of the user's content.
|
|
*
|
|
* Use this when:
|
|
* - Importing content that will be owned and editable by the user.
|
|
* - You need WordPress attachment posts with full metadata support.
|
|
* - Media should be searchable and manageable in the Media Library.
|
|
* - Working with content that will be part of the user's site (e.g., importers).
|
|
*
|
|
* @param array $attachments Array of ActivityPub attachment objects.
|
|
* @param int $post_id The post ID to attach files to.
|
|
* @param int $author_id Optional. User ID to set as attachment author. Default 0.
|
|
*
|
|
* @return array Array of attachment IDs.
|
|
*/
|
|
public static function import( $attachments, $post_id, $author_id = 0 ) {
|
|
// First, import inline images from the post content.
|
|
$inline_mappings = self::import_inline_images( $post_id, $author_id );
|
|
|
|
if ( empty( $attachments ) || ! is_array( $attachments ) ) {
|
|
return array();
|
|
}
|
|
|
|
$attachment_ids = array();
|
|
foreach ( $attachments as $attachment ) {
|
|
$attachment_data = self::normalize_attachment( $attachment );
|
|
|
|
if ( empty( $attachment_data['url'] ) ) {
|
|
continue;
|
|
}
|
|
|
|
// Skip if this URL was already processed as an inline image.
|
|
if ( isset( $inline_mappings[ $attachment_data['url'] ] ) ) {
|
|
continue;
|
|
}
|
|
|
|
$attachment_id = self::save_attachment( $attachment_data, $post_id, $author_id );
|
|
|
|
if ( ! \is_wp_error( $attachment_id ) ) {
|
|
$attachment_ids[] = $attachment_id;
|
|
}
|
|
}
|
|
|
|
// Append media markup to post content.
|
|
if ( ! empty( $attachment_ids ) ) {
|
|
self::append_media_to_post_content( $post_id, $attachment_ids );
|
|
}
|
|
|
|
return $attachment_ids;
|
|
}
|
|
|
|
/**
|
|
* Check if an attachment with the same source URL already exists for a post.
|
|
*
|
|
* @param string $source_url The source URL to check.
|
|
* @param int $post_id The post ID to check attachments for.
|
|
*
|
|
* @return int|false The existing attachment ID or false if not found.
|
|
*/
|
|
private static function get_existing_attachment( $source_url, $post_id ) {
|
|
foreach ( \get_attached_media( '', $post_id ) as $attachment ) {
|
|
if ( \get_post_meta( $attachment->ID, '_source_url', true ) === $source_url ) {
|
|
return $attachment->ID;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Process inline images from post content.
|
|
*
|
|
* @param int $post_id The post ID.
|
|
* @param int $author_id Optional. User ID to set as attachment author. Default 0.
|
|
*
|
|
* @return array Array of URL mappings (old URL => new URL).
|
|
*/
|
|
private static function import_inline_images( $post_id, $author_id = 0 ) {
|
|
$post = \get_post( $post_id );
|
|
if ( ! $post || empty( $post->post_content ) ) {
|
|
return array();
|
|
}
|
|
|
|
// Find all img tags in the content.
|
|
preg_match_all( '/<img[^>]+src=["\']([^"\']+)["\'][^>]*>/i', $post->post_content, $matches );
|
|
|
|
if ( empty( $matches[1] ) ) {
|
|
return array();
|
|
}
|
|
|
|
$url_mappings = array();
|
|
$content = $post->post_content;
|
|
|
|
foreach ( $matches[1] as $image_url ) {
|
|
// Skip if already processed or is a local URL.
|
|
if ( isset( $url_mappings[ $image_url ] ) ) {
|
|
continue;
|
|
}
|
|
|
|
// Check if this image was already processed as an attachment.
|
|
$attachment_id = self::get_existing_attachment( $image_url, $post_id );
|
|
if ( ! $attachment_id ) {
|
|
$attachment_id = self::save_attachment( array( 'url' => $image_url ), $post_id, $author_id );
|
|
|
|
if ( \is_wp_error( $attachment_id ) ) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
$new_url = \wp_get_attachment_url( $attachment_id );
|
|
if ( $new_url ) {
|
|
$url_mappings[ $image_url ] = $new_url;
|
|
$content = \str_replace( $image_url, $new_url, $content );
|
|
}
|
|
}
|
|
|
|
// Update post content if URLs were replaced.
|
|
if ( ! empty( $url_mappings ) ) {
|
|
\wp_update_post(
|
|
array(
|
|
'ID' => $post_id,
|
|
'post_content' => $content,
|
|
)
|
|
);
|
|
}
|
|
|
|
return $url_mappings;
|
|
}
|
|
|
|
/**
|
|
* Normalize an ActivityPub attachment object to a standard format.
|
|
*
|
|
* @param mixed $attachment The attachment data (array or object).
|
|
*
|
|
* @return array|false Normalized attachment data or false on failure.
|
|
*/
|
|
private static function normalize_attachment( $attachment ) {
|
|
// Convert object to array if needed.
|
|
if ( \is_object( $attachment ) ) {
|
|
$attachment = \get_object_vars( $attachment );
|
|
}
|
|
|
|
if ( ! is_array( $attachment ) || empty( $attachment['url'] ) ) {
|
|
return false;
|
|
}
|
|
|
|
return array(
|
|
'url' => $attachment['url'],
|
|
'mediaType' => $attachment['mediaType'] ?? '',
|
|
'name' => $attachment['name'] ?? '',
|
|
'type' => $attachment['type'] ?? 'Document',
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Save an attachment (local file or remote URL) to the media library.
|
|
*
|
|
* @param array $attachment_data The normalized attachment data.
|
|
* @param int $post_id The post ID to attach to.
|
|
* @param int $author_id Optional. User ID to set as attachment author. Default 0.
|
|
*
|
|
* @return int|\WP_Error The attachment ID or WP_Error on failure.
|
|
*/
|
|
private static function save_attachment( $attachment_data, $post_id, $author_id = 0 ) {
|
|
// Ensure required WordPress functions are loaded.
|
|
if ( ! \function_exists( 'media_handle_sideload' ) || ! \function_exists( 'download_url' ) ) {
|
|
require_once ABSPATH . 'wp-admin/includes/media.php';
|
|
require_once ABSPATH . 'wp-admin/includes/file.php';
|
|
require_once ABSPATH . 'wp-admin/includes/image.php';
|
|
}
|
|
|
|
// Use WP_Filesystem_Direct explicitly to avoid FTP fallback from WP_Filesystem().
|
|
require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-base.php';
|
|
require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-direct.php';
|
|
|
|
$filesystem = new \WP_Filesystem_Direct( null );
|
|
|
|
$is_local = ! preg_match( '#^https?://#i', $attachment_data['url'] );
|
|
|
|
if ( $is_local ) {
|
|
// Validate local path is within allowed directories to prevent file disclosure.
|
|
$allowed = self::is_allowed_local_path( $attachment_data['url'] );
|
|
if ( ! $allowed ) {
|
|
return new \WP_Error( 'invalid_path', \__( 'Local file path is not within allowed directories.', 'activitypub' ) );
|
|
}
|
|
|
|
// Read local file from disk.
|
|
if ( ! $filesystem->exists( $attachment_data['url'] ) ) {
|
|
/* translators: %s: file path */
|
|
return new \WP_Error( 'file_not_found', sprintf( \__( 'File not found: %s', 'activitypub' ), $attachment_data['url'] ) );
|
|
}
|
|
|
|
// Copy to temp file so media_handle_sideload doesn't move the original.
|
|
$tmp_file = \wp_tempnam( \basename( $attachment_data['url'] ) );
|
|
$filesystem->copy( $attachment_data['url'], $tmp_file, true );
|
|
} else {
|
|
// Validate remote URL before downloading.
|
|
if ( ! \wp_http_validate_url( $attachment_data['url'] ) ) {
|
|
return new \WP_Error( 'invalid_url', \__( 'URL is not allowed.', 'activitypub' ) );
|
|
}
|
|
|
|
// Download remote URL.
|
|
$tmp_file = \download_url( $attachment_data['url'] );
|
|
|
|
if ( \is_wp_error( $tmp_file ) ) {
|
|
return $tmp_file;
|
|
}
|
|
}
|
|
|
|
// Get original filename from URL.
|
|
$original_name = \basename( \wp_parse_url( $attachment_data['url'], PHP_URL_PATH ) );
|
|
|
|
// Rename temp file to have proper extension for optimize_image to detect mime type.
|
|
$original_ext = \pathinfo( $original_name, PATHINFO_EXTENSION );
|
|
if ( $original_ext ) {
|
|
$renamed_tmp = $tmp_file . '.' . $original_ext;
|
|
if ( $filesystem->move( $tmp_file, $renamed_tmp, true ) ) {
|
|
$tmp_file = $renamed_tmp;
|
|
}
|
|
}
|
|
|
|
// Optimize images before sideloading (resize and convert to WebP).
|
|
$tmp_file = self::optimize_image( $tmp_file, self::MAX_IMAGE_DIMENSION );
|
|
|
|
// Update filename extension to match optimized file.
|
|
$new_ext = \pathinfo( $tmp_file, PATHINFO_EXTENSION );
|
|
if ( $new_ext ) {
|
|
$original_name = \preg_replace( '/\.[^.]+$/', '.' . $new_ext, $original_name );
|
|
}
|
|
|
|
$file_array = array(
|
|
'name' => $original_name,
|
|
'tmp_name' => $tmp_file,
|
|
);
|
|
|
|
// Prepare attachment post data.
|
|
// Let WordPress auto-detect the mime type from the file.
|
|
$post_data = array(
|
|
'post_title' => $attachment_data['name'] ?? '',
|
|
'post_content' => $attachment_data['name'] ?? '',
|
|
'post_author' => $author_id,
|
|
'meta_input' => array(
|
|
'_source_url' => $attachment_data['url'],
|
|
),
|
|
);
|
|
|
|
// Add alt text for images.
|
|
if ( ! empty( $attachment_data['name'] ) ) {
|
|
$original_mime = $attachment_data['mediaType'] ?? '';
|
|
if ( 'image' === strtok( $original_mime, '/' ) ) {
|
|
$post_data['meta_input']['_wp_attachment_image_alt'] = $attachment_data['name'];
|
|
}
|
|
}
|
|
|
|
// Sideload the attachment into WordPress.
|
|
$attachment_id = \media_handle_sideload( $file_array, $post_id, '', $post_data );
|
|
|
|
// Clean up temp file if there was an error.
|
|
if ( \is_wp_error( $attachment_id ) ) {
|
|
\wp_delete_file( $tmp_file );
|
|
}
|
|
|
|
return $attachment_id;
|
|
}
|
|
|
|
/**
|
|
* Get a unique file path by appending a counter if the file already exists.
|
|
*
|
|
* @param string $file_path The desired file path.
|
|
*
|
|
* @return string A unique file path that doesn't exist.
|
|
*/
|
|
private static function get_unique_path( $file_path ) {
|
|
if ( ! \file_exists( $file_path ) ) {
|
|
return $file_path;
|
|
}
|
|
|
|
$path_info = \pathinfo( $file_path );
|
|
$dir = $path_info['dirname'];
|
|
$base_name = $path_info['filename'];
|
|
$extension = isset( $path_info['extension'] ) ? '.' . $path_info['extension'] : '';
|
|
$counter = 1;
|
|
|
|
do {
|
|
$new_path = $dir . '/' . $base_name . '-' . $counter . $extension;
|
|
++$counter;
|
|
} while ( \file_exists( $new_path ) );
|
|
|
|
return $new_path;
|
|
}
|
|
|
|
/**
|
|
* Check if a local file path is within allowed directories.
|
|
*
|
|
* Prevents arbitrary file access by restricting local paths to known safe
|
|
* directories like the uploads folder or WordPress temp directory.
|
|
*
|
|
* @param string $file_path The local file path to validate.
|
|
*
|
|
* @return bool True if the path is allowed, false otherwise.
|
|
*/
|
|
private static function is_allowed_local_path( $file_path ) {
|
|
// Normalize the path and resolve any relative components.
|
|
$real_path = \realpath( $file_path );
|
|
if ( false === $real_path ) {
|
|
// If file doesn't exist yet, check the directory.
|
|
$dir_path = \realpath( \dirname( $file_path ) );
|
|
if ( false === $dir_path ) {
|
|
return false;
|
|
}
|
|
$real_path = $dir_path . '/' . \basename( $file_path );
|
|
}
|
|
|
|
// Get allowed base directories.
|
|
$upload_dir = \wp_upload_dir();
|
|
$allowed_dirs = array(
|
|
\realpath( $upload_dir['basedir'] ),
|
|
\realpath( \get_temp_dir() ),
|
|
\realpath( ABSPATH . 'wp-content' ),
|
|
);
|
|
|
|
/**
|
|
* Filters the allowed directories for local file imports.
|
|
*
|
|
* @since 5.6.0
|
|
*
|
|
* @param string[] $allowed_dirs Array of allowed directory paths.
|
|
* @param string $file_path The file path being validated.
|
|
*/
|
|
$allowed_dirs = \apply_filters( 'activitypub_allowed_import_directories', $allowed_dirs, $file_path );
|
|
|
|
// Remove any false values from realpath failures.
|
|
$allowed_dirs = \array_filter( $allowed_dirs );
|
|
|
|
// Check if the file is within any allowed directory.
|
|
foreach ( $allowed_dirs as $allowed_dir ) {
|
|
if ( \str_starts_with( $real_path, $allowed_dir ) ) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Optimize an image file by resizing and converting to WebP.
|
|
*
|
|
* Uses WordPress image editor to resize large images and convert them
|
|
* to WebP format for better compression while maintaining quality.
|
|
*
|
|
* @param string $file_path Path to the image file.
|
|
* @param int $max_dimension Maximum width/height in pixels.
|
|
*
|
|
* @return string The optimized file path.
|
|
*/
|
|
private static function optimize_image( $file_path, $max_dimension ) {
|
|
// Check if it's an image.
|
|
$mime_type = \wp_check_filetype( $file_path )['type'] ?? '';
|
|
if ( ! $mime_type || ! \str_starts_with( $mime_type, 'image/' ) ) {
|
|
return $file_path;
|
|
}
|
|
|
|
// Skip SVG and GIF files (GIFs may be animated).
|
|
if ( \in_array( $mime_type, array( 'image/svg+xml', 'image/gif' ), true ) ) {
|
|
return $file_path;
|
|
}
|
|
|
|
$editor = \wp_get_image_editor( $file_path );
|
|
if ( \is_wp_error( $editor ) ) {
|
|
return $file_path;
|
|
}
|
|
|
|
$size = $editor->get_size();
|
|
$needs_resize = $size['width'] > $max_dimension || $size['height'] > $max_dimension;
|
|
|
|
// Resize if needed.
|
|
if ( $needs_resize ) {
|
|
$editor->resize( $max_dimension, $max_dimension, false );
|
|
}
|
|
|
|
// Check if WebP is supported.
|
|
$can_webp = $editor->supports_mime_type( 'image/webp' );
|
|
|
|
// Determine output format and save.
|
|
if ( $can_webp ) {
|
|
// Convert to WebP.
|
|
$new_path = self::get_unique_path( \preg_replace( '/\.[^.]+$/', '.webp', $file_path ) );
|
|
$result = $editor->save( $new_path, 'image/webp' );
|
|
} elseif ( \in_array( $mime_type, array( 'image/png', 'image/webp' ), true ) ) {
|
|
// Keep original format for potentially transparent images when WebP not available.
|
|
if ( ! $needs_resize ) {
|
|
// No changes needed.
|
|
return $file_path;
|
|
}
|
|
$result = $editor->save( $file_path );
|
|
} else {
|
|
// Convert to JPEG when WebP not available.
|
|
$new_path = self::get_unique_path( \preg_replace( '/\.[^.]+$/', '.jpg', $file_path ) );
|
|
$result = $editor->save( $new_path, 'image/jpeg' );
|
|
}
|
|
|
|
if ( \is_wp_error( $result ) ) {
|
|
return $file_path;
|
|
}
|
|
|
|
// Handle result - $result is always an array from $editor->save().
|
|
$result_path = $result['path'] ?? $file_path;
|
|
|
|
// If path changed (format conversion), delete the original file.
|
|
if ( $result_path !== $file_path ) {
|
|
\wp_delete_file( $file_path );
|
|
}
|
|
|
|
return $result_path;
|
|
}
|
|
|
|
/**
|
|
* Append media to post content.
|
|
*
|
|
* @param int $post_id The post ID.
|
|
* @param int[] $attachment_ids Array of attachment IDs.
|
|
*/
|
|
private static function append_media_to_post_content( $post_id, $attachment_ids ) {
|
|
$post = \get_post( $post_id );
|
|
if ( ! $post ) {
|
|
return;
|
|
}
|
|
|
|
$media = self::generate_media_markup( $attachment_ids );
|
|
$separator = empty( trim( $post->post_content ) ) ? '' : "\n\n";
|
|
|
|
\wp_update_post(
|
|
array(
|
|
'ID' => $post_id,
|
|
'post_content' => $post->post_content . $separator . $media,
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Generate media markup for attachments.
|
|
*
|
|
* @param int[] $attachment_ids Array of attachment IDs.
|
|
*
|
|
* @return string The generated markup.
|
|
*/
|
|
private static function generate_media_markup( $attachment_ids ) {
|
|
if ( empty( $attachment_ids ) ) {
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* Filters the media markup for ActivityPub attachments.
|
|
*
|
|
* Allows plugins to provide custom markup for attachments.
|
|
* If this filter returns a non-empty string, it will be used instead of
|
|
* the default block markup.
|
|
*
|
|
* @param string $markup The custom markup. Default empty string.
|
|
* @param int[] $attachment_ids Array of attachment IDs.
|
|
*/
|
|
$custom_markup = \apply_filters( 'activitypub_attachments_media_markup', '', $attachment_ids );
|
|
|
|
if ( ! empty( $custom_markup ) ) {
|
|
return $custom_markup;
|
|
}
|
|
|
|
// Default to block markup.
|
|
$type = strtok( \get_post_mime_type( $attachment_ids[0] ), '/' );
|
|
|
|
// Single video or audio file.
|
|
if ( 1 === \count( $attachment_ids ) && ( 'video' === $type || 'audio' === $type ) ) {
|
|
return sprintf(
|
|
'<!-- wp:%1$s {"id":"%2$s"} --><figure class="wp-block-%1$s"><%1$s controls src="%3$s"></%1$s></figure><!-- /wp:%1$s -->',
|
|
\esc_attr( $type ),
|
|
\esc_attr( $attachment_ids[0] ),
|
|
\esc_url( \wp_get_attachment_url( $attachment_ids[0] ) )
|
|
);
|
|
}
|
|
|
|
// Single image: use standalone image block.
|
|
if ( 1 === \count( $attachment_ids ) && 'image' === $type ) {
|
|
return self::get_image_block( $attachment_ids[0] );
|
|
}
|
|
|
|
// Multiple attachments: use gallery block.
|
|
return self::get_gallery_block( $attachment_ids );
|
|
}
|
|
|
|
/**
|
|
* Get standalone image block markup.
|
|
*
|
|
* @param int $attachment_id The attachment ID.
|
|
*
|
|
* @return string The image block markup.
|
|
*/
|
|
private static function get_image_block( $attachment_id ) {
|
|
$image_src = \wp_get_attachment_image_src( $attachment_id, 'large' );
|
|
if ( ! $image_src ) {
|
|
return '';
|
|
}
|
|
|
|
$alt = \get_post_meta( $attachment_id, '_wp_attachment_image_alt', true );
|
|
if ( ! $alt ) {
|
|
$alt = \get_post_field( 'post_excerpt', $attachment_id );
|
|
}
|
|
|
|
$block = '<!-- wp:image {"id":' . \esc_attr( $attachment_id ) . ',"sizeSlug":"large","linkDestination":"none"} -->' . "\n";
|
|
$block .= '<figure class="wp-block-image size-large">';
|
|
$block .= '<img src="' . \esc_url( $image_src[0] ) . '" alt="' . \esc_attr( $alt ) . '" class="' . \esc_attr( 'wp-image-' . $attachment_id ) . '"/>';
|
|
$block .= '</figure>' . "\n";
|
|
$block .= '<!-- /wp:image -->';
|
|
|
|
return $block;
|
|
}
|
|
|
|
/**
|
|
* Get gallery block markup.
|
|
*
|
|
* @param int[] $attachment_ids The attachment IDs to use.
|
|
*
|
|
* @return string The gallery block markup.
|
|
*/
|
|
private static function get_gallery_block( $attachment_ids ) {
|
|
$gallery = '<!-- wp:gallery {"columns":2,"linkTo":"none","sizeSlug":"large","imageCrop":true} -->' . "\n";
|
|
$gallery .= '<figure class="wp-block-gallery has-nested-images columns-2 is-cropped">';
|
|
|
|
foreach ( $attachment_ids as $id ) {
|
|
$image_src = \wp_get_attachment_image_src( $id, 'large' );
|
|
if ( ! $image_src ) {
|
|
continue;
|
|
}
|
|
|
|
$alt = \get_post_meta( $id, '_wp_attachment_image_alt', true );
|
|
if ( ! $alt ) {
|
|
$alt = \get_post_field( 'post_excerpt', $id );
|
|
}
|
|
|
|
$gallery .= "\n" . '<!-- wp:image {"id":' . \esc_attr( $id ) . ',"sizeSlug":"large","linkDestination":"none"} -->' . "\n";
|
|
$gallery .= '<figure class="wp-block-image size-large">';
|
|
$gallery .= '<img src="' . \esc_url( $image_src[0] ) . '" alt="' . \esc_attr( $alt ) . '" class="' . \esc_attr( 'wp-image-' . $id ) . '"/>';
|
|
$gallery .= '</figure>';
|
|
$gallery .= "\n<!-- /wp:image -->\n";
|
|
}
|
|
|
|
$gallery .= "</figure>\n";
|
|
$gallery .= '<!-- /wp:gallery -->';
|
|
|
|
return $gallery;
|
|
}
|
|
|
|
/**
|
|
* Get content from an object based on its type.
|
|
*
|
|
* @param int $object_id The object ID (post or comment).
|
|
* @param string $object_type The object type ('post' or 'comment').
|
|
*
|
|
* @return string The object content.
|
|
*/
|
|
private static function get_object_content( $object_id, $object_type ) {
|
|
if ( 'comment' === $object_type ) {
|
|
$comment = \get_comment( $object_id );
|
|
return $comment ? $comment->comment_content : '';
|
|
}
|
|
|
|
return \get_post_field( 'post_content', $object_id );
|
|
}
|
|
|
|
/**
|
|
* Update content for an object based on its type.
|
|
*
|
|
* @param int $object_id The object ID (post or comment).
|
|
* @param string $object_type The object type ('post' or 'comment').
|
|
* @param string $content The new content.
|
|
*/
|
|
private static function update_object_content( $object_id, $object_type, $content ) {
|
|
if ( 'comment' === $object_type ) {
|
|
\wp_update_comment(
|
|
array(
|
|
'comment_ID' => $object_id,
|
|
'comment_content' => $content,
|
|
)
|
|
);
|
|
} else {
|
|
\wp_update_post(
|
|
array(
|
|
'ID' => $object_id,
|
|
'post_content' => $content,
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Append file-based media markup to an object's content.
|
|
*
|
|
* Used for cached remote media (via Cache classes) that doesn't go through
|
|
* the Media Library. Works with posts and comments.
|
|
*
|
|
* @param int $object_id The object ID (post or comment).
|
|
* @param array $files Array of file data arrays with 'url', 'mime_type', and 'alt' keys.
|
|
* @param string $object_type The object type ('post' or 'comment').
|
|
*/
|
|
public static function append_files_to_content( $object_id, $files, $object_type = 'post' ) {
|
|
$content = self::get_object_content( $object_id, $object_type );
|
|
if ( empty( $content ) ) {
|
|
return;
|
|
}
|
|
|
|
$media = self::generate_files_markup( $files );
|
|
$separator = empty( trim( $content ) ) ? '' : "\n\n";
|
|
|
|
self::update_object_content( $object_id, $object_type, $content . $separator . $media );
|
|
}
|
|
|
|
/**
|
|
* Generate media markup for file-based attachments.
|
|
*
|
|
* Creates WordPress block markup from file data arrays. Used for cached
|
|
* remote media that doesn't have WordPress attachment posts.
|
|
*
|
|
* @param array[] $files {
|
|
* Array of file data arrays.
|
|
*
|
|
* @type string $url Full URL to the file.
|
|
* @type string $mime_type MIME type of the file.
|
|
* @type string $alt Alt text for the file.
|
|
* }
|
|
*
|
|
* @return string The generated markup.
|
|
*/
|
|
public static function generate_files_markup( $files ) {
|
|
if ( empty( $files ) ) {
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* Filters the media markup for ActivityPub file-based attachments.
|
|
*
|
|
* Allows plugins to provide custom markup for file-based attachments.
|
|
* If this filter returns a non-empty string, it will be used instead of
|
|
* the default block markup.
|
|
*
|
|
* @param string $markup The custom markup. Default empty string.
|
|
* @param array $files Array of file data arrays.
|
|
*/
|
|
$custom_markup = \apply_filters( 'activitypub_files_media_markup', '', $files );
|
|
|
|
if ( ! empty( $custom_markup ) ) {
|
|
return $custom_markup;
|
|
}
|
|
|
|
// Default to block markup.
|
|
$type = strtok( $files[0]['mime_type'], '/' );
|
|
|
|
// Single video or audio file.
|
|
if ( 1 === \count( $files ) && ( 'video' === $type || 'audio' === $type ) ) {
|
|
return sprintf(
|
|
'<!-- wp:%1$s --><figure class="wp-block-%1$s"><%1$s controls src="%2$s"></%1$s></figure><!-- /wp:%1$s -->',
|
|
\esc_attr( $type ),
|
|
\esc_url( $files[0]['url'] )
|
|
);
|
|
}
|
|
|
|
// Single image: use standalone image block.
|
|
if ( 1 === \count( $files ) && 'image' === $type ) {
|
|
return self::get_files_image_block( $files[0] );
|
|
}
|
|
|
|
// Multiple attachments: use gallery block.
|
|
return self::get_files_gallery_block( $files );
|
|
}
|
|
|
|
/**
|
|
* Get standalone image block markup for file-based attachments.
|
|
*
|
|
* @param array $file {
|
|
* File data array.
|
|
*
|
|
* @type string $url Full URL to the file.
|
|
* @type string $mime_type MIME type of the file.
|
|
* @type string $alt Alt text for the file.
|
|
* }
|
|
*
|
|
* @return string The image block markup.
|
|
*/
|
|
public static function get_files_image_block( $file ) {
|
|
$block = '<!-- wp:image {"sizeSlug":"large","linkDestination":"none"} -->' . "\n";
|
|
$block .= '<figure class="wp-block-image size-large">';
|
|
$block .= '<img src="' . \esc_url( $file['url'] ) . '" alt="' . \esc_attr( $file['alt'] ?? '' ) . '"/>';
|
|
$block .= '</figure>' . "\n";
|
|
$block .= '<!-- /wp:image -->';
|
|
|
|
return $block;
|
|
}
|
|
|
|
/**
|
|
* Get gallery block markup for file-based attachments.
|
|
*
|
|
* @param array[] $files {
|
|
* Array of file data arrays.
|
|
*
|
|
* @type string $url Full URL to the file.
|
|
* @type string $mime_type MIME type of the file.
|
|
* @type string $alt Alt text for the file.
|
|
* }
|
|
*
|
|
* @return string The gallery block markup.
|
|
*/
|
|
public static function get_files_gallery_block( $files ) {
|
|
$gallery = '<!-- wp:gallery {"columns":2,"linkTo":"none","imageCrop":true} -->' . "\n";
|
|
$gallery .= '<figure class="wp-block-gallery has-nested-images columns-2 is-cropped">';
|
|
|
|
foreach ( $files as $file ) {
|
|
$gallery .= "\n<!-- wp:image {\"sizeSlug\":\"large\",\"linkDestination\":\"none\"} -->\n";
|
|
$gallery .= '<figure class="wp-block-image size-large">';
|
|
$gallery .= '<img src="' . \esc_url( $file['url'] ) . '" alt="' . \esc_attr( $file['alt'] ?? '' ) . '"/>';
|
|
$gallery .= '</figure>';
|
|
$gallery .= "\n<!-- /wp:image -->\n";
|
|
}
|
|
|
|
$gallery .= "</figure>\n";
|
|
$gallery .= '<!-- /wp:gallery -->';
|
|
|
|
return $gallery;
|
|
}
|
|
}
|