Release 4.2.0#1893
Merged
Merged
Conversation
In some cases `require_name_email` and `comment_registration` will both be enabled, the details from the email lookup should be respected before falling back to the comment author name.
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Add missing workflow trigger
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Add abstract Ability base class, Abilities loader with WP 6.9 + setting gating, and a new "Enable Abilities API" toggle under the existing Advanced settings section. The loader hooks wp_abilities_api_init and will register concrete abilities once they are added in subsequent commits. Falls back silently on WordPress < 6.9.
Implements the six read-only abilities under stream/* namespace: get-records, get-record, get-settings, get-alerts, get-connectors, and get-exclusion-rules. Each ability has hand-written JSON Schemas, delegates to existing Stream APIs, and ships with a PHPUnit test covering name, schema, permission gating, and execution. Tests skip themselves on WordPress < 6.9 via the shared Abilities_TestCase base class.
Add three abilities that mutate Stream state through the existing internal APIs: stream/create-alert (creates a wp_stream_alerts CPT post with alert_type and alert_meta), stream/update-settings (partial-merge update to the wp_stream option), and stream/create-exclusion-rule (appends to the parallel-array exclude_rules option columns Stream already uses). Each ability is gated behind the manage_options capability. Hand-written JSON schemas describe inputs and outputs for AI consumers; create-alert requires the four trigger fields, create-exclusion-rule requires at least one filter property, and update-settings requires a non-empty settings map. Each ability ships with PHPUnit coverage that verifies permissions, schema shape, and end-to-end execution against the option or post store.
Add the two destructive abilities required by the ticket: stream/purge-records (filtered DELETE against the Stream records table with a cascading meta delete that mirrors Admin::erase()) and stream/delete-alert (force-delete a wp_stream_alerts post by ID). purge-records refuses to run unless confirm: true is supplied AND at least one filter (older_than_days, connector, context, action) is set, preventing an accidental full table wipe. The row count is computed before the DELETE so the response is meaningful even though the multi-table DELETE returns the combined affected rows. delete-alert returns a 404 WP_Error when the ID is unknown or refers to a non-alert post type, which makes the ability safely idempotent. Both abilities ship with PHPUnit coverage that exercises permissions, schema validation, the happy path, the refusal paths, and (for purge-records) the meta cascade.
Cover the two infrastructure pieces left untested by the per-ability suites: tests/phpunit/test-class-ability.php exercises the Ability abstract base via an in-file Fake_Ability_For_Test subclass (verifies get_meta() emits category and show_in_rest, conditionally adds an annotations key, and that the default permission_callback denies subscribers and grants admins; also asserts register() makes the ability retrievable via wp_get_ability() when the API is available). tests/phpunit/test-class-abilities.php covers the loader: is_available() tracks the WP_Ability class presence, is_enabled() reflects the advanced_enable_abilities_api option, the constructor only hooks wp_abilities_api_init when both gates pass, get_ability_slugs() lists all eleven slugs, load_abilities() instantiates each, and register_abilities() does not double-load on a second invocation. Resolves: XWPENG-13
- Register 'stream' category on wp_abilities_api_categories_init so abilities with category=stream pass core's category-existence check - Move 'category' from meta to top-level args in Ability::register(), matching the wp_register_ability() contract in WP 6.9 - Replace get-record's broken DB::get_records(['record' => $id]) call (Query class never implemented the singular 'record' arg) with a direct $wpdb single-row lookup - Snapshot/restore $plugin->settings->options in Abilities_TestCase so in-memory mutations from write-ability tests don't leak across tests - Update tests to satisfy the doing_action() guards on wp_register_ability() and wp_register_ability_category()
Add a per-ability 'instructions' annotation: a 1-2 sentence note for AI agents about when and how to call each ability, distinct from the description (which describes what it does). Add tests/phpunit/abilities/test-rest-integration.php covering all three ability types end-to-end: dispatches actual WP_REST_Requests through WP_REST_Server and asserts 200/403/404/405 paths plus the list-abilities endpoint exposes all 11 stream/* abilities. Catches breakage in the real REST stack that direct execute() tests miss. Add idempotent: true to purge-records annotations. WP core's REST router only routes to DELETE when destructive AND idempotent are both true; without idempotent the controller expects POST. Refactor test action-firing to use the documented core test pattern of pushing onto $wp_current_filter rather than registering callbacks through add_action(). Cleaner, no global hook pollution, matches the convention used in WordPress core's own abilities-api tests. Make Abilities::register_abilities() defensive: skip per-ability register() calls when the ability is already registered, preventing spurious _doing_it_wrong notices when load_abilities() runs more than once in the same process.
WP core's WP_Ability::invoke_callback() spreads zero arguments into the execute callback when the ability declares no input_schema (see wp-includes/abilities-api/class-wp-ability.php:506-512). Our previous 'execute($input)' signature required one argument, so any GET request to a no-input-schema ability raised a fatal ArgumentCountError and returned HTTP 500 to the caller. Add '$input = null' as the default on the abstract Ability::execute() plus all 11 concrete subclasses and the test fake. Null matches WP core's own conventions (their invoke_callback and check_permissions both default $input to null). Abilities that DO declare an input_schema continue to receive the parsed value verbatim from core, so the default sits unused for those. Caught by live e2e testing against WP 6.9.1 (Phase 4 of XWPENG-13-e2e.md): get-settings, get-connectors, and get-exclusion-rules previously fataled.
- Authorization: read abilities use 'view_stream'; base default uses
WP_STREAM_SETTINGS_CAPABILITY. Abilities registers a user_has_cap
filter for REST contexts where Admin (and its filter) isn't loaded,
so allowed roles can call read abilities consistently with the UI.
- update-settings: allowlist {section}_{field} keys from registered
fields, run incoming values through Settings::sanitize_settings(),
reject payloads with no recognized keys.
- create-exclusion-rule: schema gains format:ip and maxLength bounds;
execute() sanitizes via sanitize_text_field(), validates IPs with
FILTER_VALIDATE_IP, validates connector against registered slugs,
rejects all-empty payloads.
- purge-records: use rows_affected from the DELETE itself (no stale
pre-count); run orphan-meta sweep after; fix MySQL alias syntax.
- get-record: kept direct query (Query::query has a real array_shift
bug with record__in) but adds explicit blog_id scoping on multisite
so cross-site record leakage cannot occur.
The 5 read-only abilities (get-records, get-record, get-alerts, get-connectors, get-exclusion-rules) all carried an identical permission_callback() returning current_user_can( 'view_stream' ) with the same rationale docblock. Move it into a shared trait so the authorization rule lives in one place. Each ability file require_once's the trait directly so per-test loaders (which require ability files individually) keep working without any autoloader changes. Net -35 LOC. Single-site and multisite Ability suites unchanged: 316 tests pass with the same skipped/incomplete counts as before.
- orderby: add enum bound to Query::query()'s actual sortable columns, and change the default from 'date' (not a real Stream column; silently fell back to ID) to 'created'. This makes the silent fallback impossible at the schema layer for REST callers and surfaces the contract for direct PHP callers. - user_id__in / connector__in: add maxItems: 100 so a caller cannot force an unbounded IN(...) clause from a single request. Tests cover schema shape, REST schema validation (orderby=date now rejected, 101 items rejected), and a behavioral regression that seeds two records with out-of-order created/ID and asserts orderby=created ASC actually orders by created -- not by ID, which is what the old silent fallback was doing.
β¦or-context Mirror the admin form's create-alert flow (classes/class-alerts.php:766- 806) so API-created alerts behave identically to UI-created ones: - Validate alert_type against $plugin->alerts->alert_types (the registered notifier slugs). Schema can't enum these because wp_stream_alert_types is a filter -- a hardcoded enum would lock out 3rd-party notifiers. Reject unknown slugs with stream_unknown_alert_type / status 400 BEFORE inserting the post. - Split 'connector-context' input into trigger_connector + trigger_context meta keys, exactly like the admin form does. Without the split, Alert_Trigger_Context::check_record() silently let any connector through because trigger_connector was never populated -- alerts created via the API were effectively connector-agnostic. - Build an Alert model from the split meta and use $alert->get_title() for post_title, so the admin list shows a meaningful title instead of 'Auto Draft'. Tests cover the title regression, the connector-dash-context split, and the alert_type rejection path (including no-side-effects: no post is inserted when validation fails).
β¦ed multisite On a network-activated multisite install, the Abilities API toggle is saved to the wp_stream_network site option via Network::update_site_option(). However, Settings::get_options() only reads from get_site_option() when is_network_admin() is true; in REST and frontend contexts $plugin->settings->options reflects the (typically empty) per-site option. As a result, is_enabled() returned false in REST even when the network admin had enabled the API, making the entire Abilities API silently unreachable on network-activated sites. Read the network option directly via get_site_option($settings->network_options_key) when is_multisite() && $plugin->is_network_activated(), and fall back to the existing in-memory per-site options otherwise (preserves single-site and per-site-activated behavior). Adds two regression tests: - test_is_enabled_reads_network_option_when_network_activated (@group ms-required) flips the wp_stream_is_network_activated filter and proves is_enabled() follows the network option even when in-memory options say disabled. - test_is_enabled_reads_per_site_options_when_not_network_activated proves the network option is ignored when the plugin isn't network-activated.
Add //end try comments to satisfy Squiz.Commenting.LongConditionClosingComment on the new is_enabled() multisite tests, and lift the inline 'not a registered notifier' comment above the array literal so it doesn't trip Squiz.Commenting.PostStatementComment in the unknown-alert_type test.
The comment claimed format:ip is a hint not enforced by rest_validate_value_from_schema(), which is wrong. WP core's rest_is_ip_address() in wp-includes/rest-api.php DOES validate the format and rejects bogus IPs at the schema layer with ability_invalid_input before our execute() runs. Reframe the in-method check as defense-in-depth for direct PHP callers who invoke $ability->execute() outside the REST stack.
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
β¦view_stream cap
The inline @php -r string in composer.json had grown into an unreadable nested-quote mess. Move the same logic into local/scripts/install-mcp-adapter-deps.php with proper comments explaining the three branches: - Adapter directory absent (production --no-dev releases): silent no-op. - Adapter directory present, vendor populated: silent no-op. - Adapter directory present, vendor missing (typical dev install): run `composer install --no-dev` inside the adapter directory so its Autoloader can find vendor/autoload.php on plugin activation. No behavior change for end users. Release safety is unchanged: --no-dev installs skip wordpress/mcp-adapter entirely, so the adapter directory never exists in shipped artifacts and the script's first guard returns silently. /local/ is also excluded by .distignore as a second backstop.
Move the dev environment from HTTP to HTTPS so it matches what WordPress Application Passwords and most MCP clients expect by default. - wp-cli.yml: set url to https://stream.wpenv.net so wp core multisite-install writes https values to wp_options at install time. No wp-config overrides needed -- the DB values are authoritative. - Playwright: swap all http://stream.wpenv.net references to https://. Add ignoreHTTPSErrors: true so Playwright's Chromium accepts the mkcert-issued certificate without requiring `mkcert -install` on the runner / host. - CI workflow: wait for TCP :443 instead of :80 in the E2E job since HTTPS is now the canonical entry point. - contributing.md: dev-environment URL is now https; documented one-line wp search-replace for existing checkouts to upgrade their DB values without a full reinstall. No Apache HTTP->HTTPS redirect added; with siteurl and home both https in the DB, WordPress already generates https URLs everywhere and redirects http wp-admin / wp-login hits via the canonical-URL machinery, so the extra redirect would only catch direct `curl http://` cases that are useful to keep working for quick TCP probes.
The published ghcr.io/xwp/stream-wordpress image is refreshed only by docker-images.yml on master pushes. When this branch updates local/docker/wordpress/Dockerfile (e.g. `a2enmod ssl` + the stream-ssl vhost), `docker compose pull wordpress` happily fetches the stale published image and skips the local build context. Add an explicit `docker compose build wordpress` step after the pull so the branch's Dockerfile actually lands in the running container. Without this, CI's HTTPS smoke fails with ERR_CONNECTION_CLOSED on :443 because Apache has no SSL vhost configured. Docker's layer cache keeps the rebuild cheap when the Dockerfile is unchanged from the published base.
The previous description packed three sentences and an internal URL into a single line, making the Stream settings UI visually dense and forcing users to scan past technical details to find the primary action. Trim to one sentence that surfaces what enabling does, treats MCP as a parenthetical secondary fact, and keeps the WP version requirement. Title is unchanged (still "Enable Abilities API and MCP").
1. Settings::updated_option_ttl_remove_records() now triggers an immediate purge directly. Previously it relied on the legacy wp_stream_auto_purge action being hooked to purge_scheduled_action, which this PR severed. Without this fix, shortening the TTL did not take effect until the next 12h recurring tick. 2. TTL fallback uses isset() instead of empty(), so an explicit '0' set via CLI/SQL is no longer silently overridden to 30. Added an explicit short-circuit: a non-positive TTL bails out of the cycle entirely. The UI enforces min=1; the only paths to 0 are operator error, and bailing out (records stop being purged) is a less destructive failure mode than honoring it (records get wiped repeatedly every 12h). 3. Overlap guard now reuses Admin::is_running_auto_purge(), so a pending reaper also blocks a new chain. Previously only a pending batch action blocked the guard, leaving a small window between chain completion and reaper completion where a new chain could stack. Three new PHPUnit tests cover each behaviour. Refs XWPENG-28
- Move wp_stream_auto_purge BC action to after all bail-out checks so it fires only when a purge actually runs (was firing on every recurring tick regardless of whether work happened). - Extend is_running_auto_purge() to also check IN-PROGRESS actions via as_get_scheduled_actions() so the overlap guard cannot let a second chain stack against rows the running batch worker is still touching. - Settings TTL-shortened path now enqueues AUTO_PURGE_ACTION via as_enqueue_async_action() so the immediate purge serializes through Action Scheduler instead of bypassing the overlap guard with an inline call. Inline fallback retained for when AS is unavailable. - Render an admin notice for wp_stream_message=orphan_meta_cleanup_scheduled on both admin_notices and network_admin_notices so the post-redirect UX is actually visible (was a half-built feature: redirect happened, no notice rendered). - E2E wpEval(): switch from 'docker compose run --rm' to 'docker compose exec -T' to attach to the long-lived container (~3-5s saved per call) and surface failures via console.warn instead of silently swallowing errors. - PHPUnit: add coverage for both the BC-action-suppressed-on-bailout path and the in-progress-action overlap guard. Update the TTL-shortened test to assert the AS enqueue path.
The previous quick-win switch from 'docker compose run --rm --user $(id -u)' to 'docker compose exec -T' dropped the user mapping. On local dev that happened to work because the wordpress container's default exec user is what the runtime image inherits; on CI it defaults to root and wp-cli refuses to run with: YIKES! It looks like you're running this as root. $(id -u) only worked on the host because UID 1000 mapped to www-data inside the container β that's not portable to the GitHub Actions runner (UID 1001). Pin the exec user explicitly to www-data, which owns the WordPress files inside the container. Failing run: actions/runs/26144103763
Register Stream operations as WordPress Abilities
β¦#1879) Bumps [johnbillion/query-monitor](https://github.com/johnbillion/query-monitor) from 3.16.3 to 3.20.4. - [Release notes](https://github.com/johnbillion/query-monitor/releases) - [Commits](johnbillion/query-monitor@3.16.3...3.20.4) --- updated-dependencies: - dependency-name: johnbillion/query-monitor dependency-version: 3.20.4 dependency-type: direct:development ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
β¦-5.4.51 chore(deps-dev): bump symfony/process from 5.4.46 to 5.4.51
β¦auto-purge-action-scheduler # Conflicts: # classes/class-settings.php
Bumps [phpunit/phpunit](https://github.com/sebastianbergmann/phpunit) from 9.6.20 to 9.6.33. - [Release notes](https://github.com/sebastianbergmann/phpunit/releases) - [Changelog](https://github.com/sebastianbergmann/phpunit/blob/9.6.33/ChangeLog-9.6.md) - [Commits](sebastianbergmann/phpunit@9.6.20...9.6.33) --- updated-dependencies: - dependency-name: phpunit/phpunit dependency-version: 9.6.33 dependency-type: direct:development ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Comments Connector: Avoid a PHP Warning on pingbacks.
Bump axios from 1.8.4 to 1.16.0
Bumps [composer/composer](https://github.com/composer/composer) from 2.7.7 to 2.9.8. - [Release notes](https://github.com/composer/composer/releases) - [Changelog](https://github.com/composer/composer/blob/main/CHANGELOG.md) - [Commits](composer/composer@2.7.7...2.9.8) --- updated-dependencies: - dependency-name: composer/composer dependency-version: 2.9.8 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Set baseURL in playwright.config.js and replace absolute https://stream.wpenv.net references in every spec with relative paths. Also drops the docker-exec state seeding from admin-orphan-cleanup.spec.js so it matches the browser-only convention of the rest of the suite; the running-state UX it seeded is covered by PHPUnit.
Co-authored-by: Utkarsh Patel <itismeutkarsh@gmail.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
1. wp_ajax_clean_orphan_meta() now uses is_running_auto_purge() for its idempotency check instead of as_has_scheduled_action(), which only probes PENDING. The new probe checks PENDING + RUNNING across the batch worker and reaper, matching the UI hide condition and closing the small CSRF/stale-URL window where a duplicate reaper could be enqueued while a chain is mid-flight. function_exists() guard updated to as_get_scheduled_actions accordingly. 2. Add explicit case 'none' in Settings::render_field() so the intent of the running-state UI swap (hide the value column, let the description carry the message) is captured in code instead of relying on the switch's implicit fall-through. Used by both the Reset Stream Database row and the new Clean Orphaned Meta row. 3. Expand auto_purge_batch() docblock to acknowledge the last_entry-based forward-progress trade-off: any eligible row that lands inside the already-touched ID range after that batch ran is skipped by the current chain and picked up on the next tick. Sources are bounded (dev seeders, importers, clock skew); steady-state logging is monotonic so this is a no-op for production traffic. 4. Changelog clarifies that the wp_stream_auto_purge BC action's semantics have changed: it used to fire on every WP-Cron tick regardless of whether work happened, now fires only after all bail-out checks pass. Integrations that relied on the legacy tick-rate semantics should switch to AUTO_PURGE_ACTION. 5. New PHPUnit test test_settings_ttl_shortened_via_option_update_enqueues_purge exercises the full hook wiring (update_option / update_site_option β Settings::updated_option_ttl_remove_records β AS enqueue) end-to- end. The existing unit test calls the method directly and would still pass if Network::__construct() lost its update_site_option_wp_stream_network hook registration. 6. New PHPUnit test test_ajax_clean_orphan_meta_denies_users_without_settings_cap covers the security boundary on the manual cleanup handler. Uses _handleAjax() so WP_Ajax_UnitTestCase's buffer machinery runs. 7. New PHPUnit test test_clean_orphan_meta_field_reflects_running_state asserts the integration between is_running_auto_purge() and the field-rendering decision in Settings::build_clean_orphan_meta_field(). Replaces the e2e specs removed in b4c8f28 for activation-race fragility with deterministic PHPUnit coverage. 8. New PHPUnit test test_purge_scheduled_action_scopes_to_current_blog_when_not_network_activated forces the multisite-not-network-activated branch via a Plugin stub and asserts the enqueued batch carries get_current_blog_id() rather than 0. The acceptance criterion ("per-site activations only purge the current blog") was only covered at the batch-worker layer; the routing decision in purge_scheduled_action() was untested. Refs XWPENG-28
Production loads the trait via Abilities::load_abilities() before any class-ability-*.php file is included, but PHPUnit's coverage post-processor with processUncoveredFiles="true" walks the <coverage><include> directories directly via include_once and bypasses the plugin's loader. On PHP versions where the order surfaces the issue, each read ability fatals on `use Trait_View_Stream_Permission` during coverage generation, breaking the clover.xml emit step and failing the test target with a non-zero exit even when all tests pass. Require the trait at bootstrap so it is available before any include_once walks the abilities directory. Mirrors the production chokepoint without reintroducing per-file require_once at the top of each ability class.
β¦auto-purge-action-scheduler
* feat(purge): add Action Scheduler constants for auto-purge migration Refs XWPENG-28 * feat(purge): wire Admin to AS-based auto-purge callbacks Adds stub method bodies so the action registration resolves; the real implementations land in subsequent commits. Refs XWPENG-28 * feat(purge): schedule auto-purge via Action Scheduler Replaces the legacy twicedaily WP-Cron event with a recurring AS action scheduled at 12h intervals. Clears any pre-existing legacy event on upgrade so the two cannot double-fire. Idempotent: re-running the setup while a recurring action is pending is a no-op. Refs XWPENG-28 * feat(purge): replace inline DELETE with AS chain enqueue The recurring action now snapshots the TTL cutoff in UTC, fires wp_stream_auto_purge for back-compat, applies an overlap guard, and enqueues the first batch into the auto-purge chain. Multisite scoping (per-site activation) is encoded as blog_id; 0 means 'all blogs'. Defaults are merged into the options array via wp_parse_args so a partially-saved option still gets the missing keys. Refs XWPENG-28 * feat(purge): add batched auto_purge_batch worker Window-based deletion (ID range \u2264 wp_stream_batch_size) joined against stream_meta in a single statement, mirroring erase_large_records(). Snapshotted UTC cutoff is threaded through each batch in the chain. blog_id == 0 means 'all blogs' for network-activated installs; non-zero scopes to that blog. Schedules the orphan reaper as the terminal step when no more rows are eligible. Refs XWPENG-28 * feat(purge): add terminal orphan reaper to auto-purge chain Runs delete_orphaned_meta() once at the end of every chain so installs that already had orphan meta from historical timed-out purges heal over time without operator intervention. Lifts delete_orphaned_meta() visibility from private to protected so the reaper can call it. Refs XWPENG-28 * feat(purge): add manual 'Clean Orphaned Meta' link on Settings \u2192 Advanced Settings UI link is nonced for users with WP_STREAM_SETTINGS_CAPABILITY; the ajax handler schedules a one-shot reaper via Action Scheduler. Idempotent: re-clicking while a reaper is pending is a no-op. Bails out early under WP_STREAM_TESTS so PHPUnit doesn't exit the worker. Refs XWPENG-28 * test(e2e): cover manual orphan-meta cleanup link Asserts the Clean Orphaned Meta link renders on Settings \u2192 Advanced, points at admin-ajax.php with the expected action + nonce, and that following the link redirects to the settings page with a confirmation marker in the URL. Also fixes the redirect target in the handler to use network_settings_page_slug on network-activated installs. Refs XWPENG-28 * fix(purge): ensure forward progress on tables with concurrent writes The previous auto_purge_batch SELECT used 'WHERE created < cutoff ORDER BY ID DESC LIMIT 1' to find the next window's top, but on hosts that are actively logging during a chain the ID space is sparse (eligible rows interleaved with fresh rows whose created > cutoff). That caused each subsequent batch to find a top only ~30 IDs below the previous, stalling progress. Match Admin::erase_large_records()'s pattern instead: pass last_entry (the lower bound of the previous window) through the chain and use 'WHERE ID < last_entry' to guarantee the next batch starts strictly below the previous window. Stride is now exactly wp_stream_batch_size IDs per batch. Verified end-to-end on a multisite install seeded to ~320k aged records: chain drains in ~35 batches at batch_size=10000 and terminates with zero orphans. Refs XWPENG-28 * style: address PHPCS findings in auto-purge implementation - Replace interpolated-SQL pattern in auto_purge_batch with explicit prepared statements per (blog_id, last_entry) combination. - Correct @return doctype on wp_ajax_clean_orphan_meta to bool|void. - Settings UI array alignment. - Add @param to set_records_ttl test helper. Refs XWPENG-28 * docs(changelog): note XWPENG-28 auto-purge migration Refs XWPENG-28 * fix(purge): harden missing-option fallback against filtered defaults Settings::get_defaults() runs every field through wp_stream_settings_option_fields, which Network::get_network_admin_fields() uses to strip 'records_ttl' from the per-site option's defaults set. Outside any admin context (Action Scheduler, WP-CLI, system cron) the per-site option_key is in effect, so the filtered defaults never contained general_records_ttl. Without this fallback the auto-purge silently no-ops on every install where the option is missing, defeating the whole point of fixing this on bloated sites. Hardcoded fallback to 30 days (the documented default on the settings field itself) when general_records_ttl is absent after the merge. Caught by section 9 of the e2e plan. Refs XWPENG-28 * feat(purge): consult wp_stream_is_large_records_table for small-table fast path The acceptance criteria require the auto-purge to honor the same wp_stream_is_large_records_table filter the manual reset uses, so ops only have one knob to tune table-size semantics. The recurring callback now counts eligible rows once, passes the count through Plugin::is_large_records_table(), and: - Small table (filter returns false): runs a single inline multi-table DELETE for the eligible rows, then enqueues the orphan reaper as a terminal AS action so the heal step stays observable in Tools \u2192 Scheduled Actions. - Large table (filter returns true, default for record_count > 1M): enqueues the batched chain as before. Tests: - New test_purge_scheduled_action_small_table_fast_path covering the inline-DELETE branch. - New test_purge_scheduled_action_large_table_uses_batched_chain exercising the batched branch via the filter. - Existing batched-path tests now opt into the chain explicitly via add_filter('wp_stream_is_large_records_table','__return_true'). Refs XWPENG-28 * feat(purge): expose Admin::is_running_auto_purge() state probe Mirrors is_running_async_deletion() but checks the auto-purge group (batch + reaper). The recurring scheduler is intentionally excluded from the probe so it doesn't always report 'running' under normal operation. Settings UI uses this in the next commit to render an 'Auto-purge currently running' notice on Settings \u2192 Advanced. Refs XWPENG-28 * feat(purge): hide manual Clean Orphaned Meta link while chain is running When the auto-purge chain is active (batch worker or reaper pending), the manual Settings \u2192 Advanced cleanup link is hidden and the field description is swapped to explain that the reaper will run as part of the active cycle. Mirrors how Reset Stream Database hides itself during async deletion. Refs XWPENG-28 * test(e2e): cover purge-active and purge-idle UI states Two new specs exercise the Settings \u2192 Advanced field behaviour driven by Admin::is_running_auto_purge(): - Active state: seed a pending reaper action via wp-cli, assert the Clean Orphaned Meta link is removed from the DOM and the swapped description ('Auto-purge is currently running') is visible. - Idle state: drain the seeded action and assert the link is restored. Both specs use a small wp-cli helper (execSync into the wordpress container) to seed/clear AS state, keeping the test free of any browser-side timing on the AS worker. Refs XWPENG-28 * test(e2e): harden orphan-cleanup spec against activation races - wpEval helper now swallows non-zero exits from the wordpress container instead of throwing into the test runner. State seeding is best-effort; the test's own assertions are the source of truth. - beforeAll waits for the post-activation navigation and explicitly confirms 'Network Deactivate Stream' is visible before any test runs, failing fast instead of letting every test silently hit the 'Sorry, you are not allowed to access this page' redirect when a prior suite leaves Stream deactivated. In-isolation: spec is stable across 3 consecutive runs. Pre-existing cross-spec activation races (editor-new-post, admin-ui-smoke) remain out of scope here. Refs XWPENG-28 * docs(changelog): note small-table fast path + running-state UX Refs XWPENG-28 * fix(e2e): drop unused catch binding for ESLint CI runs ESLint with no-unused-vars; the wpEval helper's catch block declared 'err' but never referenced it. Use the optional binding form 'catch {}' which is supported on the runner's Node version (22+). Refs XWPENG-28 * test: drop two low-value auto-purge unit tests - test_auto_purge_action_constants_exist was a tautology: it asserted that four constants equal the string values they are declared with. The test catches nothing that static analysis or a typo in the consumer wouldn't catch. - test_auto_purge_batch_respects_wp_stream_batch_size_filter only verified the filter was *called* (invocation counter), not that the returned value affected behaviour. test_auto_purge_batch_deletes_ window_and_chains_next_batch already drives the chain with a custom batch_size and asserts on the resulting chain, which is the functional contract that matters. No coverage lost. Refs XWPENG-28 * review: address code review feedback 1. auto_purge_batch() now throws InvalidArgumentException on empty cutoff instead of silently returning. A bare 'return' caused AS to mark the action complete; throwing makes AS log it as failed and surface it in Tools > Scheduled Actions. New PHPUnit test covers the throw path. In practice this branch is unreachable because purge_scheduled_action() always populates the cutoff, but the guard exists for third-party code that may enqueue with bad input. 2. wp_ajax_clean_orphan_meta() now uses $this->settings_cap to match the rest of the file (wp_ajax_reset, ajax_filters). The bare WP_STREAM_SETTINGS_CAPABILITY constant could break installs that override the capability via the property after construction. 3. The Settings 'Clean Orphaned Meta' field now calls Admin::is_running_auto_purge() once per render instead of twice. Extracted the field-building logic into a small helper so the state probe runs once and both the 'type' and 'desc' branches reuse the result. Refs XWPENG-28 * review: address second round of code review feedback 1. Settings::updated_option_ttl_remove_records() now triggers an immediate purge directly. Previously it relied on the legacy wp_stream_auto_purge action being hooked to purge_scheduled_action, which this PR severed. Without this fix, shortening the TTL did not take effect until the next 12h recurring tick. 2. TTL fallback uses isset() instead of empty(), so an explicit '0' set via CLI/SQL is no longer silently overridden to 30. Added an explicit short-circuit: a non-positive TTL bails out of the cycle entirely. The UI enforces min=1; the only paths to 0 are operator error, and bailing out (records stop being purged) is a less destructive failure mode than honoring it (records get wiped repeatedly every 12h). 3. Overlap guard now reuses Admin::is_running_auto_purge(), so a pending reaper also blocks a new chain. Previously only a pending batch action blocked the guard, leaving a small window between chain completion and reaper completion where a new chain could stack. Three new PHPUnit tests cover each behaviour. Refs XWPENG-28 * review: address third round of code review feedback - Move wp_stream_auto_purge BC action to after all bail-out checks so it fires only when a purge actually runs (was firing on every recurring tick regardless of whether work happened). - Extend is_running_auto_purge() to also check IN-PROGRESS actions via as_get_scheduled_actions() so the overlap guard cannot let a second chain stack against rows the running batch worker is still touching. - Settings TTL-shortened path now enqueues AUTO_PURGE_ACTION via as_enqueue_async_action() so the immediate purge serializes through Action Scheduler instead of bypassing the overlap guard with an inline call. Inline fallback retained for when AS is unavailable. - Render an admin notice for wp_stream_message=orphan_meta_cleanup_scheduled on both admin_notices and network_admin_notices so the post-redirect UX is actually visible (was a half-built feature: redirect happened, no notice rendered). - E2E wpEval(): switch from 'docker compose run --rm' to 'docker compose exec -T' to attach to the long-lived container (~3-5s saved per call) and surface failures via console.warn instead of silently swallowing errors. - PHPUnit: add coverage for both the BC-action-suppressed-on-bailout path and the in-progress-action overlap guard. Update the TTL-shortened test to assert the AS enqueue path. * fix(e2e): pass --user www-data to docker compose exec The previous quick-win switch from 'docker compose run --rm --user $(id -u)' to 'docker compose exec -T' dropped the user mapping. On local dev that happened to work because the wordpress container's default exec user is what the runtime image inherits; on CI it defaults to root and wp-cli refuses to run with: YIKES! It looks like you're running this as root. $(id -u) only worked on the host because UID 1000 mapped to www-data inside the container β that's not portable to the GitHub Actions runner (UID 1001). Pin the exec user explicitly to www-data, which owns the WordPress files inside the container. Failing run: actions/runs/26144103763 * refactor(e2e): use Playwright baseURL for admin URLs Set baseURL in playwright.config.js and replace absolute https://stream.wpenv.net references in every spec with relative paths. Also drops the docker-exec state seeding from admin-orphan-cleanup.spec.js so it matches the browser-only convention of the rest of the suite; the running-state UX it seeded is covered by PHPUnit. * review: address fourth round of code review feedback 1. wp_ajax_clean_orphan_meta() now uses is_running_auto_purge() for its idempotency check instead of as_has_scheduled_action(), which only probes PENDING. The new probe checks PENDING + RUNNING across the batch worker and reaper, matching the UI hide condition and closing the small CSRF/stale-URL window where a duplicate reaper could be enqueued while a chain is mid-flight. function_exists() guard updated to as_get_scheduled_actions accordingly. 2. Add explicit case 'none' in Settings::render_field() so the intent of the running-state UI swap (hide the value column, let the description carry the message) is captured in code instead of relying on the switch's implicit fall-through. Used by both the Reset Stream Database row and the new Clean Orphaned Meta row. 3. Expand auto_purge_batch() docblock to acknowledge the last_entry-based forward-progress trade-off: any eligible row that lands inside the already-touched ID range after that batch ran is skipped by the current chain and picked up on the next tick. Sources are bounded (dev seeders, importers, clock skew); steady-state logging is monotonic so this is a no-op for production traffic. 4. Changelog clarifies that the wp_stream_auto_purge BC action's semantics have changed: it used to fire on every WP-Cron tick regardless of whether work happened, now fires only after all bail-out checks pass. Integrations that relied on the legacy tick-rate semantics should switch to AUTO_PURGE_ACTION. 5. New PHPUnit test test_settings_ttl_shortened_via_option_update_enqueues_purge exercises the full hook wiring (update_option / update_site_option β Settings::updated_option_ttl_remove_records β AS enqueue) end-to- end. The existing unit test calls the method directly and would still pass if Network::__construct() lost its update_site_option_wp_stream_network hook registration. 6. New PHPUnit test test_ajax_clean_orphan_meta_denies_users_without_settings_cap covers the security boundary on the manual cleanup handler. Uses _handleAjax() so WP_Ajax_UnitTestCase's buffer machinery runs. 7. New PHPUnit test test_clean_orphan_meta_field_reflects_running_state asserts the integration between is_running_auto_purge() and the field-rendering decision in Settings::build_clean_orphan_meta_field(). Replaces the e2e specs removed in b4c8f28 for activation-race fragility with deterministic PHPUnit coverage. 8. New PHPUnit test test_purge_scheduled_action_scopes_to_current_blog_when_not_network_activated forces the multisite-not-network-activated branch via a Plugin stub and asserts the enqueued batch carries get_current_blog_id() rather than 0. The acceptance criterion ("per-site activations only purge the current blog") was only covered at the batch-worker layer; the routing decision in purge_scheduled_action() was untested. Refs XWPENG-28
* Skip async deletion AS query outside admin and memoise it `Admin::is_running_async_deletion()` is called from `Settings::get_fields()` and `Settings::get_deletion_warning()` to decide whether to render a "deletion in progress" warning on the Stream settings screen. Because `Settings::__construct` eagerly calls `get_options()` on `init`, and `get_options()` walks the field definition via `get_defaults()`, the AS query runs twice on every pageload β front-end included β even though the result is only ever consumed by admin UI. Short-circuit when `is_admin()` is false (the only callers are admin UI render paths) and memoise per request to collapse the duplicate within an admin pageload. Fixes #1884 * Lift AS gate into Settings; also skip auto-purge query on front-end The previous commit short-circuited Admin::is_running_async_deletion() outside admin and memoised it. That works for the immediate cost, but has two issues: 1. It changes the semantic contract of a public static method β `is_running_async_deletion()` no longer means "is async deletion running" but "is async deletion running AND we're in admin". Any non-Settings caller (custom CLI, REST endpoint, AS worker) gets a silently wrong answer. 2. The sibling probe Admin::is_running_auto_purge() has the same per-pageload cost (called from Settings::build_clean_orphan_meta_field on every front-end pageload via the init-time get_options walk), but cannot use the same fix: it has a non-admin caller β the recurring auto-purge callback at class-admin.php uses it as an overlap guard, running under Action Scheduler where is_admin() is false. Adding the short-circuit there would let parallel auto-purge chains stack. A process-static memo is also unsafe under AS workers that process multiple actions per PHP process. Move the gate to the UI layer instead. Both Settings fields that depend on AS state now skip the probe when not in admin, symmetrically: - Extract delete_all_records into build_delete_all_records_field(), mirroring the existing build_clean_orphan_meta_field() shape. - Gate both helpers on is_admin() before calling the AS probes. - Reuse the pre-computed deletion state inside one render by passing it to get_deletion_warning() (new optional parameter, backwards compatible). - Revert Admin::is_running_async_deletion() to a one-liner. The probe now reports the truth in all contexts; the "UI doesn't need this outside admin" knowledge lives where it belongs β in Settings. Query budget per pageload: Front-end: 2 AS queries -> 0 Admin / admin-ajax: 3 AS queries -> 2 (one async-deletion + one auto-purge) AS workers (overlap guard): unchanged β still reports truth. Refs #1884 * test(phpunit): cover async-deletion field state and warning helper Adds three PHPUnit tests for the lifted-into-Settings AS gate: - test_is_running_async_deletion_reflects_scheduled_state β closes a pre-existing gap; the probe had no direct unit coverage on develop. Mirrors test_is_running_auto_purge_reflects_chain_state. - test_delete_all_records_field_reflects_running_state β integration test for the running-state UI swap on the "Reset Stream Database" field. Asserts type=link <-> type=none and the warning text swap. Mirrors test_clean_orphan_meta_field_reflects_running_state. - test_get_deletion_warning_respects_precomputed_state β verifies the new optional parameter on get_deletion_warning(): when the caller passes the pre-computed state, the warning reflects the argument regardless of the real AS state. Guards against future regressions that bypass the cached value and re-query AS. All three pass on single-site and multisite. The is_admin()=false branch is not directly asserted because WP_ADMIN gets defined globally by earlier tests and cannot be undefined within the same process. The behavioural contract for that branch is covered by passing false explicitly to get_deletion_warning() in the third test. --------- Co-authored-by: Dion Hulse <dion@a8c.com> Co-authored-by: Utkarsh Patel <itismeutkarsh@gmail.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
* chore(release): prepare 4.2.0 Bump plugin version to 4.2.0 across stream.php, Plugin::VERSION, and readme.txt Stable tag. Promote changelog Unreleased section to 4.2.0 and add notable items (Abilities API + MCP, HTTPS dev env, Playwright E2E in CI, Node v24, jQuery v4). * chore(release): set 4.2.0 changelog date to May 28, 2026 * docs(changelog): note skipped AS queries on front-end pageloads Adds an entry for #1885 / #1884 to the 4.2.0 changelog under Bug Fixes: front-end pageloads no longer issue Action Scheduler queries from the delete_all_records and clean_orphan_meta settings fields. * chore(release): bump Tested up to 7.0 WordPress 7.0 is the current target release; the plugin has been smoke tested against it as part of the 4.2.0 release validation. Bump the readme.txt 'Tested up to' header so WP.org search and the plugin directory listing reflect the supported range.
bartoszgadomski
approved these changes
May 28, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Release 4.2.0. Merge
release/v4.2.0intomasterso the version tag and WP.org deploy land onmaster.Release Changelog
See changelog.md β 4.2.0 for the full list. Highlights:
stream/stream_metagrowth (XWPENG-28, Migrate auto-purge to Action Scheduler with batched deletionΒ #1882).Release Checklist
masterbranch.readme.txt.stream.php(4.2.0).Stable taginreadme.txt(4.2.0).classes/class-plugin.php(4.2.0).Pre-release verification
v4.2.0-rc.1published 2026-05-27, WP.org SVN dry-run succeeded (run).After this PR merges to
master, draft thev4.2.0GitHub release targetingmasterto trigger the WP.org deploy.