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 +4 -442 8.2.15.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,16 +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 75 // Check for login or nicename.
169 76 $user = new \WP_User_Query(
170 77 array(
171 78 'search' => $sanitized,
@@ -210,351 +117,6 @@
210 117 return print_r( $value, true );
211 118 }
212 119
213 120 return $value;
214 - }
215 -
216 - /**
217 - * Sanitize a webfinger identifier.
218 - *
219 - * @param string $value The value to sanitize.
220 - *
221 - * @return string The sanitized webfinger identifier.
222 - */
223 - public static function webfinger( $value ) {
224 - $value = \str_replace( 'acct:', '', $value );
225 - $value = \trim( $value, '@' );
226 -
227 - return $value;
228 - }
229 -
230 - /**
231 - * Sanitize content for ActivityPub.
232 - *
233 - * @param string $content The content to convert.
234 - *
235 - * @return string The converted content.
236 - */
237 - public static function content( $content ) {
238 - // Only make URLs clickable if no anchor tags exist, to avoid corrupting existing links.
239 - if ( false === \strpos( $content, '<a ' ) ) {
240 - $content = \make_clickable( $content );
241 - }
242 -
243 - $content = \wpautop( $content );
244 - $content = \wp_kses_post( $content );
245 -
246 - return $content;
247 - }
248 -
249 - /**
250 - * Strip whitespace between HTML tags.
251 - *
252 - * Removes newlines, carriage returns, and tabs that appear between HTML tags,
253 - * preserving whitespace within text content and preformatted elements.
254 - *
255 - * @param string $content The content to process.
256 - *
257 - * @return string The content with whitespace between tags removed.
258 - */
259 - public static function strip_whitespace( $content ) {
260 - return \trim( \preg_replace( '/>[\n\r\t]+</', '><', $content ) );
261 - }
262 -
263 - /**
264 - * Sanitize a redirect URI, preserving custom protocol schemes.
265 - *
266 - * WordPress's sanitize_url() and esc_url_raw() strip unknown protocols.
267 - * This method extracts the scheme and passes it as allowed so custom
268 - * URI schemes for native apps (RFC 8252 Section 7.1) are preserved.
269 - *
270 - * @since 8.1.0
271 - *
272 - * @param string $uri The redirect URI to sanitize.
273 - * @return string The sanitized URI.
274 - */
275 - public static function redirect_uri( $uri ) {
276 - /*
277 - * Extract scheme manually because wp_parse_url() returns false
278 - * for URIs like "myapp://" (scheme + empty authority, no path).
279 - */
280 - if ( ! preg_match( '/^([a-zA-Z][a-zA-Z0-9+.\-]*):/', $uri, $matches ) ) {
281 - return '';
282 - }
283 -
284 - $scheme = \strtolower( $matches[1] );
285 -
286 - // For standard schemes, use default sanitization.
287 - if ( in_array( $scheme, array( 'http', 'https' ), true ) ) {
288 - return \sanitize_url( $uri );
289 - }
290 -
291 - // For custom schemes, include the scheme in allowed protocols.
292 - return \sanitize_url( $uri, array_merge( \wp_allowed_protocols(), array( $scheme ) ) );
293 - }
294 -
295 - /**
296 - * Clean HTML for ActivityPub federation.
297 - *
298 - * Uses a positive allowlist based on FEP-b2b8 (Long-form Text) for the
299 - * `content` property, extended with common WordPress content elements.
300 - * Interactive, navigational, and scripting elements are stripped entirely.
301 - *
302 - * @see https://codeberg.org/fediverse/fep/src/branch/main/fep/b2b8/fep-b2b8.md
303 - * @see https://github.com/Automattic/wordpress-activitypub/issues/2619
304 - *
305 - * @param string $content The HTML content to clean.
306 - *
307 - * @return string The cleaned HTML content.
308 - */
309 - public static function clean_html( $content ) {
310 - if ( empty( $content ) ) {
311 - return $content;
312 - }
313 -
314 - /*
315 - * Strip elements whose inner content is noise (scripts, styles, interactive UI, embeds).
316 - * This runs before wp_kses because wp_kses strips tags but keeps inner text,
317 - * and content inside <script>, <style>, <nav>, etc. is meaningless on its own.
318 - */
319 - $strip_pattern = \implode( '|', self::STRIP_ELEMENTS );
320 - $content = \preg_replace( '@<(' . $strip_pattern . ')[^>]*?>.*?</\\1>@si', '', $content );
321 - // Also catch self-closing variants (e.g. <input />, <embed />).
322 - $content = \preg_replace( '@<(' . $strip_pattern . ')[^>]*?/?>@si', '', $content );
323 -
324 - /**
325 - * Fires the deprecated attribute removal filter.
326 - *
327 - * @deprecated 8.1.0 Use the {@see 'activitypub_allowed_html'} filter instead.
328 - */
329 - if ( \has_filter( 'activitypub_remove_html_attributes' ) ) {
330 - \_deprecated_hook( 'activitypub_remove_html_attributes', '8.1.0', 'activitypub_allowed_html' );
331 - }
332 -
333 - /**
334 - * Filters the allowed HTML for ActivityPub content.
335 - *
336 - * The default allowlist is based on FEP-b2b8 (Long-form Text),
337 - * extended with common WordPress content elements like figures,
338 - * tables, definition lists, and horizontal rules.
339 - *
340 - * @param array $allowed_html The allowed HTML structure for wp_kses.
341 - */
342 - $allowed_html = \apply_filters( 'activitypub_allowed_html', self::get_allowed_html() );
343 -
344 - return \wp_kses( $content, $allowed_html, \wp_allowed_protocols() );
345 - }
346 -
347 - /**
348 - * Returns the allowed HTML elements and attributes for ActivityPub content.
349 - *
350 - * Based on the FEP-b2b8 allowlist for the `content` property, extended
351 - * with additional WordPress content elements (figures, tables, definition
352 - * lists, horizontal rules, etc.).
353 - *
354 - * @see https://codeberg.org/fediverse/fep/src/branch/main/fep/b2b8/fep-b2b8.md
355 - *
356 - * @return array The allowed HTML structure for wp_kses.
357 - */
358 - public static function get_allowed_html() {
359 - // FEP-b2b8 core allowlist.
360 - $allowed_html = array(
361 - 'p' => array(),
362 - 'span' => array(
363 - 'class' => true,
364 - ),
365 - 'br' => array(),
366 - 'a' => array(
367 - 'href' => true,
368 - 'rel' => true,
369 - 'class' => true,
370 - 'title' => true,
371 - ),
372 - 'h1' => array(),
373 - 'h2' => array(),
374 - 'h3' => array(),
375 - 'h4' => array(),
376 - 'h5' => array(),
377 - 'h6' => array(),
378 - 'del' => array(),
379 - 'pre' => array(),
380 - 'code' => array(),
381 - 'em' => array(),
382 - 'strong' => array(),
383 - 'b' => array(),
384 - 'i' => array(),
385 - 'u' => array(),
386 - 'ul' => array(),
387 - 'ol' => array(
388 - 'start' => true,
389 - 'reversed' => true,
390 - ),
391 - 'li' => array(
392 - 'value' => true,
393 - ),
394 - 'blockquote' => array(
395 - 'cite' => true,
396 - ),
397 - 'img' => array(
398 - 'src' => true,
399 - 'alt' => true,
400 - 'title' => true,
401 - 'width' => true,
402 - 'height' => true,
403 - ),
404 - 'video' => array(
405 - 'src' => true,
406 - 'controls' => true,
407 - 'loop' => true,
408 - 'poster' => true,
409 - 'width' => true,
410 - 'height' => true,
411 - ),
412 - 'audio' => array(
413 - 'src' => true,
414 - 'controls' => true,
415 - 'loop' => true,
416 - ),
417 - 'source' => array(
418 - 'src' => true,
419 - 'type' => true,
420 - ),
421 - 'ruby' => array(),
422 - 'rt' => array(),
423 - 'rp' => array(),
424 - );
425 -
426 - // WordPress content extensions beyond FEP-b2b8.
427 - $allowed_html['figure'] = array();
428 - $allowed_html['figcaption'] = array();
429 - $allowed_html['hr'] = array();
430 - $allowed_html['div'] = array();
431 - $allowed_html['table'] = array();
432 - $allowed_html['thead'] = array();
433 - $allowed_html['tbody'] = array();
434 - $allowed_html['tfoot'] = array();
435 - $allowed_html['tr'] = array();
436 - $allowed_html['th'] = array(
437 - 'colspan' => true,
438 - 'rowspan' => true,
439 - );
440 - $allowed_html['td'] = array(
441 - 'colspan' => true,
442 - 'rowspan' => true,
443 - );
444 - $allowed_html['caption'] = array();
445 - $allowed_html['dl'] = array();
446 - $allowed_html['dt'] = array();
447 - $allowed_html['dd'] = array();
448 - $allowed_html['s'] = array();
449 - $allowed_html['sub'] = array();
450 - $allowed_html['sup'] = array();
451 - $allowed_html['abbr'] = array(
452 - 'title' => true,
453 - );
454 - $allowed_html['mark'] = array();
455 - $allowed_html['ins'] = array();
456 - $allowed_html['cite'] = array();
457 - $allowed_html['time'] = array(
458 - 'datetime' => true,
459 - );
460 - $allowed_html['track'] = array(
461 - 'src' => true,
462 - 'kind' => true,
463 - 'label' => true,
464 - 'srclang' => true,
465 - );
466 -
467 - // MathML safe elements per W3C MathML safe list.
468 - $allowed_html['math'] = \array_merge(
469 - self::MATHML_GLOBAL_ATTRS,
470 - array(
471 - 'display' => true,
472 - )
473 - );
474 - $allowed_html['merror'] = self::MATHML_GLOBAL_ATTRS;
475 - $allowed_html['mfrac'] = \array_merge(
476 - self::MATHML_GLOBAL_ATTRS,
477 - array(
478 - 'linethickness' => true,
479 - )
480 - );
481 - $allowed_html['mi'] = self::MATHML_GLOBAL_ATTRS;
482 - $allowed_html['mmultiscripts'] = self::MATHML_GLOBAL_ATTRS;
483 - $allowed_html['mn'] = self::MATHML_GLOBAL_ATTRS;
484 - $allowed_html['mo'] = \array_merge(
485 - self::MATHML_GLOBAL_ATTRS,
486 - array(
487 - 'form' => true,
488 - 'fence' => true,
489 - 'separator' => true,
490 - 'lspace' => true,
491 - 'rspace' => true,
492 - 'stretchy' => true,
493 - 'symmetric' => true,
494 - 'maxsize' => true,
495 - 'minsize' => true,
496 - 'largeop' => true,
497 - 'movablelimits' => true,
498 - )
499 - );
500 - $allowed_html['mover'] = self::MATHML_GLOBAL_ATTRS;
501 - $allowed_html['mpadded'] = \array_merge(
502 - self::MATHML_GLOBAL_ATTRS,
503 - array(
504 - 'width' => true,
505 - 'height' => true,
506 - 'depth' => true,
507 - 'lspace' => true,
508 - 'voffset' => true,
509 - )
510 - );
511 - $allowed_html['mprescripts'] = self::MATHML_GLOBAL_ATTRS;
512 - $allowed_html['mroot'] = self::MATHML_GLOBAL_ATTRS;
513 - $allowed_html['mrow'] = self::MATHML_GLOBAL_ATTRS;
514 - $allowed_html['ms'] = self::MATHML_GLOBAL_ATTRS;
515 - $allowed_html['mspace'] = \array_merge(
516 - self::MATHML_GLOBAL_ATTRS,
517 - array(
518 - 'width' => true,
519 - 'height' => true,
520 - 'depth' => true,
521 - )
522 - );
523 - $allowed_html['msqrt'] = self::MATHML_GLOBAL_ATTRS;
524 - $allowed_html['mstyle'] = self::MATHML_GLOBAL_ATTRS;
525 - $allowed_html['msub'] = self::MATHML_GLOBAL_ATTRS;
526 - $allowed_html['msubsup'] = self::MATHML_GLOBAL_ATTRS;
527 - $allowed_html['msup'] = self::MATHML_GLOBAL_ATTRS;
528 - $allowed_html['mtable'] = self::MATHML_GLOBAL_ATTRS;
529 - $allowed_html['mtd'] = \array_merge(
530 - self::MATHML_GLOBAL_ATTRS,
531 - array(
532 - 'columnspan' => true,
533 - 'rowspan' => true,
534 - )
535 - );
536 - $allowed_html['mtext'] = self::MATHML_GLOBAL_ATTRS;
537 - $allowed_html['mtr'] = self::MATHML_GLOBAL_ATTRS;
538 - $allowed_html['munder'] = self::MATHML_GLOBAL_ATTRS;
539 - $allowed_html['munderover'] = \array_merge(
540 - self::MATHML_GLOBAL_ATTRS,
541 - array(
542 - 'accent' => true,
543 - 'accentunder' => true,
544 - )
545 - );
546 - $allowed_html['semantics'] = \array_merge(
547 - self::MATHML_GLOBAL_ATTRS,
548 - array(
549 - 'encoding' => true,
550 - )
551 - );
552 - $allowed_html['annotation'] = \array_merge(
553 - self::MATHML_GLOBAL_ATTRS,
554 - array(
555 - 'encoding' => true,
556 - )
557 - );
558 - return $allowed_html;
559 121 }
560 122 }