PluginProbe
ActivityPub / 1.2.0
ActivityPub v1.2.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 / functions.php

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

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