PluginProbe
ActivityPub / 7.7.0
ActivityPub v7.7.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-router.php

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

305 lines 7.6 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 if ( ! \class_exists( 'Webfinger' ) ) {
45 \add_rewrite_rule(
46 '^.well-known/webfinger',
47 'index.php?rest_route=/' . ACTIVITYPUB_REST_NAMESPACE . '/webfinger',
48 'top'
49 );
50 }
51
52 if ( ! \class_exists( 'Nodeinfo_Endpoint' ) && true === (bool) \get_option( 'blog_public', 1 ) ) {
53 \add_rewrite_rule(
54 '^.well-known/nodeinfo',
55 'index.php?rest_route=/' . ACTIVITYPUB_REST_NAMESPACE . '/nodeinfo',
56 'top'
57 );
58 }
59
60 \add_rewrite_rule( '^@([\w\-\.]+)\/?$', 'index.php?actor=$matches[1]', 'top' );
61 \add_rewrite_endpoint( 'activitypub', EP_AUTHORS | EP_PERMALINK | EP_PAGES );
62 }
63
64 /**
65 * Return a AS2 JSON version of an author, post or page.
66 *
67 * @param string $template The path to the template object.
68 *
69 * @return string The new path to the JSON template.
70 */
71 public static function render_activitypub_template( $template ) {
72 if ( \wp_is_serving_rest_request() || \wp_doing_ajax() ) {
73 return $template;
74 }
75
76 self::add_headers();
77
78 if ( ! is_activitypub_request() || ! should_negotiate_content() ) {
79 if ( \get_query_var( 'p' ) && Outbox::POST_TYPE === \get_post_type( \get_query_var( 'p' ) ) ) {
80 \set_query_var( 'is_404', true );
81 \status_header( 406 );
82 }
83 return $template;
84 }
85
86 if ( Tombstone::exists_local( Query::get_instance()->get_request_url() ) ) {
87 \status_header( 410 );
88 return ACTIVITYPUB_PLUGIN_DIR . 'templates/tombstone-json.php';
89 }
90
91 $activitypub_template = false;
92 $activitypub_object = Query::get_instance()->get_activitypub_object();
93
94 if ( $activitypub_object ) {
95 if ( \get_query_var( 'preview' ) ) {
96 \define( 'ACTIVITYPUB_PREVIEW', true );
97
98 /**
99 * Filter the template used for the ActivityPub preview.
100 *
101 * @param string $activitypub_template Absolute path to the template file.
102 */
103 $activitypub_template = apply_filters( 'activitypub_preview_template', ACTIVITYPUB_PLUGIN_DIR . '/templates/post-preview.php' );
104 } else {
105 $activitypub_template = ACTIVITYPUB_PLUGIN_DIR . 'templates/activitypub-json.php';
106 }
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 ( $activitypub_template && use_authorized_fetch() ) {
116 $verification = Signature::verify_http_signature( $_SERVER );
117 if ( \is_wp_error( $verification ) ) {
118 \status_header( 401 );
119
120 // Fallback as template_loader can't return http headers.
121 return $template;
122 }
123 }
124
125 if ( $activitypub_template ) {
126 \set_query_var( 'is_404', false );
127
128 // Check if header already sent.
129 if ( ! \headers_sent() ) {
130 // Send 200 status header.
131 \status_header( 200 );
132 }
133
134 return $activitypub_template;
135 }
136
137 return $template;
138 }
139
140 /**
141 * Add the 'self' link to the header.
142 */
143 public static function add_headers() {
144 $id = Query::get_instance()->get_activitypub_object_id();
145
146 if ( ! $id ) {
147 return;
148 }
149
150 if ( ! headers_sent() ) {
151 \header( 'Link: <' . esc_url( $id ) . '>; title="ActivityPub (JSON)"; rel="alternate"; type="application/activity+json"', false );
152
153 if ( \get_option( 'activitypub_vary_header', '1' ) ) {
154 // Send Vary header for Accept header.
155 \header( 'Vary: Accept', false );
156 }
157 }
158
159 add_action(
160 'wp_head',
161 function () use ( $id ) {
162 echo PHP_EOL . '<link rel="alternate" title="ActivityPub (JSON)" type="application/activity+json" href="' . esc_url( $id ) . '" />' . PHP_EOL;
163 }
164 );
165 }
166
167 /**
168 * Remove trailing slash from ActivityPub @username requests.
169 *
170 * @param string $redirect_url The URL to redirect to.
171 * @param string $requested_url The requested URL.
172 *
173 * @return string $redirect_url The possibly-unslashed redirect URL.
174 */
175 public static function no_trailing_redirect( $redirect_url, $requested_url ) {
176 if ( get_query_var( 'actor' ) ) {
177 return $requested_url;
178 }
179
180 return $redirect_url;
181 }
182
183 /**
184 * Add support for `p` and `author` query vars.
185 *
186 * @param string $redirect_url The URL to redirect to.
187 * @param string $requested_url The requested URL.
188 *
189 * @return string $redirect_url
190 */
191 public static function redirect_canonical( $redirect_url, $requested_url ) {
192 if ( ! is_activitypub_request() ) {
193 return $redirect_url;
194 }
195
196 $query = \wp_parse_url( $requested_url, PHP_URL_QUERY );
197
198 if ( ! $query ) {
199 return $redirect_url;
200 }
201
202 $query_params = \wp_parse_args( $query );
203 unset( $query_params['activitypub'] );
204 unset( $query_params['stamp'] );
205
206 if ( 1 !== count( $query_params ) ) {
207 return $redirect_url;
208 }
209
210 if ( isset( $query_params['p'] ) ) {
211 return null;
212 }
213
214 if ( isset( $query_params['author'] ) ) {
215 return null;
216 }
217
218 return $requested_url;
219 }
220
221 /**
222 * Custom redirects for ActivityPub requests.
223 *
224 * @return void
225 */
226 public static function template_redirect() {
227 global $wp_query;
228
229 $comment_id = \get_query_var( 'c', null );
230
231 // Check if it seems to be a comment.
232 if ( $comment_id ) {
233 $comment = \get_comment( $comment_id );
234
235 // Load a 404-page if `c` is set but not valid.
236 if ( ! $comment ) {
237 $wp_query->set_404();
238 return;
239 }
240
241 // Stop if it's not an ActivityPub comment.
242 if ( is_activitypub_request() && ! is_local_comment( $comment ) ) {
243 return;
244 }
245
246 \wp_safe_redirect( get_comment_link( $comment ) );
247 exit;
248 }
249
250 $actor = \get_query_var( 'actor', null );
251 if ( $actor ) {
252 $actor = Actors::get_by_username( $actor );
253 if ( ! $actor || \is_wp_error( $actor ) ) {
254 $wp_query->set_404();
255 return;
256 }
257
258 if ( is_activitypub_request() ) {
259 return;
260 }
261
262 \wp_safe_redirect( $actor->get_url(), 301 );
263 exit;
264 }
265 }
266
267 /**
268 * Add the 'activitypub' query variable so WordPress won't mangle it.
269 *
270 * @param array $vars The query variables.
271 *
272 * @return array The query variables.
273 */
274 public static function add_query_vars( $vars ) {
275 $vars[] = 'activitypub';
276 $vars[] = 'preview';
277 $vars[] = 'author';
278 $vars[] = 'actor';
279 $vars[] = 'stamp';
280 $vars[] = 'type';
281 $vars[] = 'c';
282 $vars[] = 'p';
283
284 return $vars;
285 }
286
287 /**
288 * Optimize home page query for ActivityPub requests.
289 *
290 * Skip the database query entirely for ActivityPub requests on the home page
291 * since we only need to return the blog actor, not posts.
292 *
293 * @param \WP_Query $wp_query The WP_Query instance.
294 */
295 public static function fix_is_home_check( $wp_query ) {
296 if (
297 $wp_query->get( 'actor' ) ||
298 $wp_query->get( 'stamp' ) ||
299 $wp_query->get( 'c' )
300 ) {
301 $wp_query->is_home = false;
302 }
303 }
304 }
305