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 | integration/class-jetpack.php +8 -256 9.3.07.7.0 View file →
@@ -10,16 +10,10 @@
10 10 use Activitypub\Collection\Followers;
11 11 use Activitypub\Collection\Following;
12 12 use Activitypub\Http;
13 13 use Automattic\Jetpack\Connection\Manager;
14 -use Automattic\Jetpack\Podcast\Feed\Customize_Feed;
15 -use Automattic\Jetpack\Podcast\Feed\Episode_Block_Tags;
16 -use Automattic\Jetpack\Podcast\Settings as Podcast_Settings;
17 14
18 -use function Activitypub\get_enclosures;
19 -use function Activitypub\get_max_attachments;
20 15 use function Activitypub\is_activity_object;
21 -use function Activitypub\normalize_url;
22 16
23 17 /**
24 18 * Jetpack integration class.
25 19 */
@@ -29,9 +23,8 @@
29 23 * Initialize the class, registering WordPress hooks.
30 24 */
31 25 public static function init() {
32 26 if ( ! \defined( 'IS_WPCOM' ) ) {
33 - \add_filter( 'jetpack_sync_options_whitelist', array( self::class, 'add_sync_options' ) );
34 27 \add_filter( 'jetpack_sync_post_meta_whitelist', array( self::class, 'add_sync_meta' ) );
35 28 \add_filter( 'jetpack_sync_comment_meta_whitelist', array( self::class, 'add_sync_comment_meta' ) );
36 29 \add_filter( 'jetpack_sync_whitelisted_comment_types', array( self::class, 'add_comment_types' ) );
37 30 \add_filter( 'jetpack_json_api_comment_types', array( self::class, 'add_comment_types' ) );
@@ -46,34 +39,11 @@
46 39 \add_filter( 'pre_option_activitypub_following_ui', array( self::class, 'pre_option_activitypub_following_ui' ) );
47 40 }
48 41
49 42 \add_action( 'load-post-new.php', array( self::class, 'adapt_post_share' ) );
50 -
51 - // Enriched onto the already-assembled attachments rather than through a transformer subclass
52 - // like Podlove/SSP: a subclass is winner-take-all, so a site running one of those alongside a
53 - // Jetpack podcast would get one behaviour instead of both.
54 - \add_filter( 'activitypub_attachments', array( self::class, 'add_podcast_attachments' ), 10, 2 );
55 43 }
56 44
57 45 /**
58 - * Add ActivityPub options to the Jetpack sync allow list.
59 - *
60 - * @since 8.1.0
61 - *
62 - * @param array $allow_list The Jetpack sync options allow list.
63 - *
64 - * @return array The allow list with ActivityPub options.
65 - */
66 - public static function add_sync_options( $allow_list ) {
67 - $allow_list[] = 'activitypub_blog_identifier';
68 - $allow_list[] = 'activitypub_blog_description';
69 - $allow_list[] = 'activitypub_header_image';
70 - $allow_list[] = 'activitypub_actor_mode';
71 -
72 - return $allow_list;
73 - }
74 -
75 - /**
76 46 * Add ActivityPub meta keys to the Jetpack sync allow list.
77 47 *
78 48 * @param array $allow_list The Jetpack sync allow list.
79 49 *
@@ -110,9 +80,9 @@
110 80 $comment_types[] = 'like';
111 81 $comment_types[] = 'quote';
112 82 $comment_types[] = 'repost';
113 83
114 - return \array_unique( $comment_types );
84 + return array_unique( $comment_types );
115 85 }
116 86
117 87 /**
118 88 * Add a "Reader" link to the bulk actions dropdown on the following list screen.
@@ -134,21 +104,21 @@
134 104 if ( \defined( 'IS_WPCOM' ) && IS_WPCOM ) {
135 105 if ( empty( $feed['feed_id'] ) ) {
136 106 return $actions; // No feed_id available on WPCOM.
137 107 }
138 - $url = \sprintf( 'https://wordpress.com/reader/feeds/%d', (int) $feed['feed_id'] );
108 + $url = sprintf( 'https://wordpress.com/reader/feeds/%d', (int) $feed['feed_id'] );
139 109 } else {
140 - $url = \sprintf( 'https://wordpress.com/reader/feeds/lookup/%s', \rawurlencode( $item['identifier'] ) );
110 + $url = sprintf( 'https://wordpress.com/reader/feeds/lookup/%s', rawurlencode( $item['identifier'] ) );
141 111 }
142 112
143 - return \array_merge(
113 + return array_merge(
144 114 array(
145 - 'reader' => \sprintf(
115 + 'reader' => sprintf(
146 116 '<a href="%1$s" target="_blank">%2$s<span class="screen-reader-text"> %3$s</span><span aria-hidden="true" class="dashicons dashicons-external"></span></a>',
147 - \esc_url( $url ),
148 - \esc_html__( 'View Feed', 'activitypub' ),
117 + esc_url( $url ),
118 + esc_html__( 'View Feed', 'activitypub' ),
149 119 /* translators: Hidden accessibility text. */
150 - \esc_html__( '(opens in a new tab)', 'activitypub' )
120 + esc_html__( '(opens in a new tab)', 'activitypub' )
151 121 ),
152 122 ),
153 123 $actions
154 124 );
@@ -181,224 +151,6 @@
181 151
182 152 \wp_safe_redirect( \add_query_arg( $args, \admin_url( 'post-new.php' ) ) );
183 153 exit;
184 154 }
185 - }
186 -
187 -
188 - /**
189 - * Federate a podcast episode's audio as an ActivityPub attachment.
190 - *
191 - * Jetpack has two podcast surfaces, developed separately, and an episode can come from either:
192 - *
193 - * - Posts to Podcast writes a `jetpack/podcast-episode` block carrying the audio it produced.
194 - * - Jetpack Podcast treats every post in the configured podcast category as an episode, whose
195 - * audio is an ordinary WordPress enclosure.
196 - *
197 - * Neither survives the transformer on its own. Episode audio is usually hosted off-site, so it
198 - * has no attachment ID, and {@see \Activitypub\Transformer\Base::filter_unique_attachments()}
199 - * drops every media entry without one, meaning that audio never reaches this filter. It is
200 - * resolved here instead, and either added or, when the media library already contributed it,
201 - * given the artwork the podcast feed shows for the same episode.
202 - *
203 - * @param array $attachments The ActivityPub attachments.
204 - * @param \WP_Post $post The post being transformed.
205 - *
206 - * @return array The attachments, with the episode audio added or enriched.
207 - */
208 - public static function add_podcast_attachments( $attachments, $post ) {
209 - $is_show_episode = self::is_show_episode( $post );
210 - $episode = self::get_episode_audio( $post, $is_show_episode );
211 -
212 - if ( ! $episode ) {
213 - return $attachments;
214 - }
215 -
216 - $icon = self::get_cover_art( $post, $episode['coverArt'] ?? '', $is_show_episode );
217 - $index = self::find_attachment_by_url( $attachments, $episode['url'] );
218 -
219 - if ( null !== $index ) {
220 - /*
221 - * The audio is already attached, so only the artwork is missing. It is replaced rather
222 - * than filled in: the transformer falls back to the site icon for any audio without a
223 - * poster, and the show's own cover is the better answer for an episode.
224 - */
225 - if ( $icon ) {
226 - $attachments[ $index ]['icon'] = $icon;
227 - }
228 -
229 - return $attachments;
230 - }
231 -
232 - $audio = array(
233 - 'type' => $episode['type'],
234 - 'url' => $episode['url'],
235 - 'name' => \html_entity_decode( \wp_strip_all_tags( \get_the_title( $post ) ), ENT_QUOTES, 'UTF-8' ),
236 - );
237 -
238 - // An episode attached by URL carries no mime type, and omitting the property beats sending
239 - // an empty one, which stops a receiver classifying the attachment at all.
240 - if ( $episode['mediaType'] ) {
241 - $audio['mediaType'] = $episode['mediaType'];
242 - }
243 -
244 - if ( $icon ) {
245 - $audio['icon'] = $icon;
246 - }
247 -
248 - \array_unshift( $attachments, $audio );
249 -
250 - // The transformer trimmed to the configured maximum before this filter ran, so prepending
251 - // the audio would otherwise put the post one attachment over a limit the site chose. An
252 - // episode that arrived alone cannot exceed it, and the maximum is never 0 here because the
253 - // transformer returns before this filter in that case.
254 - if ( \count( $attachments ) > 1 ) {
255 - $attachments = \array_slice( $attachments, 0, get_max_attachments( $post->ID ) );
256 - }
257 -
258 - return $attachments;
259 - }
260 -
261 - /**
262 - * Resolve the audio of a podcast episode.
263 - *
264 - * A Posts to Podcast episode keeps its generated audio in the `jetpack/podcast-episode` block.
265 - * A Jetpack Podcast episode instead carries an ordinary enclosure, which only counts as an
266 - * episode when the post is filed in the configured podcast category, since any post may have an
267 - * enclosure without being part of the show.
268 - *
269 - * @param \WP_Post $post The post being transformed.
270 - * @param bool $is_show_episode Whether the post is filed in the configured podcast category.
271 - *
272 - * @return array|null The episode `type`, `url`, `mediaType` and `coverArt`, or null when the post is not an episode.
273 - */
274 - private static function get_episode_audio( $post, $is_show_episode ) {
275 - $attrs = self::get_episode_block_attrs( $post );
276 -
277 - if ( ! empty( $attrs['mediaUrl'] ) ) {
278 - // Sanitizing drops an unsafe scheme, and an attachment without a URL is invalid.
279 - $url = \esc_url_raw( $attrs['mediaUrl'] );
280 -
281 - if ( $url ) {
282 - return array(
283 - 'type' => \ucfirst( $attrs['mediaType'] ?? 'audio' ),
284 - 'url' => $url,
285 - 'mediaType' => \esc_attr( $attrs['mediaMimeType'] ?? '' ),
286 - 'coverArt' => empty( $attrs['coverArt']['url'] ) ? '' : \esc_url_raw( $attrs['coverArt']['url'] ),
287 - );
288 - }
289 - }
290 -
291 - if ( ! $is_show_episode ) {
292 - return null;
293 - }
294 -
295 - foreach ( get_enclosures( $post->ID ) as $enclosure ) {
296 - $mime_type = $enclosure['mediaType'] ?? '';
297 -
298 - if ( ! \str_starts_with( $mime_type, 'audio/' ) ) {
299 - continue;
300 - }
301 -
302 - $url = \esc_url_raw( $enclosure['url'] );
303 -
304 - if ( $url ) {
305 - return array(
306 - 'type' => 'Audio',
307 - 'url' => $url,
308 - 'mediaType' => \esc_attr( $mime_type ),
309 - );
310 - }
311 - }
312 -
313 - return null;
314 - }
315 -
316 - /**
317 - * Resolve the cover art for a podcast episode.
318 - *
319 - * The episode's own artwork wins, then the post's featured image, then the show image. That is
320 - * the order the podcast feed covers an item with, so a federated episode carries the artwork
321 - * subscribers already see. The show image applies only to an episode of the show itself, so a
322 - * generated episode on an unrelated post does not advertise the podcast's cover.
323 - *
324 - * @param \WP_Post $post The post being transformed.
325 - * @param string $cover_art The episode's own artwork, when it has any.
326 - * @param bool $is_show_episode Whether the post is filed in the configured podcast category.
327 - *
328 - * @return string The cover art URL, or an empty string when none is set.
329 - */
330 - private static function get_cover_art( $post, $cover_art, $is_show_episode ) {
331 - if ( $cover_art ) {
332 - return $cover_art;
333 - }
334 -
335 - $thumbnail = \get_the_post_thumbnail_url( $post, 'full' );
336 -
337 - if ( $thumbnail ) {
338 - return \esc_url_raw( $thumbnail );
339 - }
340 -
341 - if ( ! $is_show_episode || ! \method_exists( Podcast_Settings::class, 'raw_show_image_url' ) ) {
342 - return '';
343 - }
344 -
345 - return \esc_url_raw( (string) Podcast_Settings::raw_show_image_url() );
346 - }
347 -
348 - /**
349 - * Test whether a post is an episode of the site's own podcast.
350 - *
351 - * @param \WP_Post $post The post being transformed.
352 - *
353 - * @return bool Whether the post is in the configured podcast category.
354 - */
355 - private static function is_show_episode( $post ) {
356 - if ( ! \method_exists( Customize_Feed::class, 'resolve_category_id' ) ) {
357 - return false;
358 - }
359 -
360 - // The category can be stored as an ID or as an archive slug, and only Jetpack knows which applies.
361 - $category_id = (int) Customize_Feed::resolve_category_id();
362 -
363 - return $category_id && \in_category( $category_id, $post );
364 - }
365 -
366 - /**
367 - * Read the attributes of the post's `jetpack/podcast-episode` block.
368 - *
369 - * @param \WP_Post $post The post being transformed.
370 - *
371 - * @return array The block attributes, empty when the post has no episode block.
372 - */
373 - private static function get_episode_block_attrs( $post ) {
374 - if ( ! \method_exists( Episode_Block_Tags::class, 'get_block_attrs' ) ) {
375 - return array();
376 - }
377 -
378 - return (array) Episode_Block_Tags::get_block_attrs( $post );
379 - }
380 -
381 - /**
382 - * Find the attachment carrying a given media URL.
383 - *
384 - * Matched on host and path so the same file is still recognised when the stored enclosure and
385 - * the episode block disagree on the scheme, which is the case on every site that moved to HTTPS
386 - * after publishing.
387 - *
388 - * @param array $attachments The ActivityPub attachments.
389 - * @param string $url The media URL to look for.
390 - *
391 - * @return int|string|null The attachment key, or null when the media is not in the list.
392 - */
393 - private static function find_attachment_by_url( $attachments, $url ) {
394 - $needle = normalize_url( $url );
395 -
396 - foreach ( $attachments as $index => $attachment ) {
397 - if ( isset( $attachment['url'] ) && normalize_url( $attachment['url'] ) === $needle ) {
398 - return $index;
399 - }
400 - }
401 -
402 - return null;
403 155 }
404 156 }