Skip to content

Modernize codebase: namespaces, autoloader, folder structure#306

Open
pfefferle wants to merge 6 commits intotrunkfrom
feature/modernize-namespaces
Open

Modernize codebase: namespaces, autoloader, folder structure#306
pfefferle wants to merge 6 commits intotrunkfrom
feature/modernize-namespaces

Conversation

@pfefferle
Copy link
Copy Markdown
Member

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

  • Root namespace IndieAuth with custom autoloader (same as ActivityPub/Webmention)
  • Singleton main class IndieAuth\IndieAuth with get_instance()/init() pattern
  • Centralized hook registration — controllers are lightweight, all hooks registered in main class
  • Centralized HTTP/HTML header output for indieauth-metadata, authorization_endpoint, token_endpoint

Folder structure

includes/
├── class-autoloader.php          # PSR-4 autoloader
├── class-indieauth.php           # Main singleton loader
├── class-authorize.php           # Token verification
├── class-client.php              # IndieAuth client
├── class-client-discovery.php    # Client URL discovery
├── class-client-taxonomy.php     # Client taxonomy
├── class-debug.php               # Debug tools
├── class-oauth-response.php      # OAuth error response
├── class-scopes.php              # Scope registry
├── class-web-signin.php          # Login form integration
├── class-webidentity.php         # FedCM well-known handler
├── functions.php                 # Namespaced helpers
├── functions-api.php             # Backward-compat global wrappers
├── rest/                         # REST controllers (WP_REST_Controller)
├── token/                        # Token storage classes
├── scope/                        # Scope definition
├── ticket/                       # Experimental ticket endpoint
└── wp-admin/                     # Admin UI, settings, list tables

REST controllers

  • All extend \WP_REST_Controller with $namespace/$rest_base properties
  • get_endpoint() uses $this->namespace . '/' . $this->rest_base instead of hardcoded URLs
  • Shared token management via Token_Management trait
  • Non-REST code (HTML headers, login forms, rewrite rules) moved out of controllers

Settings

  • New Settings_Fields class registers sections and fields with render callbacks
  • Template only calls settings_fields(), do_settings_sections(), submit_button()
  • Settings registration separated from field rendering (ActivityPub pattern)

Backward compatibility

  • functions-api.php provides global wrapper functions (indieauth_get_scopes(), indieauth_get_response(), etc.)
  • Tests use aliased use statements to minimize changes

Tests

  • Restructured to mirror source layout (rest/, token/)
  • Class names use Test_ prefix with snake_case
  • PHPUnit config updated to modern <coverage> format

Other

  • Site health badges use red color when failing
  • Fixed early translation loading issue (_load_textdomain_just_in_time)
  • All plugin_dir_path(__DIR__) replaced with INDIEAUTH_PLUGIN_DIR constant

Test plan

  • Verify plugin activates without errors
  • Check IndieAuth settings page renders correctly
  • Verify <link> headers appear on front page (indieauth-metadata, authorization_endpoint, token_endpoint)
  • Test authorization flow with an IndieAuth client
  • Run composer test:wp-env for PHPUnit tests
  • Run composer lint for PHPCS compliance
  • Verify FedCM endpoints respond correctly
  • Test web sign-in on login form

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
@pfefferle pfefferle requested review from Copilot and dshanske April 1, 2026 09:13
- Add doc comments to backward-compat wrapper functions
- Fix alignment and spacing issues (auto-fixed by phpcbf)
@pfefferle
Copy link
Copy Markdown
Member Author

@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.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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\IndieAuth singleton 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 a Token_Management trait 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 in IndieAuth::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 $tokens array being saved. This will produce warnings/fatal errors and prevent token expiration from working. Fix the condition to check the current $token array and update/unset entries in $tokens before saving.
    includes/ticket/class-external-user-token.php:166
  • get() reuses $key as the foreach index and then calls in_array( $token['access_token'], $key, true ) where $key is a string. This will never match and may emit warnings. Compare the requested token (parameter) against each stored token's access_token without overwriting the parameter name.
    includes/ticket/class-external-user-token.php:216
  • In update(), $found can legitimately be 0 (first element), but if ( ! $found ) treats that as "not found" and will append a duplicate token instead of updating. Use a strict check like null === $found (or false === $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 like 127.0.0.1 (SSRF risk). If the intent is to block local/private IPs, return when $ip is in the disallowed list (and consider expanding to RFC1918 ranges).
    includes/class-client-discovery.php:103
  • __construct() and parse() call self::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 like application/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 with esc_url() (and consider adding alt text).
    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 into wp_http_validate_url() will always fail, breaking refresh/revoke/verify flows. Request array( 'token_endpoint' ) and then read the 'token_endpoint' key from the result (or adjust find_rels to accept a string).
    includes/ticket/class-external-user-token.php:314
  • verify() sends the Authorization header as 'Bearer: ' . $token, but the bearer token scheme is Bearer <token> (no colon). This will cause verification requests to fail against conforming servers.
    includes/rest/trait-token-management.php:40
  • get_token() (later in this trait) overwrites the $token parameter with the access-token lookup result, then passes that (false/array) into $this->refresh_tokens->get(...). When $type is 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_Response will default to 200. For an invalid request like providing both action and grant_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.

Comment on lines +104 to +107
$scopes = \sanitize_text_field( $scopes );
$expires = isset( $_REQUEST['expires_in'] ) ? \absint( $_REQUEST['expires_in'] ) : 0;
$token = self::generate_local_token( $client_name, $scopes, $expires );
?>
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

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(...).

Copilot uses AI. Check for mistakes.
Comment on lines +175 to +178
<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>
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

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).

Copilot uses AI. Check for mistakes.
Comment on lines +180 to +205
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' ) );
}
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +78 to +85
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
}
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
$t->get_all();
$t = new Token_User( '_indieauth_code_' );
$t->get_all();
$t = new Token_User( '_indieauth_refresh_token_' );
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

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).

Suggested change
$t = new Token_User( '_indieauth_refresh_token_' );
$t = new Token_User( '_indieauth_refresh_' );

Copilot uses AI. Check for mistakes.
- 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants