PluginProbe
ActivityPub / 1.0.9
ActivityPub v1.0.9
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.9, at includes/functions.php

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