From 06b58b1ae3dab5157b0ed53fab46fe7d65aeca5c Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 30 Dec 2025 12:26:05 +0100 Subject: [PATCH 01/12] Add singleton-based plugin loading mechanism Introduce Nodeinfo class with centralized initialization pattern: - Singleton pattern with get_instance() for global access - Consolidated hook registration in register_hooks() - Admin hooks separated into register_admin_hooks() - Static activate/deactivate methods for plugin lifecycle - Simplified main plugin file to use new class --- includes/class-nodeinfo.php | 154 ++++++++++++++++++++++++++++++++++++ nodeinfo.php | 68 +++------------- 2 files changed, 164 insertions(+), 58 deletions(-) create mode 100644 includes/class-nodeinfo.php diff --git a/includes/class-nodeinfo.php b/includes/class-nodeinfo.php new file mode 100644 index 0000000..247fc34 --- /dev/null +++ b/includes/class-nodeinfo.php @@ -0,0 +1,154 @@ +initialized ) { + return; + } + + $this->register_hooks(); + $this->register_admin_hooks(); + + // Load language files. + \load_plugin_textdomain( + self::TEXT_DOMAIN, + false, + \dirname( \plugin_basename( NODEINFO_PLUGIN_FILE ) ) . '/languages' + ); + + $this->initialized = true; + } + + /** + * Register hooks. + */ + public function register_hooks() { + // Initialize NodeInfo version integrations. + \add_action( 'init', array( Nodeinfo10::class, 'init' ) ); + \add_action( 'init', array( Nodeinfo11::class, 'init' ) ); + \add_action( 'init', array( Nodeinfo20::class, 'init' ) ); + \add_action( 'init', array( Nodeinfo21::class, 'init' ) ); + \add_action( 'init', array( Nodeinfo22::class, 'init' ) ); + + // Register REST routes. + \add_action( 'rest_api_init', array( $this, 'register_routes' ) ); + + // Add WebFinger and Host-Meta discovery. + \add_filter( 'webfinger_user_data', array( Controller_Nodeinfo::class, 'jrd' ), 10, 3 ); + \add_filter( 'webfinger_post_data', array( Controller_Nodeinfo::class, 'jrd' ), 10, 3 ); + \add_filter( 'host_meta', array( Controller_Nodeinfo::class, 'jrd' ) ); + + // Add rewrite rules for well-known endpoints. + \add_action( 'init', array( $this, 'add_rewrite_rules' ), 1 ); + } + + /** + * Register admin hooks. + */ + public function register_admin_hooks() { + // Initialize Site Health checks. + \add_action( 'admin_init', array( Health_Check::class, 'init' ) ); + } + + /** + * Register REST API routes. + */ + public function register_routes() { + $nodeinfo_controller = new Controller_Nodeinfo(); + $nodeinfo_controller->register_routes(); + + $nodeinfo2_controller = new Controller_Nodeinfo2(); + $nodeinfo2_controller->register_routes(); + } + + /** + * Add rewrite rules for well-known endpoints. + */ + public function add_rewrite_rules() { + \add_rewrite_rule( '^.well-known/nodeinfo', 'index.php?rest_route=/nodeinfo/discovery', 'top' ); + \add_rewrite_rule( '^.well-known/x-nodeinfo2', 'index.php?rest_route=/nodeinfo2/1.0', 'top' ); + } + + /** + * Flush rewrite rules. + * + * Should be called on plugin activation. + */ + public static function activate() { + self::get_instance()->add_rewrite_rules(); + \flush_rewrite_rules(); + } + + /** + * Deactivate the plugin. + * + * Should be called on plugin deactivation. + */ + public static function deactivate() { + \flush_rewrite_rules(); + } +} diff --git a/nodeinfo.php b/nodeinfo.php index a7f264a..492a1c6 100644 --- a/nodeinfo.php +++ b/nodeinfo.php @@ -32,65 +32,17 @@ require_once NODEINFO_PLUGIN_DIR . 'includes/class-nodeinfo-endpoint.php'; /** - * Initialize the plugin. - */ -function nodeinfo_init() { - // Initialize NodeInfo version integrations. - Nodeinfo\Integration\Nodeinfo10::init(); - Nodeinfo\Integration\Nodeinfo11::init(); - Nodeinfo\Integration\Nodeinfo20::init(); - Nodeinfo\Integration\Nodeinfo21::init(); - Nodeinfo\Integration\Nodeinfo22::init(); - - // Register REST routes. - add_action( 'rest_api_init', 'nodeinfo_register_routes' ); - - // Add WebFinger and Host-Meta discovery. - add_filter( 'webfinger_user_data', array( Nodeinfo\Controller\Nodeinfo::class, 'jrd' ), 10, 3 ); - add_filter( 'webfinger_post_data', array( Nodeinfo\Controller\Nodeinfo::class, 'jrd' ), 10, 3 ); - add_filter( 'host_meta', array( Nodeinfo\Controller\Nodeinfo::class, 'jrd' ) ); -} -add_action( 'init', 'nodeinfo_init', 9 ); - -/** - * Initialize admin-only features. - */ -function nodeinfo_admin_init() { - // Initialize Site Health checks. - Nodeinfo\Health_Check::init(); -} -add_action( 'admin_init', 'nodeinfo_admin_init' ); - -/** - * Register REST API routes. - */ -function nodeinfo_register_routes() { - $nodeinfo_controller = new Nodeinfo\Controller\Nodeinfo(); - $nodeinfo_controller->register_routes(); - - $nodeinfo2_controller = new Nodeinfo\Controller\Nodeinfo2(); - $nodeinfo2_controller->register_routes(); -} - -/** - * Add rewrite rules for well-known endpoints. + * Plugin initialization function. + * + * @return Nodeinfo\Nodeinfo The plugin instance. */ -function nodeinfo_add_rewrite_rules() { - add_rewrite_rule( '^.well-known/nodeinfo', 'index.php?rest_route=/nodeinfo/discovery', 'top' ); - add_rewrite_rule( '^.well-known/x-nodeinfo2', 'index.php?rest_route=/nodeinfo2/1.0', 'top' ); +function nodeinfo_plugin() { + return Nodeinfo\Nodeinfo::get_instance(); } -add_action( 'init', 'nodeinfo_add_rewrite_rules', 1 ); -/** - * Flush rewrite rules on activation. - */ -function nodeinfo_activate() { - nodeinfo_add_rewrite_rules(); - flush_rewrite_rules(); -} -register_activation_hook( __FILE__, 'nodeinfo_activate' ); +// Initialize the plugin. +nodeinfo_plugin()->init(); -/** - * Flush rewrite rules on deactivation. - */ -register_deactivation_hook( __FILE__, 'flush_rewrite_rules' ); +// Register activation and deactivation hooks. +register_activation_hook( __FILE__, array( Nodeinfo\Nodeinfo::class, 'activate' ) ); +register_deactivation_hook( __FILE__, array( Nodeinfo\Nodeinfo::class, 'deactivate' ) ); From 06c27b51c325d22e40fb63b1e99528e7d80a93a6 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 30 Dec 2025 12:29:40 +0100 Subject: [PATCH 02/12] Deprecate wellknown_nodeinfo_data filter Add backwards compatibility handler using apply_filters_deprecated() to map the old wellknown_nodeinfo_data filter to nodeinfo_discovery. --- includes/class-nodeinfo.php | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/includes/class-nodeinfo.php b/includes/class-nodeinfo.php index 247fc34..f6c040f 100644 --- a/includes/class-nodeinfo.php +++ b/includes/class-nodeinfo.php @@ -104,6 +104,31 @@ public function register_hooks() { // Add rewrite rules for well-known endpoints. \add_action( 'init', array( $this, 'add_rewrite_rules' ), 1 ); + + // Register deprecated filter handlers. + \add_filter( 'nodeinfo_discovery', array( $this, 'deprecated_wellknown_nodeinfo_data' ), 99 ); + } + + /** + * Handles the deprecated wellknown_nodeinfo_data filter. + * + * @param array $discovery The discovery document. + * @return array The filtered discovery document. + */ + public function deprecated_wellknown_nodeinfo_data( $discovery ) { + /** + * Filters the NodeInfo discovery document. + * + * @deprecated 3.0.0 Use nodeinfo_discovery instead. + * + * @param array $discovery The discovery document. + */ + return \apply_filters_deprecated( + 'wellknown_nodeinfo_data', + array( $discovery ), + '3.0.0', + 'nodeinfo_discovery' + ); } /** From d3e45197a08aaab16f4207be9686393d9a27f891 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 30 Dec 2025 12:34:32 +0100 Subject: [PATCH 03/12] Release 3.1.0 --- README.md | 7 ++++++- nodeinfo.php | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index eb5bdf3..76a3567 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ - Tags: nodeinfo, fediverse, ostatus, diaspora, activitypub - Requires at least: 6.6 - Tested up to: 6.9 -- Stable tag: 3.0.0 +- Stable tag: 3.1.0 - Requires PHP: 7.2 - License: MIT - License URI: https://opensource.org/licenses/MIT @@ -81,6 +81,11 @@ If either check fails, you'll see recommendations on how to fix the issue. Project and support maintained on github at [pfefferle/wordpress-nodeinfo](https://github.com/pfefferle/wordpress-nodeinfo). +### 3.1.0 + +* Added singleton-based plugin loading mechanism for better extensibility +* Deprecated `wellknown_nodeinfo_data` filter in favor of `nodeinfo_discovery` + ### 3.0.0 * Refactored to filter-based architecture for better extensibility diff --git a/nodeinfo.php b/nodeinfo.php index 492a1c6..be8863b 100644 --- a/nodeinfo.php +++ b/nodeinfo.php @@ -3,7 +3,7 @@ * Plugin Name: NodeInfo * Plugin URI: https://github.com/pfefferle/wordpress-nodeinfo/ * Description: NodeInfo is an effort to create a standardized way of exposing metadata about a server running one of the distributed social networks. - * Version: 3.0.0 + * Version: 3.1.0 * Author: Matthias Pfefferle * Author URI: https://notiz.blog/ * License: MIT From 5882e609a0746de0b1176c273dc8ed2e1b2c8046 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 30 Dec 2025 12:47:34 +0100 Subject: [PATCH 04/12] Address Copilot review feedback - Move load_plugin_textdomain to init hook for proper timing - Add priority 9 to integration hooks to maintain original behavior --- includes/class-nodeinfo.php | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/includes/class-nodeinfo.php b/includes/class-nodeinfo.php index f6c040f..440a705 100644 --- a/includes/class-nodeinfo.php +++ b/includes/class-nodeinfo.php @@ -73,26 +73,33 @@ public function init() { $this->register_hooks(); $this->register_admin_hooks(); - // Load language files. + $this->initialized = true; + } + + /** + * Load the plugin text domain. + */ + public function load_textdomain() { \load_plugin_textdomain( self::TEXT_DOMAIN, false, \dirname( \plugin_basename( NODEINFO_PLUGIN_FILE ) ) . '/languages' ); - - $this->initialized = true; } /** * Register hooks. */ public function register_hooks() { + // Load plugin text domain. + \add_action( 'init', array( $this, 'load_textdomain' ) ); + // Initialize NodeInfo version integrations. - \add_action( 'init', array( Nodeinfo10::class, 'init' ) ); - \add_action( 'init', array( Nodeinfo11::class, 'init' ) ); - \add_action( 'init', array( Nodeinfo20::class, 'init' ) ); - \add_action( 'init', array( Nodeinfo21::class, 'init' ) ); - \add_action( 'init', array( Nodeinfo22::class, 'init' ) ); + \add_action( 'init', array( Nodeinfo10::class, 'init' ), 9 ); + \add_action( 'init', array( Nodeinfo11::class, 'init' ), 9 ); + \add_action( 'init', array( Nodeinfo20::class, 'init' ), 9 ); + \add_action( 'init', array( Nodeinfo21::class, 'init' ), 9 ); + \add_action( 'init', array( Nodeinfo22::class, 'init' ), 9 ); // Register REST routes. \add_action( 'rest_api_init', array( $this, 'register_routes' ) ); From bfd0ed52a24283c3b30321e6c925c7ed879f4914 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 30 Dec 2025 12:50:25 +0100 Subject: [PATCH 05/12] Remove manual textdomain loading WordPress handles translations automatically for plugins on WordPress.org. Remove pot file, Domain Path header, and load_textdomain method. --- includes/class-nodeinfo.php | 21 ----------------- languages/nodeinfo.pot | 45 ------------------------------------- nodeinfo.php | 1 - 3 files changed, 67 deletions(-) delete mode 100644 languages/nodeinfo.pot diff --git a/includes/class-nodeinfo.php b/includes/class-nodeinfo.php index 440a705..3f966bf 100644 --- a/includes/class-nodeinfo.php +++ b/includes/class-nodeinfo.php @@ -28,13 +28,6 @@ class Nodeinfo { */ private static $instance; - /** - * Text domain. - * - * @var string - */ - const TEXT_DOMAIN = 'nodeinfo'; - /** * Whether the class has been initialized. * @@ -76,24 +69,10 @@ public function init() { $this->initialized = true; } - /** - * Load the plugin text domain. - */ - public function load_textdomain() { - \load_plugin_textdomain( - self::TEXT_DOMAIN, - false, - \dirname( \plugin_basename( NODEINFO_PLUGIN_FILE ) ) . '/languages' - ); - } - /** * Register hooks. */ public function register_hooks() { - // Load plugin text domain. - \add_action( 'init', array( $this, 'load_textdomain' ) ); - // Initialize NodeInfo version integrations. \add_action( 'init', array( Nodeinfo10::class, 'init' ), 9 ); \add_action( 'init', array( Nodeinfo11::class, 'init' ), 9 ); diff --git a/languages/nodeinfo.pot b/languages/nodeinfo.pot deleted file mode 100644 index 9c4b212..0000000 --- a/languages/nodeinfo.pot +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright (C) 2024 Matthias Pfefferle -# This file is distributed under the MIT. -msgid "" -msgstr "" -"Project-Id-Version: NodeInfo 2.3.1\n" -"Report-Msgid-Bugs-To: " -"https://wordpress.org/support/plugin/wordpress-nodeinfo\n" -"POT-Creation-Date: 2024-04-05 14:07:42+00:00\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=utf-8\n" -"Content-Transfer-Encoding: 8bit\n" -"PO-Revision-Date: 2024-MO-DA HO:MI+ZONE\n" -"Last-Translator: FULL NAME \n" -"Language-Team: LANGUAGE \n" -"X-Generator: grunt-wp-i18n 1.0.3\n" - -#: includes/class-nodeinfo-endpoint.php:35 -msgid "The version of the NodeInfo scheme" -msgstr "" - -#: includes/class-nodeinfo-endpoint.php:60 -msgid "The version of the NodeInfo2 scheme" -msgstr "" - -#. Plugin Name of the plugin/theme -msgid "NodeInfo" -msgstr "" - -#. Plugin URI of the plugin/theme -msgid "https://github.com/pfefferle/wordpress-nodeinfo/" -msgstr "" - -#. Description of the plugin/theme -msgid "" -"NodeInfo is an effort to create a standardized way of exposing metadata " -"about a server running one of the distributed social networks." -msgstr "" - -#. Author of the plugin/theme -msgid "Matthias Pfefferle" -msgstr "" - -#. Author URI of the plugin/theme -msgid "https://notiz.blog/" -msgstr "" \ No newline at end of file diff --git a/nodeinfo.php b/nodeinfo.php index be8863b..a569fb9 100644 --- a/nodeinfo.php +++ b/nodeinfo.php @@ -9,7 +9,6 @@ * License: MIT * License URI: http://opensource.org/licenses/MIT * Text Domain: nodeinfo - * Domain Path: /languages * * @package Nodeinfo */ From 3c7475c9ba1098745051fefbe007f7205b193634 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 30 Dec 2025 12:52:12 +0100 Subject: [PATCH 06/12] Call integrations directly instead of hooking on init Integrations only register filters, so they don't need to wait for the init hook. This removes the need for priority handling. --- includes/class-nodeinfo.php | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/includes/class-nodeinfo.php b/includes/class-nodeinfo.php index 3f966bf..4fb7933 100644 --- a/includes/class-nodeinfo.php +++ b/includes/class-nodeinfo.php @@ -63,23 +63,30 @@ public function init() { return; } + $this->register_integrations(); $this->register_hooks(); $this->register_admin_hooks(); $this->initialized = true; } + /** + * Register NodeInfo version integrations. + * + * These only register filters, so they can be called directly. + */ + public function register_integrations() { + Nodeinfo10::init(); + Nodeinfo11::init(); + Nodeinfo20::init(); + Nodeinfo21::init(); + Nodeinfo22::init(); + } + /** * Register hooks. */ public function register_hooks() { - // Initialize NodeInfo version integrations. - \add_action( 'init', array( Nodeinfo10::class, 'init' ), 9 ); - \add_action( 'init', array( Nodeinfo11::class, 'init' ), 9 ); - \add_action( 'init', array( Nodeinfo20::class, 'init' ), 9 ); - \add_action( 'init', array( Nodeinfo21::class, 'init' ), 9 ); - \add_action( 'init', array( Nodeinfo22::class, 'init' ), 9 ); - // Register REST routes. \add_action( 'rest_api_init', array( $this, 'register_routes' ) ); From 2086fb07018a82651480ee233bf2116992a4dfb2 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 30 Dec 2025 13:28:20 +0100 Subject: [PATCH 07/12] Add tests for Nodeinfo main class Tests cover: - Singleton pattern behavior - REST routes registration - Integration registration - Rewrite rules - Deprecated filter handling - WebFinger and admin hooks registration --- tests/phpunit/tests/class-test-nodeinfo.php | 137 ++++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 tests/phpunit/tests/class-test-nodeinfo.php diff --git a/tests/phpunit/tests/class-test-nodeinfo.php b/tests/phpunit/tests/class-test-nodeinfo.php new file mode 100644 index 0000000..f88a8cf --- /dev/null +++ b/tests/phpunit/tests/class-test-nodeinfo.php @@ -0,0 +1,137 @@ +assertSame( $instance1, $instance2 ); + } + + /** + * Test that get_instance returns Nodeinfo instance. + * + * @covers ::get_instance + */ + public function test_get_instance_returns_nodeinfo() { + $instance = \Nodeinfo\Nodeinfo::get_instance(); + + $this->assertInstanceOf( \Nodeinfo\Nodeinfo::class, $instance ); + } + + /** + * Test that REST routes are registered. + * + * @covers ::register_routes + */ + public function test_rest_routes_registered() { + global $wp_rest_server; + + $wp_rest_server = new \WP_REST_Server(); + do_action( 'rest_api_init' ); + + $routes = $wp_rest_server->get_routes(); + + $this->assertArrayHasKey( '/nodeinfo/discovery', $routes ); + $this->assertArrayHasKey( '/nodeinfo2/1.0', $routes ); + + $wp_rest_server = null; + } + + /** + * Test that integrations register nodeinfo_versions filter. + * + * @covers ::register_integrations + */ + public function test_integrations_register_versions() { + $versions = apply_filters( 'nodeinfo_versions', array() ); + + $this->assertContains( '1.0', $versions ); + $this->assertContains( '1.1', $versions ); + $this->assertContains( '2.0', $versions ); + $this->assertContains( '2.1', $versions ); + $this->assertContains( '2.2', $versions ); + } + + /** + * Test that rewrite rules are added. + * + * @covers ::add_rewrite_rules + */ + public function test_rewrite_rules_added() { + global $wp_rewrite; + + // Flush rules to ensure they're registered. + \Nodeinfo\Nodeinfo::get_instance()->add_rewrite_rules(); + $wp_rewrite->flush_rules(); + + $rules = $wp_rewrite->wp_rewrite_rules(); + + $this->assertArrayHasKey( '^.well-known/nodeinfo', $rules ); + $this->assertArrayHasKey( '^.well-known/x-nodeinfo2', $rules ); + } + + /** + * Test deprecated wellknown_nodeinfo_data filter triggers deprecation. + * + * @covers ::deprecated_wellknown_nodeinfo_data + */ + public function test_deprecated_filter_triggers_notice() { + // Add a callback to the deprecated filter. + add_filter( + 'wellknown_nodeinfo_data', + function ( $data ) { + $data['test'] = 'value'; + return $data; + } + ); + + // Expect a deprecation notice. + $this->setExpectedDeprecated( 'wellknown_nodeinfo_data' ); + + // Trigger the filter chain. + $discovery = apply_filters( 'nodeinfo_discovery', array( 'links' => array() ) ); + + // Verify the deprecated filter was applied. + $this->assertArrayHasKey( 'test', $discovery ); + $this->assertEquals( 'value', $discovery['test'] ); + } + + /** + * Test WebFinger filter is registered. + * + * @covers ::register_hooks + */ + public function test_webfinger_filter_registered() { + $this->assertTrue( has_filter( 'webfinger_user_data' ) !== false ); + $this->assertTrue( has_filter( 'webfinger_post_data' ) !== false ); + $this->assertTrue( has_filter( 'host_meta' ) !== false ); + } + + /** + * Test admin hooks are registered. + * + * @covers ::register_admin_hooks + */ + public function test_admin_hooks_registered() { + $this->assertTrue( has_action( 'admin_init' ) !== false ); + } +} From 0a57ee3014b72bd8a6e468e48d2798fe0d3ca943 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 30 Dec 2025 13:30:41 +0100 Subject: [PATCH 08/12] Fix failing tests - Use correct route pattern for nodeinfo2 endpoint - Enable permalinks before testing rewrite rules --- tests/phpunit/tests/class-test-nodeinfo.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/phpunit/tests/class-test-nodeinfo.php b/tests/phpunit/tests/class-test-nodeinfo.php index f88a8cf..544009e 100644 --- a/tests/phpunit/tests/class-test-nodeinfo.php +++ b/tests/phpunit/tests/class-test-nodeinfo.php @@ -51,7 +51,7 @@ public function test_rest_routes_registered() { $routes = $wp_rest_server->get_routes(); $this->assertArrayHasKey( '/nodeinfo/discovery', $routes ); - $this->assertArrayHasKey( '/nodeinfo2/1.0', $routes ); + $this->assertArrayHasKey( '/nodeinfo2/(?P\\d\\.\\d)', $routes ); $wp_rest_server = null; } @@ -79,14 +79,22 @@ public function test_integrations_register_versions() { public function test_rewrite_rules_added() { global $wp_rewrite; - // Flush rules to ensure they're registered. + // Enable permalinks for testing. + $wp_rewrite->set_permalink_structure( '/%postname%/' ); + + // Add rewrite rules. \Nodeinfo\Nodeinfo::get_instance()->add_rewrite_rules(); $wp_rewrite->flush_rules(); $rules = $wp_rewrite->wp_rewrite_rules(); + // Ensure rules is an array. + $this->assertIsArray( $rules ); $this->assertArrayHasKey( '^.well-known/nodeinfo', $rules ); $this->assertArrayHasKey( '^.well-known/x-nodeinfo2', $rules ); + + // Reset permalink structure. + $wp_rewrite->set_permalink_structure( '' ); } /** From aae2d654cd89b4cbeea46072c13c7ec84f880fb8 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 30 Dec 2025 17:35:43 +0100 Subject: [PATCH 09/12] Address Copilot review feedback - Hook init to plugins_loaded action for proper timing - Add is_admin() check before registering admin hooks - Call init() in activate() to ensure integrations are loaded - Update activate() docblock to better describe its purpose - Add proper test cleanup for global variables - Clarify changelog wording about deprecated filter --- README.md | 2 +- includes/class-nodeinfo.php | 13 +++++++++---- nodeinfo.php | 9 +++++++-- tests/phpunit/tests/class-test-nodeinfo.php | 21 ++++++++++++++++----- 4 files changed, 33 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 76a3567..52f4b63 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ Project and support maintained on github at [pfefferle/wordpress-nodeinfo](https ### 3.1.0 * Added singleton-based plugin loading mechanism for better extensibility -* Deprecated `wellknown_nodeinfo_data` filter in favor of `nodeinfo_discovery` +* Added backwards compatibility handler for deprecated `wellknown_nodeinfo_data` filter ### 3.0.0 diff --git a/includes/class-nodeinfo.php b/includes/class-nodeinfo.php index 4fb7933..ba0c2e2 100644 --- a/includes/class-nodeinfo.php +++ b/includes/class-nodeinfo.php @@ -65,7 +65,10 @@ public function init() { $this->register_integrations(); $this->register_hooks(); - $this->register_admin_hooks(); + + if ( \is_admin() ) { + $this->register_admin_hooks(); + } $this->initialized = true; } @@ -152,12 +155,14 @@ public function add_rewrite_rules() { } /** - * Flush rewrite rules. + * Handle plugin activation. * - * Should be called on plugin activation. + * Initializes the plugin and flushes rewrite rules. */ public static function activate() { - self::get_instance()->add_rewrite_rules(); + $instance = self::get_instance(); + $instance->init(); + $instance->add_rewrite_rules(); \flush_rewrite_rules(); } diff --git a/nodeinfo.php b/nodeinfo.php index a569fb9..762129c 100644 --- a/nodeinfo.php +++ b/nodeinfo.php @@ -39,8 +39,13 @@ function nodeinfo_plugin() { return Nodeinfo\Nodeinfo::get_instance(); } -// Initialize the plugin. -nodeinfo_plugin()->init(); +// Initialize the plugin after all plugins are loaded. +add_action( + 'plugins_loaded', + function () { + nodeinfo_plugin()->init(); + } +); // Register activation and deactivation hooks. register_activation_hook( __FILE__, array( Nodeinfo\Nodeinfo::class, 'activate' ) ); diff --git a/tests/phpunit/tests/class-test-nodeinfo.php b/tests/phpunit/tests/class-test-nodeinfo.php index 544009e..f154bab 100644 --- a/tests/phpunit/tests/class-test-nodeinfo.php +++ b/tests/phpunit/tests/class-test-nodeinfo.php @@ -45,6 +45,9 @@ public function test_get_instance_returns_nodeinfo() { public function test_rest_routes_registered() { global $wp_rest_server; + // Save original state. + $original_server = $wp_rest_server; + $wp_rest_server = new \WP_REST_Server(); do_action( 'rest_api_init' ); @@ -53,7 +56,8 @@ public function test_rest_routes_registered() { $this->assertArrayHasKey( '/nodeinfo/discovery', $routes ); $this->assertArrayHasKey( '/nodeinfo2/(?P\\d\\.\\d)', $routes ); - $wp_rest_server = null; + // Restore original state. + $wp_rest_server = $original_server; } /** @@ -79,6 +83,9 @@ public function test_integrations_register_versions() { public function test_rewrite_rules_added() { global $wp_rewrite; + // Save original permalink structure. + $original_structure = $wp_rewrite->permalink_structure; + // Enable permalinks for testing. $wp_rewrite->set_permalink_structure( '/%postname%/' ); @@ -93,8 +100,9 @@ public function test_rewrite_rules_added() { $this->assertArrayHasKey( '^.well-known/nodeinfo', $rules ); $this->assertArrayHasKey( '^.well-known/x-nodeinfo2', $rules ); - // Reset permalink structure. - $wp_rewrite->set_permalink_structure( '' ); + // Restore original permalink structure. + $wp_rewrite->set_permalink_structure( $original_structure ); + $wp_rewrite->flush_rules(); } /** @@ -135,11 +143,14 @@ public function test_webfinger_filter_registered() { } /** - * Test admin hooks are registered. + * Test admin hooks can be registered. * * @covers ::register_admin_hooks */ public function test_admin_hooks_registered() { - $this->assertTrue( has_action( 'admin_init' ) !== false ); + // Call register_admin_hooks directly to test it works. + \Nodeinfo\Nodeinfo::get_instance()->register_admin_hooks(); + + $this->assertNotFalse( has_action( 'admin_init' ) ); } } From 35e31eb62e68706f65f80baa69c98c093f177a95 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 30 Dec 2025 17:46:08 +0100 Subject: [PATCH 10/12] Add documentation and tests for lifecycle methods - Document why activate() calls both init() and add_rewrite_rules() - Add test for init() guard clause preventing double initialization - Add test for activate() method - Add test for deactivate() method --- includes/class-nodeinfo.php | 4 ++ tests/phpunit/tests/class-test-nodeinfo.php | 61 +++++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/includes/class-nodeinfo.php b/includes/class-nodeinfo.php index ba0c2e2..b6a82bb 100644 --- a/includes/class-nodeinfo.php +++ b/includes/class-nodeinfo.php @@ -158,6 +158,10 @@ public function add_rewrite_rules() { * Handle plugin activation. * * Initializes the plugin and flushes rewrite rules. + * + * Note: We call init() to register all hooks. However, during activation + * the 'init' hook may have already fired, so we also call add_rewrite_rules() + * directly to ensure rules are registered. */ public static function activate() { $instance = self::get_instance(); diff --git a/tests/phpunit/tests/class-test-nodeinfo.php b/tests/phpunit/tests/class-test-nodeinfo.php index f154bab..6e8de25 100644 --- a/tests/phpunit/tests/class-test-nodeinfo.php +++ b/tests/phpunit/tests/class-test-nodeinfo.php @@ -153,4 +153,65 @@ public function test_admin_hooks_registered() { $this->assertNotFalse( has_action( 'admin_init' ) ); } + + /** + * Test that init() guard prevents multiple initializations. + * + * @covers ::init + */ + public function test_init_guard_prevents_double_initialization() { + $instance = \Nodeinfo\Nodeinfo::get_instance(); + + // Get the filter count before calling init again. + $filter_count_before = has_filter( 'nodeinfo_discovery' ); + + // Call init() again - should be guarded. + $instance->init(); + + // Filter count should remain the same. + $filter_count_after = has_filter( 'nodeinfo_discovery' ); + + $this->assertSame( $filter_count_before, $filter_count_after ); + } + + /** + * Test activate() method registers rewrite rules. + * + * @covers ::activate + */ + public function test_activate_registers_rewrite_rules() { + global $wp_rewrite; + + // Save original permalink structure. + $original_structure = $wp_rewrite->permalink_structure; + + // Enable permalinks for testing. + $wp_rewrite->set_permalink_structure( '/%postname%/' ); + + // Call activate. + \Nodeinfo\Nodeinfo::activate(); + + $rules = $wp_rewrite->wp_rewrite_rules(); + + $this->assertIsArray( $rules ); + $this->assertArrayHasKey( '^.well-known/nodeinfo', $rules ); + + // Restore original permalink structure. + $wp_rewrite->set_permalink_structure( $original_structure ); + $wp_rewrite->flush_rules(); + } + + /** + * Test deactivate() method flushes rewrite rules. + * + * @covers ::deactivate + */ + public function test_deactivate_flushes_rewrite_rules() { + // This test verifies deactivate() runs without errors. + // The actual effect (flushing rules) is internal to WordPress. + \Nodeinfo\Nodeinfo::deactivate(); + + // If we get here without exceptions, the test passes. + $this->assertTrue( true ); + } } From deb6c7f90f635d082aab7ba13f379d50069c7d84 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 30 Dec 2025 17:49:00 +0100 Subject: [PATCH 11/12] Use rewrite_rules_array filter for better performance Instead of calling add_rewrite_rule() on every init hook, use the rewrite_rules_array filter which only fires during flush operations. This is more efficient as rules are cached in the database. --- includes/class-nodeinfo.php | 28 +++++++------ tests/phpunit/tests/class-test-nodeinfo.php | 46 +++++---------------- 2 files changed, 25 insertions(+), 49 deletions(-) diff --git a/includes/class-nodeinfo.php b/includes/class-nodeinfo.php index b6a82bb..cfc1314 100644 --- a/includes/class-nodeinfo.php +++ b/includes/class-nodeinfo.php @@ -98,8 +98,8 @@ public function register_hooks() { \add_filter( 'webfinger_post_data', array( Controller_Nodeinfo::class, 'jrd' ), 10, 3 ); \add_filter( 'host_meta', array( Controller_Nodeinfo::class, 'jrd' ) ); - // Add rewrite rules for well-known endpoints. - \add_action( 'init', array( $this, 'add_rewrite_rules' ), 1 ); + // Add rewrite rules for well-known endpoints (only during flush). + \add_filter( 'rewrite_rules_array', array( $this, 'add_rewrite_rules' ) ); // Register deprecated filter handlers. \add_filter( 'nodeinfo_discovery', array( $this, 'deprecated_wellknown_nodeinfo_data' ), 99 ); @@ -148,25 +148,27 @@ public function register_routes() { /** * Add rewrite rules for well-known endpoints. + * + * @param array $rules The existing rewrite rules. + * @return array The modified rewrite rules. */ - public function add_rewrite_rules() { - \add_rewrite_rule( '^.well-known/nodeinfo', 'index.php?rest_route=/nodeinfo/discovery', 'top' ); - \add_rewrite_rule( '^.well-known/x-nodeinfo2', 'index.php?rest_route=/nodeinfo2/1.0', 'top' ); + public function add_rewrite_rules( $rules ) { + $new_rules = array( + '^.well-known/nodeinfo' => 'index.php?rest_route=/nodeinfo/discovery', + '^.well-known/x-nodeinfo2' => 'index.php?rest_route=/nodeinfo2/1.0', + ); + + return \array_merge( $new_rules, $rules ); } /** * Handle plugin activation. * - * Initializes the plugin and flushes rewrite rules. - * - * Note: We call init() to register all hooks. However, during activation - * the 'init' hook may have already fired, so we also call add_rewrite_rules() - * directly to ensure rules are registered. + * Initializes the plugin and flushes rewrite rules. The rewrite_rules_array + * filter will add our rules during the flush. */ public static function activate() { - $instance = self::get_instance(); - $instance->init(); - $instance->add_rewrite_rules(); + self::get_instance()->init(); \flush_rewrite_rules(); } diff --git a/tests/phpunit/tests/class-test-nodeinfo.php b/tests/phpunit/tests/class-test-nodeinfo.php index 6e8de25..63584c3 100644 --- a/tests/phpunit/tests/class-test-nodeinfo.php +++ b/tests/phpunit/tests/class-test-nodeinfo.php @@ -81,28 +81,16 @@ public function test_integrations_register_versions() { * @covers ::add_rewrite_rules */ public function test_rewrite_rules_added() { - global $wp_rewrite; - - // Save original permalink structure. - $original_structure = $wp_rewrite->permalink_structure; - - // Enable permalinks for testing. - $wp_rewrite->set_permalink_structure( '/%postname%/' ); - - // Add rewrite rules. - \Nodeinfo\Nodeinfo::get_instance()->add_rewrite_rules(); - $wp_rewrite->flush_rules(); + $instance = \Nodeinfo\Nodeinfo::get_instance(); - $rules = $wp_rewrite->wp_rewrite_rules(); + // Test the filter directly. + $rules = $instance->add_rewrite_rules( array() ); - // Ensure rules is an array. $this->assertIsArray( $rules ); $this->assertArrayHasKey( '^.well-known/nodeinfo', $rules ); $this->assertArrayHasKey( '^.well-known/x-nodeinfo2', $rules ); - - // Restore original permalink structure. - $wp_rewrite->set_permalink_structure( $original_structure ); - $wp_rewrite->flush_rules(); + $this->assertEquals( 'index.php?rest_route=/nodeinfo/discovery', $rules['^.well-known/nodeinfo'] ); + $this->assertEquals( 'index.php?rest_route=/nodeinfo2/1.0', $rules['^.well-known/x-nodeinfo2'] ); } /** @@ -175,30 +163,16 @@ public function test_init_guard_prevents_double_initialization() { } /** - * Test activate() method registers rewrite rules. + * Test activate() method initializes plugin and flushes rewrite rules. * * @covers ::activate */ - public function test_activate_registers_rewrite_rules() { - global $wp_rewrite; - - // Save original permalink structure. - $original_structure = $wp_rewrite->permalink_structure; - - // Enable permalinks for testing. - $wp_rewrite->set_permalink_structure( '/%postname%/' ); - - // Call activate. + public function test_activate_initializes_and_flushes() { + // Verify activate() runs without errors and initializes the plugin. \Nodeinfo\Nodeinfo::activate(); - $rules = $wp_rewrite->wp_rewrite_rules(); - - $this->assertIsArray( $rules ); - $this->assertArrayHasKey( '^.well-known/nodeinfo', $rules ); - - // Restore original permalink structure. - $wp_rewrite->set_permalink_structure( $original_structure ); - $wp_rewrite->flush_rules(); + // Verify the rewrite_rules_array filter is registered. + $this->assertNotFalse( has_filter( 'rewrite_rules_array' ) ); } /** From b52bf7fdd01f7a09a9e848ddbd0b2ad43d8811db Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 30 Dec 2025 17:55:58 +0100 Subject: [PATCH 12/12] Update includes/class-nodeinfo.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- includes/class-nodeinfo.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/class-nodeinfo.php b/includes/class-nodeinfo.php index cfc1314..de672cf 100644 --- a/includes/class-nodeinfo.php +++ b/includes/class-nodeinfo.php @@ -173,7 +173,7 @@ public static function activate() { } /** - * Deactivate the plugin. + * Handle plugin deactivation. * * Should be called on plugin deactivation. */