PluginProbe
ActivityPub / 2.6.0
ActivityPub v2.6.0
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 2.6.0, at includes/class-activitypub.php

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