PluginProbe
ActivityPub / trunk
ActivityPub vtrunk
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 / functions-media.php

functions-media.php in ActivityPub trunk, at includes/functions-media.php

226 lines 6.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Media functions file.
4 *
5 * Functions for processing remote media (images, audio, video) in ActivityPub content.
6 *
7 * @package Activitypub
8 */
9
10 namespace Activitypub;
11
12 /**
13 * Check if a URL is a remote URL (not local to this site).
14 *
15 * @param string $url The URL to check.
16 *
17 * @return bool True if the URL is remote, false otherwise.
18 */
19 function is_remote_url( $url ) {
20 // Must be http/https URL.
21 if ( ! \preg_match( '#^https?://#i', $url ) ) {
22 return false;
23 }
24
25 // Compare hosts to determine if URL is local.
26 $site_host = \wp_parse_url( \home_url(), PHP_URL_HOST );
27 $url_host = \wp_parse_url( $url, PHP_URL_HOST );
28
29 // If hosts match, it's a local URL.
30 if ( $site_host && $url_host && 0 === \strcasecmp( $site_host, $url_host ) ) {
31 return false;
32 }
33
34 // Also check uploads directory with strict prefix match.
35 $upload_dir = \wp_upload_dir();
36 $upload_base = isset( $upload_dir['baseurl'] ) ? \trailingslashit( $upload_dir['baseurl'] ) : '';
37
38 if ( $upload_base && \str_starts_with( $url, $upload_base ) ) {
39 return false;
40 }
41
42 return true;
43 }
44
45 /**
46 * Generate an image block wrapper for a remote image.
47 *
48 * @param string $url The remote image URL.
49 * @param string $img_html The img tag HTML.
50 *
51 * @return string The wrapped image block.
52 */
53 function generate_image_block( $url, $img_html ) {
54 return \sprintf(
55 '<!-- wp:activitypub/image %s -->%s<!-- /wp:activitypub/image -->',
56 \wp_json_encode( array( 'url' => $url ) ),
57 $img_html
58 );
59 }
60
61 /**
62 * Generate an audio block wrapper for a remote audio file.
63 *
64 * @param string $url The remote audio URL.
65 *
66 * @return string The audio block markup.
67 */
68 function generate_audio_block( $url ) {
69 return \sprintf(
70 '<!-- wp:activitypub/audio %s --><figure class="wp-block-audio"><audio controls src="%s"></audio></figure><!-- /wp:activitypub/audio -->',
71 \wp_json_encode( array( 'url' => $url ) ),
72 \esc_url( $url )
73 );
74 }
75
76 /**
77 * Generate a video block wrapper for a remote video file.
78 *
79 * @param string $url The remote video URL.
80 *
81 * @return string The video block markup.
82 */
83 function generate_video_block( $url ) {
84 return \sprintf(
85 '<!-- wp:activitypub/video %s --><figure class="wp-block-video"><video controls src="%s"></video></figure><!-- /wp:activitypub/video -->',
86 \wp_json_encode( array( 'url' => $url ) ),
87 \esc_url( $url )
88 );
89 }
90
91 /**
92 * Process remote images in content and from attachments.
93 *
94 * Wraps remote `<img>` tags with activitypub/image blocks for lazy caching,
95 * and appends any attachments not already present in the content.
96 *
97 * @param string $content The content to process.
98 * @param array $attachments Optional. Array of attachments with 'url' and 'alt' keys.
99 *
100 * @return string The content with wrapped images and appended attachments.
101 */
102 function process_remote_images( $content, $attachments = array() ) {
103 if ( empty( $content ) && empty( $attachments ) ) {
104 return $content;
105 }
106
107 // Track URLs we've seen to avoid duplicates.
108 $seen_urls = array();
109
110 // Process existing images in content.
111 if ( ! empty( $content ) && false !== \strpos( $content, '<img' ) ) {
112 $processor = new \WP_HTML_Tag_Processor( $content );
113
114 // Mark remote images for wrapping using a data attribute.
115 while ( $processor->next_tag( 'IMG' ) ) {
116 $src = $processor->get_attribute( 'src' );
117
118 if ( $src && is_remote_url( $src ) && ! isset( $seen_urls[ $src ] ) ) {
119 $processor->set_attribute( 'data-activitypub-wrap', '1' );
120 $seen_urls[ $src ] = true;
121 }
122 }
123
124 $content = $processor->get_updated_html();
125
126 // Wrap marked images, skipping those already in image blocks.
127 $content = \preg_replace_callback(
128 '/<!-- wp:activitypub\/image[^>]*-->.*?<!-- \/wp:activitypub\/image -->(*SKIP)(?!)|<img\s+([^>]*)data-activitypub-wrap="1"([^>]*)>/is',
129 function ( $matches ) {
130 if ( empty( $matches[1] ) && empty( $matches[2] ) ) {
131 return $matches[0]; // Skipped block.
132 }
133
134 // Reconstruct img tag without the marker attribute.
135 $img_html = '<img ' . \trim( $matches[1] . $matches[2] ) . '>';
136
137 // Extract src URL from the img tag.
138 if ( \preg_match( '/src=["\']([^"\']+)["\']/', $img_html, $src_match ) ) {
139 return generate_image_block( $src_match[1], $img_html );
140 }
141
142 return $matches[0];
143 },
144 $content
145 );
146 }
147
148 // Append attachments not already in content.
149 if ( ! empty( $attachments ) ) {
150 foreach ( $attachments as $attachment ) {
151 $url = $attachment['url'] ?? '';
152 if ( empty( $url ) || isset( $seen_urls[ $url ] ) ) {
153 continue;
154 }
155
156 // Also check if URL appears in content (in case it wasn't in an img tag).
157 if ( false !== \strpos( $content, $url ) ) {
158 continue;
159 }
160
161 $alt = ! empty( $attachment['alt'] ) ? \esc_attr( $attachment['alt'] ) : '';
162 $img_tag = $alt
163 ? \sprintf( '<img src="%s" alt="%s" />', \esc_url( $url ), $alt )
164 : \sprintf( '<img src="%s" />', \esc_url( $url ) );
165
166 $content .= "\n\n" . generate_image_block( $url, $img_tag );
167 $seen_urls[ $url ] = true;
168 }
169 }
170
171 return $content;
172 }
173
174 /**
175 * Process remote media in content and from attachments.
176 *
177 * Delegates image attachments to process_remote_images() and appends
178 * audio/video attachments as their respective block types.
179 *
180 * @param string $content The content to process.
181 * @param array $attachments Optional. Array of attachments with 'url', 'alt', and 'type' keys.
182 *
183 * @return string The content with wrapped images and appended audio/video blocks.
184 */
185 function process_remote_media( $content, $attachments = array() ) {
186 // Separate attachments by type.
187 $image_attachments = array();
188 $media_attachments = array();
189
190 foreach ( $attachments as $attachment ) {
191 $type = $attachment['type'] ?? 'image';
192
193 if ( 'audio' === $type || 'video' === $type ) {
194 $media_attachments[] = $attachment;
195 } else {
196 $image_attachments[] = $attachment;
197 }
198 }
199
200 // Delegate image attachments to existing handler.
201 $content = process_remote_images( $content, $image_attachments );
202
203 // Append audio/video blocks.
204 foreach ( $media_attachments as $attachment ) {
205 $url = $attachment['url'] ?? '';
206 if ( empty( $url ) ) {
207 continue;
208 }
209
210 // Skip if URL already appears in content.
211 if ( false !== \strpos( $content, $url ) ) {
212 continue;
213 }
214
215 $type = $attachment['type'];
216
217 if ( 'audio' === $type ) {
218 $content .= "\n\n" . generate_audio_block( $url );
219 } elseif ( 'video' === $type ) {
220 $content .= "\n\n" . generate_video_block( $url );
221 }
222 }
223
224 return $content;
225 }
226