PluginProbe
ActivityPub / 3.2.5
ActivityPub v3.2.5
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 3.2.5, at includes/class-activitypub.php

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