| @@ -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 | } |