Skip to content

Deduplicate and optimize the collection classes#3527

Open
pfefferle wants to merge 11 commits into
trunkfrom
refactor/optimize-collections
Open

Deduplicate and optimize the collection classes#3527
pfefferle wants to merge 11 commits into
trunkfrom
refactor/optimize-collections

Conversation

@pfefferle

Copy link
Copy Markdown
Member

Proposed changes:

Refactors includes/collection/ to remove duplication and cut complexity, keeping every public call site identical (composable traits via late static binding, static shape retained).

  • Bug fix: Interactions::persist() attached an anonymous akismet_comment_nonce filter it never removed, leaking a closure per federated comment and forcing Akismet's nonce check to inactive for every later comment in the same request. Now a named method plus try/finally restores all four temporary filters on every path.
  • Six new traits de-duplicate the copy-pasted patterns across the CPT-backed collections: recipient meta CRUD, paginated query() + get_by_authority(), object-title derivation, guid lookup, KSES-safe insert, and the batched purge loop.
  • Followers::get_inboxes collapses a two-step query into a single JOIN (runs on every federation send).
  • Remote_Actors actor serialization now suspends and restores only the mention/hashtag/link filters that were actually registered, at their original priority (fixes an unconditional re-add edge).
  • Actors::get_id_by_resource replaces a sloppy scheme regex with wp_parse_url().
  • Remote_Posts::get_by_remote_actor_id swaps deprecated meta_key/meta_value for a meta_query (drops two PHPCS suppressions).

Net roughly −435 lines across the collection classes, plus six small trait files. With_Guid_Lookup also drops a latent double-escape in Remote_Actors::get_by_uri (it ran esc_sql() inside prepare()), standardizing on the same esc_url_raw normalization used at write time.

Other information:

  • Have you written new tests for your changes, if applicable?

New regression test asserts the comment filters/actions are fully restored after persist().

Testing instructions:

  • Ensure the full PHP suite passes: npm run env-test (2760 tests green).
  • Ensure PHP standards pass: composer lint (clean).
  • Functional spot checks against trunk:
    • Receive a comment from the fediverse on a post, then submit a normal local comment, and confirm spam protection still applies to the local comment.
    • Follow and unfollow a remote actor; view followers and following lists with pagination.
    • Confirm inbox/outbox/remote-post cleanup (purge) still trims old items and preserves remote posts that have local comments.

Changelog entry

Changelog entry for the Akismet fix is included in the branch (.github/changelog/restore-akismet-nonce-after-federated-comment); the rest is internal refactor.

pfefferle added 2 commits July 8, 2026 19:04
Persist() attached an anonymous akismet_comment_nonce filter that was never
removed, leaking a closure per federated comment and forcing Akismet's nonce
check to 'inactive' for every later comment in the request. Use a named method
and a try/finally so all four temporary filters are restored on every path.

Claude-Session: https://claude.ai/code/session_01HDA9QR3bJoqM7NhnhT1whg
Extract the copy-pasted patterns across the CPT-backed collections into six
composable traits (recipients, guid lookup, KSES-safe insert, object title,
paginated query, purge) using late static binding, keeping the static call
sites unchanged. Collapse Followers::get_inboxes into a single JOIN, replace a
sloppy scheme regex with wp_parse_url, harden the actor serialization filter
toggle to restore only the filters it removed, and drop deprecated meta_key
query args.

Claude-Session: https://claude.ai/code/session_01HDA9QR3bJoqM7NhnhT1whg
Split Interactions::activity_to_comment into prepare_local_comment_data,
prepare_remote_comment_data, and prepare_remote_comment_meta helpers, and
funnel Remote_Actors create/update through a single persist() method so the
KSES-safe insert/update path lives in one place.

Claude-Session: https://claude.ai/code/session_01HDA9QR3bJoqM7NhnhT1whg
Collapse activity_to_comment() so each prepare helper owns its own fields and
the shared date/type keys are merged in once, instead of re-assembling every
field by hand. Reuse Sanitize::webfinger() for the comment author email, drop a
needless md5() around the interaction-counts cache key, and restore the note
that serialize_for_storage()'s filter suspension is a workaround for a missing
incoming/outgoing serialization context.

Claude-Session: https://claude.ai/code/session_01HDA9QR3bJoqM7NhnhT1whg
@pfefferle pfefferle marked this pull request as ready for review July 13, 2026 13:05
Copilot AI review requested due to automatic review settings July 13, 2026 13:05

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors and optimizes several includes/collection/ classes, focusing on removing duplicated logic, improving query efficiency for inbox lookups, and fixing global hook/filter leakage during inbound comment persistence.

