PluginProbe
ActivityPub / 8.0.2
ActivityPub v8.0.2
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 +51 -329 9.2.08.0.2 View file →
@@ -13,53 +13,9 @@
13 13 /**
14 14 * Sanitization class.
15 15 */
16 16 class Sanitize {
17 -
18 17 /**
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 18 * Sanitize a list of URLs.
63 19 *
64 20 * @param string|array $value The value to sanitize.
65 21 * @return array The sanitized list of URLs.
@@ -164,19 +120,8 @@
164 120 if ( empty( $sanitized ) ) {
165 121 return Blog::get_default_username();
166 122 }
167 123
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 124 // Check for login or nicename.
180 125 $user = new \WP_User_Query(
181 126 array(
182 127 'search' => $sanitized,
@@ -207,19 +152,19 @@
207 152 *
208 153 * @return string The sanitized value.
209 154 */
210 155 public static function constant_value( $value ) {
211 - if ( \is_bool( $value ) ) {
156 + if ( is_bool( $value ) ) {
212 157 return $value ? 'true' : 'false';
213 158 }
214 159
215 - if ( \is_string( $value ) ) {
216 - return \esc_attr( $value );
160 + if ( is_string( $value ) ) {
161 + return esc_attr( $value );
217 162 }
218 163
219 - if ( \is_array( $value ) ) {
164 + if ( is_array( $value ) ) {
220 165 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r
221 - return \print_r( $value, true );
166 + return print_r( $value, true );
222 167 }
223 168
224 169 return $value;
225 170 }
@@ -271,47 +216,13 @@
271 216 return \trim( \preg_replace( '/>[\n\r\t]+</', '><', $content ) );
272 217 }
273 218
274 219 /**
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 220 * Clean HTML for ActivityPub federation.
308 221 *
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.
222 + * Keeps all WordPress allowed tags but removes global attributes like
223 + * class, id, style, data-*, aria-* that increase payload size.
312 224 *
313 - * @see https://codeberg.org/fediverse/fep/src/branch/main/fep/b2b8/fep-b2b8.md
314 225 * @see https://github.com/Automattic/wordpress-activitypub/issues/2619
315 226 *
316 227 * @param string $content The HTML content to clean.
317 228 *
@@ -321,251 +232,62 @@
321 232 if ( empty( $content ) ) {
322 233 return $content;
323 234 }
324 235
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 );
236 + // Start with all WordPress allowed post tags.
237 + $allowed_html = \wp_kses_allowed_html( 'post' );
334 238
239 + // Global attributes to remove from all elements.
240 + $remove_attrs = array(
241 + 'aria-controls',
242 + 'aria-current',
243 + 'aria-describedby',
244 + 'aria-details',
245 + 'aria-expanded',
246 + 'aria-hidden',
247 + 'aria-label',
248 + 'aria-labelledby',
249 + 'aria-live',
250 + 'class',
251 + 'data-*',
252 + 'decoding',
253 + 'dir',
254 + 'hidden',
255 + 'id',
256 + 'lang',
257 + 'loading',
258 + 'role',
259 + 'style',
260 + 'tabindex',
261 + 'title',
262 + 'xml:lang',
263 + );
264 +
335 265 /**
336 - * Fires the deprecated attribute removal filter.
266 + * Filter the global attributes to remove from all elements.
337 267 *
338 - * @deprecated 8.1.0 Use the {@see 'activitypub_allowed_html'} filter instead.
268 + * @param array $remove_attrs Global attributes to remove.
339 269 */
340 - if ( \has_filter( 'activitypub_remove_html_attributes' ) ) {
341 - \_deprecated_hook( 'activitypub_remove_html_attributes', '8.1.0', 'activitypub_allowed_html' );
270 + $remove_attrs = \apply_filters( 'activitypub_remove_html_attributes', $remove_attrs );
271 +
272 + // Remove global attributes from all tags.
273 + foreach ( $allowed_html as $tag => $attrs ) {
274 + $allowed_html[ $tag ] = \array_diff_key( $attrs, \array_flip( $remove_attrs ) );
342 275 }
343 276
277 + // Re-add class and title for anchors (needed for microformats).
278 + $allowed_html['a']['class'] = true;
279 + $allowed_html['a']['title'] = true;
280 +
281 + // Re-add class for spans (needed for microformats).
282 + $allowed_html['span']['class'] = true;
283 +
344 284 /**
345 - * Filters the allowed HTML for ActivityPub content.
285 + * Filter the final allowed HTML for ActivityPub content.
346 286 *
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 287 * @param array $allowed_html The allowed HTML structure for wp_kses.
352 288 */
353 - $allowed_html = \apply_filters( 'activitypub_allowed_html', self::get_allowed_html() );
289 + $allowed_html = \apply_filters( 'activitypub_allowed_html', $allowed_html );
354 290
355 291 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 292 }
571 293 }