PluginProbe
ActivityPub / 4.1.1
ActivityPub v4.1.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.1.1, at includes/class-activitypub.php

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