PluginProbe
ActivityPub / 7.7.0
ActivityPub v7.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-moderation.php +34 -267 9.3.07.7.0 View file →
@@ -84,13 +84,9 @@
84 84 */
85 85 public static function activity_is_blocked_site_wide( $activity ) {
86 86 $blocks = self::get_site_blocks();
87 87
88 - if ( ! self::has_blocks( $blocks ) ) {
89 - return false;
90 - }
91 -
92 - return self::check_activity_against_blocks( $activity, $blocks );
88 + return self::check_activity_against_blocks( $activity, $blocks['actors'], $blocks['domains'], $blocks['keywords'] );
93 89 }
94 90
95 91 /**
96 92 * Check if an activity is blocked for a specific user.
@@ -101,71 +97,12 @@
101 97 */
102 98 public static function activity_is_blocked_for_user( $activity, $user_id ) {
103 99 $blocks = self::get_user_blocks( $user_id );
104 100
105 - if ( ! self::has_blocks( $blocks ) ) {
106 - return false;
107 - }
108 -
109 - return self::check_activity_against_blocks( $activity, $blocks );
101 + return self::check_activity_against_blocks( $activity, $blocks['actors'], $blocks['domains'], $blocks['keywords'] );
110 102 }
111 103
112 104 /**
113 - * Check whether a set of blocks has anything in it.
114 - *
115 - * Worth asking before the checks run: they parse the activity and can go to the network for a
116 - * handle, and this runs once per local recipient of every delivery. Most sites block nothing.
117 - *
118 - * @since 9.3.0
119 - *
120 - * @param array $blocks Blocks organized by type, as returned by get_site_blocks().
121 - *
122 - * @return bool True if any list has an entry, false otherwise.
123 - */
124 - public static function has_blocks( $blocks ) {
125 - return self::has_actor_blocks( $blocks ) || self::has_domain_blocks( $blocks ) || self::has_keyword_blocks( $blocks );
126 - }
127 -
128 - /**
129 - * Check whether a set of blocks names any actor.
130 - *
131 - * @since 9.3.0
132 - *
133 - * @param array $blocks Blocks organized by type.
134 - *
135 - * @return bool True if an actor is blocked, false otherwise.
136 - */
137 - public static function has_actor_blocks( $blocks ) {
138 - return ! empty( $blocks['actors'] );
139 - }
140 -
141 - /**
142 - * Check whether a set of blocks names any domain.
143 - *
144 - * @since 9.3.0
145 - *
146 - * @param array $blocks Blocks organized by type.
147 - *
148 - * @return bool True if a domain is blocked, false otherwise.
149 - */
150 - public static function has_domain_blocks( $blocks ) {
151 - return ! empty( $blocks['domains'] );
152 - }
153 -
154 - /**
155 - * Check whether a set of blocks names any keyword.
156 - *
157 - * @since 9.3.0
158 - *
159 - * @param array $blocks Blocks organized by type.
160 - *
161 - * @return bool True if a keyword is blocked, false otherwise.
162 - */
163 - public static function has_keyword_blocks( $blocks ) {
164 - return ! empty( $blocks['keywords'] );
165 - }
166 -
167 - /**
168 105 * Add a block for a user.
169 106 *
170 107 * @param int $user_id The user ID.
171 108 * @param string $type The block type (actor, domain, keyword).
@@ -286,40 +223,8 @@
286 223 return true; // Already blocked.
287 224 }
288 225
289 226 /**
290 - * Add multiple site-wide blocks at once.
291 - *
292 - * More efficient than calling add_site_block() in a loop as it
293 - * performs a single database update.
294 - *
295 - * @param string $type The block type (domain or keyword only).
296 - * @param array $values Array of values to block.
297 - */
298 - public static function add_site_blocks( $type, $values ) {
299 - if ( ! \in_array( $type, array( self::TYPE_DOMAIN, self::TYPE_KEYWORD ), true ) ) {
300 - return;
301 - }
302 -
303 - if ( empty( $values ) ) {
304 - return;
305 - }
306 -
307 - foreach ( $values as $value ) {
308 - /**
309 - * Fired when a domain or keyword is blocked site-wide.
310 - *
311 - * @param string $value The blocked domain or keyword.
312 - * @param string $type The block type (actor, domain, keyword).
313 - */
314 - \do_action( 'activitypub_add_site_block', $value, $type );
315 - }
316 -
317 - $existing = \get_option( self::OPTION_KEYS[ $type ], array() );
318 - \update_option( self::OPTION_KEYS[ $type ], \array_unique( \array_merge( $existing, $values ) ) );
319 - }
320 -
321 - /**
322 227 * Remove a site-wide block.
323 228 *
324 229 * @param string $type The block type (actor, domain, keyword).
325 230 * @param string $value The value to unblock.
@@ -378,18 +283,17 @@
378 283 if ( ! $actor_uri ) {
379 284 return false;
380 285 }
381 286
382 - $hosts = array( Webfinger::get_host( $actor_uri ) );
383 -
384 287 // Check site-wide blocks.
385 288 $site_blocks = self::get_site_blocks();
386 - if ( self::uri_matches_actors( $actor_uri, $site_blocks['actors'] ) ) {
289 + if ( \in_array( $actor_uri, $site_blocks['actors'], true ) ) {
387 290 return true;
388 291 }
389 292
390 293 // Check site-wide domain blocks.
391 - if ( self::hosts_are_blocked( $hosts, $site_blocks['domains'] ) ) {
294 + $actor_domain = \wp_parse_url( $actor_uri, PHP_URL_HOST );
295 + if ( $actor_domain && \in_array( $actor_domain, $site_blocks['domains'], true ) ) {
392 296 return true;
393 297 }
394 298
395 299 // Check user-specific blocks if user_id is provided.
@@ -394,14 +298,14 @@
394 298
395 299 // Check user-specific blocks if user_id is provided.
396 300 if ( $user_id > 0 ) {
397 301 $user_blocks = self::get_user_blocks( $user_id );
398 - if ( self::uri_matches_actors( $actor_uri, $user_blocks['actors'] ) ) {
302 + if ( \in_array( $actor_uri, $user_blocks['actors'], true ) ) {
399 303 return true;
400 304 }
401 305
402 306 // Check user-specific domain blocks.
403 - if ( self::hosts_are_blocked( $hosts, $user_blocks['domains'] ) ) {
307 + if ( $actor_domain && \in_array( $actor_domain, $user_blocks['domains'], true ) ) {
404 308 return true;
405 309 }
406 310 }
407 311
@@ -408,188 +312,51 @@
408 312 return false;
409 313 }
410 314
411 315 /**
412 - * Check a set of hosts against the blocked domains.
413 - *
414 - * @param string[] $hosts The folded hosts to check.
415 - * @param array $blocked_domains The blocked domains.
416 - *
417 - * @return bool True if any host is blocked, false otherwise.
418 - */
419 - private static function hosts_are_blocked( $hosts, $blocked_domains ) {
420 - /*
421 - * Cast because a stored list is not guaranteed to be one: array_map() fatals on a scalar
422 - * where the foreach this replaced only warned, and this runs on every delivery.
423 - * array_filter drops the empty hosts, so an empty stored entry can never match one.
424 - */
425 - return (bool) \array_intersect( \array_filter( $hosts ), \array_map( __NAMESPACE__ . '\\fold_host', (array) $blocked_domains ) );
426 - }
427 -
428 - /**
429 - * Check an actor URI against a list of blocked actors.
430 - *
431 - * Compared entry by entry rather than by normalizing the whole list first, so a match
432 - * returns without touching the rest of it. This runs on every delivery, and a site can
433 - * block a lot of accounts.
434 - *
435 - * @param string $uri The actor URI to look for, in any spelling.
436 - * @param string[] $blocked_actors The blocked actor URIs.
437 - *
438 - * @return bool True if the URI is blocked, false otherwise.
439 - */
440 - private static function uri_matches_actors( $uri, $blocked_actors ) {
441 - $normalized = normalize_actor_uri( $uri );
442 -
443 - if ( '' === $normalized ) {
444 - return false;
445 - }
446 -
447 - foreach ( $blocked_actors as $blocked ) {
448 - if ( normalize_actor_uri( $blocked ) === $normalized ) {
449 - return true;
450 - }
451 - }
452 -
453 - return false;
454 - }
455 -
456 - /**
457 - * Check a delivered actor URI against the blocked actors.
458 - *
459 - * The delivered `actor` is only bound to a host by the signature, not to an exact string, so a
460 - * spelling that normalizes differently still has to be resolved to be ruled out.
461 - *
462 - * Two steps. The delivered string is compared normalized, which settles the ordinary case
463 - * without leaving the site. A delivery that gets past that is resolved over the network, so a
464 - * spelling the normalizer cannot fold still gets ruled out. A failed fetch leaves the delivery
465 - * unblocked, as before: a host that will not answer cannot be confirmed as blocked, and the
466 - * host owns the actor being claimed, so refusing to answer is a way out of a block. The
467 - * domain list is the tool for a host behaving that way.
468 - *
469 - * @param string $actor_id The actor URI from the delivered activity.
470 - * @param string[] $blocked_actors The blocked actor URIs.
471 - *
472 - * @return bool True if the actor is blocked, false otherwise.
473 - */
474 - private static function actor_matches_blocklist( $actor_id, $blocked_actors ) {
475 - if ( empty( $blocked_actors ) ) {
476 - return false;
477 - }
478 -
479 - /*
480 - * Narrow by host first, so only a delivery that could plausibly be blocked pays for a
481 - * resolution. Most deliveries reach a list with nothing on their host and leave here. The
482 - * host comes from the delivered actor only: taking it from the key id would let a forged
483 - * header choose which entries get compared.
484 - *
485 - * This is a cost gate, not a completeness one. An actor whose own host carries no entry is
486 - * never resolved, so a document served elsewhere that declares a blocked id is not caught
487 - * here even though the resolved comparison below would match it. Resolving every delivery
488 - * to close that would mean an outbound request per delivery whenever any actor is blocked.
489 - * Block the host to cover it.
490 - */
491 - $host = Webfinger::get_host( $actor_id );
492 -
493 - if ( '' === $host ) {
494 - return false;
495 - }
496 -
497 - $on_host = \array_filter(
498 - $blocked_actors,
499 - static function ( $blocked ) use ( $host ) {
500 - return Webfinger::get_host( $blocked ) === $host;
501 - }
502 - );
503 -
504 - if ( empty( $on_host ) ) {
505 - return false;
506 - }
507 -
508 - if ( self::uri_matches_actors( $actor_id, $on_host ) ) {
509 - return true;
510 - }
511 -
512 - /*
513 - * Resolved rather than read from a locally stored actor: a `guid` is the id the actor
514 - * declared, and `Update` stores an embedded actor object bound only to the sender's
515 - * host, so a remote server can store itself under any same-host id it likes.
516 - */
517 - $object = Http::get_remote_object( $actor_id );
518 -
519 - /*
520 - * Compared against the whole list, not the host-narrowed one: that narrowing decides
521 - * whether resolving is worth a request, but the id it resolves to can be on another host
522 - * and still be blocked.
523 - */
524 - return ! \is_wp_error( $object )
525 - && isset( $object['id'] )
526 - && \is_string( $object['id'] )
527 - && self::uri_matches_actors( $object['id'], $blocked_actors );
528 - }
529 -
530 - /**
531 316 * Check activity against blocklists.
532 317 *
533 - * @param Activity $activity The activity.
534 - * @param array $blocks Blocks organized by type, as returned by get_site_blocks().
318 + * @param Activity $activity The activity.
319 + * @param array $blocked_actors List of blocked actors.
320 + * @param array $blocked_domains List of blocked domains.
321 + * @param array $blocked_keywords List of blocked keywords.
535 322 * @return bool True if blocked, false otherwise.
536 323 */
537 - private static function check_activity_against_blocks( $activity, $blocks ) {
324 + private static function check_activity_against_blocks( $activity, $blocked_actors, $blocked_domains, $blocked_keywords ) {
325 + $has_object = \is_object( $activity->get_object() );
326 +
538 327 // Extract actor information.
539 328 $actor_id = object_to_uri( $activity->get_actor() );
540 329
541 - /*
542 - * Domains are checked before anything that goes to the network: the webfinger lookup
543 - * below and the actor check both issue requests, and a blocked domain must not be
544 - * contacted to find out that it is blocked.
545 - */
546 - if ( self::has_domain_blocks( $blocks ) ) {
547 - $hosts = array(
548 - Webfinger::get_host( $actor_id ),
549 - Webfinger::get_host( $activity->get_id() ),
550 - Webfinger::get_host( object_to_uri( $activity->get_object() ) ),
551 - );
330 + // Check blocked actors.
331 + if ( $actor_id ) {
332 + // If actor_id is not a URL, resolve it via webfinger.
333 + if ( ! \str_starts_with( $actor_id, 'http' ) ) {
334 + $resolved_url = Webfinger::resolve( $actor_id );
335 + if ( ! \is_wp_error( $resolved_url ) ) {
336 + $actor_id = $resolved_url;
337 + }
338 + }
552 339
553 - if ( self::hosts_are_blocked( $hosts, $blocks['domains'] ) ) {
340 + if ( \in_array( $actor_id, $blocked_actors, true ) ) {
554 341 return true;
555 342 }
556 343 }
557 344
558 - /*
559 - * `is_acct()` is too strict to decide this. Its host grammar rejects a trailing-dot FQDN
560 - * and any non-ASCII host, both of which `Webfinger::resolve()` resolves fine, and a
561 - * delivery that skips resolution over a spelling is a delivery that skips the block. All
562 - * that matters here is whether this is a handle rather than a URL already worth comparing.
563 - */
564 - $is_handle = false === \strpos( $actor_id, '://' ) && false !== \strpos( $actor_id, '@' );
565 -
566 - /*
567 - * Resolve a handle to its URL, but only for a list that could match it: a keyword-only
568 - * blocklist has no use for the actor's URL and should not pay a lookup for one. Its own
569 - * host is not blocked, or we would have returned above.
570 - */
571 -
572 - if ( ( self::has_domain_blocks( $blocks ) || self::has_actor_blocks( $blocks ) ) && $is_handle ) {
573 - $resolved_url = Webfinger::resolve( $actor_id );
574 - $actor_id = \is_wp_error( $resolved_url ) ? $actor_id : $resolved_url;
575 -
576 - /*
577 - * Checked again: webfinger returns whatever `href` the remote document names, and
578 - * that can be on a different host than the handle it was looked up under.
579 - */
580 - if ( self::hosts_are_blocked( array( Webfinger::get_host( $actor_id ) ), $blocks['domains'] ) ) {
345 + // Check blocked domains.
346 + $urls = array(
347 + \wp_parse_url( $actor_id, PHP_URL_HOST ),
348 + \wp_parse_url( $activity->get_id(), PHP_URL_HOST ),
349 + \wp_parse_url( object_to_uri( $activity->get_object() ) ?? '', PHP_URL_HOST ),
350 + );
351 + foreach ( $blocked_domains as $domain ) {
352 + if ( \in_array( $domain, $urls, true ) ) {
581 353 return true;
582 354 }
583 355 }
584 356
585 - // Check blocked actors.
586 - if ( self::actor_matches_blocklist( $actor_id, $blocks['actors'] ) ) {
587 - return true;
588 - }
589 -
590 357 // Check blocked keywords in activity content.
591 - if ( self::has_keyword_blocks( $blocks ) && \is_object( $activity->get_object() ) ) {
358 + if ( $has_object ) {
592 359 $object = $activity->get_object();
593 360 $content_map = array();
594 361 $content_map[] = $object->get_content();
595 362 $content_map[] = $object->get_summary();
@@ -614,9 +381,9 @@
614 381
615 382 $content_map = \array_filter( $content_map );
616 383 $content = \implode( ' ', $content_map );
617 384
618 - foreach ( (array) $blocks['keywords'] as $keyword ) {
385 + foreach ( $blocked_keywords as $keyword ) {
619 386 if ( \stripos( $content, $keyword ) !== false ) {
620 387 return true;
621 388 }
622 389 }