diff --git a/README.md b/README.md index eb5bdf3..52f4b63 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 +* Added backwards compatibility handler for deprecated `wellknown_nodeinfo_data` filter + ### 3.0.0 * Refactored to filter-based architecture for better extensibility diff --git a/includes/class-nodeinfo.php b/includes/class-nodeinfo.php new file mode 100644 index 0000000..de672cf --- /dev/null +++ b/includes/class-nodeinfo.php @@ -0,0 +1,183 @@ +initialized ) { + return; + } + + $this->register_integrations(); + $this->register_hooks(); + + if ( \is_admin() ) { + $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() { + // 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 (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 ); + } + + /** + * 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' + ); + } + + /** + * 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. + * + * @param array $rules The existing rewrite rules. + * @return array The modified rewrite rules. + */ + 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. The rewrite_rules_array + * filter will add our rules during the flush. + */ + public static function activate() { + self::get_instance()->init(); + \flush_rewrite_rules(); + } + + /** + * Handle plugin deactivation. + * + * Should be called on plugin deactivation. + */ + public static function deactivate() { + \flush_rewrite_rules(); + } +} 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 a7f264a..762129c 100644 --- a/nodeinfo.php +++ b/nodeinfo.php @@ -3,13 +3,12 @@ * 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 * License URI: http://opensource.org/licenses/MIT * Text Domain: nodeinfo - * Domain Path: /languages * * @package Nodeinfo */ @@ -32,65 +31,22 @@ 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. - */ -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' ); -} -add_action( 'init', 'nodeinfo_add_rewrite_rules', 1 ); - -/** - * Flush rewrite rules on activation. + * Plugin initialization function. + * + * @return Nodeinfo\Nodeinfo The plugin instance. */ -function nodeinfo_activate() { - nodeinfo_add_rewrite_rules(); - flush_rewrite_rules(); +function nodeinfo_plugin() { + return Nodeinfo\Nodeinfo::get_instance(); } -register_activation_hook( __FILE__, 'nodeinfo_activate' ); -/** - * Flush rewrite rules on deactivation. - */ -register_deactivation_hook( __FILE__, 'flush_rewrite_rules' ); +// 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' ) ); +register_deactivation_hook( __FILE__, array( Nodeinfo\Nodeinfo::class, 'deactivate' ) ); diff --git a/tests/phpunit/tests/class-test-nodeinfo.php b/tests/phpunit/tests/class-test-nodeinfo.php new file mode 100644 index 0000000..63584c3 --- /dev/null +++ b/tests/phpunit/tests/class-test-nodeinfo.php @@ -0,0 +1,191 @@ +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; + + // Save original state. + $original_server = $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/(?P\\d\\.\\d)', $routes ); + + // Restore original state. + $wp_rest_server = $original_server; + } + + /** + * 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() { + $instance = \Nodeinfo\Nodeinfo::get_instance(); + + // Test the filter directly. + $rules = $instance->add_rewrite_rules( array() ); + + $this->assertIsArray( $rules ); + $this->assertArrayHasKey( '^.well-known/nodeinfo', $rules ); + $this->assertArrayHasKey( '^.well-known/x-nodeinfo2', $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'] ); + } + + /** + * 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 can be registered. + * + * @covers ::register_admin_hooks + */ + public function test_admin_hooks_registered() { + // Call register_admin_hooks directly to test it works. + \Nodeinfo\Nodeinfo::get_instance()->register_admin_hooks(); + + $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 initializes plugin and flushes rewrite rules. + * + * @covers ::activate + */ + public function test_activate_initializes_and_flushes() { + // Verify activate() runs without errors and initializes the plugin. + \Nodeinfo\Nodeinfo::activate(); + + // Verify the rewrite_rules_array filter is registered. + $this->assertNotFalse( has_filter( 'rewrite_rules_array' ) ); + } + + /** + * 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 ); + } +}