PluginProbe
ActivityPub / 2.0.0
ActivityPub v2.0.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.0.0, at includes/class-activitypub.php

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