Changes:

  • Fix and harden inbound comment persistence (Interactions) by preventing Akismet/kses/option overrides from leaking across later comments in the same request, and add regression coverage.
  • Optimize inbox resolution for Remote_Actors::get_inboxes() and Followers::get_inboxes() (single-query JOIN approach) and ensure empty results are cached.
  • Improve correctness/robustness in a few utility paths (e.g., Actors::get_id_by_resource() scheme parsing, outbox title derivation guardrails) with expanded PHPUnit coverage.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
includes/collection/class-interactions.php Refactors target resolution and comment persistence; adds cached grouped counts and named Akismet callback.
includes/collection/class-remote-actors.php Optimizes inbox fetching query, centralizes persistence, and adds inbox-cache invalidation + safer filter suspension for storage serialization.
includes/collection/class-followers.php Reworks follower inbox retrieval into a single JOIN query and improves cache handling for empty results.
includes/collection/class-following.php Adjusts blocked-actor removal to resolve via Remote_Actors::get_by_uri() before unfollowing.
includes/collection/class-actors.php Uses wp_parse_url() for scheme detection (vs regex) when resolving resources.
includes/collection/class-outbox.php Prevents fatal errors by guarding get_object_title() when given an unexpected array.
tests/phpunit/tests/includes/collection/class-test-interactions.php Adds regression tests ensuring global hooks/filters are restored and introduces query-count caching assertions.
tests/phpunit/tests/includes/collection/class-test-remote-actors.php Adds tests for restoring only registered outbound filters and for inbox cache invalidation on actor lifecycle changes.
tests/phpunit/tests/includes/collection/class-test-followers.php Extends follower inbox tests for single-query behavior and caching of empty results; adds blocked-actor removal test.
tests/phpunit/tests/includes/collection/class-test-following.php Adds blocked-actor removal test for following collection.
.github/changelog/restore-akismet-nonce-after-federated-comment Adds changelog entry for the Akismet nonce restoration bug fix.

Comment on lines +552 to 583
$is_insert = self::INSERT === $action;
$flood_priority = \has_action( 'check_comment_flood', 'check_comment_flood_db' );
$had_name_filter = false !== \has_filter( 'pre_option_require_name_email', '__return_false' );
$akismet_callback = array( self::class, 'akismet_comment_nonce_inactive' );
$had_akismet_filter = false !== \has_filter( 'akismet_comment_nonce', $akismet_callback );
$kses_callback = array( self::class, 'allowed_comment_html' );
$had_allowed_html = false !== \has_filter( 'wp_kses_allowed_html', $kses_callback );

// Disable flood control.
\remove_action( 'check_comment_flood', 'check_comment_flood_db' );
if ( false !== $flood_priority ) {
\remove_action( 'check_comment_flood', 'check_comment_flood_db', $flood_priority );
}

// Do not require email for AP entries.
\add_filter( 'pre_option_require_name_email', '__return_false' );
if ( ! $had_name_filter ) {
\add_filter( 'pre_option_require_name_email', '__return_false' );
}

// No nonce possible for this submission route.
\add_filter(
'akismet_comment_nonce',
static function () {
return 'inactive';
}
);
\add_filter( 'wp_kses_allowed_html', array( self::class, 'allowed_comment_html' ), 10, 2 );
if ( ! $had_akismet_filter ) {
\add_filter( 'akismet_comment_nonce', $akismet_callback );
}

if ( self::INSERT === $action ) {
if ( ! $had_allowed_html ) {
\add_filter( 'wp_kses_allowed_html', $kses_callback, 10, 2 );
}

if ( $is_insert ) {
$state = \wp_new_comment( $comment_data, true );
} else {
$state = \wp_update_comment( $comment_data, true );
}
Comment on lines +164 to 178
private static function persist( $args ) {
$is_update = ! empty( $args['ID'] );

$has_kses = false !== \has_filter( 'content_save_pre', 'wp_filter_post_kses' );
if ( $has_kses ) {
// Prevent KSES from corrupting JSON in post_content.
\kses_remove_filters();
}

$post_id = \wp_update_post( $args );
$post_id = $is_update ? \wp_update_post( $args ) : \wp_insert_post( $args );

if ( $has_kses ) {
// Restore KSES filters.
\kses_init_filters();
}
Comment on lines +657 to +680
private static function serialize_for_storage( $actor ) {
$callbacks = array(
array( 'Activitypub\Mention', 'filter_activity_object' ),
array( 'Activitypub\Hashtag', 'filter_activity_object' ),
array( 'Activitypub\Link', 'filter_activity_object' ),
);
$registered = array();

foreach ( $callbacks as $callback ) {
$priority = \has_filter( 'activitypub_activity_object_array', $callback );
if ( false !== $priority ) {
$registered[] = array( $callback, $priority );
\remove_filter( 'activitypub_activity_object_array', $callback, $priority );
}
}

$serialized = array( $actor->to_json(), $actor->to_array() );

foreach ( $registered as $filter ) {
\add_filter( 'activitypub_activity_object_array', $filter[0], $filter[1] );
}

return $serialized;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants