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

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

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