Refactor: Extract Application from the shared actor pipeline#2926
Refactor: Extract Application from the shared actor pipeline#2926pfefferle wants to merge 27 commits into
Conversation
The Application actor is not a real actor — it cannot be followed, addressed, or interacted with. It exists only as a JSON-LD document and a signing identity for outbound HTTP GET requests. This introduces a standalone `Application` utility class with static methods for identity (`get_id()`, `get_url()`, `get_webfinger()`) and key management (`get_key_id()`, `get_public_key()`, `get_private_key()`). - Remove `APPLICATION_USER_ID` (-1) from `Actors` collection - Remove Application cases from `user_can_activitypub()`, Follow handler, Mailer, Outbox, CLI, Health Check, and Stream connector - Make `Application_Controller` delegate to the new `Application` class - Deprecate `Model\Application` (kept for backward compatibility) - Rename option from `activitypub_keypair_for_-1` to `activitypub_application_keypair` with migration - Add self-contained WebFinger discovery via `webfinger_data` filter, handling `acct:`, `/@application`, and REST API URL patterns
- Add missing backslash prefix on \_deprecated_class() call. - Fix WP_Query parameter from 'number' to 'posts_per_page'. - Guard strtotime() against false when post_date_gmt is empty. - Add 'invisible' property to Application REST schema. - Use pretty URL for Application 'url' field. - Add @SInCE unreleased tags to new Application class. - Add comment explaining @Application rewrite rule ordering.
- Remove Application user (ID -1) from Following test. - Add backslash prefix to wp_cache_flush() in Migration. - Add backslash prefix to is_string() in Application.
Run the Application WebFinger filter at priority 2 (after Integration\Webfinger::add_pseudo_user_discovery at priority 1) so the Application JRD is not overwritten by a WP_Error. Register a /application/outbox route returning an empty OrderedCollection to back the URL already advertised in the Application actor document.
- Backslash-prefix `is_string()` calls in Application::generate_key_pair(). - Use `update_option()` instead of `add_option()` to prevent keypair race. - Use Application::get_webfinger() in health check so it works regardless of actor mode or authentication context. - Add `@since unreleased` to generate_key_pair(), check_legacy_key_pair(), and Application_Controller::get_item(). - Add negative test for `acct:application@host` resolving to WP_Error in the Actors collection.
- Probe the Application endpoint (GET-readable) instead of the shared inbox (POST-only) in is_rest_api_accessible() health check. - Guard strrchr() return in is_application_resource() to avoid passing false to substr() (PHP 8 deprecation). - Fix migration docblock: clarify that legacy separate key options are migrated lazily, not by this function. Add missing @SInCE tag.
…lication-from-actor-system # Conflicts: # includes/class-migration.php # includes/class-router.php # tests/phpunit/tests/includes/class-test-migration.php # tests/phpunit/tests/includes/collection/class-test-following.php
- Restore Actors::APPLICATION_USER_ID as a deprecated back-compat constant (class-stats-image.php still references it; removing it caused a fatal). - Cover the activitypub_keypair_for_-1 option in check_legacy_key_pair() so the Application signing key is never regenerated when get_keypair() runs before the keypair migration. - Add changelog entry.
- following: drop the stray $accept_5 block (no $outbox_item_5 setup; left over from the trunk merge) that caused an undefined-variable error and phpcs warnings. - feature-request: resolve the Application id via \Activitypub\Application::get_id() instead of Actors::get_by_id( -1 ) (which now returns WP_Error → get_id() fatal). - interaction-policy: declare the expected Model\Application deprecation notice.
- stats-image: resolve the Application webfinger via the new \Activitypub\Application::get_webfinger() instead of instantiating the deprecated Model\Application (which emitted a deprecation notice). - application: move class constants to the top of the class. - migration: drop the orphaned activitypub_keypair_for_-1 row when the destination keypair option already exists (early get_keypair() read).
- Reserve the 'application' username in Actors::get_id_by_username() so the advertised /@Application handle can't be hijacked by a local user named 'application' during shared-inbox recipient resolution (the special-case was dropped when the Application left the Actors collection). - Re-apply the activitypub_activity_object_array and activitypub_activity_application_object_array filters in the /application endpoint so integrations that add actor fields via those hooks still apply.
…lication-from-actor-system
The Application authors no posts and is not in Actors::get_all_ids(), so the annual stats image always bails with no_stats before reaching the APPLICATION_USER_ID webfinger branch. Drop the dead branch and the now-unused Application import.
The Application is no longer an actor and -1 must not resolve through the actor pipeline. Drop the (unreleased) compatibility constant entirely; get_by_id( -1 ) already returns a 404 with no special-casing. Adds a test that locks in the hard 404 so a back-compat shim is not silently reintroduced.
…CLI delete, simplify get_url - Replace wp_cache_flush() in migrate_application_keypair_option() with targeted option cache deletes. - Reject invalid (negative) actor IDs in 'wp activitypub actor delete' with a friendly error. - Return the canonical Application id from Application::get_url().
…ation Reuse Webfinger::get_identifier_and_host() in is_application_resource() instead of re-deriving the scheme/path/host parsing from Actors::get_id_by_resource(). This also fixes a host-confusion bug where any URL ending in /@Application matched regardless of host. Adds direct test coverage for the matcher. Move the legacy Application key-pair fallback out of the runtime get_keypair() path into a dedicated migrate_legacy_application_keys() migration and drop check_legacy_key_pair(), so reads no longer carry migration logic.
jeherve
left a comment
There was a problem hiding this comment.
Verify https://example.com/@application redirects/resolves to the Application endpoint.
The json gets served, but the redirect doesn't happen for me.
On an existing installation, verify the Application keypair is preserved after upgrade (keys should not change).
They do change for me when I switch from one branch to the other.
Everything else lines up and looks good for me right now.
Reading the key only from the new option and relying on the migration to rename the legacy one is timing-dependent: if get_keypair() runs before the migration (or the version doesn't change, as when switching branches), it generated a fresh key and the migration then dropped the legacy row. Restore check_legacy_key_pair() in get_keypair() so the existing key is recovered on read regardless of migration timing, and drop the now-redundant migrate_legacy_application_keys() migration.
There was a problem hiding this comment.
Pull request overview
This PR refactors the plugin’s “Application actor” so it no longer flows through the shared actor pipeline as a virtual user (-1). Instead, it becomes a standalone utility class (\Activitypub\Application) for signing/key management plus a dedicated REST controller for serving the Application actor JSON-LD (including an empty outbox).
Changes:
- Introduces
\Activitypub\Applicationand a self-contained REST controller with/applicationand/application/outboxroutes, plus a dedicated@applicationrewrite rule. - Removes the old
Actors::APPLICATION_USER_IDpath and updates callers (HTTP signing, health check, stream integration, handlers, mailer, etc.) accordingly. - Adds/updates migrations and tests, including renaming the Application keypair option and adding WebFinger coverage.
Reviewed changes
Copilot reviewed 27 out of 27 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/phpunit/tests/integration/class-test-webfinger.php | Adds coverage ensuring Application WebFinger discovery overrides pseudo-user errors; updates filter cleanup. |
| tests/phpunit/tests/includes/rest/class-test-application-controller.php | Updates Application REST endpoint assertions and adds outbox + key management tests. |
| tests/phpunit/tests/includes/model/class-test-interaction-policy.php | Adjusts for deprecated Model\Application by expecting deprecation notice. |
| tests/phpunit/tests/includes/handler/class-test-follow.php | Removes obsolete follow test case targeting the former application actor ID. |
| tests/phpunit/tests/includes/handler/class-test-feature-request.php | Updates feature-request handler test to target Application::get_id() instead of actor -1. |
| tests/phpunit/tests/includes/collection/class-test-following.php | Removes application-actor follow/unfollow scenarios. |
| tests/phpunit/tests/includes/collection/class-test-actors.php | Ensures Actors::get_by_id(-1) and acct:application@host no longer resolve via Actors collection. |
| tests/phpunit/tests/includes/class-test-migration.php | Updates migration tests to use -1 literals and adds tests for renaming the application keypair option. |
| tests/phpunit/tests/includes/class-test-application.php | Adds unit tests for recognizing Application resources (acct, handle, pretty URL, REST ID). |
| integration/stream/class-connector.php | Removes display handling for the removed “Application User” actor ID. |
| includes/wp-admin/class-health-check.php | Switches health checks from Actors(-1) to Application/WebFinger + dedicated REST URL. |
| includes/rest/class-application-controller.php | Implements the Application actor document and empty outbox routes; adds schema fields. |
| includes/model/class-application.php | Deprecates the old model, redirects key-related behavior to the new utility, updates inbox/outbox URLs, and adjusts query args. |
| includes/handler/class-follow.php | Removes special-case rejection for the former application actor ID. |
| includes/functions-user.php | Removes “application user always enabled” logic tied to APPLICATION_USER_ID. |
| includes/collection/class-outbox.php | Removes support for resolving outbox actor as “application” via Actors collection. |
| includes/collection/class-actors.php | Removes APPLICATION_USER_ID constant; reserves application username and prevents resolving it to a normal user/actor. |
| includes/cli/class-actor-command.php | Updates delete guardrails so negative IDs (including former -1) are treated as non-deletable/invalid. |
| includes/class-router.php | Adds a dedicated @application rewrite rule ahead of the generic @username rule. |
| includes/class-query.php | Updates documentation comment to reflect only the blog actor remains a virtual object. |
| includes/class-migration.php | Adds migration to rename activitypub_keypair_for_-1 to activitypub_application_keypair and flush rewrite rules. |
| includes/class-mailer.php | Removes special-case “don’t email the application user” logic tied to APPLICATION_USER_ID. |
| includes/class-http.php | Switches outbound HTTP signing identity from Actors(-1) to Application key utilities. |
| includes/class-application.php | Adds new \Activitypub\Application utility: WebFinger integration + keypair/legacy fallback + resource matching. |
| activitypub.php | Hooks Application::init() on init. |
| .github/changelog/refactor-extract-application | Adds changelog entry describing the refactor. |
|
@jeherve Thanks for testing! Two follow-ups: Keypair changing: Fixed in the latest push. There's now a runtime legacy fallback that reads the old
Mind giving the keypair path another spin after the latest commits? |
- Fix fatal in the stats block: remove the deleted APPLICATION_USER_ID constant reference from src/stats/render.php (and the build copy). - Retire legacy 'application' outbox items in Outbox::get_actor() instead of misattributing them to the Blog actor. - Reject Follows aimed at the Application actor: new Follow::reject_application_follow() sends a direct Reject signed with the Application key (Http::post() now accepts null for that). - Reserve the 'application' blog identifier in Sanitize::blog_identifier(). - Deduplicate get_icon()/get_published() into the Application class and tune the published-date WP_Query. - Pass a hydrated Generic_Object to the activitypub_activity_*_array filters instead of null. - Use Application::get_id() in the Health Check instead of an inline namespace reference. - Declare the changelog entry as major: the removed -1 actor paths are a compatibility break.
Fixes #
Proposed changes:
\Activitypub\Application) and self-contained REST controller.-1flowing throughActors::get_by_id(). It exists only as a JSON-LD document and HTTP signing identity.Actors::APPLICATION_USER_IDconstant and all-1guard clauses from Mailer, Follow handler, CLI, Health Check, Outbox, Stream integration, etc.activitypub_keypair_for_-1toactivitypub_application_keypair, with legacy fallback.\Activitypub\Model\Applicationwith_deprecated_class()notice pointing to the new classes.@applicationrewrite rule so the pretty URL resolves outside the actor routing system.WP_Queryparameter (number→posts_per_page) and guardstrtotime()against emptypost_date_gmt.Follow::reject_application_follow()sends a directly-delivered Reject signed with the Application key (Http::post()acceptsnullas user ID for that), so remote follow requests don't sit "pending" forever.applicationoutbox items instead of misattributing them to the Blog actor (they sharepost_author0).applicationidentifier in the blog-identifier sanitizer to avoid handle collisions with the Application actor.get_icon()/get_published()into theApplicationutility class (controller and deprecated model delegate) and tune the published-dateWP_Query.Generic_Objectto theactivitypub_activity_object_arrayfilters instead ofnull, keeping the documented filter contract.APPLICATION_USER_IDconstant.Why handle the Application separately?
This is the first step of a larger effort to move the local actor model off magic IDs (
0for the blog actor,-1for the application actor) and onto real, WordPress-native identity. The blog/group actor and the application actor need different treatments, and this PR settles the easy half so the harder blog-actor work can focus on a single problem.The application actor has no actor surface to preserve:
post_authorrows (it authors no content),It exists only as a JSON-LD document and an HTTP-signing identity for signed fetches / secure mode. Because there is nothing to migrate and nothing that needs a
wp_usersrow, the cheapest correct move is to take it out of the actor abstraction entirely (this PR) rather than give it a real user or keep it as a resolver alias.The blog/group actor is the opposite case and is intentionally not touched here: it owns content (
post_author), followers, inbox entries, images, and external/actors/0/...URLs that must stay resolvable. That actor is the subject of separate work and will move toward a realWP_Userwith0kept only as a compatibility alias. Rule of thumb established by this PR: extraction for identities with no actor surface (application); alias-then-real-user for identities that own content and must stay resolvable (blog).Other information:
Testing instructions:
https://example.com/wp-json/activitypub/1.0/applicationreturns a valid Application actor withpublicKey,inbox,outbox, and proper@context.https://example.com/@applicationserves the Application actor JSON directly (an internal rewrite to the/wp-json/activitypub/1.0/applicationendpoint; it resolves in place, it does not issue an HTTP redirect).acct:application@example.comreturns correct data.application@example.com— the follow request should be explicitly rejected instead of staying "pending".applicationin the settings — it should be refused as reserved.Changelog entry
Changelog Entry Details
Significance
Type
Message
Improve the internal handling of the Application actor used for server-to-server requests.