PluginProbe
ActivityPub / 2.5.0
ActivityPub v2.5.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.5.0, at includes/class-activitypub.php

477 lines 11.6 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 // register several post_types
49 self::register_post_types();
50 }
51
52 /**
53 * Activation Hook
54 *
55 * @return void
56 */
57 public static function activate() {
58 self::flush_rewrite_rules();
59 Scheduler::register_schedules();
60 }
61
62 /**
63 * Deactivation Hook
64 *
65 * @return void
66 */
67 public static function deactivate() {
68 self::flush_rewrite_rules();
69 Scheduler::deregister_schedules();
70 }
71
72 /**
73 * Uninstall Hook
74 *
75 * @return void
76 */
77 public static function uninstall() {
78 Scheduler::deregister_schedules();
79 }
80
81 /**
82 * Return a AS2 JSON version of an author, post or page.
83 *
84 * @param string $template The path to the template object.
85 *
86 * @return string The new path to the JSON template.
87 */
88 public static function render_json_template( $template ) {
89 if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
90 return $template;
91 }
92
93 if ( ! is_activitypub_request() ) {
94 return $template;
95 }
96
97 $json_template = false;
98
99 if ( \is_author() && ! is_user_disabled( \get_the_author_meta( 'ID' ) ) ) {
100 $json_template = ACTIVITYPUB_PLUGIN_DIR . '/templates/author-json.php';
101 } elseif ( is_comment() ) {
102 $json_template = ACTIVITYPUB_PLUGIN_DIR . '/templates/comment-json.php';
103 } elseif ( \is_singular() ) {
104 $json_template = ACTIVITYPUB_PLUGIN_DIR . '/templates/post-json.php';
105 } elseif ( \is_home() && ! is_user_type_disabled( 'blog' ) ) {
106 $json_template = ACTIVITYPUB_PLUGIN_DIR . '/templates/blog-json.php';
107 }
108
109 /*
110 * Check if the request is authorized.
111 *
112 * @see https://www.w3.org/wiki/SocialCG/ActivityPub/Primer/Authentication_Authorization#Authorized_fetch
113 * @see https://swicg.github.io/activitypub-http-signature/#authorized-fetch
114 */
115 if ( $json_template && ACTIVITYPUB_AUTHORIZED_FETCH ) {
116 $verification = Signature::verify_http_signature( $_SERVER );
117 if ( \is_wp_error( $verification ) ) {
118 header( 'HTTP/1.1 401 Unauthorized' );
119
120 // fallback as template_loader can't return http headers
121 return $template;
122 }
123 }
124
125 if ( $json_template ) {
126 return $json_template;
127 }
128
129 return $template;
130 }
131
132 /**
133 * Custom redirects for ActivityPub requests.
134 *
135 * @return void
136 */
137 public static function template_redirect() {
138 $comment_id = get_query_var( 'c', null );
139
140 // check if it seems to be a comment
141 if ( ! $comment_id ) {
142 return;
143 }
144
145 $comment = get_comment( $comment_id );
146
147 // load a 404 page if `c` is set but not valid
148 if ( ! $comment ) {
149 global $wp_query;
150 $wp_query->set_404();
151 return;
152 }
153
154 // stop if it's not an ActivityPub comment
155 if ( is_activitypub_request() && ! is_local_comment( $comment ) ) {
156 return;
157 }
158
159 wp_safe_redirect( get_comment_link( $comment ) );
160 exit;
161 }
162
163 /**
164 * Add the 'activitypub' query variable so WordPress won't mangle it.
165 */
166 public static function add_query_vars( $vars ) {
167 $vars[] = 'activitypub';
168 $vars[] = 'c';
169 $vars[] = 'p';
170
171 return $vars;
172 }
173
174 /**
175 * Replaces the default avatar.
176 *
177 * @param array $args Arguments passed to get_avatar_data(), after processing.
178 * @param int|string|object $id_or_email A user ID, email address, or comment object.
179 *
180 * @return array $args
181 */
182 public static function pre_get_avatar_data( $args, $id_or_email ) {
183 if (
184 ! $id_or_email instanceof \WP_Comment ||
185 ! isset( $id_or_email->comment_type ) ||
186 $id_or_email->user_id
187 ) {
188 return $args;
189 }
190
191 $allowed_comment_types = \apply_filters( 'get_avatar_comment_types', array( 'comment' ) );
192 if (
193 ! empty( $id_or_email->comment_type ) &&
194 ! \in_array(
195 $id_or_email->comment_type,
196 (array) $allowed_comment_types,
197 true
198 )
199 ) {
200 $args['url'] = false;
201 /** This filter is documented in wp-includes/link-template.php */
202 return \apply_filters( 'get_avatar_data', $args, $id_or_email );
203 }
204
205 // Check if comment has an avatar.
206 $avatar = self::get_avatar_url( $id_or_email->comment_ID );
207
208 if ( $avatar ) {
209 if ( empty( $args['class'] ) ) {
210 $args['class'] = array();
211 } elseif ( \is_string( $args['class'] ) ) {
212 $args['class'] = \explode( ' ', $args['class'] );
213 }
214
215 $args['url'] = $avatar;
216 $args['class'][] = 'avatar-activitypub';
217 $args['class'][] = 'u-photo';
218 $args['class'] = \array_unique( $args['class'] );
219 }
220
221 return $args;
222 }
223
224 /**
225 * Function to retrieve Avatar URL if stored in meta.
226 *
227 * @param int|WP_Comment $comment
228 *
229 * @return string $url
230 */
231 public static function get_avatar_url( $comment ) {
232 if ( \is_numeric( $comment ) ) {
233 $comment = \get_comment( $comment );
234 }
235 return \get_comment_meta( $comment->comment_ID, 'avatar_url', true );
236 }
237
238 /**
239 * Store permalink in meta, to send delete Activity.
240 *
241 * @param string $post_id The Post ID.
242 *
243 * @return void
244 */
245 public static function trash_post( $post_id ) {
246 \add_post_meta(
247 $post_id,
248 'activitypub_canonical_url',
249 \get_permalink( $post_id ),
250 true
251 );
252 }
253
254 /**
255 * Delete permalink from meta
256 *
257 * @param string $post_id The Post ID
258 *
259 * @return void
260 */
261 public static function untrash_post( $post_id ) {
262 \delete_post_meta( $post_id, 'activitypub_canonical_url' );
263 }
264
265 /**
266 * Add rewrite rules
267 */
268 public static function add_rewrite_rules() {
269 // If another system needs to take precedence over the ActivityPub rewrite rules,
270 // they can define their own and will manually call the appropriate functions as required.
271 if ( ACTIVITYPUB_DISABLE_REWRITES ) {
272 return;
273 }
274
275 if ( ! \class_exists( 'Webfinger' ) ) {
276 \add_rewrite_rule(
277 '^.well-known/webfinger',
278 'index.php?rest_route=/' . ACTIVITYPUB_REST_NAMESPACE . '/webfinger',
279 'top'
280 );
281 }
282
283 if ( ! \class_exists( 'Nodeinfo_Endpoint' ) && true === (bool) \get_option( 'blog_public', 1 ) ) {
284 \add_rewrite_rule(
285 '^.well-known/nodeinfo',
286 'index.php?rest_route=/' . ACTIVITYPUB_REST_NAMESPACE . '/nodeinfo/discovery',
287 'top'
288 );
289 \add_rewrite_rule(
290 '^.well-known/x-nodeinfo2',
291 'index.php?rest_route=/' . ACTIVITYPUB_REST_NAMESPACE . '/nodeinfo2',
292 'top'
293 );
294 }
295
296 \add_rewrite_rule(
297 '^@([\w\-\.]+)',
298 'index.php?rest_route=/' . ACTIVITYPUB_REST_NAMESPACE . '/actors/$matches[1]',
299 'top'
300 );
301
302 \add_rewrite_endpoint( 'activitypub', EP_AUTHORS | EP_PERMALINK | EP_PAGES );
303 }
304
305 /**
306 * Flush rewrite rules;
307 */
308 public static function flush_rewrite_rules() {
309 self::add_rewrite_rules();
310 \flush_rewrite_rules();
311 }
312
313 /**
314 * Theme compatibility stuff
315 *
316 * @return void
317 */
318 public static function theme_compat() {
319 $site_icon = get_theme_support( 'custom-logo' );
320
321 if ( ! $site_icon ) {
322 // custom logo support
323 add_theme_support(
324 'custom-logo',
325 array(
326 'height' => 80,
327 'width' => 80,
328 )
329 );
330 }
331
332 $custom_header = get_theme_support( 'custom-header' );
333
334 if ( ! $custom_header ) {
335 // This theme supports a custom header
336 $custom_header_args = array(
337 'width' => 1250,
338 'height' => 600,
339 'header-text' => true,
340 );
341 add_theme_support( 'custom-header', $custom_header_args );
342 }
343
344 // We assume that you want to use Post-Formats when enabling the setting
345 if ( 'wordpress-post-format' === \get_option( 'activitypub_object_type', ACTIVITYPUB_DEFAULT_OBJECT_TYPE ) ) {
346 if ( ! get_theme_support( 'post-formats' ) ) {
347 // Add support for the Aside, Gallery Post Formats...
348 add_theme_support(
349 'post-formats',
350 array(
351 'gallery',
352 'status',
353 'image',
354 'video',
355 'audio',
356 )
357 );
358 }
359 }
360 }
361
362 /**
363 * Display plugin upgrade notice to users
364 *
365 * @param array $data The plugin data
366 *
367 * @return void
368 */
369 public static function plugin_update_message( $data ) {
370 if ( ! isset( $data['upgrade_notice'] ) ) {
371 return;
372 }
373
374 printf(
375 '<div class="update-message">%s</div>',
376 wp_kses(
377 wpautop( $data['upgrade_notice '] ),
378 array(
379 'p' => array(),
380 'a' => array( 'href', 'title' ),
381 'strong' => array(),
382 'em' => array(),
383 )
384 )
385 );
386 }
387
388 /**
389 * Register the "Followers" Taxonomy
390 *
391 * @return void
392 */
393 private static function register_post_types() {
394 \register_post_type(
395 Followers::POST_TYPE,
396 array(
397 'labels' => array(
398 'name' => _x( 'Followers', 'post_type plural name', 'activitypub' ),
399 'singular_name' => _x( 'Follower', 'post_type single name', 'activitypub' ),
400 ),
401 'public' => false,
402 'hierarchical' => false,
403 'rewrite' => false,
404 'query_var' => false,
405 'delete_with_user' => false,
406 'can_export' => true,
407 'supports' => array(),
408 )
409 );
410
411 \register_post_meta(
412 Followers::POST_TYPE,
413 'activitypub_inbox',
414 array(
415 'type' => 'string',
416 'single' => true,
417 'sanitize_callback' => 'sanitize_url',
418 )
419 );
420
421 \register_post_meta(
422 Followers::POST_TYPE,
423 'activitypub_errors',
424 array(
425 'type' => 'string',
426 'single' => false,
427 'sanitize_callback' => function ( $value ) {
428 if ( ! is_string( $value ) ) {
429 throw new Exception( 'Error message is no valid string' );
430 }
431
432 return esc_sql( $value );
433 },
434 )
435 );
436
437 \register_post_meta(
438 Followers::POST_TYPE,
439 'activitypub_user_id',
440 array(
441 'type' => 'string',
442 'single' => false,
443 'sanitize_callback' => function ( $value ) {
444 return esc_sql( $value );
445 },
446 )
447 );
448
449 \register_post_meta(
450 Followers::POST_TYPE,
451 'activitypub_actor_json',
452 array(
453 'type' => 'string',
454 'single' => true,
455 'sanitize_callback' => function ( $value ) {
456 return sanitize_text_field( $value );
457 },
458 )
459 );
460
461 \do_action( 'activitypub_after_register_post_type' );
462 }
463
464 /**
465 * Add the 'activitypub' query variable so WordPress won't mangle it.
466 *
467 * @param int $user_id User ID.
468 * @param array $userdata The raw array of data passed to wp_insert_user().
469 */
470 public static function user_register( $user_id ) {
471 if ( \user_can( $user_id, 'publish_posts' ) ) {
472 $user = \get_user_by( 'id', $user_id );
473 $user->add_cap( 'activitypub' );
474 }
475 }
476 }
477