PluginProbe
ActivityPub / 4.7.1
ActivityPub v4.7.1
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 / class-activitypub.php

class-activitypub.php in ActivityPub 4.7.1, at includes/class-activitypub.php

605 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 * ActivityPub Class.
4 *
5 * @package Activitypub
6 */
7
8 namespace Activitypub;
9
10 use Exception;
11 use Activitypub\Collection\Followers;
12 use Activitypub\Collection\Extra_Fields;
13
14 /**
15 * ActivityPub Class.
16 *
17 * @author Matthias Pfefferle
18 */
19 class Activitypub {
20 /**
21 * Initialize the class, registering WordPress hooks.
22 */
23 public static function init() {
24 \add_filter( 'template_include', array( self::class, 'render_activitypub_template' ), 99 );
25 \add_action( 'template_redirect', array( self::class, 'template_redirect' ) );
26 \add_filter( 'redirect_canonical', array( self::class, 'redirect_canonical' ), 10, 2 );
27 \add_filter( 'query_vars', array( self::class, 'add_query_vars' ) );
28 \add_filter( 'pre_get_avatar_data', array( self::class, 'pre_get_avatar_data' ), 11, 2 );
29
30 // Add support for ActivityPub to custom post types.
31 $post_types = \get_option( 'activitypub_support_post_types', array( 'post' ) ) ? \get_option( 'activitypub_support_post_types', array( 'post' ) ) : array();
32
33 foreach ( $post_types as $post_type ) {
34 \add_post_type_support( $post_type, 'activitypub' );
35 }
36
37 \add_action( 'wp_trash_post', array( self::class, 'trash_post' ), 1 );
38 \add_action( 'untrash_post', array( self::class, 'untrash_post' ), 1 );
39
40 \add_action( 'init', array( self::class, 'add_rewrite_rules' ), 11 );
41 \add_action( 'init', array( self::class, 'theme_compat' ), 11 );
42
43 \add_action( 'user_register', array( self::class, 'user_register' ) );
44
45 \add_action( 'in_plugin_update_message-' . ACTIVITYPUB_PLUGIN_BASENAME, array( self::class, 'plugin_update_message' ) );
46
47 if ( site_supports_blocks() ) {
48 \add_action( 'tool_box', array( self::class, 'tool_box' ) );
49 }
50
51 \add_filter( 'activitypub_get_actor_extra_fields', array( Extra_Fields::class, 'default_actor_extra_fields' ), 10, 2 );
52
53 \add_action( 'updated_postmeta', array( self::class, 'updated_postmeta' ), 10, 4 );
54
55 // Register several post_types.
56 self::register_post_types();
57 }
58
59 /**
60 * Activation Hook.
61 */
62 public static function activate() {
63 self::flush_rewrite_rules();
64 Scheduler::register_schedules();
65
66 \add_filter( 'pre_wp_update_comment_count_now', array( Comment::class, 'pre_wp_update_comment_count_now' ), 10, 3 );
67 Migration::update_comment_counts();
68 }
69
70 /**
71 * Deactivation Hook.
72 */
73 public static function deactivate() {
74 self::flush_rewrite_rules();
75 Scheduler::deregister_schedules();
76
77 \remove_filter( 'pre_wp_update_comment_count_now', array( Comment::class, 'pre_wp_update_comment_count_now' ) );
78 Migration::update_comment_counts( 2000 );
79 }
80
81 /**
82 * Uninstall Hook.
83 */
84 public static function uninstall() {
85 Scheduler::deregister_schedules();
86
87 \remove_filter( 'pre_wp_update_comment_count_now', array( Comment::class, 'pre_wp_update_comment_count_now' ) );
88 Migration::update_comment_counts( 2000 );
89 }
90
91 /**
92 * Return a AS2 JSON version of an author, post or page.
93 *
94 * @param string $template The path to the template object.
95 *
96 * @return string The new path to the JSON template.
97 */
98 public static function render_activitypub_template( $template ) {
99 if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
100 return $template;
101 }
102
103 if ( ! is_activitypub_request() ) {
104 return $template;
105 }
106
107 $activitypub_template = false;
108
109 if ( \is_author() && ! is_user_disabled( \get_the_author_meta( 'ID' ) ) ) {
110 $activitypub_template = ACTIVITYPUB_PLUGIN_DIR . '/templates/user-json.php';
111 } elseif ( is_comment() ) {
112 $activitypub_template = ACTIVITYPUB_PLUGIN_DIR . '/templates/comment-json.php';
113 } elseif ( \is_singular() && ! is_post_disabled( \get_the_ID() ) ) {
114 if ( \get_query_var( 'preview' ) ) {
115 \define( 'ACTIVITYPUB_PREVIEW', true );
116
117 /**
118 * Filter the template used for the ActivityPub preview.
119 *
120 * @param string $activitypub_template Absolute path to the template file.
121 */
122 $activitypub_template = apply_filters( 'activitypub_preview_template', ACTIVITYPUB_PLUGIN_DIR . '/templates/post-preview.php' );
123 } else {
124 $activitypub_template = ACTIVITYPUB_PLUGIN_DIR . '/templates/post-json.php';
125 }
126 } elseif ( \is_home() && ! is_user_type_disabled( 'blog' ) ) {
127 $activitypub_template = ACTIVITYPUB_PLUGIN_DIR . '/templates/blog-json.php';
128 }
129
130 /*
131 * Check if the request is authorized.
132 *
133 * @see https://www.w3.org/wiki/SocialCG/ActivityPub/Primer/Authentication_Authorization#Authorized_fetch
134 * @see https://swicg.github.io/activitypub-http-signature/#authorized-fetch
135 */
136 if ( $activitypub_template && use_authorized_fetch() ) {
137 $verification = Signature::verify_http_signature( $_SERVER );
138 if ( \is_wp_error( $verification ) ) {
139 header( 'HTTP/1.1 401 Unauthorized' );
140
141 // Fallback as template_loader can't return http headers.
142 return $template;
143 }
144 }
145
146 if ( $activitypub_template ) {
147 return $activitypub_template;
148 }
149
150 return $template;
151 }
152
153 /**
154 * Add the 'self' link to the header.
155 */
156 public static function add_headers() {
157 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
158 $request_uri = $_SERVER['REQUEST_URI'];
159
160 if ( ! $request_uri ) {
161 return;
162 }
163
164 $id = false;
165
166 // Only add self link to author pages...
167 if ( is_author() ) {
168 if ( ! is_user_disabled( get_queried_object_id() ) ) {
169 $id = get_user_id( get_queried_object_id() );
170 }
171 } elseif ( is_singular() ) { // or posts/pages/custom-post-types...
172 if ( \post_type_supports( \get_post_type(), 'activitypub' ) ) {
173 $id = get_post_id( get_queried_object_id() );
174 }
175 }
176
177 if ( ! $id ) {
178 return;
179 }
180
181 if ( ! headers_sent() ) {
182 header( 'Link: <' . esc_url( $id ) . '>; title="ActivityPub (JSON)"; rel="alternate"; type="application/activity+json"' );
183 }
184
185 add_action(
186 'wp_head',
187 function () use ( $id ) {
188 echo PHP_EOL . '<link rel="alternate" title="ActivityPub (JSON)" type="application/activity+json" href="' . esc_url( $id ) . '" />' . PHP_EOL;
189 }
190 );
191 }
192
193 /**
194 * Add support for `p` and `author` query vars.
195 *
196 * @param string $redirect_url The URL to redirect to.
197 * @param string $requested_url The requested URL.
198 *
199 * @return string $redirect_url
200 */
201 public static function redirect_canonical( $redirect_url, $requested_url ) {
202 if ( ! is_activitypub_request() ) {
203 return $redirect_url;
204 }
205
206 $query = \wp_parse_url( $requested_url, PHP_URL_QUERY );
207
208 if ( ! $query ) {
209 return $redirect_url;
210 }
211
212 $query_params = \wp_parse_args( $query );
213 unset( $query_params['activitypub'] );
214
215 if ( 1 !== count( $query_params ) ) {
216 return $redirect_url;
217 }
218
219 if ( isset( $query_params['p'] ) ) {
220 return null;
221 }
222
223 if ( isset( $query_params['author'] ) ) {
224 return null;
225 }
226
227 return $requested_url;
228 }
229
230 /**
231 * Custom redirects for ActivityPub requests.
232 *
233 * @return void
234 */
235 public static function template_redirect() {
236 self::add_headers();
237
238 $comment_id = get_query_var( 'c', null );
239
240 // Check if it seems to be a comment.
241 if ( ! $comment_id ) {
242 return;
243 }
244
245 $comment = get_comment( $comment_id );
246
247 // Load a 404 page if `c` is set but not valid.
248 if ( ! $comment ) {
249 global $wp_query;
250 $wp_query->set_404();
251 return;
252 }
253
254 // Stop if it's not an ActivityPub comment.
255 if ( is_activitypub_request() && ! is_local_comment( $comment ) ) {
256 return;
257 }
258
259 wp_safe_redirect( get_comment_link( $comment ) );
260 exit;
261 }
262
263 /**
264 * Add the 'activitypub' query variable so WordPress won't mangle it.
265 *
266 * @param array $vars The query variables.
267 *
268 * @return array The query variables.
269 */
270 public static function add_query_vars( $vars ) {
271 $vars[] = 'activitypub';
272 $vars[] = 'preview';
273 $vars[] = 'c';
274 $vars[] = 'p';
275
276 return $vars;
277 }
278
279 /**
280 * Replaces the default avatar.
281 *
282 * @param array $args Arguments passed to get_avatar_data(), after processing.
283 * @param int|string|object $id_or_email A user ID, email address, or comment object.
284 *
285 * @return array $args
286 */
287 public static function pre_get_avatar_data( $args, $id_or_email ) {
288 if (
289 ! $id_or_email instanceof \WP_Comment ||
290 ! isset( $id_or_email->comment_type ) ||
291 $id_or_email->user_id
292 ) {
293 return $args;
294 }
295
296 $allowed_comment_types = \apply_filters( 'get_avatar_comment_types', array( 'comment' ) );
297 if (
298 ! empty( $id_or_email->comment_type ) &&
299 ! \in_array(
300 $id_or_email->comment_type,
301 (array) $allowed_comment_types,
302 true
303 )
304 ) {
305 $args['url'] = false;
306 /** This filter is documented in wp-includes/link-template.php */
307 return \apply_filters( 'get_avatar_data', $args, $id_or_email );
308 }
309
310 // Check if comment has an avatar.
311 $avatar = self::get_avatar_url( $id_or_email->comment_ID );
312
313 if ( $avatar ) {
314 if ( empty( $args['class'] ) ) {
315 $args['class'] = array();
316 } elseif ( \is_string( $args['class'] ) ) {
317 $args['class'] = \explode( ' ', $args['class'] );
318 }
319
320 $args['url'] = $avatar;
321 $args['class'][] = 'avatar-activitypub';
322 $args['class'][] = 'u-photo';
323 $args['class'] = \array_unique( $args['class'] );
324 }
325
326 return $args;
327 }
328
329 /**
330 * Function to retrieve Avatar URL if stored in meta.
331 *
332 * @param int|\WP_Comment $comment The comment ID or object.
333 *
334 * @return string The Avatar URL.
335 */
336 public static function get_avatar_url( $comment ) {
337 if ( \is_numeric( $comment ) ) {
338 $comment = \get_comment( $comment );
339 }
340 return \get_comment_meta( $comment->comment_ID, 'avatar_url', true );
341 }
342
343 /**
344 * Store permalink in meta, to send delete Activity.
345 *
346 * @param string $post_id The Post ID.
347 */
348 public static function trash_post( $post_id ) {
349 \add_post_meta(
350 $post_id,
351 '_activitypub_canonical_url',
352 \get_permalink( $post_id ),
353 true
354 );
355 }
356
357 /**
358 * Delete permalink from meta.
359 *
360 * @param string $post_id The Post ID.
361 */
362 public static function untrash_post( $post_id ) {
363 \delete_post_meta( $post_id, '_activitypub_canonical_url' );
364 }
365
366 /**
367 * Add rewrite rules.
368 */
369 public static function add_rewrite_rules() {
370 /*
371 * If another system needs to take precedence over the ActivityPub rewrite rules,
372 * they can define their own and will manually call the appropriate functions as required.
373 */
374 if ( ACTIVITYPUB_DISABLE_REWRITES ) {
375 return;
376 }
377
378 if ( ! \class_exists( 'Webfinger' ) ) {
379 \add_rewrite_rule(
380 '^.well-known/webfinger',
381 'index.php?rest_route=/' . ACTIVITYPUB_REST_NAMESPACE . '/webfinger',
382 'top'
383 );
384 }
385
386 if ( ! \class_exists( 'Nodeinfo_Endpoint' ) && true === (bool) \get_option( 'blog_public', 1 ) ) {
387 \add_rewrite_rule(
388 '^.well-known/nodeinfo',
389 'index.php?rest_route=/' . ACTIVITYPUB_REST_NAMESPACE . '/nodeinfo/discovery',
390 'top'
391 );
392 \add_rewrite_rule(
393 '^.well-known/x-nodeinfo2',
394 'index.php?rest_route=/' . ACTIVITYPUB_REST_NAMESPACE . '/nodeinfo2',
395 'top'
396 );
397 }
398
399 \add_rewrite_rule(
400 '^@([\w\-\.]+)',
401 'index.php?rest_route=/' . ACTIVITYPUB_REST_NAMESPACE . '/actors/$matches[1]',
402 'top'
403 );
404
405 \add_rewrite_endpoint( 'activitypub', EP_AUTHORS | EP_PERMALINK | EP_PAGES );
406 }
407
408 /**
409 * Flush rewrite rules.
410 */
411 public static function flush_rewrite_rules() {
412 self::add_rewrite_rules();
413 \flush_rewrite_rules();
414 }
415
416 /**
417 * Adds metabox on wp-admin/tools.php.
418 */
419 public static function tool_box() {
420 if ( \current_user_can( 'edit_posts' ) ) {
421 \load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/toolbox.php' );
422 }
423 }
424
425 /**
426 * Theme compatibility stuff.
427 */
428 public static function theme_compat() {
429 // We assume that you want to use Post-Formats when enabling the setting.
430 if ( 'wordpress-post-format' === \get_option( 'activitypub_object_type', ACTIVITYPUB_DEFAULT_OBJECT_TYPE ) ) {
431 if ( ! get_theme_support( 'post-formats' ) ) {
432 // Add support for the Aside, Gallery Post Formats...
433 add_theme_support(
434 'post-formats',
435 array(
436 'gallery',
437 'status',
438 'image',
439 'video',
440 'audio',
441 )
442 );
443 }
444 }
445 }
446
447 /**
448 * Display plugin upgrade notice to users.
449 *
450 * @param array $data The plugin data.
451 */
452 public static function plugin_update_message( $data ) {
453 if ( ! isset( $data['upgrade_notice'] ) ) {
454 return;
455 }
456
457 printf(
458 '<div class="update-message">%s</div>',
459 wp_kses(
460 wpautop( $data['upgrade_notice '] ),
461 array(
462 'p' => array(),
463 'a' => array( 'href', 'title' ),
464 'strong' => array(),
465 'em' => array(),
466 )
467 )
468 );
469 }
470
471 /**
472 * Register the "Followers" Taxonomy.
473 */
474 private static function register_post_types() {
475 \register_post_type(
476 Followers::POST_TYPE,
477 array(
478 'labels' => array(
479 'name' => _x( 'Followers', 'post_type plural name', 'activitypub' ),
480 'singular_name' => _x( 'Follower', 'post_type single name', 'activitypub' ),
481 ),
482 'public' => false,
483 'hierarchical' => false,
484 'rewrite' => false,
485 'query_var' => false,
486 'delete_with_user' => false,
487 'can_export' => true,
488 'supports' => array(),
489 )
490 );
491
492 \register_post_meta(
493 Followers::POST_TYPE,
494 '_activitypub_inbox',
495 array(
496 'type' => 'string',
497 'single' => true,
498 'sanitize_callback' => 'sanitize_url',
499 )
500 );
501
502 \register_post_meta(
503 Followers::POST_TYPE,
504 '_activitypub_errors',
505 array(
506 'type' => 'string',
507 'single' => false,
508 'sanitize_callback' => function ( $value ) {
509 if ( ! is_string( $value ) ) {
510 throw new Exception( 'Error message is no valid string' );
511 }
512
513 return esc_sql( $value );
514 },
515 )
516 );
517
518 \register_post_meta(
519 Followers::POST_TYPE,
520 '_activitypub_user_id',
521 array(
522 'type' => 'string',
523 'single' => false,
524 'sanitize_callback' => function ( $value ) {
525 return esc_sql( $value );
526 },
527 )
528 );
529
530 \register_post_meta(
531 Followers::POST_TYPE,
532 '_activitypub_actor_json',
533 array(
534 'type' => 'string',
535 'single' => true,
536 'sanitize_callback' => function ( $value ) {
537 return sanitize_text_field( $value );
538 },
539 )
540 );
541
542 // Both User and Blog Extra Fields types have the same args.
543 $args = array(
544 'labels' => array(
545 'name' => _x( 'Extra fields', 'post_type plural name', 'activitypub' ),
546 'singular_name' => _x( 'Extra field', 'post_type single name', 'activitypub' ),
547 'add_new' => __( 'Add new', 'activitypub' ),
548 'add_new_item' => __( 'Add new extra field', 'activitypub' ),
549 'new_item' => __( 'New extra field', 'activitypub' ),
550 'edit_item' => __( 'Edit extra field', 'activitypub' ),
551 'view_item' => __( 'View extra field', 'activitypub' ),
552 'all_items' => __( 'All extra fields', 'activitypub' ),
553 ),
554 'public' => false,
555 'hierarchical' => false,
556 'query_var' => false,
557 'has_archive' => false,
558 'publicly_queryable' => false,
559 'show_in_menu' => false,
560 'delete_with_user' => true,
561 'can_export' => true,
562 'exclude_from_search' => true,
563 'show_in_rest' => true,
564 'map_meta_cap' => true,
565 'show_ui' => true,
566 'supports' => array( 'title', 'editor', 'page-attributes' ),
567 );
568
569 \register_post_type( Extra_Fields::USER_POST_TYPE, $args );
570 \register_post_type( Extra_Fields::BLOG_POST_TYPE, $args );
571
572 /**
573 * Fires after ActivityPub custom post types have been registered.
574 */
575 \do_action( 'activitypub_after_register_post_type' );
576 }
577
578 /**
579 * Add the 'activitypub' capability to users who can publish posts.
580 *
581 * @param int $user_id User ID.
582 */
583 public static function user_register( $user_id ) {
584 if ( \user_can( $user_id, 'publish_posts' ) ) {
585 $user = \get_user_by( 'id', $user_id );
586 $user->add_cap( 'activitypub' );
587 }
588 }
589
590 /**
591 * Delete `activitypub_content_visibility` when updated to an empty value.
592 *
593 * @param int $meta_id ID of updated metadata entry.
594 * @param int $object_id Post ID.
595 * @param string $meta_key Metadata key.
596 * @param mixed $meta_value Metadata value. This will be a PHP-serialized string representation of the value
597 * if the value is an array, an object, or itself a PHP-serialized string.
598 */
599 public static function updated_postmeta( $meta_id, $object_id, $meta_key, $meta_value ) {
600 if ( 'activitypub_content_visibility' === $meta_key && empty( $meta_value ) ) {
601 \delete_post_meta( $object_id, 'activitypub_content_visibility' );
602 }
603 }
604 }
605