PluginProbe
ActivityPub / 8.0.2
ActivityPub v8.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-router.php

class-router.php in ActivityPub 8.0.2, at includes/class-router.php

351 lines 8.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Router class.
4 *
5 * @package Activitypub
6 */
7
8 namespace Activitypub;
9
10 use Activitypub\Collection\Actors;
11 use Activitypub\Collection\Outbox;
12
13 /**
14 * Router class.
15 */
16 class Router {
17 /**
18 * Initialize the class, registering WordPress hooks.
19 */
20 public static function init() {
21 \add_action( 'init', array( self::class, 'add_rewrite_rules' ), 11 );
22
23 \add_filter( 'template_include', array( self::class, 'render_activitypub_template' ), 99 );
24 \add_action( 'template_redirect', array( self::class, 'template_redirect' ) );
25 \add_filter( 'redirect_canonical', array( self::class, 'redirect_canonical' ), 10, 2 );
26 \add_filter( 'redirect_canonical', array( self::class, 'no_trailing_redirect' ), 10, 2 );
27 \add_filter( 'query_vars', array( self::class, 'add_query_vars' ) );
28
29 \add_action( 'parse_query', array( self::class, 'fix_is_home_check' ) );
30 }
31
32 /**
33 * Add rewrite rules.
34 */
35 public static function add_rewrite_rules() {
36 /*
37 * If another system needs to take precedence over the ActivityPub rewrite rules,
38 * they can define their own and will manually call the appropriate functions as required.
39 */
40 if ( ACTIVITYPUB_DISABLE_REWRITES ) {
41 return;
42 }
43
44 \add_rewrite_rule(
45 '^authorize_interaction/?$',
46 'index.php?rest_route=/' . ACTIVITYPUB_REST_NAMESPACE . '/interactions',
47 'top'
48 );
49
50 if ( ! \class_exists( 'Webfinger' ) ) {
51 \add_rewrite_rule(
52 '^.well-known/webfinger',
53 'index.php?rest_route=/' . ACTIVITYPUB_REST_NAMESPACE . '/webfinger',
54 'top'
55 );
56 }
57
58 if ( ! \class_exists( 'Nodeinfo_Endpoint' ) && true === (bool) \get_option( 'blog_public', 1 ) ) {
59 \add_rewrite_rule(
60 '^.well-known/nodeinfo',
61 'index.php?rest_route=/' . ACTIVITYPUB_REST_NAMESPACE . '/nodeinfo',
62 'top'
63 );
64 }
65
66 \add_rewrite_rule( '^@([\w\-\.]+)\/?$', 'index.php?actor=$matches[1]', 'top' );
67 \add_rewrite_endpoint( 'activitypub', EP_AUTHORS | EP_PERMALINK | EP_PAGES );
68 }
69
70 /**
71 * Return a AS2 JSON version of an author, post or page.
72 *
73 * @param string $template The path to the template object.
74 *
75 * @return string The new path to the JSON template.
76 */
77 public static function render_activitypub_template( $template ) {
78 if ( \wp_is_serving_rest_request() || \wp_doing_ajax() ) {
79 return $template;
80 }
81
82 self::add_headers();
83
84 if ( ! is_activitypub_request() || ! should_negotiate_content() ) {
85 if ( \get_query_var( 'p' ) && Outbox::POST_TYPE === \get_post_type( \get_query_var( 'p' ) ) ) {
86 \set_query_var( 'is_404', true );
87 \status_header( 406 );
88 }
89 return $template;
90 }
91
92 $activitypub_object = Query::get_instance()->get_activitypub_object();
93
94 if ( Tombstone::exists_local( Query::get_instance()->get_request_url() ) ) {
95 // Set 410 Gone for permanently deleted posts, 200 OK for soft-deleted.
96 if ( ! $activitypub_object ) {
97 \status_header( 410 );
98 }
99 return ACTIVITYPUB_PLUGIN_DIR . 'templates/tombstone-json.php';
100 }
101
102 $activitypub_template = false;
103
104 if ( $activitypub_object ) {
105 if ( \get_query_var( 'preview' ) ) {
106 \define( 'ACTIVITYPUB_PREVIEW', true );
107
108 /**
109 * Filter the template used for the ActivityPub preview.
110 *
111 * @param string $activitypub_template Absolute path to the template file.
112 */
113 $activitypub_template = apply_filters( 'activitypub_preview_template', ACTIVITYPUB_PLUGIN_DIR . '/templates/post-preview.php' );
114 } else {
115 $activitypub_template = ACTIVITYPUB_PLUGIN_DIR . 'templates/activitypub-json.php';
116 }
117 }
118
119 /*
120 * Check if the request is authorized.
121 *
122 * @see https://www.w3.org/wiki/SocialCG/ActivityPub/Primer/Authentication_Authorization#Authorized_fetch
123 * @see https://swicg.github.io/activitypub-http-signature/#authorized-fetch
124 */
125 if ( $activitypub_template && use_authorized_fetch() ) {
126 $verification = Signature::verify_http_signature( $_SERVER );
127 if ( \is_wp_error( $verification ) ) {
128 \status_header( 401 );
129
130 // Fallback as template_loader can't return http headers.
131 return $template;
132 }
133 }
134
135 if ( $activitypub_template ) {
136 \set_query_var( 'is_404', false );
137
138 // Check if header already sent.
139 if ( ! \headers_sent() ) {
140 // Send 200 status header.
141 \status_header( 200 );
142 }
143
144 return $activitypub_template;
145 }
146
147 return $template;
148 }
149
150 /**
151 * Add the 'self' link to the header.
152 */
153 public static function add_headers() {
154 $id = Query::get_instance()->get_activitypub_object_id();
155
156 if ( ! $id ) {
157 return;
158 }
159
160 if ( ! headers_sent() ) {
161 \header( 'Link: <' . esc_url( $id ) . '>; title="ActivityPub (JSON)"; rel="alternate"; type="application/activity+json"', false );
162
163 if ( \get_option( 'activitypub_vary_header', '1' ) ) {
164 // Send Vary header for Accept header.
165 \header( 'Vary: Accept', false );
166 }
167 }
168
169 add_action(
170 'wp_head',
171 static function () use ( $id ) {
172 echo PHP_EOL . '<link rel="alternate" title="ActivityPub (JSON)" type="application/activity+json" href="' . esc_url( $id ) . '" />' . PHP_EOL;
173 }
174 );
175 }
176
177 /**
178 * Remove trailing slash from ActivityPub @username requests.
179 *
180 * @param string $redirect_url The URL to redirect to.
181 * @param string $requested_url The requested URL.
182 *
183 * @return string $redirect_url The possibly-unslashed redirect URL.
184 */
185 public static function no_trailing_redirect( $redirect_url, $requested_url ) {
186 if ( get_query_var( 'actor' ) ) {
187 return $requested_url;
188 }
189
190 return $redirect_url;
191 }
192
193 /**
194 * Add support for `p` and `author` query vars.
195 *
196 * @param string $redirect_url The URL to redirect to.
197 * @param string $requested_url The requested URL.
198 *
199 * @return string $redirect_url
200 */
201 public static function redirect_canonical( $redirect_url, $requested_url ) {
202 if ( ! is_activitypub_request() ) {
203 return $redirect_url;
204 }
205
206 $query = \wp_parse_url( $requested_url, PHP_URL_QUERY );
207
208 if ( ! $query ) {
209 return $redirect_url;
210 }
211
212 $query_params = \wp_parse_args( $query );
213 unset( $query_params['activitypub'] );
214 unset( $query_params['stamp'] );
215
216 if ( 1 !== count( $query_params ) ) {
217 return $redirect_url;
218 }
219
220 if ( isset( $query_params['p'] ) ) {
221 return null;
222 }
223
224 if ( isset( $query_params['author'] ) ) {
225 return null;
226 }
227
228 return $requested_url;
229 }
230
231 /**
232 * Custom redirects for ActivityPub requests.
233 *
234 * @return void
235 */
236 public static function template_redirect() {
237 global $wp_query;
238
239 $comment_id = \get_query_var( 'c', null );
240
241 // Check if it seems to be a comment.
242 if ( $comment_id ) {
243 $comment = \get_comment( $comment_id );
244
245 // Load a 404-page if `c` is set but not valid.
246 if ( ! $comment ) {
247 $wp_query->set_404();
248 return;
249 }
250
251 // Stop if it's not an ActivityPub comment.
252 if ( is_activitypub_request() && ! is_local_comment( $comment ) ) {
253 return;
254 }
255
256 \wp_safe_redirect( get_comment_link( $comment ) );
257 exit;
258 }
259
260 $actor = \get_query_var( 'actor', null );
261 if ( $actor ) {
262 $actor = Actors::get_by_username( $actor );
263 if ( ! $actor || \is_wp_error( $actor ) ) {
264 $wp_query->set_404();
265 return;
266 }
267
268 if ( is_activitypub_request() ) {
269 return;
270 }
271
272 \wp_safe_redirect( $actor->get_url(), 301 );
273 exit;
274 }
275
276 $term_id = \get_query_var( 'term_id', null );
277 if ( $term_id ) {
278 $term = \get_term( $term_id );
279
280 // Load a 404-page if `term_id` is set but not valid.
281 if ( ! $term || \is_wp_error( $term ) ) {
282 $wp_query->set_404();
283 return;
284 }
285
286 /**
287 * Filters the taxonomies supported for term redirects.
288 *
289 * @since 7.8.3
290 *
291 * @param array $supported_taxonomies Array of taxonomy names. Default array( 'category', 'post_tag' ).
292 */
293 $supported_taxonomies = \apply_filters( 'activitypub_supported_taxonomies', array( 'category', 'post_tag' ) );
294
295 if ( ! in_array( $term->taxonomy, $supported_taxonomies, true ) ) {
296 return;
297 }
298
299 // Don't redirect for ActivityPub requests.
300 if ( is_activitypub_request() ) {
301 return;
302 }
303
304 $term_link = \get_term_link( $term );
305 if ( ! \is_wp_error( $term_link ) ) {
306 \wp_safe_redirect( $term_link, 301 );
307 exit;
308 }
309 }
310 }
311
312 /**
313 * Add the 'activitypub' query variable so WordPress won't mangle it.
314 *
315 * @param array $vars The query variables.
316 *
317 * @return array The query variables.
318 */
319 public static function add_query_vars( $vars ) {
320 $vars[] = 'activitypub';
321 $vars[] = 'preview';
322 $vars[] = 'author';
323 $vars[] = 'actor';
324 $vars[] = 'stamp';
325 $vars[] = 'type';
326 $vars[] = 'c';
327 $vars[] = 'p';
328 $vars[] = 'term_id';
329
330 return $vars;
331 }
332
333 /**
334 * Optimize home page query for ActivityPub requests.
335 *
336 * Skip the database query entirely for ActivityPub requests on the home page
337 * since we only need to return the blog actor, not posts.
338 *
339 * @param \WP_Query $wp_query The WP_Query instance.
340 */
341 public static function fix_is_home_check( $wp_query ) {
342 if (
343 $wp_query->get( 'actor' ) ||
344 $wp_query->get( 'stamp' ) ||
345 $wp_query->get( 'c' )
346 ) {
347 $wp_query->is_home = false;
348 }
349 }
350 }
351