PluginProbe
ActivityPub / 5.7.0
ActivityPub v5.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/transformer/class-base.php +53 -399 9.2.05.7.0 View file →
@@ -6,27 +6,21 @@
6 6 */
7 7
8 8 namespace Activitypub\Transformer;
9 9
10 +use WP_Comment;
11 +use WP_Post;
12 +use WP_Term;
13 +
10 14 use Activitypub\Activity\Activity;
15 +use Activitypub\Collection\Actors;
11 16 use Activitypub\Activity\Base_Object;
12 -use Activitypub\Collection\Actors;
13 -use Activitypub\Http;
14 17
15 -use function Activitypub\get_upload_baseurl;
16 -use function Activitypub\object_to_uri;
17 -
18 18 /**
19 19 * WordPress Base Transformer.
20 20 *
21 - * Transformers are responsible for transforming WordPress objects into different ActivityPub
21 + * Transformers are responsible for transforming a WordPress objects into different ActivityPub
22 22 * Object-Types or Activities.
23 - *
24 - * @method string|null get_content() Returns the content for the transformed item.
25 - * @method string|array|null get_icon() Returns an icon for the transformed item.
26 - * @method string|null get_id() Returns the ID for the transformed item.
27 - * @method string|null get_name() Returns the name for the transformed item.
28 - * @method string|null get_summary() Returns the summary for the transformed item.
29 23 */
30 24 abstract class Base {
31 25 /**
32 26 * The WP_Post or WP_Comment object.
@@ -32,9 +26,9 @@
32 26 * The WP_Post or WP_Comment object.
33 27 *
34 28 * This is the source object of the transformer.
35 29 *
36 - * @var \WP_Post|\WP_Comment|Base_Object|string|array|\WP_Term
30 + * @var WP_Post|WP_Comment|Base_Object|string|array|WP_Term
37 31 */
38 32 protected $item;
39 33
40 34 /**
@@ -39,9 +33,11 @@
39 33
40 34 /**
41 35 * The WP_Post or WP_Comment object.
42 36 *
43 - * @var \WP_Post|\WP_Comment
37 + * @deprecated version 5.0.0
38 + *
39 + * @var WP_Post|WP_Comment
44 40 */
45 41 protected $wp_object;
46 42
47 43 /**
@@ -55,9 +51,9 @@
55 51 * Static function to Transform a WordPress Object.
56 52 *
57 53 * This helps to chain the output of the Transformer.
58 54 *
59 - * @param \WP_Post|\WP_Comment|Base_Object|string|array|\WP_term $item The item that should be transformed.
55 + * @param WP_Post|WP_Comment|Base_Object|string|array|WP_term $item The item that should be transformed.
60 56 *
61 57 * @return Base
62 58 */
63 59 public static function transform( $item ) {
@@ -66,9 +62,9 @@
66 62
67 63 /**
68 64 * Base constructor.
69 65 *
70 - * @param \WP_Post|\WP_Comment|Base_Object|string|array|\WP_Term $item The item that should be transformed.
66 + * @param WP_Post|WP_Comment|Base_Object|string|array|WP_Term $item The item that should be transformed.
71 67 */
72 68 public function __construct( $item ) {
73 69 $this->item = $item;
74 70 $this->wp_object = $item;
@@ -85,11 +81,8 @@
85 81 if ( ! $activity_object || \is_wp_error( $activity_object ) ) {
86 82 return $activity_object;
87 83 }
88 84
89 - // Save activity in the context of an activitypub request.
90 - \add_filter( 'activitypub_is_activitypub_request', '__return_true' );
91 -
92 85 $vars = $activity_object->get_object_var_keys();
93 86
94 87 foreach ( $vars as $var ) {
95 88 $getter = 'get_' . $var;
@@ -121,11 +114,8 @@
121 114 }
122 115 }
123 116 }
124 117
125 - // Remove activity in the context of an activitypub request.
126 - \remove_filter( 'activitypub_is_activitypub_request', '__return_true' );
127 -
128 118 return $activity_object;
129 119 }
130 120
131 121 /**
@@ -130,9 +120,9 @@
130 120
131 121 /**
132 122 * Transform the item into an ActivityPub Object.
133 123 *
134 - * @return Base_Object The Activity-Object.
124 + * @return Base_Object|object The Activity-Object.
135 125 */
136 126 public function to_object() {
137 127 $activity_object = new Base_Object();
138 128 $activity_object = $this->transform_object_properties( $activity_object );
@@ -140,9 +130,11 @@
140 130 if ( \is_wp_error( $activity_object ) ) {
141 131 return $activity_object;
142 132 }
143 133
144 - return $this->set_audience( $activity_object );
134 + $activity_object = $this->set_audience( $activity_object );
135 +
136 + return $activity_object;
145 137 }
146 138
147 139 /**
148 140 * Get the content visibility.
@@ -175,42 +167,30 @@
175 167 *
176 168 * @return Base_Object The ActivityPub Object.
177 169 */
178 170 protected function set_audience( $activity_object ) {
179 - $public = 'https://www.w3.org/ns/activitystreams#Public';
180 - $followers = null;
181 - $replied_to = null;
182 -
183 - $actor = Actors::get_by_resource( $this->get_attributed_to() );
184 - if ( ! \is_wp_error( $actor ) ) {
171 + $public = 'https://www.w3.org/ns/activitystreams#Public';
172 + $actor = Actors::get_by_resource( $this->get_attributed_to() );
173 + if ( ! $actor || is_wp_error( $actor ) ) {
174 + $followers = null;
175 + } else {
185 176 $followers = $actor->get_followers();
186 177 }
178 + $mentions = array_values( $this->get_mentions() );
187 179
188 - $mentions = \array_values( $this->get_mentions() );
189 -
190 - if ( $this->get_in_reply_to() ) {
191 - $object = Http::get_remote_object( $this->get_in_reply_to() );
192 - if ( $object && ! \is_wp_error( $object ) && isset( $object['attributedTo'] ) ) {
193 - $replied_to = array( object_to_uri( $object['attributedTo'] ) );
194 - }
195 - }
196 -
197 180 switch ( $this->get_content_visibility() ) {
198 181 case ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC:
199 182 $activity_object->add_to( $public );
200 183 $activity_object->add_cc( $followers );
201 184 $activity_object->add_cc( $mentions );
202 - $activity_object->add_cc( $replied_to );
203 185 break;
204 186 case ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC:
205 187 $activity_object->add_to( $followers );
206 188 $activity_object->add_to( $mentions );
207 - $activity_object->add_to( $replied_to );
208 189 $activity_object->add_cc( $public );
209 190 break;
210 191 case ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE:
211 192 $activity_object->add_to( $mentions );
212 - $activity_object->add_to( $replied_to );
213 193 }
214 194
215 195 return $activity_object;
216 196 }
@@ -225,21 +205,8 @@
225 205 return $this->get_id();
226 206 }
227 207
228 208 /**
229 - * Returns a Tombstone object for the item.
230 - *
231 - * @return Base_Object The Tombstone object.
232 - */
233 - public function to_tombstone() {
234 - $object = new Base_Object();
235 - $object->set_type( 'Tombstone' );
236 - $object->set_id( $this->to_id() );
237 -
238 - return $object;
239 - }
240 -
241 - /**
242 209 * Transforms the ActivityPub Object to an Activity
243 210 *
244 211 * @param string $type The Activity-Type.
245 212 *
@@ -250,9 +217,9 @@
250 217
251 218 $activity = new Activity();
252 219 $activity->set_type( $type );
253 220
254 - // Pre-fill the Activity with data (for example, cc and to).
221 + // Pre-fill the Activity with data (for example cc and to).
255 222 $activity->set_object( $object );
256 223
257 224 // Use simple Object (only ID-URI) for Like and Announce.
258 225 if ( 'Like' === $type ) {
@@ -269,8 +236,30 @@
269 236 */
270 237 protected function get_locale() {
271 238 $lang = \strtolower( \strtok( \get_locale(), '_-' ) );
272 239
240 + if ( $this->item instanceof \WP_Post ) {
241 + /**
242 + * Deprecates the `activitypub_post_locale` filter.
243 + *
244 + * @param string $lang The locale of the post.
245 + * @param mixed $item The post object.
246 + *
247 + * @return string The filtered locale of the post.
248 + */
249 + $lang = apply_filters_deprecated(
250 + 'activitypub_post_locale',
251 + array(
252 + $lang,
253 + $this->item->ID,
254 + $this->item,
255 + ),
256 + '5.4.0',
257 + 'activitypub_locale',
258 + 'Use the `activitypub_locale` filter instead.'
259 + );
260 + }
261 +
273 262 /**
274 263 * Filter the locale of the post.
275 264 *
276 265 * @param string $lang The locale of the post.
@@ -277,9 +266,9 @@
277 266 * @param mixed $item The post object.
278 267 *
279 268 * @return string The filtered locale of the post.
280 269 */
281 - return \apply_filters( 'activitypub_locale', $lang, $this->item );
270 + return apply_filters( 'activitypub_locale', $lang, $this->item );
282 271 }
283 272
284 273 /**
285 274 * Returns the default media type for an Object.
@@ -346,9 +335,9 @@
346 335
347 336 foreach ( $mentions as $mention => $url ) {
348 337 $tags[] = array(
349 338 'type' => 'Mention',
350 - 'href' => \esc_url_raw( $url ),
339 + 'href' => \esc_url( $url ),
351 340 'name' => \esc_html( $mention ),
352 341 );
353 342 }
354 343
@@ -371,13 +360,13 @@
371 360 */
372 361 protected function get_mentions() {
373 362 $content = '';
374 363
375 - if ( \method_exists( $this, 'get_content' ) ) {
364 + if ( method_exists( $this, 'get_content' ) ) {
376 365 $content = $content . ' ' . $this->get_content();
377 366 }
378 367
379 - if ( \method_exists( $this, 'get_summary' ) ) {
368 + if ( method_exists( $this, 'get_summary' ) ) {
380 369 $content = $content . ' ' . $this->get_summary();
381 370 }
382 371
383 372 /**
@@ -382,353 +371,18 @@
382 371
383 372 /**
384 373 * Filter the mentions in the post content.
385 374 *
386 - * @param array $mentions The mentions.
387 - * @param string $content The post content.
388 - * @param \WP_Post $post The post object.
375 + * @param array $mentions The mentions.
376 + * @param string $content The post content.
377 + * @param WP_Post $post The post object.
389 378 *
390 379 * @return array The filtered mentions.
391 380 */
392 - return \apply_filters(
381 + return apply_filters(
393 382 'activitypub_extract_mentions',
394 383 array(),
395 384 $content,
396 385 $this->item
397 - );
398 - }
399 -
400 - /**
401 - * Returns the in reply to.
402 - *
403 - * @return string|array|null The in reply to.
404 - */
405 - protected function get_in_reply_to() {
406 - return null;
407 - }
408 -
409 - /**
410 - * Parse HTML content for image tags and extract attachment information.
411 - *
412 - * This method is used by both Post and Comment transformers to find images
413 - * embedded in HTML content and extract their attachment IDs and alt text.
414 - *
415 - * @param array $media The existing media array grouped by type.
416 - * @param int $max_images Maximum number of images to extract.
417 - * @param string $content The HTML content to parse.
418 - *
419 - * @return array The updated media array with found images.
420 - */
421 - protected function parse_html_images( $media, $max_images, $content ) {
422 - // If someone calls that function directly, bail.
423 - if ( ! \class_exists( '\WP_HTML_Tag_Processor' ) ) {
424 - return $media;
425 - }
426 -
427 - // Max images can't be negative or zero.
428 - if ( $max_images <= 0 ) {
429 - return $media;
430 - }
431 -
432 - $images = array();
433 - $base = get_upload_baseurl();
434 - $tags = new \WP_HTML_Tag_Processor( $content );
435 -
436 - // This linter warning is a false positive - we have to re-count each time here as we modify $images.
437 - // phpcs:ignore Squiz.PHP.DisallowSizeFunctionsInLoops.Found
438 - while ( $tags->next_tag( 'img' ) && ( \count( $images ) <= $max_images ) ) {
439 - /**
440 - * Filter the image source URL.
441 - *
442 - * This can be used to modify the image source URL before it is used to
443 - * determine the attachment ID.
444 - *
445 - * @param string $src The image source URL.
446 - */
447 - $src = \apply_filters( 'activitypub_image_src', $tags->get_attribute( 'src' ) );
448 -
449 - /*
450 - * If the img source is in our uploads dir, get the
451 - * associated ID. Note: if there's a -500x500
452 - * type suffix, we remove it, but we try the original
453 - * first in case the original image is actually called
454 - * that. Likewise, we try adding the -scaled suffix for
455 - * the case that this is a small version of an image
456 - * that was big enough to get scaled down on upload:
457 - * https://make.wordpress.org/core/2019/10/09/introducing-handling-of-big-images-in-wordpress-5-3/
458 - */
459 - if ( null !== $src && \str_starts_with( $src, $base ) ) {
460 - $img_id = \attachment_url_to_postid( $src );
461 -
462 - if ( 0 === $img_id ) {
463 - $count = 0;
464 - $src = \strtok( $src, '?' );
465 - $img_id = \attachment_url_to_postid( $src );
466 - }
467 -
468 - if ( 0 === $img_id ) {
469 - $count = 0;
470 - $src = \preg_replace( '/-(?:\d+x\d+)(\.[a-zA-Z]+)$/', '$1', $src, 1, $count );
471 - if ( $count > 0 ) {
472 - $img_id = \attachment_url_to_postid( $src );
473 - }
474 - }
475 -
476 - if ( 0 === $img_id ) {
477 - $src = \preg_replace( '/(\.[a-zA-Z]+)$/', '-scaled$1', $src );
478 - $img_id = \attachment_url_to_postid( $src );
479 - }
480 -
481 - if ( 0 !== $img_id ) {
482 - $images[] = array(
483 - 'id' => $img_id,
484 - 'alt' => $tags->get_attribute( 'alt' ),
485 - );
486 - }
487 - }
488 - }
489 -
490 - if ( \count( $media['image'] ) <= $max_images ) {
491 - $media['image'] = \array_merge( $media['image'], $images );
492 - }
493 -
494 - return $media;
495 - }
496 -
497 - /**
498 - * Transforms a WordPress attachment array to ActivityStreams attachment format.
499 - *
500 - * @param array $media The WordPress attachment array with 'id', optional 'alt', and optional 'icon'.
501 - *
502 - * @return array The ActivityStreams attachment array.
503 - */
504 - protected function transform_attachment( $media ) {
505 - if ( ! isset( $media['id'] ) ) {
506 - return $media;
507 - }
508 -
509 - $id = $media['id'];
510 - $attachment = array();
511 - $mime_type = \get_post_mime_type( $id );
512 - $media_type = \strtok( $mime_type, '/' );
513 -
514 - // Switching on image/audio/video.
515 - switch ( $media_type ) {
516 - case 'image':
517 - $image_size = 'large';
518 -
519 - /**
520 - * Filter the image URL returned for each post.
521 - *
522 - * @param array|false $thumbnail The image URL, or false if no image is available.
523 - * @param int $id The attachment ID.
524 - * @param string $image_size The image size to retrieve. Set to 'large' by default.
525 - */
526 - $thumbnail = \apply_filters( 'activitypub_get_image', $this->get_attachment_image_src( $id, $image_size ), $id, $image_size );
527 -
528 - if ( $thumbnail ) {
529 - $image = array(
530 - 'type' => 'Image',
531 - 'url' => \esc_url_raw( $thumbnail[0] ),
532 - 'mediaType' => \esc_attr( $mime_type ),
533 - );
534 -
535 - if ( ! empty( $media['alt'] ) ) {
536 - $image['name'] = \html_entity_decode( \wp_strip_all_tags( $media['alt'] ), ENT_QUOTES, 'UTF-8' );
537 - } else {
538 - $alt = \get_post_meta( $id, '_wp_attachment_image_alt', true );
539 - if ( $alt ) {
540 - $image['name'] = \html_entity_decode( \wp_strip_all_tags( $alt ), ENT_QUOTES, 'UTF-8' );
541 - }
542 - }
543 -
544 - // Add EXIF metadata using Schema.org exifData property (FEP-ee3a).
545 - $exif_data = $this->get_exif_data( $id );
546 - if ( $exif_data ) {
547 - $image['exifData'] = $exif_data;
548 - }
549 -
550 - $attachment = $image;
551 - }
552 - break;
553 -
554 - case 'audio':
555 - case 'video':
556 - $meta = \wp_get_attachment_metadata( $id );
557 - $attachment = array(
558 - 'type' => \ucfirst( $media_type ),
559 - 'mediaType' => \esc_attr( $mime_type ),
560 - 'url' => \esc_url_raw( \wp_get_attachment_url( $id ) ),
561 - 'name' => \esc_attr( \get_the_title( $id ) ),
562 - );
563 -
564 - // Height and width for videos.
565 - if ( isset( $meta['width'], $meta['height'] ) ) {
566 - $attachment['width'] = \esc_attr( $meta['width'] );
567 - $attachment['height'] = \esc_attr( $meta['height'] );
568 - }
569 -
570 - // Use poster image from the block, or fall back to the transformer icon.
571 - if ( ! empty( $media['icon'] ) ) {
572 - $attachment['icon'] = \esc_url_raw( $media['icon'] );
573 - } elseif ( \method_exists( $this, 'get_icon' ) && $this->get_icon() ) {
574 - $attachment['icon'] = object_to_uri( $this->get_icon() );
575 - }
576 - break;
577 - }
578 -
579 - /**
580 - * Filter the attachment for a post.
581 - *
582 - * @param array $attachment The attachment.
583 - * @param int $id The attachment ID.
584 - *
585 - * @return array The filtered attachment.
586 - */
587 - return \apply_filters( 'activitypub_attachment', $attachment, $id );
588 - }
589 -
590 - /**
591 - * Return details about an image attachment.
592 - *
593 - * @param int $id The attachment ID.
594 - * @param string $image_size The image size to retrieve. Set to 'large' by default.
595 - *
596 - * @return array|false Array of image data, or boolean false if no image is available.
597 - */
598 - protected function get_attachment_image_src( $id, $image_size = 'large' ) {
599 - /**
600 - * Hook into the image retrieval process. Before image retrieval.
601 - *
602 - * @param int $id The attachment ID.
603 - * @param string $image_size The image size to retrieve. Set to 'large' by default.
604 - */
605 - \do_action( 'activitypub_get_image_pre', $id, $image_size );
606 -
607 - $image = \wp_get_attachment_image_src( $id, $image_size );
608 -
609 - /**
610 - * Hook into the image retrieval process. After image retrieval.
611 - *
612 - * @param int $id The attachment ID.
613 - * @param string $image_size The image size to retrieve. Set to 'large' by default.
614 - */
615 - \do_action( 'activitypub_get_image_post', $id, $image_size );
616 -
617 - return $image;
618 - }
619 -
620 - /**
621 - * Get EXIF metadata for an image attachment using Schema.org exifData property.
622 - *
623 - * Returns an array of PropertyValue objects as defined in FEP-ee3a.
624 - *
625 - * @link https://codeberg.org/fediverse/fep/src/branch/main/fep/ee3a/fep-ee3a.md
626 - *
627 - * @param int $attachment_id The attachment ID.
628 - *
629 - * @return array|null Array of PropertyValue objects or null if no EXIF data available.
630 - */
631 - protected function get_exif_data( $attachment_id ) {
632 - $metadata = \wp_get_attachment_metadata( $attachment_id );
633 -
634 - if ( empty( $metadata['image_meta'] ) ) {
635 - return null;
636 - }
637 -
638 - $image_meta = $metadata['image_meta'];
639 - $exif_data = array();
640 -
641 - // Map WordPress image_meta to FEP-ee3a EXIF field names.
642 - if ( ! empty( $image_meta['created_timestamp'] ) ) {
643 - $exif_data[] = array(
644 - '@type' => 'PropertyValue',
645 - 'name' => 'DateTime',
646 - 'value' => \gmdate( 'Y:m:d H:i:s', (int) $image_meta['created_timestamp'] ),
647 - );
648 - }
649 -
650 - if ( ! empty( $image_meta['shutter_speed'] ) ) {
651 - $shutter_speed = (float) $image_meta['shutter_speed'];
652 - // Format shutter speed as a fraction (e.g., "1/100") for speeds faster than 1 second.
653 - if ( $shutter_speed > 0 && $shutter_speed < 1 ) {
654 - $value = '1/' . \round( 1 / $shutter_speed );
655 - } elseif ( $shutter_speed >= 1 ) {
656 - $value = (string) $shutter_speed;
657 - }
658 - if ( isset( $value ) ) {
659 - $exif_data[] = array(
660 - '@type' => 'PropertyValue',
661 - 'name' => 'ExposureTime',
662 - 'value' => $value,
663 - );
664 - }
665 - }
666 -
667 - if ( ! empty( $image_meta['aperture'] ) ) {
668 - $exif_data[] = array(
669 - '@type' => 'PropertyValue',
670 - 'name' => 'FNumber',
671 - 'value' => 'f/' . (float) $image_meta['aperture'],
672 - );
673 - }
674 -
675 - if ( ! empty( $image_meta['focal_length'] ) ) {
676 - $exif_data[] = array(
677 - '@type' => 'PropertyValue',
678 - 'name' => 'FocalLength',
679 - 'value' => (string) (float) $image_meta['focal_length'],
680 - );
681 - }
682 -
683 - if ( ! empty( $image_meta['iso'] ) ) {
684 - $exif_data[] = array(
685 - '@type' => 'PropertyValue',
686 - 'name' => 'PhotographicSensitivity',
687 - 'value' => (string) (int) $image_meta['iso'],
688 - );
689 - }
690 -
691 - if ( ! empty( $image_meta['camera'] ) ) {
692 - $exif_data[] = array(
693 - '@type' => 'PropertyValue',
694 - 'name' => 'Model',
695 - 'value' => \sanitize_text_field( $image_meta['camera'] ),
696 - );
697 - }
698 -
699 - /**
700 - * Filter the EXIF data for an image attachment.
701 - *
702 - * @param array $exif_data Array of PropertyValue objects for Schema.org exifData.
703 - * @param array $image_meta The WordPress image_meta array.
704 - * @param int $attachment_id The attachment ID.
705 - *
706 - * @return array The filtered EXIF data array.
707 - */
708 - $exif_data = \apply_filters( 'activitypub_image_exif', $exif_data, $image_meta, $attachment_id );
709 -
710 - return ! empty( $exif_data ) ? $exif_data : null;
711 - }
712 -
713 - /**
714 - * Filter attachments to ensure uniqueness based on their ID.
715 - *
716 - * @param array $attachments Array of attachments with 'id' field.
717 - *
718 - * @return array Array with duplicate attachments removed.
719 - */
720 - protected function filter_unique_attachments( $attachments ) {
721 - $seen_ids = array();
722 -
723 - return \array_filter(
724 - $attachments,
725 - static function ( $attachment ) use ( &$seen_ids ) {
726 - if ( isset( $attachment['id'] ) && ! \in_array( $attachment['id'], $seen_ids, true ) ) {
727 - $seen_ids[] = $attachment['id'];
728 - return true;
729 - }
730 - return false;
731 - }
732 386 );
733 387 }
734 388 }