PluginProbe
ActivityPub / 8.1.1
ActivityPub v8.1.1
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 8.1.1, at includes/functions.php

394 lines 10.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Functions file.
4 *
5 * General utility functions for the ActivityPub plugin.
6 *
7 * @package Activitypub
8 */
9
10 namespace Activitypub;
11
12 /**
13 * Get the ActivityPub ID for a WordPress object.
14 *
15 * Returns the canonical ActivityPub URI for a WP_Post or WP_Comment.
16 *
17 * @param \WP_Post|\WP_Comment $wp_object The WordPress post or comment.
18 *
19 * @return string|null The ActivityPub ID (a URL), or null if unsupported type.
20 */
21 function get_object_id( $wp_object ) {
22 if ( $wp_object instanceof \WP_Post ) {
23 return get_post_id( $wp_object->ID );
24 }
25
26 if ( $wp_object instanceof \WP_Comment ) {
27 return get_comment_id( $wp_object );
28 }
29
30 return null;
31 }
32
33 /**
34 * Convert a string from camelCase to snake_case.
35 *
36 * @param string $input The string to convert.
37 *
38 * @return string The converted string.
39 */
40 function camel_to_snake_case( $input ) {
41 return strtolower( preg_replace( '/(?<!^)[A-Z]/', '_$0', $input ) );
42 }
43
44 /**
45 * Convert a string from snake_case to camelCase.
46 *
47 * @param string $input The string to convert.
48 *
49 * @return string The converted string.
50 */
51 function snake_to_camel_case( $input ) {
52 return lcfirst( str_replace( '_', '', ucwords( $input, '_' ) ) );
53 }
54
55 /**
56 * Convert seconds to ISO 8601 duration format.
57 *
58 * @param int $seconds The duration in seconds.
59 *
60 * @return string The duration in ISO 8601 format (e.g., "PT1H23M45S").
61 */
62 function seconds_to_iso8601( $seconds ) {
63 $seconds = (int) $seconds;
64
65 if ( $seconds <= 0 ) {
66 return 'PT0S';
67 }
68
69 $hours = floor( $seconds / 3600 );
70 $minutes = floor( ( $seconds % 3600 ) / 60 );
71 $secs = $seconds % 60;
72
73 $duration = 'PT';
74
75 if ( $hours > 0 ) {
76 $duration .= $hours . 'H';
77 }
78
79 if ( $minutes > 0 ) {
80 $duration .= $minutes . 'M';
81 }
82
83 if ( $secs > 0 || ( 0 === $hours && 0 === $minutes ) ) {
84 $duration .= $secs . 'S';
85 }
86
87 return $duration;
88 }
89
90 /**
91 * Check if a site supports the block editor.
92 *
93 * @return boolean True if the site supports the block editor, false otherwise.
94 */
95 function site_supports_blocks() {
96 /**
97 * Allow plugins to disable block editor support,
98 * thus disabling blocks registered by the ActivityPub plugin.
99 *
100 * @param boolean $supports_blocks True if the site supports the block editor, false otherwise.
101 */
102 return apply_filters( 'activitypub_site_supports_blocks', true );
103 }
104
105 /**
106 * Check if data is valid JSON.
107 *
108 * @deprecated 7.1.0 Use {@see \json_decode}.
109 *
110 * @param string $data The data to check.
111 *
112 * @return boolean True if the data is JSON, false otherwise.
113 */
114 function is_json( $data ) {
115 \_deprecated_function( __FUNCTION__, '7.1.0', 'json_decode' );
116
117 return \is_array( \json_decode( $data, true ) );
118 }
119
120 /**
121 * Check whether a blog is public based on the `blog_public` option.
122 *
123 * @return bool True if public, false if not
124 */
125 function is_blog_public() {
126 /**
127 * Filter whether the blog is public.
128 *
129 * @param bool $public Whether the blog is public.
130 */
131 return (bool) apply_filters( 'activitypub_is_blog_public', \get_option( 'blog_public', 1 ) );
132 }
133
134 /**
135 * Get the masked WordPress version to only show the major and minor version.
136 *
137 * @return string The masked version.
138 */
139 function get_masked_wp_version() {
140 // Only show the major and minor version.
141 $version = get_bloginfo( 'version' );
142 // Strip the RC or beta part.
143 $version = preg_replace( '/-.*$/', '', $version );
144 $version = explode( '.', $version );
145 $version = array_slice( $version, 0, 2 );
146
147 return implode( '.', $version );
148 }
149
150 /**
151 * Check if a plugin is active, loading plugin.php if necessary.
152 *
153 * This is a wrapper around the core is_plugin_active() function that ensures
154 * the function is available by loading wp-admin/includes/plugin.php if needed.
155 * This is useful when checking plugin status outside of the admin context.
156 *
157 * @param string $plugin Plugin basename (e.g., 'plugin-folder/plugin-file.php').
158 *
159 * @return bool True if the plugin is active, false otherwise.
160 */
161 function is_plugin_active( $plugin ) {
162 // Include plugin.php if not already loaded (needed for core is_plugin_active).
163 if ( ! \function_exists( 'is_plugin_active' ) ) {
164 require_once ABSPATH . 'wp-admin/includes/plugin.php';
165 }
166
167 return \is_plugin_active( $plugin );
168 }
169
170 /**
171 * Returns the website hosts allowed to credit this blog.
172 *
173 * @return array|null The attribution domains or null if not found.
174 */
175 function get_attribution_domains() {
176 if ( '1' !== \get_option( 'activitypub_use_opengraph', '1' ) ) {
177 return null;
178 }
179
180 $domains = \get_option( 'activitypub_attribution_domains', home_host() );
181 $domains = explode( PHP_EOL, $domains );
182
183 if ( ! $domains ) {
184 $domains = null;
185 }
186
187 return $domains;
188 }
189
190 /**
191 * Change the display of large numbers on the site.
192 *
193 * @author Jeremy Herve
194 *
195 * @see https://wordpress.org/support/topic/abbreviate-numbers-with-k/
196 *
197 * @param string $formatted Converted number in string format.
198 * @param float $number The number to convert based on locale.
199 *
200 * @return string Converted number in string format.
201 */
202 function custom_large_numbers( $formatted, $number ) {
203 global $wp_locale;
204
205 $decimals = 0;
206 $decimal_point = '.';
207 $thousands_sep = ',';
208
209 if ( isset( $wp_locale ) ) {
210 $decimals = (int) $wp_locale->number_format['decimal_point'];
211 $decimal_point = $wp_locale->number_format['decimal_point'];
212 $thousands_sep = $wp_locale->number_format['thousands_sep'];
213 }
214
215 if ( $number < 1000 ) { // Any number less than a Thousand.
216 return \number_format( $number, $decimals, $decimal_point, $thousands_sep );
217 } elseif ( $number < 1000000 ) { // Any number less than a million.
218 return \number_format( $number / 1000, $decimals, $decimal_point, $thousands_sep ) . 'K';
219 } elseif ( $number < 1000000000 ) { // Any number less than a billion.
220 return \number_format( $number / 1000000, $decimals, $decimal_point, $thousands_sep ) . 'M';
221 } else { // At least a billion.
222 return \number_format( $number / 1000000000, $decimals, $decimal_point, $thousands_sep ) . 'B';
223 }
224 }
225
226 /**
227 * Escapes a Tag, to be used as a hashtag.
228 *
229 * @param string $input The string to escape.
230 *
231 * @return string The escaped hashtag.
232 */
233 function esc_hashtag( $input ) {
234 $hashtag = \wp_specialchars_decode( $input, ENT_QUOTES );
235 // Remove all characters that are not letters, numbers, or hyphens.
236 $hashtag = \preg_replace( '/[^\p{L}\p{Nd}-]+/u', '-', $hashtag );
237
238 // Capitalize every letter that is preceded by a hyphen.
239 $hashtag = preg_replace_callback(
240 '/-+(.)/',
241 static function ( $matches ) {
242 return strtoupper( $matches[1] );
243 },
244 $hashtag
245 );
246
247 // Add a hashtag to the beginning of the string.
248 $hashtag = ltrim( $hashtag, '#' );
249 $hashtag = trim( $hashtag, '-' );
250 $hashtag = '#' . $hashtag;
251
252 /**
253 * Allow defining your own custom hashtag generation rules.
254 *
255 * @param string $hashtag The hashtag to be returned.
256 * @param string $input The original string.
257 */
258 $hashtag = apply_filters( 'activitypub_esc_hashtag', $hashtag, $input );
259
260 return esc_html( $hashtag );
261 }
262
263 /**
264 * Replace content with links, mentions or hashtags by Regex callback and not affect protected tags.
265 *
266 * @param string $content The content that should be changed.
267 * @param string $regex The regex to use.
268 * @param callable $regex_callback Callback for replacement logic.
269 *
270 * @return string The content with links, mentions, hashtags, etc.
271 */
272 function enrich_content_data( $content, $regex, $regex_callback ) {
273 // Small protection against execution timeouts: limit to 1 MB.
274 if ( mb_strlen( $content ) > MB_IN_BYTES ) {
275 return $content;
276 }
277 $tag_stack = array();
278 $protected_tags = array(
279 'pre',
280 'code',
281 'textarea',
282 'style',
283 'a',
284 );
285 $content_with_links = '';
286 $in_protected_tag = false;
287 foreach ( wp_html_split( $content ) as $chunk ) {
288 if ( preg_match( '#^<!--[\s\S]*-->$#i', $chunk, $m ) ) {
289 $content_with_links .= $chunk;
290 continue;
291 }
292
293 if ( preg_match( '#^<(/)?([a-z-]+)\b[^>]*>$#i', $chunk, $m ) ) {
294 $tag = strtolower( $m[2] );
295 if ( '/' === $m[1] ) {
296 // Closing tag.
297 $i = array_search( $tag, $tag_stack, true );
298 // We can only remove the tag from the stack if it is in the stack.
299 if ( false !== $i ) {
300 $tag_stack = array_slice( $tag_stack, 0, $i );
301 }
302 } else {
303 // Opening tag, add it to the stack.
304 $tag_stack[] = $tag;
305 }
306
307 // If we're in a protected tag, the tag_stack contains at least one protected tag string.
308 // The protected tag state can only change when we encounter a start or end tag.
309 $in_protected_tag = array_intersect( $tag_stack, $protected_tags );
310
311 // Never inspect tags.
312 $content_with_links .= $chunk;
313 continue;
314 }
315
316 if ( $in_protected_tag ) {
317 // Don't inspect a chunk inside an inspected tag.
318 $content_with_links .= $chunk;
319 continue;
320 }
321
322 // Only reachable when there is no protected tag in the stack.
323 $content_with_links .= \preg_replace_callback( $regex, $regex_callback, $chunk );
324 }
325
326 return $content_with_links;
327 }
328
329 /**
330 * Get an ActivityPub embed HTML for a URL.
331 *
332 * @param string $url The URL to get the embed for.
333 * @param boolean $inline_css Whether to inline CSS. Default true.
334 *
335 * @return string|false The embed HTML or false if not found.
336 */
337 function get_embed_html( $url, $inline_css = true ) {
338 return Embed::get_html( $url, $inline_css );
339 }
340
341 /**
342 * Get the client IP address for rate-limiting purposes.
343 *
344 * Checks common proxy headers before falling back to REMOTE_ADDR,
345 * similar to Jetpack's approach. The result can be overridden via
346 * the `activitypub_client_ip` filter.
347 *
348 * @since 8.1.0
349 *
350 * @return string The client IP address.
351 */
352 function get_client_ip() {
353 // phpcs:disable WordPressVIPMinimum.Variables.ServerVariables.UserControlledHeaders
354 $ip = 'unknown';
355
356 $headers = array(
357 'HTTP_CF_CONNECTING_IP', // Cloudflare.
358 'HTTP_CLIENT_IP',
359 'HTTP_X_FORWARDED_FOR',
360 'HTTP_X_FORWARDED',
361 'HTTP_X_CLUSTER_CLIENT_IP',
362 'HTTP_FORWARDED_FOR',
363 'HTTP_FORWARDED',
364 );
365
366 foreach ( $headers as $header ) {
367 if ( ! empty( $_SERVER[ $header ] ) ) {
368 // Some headers (e.g. X-Forwarded-For) may contain a comma-separated list; use the first IP.
369 $ip_list = \sanitize_text_field( \wp_unslash( $_SERVER[ $header ] ) );
370 $ip = \trim( \explode( ',', $ip_list )[0] );
371
372 if ( \filter_var( $ip, FILTER_VALIDATE_IP ) ) {
373 break;
374 }
375
376 $ip = 'unknown';
377 }
378 }
379
380 if ( 'unknown' === $ip && isset( $_SERVER['REMOTE_ADDR'] ) ) {
381 $ip = \sanitize_text_field( \wp_unslash( $_SERVER['REMOTE_ADDR'] ) );
382 }
383 // phpcs:enable WordPressVIPMinimum.Variables.ServerVariables.UserControlledHeaders
384
385 /**
386 * Filter the client IP address used for rate limiting.
387 *
388 * @since 8.1.0
389 *
390 * @param string $ip The detected client IP address.
391 */
392 return \apply_filters( 'activitypub_client_ip', $ip );
393 }
394