PluginProbe
ActivityPub / 5.7.0
ActivityPub v5.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
← All changes | includes/class-sanitize.php +9 -458 9.2.05.7.0 View file →
@@ -6,9 +6,8 @@
6 6 */
7 7
8 8 namespace Activitypub;
9 9
10 -use Activitypub\Collection\Remote_Actors;
11 10 use Activitypub\Model\Blog;
12 11
13 12 /**
14 13 * Sanitization class.
@@ -13,53 +12,9 @@
13 12 /**
14 13 * Sanitization class.
15 14 */
16 15 class Sanitize {
17 -
18 16 /**
19 - * Elements to strip including their inner content.
20 - *
21 - * WordPress's wp_kses removes disallowed tags but preserves their inner text.
22 - * These elements contain content that is meaningless or harmful
23 - * without the surrounding tag (scripts, styles, interactive UI,
24 - * embedded objects), so we remove them entirely before wp_kses runs.
25 - *
26 - * @var array<string>
27 - */
28 - const STRIP_ELEMENTS = array(
29 - 'script',
30 - 'style',
31 - 'button',
32 - 'nav',
33 - 'form',
34 - 'textarea',
35 - 'select',
36 - 'input',
37 - 'fieldset',
38 - 'iframe',
39 - 'embed',
40 - 'object',
41 - );
42 -
43 - /**
44 - * MathML global attributes allowed per the W3C MathML safe list.
45 - *
46 - * @see https://w3c.github.io/mathml-docs/mathml-safe-list
47 - *
48 - * @var array<string, true>
49 - */
50 - const MATHML_GLOBAL_ATTRS = array(
51 - 'dir' => true,
52 - 'displaystyle' => true,
53 - 'mathbackground' => true,
54 - 'mathcolor' => true,
55 - 'mathsize' => true,
56 - 'scriptlevel' => true,
57 - 'intent' => true,
58 - 'arg' => true,
59 - );
60 -
61 - /**
62 17 * Sanitize a list of URLs.
63 18 *
64 19 * @param string|array $value The value to sanitize.
65 20 * @return array The sanitized list of URLs.
@@ -65,9 +20,9 @@
65 20 * @return array The sanitized list of URLs.
66 21 */
67 22 public static function url_list( $value ) {
68 23 if ( ! \is_array( $value ) ) {
69 - $value = \explode( PHP_EOL, (string) $value );
24 + $value = \explode( PHP_EOL, $value );
70 25 }
71 26
72 27 $value = \array_filter( $value );
73 28 $value = \array_map( 'trim', $value );
@@ -77,52 +32,8 @@
77 32 return \array_values( $value );
78 33 }
79 34
80 35 /**
81 - * Sanitize and normalize a list of account identifiers to ActivityPub IDs.
82 - *
83 - * This function processes various identifier formats, such as URLs and
84 - * webfinger identifiers, and normalizes them into a consistent format.
85 - *
86 - * @param string|array $value The value to sanitize.
87 - *
88 - * @return array The sanitized and normalized list of account identifiers.
89 - */
90 - public static function identifier_list( $value ) {
91 - if ( ! \is_array( $value ) ) {
92 - $value = \explode( PHP_EOL, (string) $value );
93 - }
94 -
95 - $value = \array_filter( $value );
96 - $uris = array();
97 -
98 - foreach ( $value as $uri ) {
99 - $uri = \trim( $uri );
100 - $uri = \ltrim( $uri, '@' );
101 -
102 - if ( \is_email( $uri ) ) {
103 - $_uri = Webfinger::resolve( $uri );
104 - if ( \is_wp_error( $_uri ) ) {
105 - $uris[] = $uri;
106 - continue;
107 - }
108 -
109 - $uri = $_uri;
110 - }
111 -
112 - $uri = \sanitize_url( $uri );
113 - $actor = Remote_Actors::fetch_by_uri( $uri );
114 - if ( \is_wp_error( $actor ) ) {
115 - $uris[] = $uri;
116 - } else {
117 - $uris[] = \sanitize_url( $actor->guid );
118 - }
119 - }
120 -
121 - return \array_values( \array_unique( $uris ) );
122 - }
123 -
124 - /**
125 36 * Sanitize a list of hosts.
126 37 *
127 38 * @param string $value The value to sanitize.
128 39 * @return string The sanitized list of hosts.
@@ -127,11 +38,11 @@
127 38 * @param string $value The value to sanitize.
128 39 * @return string The sanitized list of hosts.
129 40 */
130 41 public static function host_list( $value ) {
131 - $value = \explode( PHP_EOL, (string) $value );
42 + $value = \explode( PHP_EOL, $value );
132 43 $value = \array_map(
133 - static function ( $host ) {
44 + function ( $host ) {
134 45 $host = \trim( $host );
135 46 $host = \strtolower( $host );
136 47 $host = \set_url_scheme( $host );
137 48 $host = \sanitize_url( $host, array( 'http', 'https' ) );
@@ -156,27 +67,12 @@
156 67 * @return string The sanitized blog identifier.
157 68 */
158 69 public static function blog_identifier( $value ) {
159 70 // Hack to allow dots in the username.
160 - $parts = \explode( '.', (string) $value );
71 + $parts = \explode( '.', $value );
161 72 $sanitized = \array_map( 'sanitize_title', $parts );
162 73 $sanitized = \implode( '.', $sanitized );
163 74
164 - if ( empty( $sanitized ) ) {
165 - return Blog::get_default_username();
166 - }
167 -
168 - // The 'application' identifier is reserved for the Application actor.
169 - if ( Application::USERNAME === $sanitized ) {
170 - \add_settings_error(
171 - 'activitypub_blog_identifier',
172 - 'activitypub_blog_identifier',
173 - \esc_html__( 'This name is reserved and cannot be used for the blog profile ID.', 'activitypub' )
174 - );
175 -
176 - return Blog::get_default_username();
177 - }
178 -
179 75 // Check for login or nicename.
180 76 $user = new \WP_User_Query(
181 77 array(
182 78 'search' => $sanitized,
@@ -207,365 +103,20 @@
207 103 *
208 104 * @return string The sanitized value.
209 105 */
210 106 public static function constant_value( $value ) {
211 - if ( \is_bool( $value ) ) {
107 + if ( is_bool( $value ) ) {
212 108 return $value ? 'true' : 'false';
213 109 }
214 110
215 - if ( \is_string( $value ) ) {
216 - return \esc_attr( $value );
111 + if ( is_string( $value ) ) {
112 + return esc_attr( $value );
217 113 }
218 114
219 - if ( \is_array( $value ) ) {
115 + if ( is_array( $value ) ) {
220 116 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r
221 - return \print_r( $value, true );
117 + return print_r( $value, true );
222 118 }
223 119
224 120 return $value;
225 - }
226 -
227 - /**
228 - * Sanitize a webfinger identifier.
229 - *
230 - * @param string $value The value to sanitize.
231 - *
232 - * @return string The sanitized webfinger identifier.
233 - */
234 - public static function webfinger( $value ) {
235 - $value = \str_replace( 'acct:', '', $value );
236 - $value = \trim( $value, '@' );
237 -
238 - return $value;
239 - }
240 -
241 - /**
242 - * Sanitize content for ActivityPub.
243 - *
244 - * @param string $content The content to convert.
245 - *
246 - * @return string The converted content.
247 - */
248 - public static function content( $content ) {
249 - // Only make URLs clickable if no anchor tags exist, to avoid corrupting existing links.
250 - if ( false === \strpos( $content, '<a ' ) ) {
251 - $content = \make_clickable( $content );
252 - }
253 -
254 - $content = \wpautop( $content );
255 - $content = \wp_kses_post( $content );
256 -
257 - return $content;
258 - }
259 -
260 - /**
261 - * Strip whitespace between HTML tags.
262 - *
263 - * Removes newlines, carriage returns, and tabs that appear between HTML tags,
264 - * preserving whitespace within text content and preformatted elements.
265 - *
266 - * @param string $content The content to process.
267 - *
268 - * @return string The content with whitespace between tags removed.
269 - */
270 - public static function strip_whitespace( $content ) {
271 - return \trim( \preg_replace( '/>[\n\r\t]+</', '><', $content ) );
272 - }
273 -
274 - /**
275 - * Sanitize a redirect URI, preserving custom protocol schemes.
276 - *
277 - * WordPress's sanitize_url() and esc_url_raw() strip unknown protocols.
278 - * This method extracts the scheme and passes it as allowed so custom
279 - * URI schemes for native apps (RFC 8252 Section 7.1) are preserved.
280 - *
281 - * @since 8.1.0
282 - *
283 - * @param string $uri The redirect URI to sanitize.
284 - * @return string The sanitized URI.
285 - */
286 - public static function redirect_uri( $uri ) {
287 - /*
288 - * Extract scheme manually because wp_parse_url() returns false
289 - * for URIs like "myapp://" (scheme + empty authority, no path).
290 - */
291 - if ( ! \preg_match( '/^([a-zA-Z][a-zA-Z0-9+.\-]*):/', $uri, $matches ) ) {
292 - return '';
293 - }
294 -
295 - $scheme = \strtolower( $matches[1] );
296 -
297 - // For standard schemes, use default sanitization.
298 - if ( \in_array( $scheme, array( 'http', 'https' ), true ) ) {
299 - return \sanitize_url( $uri );
300 - }
301 -
302 - // For custom schemes, include the scheme in allowed protocols.
303 - return \sanitize_url( $uri, \array_merge( \wp_allowed_protocols(), array( $scheme ) ) );
304 - }
305 -
306 - /**
307 - * Clean HTML for ActivityPub federation.
308 - *
309 - * Uses a positive allowlist based on FEP-b2b8 (Long-form Text) for the
310 - * `content` property, extended with common WordPress content elements.
311 - * Interactive, navigational, and scripting elements are stripped entirely.
312 - *
313 - * @see https://codeberg.org/fediverse/fep/src/branch/main/fep/b2b8/fep-b2b8.md
314 - * @see https://github.com/Automattic/wordpress-activitypub/issues/2619
315 - *
316 - * @param string $content The HTML content to clean.
317 - *
318 - * @return string The cleaned HTML content.
319 - */
320 - public static function clean_html( $content ) {
321 - if ( empty( $content ) ) {
322 - return $content;
323 - }
324 -
325 - /*
326 - * Strip elements whose inner content is noise (scripts, styles, interactive UI, embeds).
327 - * This runs before wp_kses because wp_kses strips tags but keeps inner text,
328 - * and content inside <script>, <style>, <nav>, etc. is meaningless on its own.
329 - */
330 - $strip_pattern = \implode( '|', self::STRIP_ELEMENTS );
331 - $content = \preg_replace( '@<(' . $strip_pattern . ')[^>]*?>.*?</\\1>@si', '', $content );
332 - // Also catch self-closing variants (e.g. <input />, <embed />).
333 - $content = \preg_replace( '@<(' . $strip_pattern . ')[^>]*?/?>@si', '', $content );
334 -
335 - /**
336 - * Fires the deprecated attribute removal filter.
337 - *
338 - * @deprecated 8.1.0 Use the {@see 'activitypub_allowed_html'} filter instead.
339 - */
340 - if ( \has_filter( 'activitypub_remove_html_attributes' ) ) {
341 - \_deprecated_hook( 'activitypub_remove_html_attributes', '8.1.0', 'activitypub_allowed_html' );
342 - }
343 -
344 - /**
345 - * Filters the allowed HTML for ActivityPub content.
346 - *
347 - * The default allowlist is based on FEP-b2b8 (Long-form Text),
348 - * extended with common WordPress content elements like figures,
349 - * tables, definition lists, and horizontal rules.
350 - *
351 - * @param array $allowed_html The allowed HTML structure for wp_kses.
352 - */
353 - $allowed_html = \apply_filters( 'activitypub_allowed_html', self::get_allowed_html() );
354 -
355 - return \wp_kses( $content, $allowed_html, \wp_allowed_protocols() );
356 - }
357 -
358 - /**
359 - * Returns the allowed HTML elements and attributes for ActivityPub content.
360 - *
361 - * Based on the FEP-b2b8 allowlist for the `content` property, extended
362 - * with additional WordPress content elements (figures, tables, definition
363 - * lists, horizontal rules, etc.).
364 - *
365 - * @see https://codeberg.org/fediverse/fep/src/branch/main/fep/b2b8/fep-b2b8.md
366 - *
367 - * @return array The allowed HTML structure for wp_kses.
368 - */
369 - public static function get_allowed_html() {
370 - // FEP-b2b8 core allowlist.
371 - $allowed_html = array(
372 - 'p' => array(),
373 - 'span' => array(
374 - 'class' => true,
375 - ),
376 - 'br' => array(),
377 - 'a' => array(
378 - 'href' => true,
379 - 'rel' => true,
380 - 'class' => true,
381 - 'title' => true,
382 - ),
383 - 'h1' => array(),
384 - 'h2' => array(),
385 - 'h3' => array(),
386 - 'h4' => array(),
387 - 'h5' => array(),
388 - 'h6' => array(),
389 - 'del' => array(),
390 - 'pre' => array(),
391 - 'code' => array(),
392 - 'em' => array(),
393 - 'strong' => array(),
394 - 'b' => array(),
395 - 'i' => array(),
396 - 'u' => array(),
397 - 'ul' => array(),
398 - 'ol' => array(
399 - 'start' => true,
400 - 'reversed' => true,
401 - ),
402 - 'li' => array(
403 - 'value' => true,
404 - ),
405 - 'blockquote' => array(
406 - 'cite' => true,
407 - ),
408 - 'img' => array(
409 - 'src' => true,
410 - 'alt' => true,
411 - 'title' => true,
412 - 'width' => true,
413 - 'height' => true,
414 - ),
415 - 'video' => array(
416 - 'src' => true,
417 - 'controls' => true,
418 - 'loop' => true,
419 - 'poster' => true,
420 - 'width' => true,
421 - 'height' => true,
422 - ),
423 - 'audio' => array(
424 - 'src' => true,
425 - 'controls' => true,
426 - 'loop' => true,
427 - ),
428 - 'source' => array(
429 - 'src' => true,
430 - 'type' => true,
431 - ),
432 - 'ruby' => array(),
433 - 'rt' => array(),
434 - 'rp' => array(),
435 - );
436 -
437 - // WordPress content extensions beyond FEP-b2b8.
438 - $allowed_html['figure'] = array();
439 - $allowed_html['figcaption'] = array();
440 - $allowed_html['hr'] = array();
441 - $allowed_html['div'] = array();
442 - $allowed_html['table'] = array();
443 - $allowed_html['thead'] = array();
444 - $allowed_html['tbody'] = array();
445 - $allowed_html['tfoot'] = array();
446 - $allowed_html['tr'] = array();
447 - $allowed_html['th'] = array(
448 - 'colspan' => true,
449 - 'rowspan' => true,
450 - );
451 - $allowed_html['td'] = array(
452 - 'colspan' => true,
453 - 'rowspan' => true,
454 - );
455 - $allowed_html['caption'] = array();
456 - $allowed_html['dl'] = array();
457 - $allowed_html['dt'] = array();
458 - $allowed_html['dd'] = array();
459 - $allowed_html['s'] = array();
460 - $allowed_html['sub'] = array();
461 - $allowed_html['sup'] = array();
462 - $allowed_html['abbr'] = array(
463 - 'title' => true,
464 - );
465 - $allowed_html['mark'] = array();
466 - $allowed_html['ins'] = array();
467 - $allowed_html['cite'] = array();
468 - $allowed_html['time'] = array(
469 - 'datetime' => true,
470 - );
471 - $allowed_html['track'] = array(
472 - 'src' => true,
473 - 'kind' => true,
474 - 'label' => true,
475 - 'srclang' => true,
476 - );
477 -
478 - // MathML safe elements per W3C MathML safe list.
479 - $allowed_html['math'] = \array_merge(
480 - self::MATHML_GLOBAL_ATTRS,
481 - array(
482 - 'display' => true,
483 - )
484 - );
485 - $allowed_html['merror'] = self::MATHML_GLOBAL_ATTRS;
486 - $allowed_html['mfrac'] = \array_merge(
487 - self::MATHML_GLOBAL_ATTRS,
488 - array(
489 - 'linethickness' => true,
490 - )
491 - );
492 - $allowed_html['mi'] = self::MATHML_GLOBAL_ATTRS;
493 - $allowed_html['mmultiscripts'] = self::MATHML_GLOBAL_ATTRS;
494 - $allowed_html['mn'] = self::MATHML_GLOBAL_ATTRS;
495 - $allowed_html['mo'] = \array_merge(
496 - self::MATHML_GLOBAL_ATTRS,
497 - array(
498 - 'form' => true,
499 - 'fence' => true,
500 - 'separator' => true,
501 - 'lspace' => true,
502 - 'rspace' => true,
503 - 'stretchy' => true,
504 - 'symmetric' => true,
505 - 'maxsize' => true,
506 - 'minsize' => true,
507 - 'largeop' => true,
508 - 'movablelimits' => true,
509 - )
510 - );
511 - $allowed_html['mover'] = self::MATHML_GLOBAL_ATTRS;
512 - $allowed_html['mpadded'] = \array_merge(
513 - self::MATHML_GLOBAL_ATTRS,
514 - array(
515 - 'width' => true,
516 - 'height' => true,
517 - 'depth' => true,
518 - 'lspace' => true,
519 - 'voffset' => true,
520 - )
521 - );
522 - $allowed_html['mprescripts'] = self::MATHML_GLOBAL_ATTRS;
523 - $allowed_html['mroot'] = self::MATHML_GLOBAL_ATTRS;
524 - $allowed_html['mrow'] = self::MATHML_GLOBAL_ATTRS;
525 - $allowed_html['ms'] = self::MATHML_GLOBAL_ATTRS;
526 - $allowed_html['mspace'] = \array_merge(
527 - self::MATHML_GLOBAL_ATTRS,
528 - array(
529 - 'width' => true,
530 - 'height' => true,
531 - 'depth' => true,
532 - )
533 - );
534 - $allowed_html['msqrt'] = self::MATHML_GLOBAL_ATTRS;
535 - $allowed_html['mstyle'] = self::MATHML_GLOBAL_ATTRS;
536 - $allowed_html['msub'] = self::MATHML_GLOBAL_ATTRS;
537 - $allowed_html['msubsup'] = self::MATHML_GLOBAL_ATTRS;
538 - $allowed_html['msup'] = self::MATHML_GLOBAL_ATTRS;
539 - $allowed_html['mtable'] = self::MATHML_GLOBAL_ATTRS;
540 - $allowed_html['mtd'] = \array_merge(
541 - self::MATHML_GLOBAL_ATTRS,
542 - array(
543 - 'columnspan' => true,
544 - 'rowspan' => true,
545 - )
546 - );
547 - $allowed_html['mtext'] = self::MATHML_GLOBAL_ATTRS;
548 - $allowed_html['mtr'] = self::MATHML_GLOBAL_ATTRS;
549 - $allowed_html['munder'] = self::MATHML_GLOBAL_ATTRS;
550 - $allowed_html['munderover'] = \array_merge(
551 - self::MATHML_GLOBAL_ATTRS,
552 - array(
553 - 'accent' => true,
554 - 'accentunder' => true,
555 - )
556 - );
557 - $allowed_html['semantics'] = \array_merge(
558 - self::MATHML_GLOBAL_ATTRS,
559 - array(
560 - 'encoding' => true,
561 - )
562 - );
563 - $allowed_html['annotation'] = \array_merge(
564 - self::MATHML_GLOBAL_ATTRS,
565 - array(
566 - 'encoding' => true,
567 - )
568 - );
569 - return $allowed_html;
570 121 }
571 122 }