PluginProbe
ActivityPub / 7.8.2
ActivityPub v7.8.2
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 / transformer / class-base.php

class-base.php in ActivityPub 7.8.2, at includes/transformer/class-base.php

616 lines 16.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Base Transformer Class file.
4 *
5 * @package Activitypub
6 */
7
8 namespace Activitypub\Transformer;
9
10 use Activitypub\Activity\Activity;
11 use Activitypub\Activity\Base_Object;
12 use Activitypub\Collection\Actors;
13 use Activitypub\Http;
14
15 use function Activitypub\get_upload_baseurl;
16 use function Activitypub\object_to_uri;
17
18 /**
19 * WordPress Base Transformer.
20 *
21 * Transformers are responsible for transforming WordPress objects into different ActivityPub
22 * Object-Types or Activities.
23 */
24 abstract class Base {
25 /**
26 * The WP_Post or WP_Comment object.
27 *
28 * This is the source object of the transformer.
29 *
30 * @var \WP_Post|\WP_Comment|Base_Object|string|array|\WP_Term
31 */
32 protected $item;
33
34 /**
35 * The WP_Post or WP_Comment object.
36 *
37 * @deprecated version 5.0.0
38 *
39 * @var \WP_Post|\WP_Comment
40 */
41 protected $wp_object;
42
43 /**
44 * The content visibility.
45 *
46 * @var string
47 */
48 protected $content_visibility;
49
50 /**
51 * Static function to Transform a WordPress Object.
52 *
53 * This helps to chain the output of the Transformer.
54 *
55 * @param \WP_Post|\WP_Comment|Base_Object|string|array|\WP_term $item The item that should be transformed.
56 *
57 * @return Base
58 */
59 public static function transform( $item ) {
60 return new static( $item );
61 }
62
63 /**
64 * Base constructor.
65 *
66 * @param \WP_Post|\WP_Comment|Base_Object|string|array|\WP_Term $item The item that should be transformed.
67 */
68 public function __construct( $item ) {
69 $this->item = $item;
70 $this->wp_object = $item;
71 }
72
73 /**
74 * Transform all properties with available get(ter) functions.
75 *
76 * @param Base_Object $activity_object The ActivityPub Object.
77 *
78 * @return Base_Object|\WP_Error The transformed ActivityPub Object or WP_Error on failure.
79 */
80 protected function transform_object_properties( $activity_object ) {
81 if ( ! $activity_object || \is_wp_error( $activity_object ) ) {
82 return $activity_object;
83 }
84
85 // Save activity in the context of an activitypub request.
86 \add_filter( 'activitypub_is_activitypub_request', '__return_true' );
87
88 $vars = $activity_object->get_object_var_keys();
89
90 foreach ( $vars as $var ) {
91 $getter = 'get_' . $var;
92
93 if ( \method_exists( $this, $getter ) ) {
94 $value = \call_user_func( array( $this, $getter ) );
95
96 if ( null !== $value ) {
97 $setter = 'set_' . $var;
98
99 /**
100 * Filter the value before it is set to the Activity-Object `$activity_object`.
101 *
102 * @param mixed $value The value that should be set.
103 * @param mixed $item The Object.
104 */
105 $value = \apply_filters( "activitypub_transform_{$setter}", $value, $this->item );
106
107 /**
108 * Filter the value before it is set to the Activity-Object `$activity_object`.
109 *
110 * @param mixed $value The value that should be set.
111 * @param string $var The variable name.
112 * @param mixed $item The Object.
113 */
114 $value = \apply_filters( 'activitypub_transform_set', $value, $var, $this->item );
115
116 \call_user_func( array( $activity_object, $setter ), $value );
117 }
118 }
119 }
120
121 // Remove activity in the context of an activitypub request.
122 \remove_filter( 'activitypub_is_activitypub_request', '__return_true' );
123
124 return $activity_object;
125 }
126
127 /**
128 * Transform the item into an ActivityPub Object.
129 *
130 * @return Base_Object The Activity-Object.
131 */
132 public function to_object() {
133 $activity_object = new Base_Object();
134 $activity_object = $this->transform_object_properties( $activity_object );
135
136 if ( \is_wp_error( $activity_object ) ) {
137 return $activity_object;
138 }
139
140 return $this->set_audience( $activity_object );
141 }
142
143 /**
144 * Get the content visibility.
145 *
146 * @return string The content visibility.
147 */
148 public function get_content_visibility() {
149 if ( ! $this->content_visibility ) {
150 return ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC;
151 }
152
153 return $this->content_visibility;
154 }
155
156 /**
157 * Set the content visibility.
158 *
159 * @param string $content_visibility The content visibility.
160 */
161 public function set_content_visibility( $content_visibility ) {
162 $this->content_visibility = $content_visibility;
163
164 return $this;
165 }
166
167 /**
168 * Set the audience.
169 *
170 * @param Base_Object $activity_object The ActivityPub Object.
171 *
172 * @return Base_Object The ActivityPub Object.
173 */
174 protected function set_audience( $activity_object ) {
175 $public = 'https://www.w3.org/ns/activitystreams#Public';
176 $followers = null;
177 $replied_to = null;
178
179 $actor = Actors::get_by_resource( $this->get_attributed_to() );
180 if ( ! \is_wp_error( $actor ) ) {
181 $followers = $actor->get_followers();
182 }
183
184 $mentions = array_values( $this->get_mentions() );
185
186 if ( $this->get_in_reply_to() ) {
187 $object = Http::get_remote_object( $this->get_in_reply_to() );
188 if ( $object && ! \is_wp_error( $object ) && isset( $object['attributedTo'] ) ) {
189 $replied_to = array( object_to_uri( $object['attributedTo'] ) );
190 }
191 }
192
193 switch ( $this->get_content_visibility() ) {
194 case ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC:
195 $activity_object->add_to( $public );
196 $activity_object->add_cc( $followers );
197 $activity_object->add_cc( $mentions );
198 $activity_object->add_cc( $replied_to );
199 break;
200 case ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC:
201 $activity_object->add_to( $followers );
202 $activity_object->add_to( $mentions );
203 $activity_object->add_to( $replied_to );
204 $activity_object->add_cc( $public );
205 break;
206 case ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE:
207 $activity_object->add_to( $mentions );
208 $activity_object->add_to( $replied_to );
209 }
210
211 return $activity_object;
212 }
213
214 /**
215 * Transform the item to an ActivityPub ID.
216 *
217 * @return string The ID of the WordPress Object.
218 */
219 public function to_id() {
220 /* @var Attachment|Comment|Json|Post|User $this Object transformer. */
221 return $this->get_id();
222 }
223
224 /**
225 * Transforms the ActivityPub Object to an Activity
226 *
227 * @param string $type The Activity-Type.
228 *
229 * @return Activity The Activity.
230 */
231 public function to_activity( $type ) {
232 $object = $this->to_object();
233
234 $activity = new Activity();
235 $activity->set_type( $type );
236
237 // Pre-fill the Activity with data (for example, cc and to).
238 $activity->set_object( $object );
239
240 // Use simple Object (only ID-URI) for Like and Announce.
241 if ( 'Like' === $type ) {
242 $activity->set_object( $object->get_id() );
243 }
244
245 return $activity;
246 }
247
248 /**
249 * Returns a generic locale based on the Blog settings.
250 *
251 * @return string The locale of the blog.
252 */
253 protected function get_locale() {
254 $lang = \strtolower( \strtok( \get_locale(), '_-' ) );
255
256 /**
257 * Filter the locale of the post.
258 *
259 * @param string $lang The locale of the post.
260 * @param mixed $item The post object.
261 *
262 * @return string The filtered locale of the post.
263 */
264 return apply_filters( 'activitypub_locale', $lang, $this->item );
265 }
266
267 /**
268 * Returns the default media type for an Object.
269 *
270 * @return string The media type.
271 */
272 public function get_media_type() {
273 return 'text/html';
274 }
275
276 /**
277 * Returns the content map for the post.
278 *
279 * @return array|null The content map for the post or null if not set.
280 */
281 protected function get_content_map() {
282 if ( ! \method_exists( $this, 'get_content' ) || ! $this->get_content() ) {
283 return null;
284 }
285
286 return array(
287 $this->get_locale() => $this->get_content(),
288 );
289 }
290
291 /**
292 * Returns the name map for the post.
293 *
294 * @return array|null The name map for the post or null if not set.
295 */
296 protected function get_name_map() {
297 if ( ! \method_exists( $this, 'get_name' ) || ! $this->get_name() ) {
298 return null;
299 }
300
301 return array(
302 $this->get_locale() => $this->get_name(),
303 );
304 }
305
306 /**
307 * Returns the summary map for the post.
308 *
309 * @return array|null The summary map for the post or null if not set.
310 */
311 protected function get_summary_map() {
312 if ( ! \method_exists( $this, 'get_summary' ) || ! $this->get_summary() ) {
313 return null;
314 }
315
316 return array(
317 $this->get_locale() => $this->get_summary(),
318 );
319 }
320
321 /**
322 * Returns the tags for the post.
323 *
324 * @return array The tags for the post.
325 */
326 protected function get_tag() {
327 $tags = array();
328 $mentions = $this->get_mentions();
329
330 foreach ( $mentions as $mention => $url ) {
331 $tags[] = array(
332 'type' => 'Mention',
333 'href' => \esc_url( $url ),
334 'name' => \esc_html( $mention ),
335 );
336 }
337
338 return \array_unique( $tags, SORT_REGULAR );
339 }
340
341 /**
342 * Get the attributed to.
343 *
344 * @return string The attributed to.
345 */
346 protected function get_attributed_to() {
347 return null;
348 }
349
350 /**
351 * Extracts mentions from the content.
352 *
353 * @return array The mentions.
354 */
355 protected function get_mentions() {
356 $content = '';
357
358 if ( method_exists( $this, 'get_content' ) ) {
359 $content = $content . ' ' . $this->get_content();
360 }
361
362 if ( method_exists( $this, 'get_summary' ) ) {
363 $content = $content . ' ' . $this->get_summary();
364 }
365
366 /**
367 * Filter the mentions in the post content.
368 *
369 * @param array $mentions The mentions.
370 * @param string $content The post content.
371 * @param \WP_Post $post The post object.
372 *
373 * @return array The filtered mentions.
374 */
375 return apply_filters(
376 'activitypub_extract_mentions',
377 array(),
378 $content,
379 $this->item
380 );
381 }
382
383 /**
384 * Returns the in reply to.
385 *
386 * @return string|array|null The in reply to.
387 */
388 protected function get_in_reply_to() {
389 return null;
390 }
391
392 /**
393 * Parse HTML content for image tags and extract attachment information.
394 *
395 * This method is used by both Post and Comment transformers to find images
396 * embedded in HTML content and extract their attachment IDs and alt text.
397 *
398 * @param array $media The existing media array grouped by type.
399 * @param int $max_images Maximum number of images to extract.
400 * @param string $content The HTML content to parse.
401 *
402 * @return array The updated media array with found images.
403 */
404 protected function parse_html_images( $media, $max_images, $content ) {
405 // If someone calls that function directly, bail.
406 if ( ! \class_exists( '\WP_HTML_Tag_Processor' ) ) {
407 return $media;
408 }
409
410 // Max images can't be negative or zero.
411 if ( $max_images <= 0 ) {
412 return $media;
413 }
414
415 $images = array();
416 $base = get_upload_baseurl();
417 $tags = new \WP_HTML_Tag_Processor( $content );
418
419 // This linter warning is a false positive - we have to re-count each time here as we modify $images.
420 // phpcs:ignore Squiz.PHP.DisallowSizeFunctionsInLoops.Found
421 while ( $tags->next_tag( 'img' ) && ( \count( $images ) <= $max_images ) ) {
422 /**
423 * Filter the image source URL.
424 *
425 * This can be used to modify the image source URL before it is used to
426 * determine the attachment ID.
427 *
428 * @param string $src The image source URL.
429 */
430 $src = \apply_filters( 'activitypub_image_src', $tags->get_attribute( 'src' ) );
431
432 /*
433 * If the img source is in our uploads dir, get the
434 * associated ID. Note: if there's a -500x500
435 * type suffix, we remove it, but we try the original
436 * first in case the original image is actually called
437 * that. Likewise, we try adding the -scaled suffix for
438 * the case that this is a small version of an image
439 * that was big enough to get scaled down on upload:
440 * https://make.wordpress.org/core/2019/10/09/introducing-handling-of-big-images-in-wordpress-5-3/
441 */
442 if ( null !== $src && \str_starts_with( $src, $base ) ) {
443 $img_id = \attachment_url_to_postid( $src );
444
445 if ( 0 === $img_id ) {
446 $count = 0;
447 $src = \strtok( $src, '?' );
448 $img_id = \attachment_url_to_postid( $src );
449 }
450
451 if ( 0 === $img_id ) {
452 $count = 0;
453 $src = \preg_replace( '/-(?:\d+x\d+)(\.[a-zA-Z]+)$/', '$1', $src, 1, $count );
454 if ( $count > 0 ) {
455 $img_id = \attachment_url_to_postid( $src );
456 }
457 }
458
459 if ( 0 === $img_id ) {
460 $src = \preg_replace( '/(\.[a-zA-Z]+)$/', '-scaled$1', $src );
461 $img_id = \attachment_url_to_postid( $src );
462 }
463
464 if ( 0 !== $img_id ) {
465 $images[] = array(
466 'id' => $img_id,
467 'alt' => $tags->get_attribute( 'alt' ),
468 );
469 }
470 }
471 }
472
473 if ( \count( $media['image'] ) <= $max_images ) {
474 $media['image'] = \array_merge( $media['image'], $images );
475 }
476
477 return $media;
478 }
479
480 /**
481 * Transforms a WordPress attachment array to ActivityStreams attachment format.
482 *
483 * @param array $media The WordPress attachment array with 'id' and optional 'alt'.
484 *
485 * @return array The ActivityStreams attachment array.
486 */
487 protected function transform_attachment( $media ) {
488 if ( ! isset( $media['id'] ) ) {
489 return $media;
490 }
491
492 $id = $media['id'];
493 $attachment = array();
494 $mime_type = \get_post_mime_type( $id );
495 $media_type = \strtok( $mime_type, '/' );
496
497 // Switching on image/audio/video.
498 switch ( $media_type ) {
499 case 'image':
500 $image_size = 'large';
501
502 /**
503 * Filter the image URL returned for each post.
504 *
505 * @param array|false $thumbnail The image URL, or false if no image is available.
506 * @param int $id The attachment ID.
507 * @param string $image_size The image size to retrieve. Set to 'large' by default.
508 */
509 $thumbnail = \apply_filters( 'activitypub_get_image', $this->get_attachment_image_src( $id, $image_size ), $id, $image_size );
510
511 if ( $thumbnail ) {
512 $image = array(
513 'type' => 'Image',
514 'url' => \esc_url( $thumbnail[0] ),
515 'mediaType' => \esc_attr( $mime_type ),
516 );
517
518 if ( ! empty( $media['alt'] ) ) {
519 $image['name'] = \html_entity_decode( \wp_strip_all_tags( $media['alt'] ), ENT_QUOTES, 'UTF-8' );
520 } else {
521 $alt = \get_post_meta( $id, '_wp_attachment_image_alt', true );
522 if ( $alt ) {
523 $image['name'] = \html_entity_decode( \wp_strip_all_tags( $alt ), ENT_QUOTES, 'UTF-8' );
524 }
525 }
526
527 $attachment = $image;
528 }
529 break;
530
531 case 'audio':
532 case 'video':
533 $meta = \wp_get_attachment_metadata( $id );
534 $attachment = array(
535 'type' => \ucfirst( $media_type ),
536 'mediaType' => \esc_attr( $mime_type ),
537 'url' => \esc_url( \wp_get_attachment_url( $id ) ),
538 'name' => \esc_attr( \get_the_title( $id ) ),
539 );
540
541 // Height and width for videos.
542 if ( isset( $meta['width'], $meta['height'] ) ) {
543 $attachment['width'] = \esc_attr( $meta['width'] );
544 $attachment['height'] = \esc_attr( $meta['height'] );
545 }
546
547 if ( \method_exists( $this, 'get_icon' ) && $this->get_icon() ) {
548 $attachment['icon'] = object_to_uri( $this->get_icon() );
549 }
550 break;
551 }
552
553 /**
554 * Filter the attachment for a post.
555 *
556 * @param array $attachment The attachment.
557 * @param int $id The attachment ID.
558 *
559 * @return array The filtered attachment.
560 */
561 return \apply_filters( 'activitypub_attachment', $attachment, $id );
562 }
563
564 /**
565 * Return details about an image attachment.
566 *
567 * @param int $id The attachment ID.
568 * @param string $image_size The image size to retrieve. Set to 'large' by default.
569 *
570 * @return array|false Array of image data, or boolean false if no image is available.
571 */
572 protected function get_attachment_image_src( $id, $image_size = 'large' ) {
573 /**
574 * Hook into the image retrieval process. Before image retrieval.
575 *
576 * @param int $id The attachment ID.
577 * @param string $image_size The image size to retrieve. Set to 'large' by default.
578 */
579 \do_action( 'activitypub_get_image_pre', $id, $image_size );
580
581 $image = \wp_get_attachment_image_src( $id, $image_size );
582
583 /**
584 * Hook into the image retrieval process. After image retrieval.
585 *
586 * @param int $id The attachment ID.
587 * @param string $image_size The image size to retrieve. Set to 'large' by default.
588 */
589 \do_action( 'activitypub_get_image_post', $id, $image_size );
590
591 return $image;
592 }
593
594 /**
595 * Filter attachments to ensure uniqueness based on their ID.
596 *
597 * @param array $attachments Array of attachments with 'id' field.
598 *
599 * @return array Array with duplicate attachments removed.
600 */
601 protected function filter_unique_attachments( $attachments ) {
602 $seen_ids = array();
603
604 return \array_filter(
605 $attachments,
606 static function ( $attachment ) use ( &$seen_ids ) {
607 if ( isset( $attachment['id'] ) && ! in_array( $attachment['id'], $seen_ids, true ) ) {
608 $seen_ids[] = $attachment['id'];
609 return true;
610 }
611 return false;
612 }
613 );
614 }
615 }
616