Modernize codebase: namespaces, autoloader, folder structure#306
Modernize codebase: namespaces, autoloader, folder structure#306
Conversation
Refactor the entire plugin to use PHP namespaces, a PSR-4-ish autoloader, and a proper folder structure following the wordpress-activitypub pattern. - Add `IndieAuth` root namespace with autoloader - Organize classes into `rest/`, `token/`, `scope/`, `ticket/`, `wp-admin/` - REST controllers extend WP_REST_Controller with $namespace/$rest_base - Singleton main class (IndieAuth\IndieAuth) with get_instance()/init() - Centralized hook registration in main class - Settings refactored with Settings_Fields class and clean template - Backward-compatible global API functions in functions-api.php - Tests restructured to mirror source layout with Test_ prefix
- Add doc comments to backward-compat wrapper functions - Fix alignment and spacing issues (auto-fixed by phpcbf)
|
@dshanske I known that this is a quite big rework, but I think it modernizes the structure, so that we have that from the table for quite some time ;) Happy to have a session to merge that with you! |
All namespaced utility functions (find_rels, url_to_author, pkce_verifier, base64_urlencode, normalize_url, build_url, add_query_params_to_url, rest_is_valid_url, get_oauth_error, is_oauth_error, wp_error_to_oauth_response) now have global wrappers in functions-api.php.
There was a problem hiding this comment.
Pull request overview
Refactors the IndieAuth WordPress plugin to a namespaced, PSR-4-style architecture with a custom autoloader, reorganized folder structure, and updated admin/settings + REST controller wiring to better match patterns used in related plugins (e.g., ActivityPub/Webmention).
Changes:
- Introduces
IndieAuth\IndieAuthsingleton bootstrap,IndieAuth\Autoloader, and namespaced helpers + backward-compat global wrapper functions. - Migrates REST endpoints to
\WP_REST_Controller-based controllers, with shared token handling via aToken_Managementtrait and centralized header output. - Reworks admin/settings UI into dedicated WP_Admin classes and updates PHPUnit config + test class names/structure.
Reviewed changes
Copilot reviewed 51 out of 51 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/phpunit/tests/includes/token/class-test-tokens.php | Renames test class and adds namespaced imports for updated token classes. |
| tests/phpunit/tests/includes/rest/class-test-userinfo-controller.php | Renames test class and updates imports to match namespaced implementation. |
| tests/phpunit/tests/includes/rest/class-test-token-controller.php | Renames test class and updates imports to match namespaced implementation. |
| tests/phpunit/tests/includes/rest/class-test-ticket-controller.php | Renames test class to match new controller naming. |
| tests/phpunit/tests/includes/rest/class-test-revocation-controller.php | Renames test class and updates imports to match namespaced implementation. |
| tests/phpunit/tests/includes/rest/class-test-introspection-controller.php | Renames test class and updates imports to match namespaced implementation. |
| tests/phpunit/tests/includes/rest/class-test-fedcm-controller.php | Renames test class and adds namespaced imports for updated token classes. |
| tests/phpunit/tests/includes/rest/class-test-authorization-controller.php | Renames test class and updates imports to match namespaced implementation. |
| tests/phpunit/tests/includes/class-test-functions.php | Renames functions test class to match new naming convention. |
| tests/phpunit/tests/includes/class-test-discovery.php | Renames discovery test class to match new naming convention. |
| tests/phpunit/tests/includes/class-test-authorize.php | Renames authorize test class and updates imports for namespaced classes. |
| templates/indieauth-settings.php | Simplifies settings template to rely on registered sections/fields + standard WP settings rendering. |
| phpunit.xml.dist | Updates PHPUnit configuration to use modern <coverage> include format. |
| package.json | Pins @wordpress/env devDependency version for local tooling. |
| indieauth.php | Adds IndieAuth namespace bootstrap, defines plugin constants, registers autoloader, and initializes singleton. |
| includes/wp-admin/class-token-ui.php | Adds namespaced token management UI page + token creation and client discovery actions. |
| includes/wp-admin/class-token-list-table.php | Namespaces token list table and updates references to namespaced client/token classes. |
| includes/wp-admin/class-settings-fields.php | Adds class to register and render settings sections/fields (ActivityPub-style pattern). |
| includes/wp-admin/class-external-token-list-table.php | Namespaces external token list table and updates strings/utility calls. |
| includes/wp-admin/class-admin.php | Adds namespaced admin bootstrap for settings registration, site health tests, menu entries, and auth diagnostics. |
| includes/token/class-user.php | Namespaces token user storage (IndieAuth\Token\User) and updates WP function calls for namespaced context. |
| includes/token/class-transient.php | Namespaces transient token storage (IndieAuth\Token\Transient) and updates WP function calls. |
| includes/token/class-generic.php | Namespaces base token class (IndieAuth\Token\Generic) and updates WP function calls. |
| includes/ticket/class-external-user-token.php | Namespaces external token storage/refresh/revoke/verify logic for ticket endpoint. |
| includes/ticket/class-external-token-page.php | Updates external token admin page wiring to new main class + new list-table class name. |
| includes/scope/class-scope.php | Namespaces scope definition class (IndieAuth\Scope\Scope) and updates filter calls. |
| includes/rest/trait-token-management.php | Introduces shared token management trait for REST controllers (token + refresh token storage). |
| includes/rest/class-userinfo-controller.php | Migrates userinfo endpoint into a \WP_REST_Controller implementation using the shared token trait. |
| includes/rest/class-token-controller.php | Migrates token endpoint into a \WP_REST_Controller implementation using the shared token trait. |
| includes/rest/class-ticket-controller.php | Migrates ticket endpoint into a \WP_REST_Controller implementation (conditional feature flag). |
| includes/rest/class-revocation-controller.php | Migrates revocation endpoint into a \WP_REST_Controller implementation using the shared token trait. |
| includes/rest/class-metadata-controller.php | Adds REST controller for metadata endpoint and centralized Link: header injection on REST responses. |
| includes/rest/class-introspection-controller.php | Migrates introspection endpoint into a \WP_REST_Controller implementation using the shared token trait. |
| includes/rest/class-fedcm-controller.php | Migrates FedCM endpoint controller to namespaced class and updates route registration + script enqueueing. |
| includes/functions.php | Moves helper functions into IndieAuth namespace and adds new OAuth error helpers. |
| includes/functions-api.php | Adds global wrapper functions to preserve backward compatibility for external integrations. |
| includes/class-webidentity.php | Namespaces well-known web-identity handler and wires it to the new FedCM controller config endpoint. |
| includes/class-web-signin.php | Namespaces Web Sign-In implementation and updates token state handling to new namespaced token classes. |
| includes/class-scopes.php | Namespaces scopes registry (IndieAuth\Scopes) and updates internal scope registration. |
| includes/class-oauth-response.php | Namespaces OAuth response class (IndieAuth\OAuth_Response) and removes old global helper duplicates. |
| includes/class-indieauth.php | Adds main singleton orchestrator: registers hooks, controllers, admin wiring, and centralized header output. |
| includes/class-indieauth-token-ui.php | Removes legacy non-namespaced token UI class (replaced by includes/wp-admin/class-token-ui.php). |
| includes/class-indieauth-metadata-endpoint.php | Removes legacy non-namespaced metadata endpoint class (replaced by REST controller). |
| includes/class-indieauth-admin.php | Removes legacy non-namespaced admin class (replaced by includes/wp-admin/class-admin.php). |
| includes/class-debug.php | Namespaces debug tooling class and updates hooks + logging calls. |
| includes/class-client.php | Namespaces IndieAuth client (IndieAuth\Client) and updates remote request + error handling to new OAuth response class. |
| includes/class-client-taxonomy.php | Namespaces client taxonomy (IndieAuth\Client_Taxonomy) and updates hooks/meta registration calls. |
| includes/class-client-discovery.php | Namespaces client discovery (IndieAuth\Client_Discovery) and updates parsing + logging calls. |
| includes/class-autoloader.php | Adds custom WordPress-friendly autoloader to map namespace/class names to class-*.php files. |
| includes/class-authorize.php | Namespaces authorization handler (IndieAuth\Authorize) and updates error handling + WP hook integration. |
Comments suppressed due to low confidence (12)
includes/rest/trait-token-management.php:40
- Refresh tokens are initialized with the prefix
_indieauth_refresh_, but the scheduled cleanup inIndieAuth::expires()iterates_indieauth_refresh_token_. This mismatch means refresh tokens won't be cleaned up. Use a single consistent prefix everywhere (trait + cleanup).
includes/ticket/class-external-user-token.php:92 expire_tokens()is currently broken:array_key_exists( 'refresh_token' )is missing the array parameter, and the code mutates$token[...]/unset( $token[...] )instead of updating the$tokensarray being saved. This will produce warnings/fatal errors and prevent token expiration from working. Fix the condition to check the current$tokenarray and update/unset entries in$tokensbefore saving.
includes/ticket/class-external-user-token.php:166get()reuses$keyas the foreach index and then callsin_array( $token['access_token'], $key, true )where$keyis a string. This will never match and may emit warnings. Compare the requested token (parameter) against each stored token'saccess_tokenwithout overwriting the parameter name.
includes/ticket/class-external-user-token.php:216- In
update(),$foundcan legitimately be0(first element), butif ( ! $found )treats that as "not found" and will append a duplicate token instead of updating. Use a strict check likenull === $found(orfalse === $found) for the sentinel value.
includes/class-client-discovery.php:97 - The IP blocking logic is inverted:
if ( $ip && ! in_array( $ip, $donotfetch, true ) ) { return; }prevents fetching most IPs but still allows fetching loopback addresses like127.0.0.1(SSRF risk). If the intent is to block local/private IPs, return when$ipis in the disallowed list (and consider expanding to RFC1918 ranges).
includes/class-client-discovery.php:103 __construct()andparse()callself::parse()/self::fetch()even though these are non-static methods. This triggers deprecation notices (and may break under stricter settings). Call them as instance methods ($this->parse(...),$this->fetch(...)) instead.
includes/class-client-discovery.php:196- Content-Type checks are too strict: comparing header to
'application/json'or'text/html'will fail for common values likeapplication/json; charset=utf-8. Use a prefix/substring check (e.g.,str_starts_with) or parse the mime type before branching.
includes/wp-admin/class-token-list-table.php:249 column_client_icon()outputs<img src="...">using the stored client icon URL without escaping. Since client icon URLs can originate from remote discovery, this can allow attribute injection/XSS. Escape withesc_url()(and consider addingalttext).
includes/ticket/class-external-user-token.php:115- When discovering token endpoints,
find_rels($token['resource'], 'token_endpoint')returns an array of rels (not a single URL). Passing that array intowp_http_validate_url()will always fail, breaking refresh/revoke/verify flows. Requestarray( 'token_endpoint' )and then read the'token_endpoint'key from the result (or adjustfind_relsto accept a string).
includes/ticket/class-external-user-token.php:314 verify()sends the Authorization header as'Bearer: ' . $token, but the bearer token scheme isBearer <token>(no colon). This will cause verification requests to fail against conforming servers.
includes/rest/trait-token-management.php:40get_token()(later in this trait) overwrites the$tokenparameter with the access-token lookup result, then passes that (false/array) into$this->refresh_tokens->get(...). When$typeis null, refresh tokens can never be found. Preserve the original token ID (e.g.,$id = $token) for both lookups.
includes/rest/class-token-controller.php:187- This OAuth error response omits the HTTP status code, so
OAuth_Responsewill default to200. For an invalid request like providing bothactionandgrant_type, the response should use a 4xx status (typically 400) to match OAuth2/IndieAuth expectations and to avoid clients treating it as success.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| $scopes = \sanitize_text_field( $scopes ); | ||
| $expires = isset( $_REQUEST['expires_in'] ) ? \absint( $_REQUEST['expires_in'] ) : 0; | ||
| $token = self::generate_local_token( $client_name, $scopes, $expires ); | ||
| ?> |
There was a problem hiding this comment.
new_token() calls self::generate_local_token(...), but generate_local_token() is defined as a non-static private method. This will trigger a fatal error at runtime. Make generate_local_token() static or call it via $this->generate_local_token(...).
| <input type="hidden" name="action" id="action" value="indieauth_newtoken" /> | ||
| <h4><?php \esc_html_e( 'Scopes', 'indieauth' ); ?></h4> | ||
| <?php echo \esc_html( $this->scopes() ); ?> | ||
| <p><label><?php \esc_html_e( 'Set Expiry Time in Seconds(0 to disable)', 'indieauth' ); ?></label> |
There was a problem hiding this comment.
scopes() echoes HTML directly and returns void, but options_form() wraps the call in echo esc_html( $this->scopes() );. This is misleading and can result in the markup being output without the intended escaping/sanitization flow. Call $this->scopes(); directly (or refactor scopes() to return a string and escape with wp_kses).
| public function site_health_header_test() { | ||
| $result = array( | ||
| 'label' => \__( 'Authorization Header Passed', 'indieauth' ), | ||
| 'status' => 'good', | ||
| 'badge' => array( | ||
| 'label' => \__( 'IndieAuth', 'indieauth' ), | ||
| 'color' => 'green', | ||
| ), | ||
| 'description' => \sprintf( | ||
| '<p>%s</p>', | ||
| \__( 'Your hosting provider allows authorization headers to pass so IndieAuth should work', 'indieauth' ) | ||
| ), | ||
| 'actions' => '', | ||
| 'test' => 'indieauth_headers', | ||
| ); | ||
|
|
||
| if ( ! self::test_auth() ) { | ||
| $result['status'] = 'critical'; | ||
| $result['badge']['color'] = 'red'; | ||
| $result['label'] = \__( 'Authorization Test Failed', 'indieauth' ); | ||
| \ob_start(); | ||
| include INDIEAUTH_PLUGIN_DIR . 'templates/authdiagfail.php'; | ||
| $result['description'] = \ob_get_contents(); | ||
| \ob_end_clean(); | ||
| $result['actions'] = \sprintf( '<a href="%1$s" >%2$s</a>', 'https://github.com/indieweb/wordpress-indieauth/issues', \__( 'If contacting your hosting provider does not work you can open an issue on GitHub and we will try to assist', 'indieauth' ) ); | ||
| } |
There was a problem hiding this comment.
site_health_header_test() treats any non-false return from test_auth() as a pass, but test_auth() returns a diagnostic message string even on failure (see login_form_authdiag() which always returns a non-empty message). This will cause the Site Health test to report "good" even when Authorization headers are stripped. Consider returning a structured JSON payload with a boolean success flag (or using HTTP status codes) and have test_auth() return true/false separately from the message.
| public function general_settings() { | ||
| if ( \class_exists( 'IndieWeb_Plugin' ) ) { | ||
| $path = 'admin.php?page=indieauth'; | ||
| } else { | ||
| $path = 'options-general.php?page=indieauth'; | ||
| } | ||
| \printf( \__( 'Based on your feedback and to improve the user experience, we decided to move the settings to a separate <a href="%1$s">settings-page</a>.', 'indieauth' ), $path ); // phpcs:ignore | ||
| } |
There was a problem hiding this comment.
The settings link is printed into an <a href> via printf() without escaping the URL. Even though $path is internal, it should be wrapped in esc_url() (and ideally generated via admin_url()/menu_page_url()) to avoid accidental injection and to match WP escaping expectations.
| $t->get_all(); | ||
| $t = new Token_User( '_indieauth_code_' ); | ||
| $t->get_all(); | ||
| $t = new Token_User( '_indieauth_refresh_token_' ); |
There was a problem hiding this comment.
Refresh tokens are initialized/stored with the _indieauth_refresh_ prefix (see Token_Management::init_tokens()), but cleanup here uses _indieauth_refresh_token_. This inconsistency will prevent refresh tokens from being expired/cleaned up by the scheduled task. Align the prefix with the rest of the codebase (or centralize it in a constant).
| $t = new Token_User( '_indieauth_refresh_token_' ); | |
| $t = new Token_User( '_indieauth_refresh_' ); |
- Defer builtin scope registration to init hook via Scopes::init() while creating the Scopes instance early so map_meta_cap filter works - Revert phpunit.xml.dist to <filter><whitelist> format for PHPUnit 8
Update CI matrix, composer.json, and PHPCS config to match the Requires PHP: 7.4 already declared in the plugin header.
Move Authorize, Scopes, Web_Signin, and other component creation to the init hook (priority 2) matching the original plugin behavior. This fixes test failures where creating a new Authorize instance in tests conflicted with an already-registered instance from early plugin load.
Summary
Refactors the entire plugin to use PHP namespaces, a PSR-4 autoloader, and organized folder structure — following the patterns established in wordpress-activitypub and wordpress-webmention.
Changes
Architecture
IndieAuthwith custom autoloader (same as ActivityPub/Webmention)IndieAuth\IndieAuthwithget_instance()/init()patternindieauth-metadata,authorization_endpoint,token_endpointFolder structure
REST controllers
\WP_REST_Controllerwith$namespace/$rest_basepropertiesget_endpoint()uses$this->namespace . '/' . $this->rest_baseinstead of hardcoded URLsToken_ManagementtraitSettings
Settings_Fieldsclass registers sections and fields with render callbackssettings_fields(),do_settings_sections(),submit_button()Backward compatibility
functions-api.phpprovides global wrapper functions (indieauth_get_scopes(),indieauth_get_response(), etc.)usestatements to minimize changesTests
rest/,token/)Test_prefix with snake_case<coverage>formatOther
_load_textdomain_just_in_time)plugin_dir_path(__DIR__)replaced withINDIEAUTH_PLUGIN_DIRconstantTest plan
<link>headers appear on front page (indieauth-metadata, authorization_endpoint, token_endpoint)composer test:wp-envfor PHPUnit testscomposer lintfor PHPCS compliance