Skip to content

Commit 2bd4674

Browse files
authored
Modernize codebase: namespaces, autoloader, folder structure (#306)
* Modernize codebase: namespaces, autoloader, folder structure 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 * Fix PHPCS violations - Add doc comments to backward-compat wrapper functions - Fix alignment and spacing issues (auto-fixed by phpcbf) * Add missing backward-compat function wrappers 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. * Fix PHP 7.2 compatibility and test failures - 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 * Raise minimum PHP version to 7.4 Update CI matrix, composer.json, and PHPCS config to match the Requires PHP: 7.4 already declared in the plugin header. * Defer component initialization to init hook at priority 2 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. * Address Copilot review feedback - Align refresh token prefix in cleanup with the rest of the codebase (_indieauth_refresh_) so scheduled cleanup actually expires refresh tokens - Return a proper boolean from Admin::test_auth() via a success flag in the authdiag JSON response, so the Site Health test no longer always passes - Escape the settings page link with esc_url()/admin_url() and wp_kses() - Call scopes() directly instead of wrapping the void return in esc_html() - Use $this-> instead of self:: for the non-static generate_local_token() * Fix Test_Authorize failures on PHP 7.4 Renaming the test class to Test_Authorize made its test_authorize() method a PHP4-style constructor on PHP < 8.0, bypassing the PHPUnit TestCase constructor and erroring every test in the class. Rename the method to test_authorize_with_access_token().
1 parent e73fb29 commit 2bd4674

51 files changed

Lines changed: 2757 additions & 2027 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 35 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,23 @@
55
* @package IndieAuth
66
*/
77

8+
namespace IndieAuth;
9+
10+
use IndieAuth\Token\User as Token_User;
11+
812
/**
913
* IndieAuth Authorize class.
1014
*
1115
* Handles token verification and authentication for IndieAuth.
1216
*
1317
* @since 1.0.0
1418
*/
15-
class IndieAuth_Authorize {
19+
class Authorize {
1620

1721
/**
1822
* Error object.
1923
*
20-
* @var WP_Error|WP_OAuth_Response|null
24+
* @var \WP_Error|OAuth_Response|null
2125
*/
2226
public $error = null;
2327

@@ -58,30 +62,30 @@ public function load() {
5862

5963
// WordPress validates the auth cookie at priority 10 and this cannot be overridden by an earlier priority.
6064
// It validates the logged in cookie at 20 and can be overridden by something with a higher priority.
61-
add_filter( 'determine_current_user', array( $this, 'determine_current_user' ), 15 );
62-
add_filter( 'rest_authentication_errors', array( $this, 'rest_authentication_errors' ) );
65+
\add_filter( 'determine_current_user', array( $this, 'determine_current_user' ), 15 );
66+
\add_filter( 'rest_authentication_errors', array( $this, 'rest_authentication_errors' ) );
6367

64-
add_filter( 'indieauth_scopes', array( $this, 'get_indieauth_scopes' ), 9 );
65-
add_filter( 'indieauth_response', array( $this, 'get_indieauth_response' ), 9 );
66-
add_filter( 'wp_rest_server_class', array( $this, 'wp_rest_server_class' ) );
67-
add_filter( 'rest_request_after_callbacks', array( $this, 'return_oauth_error' ), 10, 3 );
68+
\add_filter( 'indieauth_scopes', array( $this, 'get_indieauth_scopes' ), 9 );
69+
\add_filter( 'indieauth_response', array( $this, 'get_indieauth_response' ), 9 );
70+
\add_filter( 'wp_rest_server_class', array( $this, 'wp_rest_server_class' ) );
71+
\add_filter( 'rest_request_after_callbacks', array( $this, 'return_oauth_error' ), 10, 3 );
6872
}
6973

7074

7175
/**
7276
* Ensures responses to any IndieAuth endpoints are always OAuth Responses rather than WP_Error.
7377
*
74-
* @param WP_REST_Response|WP_HTTP_Response|WP_Error|mixed $response Result to send to the client.
75-
* @param array $handler Route handler used for the request.
76-
* @param WP_REST_Request $request Request used to generate the response.
77-
* @return WP_REST_Response|WP_OAuth_Response|mixed Modified response.
78+
* @param \WP_REST_Response|\WP_HTTP_Response|\WP_Error|mixed $response Result to send to the client.
79+
* @param array $handler Route handler used for the request.
80+
* @param \WP_REST_Request $request Request used to generate the response.
81+
* @return \WP_REST_Response|OAuth_Response|mixed Modified response.
7882
*/
7983
public static function return_oauth_error( $response, $handler, $request ) {
8084
if ( 0 !== strpos( $request->get_route(), '/indieauth/1.0/' ) ) {
8185
return $response;
8286
}
8387

84-
if ( is_wp_error( $response ) ) {
88+
if ( \is_wp_error( $response ) ) {
8589
return wp_error_to_oauth_response( $response );
8690
}
8791
return $response;
@@ -98,7 +102,7 @@ public static function return_oauth_error( $response, $handler, $request ) {
98102
*/
99103
public static function wp_rest_server_class( $class ) { // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.classFound
100104
global $current_user;
101-
if ( defined( 'REST_REQUEST' ) && REST_REQUEST && $current_user instanceof WP_User && 0 === $current_user->ID ) {
105+
if ( defined( 'REST_REQUEST' ) && REST_REQUEST && $current_user instanceof \WP_User && 0 === $current_user->ID ) {
102106
/*
103107
* For our authentication to work, we need to remove the cached lack
104108
* of a current user, so the next time it checks, we can detect that
@@ -137,16 +141,16 @@ public function get_indieauth_response( $response ) {
137141
* Attached to the rest_authentication_errors filter. Passes through existing
138142
* errors registered on the filter.
139143
*
140-
* @param WP_Error|null|true $error Current error, null or true.
141-
* @return WP_Error|null|true Error if one is set, unchanged otherwise.
144+
* @param \WP_Error|null|true $error Current error, null or true.
145+
* @return \WP_Error|null|true Error if one is set, unchanged otherwise.
142146
*/
143147
public function rest_authentication_errors( $error = null ) {
144-
if ( is_user_logged_in() ) {
148+
if ( \is_user_logged_in() ) {
145149
// Another OAuth2 plugin successfully authenticated.
146150
return $error;
147151
}
148152

149-
if ( is_wp_error( $this->error ) ) {
153+
if ( \is_wp_error( $this->error ) ) {
150154
return $this->error;
151155
}
152156

@@ -193,9 +197,9 @@ public function determine_current_user( $user_id ) {
193197
}
194198
}
195199

196-
$this->error = new WP_OAuth_Response(
200+
$this->error = new OAuth_Response(
197201
'unauthorized',
198-
__( 'User Not Found on this Site', 'indieauth' ),
202+
\__( 'User Not Found on this Site', 'indieauth' ),
199203
401,
200204
array(
201205
'response' => $params,
@@ -217,16 +221,16 @@ public function determine_current_user( $user_id ) {
217221
public function get_authorization_header() {
218222
$auth = null;
219223
if ( ! empty( $_SERVER['HTTP_AUTHORIZATION'] ) ) {
220-
$auth = wp_unslash( $_SERVER['HTTP_AUTHORIZATION'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
224+
$auth = \wp_unslash( $_SERVER['HTTP_AUTHORIZATION'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
221225
} elseif ( ! empty( $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ) ) {
222226
// When Apache speaks via FastCGI with PHP, then the authorization header is often available as REDIRECT_HTTP_AUTHORIZATION.
223-
$auth = wp_unslash( $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
227+
$auth = \wp_unslash( $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
224228
} else {
225229
$headers = getallheaders();
226230
// Check for the authorization header case-insensitively.
227231
foreach ( $headers as $key => $value ) {
228232
if ( strtolower( $key ) === 'authorization' ) {
229-
$auth = wp_unslash( $value );
233+
$auth = \wp_unslash( $value );
230234
break;
231235
}
232236
}
@@ -278,7 +282,7 @@ public function get_token_from_request() {
278282
if ( empty( $_POST['access_token'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
279283
return null;
280284
}
281-
$token = sanitize_text_field( wp_unslash( $_POST['access_token'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
285+
$token = \sanitize_text_field( \wp_unslash( $_POST['access_token'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
282286

283287
if ( is_string( $token ) ) {
284288
return $token;
@@ -290,23 +294,23 @@ public function get_token_from_request() {
290294
* Verifies Access Token.
291295
*
292296
* @param string $token The token to verify.
293-
* @return array|WP_OAuth_Response Return either the token information or an OAuth Error Object.
297+
* @return array|OAuth_Response Return either the token information or an OAuth Error Object.
294298
*/
295299
public function verify_access_token( $token ) {
296300
$tokens = new Token_User( '_indieauth_token_' );
297301
$return = $tokens->get( $token );
298302
if ( empty( $return ) ) {
299-
return new WP_OAuth_Response(
303+
return new OAuth_Response(
300304
'invalid_token',
301-
__( 'Invalid access token', 'indieauth' ),
305+
\__( 'Invalid access token', 'indieauth' ),
302306
401
303307
);
304308
}
305309
if ( is_oauth_error( $return ) ) {
306310
return $return;
307311
}
308312
$return['last_accessed'] = time();
309-
$return['last_ip'] = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : '';
313+
$return['last_ip'] = isset( $_SERVER['REMOTE_ADDR'] ) ? \sanitize_text_field( \wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : '';
310314
$tokens->update( $token, $return );
311315
if ( array_key_exists( 'exp', $return ) ) {
312316
$return['expires_in'] = $return['exp'] - time();
@@ -318,15 +322,15 @@ public function verify_access_token( $token ) {
318322
* Verifies authorization code.
319323
*
320324
* @param string $code Authorization Code.
321-
* @return array|WP_OAuth_Response Return either the code information or an OAuth Error object.
325+
* @return array|OAuth_Response Return either the code information or an OAuth Error object.
322326
*/
323327
public static function verify_authorization_code( $code ) {
324328
$tokens = new Token_User( '_indieauth_code_' );
325329
$return = $tokens->get( $code );
326330
if ( empty( $return ) ) {
327-
return new WP_OAuth_Response(
331+
return new OAuth_Response(
328332
'invalid_code',
329-
__( 'Invalid authorization code', 'indieauth' ),
333+
\__( 'Invalid authorization code', 'indieauth' ),
330334
401
331335
);
332336
}

includes/class-autoloader.php

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
<?php
2+
/**
3+
* Autoloader for IndieAuth.
4+
*
5+
* @package IndieAuth
6+
*/
7+
8+
namespace IndieAuth;
9+
10+
/**
11+
* An Autoloader that respects WordPress's filename standards.
12+
*/
13+
class Autoloader {
14+
15+
/**
16+
* Namespace separator.
17+
*/
18+
const NS_SEPARATOR = '\\';
19+
20+
/**
21+
* The prefix to compare classes against.
22+
*
23+
* @var string
24+
* @access protected
25+
*/
26+
protected $prefix;
27+
28+
/**
29+
* Length of the prefix string.
30+
*
31+
* @var int
32+
* @access protected
33+
*/
34+
protected $prefix_length;
35+
36+
/**
37+
* Path to the file to be loaded.
38+
*
39+
* @var string
40+
* @access protected
41+
*/
42+
protected $path;
43+
44+
/**
45+
* Constructor.
46+
*
47+
* @param string $prefix Namespace prefix all classes have in common.
48+
* @param string $path Path to the files to be loaded.
49+
*/
50+
public function __construct( $prefix, $path ) {
51+
$this->prefix = $prefix;
52+
$this->prefix_length = \strlen( $prefix );
53+
$this->path = \rtrim( $path . '/' );
54+
}
55+
56+
/**
57+
* Registers Autoloader's autoload function.
58+
*
59+
* @throws \Exception When autoload_function cannot be registered.
60+
*
61+
* @param string $prefix Namespace prefix all classes have in common.
62+
* @param string $path Path to the files to be loaded.
63+
*/
64+
public static function register_path( $prefix, $path ) {
65+
$loader = new self( $prefix, $path );
66+
\spl_autoload_register( array( $loader, 'load' ) );
67+
}
68+
69+
/**
70+
* Loads a class if its namespace starts with `$this->prefix`.
71+
*
72+
* @param string $class_name The class to be loaded.
73+
*/
74+
public function load( $class_name ) {
75+
if ( \strpos( $class_name, $this->prefix . self::NS_SEPARATOR ) !== 0 ) {
76+
return;
77+
}
78+
79+
// Strip prefix from the start (ala PSR-4).
80+
$class_name = \substr( $class_name, $this->prefix_length + 1 );
81+
$class_name = \strtolower( $class_name );
82+
$dir = '';
83+
84+
$last_ns_pos = \strripos( $class_name, self::NS_SEPARATOR );
85+
if ( false !== $last_ns_pos ) {
86+
$namespace = \substr( $class_name, 0, $last_ns_pos );
87+
$namespace = \str_replace( '_', '-', $namespace );
88+
$class_name = \substr( $class_name, $last_ns_pos + 1 );
89+
$dir = \str_replace( self::NS_SEPARATOR, DIRECTORY_SEPARATOR, $namespace ) . DIRECTORY_SEPARATOR;
90+
}
91+
92+
$path = $this->path . $dir . 'class-' . \str_replace( '_', '-', $class_name ) . '.php';
93+
94+
if ( ! \file_exists( $path ) ) {
95+
$path = $this->path . $dir . 'interface-' . \str_replace( '_', '-', $class_name ) . '.php';
96+
}
97+
98+
if ( ! \file_exists( $path ) ) {
99+
$path = $this->path . $dir . 'trait-' . \str_replace( '_', '-', $class_name ) . '.php';
100+
}
101+
102+
if ( \file_exists( $path ) ) {
103+
require_once $path;
104+
}
105+
}
106+
}

0 commit comments

Comments
 (0)