PluginProbe
ActivityPub / 1.0.7
ActivityPub v1.0.7
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 / functions.php

functions.php in ActivityPub 1.0.7, at includes/functions.php

484 lines 12.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 WP_Error;
5 use Activitypub\Http;
6 use Activitypub\Activity\Activity;
7 use Activitypub\Collection\Followers;
8
9 /**
10 * Returns the ActivityPub default JSON-context
11 *
12 * @return array the activitypub context
13 */
14 function get_context() {
15 $context = Activity::CONTEXT;
16
17 return \apply_filters( 'activitypub_json_context', $context );
18 }
19
20 function safe_remote_post( $url, $body, $user_id ) {
21 return Http::post( $url, $body, $user_id );
22 }
23
24 function safe_remote_get( $url ) {
25 return Http::get( $url );
26 }
27
28 /**
29 * Returns a users WebFinger "resource"
30 *
31 * @param int $user_id The User-ID.
32 *
33 * @return string The User-Resource.
34 */
35 function get_webfinger_resource( $user_id ) {
36 return Webfinger::get_user_resource( $user_id );
37 }
38
39 /**
40 * Requests the Meta-Data from the Actors profile
41 *
42 * @param string $actor The Actor URL.
43 * @param bool $cached If the result should be cached.
44 *
45 * @return array The Actor profile as array
46 */
47 function get_remote_metadata_by_actor( $actor, $cached = true ) {
48 $pre = apply_filters( 'pre_get_remote_metadata_by_actor', false, $actor );
49 if ( $pre ) {
50 return $pre;
51 }
52 if ( preg_match( '/^@?' . ACTIVITYPUB_USERNAME_REGEXP . '$/i', $actor ) ) {
53 $actor = Webfinger::resolve( $actor );
54 }
55
56 if ( ! $actor ) {
57 return new WP_Error( 'activitypub_no_valid_actor_identifier', \__( 'The "actor" identifier is not valid', 'activitypub' ), array( 'status' => 404, 'actor' => $actor ) );
58 }
59
60 if ( is_wp_error( $actor ) ) {
61 return $actor;
62 }
63
64 $transient_key = 'activitypub_' . $actor;
65
66 // only check the cache if needed.
67 if ( $cached ) {
68 $metadata = \get_transient( $transient_key );
69
70 if ( $metadata ) {
71 return $metadata;
72 }
73 }
74
75 if ( ! \wp_http_validate_url( $actor ) ) {
76 $metadata = new WP_Error( 'activitypub_no_valid_actor_url', \__( 'The "actor" is no valid URL', 'activitypub' ), array( 'status' => 400, 'actor' => $actor ) );
77 \set_transient( $transient_key, $metadata, HOUR_IN_SECONDS ); // Cache the error for a shorter period.
78 return $metadata;
79 }
80
81 $short_timeout = function() {
82 return 3;
83 };
84 add_filter( 'activitypub_remote_get_timeout', $short_timeout );
85 $response = Http::get( $actor );
86 remove_filter( 'activitypub_remote_get_timeout', $short_timeout );
87 if ( \is_wp_error( $response ) ) {
88 \set_transient( $transient_key, $response, HOUR_IN_SECONDS ); // Cache the error for a shorter period.
89 return $response;
90 }
91
92 $metadata = \wp_remote_retrieve_body( $response );
93 $metadata = \json_decode( $metadata, true );
94
95 \set_transient( $transient_key, $metadata, WEEK_IN_SECONDS );
96
97 if ( ! $metadata ) {
98 $metadata = new WP_Error( 'activitypub_invalid_json', \__( 'No valid JSON data', 'activitypub' ), array( 'status' => 400, 'actor' => $actor ) );
99 \set_transient( $transient_key, $metadata, HOUR_IN_SECONDS ); // Cache the error for a shorter period.
100 return $metadata;
101 }
102
103 return $metadata;
104 }
105
106 /**
107 * Returns the followers of a given user.
108 *
109 * @param int $user_id The User-ID.
110 *
111 * @return array The followers.
112 */
113 function get_followers( $user_id ) {
114 return Followers::get_followers( $user_id );
115 }
116
117 /**
118 * Count the number of followers for a given user.
119 *
120 * @param int $user_id The User-ID.
121 *
122 * @return int The number of followers.
123 */
124 function count_followers( $user_id ) {
125 return Followers::count_followers( $user_id );
126 }
127
128 /**
129 * Examine a url and try to determine the author ID it represents.
130 *
131 * Checks are supposedly from the hosted site blog.
132 *
133 * @param string $url Permalink to check.
134 *
135 * @return int User ID, or 0 on failure.
136 */
137 function url_to_authorid( $url ) {
138 global $wp_rewrite;
139
140 // check if url hase the same host
141 if ( \wp_parse_url( \site_url(), \PHP_URL_HOST ) !== \wp_parse_url( $url, \PHP_URL_HOST ) ) {
142 return 0;
143 }
144
145 // first, check to see if there is a 'author=N' to match against
146 if ( \preg_match( '/[?&]author=(\d+)/i', $url, $values ) ) {
147 $id = \absint( $values[1] );
148 if ( $id ) {
149 return $id;
150 }
151 }
152
153 // check to see if we are using rewrite rules
154 $rewrite = $wp_rewrite->wp_rewrite_rules();
155
156 // not using rewrite rules, and 'author=N' method failed, so we're out of options
157 if ( empty( $rewrite ) ) {
158 return 0;
159 }
160
161 // generate rewrite rule for the author url
162 $author_rewrite = $wp_rewrite->get_author_permastruct();
163 $author_regexp = \str_replace( '%author%', '', $author_rewrite );
164
165 // match the rewrite rule with the passed url
166 if ( \preg_match( '/https?:\/\/(.+)' . \preg_quote( $author_regexp, '/' ) . '([^\/]+)/i', $url, $match ) ) {
167 $user = \get_user_by( 'slug', $match[2] );
168 if ( $user ) {
169 return $user->ID;
170 }
171 }
172
173 return 0;
174 }
175
176 /**
177 * Check for Tombstone Objects
178 *
179 * @see https://www.w3.org/TR/activitypub/#delete-activity-outbox
180 *
181 * @param WP_Error $wp_error A WP_Error-Response of an HTTP-Request
182 *
183 * @return boolean true if HTTP-Code is 410 or 404
184 */
185 function is_tombstone( $wp_error ) {
186 if ( ! is_wp_error( $wp_error ) ) {
187 return false;
188 }
189
190 if ( in_array( (int) $wp_error->get_error_code(), array( 404, 410 ), true ) ) {
191 return true;
192 }
193
194 return false;
195 }
196
197 /**
198 * Get the REST URL relative to this plugin's namespace.
199 *
200 * @param string $path Optional. REST route path. Otherwise this plugin's namespaced root.
201 *
202 * @return string REST URL relative to this plugin's namespace.
203 */
204 function get_rest_url_by_path( $path = '' ) {
205 // we'll handle the leading slash.
206 $path = ltrim( $path, '/' );
207 $namespaced_path = sprintf( '/%s/%s', ACTIVITYPUB_REST_NAMESPACE, $path );
208 return \get_rest_url( null, $namespaced_path );
209 }
210
211 /**
212 * Convert a string from camelCase to snake_case.
213 *
214 * @param string $string The string to convert.
215 *
216 * @return string The converted string.
217 */
218 // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.stringFound
219 function camel_to_snake_case( $string ) {
220 return strtolower( preg_replace( '/(?<!^)[A-Z]/', '_$0', $string ) );
221 }
222
223 /**
224 * Convert a string from snake_case to camelCase.
225 *
226 * @param string $string The string to convert.
227 *
228 * @return string The converted string.
229 */
230 // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.stringFound
231 function snake_to_camel_case( $string ) {
232 return lcfirst( str_replace( '_', '', ucwords( $string, '_' ) ) );
233 }
234
235 /**
236 * Escapes a Tag, to be used as a hashtag.
237 *
238 * @param string $string The string to escape.
239 *
240 * @return string The escaped hastag.
241 */
242 function esc_hashtag( $string ) {
243
244 $hashtag = \wp_specialchars_decode( $string, ENT_QUOTES );
245 // Remove all characters that are not letters, numbers, or underscores.
246 $hashtag = \preg_replace( '/emoji-regex(*SKIP)(?!)|[^\p{L}\p{Nd}_]+/u', '_', $hashtag );
247
248 // Capitalize every letter that is preceded by an underscore.
249 $hashtag = preg_replace_callback(
250 '/_(.)/',
251 function ( $matches ) {
252 return '' . strtoupper( $matches[1] );
253 },
254 $hashtag
255 );
256
257 // Add a hashtag to the beginning of the string.
258 $hashtag = ltrim( $hashtag, '#' );
259 $hashtag = '#' . $hashtag;
260
261 /**
262 * Allow defining your own custom hashtag generation rules.
263 *
264 * @param string $hashtag The hashtag to be returned.
265 * @param string $string The original string.
266 */
267 $hashtag = apply_filters( 'activitypub_esc_hashtag', $hashtag, $string );
268
269 return esc_html( $hashtag );
270 }
271
272 /**
273 * Check if a request is for an ActivityPub request.
274 *
275 * @return bool False by default.
276 */
277 function is_activitypub_request() {
278 global $wp_query;
279
280 /*
281 * ActivityPub requests are currently only made for
282 * author archives, singular posts, and the homepage.
283 */
284 if ( ! \is_author() && ! \is_singular() && ! \is_home() && ! defined( '\REST_REQUEST' ) ) {
285 return false;
286 }
287
288 // One can trigger an ActivityPub request by adding ?activitypub to the URL.
289 // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.VariableRedeclaration
290 global $wp_query;
291 if ( isset( $wp_query->query_vars['activitypub'] ) ) {
292 return true;
293 }
294
295 /*
296 * The other (more common) option to make an ActivityPub request
297 * is to send an Accept header.
298 */
299 if ( isset( $_SERVER['HTTP_ACCEPT'] ) ) {
300 $accept = sanitize_text_field( wp_unslash( $_SERVER['HTTP_ACCEPT'] ) );
301
302 /*
303 * $accept can be a single value, or a comma separated list of values.
304 * We want to support both scenarios,
305 * and return true when the header includes at least one of the following:
306 * - application/activity+json
307 * - application/ld+json
308 * - application/json
309 */
310 if ( preg_match( '/(application\/(ld\+json|activity\+json|json))/i', $accept ) ) {
311 return true;
312 }
313 }
314
315 return false;
316 }
317
318 /**
319 * This function checks if a user is disabled for ActivityPub.
320 *
321 * @param int $user_id The User-ID.
322 *
323 * @return boolean True if the user is disabled, false otherwise.
324 */
325 function is_user_disabled( $user_id ) {
326 $return = false;
327
328 switch ( $user_id ) {
329 // if the user is the application user, it's always enabled.
330 case \Activitypub\Collection\Users::APPLICATION_USER_ID:
331 $return = false;
332 break;
333 // if the user is the blog user, it's only enabled in single-user mode.
334 case \Activitypub\Collection\Users::BLOG_USER_ID:
335 if ( is_user_type_disabled( 'blog' ) ) {
336 $return = true;
337 break;
338 }
339
340 $return = false;
341 break;
342 // if the user is any other user, it's enabled if it can publish posts.
343 default:
344 if ( ! \get_user_by( 'id', $user_id ) ) {
345 $return = true;
346 break;
347 }
348
349 if ( is_user_type_disabled( 'user' ) ) {
350 $return = true;
351 break;
352 }
353
354 if ( ! \user_can( $user_id, 'publish_posts' ) ) {
355 $return = true;
356 break;
357 }
358
359 $return = false;
360 break;
361 }
362
363 return apply_filters( 'activitypub_is_user_disabled', $return, $user_id );
364 }
365
366 /**
367 * Checks if a User-Type is disabled for ActivityPub.
368 *
369 * This function is used to check if the 'blog' or 'user'
370 * type is disabled for ActivityPub.
371 *
372 * @param enum $type Can be 'blog' or 'user'.
373 *
374 * @return boolean True if the user type is disabled, false otherwise.
375 */
376 function is_user_type_disabled( $type ) {
377 switch ( $type ) {
378 case 'blog':
379 if ( \defined( 'ACTIVITYPUB_SINGLE_USER_MODE' ) ) {
380 if ( ACTIVITYPUB_SINGLE_USER_MODE ) {
381 $return = false;
382 break;
383 }
384 }
385
386 if ( \defined( 'ACTIVITYPUB_DISABLE_BLOG_USER' ) ) {
387 $return = ACTIVITYPUB_DISABLE_BLOG_USER;
388 break;
389 }
390
391 if ( '1' !== \get_option( 'activitypub_enable_blog_user', '0' ) ) {
392 $return = true;
393 break;
394 }
395
396 $return = false;
397 break;
398 case 'user':
399 if ( \defined( 'ACTIVITYPUB_SINGLE_USER_MODE' ) ) {
400 if ( ACTIVITYPUB_SINGLE_USER_MODE ) {
401 $return = true;
402 break;
403 }
404 }
405
406 if ( \defined( 'ACTIVITYPUB_DISABLE_USER' ) ) {
407 $return = ACTIVITYPUB_DISABLE_USER;
408 break;
409 }
410
411 if ( '1' !== \get_option( 'activitypub_enable_users', '1' ) ) {
412 $return = true;
413 break;
414 }
415
416 $return = false;
417 break;
418 default:
419 $return = new WP_Error( 'activitypub_wrong_user_type', __( 'Wrong user type', 'activitypub' ), array( 'status' => 400 ) );
420 break;
421 }
422
423 return apply_filters( 'activitypub_is_user_type_disabled', $return, $type );
424 }
425
426 /**
427 * Check if the blog is in single-user mode.
428 *
429 * @return boolean True if the blog is in single-user mode, false otherwise.
430 */
431 function is_single_user() {
432 if (
433 false === is_user_type_disabled( 'blog' ) &&
434 true === is_user_type_disabled( 'user' )
435 ) {
436 return true;
437 }
438
439 return false;
440 }
441
442 /**
443 * Check if a site supports the block editor.
444 *
445 * @return boolean True if the site supports the block editor, false otherwise.
446 */
447 function site_supports_blocks() {
448 if ( \version_compare( \get_bloginfo( 'version' ), '5.9', '<' ) ) {
449 return false;
450 }
451
452 if ( ! \function_exists( 'register_block_type_from_metadata' ) ) {
453 return false;
454 }
455
456 /**
457 * Allow plugins to disable block editor support,
458 * thus disabling blocks registered by the ActivityPub plugin.
459 *
460 * @param boolean $supports_blocks True if the site supports the block editor, false otherwise.
461 */
462 return apply_filters( 'activitypub_site_supports_blocks', true );
463 }
464
465 /**
466 * Check if data is valid JSON.
467 *
468 * @param string $data The data to check.
469 *
470 * @return boolean True if the data is JSON, false otherwise.
471 */
472 function is_json( $data ) {
473 return \is_array( \json_decode( $data, true ) ) ? true : false;
474 }
475
476 /**
477 * Check if a blog is public based on the `blog_public` option
478 *
479 * @return bollean True if public, false if not
480 */
481 function is_blog_public() {
482 return (bool) apply_filters( 'activitypub_is_blog_public', \get_option( 'blog_public', 1 ) );
483 }
484