From a6c0ccf0ba750fe5597650e023969a3632ef8b4d Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 8 Dec 2025 10:21:23 +0100 Subject: [PATCH 01/21] Refactor to filter-based architecture - Add autoloader for PSR-4 style class loading - Split endpoints into controller/ folder (Nodeinfo\Controller namespace) - Add integration/ folder for WordPress-specific implementations - NodeInfo endpoint uses filters for extensibility (other plugins can hook in) - NodeInfo2 endpoint is self-contained with filters for customization - Add Nodeinfo namespace to all classes - Follow WordPress coding standards for file/class naming This aligns with the architecture used in wordpress-activitypub plugin, making it easier for other plugins to extend NodeInfo data via filters. --- includes/class-autoloader.php | 94 ++++++++ includes/class-nodeinfo-endpoint.php | 166 --------------- includes/class-nodeinfo.php | 126 ----------- includes/class-nodeinfo2.php | 108 ---------- includes/controller/class-nodeinfo.php | 272 ++++++++++++++++++++++++ includes/controller/class-nodeinfo2.php | 231 ++++++++++++++++++++ includes/functions.php | 63 +++++- includes/integration/class-nodeinfo.php | 130 +++++++++++ nodeinfo.php | 78 ++++--- 9 files changed, 819 insertions(+), 449 deletions(-) create mode 100644 includes/class-autoloader.php delete mode 100644 includes/class-nodeinfo-endpoint.php delete mode 100644 includes/class-nodeinfo.php delete mode 100644 includes/class-nodeinfo2.php create mode 100644 includes/controller/class-nodeinfo.php create mode 100644 includes/controller/class-nodeinfo2.php create mode 100644 includes/integration/class-nodeinfo.php diff --git a/includes/class-autoloader.php b/includes/class-autoloader.php new file mode 100644 index 0000000..1db959b --- /dev/null +++ b/includes/class-autoloader.php @@ -0,0 +1,94 @@ +prefix = $prefix; + $this->prefix_length = \strlen( $prefix ); + $this->path = \trailingslashit( $path ); + } + + /** + * Registers the autoloader. + * + * @param string $prefix Namespace prefix all classes have in common. + * @param string $path Path to the files to be loaded. + */ + public static function register_path( $prefix, $path ) { + $loader = new self( $prefix, $path ); + \spl_autoload_register( array( $loader, 'load' ) ); + } + + /** + * Loads a class if its namespace starts with `$this->prefix`. + * + * @param string $class_name The class to be loaded. + */ + public function load( $class_name ) { + if ( \strpos( $class_name, $this->prefix . self::NS_SEPARATOR ) !== 0 ) { + return; + } + + // Strip prefix from the start (PSR-4 style). + $class_name = \substr( $class_name, $this->prefix_length + 1 ); + $class_name = \strtolower( $class_name ); + $dir = ''; + + $last_ns_pos = \strripos( $class_name, self::NS_SEPARATOR ); + if ( false !== $last_ns_pos ) { + $namespace = \substr( $class_name, 0, $last_ns_pos ); + $namespace = \str_replace( '_', '-', $namespace ); + $class_name = \substr( $class_name, $last_ns_pos + 1 ); + $dir = \str_replace( self::NS_SEPARATOR, DIRECTORY_SEPARATOR, $namespace ) . DIRECTORY_SEPARATOR; + } + + $class_name = \str_replace( '_', '-', $class_name ); + $path = $this->path . $dir . 'class-' . $class_name . '.php'; + + if ( \file_exists( $path ) ) { + require_once $path; + } + } +} diff --git a/includes/class-nodeinfo-endpoint.php b/includes/class-nodeinfo-endpoint.php deleted file mode 100644 index adb5506..0000000 --- a/includes/class-nodeinfo-endpoint.php +++ /dev/null @@ -1,166 +0,0 @@ - WP_REST_Server::READABLE, - 'callback' => array( 'Nodeinfo_Endpoint', 'render_discovery' ), - 'permission_callback' => '__return_true', - ), - ) - ); - - register_rest_route( - 'nodeinfo', - '/(?P[\.\d]+)', - array( - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( 'Nodeinfo_Endpoint', 'render_nodeinfo' ), - 'permission_callback' => '__return_true', - 'args' => array( - 'version' => array( - 'required' => true, - 'type' => 'string', - 'description' => __( 'The version of the NodeInfo scheme', 'nodeinfo' ), - 'enum' => array( - '1.0', - '1.1', - '2.0', - '2.1', - ), - ), - ), - ), - ) - ); - - register_rest_route( - 'nodeinfo2', - '/(?P[\.\d]+)', - array( - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( 'Nodeinfo_Endpoint', 'render_nodeinfo2' ), - 'permission_callback' => '__return_true', - 'args' => array( - 'version' => array( - 'required' => true, - 'type' => 'string', - 'description' => __( 'The version of the NodeInfo2 scheme', 'nodeinfo' ), - 'enum' => array( - '1.0', - ), - ), - ), - ), - ) - ); - } - - /** - * Render the discovery file. - * - * @param WP_REST_Request $request the request object - * @return WP_REST_Response the response object - */ - public static function render_discovery( WP_REST_Request $request ) { - $discovery = array(); - $discovery['links'] = array( - array( - 'rel' => 'http://nodeinfo.diaspora.software/ns/schema/2.1', - 'href' => get_rest_url( null, '/nodeinfo/2.1' ), - ), - array( - 'rel' => 'http://nodeinfo.diaspora.software/ns/schema/2.0', - 'href' => get_rest_url( null, '/nodeinfo/2.0' ), - ), - array( - 'rel' => 'http://nodeinfo.diaspora.software/ns/schema/1.1', - 'href' => get_rest_url( null, '/nodeinfo/1.1' ), - ), - array( - 'rel' => 'http://nodeinfo.diaspora.software/ns/schema/1.0', - 'href' => get_rest_url( null, '/nodeinfo/1.0' ), - ), - ); - - $discovery = apply_filters( 'wellknown_nodeinfo_data', $discovery ); - - // Create the response object - $response = new WP_REST_Response( $discovery ); - $response->header( 'Content-Type', 'application/json; profile=http://nodeinfo.diaspora.software' ); - - return $response; - } - - /** - * Render the NodeInfo file. - * - * @param WP_REST_Request $request the request object - * @return WP_REST_Response the response object - */ - public static function render_nodeinfo( WP_REST_Request $request ) { - require_once 'class-nodeinfo.php'; - - $nodeinfo = new Nodeinfo( $request->get_param( 'version' ) ); - - // Create the response object - return new WP_REST_Response( $nodeinfo->to_array() ); - } - - /** - * Render the NodeInfo2 file. - * - * @param WP_REST_Request $request the request object - * @return WP_REST_Response the response object - */ - public static function render_nodeinfo2( WP_REST_Request $request ) { - require_once 'class-nodeinfo2.php'; - - $nodeinfo2 = new Nodeinfo2( $request->get_param( 'version' ) ); - - // Create the response object - return new WP_REST_Response( $nodeinfo2->to_array() ); - } - - /** - * Add Host-Meta and WebFinger discovery links - * - * @param array $jrd the JRD file used by Host-Meta and WebFinger - * @return array the extended JRD file - */ - public static function render_jrd( $jrd ) { - $jrd['links'][] = array( - 'rel' => 'http://nodeinfo.diaspora.software/ns/schema/2.1', - 'href' => get_rest_url( null, '/nodeinfo/2.1' ), - ); - - $jrd['links'][] = array( - 'rel' => 'http://nodeinfo.diaspora.software/ns/schema/2.0', - 'href' => get_rest_url( null, '/nodeinfo/2.0' ), - ); - - $jrd['links'][] = array( - 'rel' => 'http://nodeinfo.diaspora.software/ns/schema/1.1', - 'href' => get_rest_url( null, '/nodeinfo/1.1' ), - ); - - $jrd['links'][] = array( - 'rel' => 'http://nodeinfo.diaspora.software/ns/schema/1.0', - 'href' => get_rest_url( null, '/nodeinfo/1.0' ), - ); - - return $jrd; - } -} diff --git a/includes/class-nodeinfo.php b/includes/class-nodeinfo.php deleted file mode 100644 index d31f0c6..0000000 --- a/includes/class-nodeinfo.php +++ /dev/null @@ -1,126 +0,0 @@ - array(), - 'outbound' => array(), - ); - public $protocols = array(); - public $metadata = array(); - - public function __construct( $version = '2.0' ) { - if ( in_array( $version, array( '1.0', '1.1', '2.0', '2.1' ), true ) ) { - $this->version = $version; - } - - $this->generate_software(); - $this->generate_usage(); - $this->generate_protocols(); - $this->generate_services(); - $this->generate_metadata(); - $this->openRegistrations = (boolean) get_option( 'users_can_register', false ); // phpcs:ignore - } - - public function generate_usage() { - $users = get_users( - array( - 'fields' => 'ID', - 'capability__in' => array( 'publish_posts' ), - ) - ); - - if ( is_array( $users ) ) { - $users = count( $users ); - } else { - $users = 1; - } - - $posts = wp_count_posts(); - $comments = wp_count_comments(); - - $this->usage = apply_filters( - 'nodeinfo_data_usage', - array( - 'users' => array( - 'total' => $users, - 'activeMonth' => nodeinfo_get_active_users( '1 month ago' ), - 'activeHalfyear' => nodeinfo_get_active_users( '6 month ago' ), - ), - 'localPosts' => (int) $posts->publish, - 'localComments' => (int) $comments->approved, - ), - $this->version - ); - } - - public function generate_software() { - $software = array( - 'name' => 'wordpress', - 'version' => nodeinfo_get_masked_version(), - ); - - if ( '2.1' === $this->version ) { - $software['repository'] = 'https://github.com/wordpress/wordpress'; - } - - $this->software = apply_filters( - 'nodeinfo_data_software', - $software, - $this->version - ); - } - - public function generate_protocols() { - $protocols = $this->protocols; - - if ( version_compare( $this->version, '2.0', '>=' ) ) { - $protocols = array(); - } else { - $protocols['inbound'] = array( 'smtp' ); - $protocols['outbound'] = array( 'smtp' ); - } - - $this->protocols = apply_filters( 'nodeinfo_data_protocols', $protocols, $this->version ); - } - - public function generate_services() { - $services = $this->services; - - if ( version_compare( $this->version, '2.0', '>=' ) ) { - $services['inbound'] = array( 'atom1.0', 'rss2.0', 'pop3' ); - $services['outbound'] = array( 'atom1.0', 'rss2.0', 'wordpress', 'smtp' ); - } else { - $services['outbound'] = array( 'smtp' ); - } - - $this->services = apply_filters( 'nodeinfo_data_services', $services, $this->version ); - } - - public function generate_metadata() { - $metadata = $this->metadata; - - $metadata['generator'] = array( - 'name' => 'NodeInfo WordPress-Plugin', - 'version' => nodeinfo_version(), - 'repository' => 'https://github.com/pfefferle/wordpress-nodeinfo/', - ); - - $metadata['nodeName'] = \get_bloginfo( 'name' ); - $metadata['nodeDescription'] = \get_bloginfo( 'description' ); - $metadata['nodeIcon'] = \get_site_icon_url(); - - $this->metadata = apply_filters( 'nodeinfo_data_metadata', $metadata, $this->version ); - } - - public function to_array() { - return apply_filters( 'nodeinfo_data', get_object_vars( $this ), $this->version ); - } -} diff --git a/includes/class-nodeinfo2.php b/includes/class-nodeinfo2.php deleted file mode 100644 index 5f6d8fe..0000000 --- a/includes/class-nodeinfo2.php +++ /dev/null @@ -1,108 +0,0 @@ - array(), - 'outbound' => array(), - ); - public $protocols = array(); - public $metadata = array(); - - public function __construct( $version = '1.0' ) { - if ( in_array( $version, array( '1.0' ), true ) ) { - $this->version = $version; - } - - $this->generate_server(); - $this->generate_usage(); - $this->generate_protocols(); - $this->generate_services(); - $this->generate_metadata(); - $this->openRegistrations = (boolean) get_option( 'users_can_register', false ); // phpcs:ignore - } - - public function generate_usage() { - $users = get_users( - array( - 'capability__in' => array( 'publish_posts' ), - ) - ); - - if ( is_array( $users ) ) { - $users = count( $users ); - } else { - $users = 1; - } - - $posts = wp_count_posts(); - $comments = wp_count_comments(); - - $this->usage = apply_filters( - 'nodeinfo2_data_usage', - array( - 'users' => array( - 'total' => $users, - 'activeMonth' => nodeinfo_get_active_users( '1 month ago' ), - 'activeHalfyear' => nodeinfo_get_active_users( '6 month ago' ), - ), - 'localPosts' => (int) $posts->publish, - 'localComments' => (int) $comments->approved, - ), - $this->version - ); - } - - public function generate_server() { - $this->server = apply_filters( - 'nodeinfo2_data_server', - array( - 'baseUrl' => home_url( '/' ), - 'name' => get_bloginfo( 'name' ), - 'software' => 'wordpress', - 'version' => nodeinfo_get_masked_version(), - ), - $this->version - ); - } - - public function generate_protocols() { - $this->protocols = apply_filters( 'nodeinfo2_data_protocols', $this->protocols, $this->version ); - } - - public function generate_services() { - $services = $this->services; - - $services['inbound'] = array( 'atom1.0', 'rss2.0', 'wordpress', 'pop3' ); - $services['outbound'] = array( 'atom1.0', 'rss2.0', 'wordpress', 'smtp' ); - - $this->services = apply_filters( 'nodeinfo2_data_services', $services, $this->version ); - } - - public function generate_metadata() { - $metadata = $this->metadata; - - $metadata['generator'] = array( - 'name' => 'NodeInfo WordPress-Plugin', - 'version' => nodeinfo_version(), - 'repository' => 'https://github.com/pfefferle/wordpress-nodeinfo/', - ); - - $metadata['nodeName'] = \get_bloginfo( 'name' ); - $metadata['nodeDescription'] = \get_bloginfo( 'description' ); - $metadata['nodeIcon'] = \get_site_icon_url(); - - $this->metadata = apply_filters( 'nodeinfo2_data_metadata', $metadata, $this->version ); - } - - public function to_array() { - return apply_filters( 'nodeinfo2_data', get_object_vars( $this ), $this->version ); - } -} diff --git a/includes/controller/class-nodeinfo.php b/includes/controller/class-nodeinfo.php new file mode 100644 index 0000000..52dc32d --- /dev/null +++ b/includes/controller/class-nodeinfo.php @@ -0,0 +1,272 @@ +namespace, + '/discovery', + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_discovery' ), + 'permission_callback' => '__return_true', + ), + ) + ); + + register_rest_route( + $this->namespace, + '/(?P\d\.\d)', + array( + 'args' => array( + 'version' => array( + 'description' => __( 'The NodeInfo schema version.', 'nodeinfo' ), + 'type' => 'string', + 'enum' => array( '1.0', '1.1', '2.0', '2.1' ), + 'required' => true, + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => '__return_true', + ), + 'schema' => array( $this, 'get_item_schema' ), + ) + ); + } + + /** + * Retrieves the discovery document. + * + * @param \WP_REST_Request $request The request object. + * @return WP_REST_Response The response object. + */ + public function get_discovery( $request ) { + $discovery = array( + 'links' => array( + array( + 'rel' => 'http://nodeinfo.diaspora.software/ns/schema/2.1', + 'href' => get_rest_url( null, '/nodeinfo/2.1' ), + ), + array( + 'rel' => 'http://nodeinfo.diaspora.software/ns/schema/2.0', + 'href' => get_rest_url( null, '/nodeinfo/2.0' ), + ), + array( + 'rel' => 'http://nodeinfo.diaspora.software/ns/schema/1.1', + 'href' => get_rest_url( null, '/nodeinfo/1.1' ), + ), + array( + 'rel' => 'http://nodeinfo.diaspora.software/ns/schema/1.0', + 'href' => get_rest_url( null, '/nodeinfo/1.0' ), + ), + ), + ); + + /** + * Filters the NodeInfo discovery document. + * + * @param array $discovery The discovery document. + */ + $discovery = apply_filters( 'nodeinfo_discovery', $discovery ); + + $response = new WP_REST_Response( $discovery ); + $response->header( 'Content-Type', 'application/json; profile=http://nodeinfo.diaspora.software' ); + + return $response; + } + + /** + * Retrieves NodeInfo for a specific version. + * + * @param \WP_REST_Request $request The request object. + * @return WP_REST_Response The response object. + */ + public function get_item( $request ) { + $version = $request->get_param( 'version' ); + + $nodeinfo = array( + 'version' => $version, + 'software' => apply_filters( 'nodeinfo_data_software', array(), $version ), + 'protocols' => apply_filters( 'nodeinfo_data_protocols', array(), $version ), + 'services' => apply_filters( + 'nodeinfo_data_services', + array( + 'inbound' => array(), + 'outbound' => array(), + ), + $version + ), + 'openRegistrations' => (bool) get_option( 'users_can_register', false ), + 'usage' => apply_filters( 'nodeinfo_data_usage', array(), $version ), + 'metadata' => apply_filters( 'nodeinfo_data_metadata', array(), $version ), + ); + + /** + * Filters the complete NodeInfo response. + * + * @param array $nodeinfo The NodeInfo data. + * @param string $version The NodeInfo version. + */ + $nodeinfo = apply_filters( 'nodeinfo_data', $nodeinfo, $version ); + + return new WP_REST_Response( $nodeinfo ); + } + + /** + * Adds Host-Meta and WebFinger discovery links. + * + * @param array $jrd The JRD document. + * @return array The modified JRD document. + */ + public static function jrd( $jrd ) { + $jrd['links'][] = array( + 'rel' => 'http://nodeinfo.diaspora.software/ns/schema/2.1', + 'href' => get_rest_url( null, '/nodeinfo/2.1' ), + ); + + $jrd['links'][] = array( + 'rel' => 'http://nodeinfo.diaspora.software/ns/schema/2.0', + 'href' => get_rest_url( null, '/nodeinfo/2.0' ), + ); + + $jrd['links'][] = array( + 'rel' => 'http://nodeinfo.diaspora.software/ns/schema/1.1', + 'href' => get_rest_url( null, '/nodeinfo/1.1' ), + ); + + $jrd['links'][] = array( + 'rel' => 'http://nodeinfo.diaspora.software/ns/schema/1.0', + 'href' => get_rest_url( null, '/nodeinfo/1.0' ), + ); + + return $jrd; + } + + /** + * Retrieves the NodeInfo schema. + * + * @return array The schema data. + */ + public function get_item_schema() { + return array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'nodeinfo', + 'type' => 'object', + 'properties' => array( + 'version' => array( + 'description' => __( 'The NodeInfo schema version.', 'nodeinfo' ), + 'type' => 'string', + 'enum' => array( '1.0', '1.1', '2.0', '2.1' ), + ), + 'software' => array( + 'description' => __( 'Metadata about server software in use.', 'nodeinfo' ), + 'type' => 'object', + 'properties' => array( + 'name' => array( + 'type' => 'string', + ), + 'version' => array( + 'type' => 'string', + ), + 'homepage' => array( + 'type' => 'string', + 'format' => 'uri', + ), + 'repository' => array( + 'type' => 'string', + 'format' => 'uri', + ), + ), + ), + 'protocols' => array( + 'description' => __( 'The protocols supported on this server.', 'nodeinfo' ), + 'type' => 'array', + 'items' => array( + 'type' => 'string', + ), + ), + 'services' => array( + 'description' => __( 'Third party sites this server can connect to.', 'nodeinfo' ), + 'type' => 'object', + 'properties' => array( + 'inbound' => array( + 'type' => 'array', + 'items' => array( + 'type' => 'string', + ), + ), + 'outbound' => array( + 'type' => 'array', + 'items' => array( + 'type' => 'string', + ), + ), + ), + ), + 'openRegistrations' => array( + 'description' => __( 'Whether this server allows open self-registration.', 'nodeinfo' ), + 'type' => 'boolean', + ), + 'usage' => array( + 'description' => __( 'Usage statistics for this server.', 'nodeinfo' ), + 'type' => 'object', + 'properties' => array( + 'users' => array( + 'type' => 'object', + 'properties' => array( + 'total' => array( + 'type' => 'integer', + ), + 'activeMonth' => array( + 'type' => 'integer', + ), + 'activeHalfyear' => array( + 'type' => 'integer', + ), + ), + ), + 'localPosts' => array( + 'type' => 'integer', + ), + 'localComments' => array( + 'type' => 'integer', + ), + ), + ), + 'metadata' => array( + 'description' => __( 'Free form key value pairs for software specific values.', 'nodeinfo' ), + 'type' => 'object', + ), + ), + ); + } +} diff --git a/includes/controller/class-nodeinfo2.php b/includes/controller/class-nodeinfo2.php new file mode 100644 index 0000000..63ea636 --- /dev/null +++ b/includes/controller/class-nodeinfo2.php @@ -0,0 +1,231 @@ +namespace, + '/(?P\d\.\d)', + array( + 'args' => array( + 'version' => array( + 'description' => __( 'The NodeInfo2 schema version.', 'nodeinfo' ), + 'type' => 'string', + 'enum' => array( '1.0' ), + 'required' => true, + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => '__return_true', + ), + 'schema' => array( $this, 'get_item_schema' ), + ) + ); + } + + /** + * Retrieves NodeInfo2 data. + * + * @param \WP_REST_Request $request The request object. + * @return WP_REST_Response The response object. + */ + public function get_item( $request ) { + $version = $request->get_param( 'version' ); + + $users = get_users( + array( + 'capability__in' => array( 'publish_posts' ), + ) + ); + + $user_count = is_array( $users ) ? count( $users ) : 1; + + $posts = wp_count_posts(); + $comments = wp_count_comments(); + + $nodeinfo2 = array( + 'version' => $version, + 'server' => apply_filters( + 'nodeinfo2_data_server', + array( + 'baseUrl' => home_url( '/' ), + 'name' => get_bloginfo( 'name' ), + 'software' => 'wordpress', + 'version' => get_masked_version(), + ), + $version + ), + 'protocols' => apply_filters( 'nodeinfo2_data_protocols', array(), $version ), + 'services' => apply_filters( + 'nodeinfo2_data_services', + array( + 'inbound' => array( 'atom1.0', 'rss2.0', 'pop3' ), + 'outbound' => array( 'atom1.0', 'rss2.0', 'wordpress', 'smtp' ), + ), + $version + ), + 'openRegistrations' => (bool) get_option( 'users_can_register', false ), + 'usage' => apply_filters( + 'nodeinfo2_data_usage', + array( + 'users' => array( + 'total' => $user_count, + 'activeMonth' => get_active_users( '1 month ago' ), + 'activeHalfyear' => get_active_users( '6 month ago' ), + ), + 'localPosts' => (int) $posts->publish, + 'localComments' => (int) $comments->approved, + ), + $version + ), + 'metadata' => apply_filters( + 'nodeinfo2_data_metadata', + array( + 'nodeName' => get_bloginfo( 'name' ), + 'nodeDescription' => get_bloginfo( 'description' ), + 'nodeIcon' => get_site_icon_url(), + ), + $version + ), + ); + + /** + * Filters the complete NodeInfo2 response. + * + * @param array $nodeinfo2 The NodeInfo2 data. + * @param string $version The NodeInfo2 version. + */ + $nodeinfo2 = apply_filters( 'nodeinfo2_data', $nodeinfo2, $version ); + + return new WP_REST_Response( $nodeinfo2 ); + } + + /** + * Retrieves the NodeInfo2 schema. + * + * @return array The schema data. + */ + public function get_item_schema() { + return array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'nodeinfo2', + 'type' => 'object', + 'properties' => array( + 'version' => array( + 'description' => __( 'The NodeInfo2 schema version.', 'nodeinfo' ), + 'type' => 'string', + 'enum' => array( '1.0' ), + ), + 'server' => array( + 'description' => __( 'Metadata about the server.', 'nodeinfo' ), + 'type' => 'object', + 'properties' => array( + 'baseUrl' => array( + 'type' => 'string', + 'format' => 'uri', + ), + 'name' => array( + 'type' => 'string', + ), + 'software' => array( + 'type' => 'string', + ), + 'version' => array( + 'type' => 'string', + ), + ), + ), + 'protocols' => array( + 'description' => __( 'The protocols supported on this server.', 'nodeinfo' ), + 'type' => 'array', + 'items' => array( + 'type' => 'string', + ), + ), + 'services' => array( + 'description' => __( 'Third party sites this server can connect to.', 'nodeinfo' ), + 'type' => 'object', + 'properties' => array( + 'inbound' => array( + 'type' => 'array', + 'items' => array( + 'type' => 'string', + ), + ), + 'outbound' => array( + 'type' => 'array', + 'items' => array( + 'type' => 'string', + ), + ), + ), + ), + 'openRegistrations' => array( + 'description' => __( 'Whether this server allows open self-registration.', 'nodeinfo' ), + 'type' => 'boolean', + ), + 'usage' => array( + 'description' => __( 'Usage statistics for this server.', 'nodeinfo' ), + 'type' => 'object', + 'properties' => array( + 'users' => array( + 'type' => 'object', + 'properties' => array( + 'total' => array( + 'type' => 'integer', + ), + 'activeMonth' => array( + 'type' => 'integer', + ), + 'activeHalfyear' => array( + 'type' => 'integer', + ), + ), + ), + 'localPosts' => array( + 'type' => 'integer', + ), + 'localComments' => array( + 'type' => 'integer', + ), + ), + ), + 'metadata' => array( + 'description' => __( 'Free form key value pairs for software specific values.', 'nodeinfo' ), + 'type' => 'object', + ), + ), + ); + } +} diff --git a/includes/functions.php b/includes/functions.php index 2252d83..bd14b88 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -1,6 +1,19 @@ 'post', @@ -20,7 +33,6 @@ function nodeinfo_get_active_users( $duration = '1 month ago' ) { return 0; } - // get all distinct ID from $posts return count( array_unique( wp_list_pluck( @@ -32,17 +44,54 @@ function nodeinfo_get_active_users( $duration = '1 month ago' ) { } /** - * Get the masked WordPress version to only show the major and minor version. + * Gets the masked WordPress version (major.minor only). * * @return string The masked version. */ -function nodeinfo_get_masked_version() { - // only show the major and minor version +function get_masked_version() { $version = get_bloginfo( 'version' ); - // strip the RC or beta part + // Strip RC/beta suffixes. $version = preg_replace( '/-.*$/', '', $version ); $version = explode( '.', $version ); $version = array_slice( $version, 0, 2 ); return implode( '.', $version ); } + +/** + * Gets the plugin version. + * + * @return string The plugin version. + */ +function get_plugin_version() { + $meta = get_plugin_meta( array( 'Version' => 'Version' ) ); + + return $meta['Version']; +} + +/** + * Gets plugin metadata. + * + * @param array $default_headers Optional headers to retrieve. + * @return array The plugin metadata. + */ +function get_plugin_meta( $default_headers = array() ) { + if ( ! $default_headers ) { + $default_headers = array( + 'Name' => 'Plugin Name', + 'PluginURI' => 'Plugin URI', + 'Version' => 'Version', + 'Description' => 'Description', + 'Author' => 'Author', + 'AuthorURI' => 'Author URI', + 'TextDomain' => 'Text Domain', + 'DomainPath' => 'Domain Path', + 'Network' => 'Network', + 'RequiresWP' => 'Requires at least', + 'RequiresPHP' => 'Requires PHP', + 'UpdateURI' => 'Update URI', + ); + } + + return \get_file_data( NODEINFO_PLUGIN_FILE, $default_headers, 'plugin' ); +} diff --git a/includes/integration/class-nodeinfo.php b/includes/integration/class-nodeinfo.php new file mode 100644 index 0000000..c117e8f --- /dev/null +++ b/includes/integration/class-nodeinfo.php @@ -0,0 +1,130 @@ + 'ID', + 'capability__in' => array( 'publish_posts' ), + ) + ); + + $user_count = is_array( $users ) ? count( $users ) : 1; + + $posts = wp_count_posts(); + $comments = wp_count_comments(); + + $usage['users'] = array( + 'total' => $user_count, + 'activeMonth' => get_active_users( '1 month ago' ), + 'activeHalfyear' => get_active_users( '6 month ago' ), + ); + + $usage['localPosts'] = (int) $posts->publish; + $usage['localComments'] = (int) $comments->approved; + + return $usage; + } + + /** + * Adds protocols. + * + * @param array $protocols The protocols data. + * @param string $version The NodeInfo version. + * @return array The modified protocols data. + */ + public static function protocols( $protocols, $version ) { + // NodeInfo 1.x uses inbound/outbound structure for protocols. + if ( version_compare( $version, '2.0', '<' ) ) { + $protocols['inbound'] = array( 'smtp' ); + $protocols['outbound'] = array( 'smtp' ); + } + + return $protocols; + } + + /** + * Adds services. + * + * @param array $services The services data. + * @param string $version The NodeInfo version. + * @return array The modified services data. + */ + public static function services( $services, $version ) { + if ( version_compare( $version, '2.0', '>=' ) ) { + $services['inbound'] = array( 'atom1.0', 'rss2.0', 'pop3' ); + $services['outbound'] = array( 'atom1.0', 'rss2.0', 'wordpress', 'smtp' ); + } else { + $services['outbound'] = array( 'smtp' ); + } + + return $services; + } + + /** + * Adds metadata. + * + * @param array $metadata The metadata. + * @param string $version The NodeInfo version. + * @return array The modified metadata. + */ + public static function metadata( $metadata, $version ) { + $metadata['nodeName'] = get_bloginfo( 'name' ); + $metadata['nodeDescription'] = get_bloginfo( 'description' ); + $metadata['nodeIcon'] = get_site_icon_url(); + + return $metadata; + } +} diff --git a/nodeinfo.php b/nodeinfo.php index e59c1fc..1fba26b 100644 --- a/nodeinfo.php +++ b/nodeinfo.php @@ -12,34 +12,50 @@ * Domain Path: /languages */ +defined( 'ABSPATH' ) || exit; + +define( 'NODEINFO_PLUGIN_FILE', __FILE__ ); +define( 'NODEINFO_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); + +// Require the autoloader. +require_once NODEINFO_PLUGIN_DIR . 'includes/class-autoloader.php'; + +// Register the autoloader. +Nodeinfo\Autoloader::register_path( 'Nodeinfo', NODEINFO_PLUGIN_DIR . 'includes' ); + +// Require global functions. +require_once NODEINFO_PLUGIN_DIR . 'includes/functions.php'; + /** - * Initialize plugin + * Initialize the plugin. */ function nodeinfo_init() { - require_once __DIR__ . '/includes/class-nodeinfo-endpoint.php'; - require_once __DIR__ . '/includes/functions.php'; + // Initialize integrations. + Nodeinfo\Integration\Nodeinfo::init(); - // Configure the REST API route - add_action( 'rest_api_init', array( 'Nodeinfo_Endpoint', 'register_routes' ) ); + // Register REST routes. + add_action( 'rest_api_init', 'nodeinfo_register_routes' ); - // Add Webmention and Host-Meta discovery - add_filter( 'webfinger_user_data', array( 'Nodeinfo_Endpoint', 'render_jrd' ), 10, 3 ); - add_filter( 'webfinger_post_data', array( 'Nodeinfo_Endpoint', 'render_jrd' ), 10, 3 ); - add_filter( 'host_meta', array( 'Nodeinfo_Endpoint', 'render_jrd' ) ); + // 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 ); /** - * Plugin Version Number. + * Register REST API routes. */ -function nodeinfo_version() { - $meta = nodeinfo_get_plugin_meta( array( 'Version' => 'Version' ) ); +function nodeinfo_register_routes() { + $nodeinfo_controller = new Nodeinfo\Controller\Nodeinfo(); + $nodeinfo_controller->register_routes(); - return $meta['Version']; + $nodeinfo2_controller = new Nodeinfo\Controller\Nodeinfo2(); + $nodeinfo2_controller->register_routes(); } /** - * Add rewrite rules + * 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' ); @@ -48,37 +64,15 @@ function nodeinfo_add_rewrite_rules() { add_action( 'init', 'nodeinfo_add_rewrite_rules', 1 ); /** - * `get_plugin_data` wrapper - * - * @return array the plugin metadata array + * Flush rewrite rules on activation. */ -function nodeinfo_get_plugin_meta( $default_headers = array() ) { - if ( ! $default_headers ) { - $default_headers = array( - 'Name' => 'Plugin Name', - 'PluginURI' => 'Plugin URI', - 'Version' => 'Version', - 'Description' => 'Description', - 'Author' => 'Author', - 'AuthorURI' => 'Author URI', - 'TextDomain' => 'Text Domain', - 'DomainPath' => 'Domain Path', - 'Network' => 'Network', - 'RequiresWP' => 'Requires at least', - 'RequiresPHP' => 'Requires PHP', - 'UpdateURI' => 'Update URI', - ); - } - - return get_file_data( __FILE__, $default_headers, 'plugin' ); +function nodeinfo_activate() { + nodeinfo_add_rewrite_rules(); + flush_rewrite_rules(); } +register_activation_hook( __FILE__, 'nodeinfo_activate' ); /** - * Flush rewrite rules; + * Flush rewrite rules on deactivation. */ -function nodeinfo_flush_rewrite_rules() { - nodeinfo_add_rewrite_rules(); - flush_rewrite_rules(); -} -register_activation_hook( __FILE__, 'nodeinfo_flush_rewrite_rules' ); register_deactivation_hook( __FILE__, 'flush_rewrite_rules' ); From 216b1b81f618f1c527bf972643d0456d350e72a1 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 8 Dec 2025 10:30:27 +0100 Subject: [PATCH 02/21] Add per-version integration classes Each NodeInfo version now has its own integration class that: - Registers its version to the endpoint enum - Adds its discovery and JRD links - Provides version-specific schema - Implements version-specific data via filters This allows other plugins to add new NodeInfo versions or modify existing ones by hooking into the appropriate filters. New filters: - nodeinfo_versions: Register supported versions - nodeinfo_discovery_links: Add discovery document links - nodeinfo_jrd_links: Add WebFinger/Host-Meta links - nodeinfo_schema: Modify the JSON schema --- includes/controller/class-nodeinfo.php | 178 ++++-------- includes/integration/class-nodeinfo10.php | 267 ++++++++++++++++++ ...lass-nodeinfo.php => class-nodeinfo11.php} | 144 +++++++--- includes/integration/class-nodeinfo20.php | 239 ++++++++++++++++ includes/integration/class-nodeinfo21.php | 248 ++++++++++++++++ nodeinfo.php | 7 +- 6 files changed, 910 insertions(+), 173 deletions(-) create mode 100644 includes/integration/class-nodeinfo10.php rename includes/integration/{class-nodeinfo.php => class-nodeinfo11.php} (59%) create mode 100644 includes/integration/class-nodeinfo20.php create mode 100644 includes/integration/class-nodeinfo21.php diff --git a/includes/controller/class-nodeinfo.php b/includes/controller/class-nodeinfo.php index 52dc32d..34b76f3 100644 --- a/includes/controller/class-nodeinfo.php +++ b/includes/controller/class-nodeinfo.php @@ -14,7 +14,8 @@ /** * NodeInfo REST Controller class. * - * Handles NodeInfo discovery and versioned endpoints (1.0, 1.1, 2.0, 2.1). + * Handles NodeInfo discovery and versioned endpoints. + * Versions are registered dynamically via filters. */ class Nodeinfo extends WP_REST_Controller { @@ -41,6 +42,12 @@ public function register_routes() { ) ); + $versions = $this->get_versions(); + + if ( empty( $versions ) ) { + return; + } + register_rest_route( $this->namespace, '/(?P\d\.\d)', @@ -49,7 +56,7 @@ public function register_routes() { 'version' => array( 'description' => __( 'The NodeInfo schema version.', 'nodeinfo' ), 'type' => 'string', - 'enum' => array( '1.0', '1.1', '2.0', '2.1' ), + 'enum' => $versions, 'required' => true, ), ), @@ -63,6 +70,20 @@ public function register_routes() { ); } + /** + * Gets registered NodeInfo versions. + * + * @return array List of version strings. + */ + protected function get_versions() { + /** + * Filters the list of supported NodeInfo versions. + * + * @param array $versions List of version strings (e.g., '2.0', '2.1'). + */ + return apply_filters( 'nodeinfo_versions', array() ); + } + /** * Retrieves the discovery document. * @@ -70,26 +91,16 @@ public function register_routes() { * @return WP_REST_Response The response object. */ public function get_discovery( $request ) { - $discovery = array( - 'links' => array( - array( - 'rel' => 'http://nodeinfo.diaspora.software/ns/schema/2.1', - 'href' => get_rest_url( null, '/nodeinfo/2.1' ), - ), - array( - 'rel' => 'http://nodeinfo.diaspora.software/ns/schema/2.0', - 'href' => get_rest_url( null, '/nodeinfo/2.0' ), - ), - array( - 'rel' => 'http://nodeinfo.diaspora.software/ns/schema/1.1', - 'href' => get_rest_url( null, '/nodeinfo/1.1' ), - ), - array( - 'rel' => 'http://nodeinfo.diaspora.software/ns/schema/1.0', - 'href' => get_rest_url( null, '/nodeinfo/1.0' ), - ), - ), - ); + $links = array(); + + /** + * Filters the NodeInfo discovery links. + * + * @param array $links The discovery links. + */ + $links = apply_filters( 'nodeinfo_discovery_links', $links ); + + $discovery = array( 'links' => $links ); /** * Filters the NodeInfo discovery document. @@ -148,25 +159,16 @@ public function get_item( $request ) { * @return array The modified JRD document. */ public static function jrd( $jrd ) { - $jrd['links'][] = array( - 'rel' => 'http://nodeinfo.diaspora.software/ns/schema/2.1', - 'href' => get_rest_url( null, '/nodeinfo/2.1' ), - ); - - $jrd['links'][] = array( - 'rel' => 'http://nodeinfo.diaspora.software/ns/schema/2.0', - 'href' => get_rest_url( null, '/nodeinfo/2.0' ), - ); - - $jrd['links'][] = array( - 'rel' => 'http://nodeinfo.diaspora.software/ns/schema/1.1', - 'href' => get_rest_url( null, '/nodeinfo/1.1' ), - ); + /** + * Filters the NodeInfo JRD links for WebFinger/Host-Meta. + * + * @param array $links The JRD links. + */ + $links = apply_filters( 'nodeinfo_jrd_links', array() ); - $jrd['links'][] = array( - 'rel' => 'http://nodeinfo.diaspora.software/ns/schema/1.0', - 'href' => get_rest_url( null, '/nodeinfo/1.0' ), - ); + foreach ( $links as $link ) { + $jrd['links'][] = $link; + } return $jrd; } @@ -177,96 +179,18 @@ public static function jrd( $jrd ) { * @return array The schema data. */ public function get_item_schema() { - return array( + $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'nodeinfo', 'type' => 'object', - 'properties' => array( - 'version' => array( - 'description' => __( 'The NodeInfo schema version.', 'nodeinfo' ), - 'type' => 'string', - 'enum' => array( '1.0', '1.1', '2.0', '2.1' ), - ), - 'software' => array( - 'description' => __( 'Metadata about server software in use.', 'nodeinfo' ), - 'type' => 'object', - 'properties' => array( - 'name' => array( - 'type' => 'string', - ), - 'version' => array( - 'type' => 'string', - ), - 'homepage' => array( - 'type' => 'string', - 'format' => 'uri', - ), - 'repository' => array( - 'type' => 'string', - 'format' => 'uri', - ), - ), - ), - 'protocols' => array( - 'description' => __( 'The protocols supported on this server.', 'nodeinfo' ), - 'type' => 'array', - 'items' => array( - 'type' => 'string', - ), - ), - 'services' => array( - 'description' => __( 'Third party sites this server can connect to.', 'nodeinfo' ), - 'type' => 'object', - 'properties' => array( - 'inbound' => array( - 'type' => 'array', - 'items' => array( - 'type' => 'string', - ), - ), - 'outbound' => array( - 'type' => 'array', - 'items' => array( - 'type' => 'string', - ), - ), - ), - ), - 'openRegistrations' => array( - 'description' => __( 'Whether this server allows open self-registration.', 'nodeinfo' ), - 'type' => 'boolean', - ), - 'usage' => array( - 'description' => __( 'Usage statistics for this server.', 'nodeinfo' ), - 'type' => 'object', - 'properties' => array( - 'users' => array( - 'type' => 'object', - 'properties' => array( - 'total' => array( - 'type' => 'integer', - ), - 'activeMonth' => array( - 'type' => 'integer', - ), - 'activeHalfyear' => array( - 'type' => 'integer', - ), - ), - ), - 'localPosts' => array( - 'type' => 'integer', - ), - 'localComments' => array( - 'type' => 'integer', - ), - ), - ), - 'metadata' => array( - 'description' => __( 'Free form key value pairs for software specific values.', 'nodeinfo' ), - 'type' => 'object', - ), - ), + 'properties' => array(), ); + + /** + * Filters the NodeInfo schema. + * + * @param array $schema The schema data. + */ + return apply_filters( 'nodeinfo_schema', $schema ); } } diff --git a/includes/integration/class-nodeinfo10.php b/includes/integration/class-nodeinfo10.php new file mode 100644 index 0000000..27c9508 --- /dev/null +++ b/includes/integration/class-nodeinfo10.php @@ -0,0 +1,267 @@ + 'http://nodeinfo.diaspora.software/ns/schema/' . self::VERSION, + 'href' => get_rest_url( null, '/nodeinfo/' . self::VERSION ), + ); + return $links; + } + + /** + * Adds the JRD link. + * + * @param array $links The JRD links. + * @return array The modified links. + */ + public static function jrd_link( $links ) { + $links[] = array( + 'rel' => 'http://nodeinfo.diaspora.software/ns/schema/' . self::VERSION, + 'href' => get_rest_url( null, '/nodeinfo/' . self::VERSION ), + ); + return $links; + } + + /** + * Adds the schema for this version. + * + * @param array $schema The schema. + * @return array The modified schema. + */ + public static function schema( $schema ) { + $schema['properties'] = array_merge( + $schema['properties'], + array( + 'version' => array( + 'description' => __( 'The NodeInfo schema version.', 'nodeinfo' ), + 'type' => 'string', + ), + 'software' => array( + 'description' => __( 'Metadata about server software in use.', 'nodeinfo' ), + 'type' => 'object', + 'properties' => array( + 'name' => array( 'type' => 'string' ), + 'version' => array( 'type' => 'string' ), + ), + ), + 'protocols' => array( + 'description' => __( 'The protocols supported on this server.', 'nodeinfo' ), + 'type' => 'object', + 'properties' => array( + 'inbound' => array( + 'type' => 'array', + 'items' => array( 'type' => 'string' ), + ), + 'outbound' => array( + 'type' => 'array', + 'items' => array( 'type' => 'string' ), + ), + ), + ), + 'services' => array( + 'description' => __( 'Third party sites this server can connect to.', 'nodeinfo' ), + 'type' => 'object', + 'properties' => array( + 'inbound' => array( + 'type' => 'array', + 'items' => array( 'type' => 'string' ), + ), + 'outbound' => array( + 'type' => 'array', + 'items' => array( 'type' => 'string' ), + ), + ), + ), + 'openRegistrations' => array( + 'description' => __( 'Whether this server allows open self-registration.', 'nodeinfo' ), + 'type' => 'boolean', + ), + 'usage' => array( + 'description' => __( 'Usage statistics for this server.', 'nodeinfo' ), + 'type' => 'object', + 'properties' => array( + 'users' => array( + 'type' => 'object', + 'properties' => array( + 'total' => array( 'type' => 'integer' ), + 'activeMonth' => array( 'type' => 'integer' ), + 'activeHalfyear' => array( 'type' => 'integer' ), + ), + ), + 'localPosts' => array( 'type' => 'integer' ), + 'localComments' => array( 'type' => 'integer' ), + ), + ), + 'metadata' => array( + 'description' => __( 'Free form key value pairs for software specific values.', 'nodeinfo' ), + 'type' => 'object', + ), + ) + ); + + return $schema; + } + + /** + * Adds software information. + * + * @param array $software The software data. + * @param string $version The NodeInfo version. + * @return array The modified software data. + */ + public static function software( $software, $version ) { + if ( self::VERSION !== $version ) { + return $software; + } + + $software['name'] = 'wordpress'; + $software['version'] = get_masked_version(); + + return $software; + } + + /** + * Adds protocols. + * + * @param array $protocols The protocols data. + * @param string $version The NodeInfo version. + * @return array The modified protocols data. + */ + public static function protocols( $protocols, $version ) { + if ( self::VERSION !== $version ) { + return $protocols; + } + + // NodeInfo 1.0 uses inbound/outbound structure. + $protocols['inbound'] = array(); + $protocols['outbound'] = array(); + + return $protocols; + } + + /** + * Adds services. + * + * @param array $services The services data. + * @param string $version The NodeInfo version. + * @return array The modified services data. + */ + public static function services( $services, $version ) { + if ( self::VERSION !== $version ) { + return $services; + } + + $services['inbound'] = array(); + $services['outbound'] = array( 'smtp' ); + + return $services; + } + + /** + * Adds usage statistics. + * + * @param array $usage The usage data. + * @param string $version The NodeInfo version. + * @return array The modified usage data. + */ + public static function usage( $usage, $version ) { + if ( self::VERSION !== $version ) { + return $usage; + } + + $users = get_users( + array( + 'fields' => 'ID', + 'capability__in' => array( 'publish_posts' ), + ) + ); + + $user_count = is_array( $users ) ? count( $users ) : 1; + + $posts = wp_count_posts(); + $comments = wp_count_comments(); + + $usage['users'] = array( + 'total' => $user_count, + 'activeMonth' => get_active_users( '1 month ago' ), + 'activeHalfyear' => get_active_users( '6 month ago' ), + ); + + $usage['localPosts'] = (int) $posts->publish; + $usage['localComments'] = (int) $comments->approved; + + return $usage; + } + + /** + * Adds metadata. + * + * @param array $metadata The metadata. + * @param string $version The NodeInfo version. + * @return array The modified metadata. + */ + public static function metadata( $metadata, $version ) { + if ( self::VERSION !== $version ) { + return $metadata; + } + + $metadata['nodeName'] = get_bloginfo( 'name' ); + $metadata['nodeDescription'] = get_bloginfo( 'description' ); + $metadata['nodeIcon'] = get_site_icon_url(); + + return $metadata; + } +} diff --git a/includes/integration/class-nodeinfo.php b/includes/integration/class-nodeinfo11.php similarity index 59% rename from includes/integration/class-nodeinfo.php rename to includes/integration/class-nodeinfo11.php index c117e8f..c0d113c 100644 --- a/includes/integration/class-nodeinfo.php +++ b/includes/integration/class-nodeinfo11.php @@ -1,10 +1,9 @@ 'http://nodeinfo.diaspora.software/ns/schema/' . self::VERSION, + 'href' => get_rest_url( null, '/nodeinfo/' . self::VERSION ), + ); + return $links; + } + + /** + * Adds the JRD link. + * + * @param array $links The JRD links. + * @return array The modified links. + */ + public static function jrd_link( $links ) { + $links[] = array( + 'rel' => 'http://nodeinfo.diaspora.software/ns/schema/' . self::VERSION, + 'href' => get_rest_url( null, '/nodeinfo/' . self::VERSION ), + ); + return $links; + } + /** * Adds software information. * @@ -36,14 +82,51 @@ public static function init() { * @return array The modified software data. */ public static function software( $software, $version ) { + if ( self::VERSION !== $version ) { + return $software; + } + $software['name'] = 'wordpress'; $software['version'] = get_masked_version(); - if ( '2.1' === $version ) { - $software['repository'] = 'https://github.com/wordpress/wordpress'; + return $software; + } + + /** + * Adds protocols. + * + * @param array $protocols The protocols data. + * @param string $version The NodeInfo version. + * @return array The modified protocols data. + */ + public static function protocols( $protocols, $version ) { + if ( self::VERSION !== $version ) { + return $protocols; } - return $software; + // NodeInfo 1.1 uses inbound/outbound structure. + $protocols['inbound'] = array(); + $protocols['outbound'] = array(); + + return $protocols; + } + + /** + * Adds services. + * + * @param array $services The services data. + * @param string $version The NodeInfo version. + * @return array The modified services data. + */ + public static function services( $services, $version ) { + if ( self::VERSION !== $version ) { + return $services; + } + + $services['inbound'] = array(); + $services['outbound'] = array( 'smtp' ); + + return $services; } /** @@ -54,6 +137,10 @@ public static function software( $software, $version ) { * @return array The modified usage data. */ public static function usage( $usage, $version ) { + if ( self::VERSION !== $version ) { + return $usage; + } + $users = get_users( array( 'fields' => 'ID', @@ -78,41 +165,6 @@ public static function usage( $usage, $version ) { return $usage; } - /** - * Adds protocols. - * - * @param array $protocols The protocols data. - * @param string $version The NodeInfo version. - * @return array The modified protocols data. - */ - public static function protocols( $protocols, $version ) { - // NodeInfo 1.x uses inbound/outbound structure for protocols. - if ( version_compare( $version, '2.0', '<' ) ) { - $protocols['inbound'] = array( 'smtp' ); - $protocols['outbound'] = array( 'smtp' ); - } - - return $protocols; - } - - /** - * Adds services. - * - * @param array $services The services data. - * @param string $version The NodeInfo version. - * @return array The modified services data. - */ - public static function services( $services, $version ) { - if ( version_compare( $version, '2.0', '>=' ) ) { - $services['inbound'] = array( 'atom1.0', 'rss2.0', 'pop3' ); - $services['outbound'] = array( 'atom1.0', 'rss2.0', 'wordpress', 'smtp' ); - } else { - $services['outbound'] = array( 'smtp' ); - } - - return $services; - } - /** * Adds metadata. * @@ -121,6 +173,10 @@ public static function services( $services, $version ) { * @return array The modified metadata. */ public static function metadata( $metadata, $version ) { + if ( self::VERSION !== $version ) { + return $metadata; + } + $metadata['nodeName'] = get_bloginfo( 'name' ); $metadata['nodeDescription'] = get_bloginfo( 'description' ); $metadata['nodeIcon'] = get_site_icon_url(); diff --git a/includes/integration/class-nodeinfo20.php b/includes/integration/class-nodeinfo20.php new file mode 100644 index 0000000..a9355b8 --- /dev/null +++ b/includes/integration/class-nodeinfo20.php @@ -0,0 +1,239 @@ + 'http://nodeinfo.diaspora.software/ns/schema/' . self::VERSION, + 'href' => get_rest_url( null, '/nodeinfo/' . self::VERSION ), + ); + return $links; + } + + /** + * Adds the JRD link. + * + * @param array $links The JRD links. + * @return array The modified links. + */ + public static function jrd_link( $links ) { + $links[] = array( + 'rel' => 'http://nodeinfo.diaspora.software/ns/schema/' . self::VERSION, + 'href' => get_rest_url( null, '/nodeinfo/' . self::VERSION ), + ); + return $links; + } + + /** + * Adds the schema for NodeInfo 2.0. + * + * @param array $schema The schema. + * @return array The modified schema. + */ + public static function schema( $schema ) { + // NodeInfo 2.0 schema - protocols is a flat array, not inbound/outbound. + $schema['properties'] = array_merge( + $schema['properties'], + array( + 'version' => array( + 'description' => __( 'The NodeInfo schema version.', 'nodeinfo' ), + 'type' => 'string', + ), + 'software' => array( + 'description' => __( 'Metadata about server software in use.', 'nodeinfo' ), + 'type' => 'object', + 'properties' => array( + 'name' => array( 'type' => 'string' ), + 'version' => array( 'type' => 'string' ), + ), + ), + 'protocols' => array( + 'description' => __( 'The protocols supported on this server.', 'nodeinfo' ), + 'type' => 'array', + 'items' => array( 'type' => 'string' ), + ), + 'services' => array( + 'description' => __( 'Third party sites this server can connect to.', 'nodeinfo' ), + 'type' => 'object', + 'properties' => array( + 'inbound' => array( + 'type' => 'array', + 'items' => array( 'type' => 'string' ), + ), + 'outbound' => array( + 'type' => 'array', + 'items' => array( 'type' => 'string' ), + ), + ), + ), + 'openRegistrations' => array( + 'description' => __( 'Whether this server allows open self-registration.', 'nodeinfo' ), + 'type' => 'boolean', + ), + 'usage' => array( + 'description' => __( 'Usage statistics for this server.', 'nodeinfo' ), + 'type' => 'object', + 'properties' => array( + 'users' => array( + 'type' => 'object', + 'properties' => array( + 'total' => array( 'type' => 'integer' ), + 'activeMonth' => array( 'type' => 'integer' ), + 'activeHalfyear' => array( 'type' => 'integer' ), + ), + ), + 'localPosts' => array( 'type' => 'integer' ), + 'localComments' => array( 'type' => 'integer' ), + ), + ), + 'metadata' => array( + 'description' => __( 'Free form key value pairs for software specific values.', 'nodeinfo' ), + 'type' => 'object', + ), + ) + ); + + return $schema; + } + + /** + * Adds software information. + * + * @param array $software The software data. + * @param string $version The NodeInfo version. + * @return array The modified software data. + */ + public static function software( $software, $version ) { + if ( self::VERSION !== $version ) { + return $software; + } + + $software['name'] = 'wordpress'; + $software['version'] = get_masked_version(); + + return $software; + } + + /** + * Adds services. + * + * @param array $services The services data. + * @param string $version The NodeInfo version. + * @return array The modified services data. + */ + public static function services( $services, $version ) { + if ( self::VERSION !== $version ) { + return $services; + } + + $services['inbound'] = array( 'atom1.0', 'rss2.0', 'pop3' ); + $services['outbound'] = array( 'atom1.0', 'rss2.0', 'wordpress', 'smtp' ); + + return $services; + } + + /** + * Adds usage statistics. + * + * @param array $usage The usage data. + * @param string $version The NodeInfo version. + * @return array The modified usage data. + */ + public static function usage( $usage, $version ) { + if ( self::VERSION !== $version ) { + return $usage; + } + + $users = get_users( + array( + 'fields' => 'ID', + 'capability__in' => array( 'publish_posts' ), + ) + ); + + $user_count = is_array( $users ) ? count( $users ) : 1; + + $posts = wp_count_posts(); + $comments = wp_count_comments(); + + $usage['users'] = array( + 'total' => $user_count, + 'activeMonth' => get_active_users( '1 month ago' ), + 'activeHalfyear' => get_active_users( '6 month ago' ), + ); + + $usage['localPosts'] = (int) $posts->publish; + $usage['localComments'] = (int) $comments->approved; + + return $usage; + } + + /** + * Adds metadata. + * + * @param array $metadata The metadata. + * @param string $version The NodeInfo version. + * @return array The modified metadata. + */ + public static function metadata( $metadata, $version ) { + if ( self::VERSION !== $version ) { + return $metadata; + } + + $metadata['nodeName'] = get_bloginfo( 'name' ); + $metadata['nodeDescription'] = get_bloginfo( 'description' ); + $metadata['nodeIcon'] = get_site_icon_url(); + + return $metadata; + } +} diff --git a/includes/integration/class-nodeinfo21.php b/includes/integration/class-nodeinfo21.php new file mode 100644 index 0000000..5063c2c --- /dev/null +++ b/includes/integration/class-nodeinfo21.php @@ -0,0 +1,248 @@ + 'http://nodeinfo.diaspora.software/ns/schema/' . self::VERSION, + 'href' => get_rest_url( null, '/nodeinfo/' . self::VERSION ), + ); + return $links; + } + + /** + * Adds the JRD link. + * + * @param array $links The JRD links. + * @return array The modified links. + */ + public static function jrd_link( $links ) { + $links[] = array( + 'rel' => 'http://nodeinfo.diaspora.software/ns/schema/' . self::VERSION, + 'href' => get_rest_url( null, '/nodeinfo/' . self::VERSION ), + ); + return $links; + } + + /** + * Adds the schema for NodeInfo 2.1. + * + * @param array $schema The schema. + * @return array The modified schema. + */ + public static function schema( $schema ) { + // NodeInfo 2.1 schema - adds repository to software. + $schema['properties'] = array_merge( + $schema['properties'], + array( + 'version' => array( + 'description' => __( 'The NodeInfo schema version.', 'nodeinfo' ), + 'type' => 'string', + ), + 'software' => array( + 'description' => __( 'Metadata about server software in use.', 'nodeinfo' ), + 'type' => 'object', + 'properties' => array( + 'name' => array( 'type' => 'string' ), + 'version' => array( 'type' => 'string' ), + 'repository' => array( + 'type' => 'string', + 'format' => 'uri', + ), + 'homepage' => array( + 'type' => 'string', + 'format' => 'uri', + ), + ), + ), + 'protocols' => array( + 'description' => __( 'The protocols supported on this server.', 'nodeinfo' ), + 'type' => 'array', + 'items' => array( 'type' => 'string' ), + ), + 'services' => array( + 'description' => __( 'Third party sites this server can connect to.', 'nodeinfo' ), + 'type' => 'object', + 'properties' => array( + 'inbound' => array( + 'type' => 'array', + 'items' => array( 'type' => 'string' ), + ), + 'outbound' => array( + 'type' => 'array', + 'items' => array( 'type' => 'string' ), + ), + ), + ), + 'openRegistrations' => array( + 'description' => __( 'Whether this server allows open self-registration.', 'nodeinfo' ), + 'type' => 'boolean', + ), + 'usage' => array( + 'description' => __( 'Usage statistics for this server.', 'nodeinfo' ), + 'type' => 'object', + 'properties' => array( + 'users' => array( + 'type' => 'object', + 'properties' => array( + 'total' => array( 'type' => 'integer' ), + 'activeMonth' => array( 'type' => 'integer' ), + 'activeHalfyear' => array( 'type' => 'integer' ), + ), + ), + 'localPosts' => array( 'type' => 'integer' ), + 'localComments' => array( 'type' => 'integer' ), + ), + ), + 'metadata' => array( + 'description' => __( 'Free form key value pairs for software specific values.', 'nodeinfo' ), + 'type' => 'object', + ), + ) + ); + + return $schema; + } + + /** + * Adds software information. + * + * @param array $software The software data. + * @param string $version The NodeInfo version. + * @return array The modified software data. + */ + public static function software( $software, $version ) { + if ( self::VERSION !== $version ) { + return $software; + } + + $software['name'] = 'wordpress'; + $software['version'] = get_masked_version(); + $software['repository'] = 'https://github.com/wordpress/wordpress'; + + return $software; + } + + /** + * Adds services. + * + * @param array $services The services data. + * @param string $version The NodeInfo version. + * @return array The modified services data. + */ + public static function services( $services, $version ) { + if ( self::VERSION !== $version ) { + return $services; + } + + $services['inbound'] = array( 'atom1.0', 'rss2.0', 'pop3' ); + $services['outbound'] = array( 'atom1.0', 'rss2.0', 'wordpress', 'smtp' ); + + return $services; + } + + /** + * Adds usage statistics. + * + * @param array $usage The usage data. + * @param string $version The NodeInfo version. + * @return array The modified usage data. + */ + public static function usage( $usage, $version ) { + if ( self::VERSION !== $version ) { + return $usage; + } + + $users = get_users( + array( + 'fields' => 'ID', + 'capability__in' => array( 'publish_posts' ), + ) + ); + + $user_count = is_array( $users ) ? count( $users ) : 1; + + $posts = wp_count_posts(); + $comments = wp_count_comments(); + + $usage['users'] = array( + 'total' => $user_count, + 'activeMonth' => get_active_users( '1 month ago' ), + 'activeHalfyear' => get_active_users( '6 month ago' ), + ); + + $usage['localPosts'] = (int) $posts->publish; + $usage['localComments'] = (int) $comments->approved; + + return $usage; + } + + /** + * Adds metadata. + * + * @param array $metadata The metadata. + * @param string $version The NodeInfo version. + * @return array The modified metadata. + */ + public static function metadata( $metadata, $version ) { + if ( self::VERSION !== $version ) { + return $metadata; + } + + $metadata['nodeName'] = get_bloginfo( 'name' ); + $metadata['nodeDescription'] = get_bloginfo( 'description' ); + $metadata['nodeIcon'] = get_site_icon_url(); + + return $metadata; + } +} diff --git a/nodeinfo.php b/nodeinfo.php index 1fba26b..1ccfa63 100644 --- a/nodeinfo.php +++ b/nodeinfo.php @@ -30,8 +30,11 @@ * Initialize the plugin. */ function nodeinfo_init() { - // Initialize integrations. - Nodeinfo\Integration\Nodeinfo::init(); + // Initialize NodeInfo version integrations. + Nodeinfo\Integration\Nodeinfo10::init(); + Nodeinfo\Integration\Nodeinfo11::init(); + Nodeinfo\Integration\Nodeinfo20::init(); + Nodeinfo\Integration\Nodeinfo21::init(); // Register REST routes. add_action( 'rest_api_init', 'nodeinfo_register_routes' ); From a5cae1ae9f7b96117bb6e0e1a31ef3796e2a11e2 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 8 Dec 2025 10:37:28 +0100 Subject: [PATCH 03/21] Centralize JRD discovery in controller Move JRD (WebFinger/Host-Meta) discovery link generation to the controller's jrd() method, which translates nodeinfo_discovery_links to JRD format. This removes duplicate jrd_link methods from individual integration classes since they now only need to register their discovery links once. --- includes/controller/class-nodeinfo.php | 47 +++++++++++++---------- includes/integration/class-nodeinfo10.php | 15 -------- includes/integration/class-nodeinfo11.php | 15 -------- includes/integration/class-nodeinfo20.php | 15 -------- includes/integration/class-nodeinfo21.php | 15 -------- 5 files changed, 26 insertions(+), 81 deletions(-) diff --git a/includes/controller/class-nodeinfo.php b/includes/controller/class-nodeinfo.php index 34b76f3..ae2a980 100644 --- a/includes/controller/class-nodeinfo.php +++ b/includes/controller/class-nodeinfo.php @@ -152,27 +152,6 @@ public function get_item( $request ) { return new WP_REST_Response( $nodeinfo ); } - /** - * Adds Host-Meta and WebFinger discovery links. - * - * @param array $jrd The JRD document. - * @return array The modified JRD document. - */ - public static function jrd( $jrd ) { - /** - * Filters the NodeInfo JRD links for WebFinger/Host-Meta. - * - * @param array $links The JRD links. - */ - $links = apply_filters( 'nodeinfo_jrd_links', array() ); - - foreach ( $links as $link ) { - $jrd['links'][] = $link; - } - - return $jrd; - } - /** * Retrieves the NodeInfo schema. * @@ -193,4 +172,30 @@ public function get_item_schema() { */ return apply_filters( 'nodeinfo_schema', $schema ); } + + /** + * Adds NodeInfo discovery links to JRD documents. + * + * Translates the nodeinfo_discovery_links filter to JRD format + * for WebFinger and Host-Meta discovery. + * + * @param array $jrd The JRD document. + * @return array The modified JRD document. + */ + public static function jrd( $jrd ) { + if ( ! isset( $jrd['links'] ) ) { + $jrd['links'] = array(); + } + + /** + * Filters the NodeInfo discovery links. + * + * @param array $links The discovery links. + */ + $links = apply_filters( 'nodeinfo_discovery_links', array() ); + + $jrd['links'] = array_merge( $jrd['links'], $links ); + + return $jrd; + } } diff --git a/includes/integration/class-nodeinfo10.php b/includes/integration/class-nodeinfo10.php index 27c9508..cde1a49 100644 --- a/includes/integration/class-nodeinfo10.php +++ b/includes/integration/class-nodeinfo10.php @@ -27,7 +27,6 @@ class Nodeinfo10 { public static function init() { add_filter( 'nodeinfo_versions', array( __CLASS__, 'register_version' ) ); add_filter( 'nodeinfo_discovery_links', array( __CLASS__, 'discovery_link' ) ); - add_filter( 'nodeinfo_jrd_links', array( __CLASS__, 'jrd_link' ) ); add_filter( 'nodeinfo_schema', array( __CLASS__, 'schema' ) ); add_filter( 'nodeinfo_data_software', array( __CLASS__, 'software' ), 10, 2 ); add_filter( 'nodeinfo_data_protocols', array( __CLASS__, 'protocols' ), 10, 2 ); @@ -61,20 +60,6 @@ public static function discovery_link( $links ) { return $links; } - /** - * Adds the JRD link. - * - * @param array $links The JRD links. - * @return array The modified links. - */ - public static function jrd_link( $links ) { - $links[] = array( - 'rel' => 'http://nodeinfo.diaspora.software/ns/schema/' . self::VERSION, - 'href' => get_rest_url( null, '/nodeinfo/' . self::VERSION ), - ); - return $links; - } - /** * Adds the schema for this version. * diff --git a/includes/integration/class-nodeinfo11.php b/includes/integration/class-nodeinfo11.php index c0d113c..543d1b7 100644 --- a/includes/integration/class-nodeinfo11.php +++ b/includes/integration/class-nodeinfo11.php @@ -27,7 +27,6 @@ class Nodeinfo11 { public static function init() { add_filter( 'nodeinfo_versions', array( __CLASS__, 'register_version' ) ); add_filter( 'nodeinfo_discovery_links', array( __CLASS__, 'discovery_link' ) ); - add_filter( 'nodeinfo_jrd_links', array( __CLASS__, 'jrd_link' ) ); add_filter( 'nodeinfo_data_software', array( __CLASS__, 'software' ), 10, 2 ); add_filter( 'nodeinfo_data_protocols', array( __CLASS__, 'protocols' ), 10, 2 ); add_filter( 'nodeinfo_data_services', array( __CLASS__, 'services' ), 10, 2 ); @@ -60,20 +59,6 @@ public static function discovery_link( $links ) { return $links; } - /** - * Adds the JRD link. - * - * @param array $links The JRD links. - * @return array The modified links. - */ - public static function jrd_link( $links ) { - $links[] = array( - 'rel' => 'http://nodeinfo.diaspora.software/ns/schema/' . self::VERSION, - 'href' => get_rest_url( null, '/nodeinfo/' . self::VERSION ), - ); - return $links; - } - /** * Adds software information. * diff --git a/includes/integration/class-nodeinfo20.php b/includes/integration/class-nodeinfo20.php index a9355b8..ae9a0b9 100644 --- a/includes/integration/class-nodeinfo20.php +++ b/includes/integration/class-nodeinfo20.php @@ -27,7 +27,6 @@ class Nodeinfo20 { public static function init() { add_filter( 'nodeinfo_versions', array( __CLASS__, 'register_version' ) ); add_filter( 'nodeinfo_discovery_links', array( __CLASS__, 'discovery_link' ) ); - add_filter( 'nodeinfo_jrd_links', array( __CLASS__, 'jrd_link' ) ); add_filter( 'nodeinfo_schema', array( __CLASS__, 'schema' ) ); add_filter( 'nodeinfo_data_software', array( __CLASS__, 'software' ), 10, 2 ); add_filter( 'nodeinfo_data_services', array( __CLASS__, 'services' ), 10, 2 ); @@ -60,20 +59,6 @@ public static function discovery_link( $links ) { return $links; } - /** - * Adds the JRD link. - * - * @param array $links The JRD links. - * @return array The modified links. - */ - public static function jrd_link( $links ) { - $links[] = array( - 'rel' => 'http://nodeinfo.diaspora.software/ns/schema/' . self::VERSION, - 'href' => get_rest_url( null, '/nodeinfo/' . self::VERSION ), - ); - return $links; - } - /** * Adds the schema for NodeInfo 2.0. * diff --git a/includes/integration/class-nodeinfo21.php b/includes/integration/class-nodeinfo21.php index 5063c2c..3a5ef65 100644 --- a/includes/integration/class-nodeinfo21.php +++ b/includes/integration/class-nodeinfo21.php @@ -27,7 +27,6 @@ class Nodeinfo21 { public static function init() { add_filter( 'nodeinfo_versions', array( __CLASS__, 'register_version' ) ); add_filter( 'nodeinfo_discovery_links', array( __CLASS__, 'discovery_link' ) ); - add_filter( 'nodeinfo_jrd_links', array( __CLASS__, 'jrd_link' ) ); add_filter( 'nodeinfo_schema', array( __CLASS__, 'schema' ) ); add_filter( 'nodeinfo_data_software', array( __CLASS__, 'software' ), 10, 2 ); add_filter( 'nodeinfo_data_services', array( __CLASS__, 'services' ), 10, 2 ); @@ -60,20 +59,6 @@ public static function discovery_link( $links ) { return $links; } - /** - * Adds the JRD link. - * - * @param array $links The JRD links. - * @return array The modified links. - */ - public static function jrd_link( $links ) { - $links[] = array( - 'rel' => 'http://nodeinfo.diaspora.software/ns/schema/' . self::VERSION, - 'href' => get_rest_url( null, '/nodeinfo/' . self::VERSION ), - ); - return $links; - } - /** * Adds the schema for NodeInfo 2.1. * From fd502cf191be252523614312428431cbd74b31a3 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 8 Dec 2025 10:44:06 +0100 Subject: [PATCH 04/21] Switch to wp-env and update test setup - Replace docker-compose with wp-env (ports 8889/8890) - Update phpcs.xml to match ActivityPub standards - Update composer.json with modern dev dependencies - Restructure tests to tests/phpunit/ directory - Add PHPUnit workflow for CI - Update PHPCS workflow with modern actions - Bump minimum PHP to 7.2 and WordPress to 6.5 --- .github/workflows/phpcs.yml | 37 ++++++++------- .github/workflows/phpunit.yml | 69 ++++++++++++++++++++++++++++ .wp-env.json | 19 ++++++++ composer.json | 86 +++++++++++++++++++---------------- docker-compose.yml | 28 ------------ phpcs.xml | 42 ++++++++--------- phpunit.xml.dist | 11 +++-- tests/bootstrap.php | 21 --------- tests/phpunit/bootstrap.php | 39 ++++++++++++++++ 9 files changed, 221 insertions(+), 131 deletions(-) create mode 100644 .github/workflows/phpunit.yml create mode 100644 .wp-env.json delete mode 100644 docker-compose.yml delete mode 100644 tests/bootstrap.php create mode 100644 tests/phpunit/bootstrap.php diff --git a/.github/workflows/phpcs.yml b/.github/workflows/phpcs.yml index c1016dd..b7f84ef 100644 --- a/.github/workflows/phpcs.yml +++ b/.github/workflows/phpcs.yml @@ -1,29 +1,34 @@ name: PHP_CodeSniffer -on: push +on: + push: + branches: + - master + paths: + - '**/*.php' + - 'composer.json' + - 'composer.lock' + - 'phpcs.xml' + - '.github/workflows/phpcs.yml' + pull_request: + paths: + - '**/*.php' + - 'composer.json' + - 'composer.lock' + - 'phpcs.xml' + - '.github/workflows/phpcs.yml' jobs: phpcs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: '7.4' coverage: none tools: composer, cs2pr - - name: Get Composer cache directory - id: composer-cache - run: echo "::set-output name=dir::$(composer config cache-files-dir)" - - name: Setup cache - uses: pat-s/always-upload-cache@v1.1.4 - with: - path: ${{ steps.composer-cache.outputs.dir }} - # Use the hash of composer.json as the key for your cache if you do not commit composer.lock. - key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} - #key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} - restore-keys: ${{ runner.os }}-composer- - - name: Install dependencies - run: composer install --prefer-dist --no-progress + - name: Install Composer dependencies for PHP + uses: ramsey/composer-install@v3 - name: Detect coding standard violations - run: ./vendor/bin/phpcs -n -q + run: ./vendor/bin/phpcs diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml new file mode 100644 index 0000000..f25003c --- /dev/null +++ b/.github/workflows/phpunit.yml @@ -0,0 +1,69 @@ +name: PHPUnit +on: + push: + branches: + - master + paths: + - '**/*.php' + - 'composer.json' + - 'composer.lock' + - 'phpunit.xml.dist' + - '.github/workflows/phpunit.yml' + pull_request: + paths: + - '**/*.php' + - 'composer.json' + - 'composer.lock' + - 'phpunit.xml.dist' + - '.github/workflows/phpunit.yml' + +jobs: + phpunit: + runs-on: ubuntu-latest + services: + mysql: + image: mariadb:10.4 + env: + MARIADB_ROOT_PASSWORD: root + MARIADB_DATABASE: wordpress_test + ports: + - 3306:3306 + options: >- + --health-cmd="healthcheck.sh --connect --innodb_initialized" + --health-interval=5s + --health-timeout=2s + --health-retries=3 + + strategy: + fail-fast: false + matrix: + php-version: ['7.2', '8.3'] + wp-version: ['latest'] + include: + - php-version: '7.2' + wp-version: '6.5' + + name: PHP ${{ matrix.php-version }} / WP ${{ matrix.wp-version }} + steps: + - name: Install svn + run: sudo apt-get update && sudo apt-get install -y subversion + + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + extensions: mbstring, intl + ini-values: post_max_size=256M, max_execution_time=180 + coverage: none + + - name: Install Composer dependencies + uses: ramsey/composer-install@v3 + + - name: Setup Test Environment + run: bash bin/install-wp-tests.sh wordpress_test root root 127.0.0.1 ${{ matrix.wp-version }} true + + - name: Run PHPUnit + run: vendor/bin/phpunit diff --git a/.wp-env.json b/.wp-env.json new file mode 100644 index 0000000..c0d9d4b --- /dev/null +++ b/.wp-env.json @@ -0,0 +1,19 @@ +{ + "core": null, + "plugins": [ "." ], + "port": 8889, + "testsPort": 8890, + "env": { + "tests": { + "config": { + "WP_ENVIRONMENT_TYPE": "production" + }, + "mappings": { + "wp-content/plugins/nodeinfo": "." + } + } + }, + "lifecycleScripts": { + "afterStart": "npx wp-env run cli wp rewrite structure /%year%/%monthnum%/%postname%/" + } +} diff --git a/composer.json b/composer.json index 91d12dc..e312c44 100644 --- a/composer.json +++ b/composer.json @@ -1,41 +1,49 @@ { - "name": "pfefferle/wordpress-nodeinfo", - "description": "NodeInfo and NodeInfo2 for WordPress!", - "require": { - "php": ">=5.6.0", - "composer/installers": "^1.0 || ^2.0" - }, - "type": "wordpress-plugin", - "license": "MIT", - "authors": [ - { - "name": "Matthias Pfefferle", - "homepage": "https://notiz.blog" - } - ], - "extra": { - "installer-name": "nodeinfo" - }, - "require-dev": { - "phpunit/phpunit": "^5.7.21 || ^6.5 || ^7.5 || ^8", - "phpcompatibility/php-compatibility": "*", - "phpcompatibility/phpcompatibility-wp": "*", - "squizlabs/php_codesniffer": "3.*", - "wp-coding-standards/wpcs": "*", - "yoast/phpunit-polyfills": "^3.0", - "dealerdirect/phpcodesniffer-composer-installer": "^1.0.0" - }, - "config": { - "allow-plugins": true - }, - "allow-plugins": { - "composer/installers": true - }, - "scripts": { - "test": [ - "composer install", - "bin/install-wp-tests.sh wordpress wordpress wordpress", - "vendor/bin/phpunit" - ] - } + "name": "pfefferle/wordpress-nodeinfo", + "description": "NodeInfo and NodeInfo2 for WordPress!", + "type": "wordpress-plugin", + "license": "MIT", + "authors": [ + { + "name": "Matthias Pfefferle", + "homepage": "https://notiz.blog" + } + ], + "require": { + "php": ">=7.2", + "composer/installers": "^1.0 || ^2.0" + }, + "require-dev": { + "phpunit/phpunit": "^8 || ^9", + "phpcompatibility/php-compatibility": "*", + "phpcompatibility/phpcompatibility-wp": "*", + "squizlabs/php_codesniffer": "3.*", + "wp-coding-standards/wpcs": "dev-develop", + "yoast/phpunit-polyfills": "^4.0", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0.0", + "sirbrillig/phpcs-variable-analysis": "^2.11", + "phpcsstandards/phpcsextra": "^1.1.0" + }, + "extra": { + "installer-name": "nodeinfo" + }, + "config": { + "allow-plugins": true + }, + "scripts": { + "test": [ + "composer install", + "bin/install-wp-tests.sh nodeinfo-test root nodeinfo-test test-db latest true", + "vendor/bin/phpunit" + ], + "test:wp-env": [ + "wp-env run tests-cli --env-cwd=\"wp-content/plugins/nodeinfo\" vendor/bin/phpunit" + ], + "lint": [ + "vendor/bin/phpcs" + ], + "lint:fix": [ + "vendor/bin/phpcbf" + ] + } } diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 253a0ee..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,28 +0,0 @@ -version: '2' -services: - db: - image: mysql:5.7 - platform: linux/x86_64 - restart: always - environment: - MYSQL_ROOT_PASSWORD: wordpress - MYSQL_DATABASE: wordpress - MYSQL_USER: wordpress - MYSQL_PASSWORD: wordpress - - wordpress: - depends_on: - - db - image: wordpress:latest - links: - - db - ports: - - "8012:80" - volumes: - - .:/var/www/html/wp-content/plugins/nodeinfo - restart: always - environment: - WORDPRESS_DB_HOST: db:3306 - WORDPRESS_DB_USER: wordpress - WORDPRESS_DB_PASSWORD: wordpress - WORDPRESS_DEBUG: 1 diff --git a/phpcs.xml b/phpcs.xml index 667ff7f..3001e85 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -1,32 +1,26 @@ - + WordPress NodeInfo Standards - ./nodeinfo.php - ./includes/ - */includes/*\.(inc|css|js|svg) + . + .(git|github|vscode|idea|wordpress-org) + *\.(inc|css|js|svg) */vendor/* - */libraries/* - */config/* */node_modules/* - */tests/* - - - - - - - - - - - - - + *.asset.php + + + + + + + + - - - - + + + + + diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 44f0fdb..0627b9a 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,5 +1,5 @@ - - ./tests/ + + ./tests/phpunit/tests + + + ./includes + + diff --git a/tests/bootstrap.php b/tests/bootstrap.php deleted file mode 100644 index 8749004..0000000 --- a/tests/bootstrap.php +++ /dev/null @@ -1,21 +0,0 @@ - Date: Mon, 8 Dec 2025 10:50:12 +0100 Subject: [PATCH 05/21] Fix PHPCS issues and add unit tests - Fix missing @package tag in nodeinfo.php - Add phpcs:ignore for lowercase software name (per NodeInfo spec) - Remove unused $request parameter in get_discovery() - Add unit tests for NodeInfo endpoint - Add unit tests for NodeInfo2 endpoint - Add unit tests for helper functions --- includes/controller/class-nodeinfo.php | 3 +- includes/controller/class-nodeinfo2.php | 2 +- includes/integration/class-nodeinfo10.php | 1 + includes/integration/class-nodeinfo11.php | 1 + includes/integration/class-nodeinfo20.php | 1 + includes/integration/class-nodeinfo21.php | 1 + nodeinfo.php | 2 + tests/phpunit/bootstrap.php | 3 +- tests/phpunit/tests/class-test-functions.php | 109 ++++++++ .../tests/class-test-nodeinfo-endpoint.php | 261 ++++++++++++++++++ .../tests/class-test-nodeinfo2-endpoint.php | 170 ++++++++++++ 11 files changed, 550 insertions(+), 4 deletions(-) create mode 100644 tests/phpunit/tests/class-test-functions.php create mode 100644 tests/phpunit/tests/class-test-nodeinfo-endpoint.php create mode 100644 tests/phpunit/tests/class-test-nodeinfo2-endpoint.php diff --git a/includes/controller/class-nodeinfo.php b/includes/controller/class-nodeinfo.php index ae2a980..e7bbc01 100644 --- a/includes/controller/class-nodeinfo.php +++ b/includes/controller/class-nodeinfo.php @@ -87,10 +87,9 @@ protected function get_versions() { /** * Retrieves the discovery document. * - * @param \WP_REST_Request $request The request object. * @return WP_REST_Response The response object. */ - public function get_discovery( $request ) { + public function get_discovery() { $links = array(); /** diff --git a/includes/controller/class-nodeinfo2.php b/includes/controller/class-nodeinfo2.php index 63ea636..f790b3f 100644 --- a/includes/controller/class-nodeinfo2.php +++ b/includes/controller/class-nodeinfo2.php @@ -36,7 +36,7 @@ public function register_routes() { $this->namespace, '/(?P\d\.\d)', array( - 'args' => array( + 'args' => array( 'version' => array( 'description' => __( 'The NodeInfo2 schema version.', 'nodeinfo' ), 'type' => 'string', diff --git a/includes/integration/class-nodeinfo10.php b/includes/integration/class-nodeinfo10.php index cde1a49..3dd60f9 100644 --- a/includes/integration/class-nodeinfo10.php +++ b/includes/integration/class-nodeinfo10.php @@ -152,6 +152,7 @@ public static function software( $software, $version ) { return $software; } + // phpcs:ignore WordPress.WP.CapitalPDangit.MisspelledInText -- NodeInfo spec uses lowercase. $software['name'] = 'wordpress'; $software['version'] = get_masked_version(); diff --git a/includes/integration/class-nodeinfo11.php b/includes/integration/class-nodeinfo11.php index 543d1b7..5cf6450 100644 --- a/includes/integration/class-nodeinfo11.php +++ b/includes/integration/class-nodeinfo11.php @@ -71,6 +71,7 @@ public static function software( $software, $version ) { return $software; } + // phpcs:ignore WordPress.WP.CapitalPDangit.MisspelledInText -- NodeInfo spec uses lowercase. $software['name'] = 'wordpress'; $software['version'] = get_masked_version(); diff --git a/includes/integration/class-nodeinfo20.php b/includes/integration/class-nodeinfo20.php index ae9a0b9..10fd4d7 100644 --- a/includes/integration/class-nodeinfo20.php +++ b/includes/integration/class-nodeinfo20.php @@ -143,6 +143,7 @@ public static function software( $software, $version ) { return $software; } + // phpcs:ignore WordPress.WP.CapitalPDangit.MisspelledInText -- NodeInfo spec uses lowercase. $software['name'] = 'wordpress'; $software['version'] = get_masked_version(); diff --git a/includes/integration/class-nodeinfo21.php b/includes/integration/class-nodeinfo21.php index 3a5ef65..8a076ed 100644 --- a/includes/integration/class-nodeinfo21.php +++ b/includes/integration/class-nodeinfo21.php @@ -151,6 +151,7 @@ public static function software( $software, $version ) { return $software; } + // phpcs:ignore WordPress.WP.CapitalPDangit.MisspelledInText -- NodeInfo spec uses lowercase. $software['name'] = 'wordpress'; $software['version'] = get_masked_version(); $software['repository'] = 'https://github.com/wordpress/wordpress'; diff --git a/nodeinfo.php b/nodeinfo.php index 1ccfa63..8591e6e 100644 --- a/nodeinfo.php +++ b/nodeinfo.php @@ -10,6 +10,8 @@ * License URI: http://opensource.org/licenses/MIT * Text Domain: nodeinfo * Domain Path: /languages + * + * @package Nodeinfo */ defined( 'ABSPATH' ) || exit; diff --git a/tests/phpunit/bootstrap.php b/tests/phpunit/bootstrap.php index fe635b8..94af5a6 100644 --- a/tests/phpunit/bootstrap.php +++ b/tests/phpunit/bootstrap.php @@ -11,7 +11,8 @@ } if ( ! file_exists( $_tests_dir . '/includes/functions.php' ) ) { - echo "Could not find $_tests_dir/includes/functions.php, have you run bin/install-wp-tests.sh?" . PHP_EOL; + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + echo "Could not find {$_tests_dir}/includes/functions.php, have you run bin/install-wp-tests.sh?" . PHP_EOL; exit( 1 ); } diff --git a/tests/phpunit/tests/class-test-functions.php b/tests/phpunit/tests/class-test-functions.php new file mode 100644 index 0000000..6b092b8 --- /dev/null +++ b/tests/phpunit/tests/class-test-functions.php @@ -0,0 +1,109 @@ +assertIsString( $version ); + $this->assertNotEmpty( $version ); + } + + /** + * Test get_masked_version with masking enabled. + * + * @covers \Nodeinfo\get_masked_version + */ + public function test_get_masked_version_with_masking() { + add_filter( 'nodeinfo_mask_version', '__return_true' ); + + $version = get_masked_version(); + $wp_version = get_bloginfo( 'version' ); + + // When masked, should only show major.minor. + $this->assertNotEquals( $wp_version, $version ); + + remove_filter( 'nodeinfo_mask_version', '__return_true' ); + } + + /** + * Test get_masked_version without masking. + * + * @covers \Nodeinfo\get_masked_version + */ + public function test_get_masked_version_without_masking() { + add_filter( 'nodeinfo_mask_version', '__return_false' ); + + $version = get_masked_version(); + $wp_version = get_bloginfo( 'version' ); + + $this->assertEquals( $wp_version, $version ); + + remove_filter( 'nodeinfo_mask_version', '__return_false' ); + } + + /** + * Test get_active_users returns an integer. + * + * @covers \Nodeinfo\get_active_users + */ + public function test_get_active_users_returns_integer() { + $active_users = get_active_users( '1 month ago' ); + + $this->assertIsInt( $active_users ); + } + + /** + * Test get_active_users with different time periods. + * + * @covers \Nodeinfo\get_active_users + */ + public function test_get_active_users_time_periods() { + $month_users = get_active_users( '1 month ago' ); + $halfyear_users = get_active_users( '6 month ago' ); + + $this->assertIsInt( $month_users ); + $this->assertIsInt( $halfyear_users ); + // Halfyear should be >= month. + $this->assertGreaterThanOrEqual( $month_users, $halfyear_users ); + } + + /** + * Test get_active_users with a user who published a post. + * + * @covers \Nodeinfo\get_active_users + */ + public function test_get_active_users_with_published_post() { + $user_id = self::factory()->user->create( array( 'role' => 'author' ) ); + + self::factory()->post->create( + array( + 'post_author' => $user_id, + 'post_status' => 'publish', + 'post_date' => gmdate( 'Y-m-d H:i:s', strtotime( '-1 week' ) ), + ) + ); + + $active_users = get_active_users( '1 month ago' ); + + $this->assertGreaterThanOrEqual( 1, $active_users ); + } +} diff --git a/tests/phpunit/tests/class-test-nodeinfo-endpoint.php b/tests/phpunit/tests/class-test-nodeinfo-endpoint.php new file mode 100644 index 0000000..d30ae05 --- /dev/null +++ b/tests/phpunit/tests/class-test-nodeinfo-endpoint.php @@ -0,0 +1,261 @@ +server = $wp_rest_server; + + do_action( 'rest_api_init' ); + } + + /** + * Tear down the test. + */ + public function tear_down() { + global $wp_rest_server; + $wp_rest_server = null; + + parent::tear_down(); + } + + /** + * Test that the discovery endpoint is registered. + * + * @covers ::register_routes + */ + public function test_discovery_endpoint_registered() { + $routes = $this->server->get_routes(); + + $this->assertArrayHasKey( '/nodeinfo/discovery', $routes ); + } + + /** + * Test that the versioned endpoint is registered. + * + * @covers ::register_routes + */ + public function test_versioned_endpoint_registered() { + $routes = $this->server->get_routes(); + + $this->assertArrayHasKey( '/nodeinfo/(?P\\d\\.\\d)', $routes ); + } + + /** + * Test the discovery endpoint response. + * + * @covers ::get_discovery + */ + public function test_discovery_endpoint_response() { + $request = new \WP_REST_Request( 'GET', '/nodeinfo/discovery' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertArrayHasKey( 'links', $data ); + $this->assertIsArray( $data['links'] ); + } + + /** + * Test that all NodeInfo versions are in discovery links. + * + * @covers ::get_discovery + */ + public function test_discovery_contains_all_versions() { + $request = new \WP_REST_Request( 'GET', '/nodeinfo/discovery' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $versions = array( '1.0', '1.1', '2.0', '2.1' ); + $links = $data['links']; + + foreach ( $versions as $version ) { + $found = false; + foreach ( $links as $link ) { + if ( strpos( $link['rel'], $version ) !== false ) { + $found = true; + break; + } + } + $this->assertTrue( $found, "Version {$version} not found in discovery links" ); + } + } + + /** + * Test the NodeInfo 2.0 endpoint response. + * + * @covers ::get_item + */ + public function test_nodeinfo_20_endpoint() { + $request = new \WP_REST_Request( 'GET', '/nodeinfo/2.0' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( '2.0', $data['version'] ); + $this->assertArrayHasKey( 'software', $data ); + $this->assertArrayHasKey( 'protocols', $data ); + $this->assertArrayHasKey( 'services', $data ); + $this->assertArrayHasKey( 'openRegistrations', $data ); + $this->assertArrayHasKey( 'usage', $data ); + $this->assertArrayHasKey( 'metadata', $data ); + } + + /** + * Test the NodeInfo 2.1 endpoint response. + * + * @covers ::get_item + */ + public function test_nodeinfo_21_endpoint() { + $request = new \WP_REST_Request( 'GET', '/nodeinfo/2.1' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( '2.1', $data['version'] ); + $this->assertArrayHasKey( 'software', $data ); + // phpcs:ignore WordPress.WP.CapitalPDangit.MisspelledInText -- NodeInfo spec uses lowercase. + $this->assertEquals( 'wordpress', $data['software']['name'] ); + } + + /** + * Test the NodeInfo 1.0 endpoint response. + * + * @covers ::get_item + */ + public function test_nodeinfo_10_endpoint() { + $request = new \WP_REST_Request( 'GET', '/nodeinfo/1.0' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( '1.0', $data['version'] ); + $this->assertArrayHasKey( 'protocols', $data ); + // NodeInfo 1.0 uses inbound/outbound for protocols. + $this->assertArrayHasKey( 'inbound', $data['protocols'] ); + $this->assertArrayHasKey( 'outbound', $data['protocols'] ); + } + + /** + * Test invalid version returns error. + * + * @covers ::get_item + */ + public function test_invalid_version_returns_error() { + $request = new \WP_REST_Request( 'GET', '/nodeinfo/9.9' ); + $response = $this->server->dispatch( $request ); + + $this->assertEquals( 404, $response->get_status() ); + } + + /** + * Test software name is lowercase per NodeInfo spec. + * + * @covers \Nodeinfo\Integration\Nodeinfo20::software + */ + public function test_software_name() { + $request = new \WP_REST_Request( 'GET', '/nodeinfo/2.0' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + // phpcs:ignore WordPress.WP.CapitalPDangit.MisspelledInText -- NodeInfo spec uses lowercase. + $this->assertEquals( 'wordpress', $data['software']['name'] ); + } + + /** + * Test services structure. + * + * @covers \Nodeinfo\Integration\Nodeinfo20::services + */ + public function test_services_structure() { + $request = new \WP_REST_Request( 'GET', '/nodeinfo/2.0' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertArrayHasKey( 'inbound', $data['services'] ); + $this->assertArrayHasKey( 'outbound', $data['services'] ); + $this->assertIsArray( $data['services']['inbound'] ); + $this->assertIsArray( $data['services']['outbound'] ); + } + + /** + * Test usage statistics structure. + * + * @covers \Nodeinfo\Integration\Nodeinfo20::usage + */ + public function test_usage_structure() { + $request = new \WP_REST_Request( 'GET', '/nodeinfo/2.0' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertArrayHasKey( 'users', $data['usage'] ); + $this->assertArrayHasKey( 'total', $data['usage']['users'] ); + $this->assertArrayHasKey( 'activeMonth', $data['usage']['users'] ); + $this->assertArrayHasKey( 'activeHalfyear', $data['usage']['users'] ); + $this->assertArrayHasKey( 'localPosts', $data['usage'] ); + $this->assertArrayHasKey( 'localComments', $data['usage'] ); + } + + /** + * Test metadata contains node info. + * + * @covers \Nodeinfo\Integration\Nodeinfo20::metadata + */ + public function test_metadata_structure() { + $request = new \WP_REST_Request( 'GET', '/nodeinfo/2.0' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertArrayHasKey( 'nodeName', $data['metadata'] ); + $this->assertArrayHasKey( 'nodeDescription', $data['metadata'] ); + } + + /** + * Test openRegistrations reflects users_can_register option. + * + * @covers ::get_item + */ + public function test_open_registrations() { + update_option( 'users_can_register', '1' ); + + $request = new \WP_REST_Request( 'GET', '/nodeinfo/2.0' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertTrue( $data['openRegistrations'] ); + + update_option( 'users_can_register', '0' ); + + $request = new \WP_REST_Request( 'GET', '/nodeinfo/2.0' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertFalse( $data['openRegistrations'] ); + } +} diff --git a/tests/phpunit/tests/class-test-nodeinfo2-endpoint.php b/tests/phpunit/tests/class-test-nodeinfo2-endpoint.php new file mode 100644 index 0000000..81f3ea9 --- /dev/null +++ b/tests/phpunit/tests/class-test-nodeinfo2-endpoint.php @@ -0,0 +1,170 @@ +server = $wp_rest_server; + + do_action( 'rest_api_init' ); + } + + /** + * Tear down the test. + */ + public function tear_down() { + global $wp_rest_server; + $wp_rest_server = null; + + parent::tear_down(); + } + + /** + * Test that the NodeInfo2 endpoint is registered. + * + * @covers ::register_routes + */ + public function test_nodeinfo2_endpoint_registered() { + $routes = $this->server->get_routes(); + + $this->assertArrayHasKey( '/nodeinfo2/1.0', $routes ); + } + + /** + * Test the NodeInfo2 endpoint response structure. + * + * @covers ::get_item + */ + public function test_nodeinfo2_endpoint_response() { + $request = new \WP_REST_Request( 'GET', '/nodeinfo2/1.0' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertArrayHasKey( 'version', $data ); + $this->assertEquals( '1.0', $data['version'] ); + $this->assertArrayHasKey( 'server', $data ); + $this->assertArrayHasKey( 'protocols', $data ); + $this->assertArrayHasKey( 'openRegistrations', $data ); + $this->assertArrayHasKey( 'usage', $data ); + } + + /** + * Test NodeInfo2 server information. + * + * @covers ::get_item + */ + public function test_nodeinfo2_server_info() { + $request = new \WP_REST_Request( 'GET', '/nodeinfo2/1.0' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertArrayHasKey( 'baseUrl', $data['server'] ); + $this->assertArrayHasKey( 'name', $data['server'] ); + $this->assertArrayHasKey( 'software', $data['server'] ); + // phpcs:ignore WordPress.WP.CapitalPDangit.MisspelledInText -- NodeInfo2 spec uses lowercase. + $this->assertEquals( 'wordpress', $data['server']['software'] ); + } + + /** + * Test NodeInfo2 usage statistics. + * + * @covers ::get_item + */ + public function test_nodeinfo2_usage() { + $request = new \WP_REST_Request( 'GET', '/nodeinfo2/1.0' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertArrayHasKey( 'users', $data['usage'] ); + $this->assertArrayHasKey( 'total', $data['usage']['users'] ); + $this->assertArrayHasKey( 'activeMonth', $data['usage']['users'] ); + $this->assertArrayHasKey( 'activeHalfyear', $data['usage']['users'] ); + $this->assertArrayHasKey( 'localPosts', $data['usage'] ); + $this->assertArrayHasKey( 'localComments', $data['usage'] ); + } + + /** + * Test NodeInfo2 protocols array. + * + * @covers ::get_item + */ + public function test_nodeinfo2_protocols() { + $request = new \WP_REST_Request( 'GET', '/nodeinfo2/1.0' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertIsArray( $data['protocols'] ); + } + + /** + * Test NodeInfo2 openRegistrations. + * + * @covers ::get_item + */ + public function test_nodeinfo2_open_registrations() { + update_option( 'users_can_register', '1' ); + + $request = new \WP_REST_Request( 'GET', '/nodeinfo2/1.0' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertTrue( $data['openRegistrations'] ); + + update_option( 'users_can_register', '0' ); + + $request = new \WP_REST_Request( 'GET', '/nodeinfo2/1.0' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertFalse( $data['openRegistrations'] ); + } + + /** + * Test nodeinfo2_data filter. + * + * @covers ::get_item + */ + public function test_nodeinfo2_data_filter() { + add_filter( + 'nodeinfo2_data', + function ( $data ) { + $data['customField'] = 'test'; + return $data; + } + ); + + $request = new \WP_REST_Request( 'GET', '/nodeinfo2/1.0' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertArrayHasKey( 'customField', $data ); + $this->assertEquals( 'test', $data['customField'] ); + } +} From f24b9f47043ec3fe3c2e5c79638a23659111f6d4 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 8 Dec 2025 10:52:07 +0100 Subject: [PATCH 06/21] Fix PHPUnit workflow database creation Remove MARIADB_DATABASE from service config to let the install script create the database instead of MariaDB creating it automatically. --- .github/workflows/phpunit.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index f25003c..f82b764 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -25,7 +25,7 @@ jobs: image: mariadb:10.4 env: MARIADB_ROOT_PASSWORD: root - MARIADB_DATABASE: wordpress_test + MARIADB_ALLOW_EMPTY_ROOT_PASSWORD: true ports: - 3306:3306 options: >- @@ -63,7 +63,7 @@ jobs: uses: ramsey/composer-install@v3 - name: Setup Test Environment - run: bash bin/install-wp-tests.sh wordpress_test root root 127.0.0.1 ${{ matrix.wp-version }} true + run: bash bin/install-wp-tests.sh wordpress_test root root 127.0.0.1 ${{ matrix.wp-version }} - name: Run PHPUnit run: vendor/bin/phpunit From d63b498816b31a21d46c77d38ab8938b0d85bd9c Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 8 Dec 2025 10:55:40 +0100 Subject: [PATCH 07/21] Fix test failures - Remove coverage config from phpunit.xml.dist (not supported in PHPUnit 8) - Fix test_get_masked_version: function always masks, test format instead - Fix test_invalid_version: returns 400 (enum validation) not 404 - Fix test_nodeinfo2_endpoint_registered: use regex route pattern --- phpunit.xml.dist | 5 --- tests/phpunit/tests/class-test-functions.php | 31 +++---------------- .../tests/class-test-nodeinfo-endpoint.php | 3 +- .../tests/class-test-nodeinfo2-endpoint.php | 2 +- 4 files changed, 8 insertions(+), 33 deletions(-) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 0627b9a..2ee884b 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -11,9 +11,4 @@ ./tests/phpunit/tests - - - ./includes - - diff --git a/tests/phpunit/tests/class-test-functions.php b/tests/phpunit/tests/class-test-functions.php index 6b092b8..a89ff2b 100644 --- a/tests/phpunit/tests/class-test-functions.php +++ b/tests/phpunit/tests/class-test-functions.php @@ -28,36 +28,15 @@ public function test_get_masked_version() { } /** - * Test get_masked_version with masking enabled. + * Test get_masked_version returns major.minor format. * * @covers \Nodeinfo\get_masked_version */ - public function test_get_masked_version_with_masking() { - add_filter( 'nodeinfo_mask_version', '__return_true' ); - - $version = get_masked_version(); - $wp_version = get_bloginfo( 'version' ); - - // When masked, should only show major.minor. - $this->assertNotEquals( $wp_version, $version ); - - remove_filter( 'nodeinfo_mask_version', '__return_true' ); - } - - /** - * Test get_masked_version without masking. - * - * @covers \Nodeinfo\get_masked_version - */ - public function test_get_masked_version_without_masking() { - add_filter( 'nodeinfo_mask_version', '__return_false' ); - - $version = get_masked_version(); - $wp_version = get_bloginfo( 'version' ); - - $this->assertEquals( $wp_version, $version ); + public function test_get_masked_version_format() { + $version = get_masked_version(); - remove_filter( 'nodeinfo_mask_version', '__return_false' ); + // Should match major.minor format (e.g., "6.5"). + $this->assertMatchesRegularExpression( '/^\d+\.\d+$/', $version ); } /** diff --git a/tests/phpunit/tests/class-test-nodeinfo-endpoint.php b/tests/phpunit/tests/class-test-nodeinfo-endpoint.php index d30ae05..aa9154b 100644 --- a/tests/phpunit/tests/class-test-nodeinfo-endpoint.php +++ b/tests/phpunit/tests/class-test-nodeinfo-endpoint.php @@ -171,7 +171,8 @@ public function test_invalid_version_returns_error() { $request = new \WP_REST_Request( 'GET', '/nodeinfo/9.9' ); $response = $this->server->dispatch( $request ); - $this->assertEquals( 404, $response->get_status() ); + // Returns 400 (bad request) because enum validation fails. + $this->assertEquals( 400, $response->get_status() ); } /** diff --git a/tests/phpunit/tests/class-test-nodeinfo2-endpoint.php b/tests/phpunit/tests/class-test-nodeinfo2-endpoint.php index 81f3ea9..ef1766a 100644 --- a/tests/phpunit/tests/class-test-nodeinfo2-endpoint.php +++ b/tests/phpunit/tests/class-test-nodeinfo2-endpoint.php @@ -53,7 +53,7 @@ public function tear_down() { public function test_nodeinfo2_endpoint_registered() { $routes = $this->server->get_routes(); - $this->assertArrayHasKey( '/nodeinfo2/1.0', $routes ); + $this->assertArrayHasKey( '/nodeinfo2/(?P\\d\\.\\d)', $routes ); } /** From b4dd90b056ebb76f2dafa8f3e67a2e0371c75460 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 8 Dec 2025 10:58:35 +0100 Subject: [PATCH 08/21] Remove unused get_plugin_meta and get_plugin_version functions --- includes/functions.php | 38 -------------------------------------- 1 file changed, 38 deletions(-) diff --git a/includes/functions.php b/includes/functions.php index bd14b88..7a585df 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -57,41 +57,3 @@ function get_masked_version() { return implode( '.', $version ); } - -/** - * Gets the plugin version. - * - * @return string The plugin version. - */ -function get_plugin_version() { - $meta = get_plugin_meta( array( 'Version' => 'Version' ) ); - - return $meta['Version']; -} - -/** - * Gets plugin metadata. - * - * @param array $default_headers Optional headers to retrieve. - * @return array The plugin metadata. - */ -function get_plugin_meta( $default_headers = array() ) { - if ( ! $default_headers ) { - $default_headers = array( - 'Name' => 'Plugin Name', - 'PluginURI' => 'Plugin URI', - 'Version' => 'Version', - 'Description' => 'Description', - 'Author' => 'Author', - 'AuthorURI' => 'Author URI', - 'TextDomain' => 'Text Domain', - 'DomainPath' => 'Domain Path', - 'Network' => 'Network', - 'RequiresWP' => 'Requires at least', - 'RequiresPHP' => 'Requires PHP', - 'UpdateURI' => 'Update URI', - ); - } - - return \get_file_data( NODEINFO_PLUGIN_FILE, $default_headers, 'plugin' ); -} From f3a66b68463a35b715356f2a52c44f8806596469 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 8 Dec 2025 11:02:07 +0100 Subject: [PATCH 09/21] Remove translatable strings from schema descriptions Schema descriptions are not user-facing and should not be translated. --- includes/controller/class-nodeinfo.php | 2 +- includes/controller/class-nodeinfo2.php | 16 ++++++++-------- includes/integration/class-nodeinfo10.php | 14 +++++++------- includes/integration/class-nodeinfo20.php | 14 +++++++------- includes/integration/class-nodeinfo21.php | 14 +++++++------- 5 files changed, 30 insertions(+), 30 deletions(-) diff --git a/includes/controller/class-nodeinfo.php b/includes/controller/class-nodeinfo.php index e7bbc01..694e2f4 100644 --- a/includes/controller/class-nodeinfo.php +++ b/includes/controller/class-nodeinfo.php @@ -54,7 +54,7 @@ public function register_routes() { array( 'args' => array( 'version' => array( - 'description' => __( 'The NodeInfo schema version.', 'nodeinfo' ), + 'description' => 'The NodeInfo schema version.', 'type' => 'string', 'enum' => $versions, 'required' => true, diff --git a/includes/controller/class-nodeinfo2.php b/includes/controller/class-nodeinfo2.php index f790b3f..aecfd0c 100644 --- a/includes/controller/class-nodeinfo2.php +++ b/includes/controller/class-nodeinfo2.php @@ -38,7 +38,7 @@ public function register_routes() { array( 'args' => array( 'version' => array( - 'description' => __( 'The NodeInfo2 schema version.', 'nodeinfo' ), + 'description' => 'The NodeInfo2 schema version.', 'type' => 'string', 'enum' => array( '1.0' ), 'required' => true, @@ -143,12 +143,12 @@ public function get_item_schema() { 'type' => 'object', 'properties' => array( 'version' => array( - 'description' => __( 'The NodeInfo2 schema version.', 'nodeinfo' ), + 'description' => 'The NodeInfo2 schema version.', 'type' => 'string', 'enum' => array( '1.0' ), ), 'server' => array( - 'description' => __( 'Metadata about the server.', 'nodeinfo' ), + 'description' => 'Metadata about the server.', 'type' => 'object', 'properties' => array( 'baseUrl' => array( @@ -167,14 +167,14 @@ public function get_item_schema() { ), ), 'protocols' => array( - 'description' => __( 'The protocols supported on this server.', 'nodeinfo' ), + 'description' => 'The protocols supported on this server.', 'type' => 'array', 'items' => array( 'type' => 'string', ), ), 'services' => array( - 'description' => __( 'Third party sites this server can connect to.', 'nodeinfo' ), + 'description' => 'Third party sites this server can connect to.', 'type' => 'object', 'properties' => array( 'inbound' => array( @@ -192,11 +192,11 @@ public function get_item_schema() { ), ), 'openRegistrations' => array( - 'description' => __( 'Whether this server allows open self-registration.', 'nodeinfo' ), + 'description' => 'Whether this server allows open self-registration.', 'type' => 'boolean', ), 'usage' => array( - 'description' => __( 'Usage statistics for this server.', 'nodeinfo' ), + 'description' => 'Usage statistics for this server.', 'type' => 'object', 'properties' => array( 'users' => array( @@ -222,7 +222,7 @@ public function get_item_schema() { ), ), 'metadata' => array( - 'description' => __( 'Free form key value pairs for software specific values.', 'nodeinfo' ), + 'description' => 'Free form key value pairs for software specific values.', 'type' => 'object', ), ), diff --git a/includes/integration/class-nodeinfo10.php b/includes/integration/class-nodeinfo10.php index 3dd60f9..aaa15b8 100644 --- a/includes/integration/class-nodeinfo10.php +++ b/includes/integration/class-nodeinfo10.php @@ -71,11 +71,11 @@ public static function schema( $schema ) { $schema['properties'], array( 'version' => array( - 'description' => __( 'The NodeInfo schema version.', 'nodeinfo' ), + 'description' => 'The NodeInfo schema version.', 'type' => 'string', ), 'software' => array( - 'description' => __( 'Metadata about server software in use.', 'nodeinfo' ), + 'description' => 'Metadata about server software in use.', 'type' => 'object', 'properties' => array( 'name' => array( 'type' => 'string' ), @@ -83,7 +83,7 @@ public static function schema( $schema ) { ), ), 'protocols' => array( - 'description' => __( 'The protocols supported on this server.', 'nodeinfo' ), + 'description' => 'The protocols supported on this server.', 'type' => 'object', 'properties' => array( 'inbound' => array( @@ -97,7 +97,7 @@ public static function schema( $schema ) { ), ), 'services' => array( - 'description' => __( 'Third party sites this server can connect to.', 'nodeinfo' ), + 'description' => 'Third party sites this server can connect to.', 'type' => 'object', 'properties' => array( 'inbound' => array( @@ -111,11 +111,11 @@ public static function schema( $schema ) { ), ), 'openRegistrations' => array( - 'description' => __( 'Whether this server allows open self-registration.', 'nodeinfo' ), + 'description' => 'Whether this server allows open self-registration.', 'type' => 'boolean', ), 'usage' => array( - 'description' => __( 'Usage statistics for this server.', 'nodeinfo' ), + 'description' => 'Usage statistics for this server.', 'type' => 'object', 'properties' => array( 'users' => array( @@ -131,7 +131,7 @@ public static function schema( $schema ) { ), ), 'metadata' => array( - 'description' => __( 'Free form key value pairs for software specific values.', 'nodeinfo' ), + 'description' => 'Free form key value pairs for software specific values.', 'type' => 'object', ), ) diff --git a/includes/integration/class-nodeinfo20.php b/includes/integration/class-nodeinfo20.php index 10fd4d7..00b4f1a 100644 --- a/includes/integration/class-nodeinfo20.php +++ b/includes/integration/class-nodeinfo20.php @@ -71,11 +71,11 @@ public static function schema( $schema ) { $schema['properties'], array( 'version' => array( - 'description' => __( 'The NodeInfo schema version.', 'nodeinfo' ), + 'description' => 'The NodeInfo schema version.', 'type' => 'string', ), 'software' => array( - 'description' => __( 'Metadata about server software in use.', 'nodeinfo' ), + 'description' => 'Metadata about server software in use.', 'type' => 'object', 'properties' => array( 'name' => array( 'type' => 'string' ), @@ -83,12 +83,12 @@ public static function schema( $schema ) { ), ), 'protocols' => array( - 'description' => __( 'The protocols supported on this server.', 'nodeinfo' ), + 'description' => 'The protocols supported on this server.', 'type' => 'array', 'items' => array( 'type' => 'string' ), ), 'services' => array( - 'description' => __( 'Third party sites this server can connect to.', 'nodeinfo' ), + 'description' => 'Third party sites this server can connect to.', 'type' => 'object', 'properties' => array( 'inbound' => array( @@ -102,11 +102,11 @@ public static function schema( $schema ) { ), ), 'openRegistrations' => array( - 'description' => __( 'Whether this server allows open self-registration.', 'nodeinfo' ), + 'description' => 'Whether this server allows open self-registration.', 'type' => 'boolean', ), 'usage' => array( - 'description' => __( 'Usage statistics for this server.', 'nodeinfo' ), + 'description' => 'Usage statistics for this server.', 'type' => 'object', 'properties' => array( 'users' => array( @@ -122,7 +122,7 @@ public static function schema( $schema ) { ), ), 'metadata' => array( - 'description' => __( 'Free form key value pairs for software specific values.', 'nodeinfo' ), + 'description' => 'Free form key value pairs for software specific values.', 'type' => 'object', ), ) diff --git a/includes/integration/class-nodeinfo21.php b/includes/integration/class-nodeinfo21.php index 8a076ed..852c45d 100644 --- a/includes/integration/class-nodeinfo21.php +++ b/includes/integration/class-nodeinfo21.php @@ -71,11 +71,11 @@ public static function schema( $schema ) { $schema['properties'], array( 'version' => array( - 'description' => __( 'The NodeInfo schema version.', 'nodeinfo' ), + 'description' => 'The NodeInfo schema version.', 'type' => 'string', ), 'software' => array( - 'description' => __( 'Metadata about server software in use.', 'nodeinfo' ), + 'description' => 'Metadata about server software in use.', 'type' => 'object', 'properties' => array( 'name' => array( 'type' => 'string' ), @@ -91,12 +91,12 @@ public static function schema( $schema ) { ), ), 'protocols' => array( - 'description' => __( 'The protocols supported on this server.', 'nodeinfo' ), + 'description' => 'The protocols supported on this server.', 'type' => 'array', 'items' => array( 'type' => 'string' ), ), 'services' => array( - 'description' => __( 'Third party sites this server can connect to.', 'nodeinfo' ), + 'description' => 'Third party sites this server can connect to.', 'type' => 'object', 'properties' => array( 'inbound' => array( @@ -110,11 +110,11 @@ public static function schema( $schema ) { ), ), 'openRegistrations' => array( - 'description' => __( 'Whether this server allows open self-registration.', 'nodeinfo' ), + 'description' => 'Whether this server allows open self-registration.', 'type' => 'boolean', ), 'usage' => array( - 'description' => __( 'Usage statistics for this server.', 'nodeinfo' ), + 'description' => 'Usage statistics for this server.', 'type' => 'object', 'properties' => array( 'users' => array( @@ -130,7 +130,7 @@ public static function schema( $schema ) { ), ), 'metadata' => array( - 'description' => __( 'Free form key value pairs for software specific values.', 'nodeinfo' ), + 'description' => 'Free form key value pairs for software specific values.', 'type' => 'object', ), ) From e87028efcb26eff77710682d12466fb18f52c602 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 8 Dec 2025 11:04:59 +0100 Subject: [PATCH 10/21] Add schema to NodeInfo 1.1 and add NodeInfo 2.2 support - Add missing schema method to NodeInfo 1.1 integration - Add NodeInfo 2.2 integration with: - instance object (name, description) - activeWeek in usage.users - repository in software --- includes/integration/class-nodeinfo11.php | 82 +++++++ includes/integration/class-nodeinfo22.php | 265 ++++++++++++++++++++++ nodeinfo.php | 1 + 3 files changed, 348 insertions(+) create mode 100644 includes/integration/class-nodeinfo22.php diff --git a/includes/integration/class-nodeinfo11.php b/includes/integration/class-nodeinfo11.php index 5cf6450..6772216 100644 --- a/includes/integration/class-nodeinfo11.php +++ b/includes/integration/class-nodeinfo11.php @@ -27,6 +27,7 @@ class Nodeinfo11 { public static function init() { add_filter( 'nodeinfo_versions', array( __CLASS__, 'register_version' ) ); add_filter( 'nodeinfo_discovery_links', array( __CLASS__, 'discovery_link' ) ); + add_filter( 'nodeinfo_schema', array( __CLASS__, 'schema' ) ); add_filter( 'nodeinfo_data_software', array( __CLASS__, 'software' ), 10, 2 ); add_filter( 'nodeinfo_data_protocols', array( __CLASS__, 'protocols' ), 10, 2 ); add_filter( 'nodeinfo_data_services', array( __CLASS__, 'services' ), 10, 2 ); @@ -59,6 +60,87 @@ public static function discovery_link( $links ) { return $links; } + /** + * Adds the schema for NodeInfo 1.1. + * + * @param array $schema The schema. + * @return array The modified schema. + */ + public static function schema( $schema ) { + // NodeInfo 1.1 schema - same as 1.0, protocols uses inbound/outbound structure. + $schema['properties'] = array_merge( + $schema['properties'], + array( + 'version' => array( + 'description' => 'The NodeInfo schema version.', + 'type' => 'string', + ), + 'software' => array( + 'description' => 'Metadata about server software in use.', + 'type' => 'object', + 'properties' => array( + 'name' => array( 'type' => 'string' ), + 'version' => array( 'type' => 'string' ), + ), + ), + 'protocols' => array( + 'description' => 'The protocols supported on this server.', + 'type' => 'object', + 'properties' => array( + 'inbound' => array( + 'type' => 'array', + 'items' => array( 'type' => 'string' ), + ), + 'outbound' => array( + 'type' => 'array', + 'items' => array( 'type' => 'string' ), + ), + ), + ), + 'services' => array( + 'description' => 'Third party sites this server can connect to.', + 'type' => 'object', + 'properties' => array( + 'inbound' => array( + 'type' => 'array', + 'items' => array( 'type' => 'string' ), + ), + 'outbound' => array( + 'type' => 'array', + 'items' => array( 'type' => 'string' ), + ), + ), + ), + 'openRegistrations' => array( + 'description' => 'Whether this server allows open self-registration.', + 'type' => 'boolean', + ), + 'usage' => array( + 'description' => 'Usage statistics for this server.', + 'type' => 'object', + 'properties' => array( + 'users' => array( + 'type' => 'object', + 'properties' => array( + 'total' => array( 'type' => 'integer' ), + 'activeMonth' => array( 'type' => 'integer' ), + 'activeHalfyear' => array( 'type' => 'integer' ), + ), + ), + 'localPosts' => array( 'type' => 'integer' ), + 'localComments' => array( 'type' => 'integer' ), + ), + ), + 'metadata' => array( + 'description' => 'Free form key value pairs for software specific values.', + 'type' => 'object', + ), + ) + ); + + return $schema; + } + /** * Adds software information. * diff --git a/includes/integration/class-nodeinfo22.php b/includes/integration/class-nodeinfo22.php new file mode 100644 index 0000000..d036ae8 --- /dev/null +++ b/includes/integration/class-nodeinfo22.php @@ -0,0 +1,265 @@ + 'http://nodeinfo.diaspora.software/ns/schema/' . self::VERSION, + 'href' => get_rest_url( null, '/nodeinfo/' . self::VERSION ), + ); + return $links; + } + + /** + * Adds the schema for NodeInfo 2.2. + * + * @param array $schema The schema. + * @return array The modified schema. + */ + public static function schema( $schema ) { + // NodeInfo 2.2 schema - adds instance and activeWeek. + $schema['properties'] = array_merge( + $schema['properties'], + array( + 'version' => array( + 'description' => 'The NodeInfo schema version.', + 'type' => 'string', + ), + 'instance' => array( + 'description' => 'Metadata about this specific instance.', + 'type' => 'object', + 'properties' => array( + 'name' => array( 'type' => 'string' ), + 'description' => array( 'type' => 'string' ), + ), + ), + 'software' => array( + 'description' => 'Metadata about server software in use.', + 'type' => 'object', + 'properties' => array( + 'name' => array( 'type' => 'string' ), + 'version' => array( 'type' => 'string' ), + 'repository' => array( + 'type' => 'string', + 'format' => 'uri', + ), + 'homepage' => array( + 'type' => 'string', + 'format' => 'uri', + ), + ), + ), + 'protocols' => array( + 'description' => 'The protocols supported on this server.', + 'type' => 'array', + 'items' => array( 'type' => 'string' ), + ), + 'services' => array( + 'description' => 'Third party sites this server can connect to.', + 'type' => 'object', + 'properties' => array( + 'inbound' => array( + 'type' => 'array', + 'items' => array( 'type' => 'string' ), + ), + 'outbound' => array( + 'type' => 'array', + 'items' => array( 'type' => 'string' ), + ), + ), + ), + 'openRegistrations' => array( + 'description' => 'Whether this server allows open self-registration.', + 'type' => 'boolean', + ), + 'usage' => array( + 'description' => 'Usage statistics for this server.', + 'type' => 'object', + 'properties' => array( + 'users' => array( + 'type' => 'object', + 'properties' => array( + 'total' => array( 'type' => 'integer' ), + 'activeMonth' => array( 'type' => 'integer' ), + 'activeHalfyear' => array( 'type' => 'integer' ), + 'activeWeek' => array( 'type' => 'integer' ), + ), + ), + 'localPosts' => array( 'type' => 'integer' ), + 'localComments' => array( 'type' => 'integer' ), + ), + ), + 'metadata' => array( + 'description' => 'Free form key value pairs for software specific values.', + 'type' => 'object', + ), + ) + ); + + return $schema; + } + + /** + * Adds software information. + * + * @param array $software The software data. + * @param string $version The NodeInfo version. + * @return array The modified software data. + */ + public static function software( $software, $version ) { + if ( self::VERSION !== $version ) { + return $software; + } + + // phpcs:ignore WordPress.WP.CapitalPDangit.MisspelledInText -- NodeInfo spec uses lowercase. + $software['name'] = 'wordpress'; + $software['version'] = get_masked_version(); + $software['repository'] = 'https://github.com/wordpress/wordpress'; + + return $software; + } + + /** + * Adds services. + * + * @param array $services The services data. + * @param string $version The NodeInfo version. + * @return array The modified services data. + */ + public static function services( $services, $version ) { + if ( self::VERSION !== $version ) { + return $services; + } + + $services['inbound'] = array( 'atom1.0', 'rss2.0', 'pop3' ); + $services['outbound'] = array( 'atom1.0', 'rss2.0', 'wordpress', 'smtp' ); + + return $services; + } + + /** + * Adds usage statistics. + * + * @param array $usage The usage data. + * @param string $version The NodeInfo version. + * @return array The modified usage data. + */ + public static function usage( $usage, $version ) { + if ( self::VERSION !== $version ) { + return $usage; + } + + $users = get_users( + array( + 'fields' => 'ID', + 'capability__in' => array( 'publish_posts' ), + ) + ); + + $user_count = is_array( $users ) ? count( $users ) : 1; + + $posts = wp_count_posts(); + $comments = wp_count_comments(); + + $usage['users'] = array( + 'total' => $user_count, + 'activeMonth' => get_active_users( '1 month ago' ), + 'activeHalfyear' => get_active_users( '6 month ago' ), + 'activeWeek' => get_active_users( '1 week ago' ), + ); + + $usage['localPosts'] = (int) $posts->publish; + $usage['localComments'] = (int) $comments->approved; + + return $usage; + } + + /** + * Adds metadata. + * + * @param array $metadata The metadata. + * @param string $version The NodeInfo version. + * @return array The modified metadata. + */ + public static function metadata( $metadata, $version ) { + if ( self::VERSION !== $version ) { + return $metadata; + } + + $metadata['nodeName'] = get_bloginfo( 'name' ); + $metadata['nodeDescription'] = get_bloginfo( 'description' ); + $metadata['nodeIcon'] = get_site_icon_url(); + + return $metadata; + } + + /** + * Adds instance information (new in 2.2). + * + * @param array $nodeinfo The NodeInfo data. + * @param string $version The NodeInfo version. + * @return array The modified NodeInfo data. + */ + public static function add_instance( $nodeinfo, $version ) { + if ( self::VERSION !== $version ) { + return $nodeinfo; + } + + $nodeinfo['instance'] = array( + 'name' => get_bloginfo( 'name' ), + 'description' => get_bloginfo( 'description' ), + ); + + return $nodeinfo; + } +} diff --git a/nodeinfo.php b/nodeinfo.php index 8591e6e..87533aa 100644 --- a/nodeinfo.php +++ b/nodeinfo.php @@ -37,6 +37,7 @@ function nodeinfo_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' ); From 485231265af9ce5ad68fc37dcc3987aed37c2849 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 8 Dec 2025 11:13:00 +0100 Subject: [PATCH 11/21] Update NodeInfo schema definitions and protocol handling Adds stricter schema validation for NodeInfo versions 1.0, 1.1, 2.0, 2.1, and 2.2, including enums, patterns, and minimums for properties. Introduces protocol handling hooks for NodeInfo 2.x versions and updates supported protocol/service lists. Adds homepage to software metadata for 2.1 and 2.2, and improves documentation with schema links. --- includes/integration/class-nodeinfo10.php | 52 +++++++++++--- includes/integration/class-nodeinfo11.php | 54 +++++++++++--- includes/integration/class-nodeinfo20.php | 69 +++++++++++++++--- includes/integration/class-nodeinfo21.php | 70 ++++++++++++++++--- includes/integration/class-nodeinfo22.php | 85 +++++++++++++++++++---- 5 files changed, 276 insertions(+), 54 deletions(-) diff --git a/includes/integration/class-nodeinfo10.php b/includes/integration/class-nodeinfo10.php index aaa15b8..44f72aa 100644 --- a/includes/integration/class-nodeinfo10.php +++ b/includes/integration/class-nodeinfo10.php @@ -63,6 +63,8 @@ public static function discovery_link( $links ) { /** * Adds the schema for this version. * + * @link https://github.com/jhass/nodeinfo/blob/main/schemas/1.0/schema.json + * * @param array $schema The schema. * @return array The modified schema. */ @@ -78,7 +80,10 @@ public static function schema( $schema ) { 'description' => 'Metadata about server software in use.', 'type' => 'object', 'properties' => array( - 'name' => array( 'type' => 'string' ), + 'name' => array( + 'type' => 'string', + 'enum' => array( 'diaspora', 'friendica', 'redmatrix' ), + ), 'version' => array( 'type' => 'string' ), ), ), @@ -88,11 +93,17 @@ public static function schema( $schema ) { 'properties' => array( 'inbound' => array( 'type' => 'array', - 'items' => array( 'type' => 'string' ), + 'items' => array( + 'type' => 'string', + 'enum' => array( 'buddycloud', 'diaspora', 'friendica', 'gnusocial', 'libertree', 'mediagoblin', 'pumpio', 'redmatrix', 'smtp', 'tent' ), + ), ), 'outbound' => array( 'type' => 'array', - 'items' => array( 'type' => 'string' ), + 'items' => array( + 'type' => 'string', + 'enum' => array( 'buddycloud', 'diaspora', 'friendica', 'gnusocial', 'libertree', 'mediagoblin', 'pumpio', 'redmatrix', 'smtp', 'tent' ), + ), ), ), ), @@ -102,11 +113,17 @@ public static function schema( $schema ) { 'properties' => array( 'inbound' => array( 'type' => 'array', - 'items' => array( 'type' => 'string' ), + 'items' => array( + 'type' => 'string', + 'enum' => array( 'appnet', 'gnusocial', 'pumpio' ), + ), ), 'outbound' => array( 'type' => 'array', - 'items' => array( 'type' => 'string' ), + 'items' => array( + 'type' => 'string', + 'enum' => array( 'appnet', 'blogger', 'buddycloud', 'diaspora', 'dreamwidth', 'drupal', 'facebook', 'friendica', 'gnusocial', 'google', 'insanejournal', 'libertree', 'linkedin', 'livejournal', 'mediagoblin', 'myspace', 'pinterest', 'posterous', 'pumpio', 'redmatrix', 'smtp', 'tent', 'tumblr', 'twitter', 'wordpress', 'xmpp' ), + ), ), ), ), @@ -121,13 +138,28 @@ public static function schema( $schema ) { 'users' => array( 'type' => 'object', 'properties' => array( - 'total' => array( 'type' => 'integer' ), - 'activeMonth' => array( 'type' => 'integer' ), - 'activeHalfyear' => array( 'type' => 'integer' ), + 'total' => array( + 'type' => 'integer', + 'minimum' => 0, + ), + 'activeMonth' => array( + 'type' => 'integer', + 'minimum' => 0, + ), + 'activeHalfyear' => array( + 'type' => 'integer', + 'minimum' => 0, + ), ), ), - 'localPosts' => array( 'type' => 'integer' ), - 'localComments' => array( 'type' => 'integer' ), + 'localPosts' => array( + 'type' => 'integer', + 'minimum' => 0, + ), + 'localComments' => array( + 'type' => 'integer', + 'minimum' => 0, + ), ), ), 'metadata' => array( diff --git a/includes/integration/class-nodeinfo11.php b/includes/integration/class-nodeinfo11.php index 6772216..8434877 100644 --- a/includes/integration/class-nodeinfo11.php +++ b/includes/integration/class-nodeinfo11.php @@ -63,11 +63,13 @@ public static function discovery_link( $links ) { /** * Adds the schema for NodeInfo 1.1. * + * @link https://github.com/jhass/nodeinfo/blob/main/schemas/1.1/schema.json + * * @param array $schema The schema. * @return array The modified schema. */ public static function schema( $schema ) { - // NodeInfo 1.1 schema - same as 1.0, protocols uses inbound/outbound structure. + // NodeInfo 1.1 schema - adds hubzilla to software enum and zot to protocols. $schema['properties'] = array_merge( $schema['properties'], array( @@ -79,7 +81,10 @@ public static function schema( $schema ) { 'description' => 'Metadata about server software in use.', 'type' => 'object', 'properties' => array( - 'name' => array( 'type' => 'string' ), + 'name' => array( + 'type' => 'string', + 'enum' => array( 'diaspora', 'friendica', 'hubzilla', 'redmatrix' ), + ), 'version' => array( 'type' => 'string' ), ), ), @@ -89,11 +94,17 @@ public static function schema( $schema ) { 'properties' => array( 'inbound' => array( 'type' => 'array', - 'items' => array( 'type' => 'string' ), + 'items' => array( + 'type' => 'string', + 'enum' => array( 'buddycloud', 'diaspora', 'friendica', 'gnusocial', 'libertree', 'mediagoblin', 'pumpio', 'redmatrix', 'smtp', 'tent', 'zot' ), + ), ), 'outbound' => array( 'type' => 'array', - 'items' => array( 'type' => 'string' ), + 'items' => array( + 'type' => 'string', + 'enum' => array( 'buddycloud', 'diaspora', 'friendica', 'gnusocial', 'libertree', 'mediagoblin', 'pumpio', 'redmatrix', 'smtp', 'tent', 'zot' ), + ), ), ), ), @@ -103,11 +114,17 @@ public static function schema( $schema ) { 'properties' => array( 'inbound' => array( 'type' => 'array', - 'items' => array( 'type' => 'string' ), + 'items' => array( + 'type' => 'string', + 'enum' => array( 'appnet', 'gnusocial', 'pumpio' ), + ), ), 'outbound' => array( 'type' => 'array', - 'items' => array( 'type' => 'string' ), + 'items' => array( + 'type' => 'string', + 'enum' => array( 'appnet', 'blogger', 'buddycloud', 'diaspora', 'dreamwidth', 'drupal', 'facebook', 'friendica', 'gnusocial', 'google', 'insanejournal', 'libertree', 'linkedin', 'livejournal', 'mediagoblin', 'myspace', 'pinterest', 'posterous', 'pumpio', 'redmatrix', 'smtp', 'tent', 'tumblr', 'twitter', 'wordpress', 'xmpp' ), + ), ), ), ), @@ -122,13 +139,28 @@ public static function schema( $schema ) { 'users' => array( 'type' => 'object', 'properties' => array( - 'total' => array( 'type' => 'integer' ), - 'activeMonth' => array( 'type' => 'integer' ), - 'activeHalfyear' => array( 'type' => 'integer' ), + 'total' => array( + 'type' => 'integer', + 'minimum' => 0, + ), + 'activeMonth' => array( + 'type' => 'integer', + 'minimum' => 0, + ), + 'activeHalfyear' => array( + 'type' => 'integer', + 'minimum' => 0, + ), ), ), - 'localPosts' => array( 'type' => 'integer' ), - 'localComments' => array( 'type' => 'integer' ), + 'localPosts' => array( + 'type' => 'integer', + 'minimum' => 0, + ), + 'localComments' => array( + 'type' => 'integer', + 'minimum' => 0, + ), ), ), 'metadata' => array( diff --git a/includes/integration/class-nodeinfo20.php b/includes/integration/class-nodeinfo20.php index 00b4f1a..ca2f51d 100644 --- a/includes/integration/class-nodeinfo20.php +++ b/includes/integration/class-nodeinfo20.php @@ -29,6 +29,7 @@ public static function init() { add_filter( 'nodeinfo_discovery_links', array( __CLASS__, 'discovery_link' ) ); add_filter( 'nodeinfo_schema', array( __CLASS__, 'schema' ) ); add_filter( 'nodeinfo_data_software', array( __CLASS__, 'software' ), 10, 2 ); + add_filter( 'nodeinfo_data_protocols', array( __CLASS__, 'protocols' ), 10, 2 ); add_filter( 'nodeinfo_data_services', array( __CLASS__, 'services' ), 10, 2 ); add_filter( 'nodeinfo_data_usage', array( __CLASS__, 'usage' ), 10, 2 ); add_filter( 'nodeinfo_data_metadata', array( __CLASS__, 'metadata' ), 10, 2 ); @@ -62,11 +63,13 @@ public static function discovery_link( $links ) { /** * Adds the schema for NodeInfo 2.0. * + * @link https://github.com/jhass/nodeinfo/blob/main/schemas/2.0/schema.json + * * @param array $schema The schema. * @return array The modified schema. */ public static function schema( $schema ) { - // NodeInfo 2.0 schema - protocols is a flat array, not inbound/outbound. + // NodeInfo 2.0 schema - protocols is a flat array, software name is pattern-based. $schema['properties'] = array_merge( $schema['properties'], array( @@ -78,14 +81,21 @@ public static function schema( $schema ) { 'description' => 'Metadata about server software in use.', 'type' => 'object', 'properties' => array( - 'name' => array( 'type' => 'string' ), + 'name' => array( + 'type' => 'string', + 'pattern' => '^[a-z0-9-]+$', + ), 'version' => array( 'type' => 'string' ), ), ), 'protocols' => array( 'description' => 'The protocols supported on this server.', 'type' => 'array', - 'items' => array( 'type' => 'string' ), + 'minItems' => 1, + 'items' => array( + 'type' => 'string', + 'enum' => array( 'activitypub', 'buddycloud', 'dfrn', 'diaspora', 'libertree', 'ostatus', 'pumpio', 'tent', 'xmpp', 'zot' ), + ), ), 'services' => array( 'description' => 'Third party sites this server can connect to.', @@ -93,11 +103,17 @@ public static function schema( $schema ) { 'properties' => array( 'inbound' => array( 'type' => 'array', - 'items' => array( 'type' => 'string' ), + 'items' => array( + 'type' => 'string', + 'enum' => array( 'atom1.0', 'gnusocial', 'imap', 'pnut', 'pop3', 'pumpio', 'rss2.0', 'twitter' ), + ), ), 'outbound' => array( 'type' => 'array', - 'items' => array( 'type' => 'string' ), + 'items' => array( + 'type' => 'string', + 'enum' => array( 'atom1.0', 'blogger', 'buddycloud', 'diaspora', 'dreamwidth', 'drupal', 'facebook', 'friendica', 'gnusocial', 'google', 'insanejournal', 'libertree', 'linkedin', 'livejournal', 'mediagoblin', 'myspace', 'pinterest', 'pnut', 'posterous', 'pumpio', 'redmatrix', 'rss2.0', 'smtp', 'tent', 'tumblr', 'twitter', 'wordpress', 'xmpp' ), + ), ), ), ), @@ -112,13 +128,28 @@ public static function schema( $schema ) { 'users' => array( 'type' => 'object', 'properties' => array( - 'total' => array( 'type' => 'integer' ), - 'activeMonth' => array( 'type' => 'integer' ), - 'activeHalfyear' => array( 'type' => 'integer' ), + 'total' => array( + 'type' => 'integer', + 'minimum' => 0, + ), + 'activeMonth' => array( + 'type' => 'integer', + 'minimum' => 0, + ), + 'activeHalfyear' => array( + 'type' => 'integer', + 'minimum' => 0, + ), ), ), - 'localPosts' => array( 'type' => 'integer' ), - 'localComments' => array( 'type' => 'integer' ), + 'localPosts' => array( + 'type' => 'integer', + 'minimum' => 0, + ), + 'localComments' => array( + 'type' => 'integer', + 'minimum' => 0, + ), ), ), 'metadata' => array( @@ -150,6 +181,24 @@ public static function software( $software, $version ) { return $software; } + /** + * Adds protocols. + * + * NodeInfo 2.0+ uses a flat array of protocol strings. + * + * @param array $protocols The protocols data. + * @param string $version The NodeInfo version. + * @return array The modified protocols data. + */ + public static function protocols( $protocols, $version ) { + if ( self::VERSION !== $version ) { + return $protocols; + } + + // Default protocols - can be extended via filter. + return apply_filters( 'nodeinfo_protocols', array() ); + } + /** * Adds services. * diff --git a/includes/integration/class-nodeinfo21.php b/includes/integration/class-nodeinfo21.php index 852c45d..988ecdd 100644 --- a/includes/integration/class-nodeinfo21.php +++ b/includes/integration/class-nodeinfo21.php @@ -29,6 +29,7 @@ public static function init() { add_filter( 'nodeinfo_discovery_links', array( __CLASS__, 'discovery_link' ) ); add_filter( 'nodeinfo_schema', array( __CLASS__, 'schema' ) ); add_filter( 'nodeinfo_data_software', array( __CLASS__, 'software' ), 10, 2 ); + add_filter( 'nodeinfo_data_protocols', array( __CLASS__, 'protocols' ), 10, 2 ); add_filter( 'nodeinfo_data_services', array( __CLASS__, 'services' ), 10, 2 ); add_filter( 'nodeinfo_data_usage', array( __CLASS__, 'usage' ), 10, 2 ); add_filter( 'nodeinfo_data_metadata', array( __CLASS__, 'metadata' ), 10, 2 ); @@ -62,11 +63,13 @@ public static function discovery_link( $links ) { /** * Adds the schema for NodeInfo 2.1. * + * @link https://github.com/jhass/nodeinfo/blob/main/schemas/2.1/schema.json + * * @param array $schema The schema. * @return array The modified schema. */ public static function schema( $schema ) { - // NodeInfo 2.1 schema - adds repository to software. + // NodeInfo 2.1 schema - adds repository and homepage to software. $schema['properties'] = array_merge( $schema['properties'], array( @@ -78,7 +81,10 @@ public static function schema( $schema ) { 'description' => 'Metadata about server software in use.', 'type' => 'object', 'properties' => array( - 'name' => array( 'type' => 'string' ), + 'name' => array( + 'type' => 'string', + 'pattern' => '^[a-z0-9-]+$', + ), 'version' => array( 'type' => 'string' ), 'repository' => array( 'type' => 'string', @@ -93,7 +99,11 @@ public static function schema( $schema ) { 'protocols' => array( 'description' => 'The protocols supported on this server.', 'type' => 'array', - 'items' => array( 'type' => 'string' ), + 'minItems' => 1, + 'items' => array( + 'type' => 'string', + 'enum' => array( 'activitypub', 'buddycloud', 'dfrn', 'diaspora', 'libertree', 'ostatus', 'pumpio', 'tent', 'xmpp', 'zot' ), + ), ), 'services' => array( 'description' => 'Third party sites this server can connect to.', @@ -101,11 +111,17 @@ public static function schema( $schema ) { 'properties' => array( 'inbound' => array( 'type' => 'array', - 'items' => array( 'type' => 'string' ), + 'items' => array( + 'type' => 'string', + 'enum' => array( 'atom1.0', 'gnusocial', 'imap', 'pnut', 'pop3', 'pumpio', 'rss2.0', 'twitter' ), + ), ), 'outbound' => array( 'type' => 'array', - 'items' => array( 'type' => 'string' ), + 'items' => array( + 'type' => 'string', + 'enum' => array( 'atom1.0', 'blogger', 'buddycloud', 'diaspora', 'dreamwidth', 'drupal', 'facebook', 'friendica', 'gnusocial', 'google', 'insanejournal', 'libertree', 'linkedin', 'livejournal', 'mediagoblin', 'myspace', 'pinterest', 'pnut', 'posterous', 'pumpio', 'redmatrix', 'rss2.0', 'smtp', 'tent', 'tumblr', 'twitter', 'wordpress', 'xmpp' ), + ), ), ), ), @@ -120,13 +136,28 @@ public static function schema( $schema ) { 'users' => array( 'type' => 'object', 'properties' => array( - 'total' => array( 'type' => 'integer' ), - 'activeMonth' => array( 'type' => 'integer' ), - 'activeHalfyear' => array( 'type' => 'integer' ), + 'total' => array( + 'type' => 'integer', + 'minimum' => 0, + ), + 'activeMonth' => array( + 'type' => 'integer', + 'minimum' => 0, + ), + 'activeHalfyear' => array( + 'type' => 'integer', + 'minimum' => 0, + ), ), ), - 'localPosts' => array( 'type' => 'integer' ), - 'localComments' => array( 'type' => 'integer' ), + 'localPosts' => array( + 'type' => 'integer', + 'minimum' => 0, + ), + 'localComments' => array( + 'type' => 'integer', + 'minimum' => 0, + ), ), ), 'metadata' => array( @@ -155,10 +186,29 @@ public static function software( $software, $version ) { $software['name'] = 'wordpress'; $software['version'] = get_masked_version(); $software['repository'] = 'https://github.com/wordpress/wordpress'; + $software['homepage'] = 'https://wordpress.org'; return $software; } + /** + * Adds protocols. + * + * NodeInfo 2.0+ uses a flat array of protocol strings. + * + * @param array $protocols The protocols data. + * @param string $version The NodeInfo version. + * @return array The modified protocols data. + */ + public static function protocols( $protocols, $version ) { + if ( self::VERSION !== $version ) { + return $protocols; + } + + // Default protocols - can be extended via filter. + return apply_filters( 'nodeinfo_protocols', array() ); + } + /** * Adds services. * diff --git a/includes/integration/class-nodeinfo22.php b/includes/integration/class-nodeinfo22.php index d036ae8..955beda 100644 --- a/includes/integration/class-nodeinfo22.php +++ b/includes/integration/class-nodeinfo22.php @@ -29,6 +29,7 @@ public static function init() { add_filter( 'nodeinfo_discovery_links', array( __CLASS__, 'discovery_link' ) ); add_filter( 'nodeinfo_schema', array( __CLASS__, 'schema' ) ); add_filter( 'nodeinfo_data_software', array( __CLASS__, 'software' ), 10, 2 ); + add_filter( 'nodeinfo_data_protocols', array( __CLASS__, 'protocols' ), 10, 2 ); add_filter( 'nodeinfo_data_services', array( __CLASS__, 'services' ), 10, 2 ); add_filter( 'nodeinfo_data_usage', array( __CLASS__, 'usage' ), 10, 2 ); add_filter( 'nodeinfo_data_metadata', array( __CLASS__, 'metadata' ), 10, 2 ); @@ -63,11 +64,13 @@ public static function discovery_link( $links ) { /** * Adds the schema for NodeInfo 2.2. * + * @link https://github.com/jhass/nodeinfo/blob/main/schemas/2.2/schema.json + * * @param array $schema The schema. * @return array The modified schema. */ public static function schema( $schema ) { - // NodeInfo 2.2 schema - adds instance and activeWeek. + // NodeInfo 2.2 schema - adds instance, activeWeek, and nostr protocol. $schema['properties'] = array_merge( $schema['properties'], array( @@ -79,15 +82,24 @@ public static function schema( $schema ) { 'description' => 'Metadata about this specific instance.', 'type' => 'object', 'properties' => array( - 'name' => array( 'type' => 'string' ), - 'description' => array( 'type' => 'string' ), + 'name' => array( + 'type' => 'string', + 'maxLength' => 500, + ), + 'description' => array( + 'type' => 'string', + 'maxLength' => 5000, + ), ), ), 'software' => array( 'description' => 'Metadata about server software in use.', 'type' => 'object', 'properties' => array( - 'name' => array( 'type' => 'string' ), + 'name' => array( + 'type' => 'string', + 'pattern' => '^[a-z0-9-]+$', + ), 'version' => array( 'type' => 'string' ), 'repository' => array( 'type' => 'string', @@ -102,7 +114,11 @@ public static function schema( $schema ) { 'protocols' => array( 'description' => 'The protocols supported on this server.', 'type' => 'array', - 'items' => array( 'type' => 'string' ), + 'minItems' => 1, + 'items' => array( + 'type' => 'string', + 'enum' => array( 'activitypub', 'buddycloud', 'dfrn', 'diaspora', 'libertree', 'nostr', 'ostatus', 'pumpio', 'tent', 'xmpp', 'zot' ), + ), ), 'services' => array( 'description' => 'Third party sites this server can connect to.', @@ -110,11 +126,17 @@ public static function schema( $schema ) { 'properties' => array( 'inbound' => array( 'type' => 'array', - 'items' => array( 'type' => 'string' ), + 'items' => array( + 'type' => 'string', + 'enum' => array( 'atom1.0', 'gnusocial', 'imap', 'pnut', 'pop3', 'pumpio', 'rss2.0', 'twitter' ), + ), ), 'outbound' => array( 'type' => 'array', - 'items' => array( 'type' => 'string' ), + 'items' => array( + 'type' => 'string', + 'enum' => array( 'atom1.0', 'blogger', 'buddycloud', 'diaspora', 'dreamwidth', 'drupal', 'facebook', 'friendica', 'gnusocial', 'google', 'insanejournal', 'libertree', 'linkedin', 'livejournal', 'mediagoblin', 'myspace', 'pinterest', 'pnut', 'posterous', 'pumpio', 'redmatrix', 'rss2.0', 'smtp', 'tent', 'tumblr', 'twitter', 'wordpress', 'xmpp' ), + ), ), ), ), @@ -129,14 +151,32 @@ public static function schema( $schema ) { 'users' => array( 'type' => 'object', 'properties' => array( - 'total' => array( 'type' => 'integer' ), - 'activeMonth' => array( 'type' => 'integer' ), - 'activeHalfyear' => array( 'type' => 'integer' ), - 'activeWeek' => array( 'type' => 'integer' ), + 'total' => array( + 'type' => 'integer', + 'minimum' => 0, + ), + 'activeMonth' => array( + 'type' => 'integer', + 'minimum' => 0, + ), + 'activeHalfyear' => array( + 'type' => 'integer', + 'minimum' => 0, + ), + 'activeWeek' => array( + 'type' => 'integer', + 'minimum' => 0, + ), ), ), - 'localPosts' => array( 'type' => 'integer' ), - 'localComments' => array( 'type' => 'integer' ), + 'localPosts' => array( + 'type' => 'integer', + 'minimum' => 0, + ), + 'localComments' => array( + 'type' => 'integer', + 'minimum' => 0, + ), ), ), 'metadata' => array( @@ -165,10 +205,29 @@ public static function software( $software, $version ) { $software['name'] = 'wordpress'; $software['version'] = get_masked_version(); $software['repository'] = 'https://github.com/wordpress/wordpress'; + $software['homepage'] = 'https://wordpress.org'; return $software; } + /** + * Adds protocols. + * + * NodeInfo 2.0+ uses a flat array of protocol strings. + * + * @param array $protocols The protocols data. + * @param string $version The NodeInfo version. + * @return array The modified protocols data. + */ + public static function protocols( $protocols, $version ) { + if ( self::VERSION !== $version ) { + return $protocols; + } + + // Default protocols - can be extended via filter. + return apply_filters( 'nodeinfo_protocols', array() ); + } + /** * Adds services. * From 71f2846b069ea0681b478ae1f51d83950da937e0 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 8 Dec 2025 11:16:15 +0100 Subject: [PATCH 12/21] Bump NodeInfo plugin version to 3.0.0 Updated the plugin version in nodeinfo.php from 2.3.1 to 3.0.0 to reflect a new release. No other changes were made. --- nodeinfo.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nodeinfo.php b/nodeinfo.php index 87533aa..5410a28 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: 2.3.1 + * Version: 3.0.0 * Author: Matthias Pfefferle * Author URI: https://notiz.blog/ * License: MIT From 55ff527642d41c084982f65b8cc25a3ae5f5ff73 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 8 Dec 2025 11:19:23 +0100 Subject: [PATCH 13/21] Update requirements and changelog for 3.0.0 release Raised minimum WordPress and PHP versions, updated stable tag to 3.0.0, and added changelog for major refactor, NodeInfo 2.2 support, new integration classes, PSR-4 autoloader, schema updates, protocol filter, and homepage field. --- README.md | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 73cbb32..0e2f7e9 100644 --- a/README.md +++ b/README.md @@ -3,10 +3,10 @@ - Contributors: pfefferle - Donate link: https://notiz.blog/donate/ - Tags: nodeinfo, fediverse, ostatus, diaspora, activitypub -- Requires at least: 4.9 +- Requires at least: 6.6 - Tested up to: 6.9 -- Stable tag: 2.3.1 -- Requires PHP: 5.6 +- Stable tag: 3.0.0 +- Requires PHP: 7.2 - License: MIT - License URI: https://opensource.org/licenses/MIT @@ -24,6 +24,16 @@ This plugin provides a barebone JSON file with basic "node"-informations. The fi Project and support maintained on github at [pfefferle/wordpress-nodeinfo](https://github.com/pfefferle/wordpress-nodeinfo). +### 3.0.0 + +* Refactored to filter-based architecture for better extensibility +* Added support for NodeInfo 2.2 +* Added separate integration classes for each NodeInfo version (1.0, 1.1, 2.0, 2.1, 2.2) +* Added PSR-4 style autoloader +* Updated schemas to match official NodeInfo specifications with enums and constraints +* Added `nodeinfo_protocols` filter for plugins to register protocols +* Added `software.homepage` field for NodeInfo 2.1 and 2.2 + ### 2.3.1 * mask version number From da08d840cf1970c7b1b5e61aa0b619a22e19fb3c Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 8 Dec 2025 11:21:39 +0100 Subject: [PATCH 14/21] Expand README with plugin usage details Added sections describing the information shared by the plugin, supported NodeInfo versions, available endpoints, and frequently asked questions. This improves documentation for users and developers integrating with the plugin. --- README.md | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/README.md b/README.md index 0e2f7e9..3d9dacf 100644 --- a/README.md +++ b/README.md @@ -18,8 +18,56 @@ NodeInfo and NodeInfo2 for WordPress! This plugin provides a barebone JSON file with basic "node"-informations. The file can be extended by other WordPress plugins, like [OStatus](https://wordpress.org/plugins/ostatus-for-wordpress/), [Diaspora](https://github.com/pfefferle/wordpress-dandelion) or [ActivityPub](https://wordpress.org/plugins/activitypub/)/[Pterotype](https://wordpress.org/plugins/pterotype/). +### What information does this plugin share? + +The plugin exposes the following public information about your site: + +* **Software**: WordPress version (major version only for privacy) +* **Usage statistics**: Number of users, posts, and comments +* **Site info**: Your site name and description +* **Protocols**: Which federation protocols your site supports (e.g., ActivityPub) +* **Services**: Which external services your site can connect to (e.g., RSS feeds) + +This information helps other servers in the Fediverse discover and interact with your site. + +### Supported NodeInfo versions + +This plugin supports all major NodeInfo specification versions: + +* **NodeInfo 1.0** and **1.1** - Original specifications +* **NodeInfo 2.0**, **2.1**, and **2.2** - Current specifications with extended metadata +* **NodeInfo2** - Alternative single-endpoint format + +### Endpoints + +After activation, the following endpoints become available: + +* `/.well-known/nodeinfo` - Discovery document (start here) +* `/wp-json/nodeinfo/2.2` - NodeInfo 2.2 (recommended) +* `/wp-json/nodeinfo/2.1` - NodeInfo 2.1 +* `/wp-json/nodeinfo/2.0` - NodeInfo 2.0 +* `/wp-json/nodeinfo/1.1` - NodeInfo 1.1 +* `/wp-json/nodeinfo/1.0` - NodeInfo 1.0 +* `/.well-known/x-nodeinfo2` - NodeInfo2 format + ## Frequently Asked Questions +### Why do I need this plugin? + +If you want your WordPress site to be part of the Fediverse (decentralized social networks like Mastodon), this plugin helps other servers discover information about your site. It works together with plugins like [ActivityPub](https://wordpress.org/plugins/activitypub/) to make your site fully federated. + +### Is any private information shared? + +No. Only public information about your site is shared, such as your site name, description, and post counts. No personal user data or private content is exposed. + +### How can I verify it's working? + +Visit `https://yoursite.com/.well-known/nodeinfo` in your browser. You should see a JSON document with links to the NodeInfo endpoints. + +### Can other plugins extend the NodeInfo data? + +Yes! This plugin is designed to be extensible. Other plugins can use WordPress filters to add their own protocols, services, or metadata. For example, the ActivityPub plugin automatically adds `activitypub` to the supported protocols list. + ## Changelog Project and support maintained on github at [pfefferle/wordpress-nodeinfo](https://github.com/pfefferle/wordpress-nodeinfo). From f6965059548858ccaeb5a36c02c0873b8f68906f Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 8 Dec 2025 11:23:39 +0100 Subject: [PATCH 15/21] Add Site Health checks for NodeInfo endpoints Introduces a Health_Check class to provide Site Health tests for NodeInfo endpoints, verifying accessibility of the well-known and REST endpoints. Updates documentation and plugin initialization to include these checks, helping users diagnose configuration issues. --- README.md | 10 ++ includes/class-health-check.php | 224 ++++++++++++++++++++++++++++++++ nodeinfo.php | 3 + 3 files changed, 237 insertions(+) create mode 100644 includes/class-health-check.php diff --git a/README.md b/README.md index 3d9dacf..eb5bdf3 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,15 @@ Visit `https://yoursite.com/.well-known/nodeinfo` in your browser. You should se Yes! This plugin is designed to be extensible. Other plugins can use WordPress filters to add their own protocols, services, or metadata. For example, the ActivityPub plugin automatically adds `activitypub` to the supported protocols list. +### How do I know if everything is configured correctly? + +Go to **Tools > Site Health** in your WordPress admin. The plugin adds two health checks: + +* **NodeInfo Well-Known Endpoint** - Verifies that `/.well-known/nodeinfo` is accessible +* **NodeInfo REST Endpoint** - Verifies that the NodeInfo 2.2 REST endpoint returns valid data + +If either check fails, you'll see recommendations on how to fix the issue. + ## Changelog Project and support maintained on github at [pfefferle/wordpress-nodeinfo](https://github.com/pfefferle/wordpress-nodeinfo). @@ -81,6 +90,7 @@ Project and support maintained on github at [pfefferle/wordpress-nodeinfo](https * Updated schemas to match official NodeInfo specifications with enums and constraints * Added `nodeinfo_protocols` filter for plugins to register protocols * Added `software.homepage` field for NodeInfo 2.1 and 2.2 +* Added Site Health checks to verify endpoints are accessible ### 2.3.1 diff --git a/includes/class-health-check.php b/includes/class-health-check.php new file mode 100644 index 0000000..9ba86d9 --- /dev/null +++ b/includes/class-health-check.php @@ -0,0 +1,224 @@ + \__( 'NodeInfo Well-Known Endpoint', 'nodeinfo' ), + 'test' => array( __CLASS__, 'test_wellknown_endpoint' ), + ); + + $tests['direct']['nodeinfo_endpoint'] = array( + 'label' => \__( 'NodeInfo REST Endpoint', 'nodeinfo' ), + 'test' => array( __CLASS__, 'test_nodeinfo_endpoint' ), + ); + + return $tests; + } + + /** + * Test if the .well-known/nodeinfo endpoint is accessible. + * + * @return array The test result. + */ + public static function test_wellknown_endpoint() { + $result = array( + 'label' => __( 'NodeInfo discovery is working', 'nodeinfo' ), + 'status' => 'good', + 'badge' => array( + 'label' => __( 'Fediverse', 'nodeinfo' ), + 'color' => 'green', + ), + 'description' => sprintf( + '

%s

', + __( 'The NodeInfo discovery endpoint is accessible and other Fediverse servers can find information about your site.', 'nodeinfo' ) + ), + 'actions' => '', + 'test' => 'nodeinfo_wellknown', + ); + + $url = home_url( '/.well-known/nodeinfo' ); + $response = wp_remote_get( + $url, + array( + 'timeout' => 10, + 'sslverify' => false, + ) + ); + + if ( is_wp_error( $response ) ) { + $result['status'] = 'critical'; + $result['label'] = __( 'NodeInfo discovery endpoint is not accessible', 'nodeinfo' ); + $result['description'] = sprintf( + '

%s

%s

', + __( 'The NodeInfo discovery endpoint could not be reached. Other Fediverse servers may not be able to discover your site.', 'nodeinfo' ), + sprintf( + /* translators: %s: Error message */ + __( 'Error: %s', 'nodeinfo' ), + $response->get_error_message() + ) + ); + $result['badge']['color'] = 'red'; + + return $result; + } + + $status_code = wp_remote_retrieve_response_code( $response ); + + if ( 200 !== $status_code ) { + $result['status'] = 'critical'; + $result['label'] = __( 'NodeInfo discovery endpoint returned an error', 'nodeinfo' ); + $result['description'] = sprintf( + '

%s

%s

', + __( 'The NodeInfo discovery endpoint returned an unexpected status code. This may indicate a server configuration issue.', 'nodeinfo' ), + sprintf( + /* translators: %d: HTTP status code */ + __( 'HTTP Status: %d', 'nodeinfo' ), + $status_code + ) + ); + $result['badge']['color'] = 'red'; + + return $result; + } + + $body = wp_remote_retrieve_body( $response ); + $data = json_decode( $body, true ); + + if ( empty( $data['links'] ) ) { + $result['status'] = 'recommended'; + $result['label'] = __( 'NodeInfo discovery returns incomplete data', 'nodeinfo' ); + $result['description'] = sprintf( + '

%s

', + __( 'The NodeInfo discovery endpoint is accessible but does not contain the expected links. This may indicate a plugin conflict or configuration issue.', 'nodeinfo' ) + ); + $result['badge']['color'] = 'orange'; + + return $result; + } + + $result['actions'] = sprintf( + '

%s

', + esc_url( $url ), + __( 'View NodeInfo discovery document', 'nodeinfo' ) + ); + + return $result; + } + + /** + * Test if a NodeInfo REST endpoint is accessible. + * + * @return array The test result. + */ + public static function test_nodeinfo_endpoint() { + $result = array( + 'label' => __( 'NodeInfo endpoint is working', 'nodeinfo' ), + 'status' => 'good', + 'badge' => array( + 'label' => __( 'Fediverse', 'nodeinfo' ), + 'color' => 'green', + ), + 'description' => sprintf( + '

%s

', + __( 'The NodeInfo REST endpoint returns valid data about your site.', 'nodeinfo' ) + ), + 'actions' => '', + 'test' => 'nodeinfo_endpoint', + ); + + // Test the latest version (2.2). + $url = get_rest_url( null, '/nodeinfo/2.2' ); + $response = wp_remote_get( + $url, + array( + 'timeout' => 10, + 'sslverify' => false, + ) + ); + + if ( is_wp_error( $response ) ) { + $result['status'] = 'critical'; + $result['label'] = __( 'NodeInfo REST endpoint is not accessible', 'nodeinfo' ); + $result['description'] = sprintf( + '

%s

%s

', + __( 'The NodeInfo REST endpoint could not be reached. This may indicate that the REST API is disabled or blocked.', 'nodeinfo' ), + sprintf( + /* translators: %s: Error message */ + __( 'Error: %s', 'nodeinfo' ), + $response->get_error_message() + ) + ); + $result['badge']['color'] = 'red'; + + return $result; + } + + $status_code = wp_remote_retrieve_response_code( $response ); + + if ( 200 !== $status_code ) { + $result['status'] = 'critical'; + $result['label'] = __( 'NodeInfo REST endpoint returned an error', 'nodeinfo' ); + $result['description'] = sprintf( + '

%s

%s

', + __( 'The NodeInfo REST endpoint returned an unexpected status code.', 'nodeinfo' ), + sprintf( + /* translators: %d: HTTP status code */ + __( 'HTTP Status: %d', 'nodeinfo' ), + $status_code + ) + ); + $result['badge']['color'] = 'red'; + + return $result; + } + + $body = wp_remote_retrieve_body( $response ); + $data = json_decode( $body, true ); + + if ( empty( $data['software']['name'] ) || empty( $data['version'] ) ) { + $result['status'] = 'recommended'; + $result['label'] = __( 'NodeInfo endpoint returns incomplete data', 'nodeinfo' ); + $result['description'] = sprintf( + '

%s

', + __( 'The NodeInfo endpoint is accessible but does not contain all expected fields.', 'nodeinfo' ) + ); + $result['badge']['color'] = 'orange'; + + return $result; + } + + $result['actions'] = sprintf( + '

%s

', + esc_url( $url ), + __( 'View NodeInfo 2.2 endpoint', 'nodeinfo' ) + ); + + return $result; + } +} diff --git a/nodeinfo.php b/nodeinfo.php index 5410a28..9900bf5 100644 --- a/nodeinfo.php +++ b/nodeinfo.php @@ -39,6 +39,9 @@ function nodeinfo_init() { Nodeinfo\Integration\Nodeinfo21::init(); Nodeinfo\Integration\Nodeinfo22::init(); + // Initialize Site Health checks. + Nodeinfo\Health_Check::init(); + // Register REST routes. add_action( 'rest_api_init', 'nodeinfo_register_routes' ); From 43aa84d68f44020a543486fe4ae0b7ccf43b43ff Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 8 Dec 2025 11:30:57 +0100 Subject: [PATCH 16/21] Prefix WordPress core functions with backslash All WordPress core functions and classes are now called with a leading backslash to ensure global namespace resolution and avoid conflicts with custom functions. Site Health checks initialization was moved to an admin-only hook for better separation of concerns. --- includes/class-health-check.php | 104 +++++++++++----------- includes/controller/class-nodeinfo.php | 44 +++++---- includes/controller/class-nodeinfo2.php | 43 +++++---- includes/functions.php | 18 ++-- includes/integration/class-nodeinfo10.php | 34 +++---- includes/integration/class-nodeinfo11.php | 34 +++---- includes/integration/class-nodeinfo20.php | 36 ++++---- includes/integration/class-nodeinfo21.php | 36 ++++---- includes/integration/class-nodeinfo22.php | 42 ++++----- nodeinfo.php | 12 ++- 10 files changed, 201 insertions(+), 202 deletions(-) diff --git a/includes/class-health-check.php b/includes/class-health-check.php index 9ba86d9..643c2f3 100644 --- a/includes/class-health-check.php +++ b/includes/class-health-check.php @@ -48,22 +48,22 @@ public static function register_tests( $tests ) { */ public static function test_wellknown_endpoint() { $result = array( - 'label' => __( 'NodeInfo discovery is working', 'nodeinfo' ), + 'label' => \__( 'NodeInfo discovery is working', 'nodeinfo' ), 'status' => 'good', 'badge' => array( - 'label' => __( 'Fediverse', 'nodeinfo' ), + 'label' => \__( 'Fediverse', 'nodeinfo' ), 'color' => 'green', ), - 'description' => sprintf( + 'description' => \sprintf( '

%s

', - __( 'The NodeInfo discovery endpoint is accessible and other Fediverse servers can find information about your site.', 'nodeinfo' ) + \__( 'The NodeInfo discovery endpoint is accessible and other Fediverse servers can find information about your site.', 'nodeinfo' ) ), 'actions' => '', 'test' => 'nodeinfo_wellknown', ); - $url = home_url( '/.well-known/nodeinfo' ); - $response = wp_remote_get( + $url = \home_url( '/.well-known/nodeinfo' ); + $response = \wp_remote_get( $url, array( 'timeout' => 10, @@ -71,15 +71,15 @@ public static function test_wellknown_endpoint() { ) ); - if ( is_wp_error( $response ) ) { + if ( \is_wp_error( $response ) ) { $result['status'] = 'critical'; - $result['label'] = __( 'NodeInfo discovery endpoint is not accessible', 'nodeinfo' ); - $result['description'] = sprintf( + $result['label'] = \__( 'NodeInfo discovery endpoint is not accessible', 'nodeinfo' ); + $result['description'] = \sprintf( '

%s

%s

', - __( 'The NodeInfo discovery endpoint could not be reached. Other Fediverse servers may not be able to discover your site.', 'nodeinfo' ), - sprintf( + \__( 'The NodeInfo discovery endpoint could not be reached. Other Fediverse servers may not be able to discover your site.', 'nodeinfo' ), + \sprintf( /* translators: %s: Error message */ - __( 'Error: %s', 'nodeinfo' ), + \__( 'Error: %s', 'nodeinfo' ), $response->get_error_message() ) ); @@ -88,17 +88,17 @@ public static function test_wellknown_endpoint() { return $result; } - $status_code = wp_remote_retrieve_response_code( $response ); + $status_code = \wp_remote_retrieve_response_code( $response ); if ( 200 !== $status_code ) { $result['status'] = 'critical'; - $result['label'] = __( 'NodeInfo discovery endpoint returned an error', 'nodeinfo' ); - $result['description'] = sprintf( + $result['label'] = \__( 'NodeInfo discovery endpoint returned an error', 'nodeinfo' ); + $result['description'] = \sprintf( '

%s

%s

', - __( 'The NodeInfo discovery endpoint returned an unexpected status code. This may indicate a server configuration issue.', 'nodeinfo' ), - sprintf( + \__( 'The NodeInfo discovery endpoint returned an unexpected status code. This may indicate a server configuration issue.', 'nodeinfo' ), + \sprintf( /* translators: %d: HTTP status code */ - __( 'HTTP Status: %d', 'nodeinfo' ), + \__( 'HTTP Status: %d', 'nodeinfo' ), $status_code ) ); @@ -107,25 +107,25 @@ public static function test_wellknown_endpoint() { return $result; } - $body = wp_remote_retrieve_body( $response ); - $data = json_decode( $body, true ); + $body = \wp_remote_retrieve_body( $response ); + $data = \json_decode( $body, true ); if ( empty( $data['links'] ) ) { $result['status'] = 'recommended'; - $result['label'] = __( 'NodeInfo discovery returns incomplete data', 'nodeinfo' ); - $result['description'] = sprintf( + $result['label'] = \__( 'NodeInfo discovery returns incomplete data', 'nodeinfo' ); + $result['description'] = \sprintf( '

%s

', - __( 'The NodeInfo discovery endpoint is accessible but does not contain the expected links. This may indicate a plugin conflict or configuration issue.', 'nodeinfo' ) + \__( 'The NodeInfo discovery endpoint is accessible but does not contain the expected links. This may indicate a plugin conflict or configuration issue.', 'nodeinfo' ) ); $result['badge']['color'] = 'orange'; return $result; } - $result['actions'] = sprintf( + $result['actions'] = \sprintf( '

%s

', - esc_url( $url ), - __( 'View NodeInfo discovery document', 'nodeinfo' ) + \esc_url( $url ), + \__( 'View NodeInfo discovery document', 'nodeinfo' ) ); return $result; @@ -138,23 +138,23 @@ public static function test_wellknown_endpoint() { */ public static function test_nodeinfo_endpoint() { $result = array( - 'label' => __( 'NodeInfo endpoint is working', 'nodeinfo' ), + 'label' => \__( 'NodeInfo endpoint is working', 'nodeinfo' ), 'status' => 'good', 'badge' => array( - 'label' => __( 'Fediverse', 'nodeinfo' ), + 'label' => \__( 'Fediverse', 'nodeinfo' ), 'color' => 'green', ), - 'description' => sprintf( + 'description' => \sprintf( '

%s

', - __( 'The NodeInfo REST endpoint returns valid data about your site.', 'nodeinfo' ) + \__( 'The NodeInfo REST endpoint returns valid data about your site.', 'nodeinfo' ) ), 'actions' => '', 'test' => 'nodeinfo_endpoint', ); // Test the latest version (2.2). - $url = get_rest_url( null, '/nodeinfo/2.2' ); - $response = wp_remote_get( + $url = \get_rest_url( null, '/nodeinfo/2.2' ); + $response = \wp_remote_get( $url, array( 'timeout' => 10, @@ -162,15 +162,15 @@ public static function test_nodeinfo_endpoint() { ) ); - if ( is_wp_error( $response ) ) { + if ( \is_wp_error( $response ) ) { $result['status'] = 'critical'; - $result['label'] = __( 'NodeInfo REST endpoint is not accessible', 'nodeinfo' ); - $result['description'] = sprintf( + $result['label'] = \__( 'NodeInfo REST endpoint is not accessible', 'nodeinfo' ); + $result['description'] = \sprintf( '

%s

%s

', - __( 'The NodeInfo REST endpoint could not be reached. This may indicate that the REST API is disabled or blocked.', 'nodeinfo' ), - sprintf( + \__( 'The NodeInfo REST endpoint could not be reached. This may indicate that the REST API is disabled or blocked.', 'nodeinfo' ), + \sprintf( /* translators: %s: Error message */ - __( 'Error: %s', 'nodeinfo' ), + \__( 'Error: %s', 'nodeinfo' ), $response->get_error_message() ) ); @@ -179,17 +179,17 @@ public static function test_nodeinfo_endpoint() { return $result; } - $status_code = wp_remote_retrieve_response_code( $response ); + $status_code = \wp_remote_retrieve_response_code( $response ); if ( 200 !== $status_code ) { $result['status'] = 'critical'; - $result['label'] = __( 'NodeInfo REST endpoint returned an error', 'nodeinfo' ); - $result['description'] = sprintf( + $result['label'] = \__( 'NodeInfo REST endpoint returned an error', 'nodeinfo' ); + $result['description'] = \sprintf( '

%s

%s

', - __( 'The NodeInfo REST endpoint returned an unexpected status code.', 'nodeinfo' ), - sprintf( + \__( 'The NodeInfo REST endpoint returned an unexpected status code.', 'nodeinfo' ), + \sprintf( /* translators: %d: HTTP status code */ - __( 'HTTP Status: %d', 'nodeinfo' ), + \__( 'HTTP Status: %d', 'nodeinfo' ), $status_code ) ); @@ -198,25 +198,25 @@ public static function test_nodeinfo_endpoint() { return $result; } - $body = wp_remote_retrieve_body( $response ); - $data = json_decode( $body, true ); + $body = \wp_remote_retrieve_body( $response ); + $data = \json_decode( $body, true ); if ( empty( $data['software']['name'] ) || empty( $data['version'] ) ) { $result['status'] = 'recommended'; - $result['label'] = __( 'NodeInfo endpoint returns incomplete data', 'nodeinfo' ); - $result['description'] = sprintf( + $result['label'] = \__( 'NodeInfo endpoint returns incomplete data', 'nodeinfo' ); + $result['description'] = \sprintf( '

%s

', - __( 'The NodeInfo endpoint is accessible but does not contain all expected fields.', 'nodeinfo' ) + \__( 'The NodeInfo endpoint is accessible but does not contain all expected fields.', 'nodeinfo' ) ); $result['badge']['color'] = 'orange'; return $result; } - $result['actions'] = sprintf( + $result['actions'] = \sprintf( '

%s

', - esc_url( $url ), - __( 'View NodeInfo 2.2 endpoint', 'nodeinfo' ) + \esc_url( $url ), + \__( 'View NodeInfo 2.2 endpoint', 'nodeinfo' ) ); return $result; diff --git a/includes/controller/class-nodeinfo.php b/includes/controller/class-nodeinfo.php index 694e2f4..a106481 100644 --- a/includes/controller/class-nodeinfo.php +++ b/includes/controller/class-nodeinfo.php @@ -7,17 +7,13 @@ namespace Nodeinfo\Controller; -use WP_REST_Controller; -use WP_REST_Server; -use WP_REST_Response; - /** * NodeInfo REST Controller class. * * Handles NodeInfo discovery and versioned endpoints. * Versions are registered dynamically via filters. */ -class Nodeinfo extends WP_REST_Controller { +class Nodeinfo extends \WP_REST_Controller { /** * The namespace. @@ -30,12 +26,12 @@ class Nodeinfo extends WP_REST_Controller { * Register the routes. */ public function register_routes() { - register_rest_route( + \register_rest_route( $this->namespace, '/discovery', array( array( - 'methods' => WP_REST_Server::READABLE, + 'methods' => \WP_REST_Server::READABLE, 'callback' => array( $this, 'get_discovery' ), 'permission_callback' => '__return_true', ), @@ -48,7 +44,7 @@ public function register_routes() { return; } - register_rest_route( + \register_rest_route( $this->namespace, '/(?P\d\.\d)', array( @@ -61,7 +57,7 @@ public function register_routes() { ), ), array( - 'methods' => WP_REST_Server::READABLE, + 'methods' => \WP_REST_Server::READABLE, 'callback' => array( $this, 'get_item' ), 'permission_callback' => '__return_true', ), @@ -81,7 +77,7 @@ protected function get_versions() { * * @param array $versions List of version strings (e.g., '2.0', '2.1'). */ - return apply_filters( 'nodeinfo_versions', array() ); + return \apply_filters( 'nodeinfo_versions', array() ); } /** @@ -97,7 +93,7 @@ public function get_discovery() { * * @param array $links The discovery links. */ - $links = apply_filters( 'nodeinfo_discovery_links', $links ); + $links = \apply_filters( 'nodeinfo_discovery_links', $links ); $discovery = array( 'links' => $links ); @@ -106,9 +102,9 @@ public function get_discovery() { * * @param array $discovery The discovery document. */ - $discovery = apply_filters( 'nodeinfo_discovery', $discovery ); + $discovery = \apply_filters( 'nodeinfo_discovery', $discovery ); - $response = new WP_REST_Response( $discovery ); + $response = new \WP_REST_Response( $discovery ); $response->header( 'Content-Type', 'application/json; profile=http://nodeinfo.diaspora.software' ); return $response; @@ -125,9 +121,9 @@ public function get_item( $request ) { $nodeinfo = array( 'version' => $version, - 'software' => apply_filters( 'nodeinfo_data_software', array(), $version ), - 'protocols' => apply_filters( 'nodeinfo_data_protocols', array(), $version ), - 'services' => apply_filters( + 'software' => \apply_filters( 'nodeinfo_data_software', array(), $version ), + 'protocols' => \apply_filters( 'nodeinfo_data_protocols', array(), $version ), + 'services' => \apply_filters( 'nodeinfo_data_services', array( 'inbound' => array(), @@ -135,9 +131,9 @@ public function get_item( $request ) { ), $version ), - 'openRegistrations' => (bool) get_option( 'users_can_register', false ), - 'usage' => apply_filters( 'nodeinfo_data_usage', array(), $version ), - 'metadata' => apply_filters( 'nodeinfo_data_metadata', array(), $version ), + 'openRegistrations' => (bool) \get_option( 'users_can_register', false ), + 'usage' => \apply_filters( 'nodeinfo_data_usage', array(), $version ), + 'metadata' => \apply_filters( 'nodeinfo_data_metadata', array(), $version ), ); /** @@ -146,9 +142,9 @@ public function get_item( $request ) { * @param array $nodeinfo The NodeInfo data. * @param string $version The NodeInfo version. */ - $nodeinfo = apply_filters( 'nodeinfo_data', $nodeinfo, $version ); + $nodeinfo = \apply_filters( 'nodeinfo_data', $nodeinfo, $version ); - return new WP_REST_Response( $nodeinfo ); + return new \WP_REST_Response( $nodeinfo ); } /** @@ -169,7 +165,7 @@ public function get_item_schema() { * * @param array $schema The schema data. */ - return apply_filters( 'nodeinfo_schema', $schema ); + return \apply_filters( 'nodeinfo_schema', $schema ); } /** @@ -191,9 +187,9 @@ public static function jrd( $jrd ) { * * @param array $links The discovery links. */ - $links = apply_filters( 'nodeinfo_discovery_links', array() ); + $links = \apply_filters( 'nodeinfo_discovery_links', array() ); - $jrd['links'] = array_merge( $jrd['links'], $links ); + $jrd['links'] = \array_merge( $jrd['links'], $links ); return $jrd; } diff --git a/includes/controller/class-nodeinfo2.php b/includes/controller/class-nodeinfo2.php index aecfd0c..de49a28 100644 --- a/includes/controller/class-nodeinfo2.php +++ b/includes/controller/class-nodeinfo2.php @@ -8,9 +8,6 @@ namespace Nodeinfo\Controller; -use WP_REST_Controller; -use WP_REST_Server; -use WP_REST_Response; use function Nodeinfo\get_active_users; use function Nodeinfo\get_masked_version; @@ -19,7 +16,7 @@ * * Handles NodeInfo2 endpoints (version 1.0). */ -class Nodeinfo2 extends WP_REST_Controller { +class Nodeinfo2 extends \WP_REST_Controller { /** * The namespace. @@ -32,7 +29,7 @@ class Nodeinfo2 extends WP_REST_Controller { * Register the routes. */ public function register_routes() { - register_rest_route( + \register_rest_route( $this->namespace, '/(?P\d\.\d)', array( @@ -45,7 +42,7 @@ public function register_routes() { ), ), array( - 'methods' => WP_REST_Server::READABLE, + 'methods' => \WP_REST_Server::READABLE, 'callback' => array( $this, 'get_item' ), 'permission_callback' => '__return_true', ), @@ -63,31 +60,31 @@ public function register_routes() { public function get_item( $request ) { $version = $request->get_param( 'version' ); - $users = get_users( + $users = \get_users( array( 'capability__in' => array( 'publish_posts' ), ) ); - $user_count = is_array( $users ) ? count( $users ) : 1; + $user_count = \is_array( $users ) ? \count( $users ) : 1; - $posts = wp_count_posts(); - $comments = wp_count_comments(); + $posts = \wp_count_posts(); + $comments = \wp_count_comments(); $nodeinfo2 = array( 'version' => $version, - 'server' => apply_filters( + 'server' => \apply_filters( 'nodeinfo2_data_server', array( - 'baseUrl' => home_url( '/' ), - 'name' => get_bloginfo( 'name' ), + 'baseUrl' => \home_url( '/' ), + 'name' => \get_bloginfo( 'name' ), 'software' => 'wordpress', 'version' => get_masked_version(), ), $version ), - 'protocols' => apply_filters( 'nodeinfo2_data_protocols', array(), $version ), - 'services' => apply_filters( + 'protocols' => \apply_filters( 'nodeinfo2_data_protocols', array(), $version ), + 'services' => \apply_filters( 'nodeinfo2_data_services', array( 'inbound' => array( 'atom1.0', 'rss2.0', 'pop3' ), @@ -95,8 +92,8 @@ public function get_item( $request ) { ), $version ), - 'openRegistrations' => (bool) get_option( 'users_can_register', false ), - 'usage' => apply_filters( + 'openRegistrations' => (bool) \get_option( 'users_can_register', false ), + 'usage' => \apply_filters( 'nodeinfo2_data_usage', array( 'users' => array( @@ -109,12 +106,12 @@ public function get_item( $request ) { ), $version ), - 'metadata' => apply_filters( + 'metadata' => \apply_filters( 'nodeinfo2_data_metadata', array( - 'nodeName' => get_bloginfo( 'name' ), - 'nodeDescription' => get_bloginfo( 'description' ), - 'nodeIcon' => get_site_icon_url(), + 'nodeName' => \get_bloginfo( 'name' ), + 'nodeDescription' => \get_bloginfo( 'description' ), + 'nodeIcon' => \get_site_icon_url(), ), $version ), @@ -126,9 +123,9 @@ public function get_item( $request ) { * @param array $nodeinfo2 The NodeInfo2 data. * @param string $version The NodeInfo2 version. */ - $nodeinfo2 = apply_filters( 'nodeinfo2_data', $nodeinfo2, $version ); + $nodeinfo2 = \apply_filters( 'nodeinfo2_data', $nodeinfo2, $version ); - return new WP_REST_Response( $nodeinfo2 ); + return new \WP_REST_Response( $nodeinfo2 ); } /** diff --git a/includes/functions.php b/includes/functions.php index 7a585df..e17a1eb 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -14,7 +14,7 @@ * @return int The number of active users. */ function get_active_users( $duration = '1 month ago' ) { - $posts = get_posts( + $posts = \get_posts( array( 'post_type' => 'post', 'post_status' => 'publish', @@ -33,9 +33,9 @@ function get_active_users( $duration = '1 month ago' ) { return 0; } - return count( - array_unique( - wp_list_pluck( + return \count( + \array_unique( + \wp_list_pluck( $posts, 'post_author' ) @@ -49,11 +49,11 @@ function get_active_users( $duration = '1 month ago' ) { * @return string The masked version. */ function get_masked_version() { - $version = get_bloginfo( 'version' ); + $version = \get_bloginfo( 'version' ); // Strip RC/beta suffixes. - $version = preg_replace( '/-.*$/', '', $version ); - $version = explode( '.', $version ); - $version = array_slice( $version, 0, 2 ); + $version = \preg_replace( '/-.*$/', '', $version ); + $version = \explode( '.', $version ); + $version = \array_slice( $version, 0, 2 ); - return implode( '.', $version ); + return \implode( '.', $version ); } diff --git a/includes/integration/class-nodeinfo10.php b/includes/integration/class-nodeinfo10.php index 44f72aa..bb44d30 100644 --- a/includes/integration/class-nodeinfo10.php +++ b/includes/integration/class-nodeinfo10.php @@ -25,14 +25,14 @@ class Nodeinfo10 { * Initialize the integration. */ public static function init() { - add_filter( 'nodeinfo_versions', array( __CLASS__, 'register_version' ) ); - add_filter( 'nodeinfo_discovery_links', array( __CLASS__, 'discovery_link' ) ); - add_filter( 'nodeinfo_schema', array( __CLASS__, 'schema' ) ); - add_filter( 'nodeinfo_data_software', array( __CLASS__, 'software' ), 10, 2 ); - add_filter( 'nodeinfo_data_protocols', array( __CLASS__, 'protocols' ), 10, 2 ); - add_filter( 'nodeinfo_data_services', array( __CLASS__, 'services' ), 10, 2 ); - add_filter( 'nodeinfo_data_usage', array( __CLASS__, 'usage' ), 10, 2 ); - add_filter( 'nodeinfo_data_metadata', array( __CLASS__, 'metadata' ), 10, 2 ); + \add_filter( 'nodeinfo_versions', array( __CLASS__, 'register_version' ) ); + \add_filter( 'nodeinfo_discovery_links', array( __CLASS__, 'discovery_link' ) ); + \add_filter( 'nodeinfo_schema', array( __CLASS__, 'schema' ) ); + \add_filter( 'nodeinfo_data_software', array( __CLASS__, 'software' ), 10, 2 ); + \add_filter( 'nodeinfo_data_protocols', array( __CLASS__, 'protocols' ), 10, 2 ); + \add_filter( 'nodeinfo_data_services', array( __CLASS__, 'services' ), 10, 2 ); + \add_filter( 'nodeinfo_data_usage', array( __CLASS__, 'usage' ), 10, 2 ); + \add_filter( 'nodeinfo_data_metadata', array( __CLASS__, 'metadata' ), 10, 2 ); } /** @@ -55,7 +55,7 @@ public static function register_version( $versions ) { public static function discovery_link( $links ) { $links[] = array( 'rel' => 'http://nodeinfo.diaspora.software/ns/schema/' . self::VERSION, - 'href' => get_rest_url( null, '/nodeinfo/' . self::VERSION ), + 'href' => \get_rest_url( null, '/nodeinfo/' . self::VERSION ), ); return $links; } @@ -69,7 +69,7 @@ public static function discovery_link( $links ) { * @return array The modified schema. */ public static function schema( $schema ) { - $schema['properties'] = array_merge( + $schema['properties'] = \array_merge( $schema['properties'], array( 'version' => array( @@ -240,17 +240,17 @@ public static function usage( $usage, $version ) { return $usage; } - $users = get_users( + $users = \get_users( array( 'fields' => 'ID', 'capability__in' => array( 'publish_posts' ), ) ); - $user_count = is_array( $users ) ? count( $users ) : 1; + $user_count = \is_array( $users ) ? \count( $users ) : 1; - $posts = wp_count_posts(); - $comments = wp_count_comments(); + $posts = \wp_count_posts(); + $comments = \wp_count_comments(); $usage['users'] = array( 'total' => $user_count, @@ -276,9 +276,9 @@ public static function metadata( $metadata, $version ) { return $metadata; } - $metadata['nodeName'] = get_bloginfo( 'name' ); - $metadata['nodeDescription'] = get_bloginfo( 'description' ); - $metadata['nodeIcon'] = get_site_icon_url(); + $metadata['nodeName'] = \get_bloginfo( 'name' ); + $metadata['nodeDescription'] = \get_bloginfo( 'description' ); + $metadata['nodeIcon'] = \get_site_icon_url(); return $metadata; } diff --git a/includes/integration/class-nodeinfo11.php b/includes/integration/class-nodeinfo11.php index 8434877..aa7b3b1 100644 --- a/includes/integration/class-nodeinfo11.php +++ b/includes/integration/class-nodeinfo11.php @@ -25,14 +25,14 @@ class Nodeinfo11 { * Initialize the integration. */ public static function init() { - add_filter( 'nodeinfo_versions', array( __CLASS__, 'register_version' ) ); - add_filter( 'nodeinfo_discovery_links', array( __CLASS__, 'discovery_link' ) ); - add_filter( 'nodeinfo_schema', array( __CLASS__, 'schema' ) ); - add_filter( 'nodeinfo_data_software', array( __CLASS__, 'software' ), 10, 2 ); - add_filter( 'nodeinfo_data_protocols', array( __CLASS__, 'protocols' ), 10, 2 ); - add_filter( 'nodeinfo_data_services', array( __CLASS__, 'services' ), 10, 2 ); - add_filter( 'nodeinfo_data_usage', array( __CLASS__, 'usage' ), 10, 2 ); - add_filter( 'nodeinfo_data_metadata', array( __CLASS__, 'metadata' ), 10, 2 ); + \add_filter( 'nodeinfo_versions', array( __CLASS__, 'register_version' ) ); + \add_filter( 'nodeinfo_discovery_links', array( __CLASS__, 'discovery_link' ) ); + \add_filter( 'nodeinfo_schema', array( __CLASS__, 'schema' ) ); + \add_filter( 'nodeinfo_data_software', array( __CLASS__, 'software' ), 10, 2 ); + \add_filter( 'nodeinfo_data_protocols', array( __CLASS__, 'protocols' ), 10, 2 ); + \add_filter( 'nodeinfo_data_services', array( __CLASS__, 'services' ), 10, 2 ); + \add_filter( 'nodeinfo_data_usage', array( __CLASS__, 'usage' ), 10, 2 ); + \add_filter( 'nodeinfo_data_metadata', array( __CLASS__, 'metadata' ), 10, 2 ); } /** @@ -55,7 +55,7 @@ public static function register_version( $versions ) { public static function discovery_link( $links ) { $links[] = array( 'rel' => 'http://nodeinfo.diaspora.software/ns/schema/' . self::VERSION, - 'href' => get_rest_url( null, '/nodeinfo/' . self::VERSION ), + 'href' => \get_rest_url( null, '/nodeinfo/' . self::VERSION ), ); return $links; } @@ -70,7 +70,7 @@ public static function discovery_link( $links ) { */ public static function schema( $schema ) { // NodeInfo 1.1 schema - adds hubzilla to software enum and zot to protocols. - $schema['properties'] = array_merge( + $schema['properties'] = \array_merge( $schema['properties'], array( 'version' => array( @@ -241,17 +241,17 @@ public static function usage( $usage, $version ) { return $usage; } - $users = get_users( + $users = \get_users( array( 'fields' => 'ID', 'capability__in' => array( 'publish_posts' ), ) ); - $user_count = is_array( $users ) ? count( $users ) : 1; + $user_count = \is_array( $users ) ? \count( $users ) : 1; - $posts = wp_count_posts(); - $comments = wp_count_comments(); + $posts = \wp_count_posts(); + $comments = \wp_count_comments(); $usage['users'] = array( 'total' => $user_count, @@ -277,9 +277,9 @@ public static function metadata( $metadata, $version ) { return $metadata; } - $metadata['nodeName'] = get_bloginfo( 'name' ); - $metadata['nodeDescription'] = get_bloginfo( 'description' ); - $metadata['nodeIcon'] = get_site_icon_url(); + $metadata['nodeName'] = \get_bloginfo( 'name' ); + $metadata['nodeDescription'] = \get_bloginfo( 'description' ); + $metadata['nodeIcon'] = \get_site_icon_url(); return $metadata; } diff --git a/includes/integration/class-nodeinfo20.php b/includes/integration/class-nodeinfo20.php index ca2f51d..22fd47a 100644 --- a/includes/integration/class-nodeinfo20.php +++ b/includes/integration/class-nodeinfo20.php @@ -25,14 +25,14 @@ class Nodeinfo20 { * Initialize the integration. */ public static function init() { - add_filter( 'nodeinfo_versions', array( __CLASS__, 'register_version' ) ); - add_filter( 'nodeinfo_discovery_links', array( __CLASS__, 'discovery_link' ) ); - add_filter( 'nodeinfo_schema', array( __CLASS__, 'schema' ) ); - add_filter( 'nodeinfo_data_software', array( __CLASS__, 'software' ), 10, 2 ); - add_filter( 'nodeinfo_data_protocols', array( __CLASS__, 'protocols' ), 10, 2 ); - add_filter( 'nodeinfo_data_services', array( __CLASS__, 'services' ), 10, 2 ); - add_filter( 'nodeinfo_data_usage', array( __CLASS__, 'usage' ), 10, 2 ); - add_filter( 'nodeinfo_data_metadata', array( __CLASS__, 'metadata' ), 10, 2 ); + \add_filter( 'nodeinfo_versions', array( __CLASS__, 'register_version' ) ); + \add_filter( 'nodeinfo_discovery_links', array( __CLASS__, 'discovery_link' ) ); + \add_filter( 'nodeinfo_schema', array( __CLASS__, 'schema' ) ); + \add_filter( 'nodeinfo_data_software', array( __CLASS__, 'software' ), 10, 2 ); + \add_filter( 'nodeinfo_data_protocols', array( __CLASS__, 'protocols' ), 10, 2 ); + \add_filter( 'nodeinfo_data_services', array( __CLASS__, 'services' ), 10, 2 ); + \add_filter( 'nodeinfo_data_usage', array( __CLASS__, 'usage' ), 10, 2 ); + \add_filter( 'nodeinfo_data_metadata', array( __CLASS__, 'metadata' ), 10, 2 ); } /** @@ -55,7 +55,7 @@ public static function register_version( $versions ) { public static function discovery_link( $links ) { $links[] = array( 'rel' => 'http://nodeinfo.diaspora.software/ns/schema/' . self::VERSION, - 'href' => get_rest_url( null, '/nodeinfo/' . self::VERSION ), + 'href' => \get_rest_url( null, '/nodeinfo/' . self::VERSION ), ); return $links; } @@ -70,7 +70,7 @@ public static function discovery_link( $links ) { */ public static function schema( $schema ) { // NodeInfo 2.0 schema - protocols is a flat array, software name is pattern-based. - $schema['properties'] = array_merge( + $schema['properties'] = \array_merge( $schema['properties'], array( 'version' => array( @@ -196,7 +196,7 @@ public static function protocols( $protocols, $version ) { } // Default protocols - can be extended via filter. - return apply_filters( 'nodeinfo_protocols', array() ); + return \apply_filters( 'nodeinfo_protocols', array() ); } /** @@ -229,17 +229,17 @@ public static function usage( $usage, $version ) { return $usage; } - $users = get_users( + $users = \get_users( array( 'fields' => 'ID', 'capability__in' => array( 'publish_posts' ), ) ); - $user_count = is_array( $users ) ? count( $users ) : 1; + $user_count = \is_array( $users ) ? \count( $users ) : 1; - $posts = wp_count_posts(); - $comments = wp_count_comments(); + $posts = \wp_count_posts(); + $comments = \wp_count_comments(); $usage['users'] = array( 'total' => $user_count, @@ -265,9 +265,9 @@ public static function metadata( $metadata, $version ) { return $metadata; } - $metadata['nodeName'] = get_bloginfo( 'name' ); - $metadata['nodeDescription'] = get_bloginfo( 'description' ); - $metadata['nodeIcon'] = get_site_icon_url(); + $metadata['nodeName'] = \get_bloginfo( 'name' ); + $metadata['nodeDescription'] = \get_bloginfo( 'description' ); + $metadata['nodeIcon'] = \get_site_icon_url(); return $metadata; } diff --git a/includes/integration/class-nodeinfo21.php b/includes/integration/class-nodeinfo21.php index 988ecdd..c108471 100644 --- a/includes/integration/class-nodeinfo21.php +++ b/includes/integration/class-nodeinfo21.php @@ -25,14 +25,14 @@ class Nodeinfo21 { * Initialize the integration. */ public static function init() { - add_filter( 'nodeinfo_versions', array( __CLASS__, 'register_version' ) ); - add_filter( 'nodeinfo_discovery_links', array( __CLASS__, 'discovery_link' ) ); - add_filter( 'nodeinfo_schema', array( __CLASS__, 'schema' ) ); - add_filter( 'nodeinfo_data_software', array( __CLASS__, 'software' ), 10, 2 ); - add_filter( 'nodeinfo_data_protocols', array( __CLASS__, 'protocols' ), 10, 2 ); - add_filter( 'nodeinfo_data_services', array( __CLASS__, 'services' ), 10, 2 ); - add_filter( 'nodeinfo_data_usage', array( __CLASS__, 'usage' ), 10, 2 ); - add_filter( 'nodeinfo_data_metadata', array( __CLASS__, 'metadata' ), 10, 2 ); + \add_filter( 'nodeinfo_versions', array( __CLASS__, 'register_version' ) ); + \add_filter( 'nodeinfo_discovery_links', array( __CLASS__, 'discovery_link' ) ); + \add_filter( 'nodeinfo_schema', array( __CLASS__, 'schema' ) ); + \add_filter( 'nodeinfo_data_software', array( __CLASS__, 'software' ), 10, 2 ); + \add_filter( 'nodeinfo_data_protocols', array( __CLASS__, 'protocols' ), 10, 2 ); + \add_filter( 'nodeinfo_data_services', array( __CLASS__, 'services' ), 10, 2 ); + \add_filter( 'nodeinfo_data_usage', array( __CLASS__, 'usage' ), 10, 2 ); + \add_filter( 'nodeinfo_data_metadata', array( __CLASS__, 'metadata' ), 10, 2 ); } /** @@ -55,7 +55,7 @@ public static function register_version( $versions ) { public static function discovery_link( $links ) { $links[] = array( 'rel' => 'http://nodeinfo.diaspora.software/ns/schema/' . self::VERSION, - 'href' => get_rest_url( null, '/nodeinfo/' . self::VERSION ), + 'href' => \get_rest_url( null, '/nodeinfo/' . self::VERSION ), ); return $links; } @@ -70,7 +70,7 @@ public static function discovery_link( $links ) { */ public static function schema( $schema ) { // NodeInfo 2.1 schema - adds repository and homepage to software. - $schema['properties'] = array_merge( + $schema['properties'] = \array_merge( $schema['properties'], array( 'version' => array( @@ -206,7 +206,7 @@ public static function protocols( $protocols, $version ) { } // Default protocols - can be extended via filter. - return apply_filters( 'nodeinfo_protocols', array() ); + return \apply_filters( 'nodeinfo_protocols', array() ); } /** @@ -239,17 +239,17 @@ public static function usage( $usage, $version ) { return $usage; } - $users = get_users( + $users = \get_users( array( 'fields' => 'ID', 'capability__in' => array( 'publish_posts' ), ) ); - $user_count = is_array( $users ) ? count( $users ) : 1; + $user_count = \is_array( $users ) ? \count( $users ) : 1; - $posts = wp_count_posts(); - $comments = wp_count_comments(); + $posts = \wp_count_posts(); + $comments = \wp_count_comments(); $usage['users'] = array( 'total' => $user_count, @@ -275,9 +275,9 @@ public static function metadata( $metadata, $version ) { return $metadata; } - $metadata['nodeName'] = get_bloginfo( 'name' ); - $metadata['nodeDescription'] = get_bloginfo( 'description' ); - $metadata['nodeIcon'] = get_site_icon_url(); + $metadata['nodeName'] = \get_bloginfo( 'name' ); + $metadata['nodeDescription'] = \get_bloginfo( 'description' ); + $metadata['nodeIcon'] = \get_site_icon_url(); return $metadata; } diff --git a/includes/integration/class-nodeinfo22.php b/includes/integration/class-nodeinfo22.php index 955beda..6ad699e 100644 --- a/includes/integration/class-nodeinfo22.php +++ b/includes/integration/class-nodeinfo22.php @@ -25,15 +25,15 @@ class Nodeinfo22 { * Initialize the integration. */ public static function init() { - add_filter( 'nodeinfo_versions', array( __CLASS__, 'register_version' ) ); - add_filter( 'nodeinfo_discovery_links', array( __CLASS__, 'discovery_link' ) ); - add_filter( 'nodeinfo_schema', array( __CLASS__, 'schema' ) ); - add_filter( 'nodeinfo_data_software', array( __CLASS__, 'software' ), 10, 2 ); - add_filter( 'nodeinfo_data_protocols', array( __CLASS__, 'protocols' ), 10, 2 ); - add_filter( 'nodeinfo_data_services', array( __CLASS__, 'services' ), 10, 2 ); - add_filter( 'nodeinfo_data_usage', array( __CLASS__, 'usage' ), 10, 2 ); - add_filter( 'nodeinfo_data_metadata', array( __CLASS__, 'metadata' ), 10, 2 ); - add_filter( 'nodeinfo_data', array( __CLASS__, 'add_instance' ), 10, 2 ); + \add_filter( 'nodeinfo_versions', array( __CLASS__, 'register_version' ) ); + \add_filter( 'nodeinfo_discovery_links', array( __CLASS__, 'discovery_link' ) ); + \add_filter( 'nodeinfo_schema', array( __CLASS__, 'schema' ) ); + \add_filter( 'nodeinfo_data_software', array( __CLASS__, 'software' ), 10, 2 ); + \add_filter( 'nodeinfo_data_protocols', array( __CLASS__, 'protocols' ), 10, 2 ); + \add_filter( 'nodeinfo_data_services', array( __CLASS__, 'services' ), 10, 2 ); + \add_filter( 'nodeinfo_data_usage', array( __CLASS__, 'usage' ), 10, 2 ); + \add_filter( 'nodeinfo_data_metadata', array( __CLASS__, 'metadata' ), 10, 2 ); + \add_filter( 'nodeinfo_data', array( __CLASS__, 'add_instance' ), 10, 2 ); } /** @@ -56,7 +56,7 @@ public static function register_version( $versions ) { public static function discovery_link( $links ) { $links[] = array( 'rel' => 'http://nodeinfo.diaspora.software/ns/schema/' . self::VERSION, - 'href' => get_rest_url( null, '/nodeinfo/' . self::VERSION ), + 'href' => \get_rest_url( null, '/nodeinfo/' . self::VERSION ), ); return $links; } @@ -71,7 +71,7 @@ public static function discovery_link( $links ) { */ public static function schema( $schema ) { // NodeInfo 2.2 schema - adds instance, activeWeek, and nostr protocol. - $schema['properties'] = array_merge( + $schema['properties'] = \array_merge( $schema['properties'], array( 'version' => array( @@ -225,7 +225,7 @@ public static function protocols( $protocols, $version ) { } // Default protocols - can be extended via filter. - return apply_filters( 'nodeinfo_protocols', array() ); + return \apply_filters( 'nodeinfo_protocols', array() ); } /** @@ -258,17 +258,17 @@ public static function usage( $usage, $version ) { return $usage; } - $users = get_users( + $users = \get_users( array( 'fields' => 'ID', 'capability__in' => array( 'publish_posts' ), ) ); - $user_count = is_array( $users ) ? count( $users ) : 1; + $user_count = \is_array( $users ) ? \count( $users ) : 1; - $posts = wp_count_posts(); - $comments = wp_count_comments(); + $posts = \wp_count_posts(); + $comments = \wp_count_comments(); $usage['users'] = array( 'total' => $user_count, @@ -295,9 +295,9 @@ public static function metadata( $metadata, $version ) { return $metadata; } - $metadata['nodeName'] = get_bloginfo( 'name' ); - $metadata['nodeDescription'] = get_bloginfo( 'description' ); - $metadata['nodeIcon'] = get_site_icon_url(); + $metadata['nodeName'] = \get_bloginfo( 'name' ); + $metadata['nodeDescription'] = \get_bloginfo( 'description' ); + $metadata['nodeIcon'] = \get_site_icon_url(); return $metadata; } @@ -315,8 +315,8 @@ public static function add_instance( $nodeinfo, $version ) { } $nodeinfo['instance'] = array( - 'name' => get_bloginfo( 'name' ), - 'description' => get_bloginfo( 'description' ), + 'name' => \get_bloginfo( 'name' ), + 'description' => \get_bloginfo( 'description' ), ); return $nodeinfo; diff --git a/nodeinfo.php b/nodeinfo.php index 9900bf5..b4aec46 100644 --- a/nodeinfo.php +++ b/nodeinfo.php @@ -39,9 +39,6 @@ function nodeinfo_init() { Nodeinfo\Integration\Nodeinfo21::init(); Nodeinfo\Integration\Nodeinfo22::init(); - // Initialize Site Health checks. - Nodeinfo\Health_Check::init(); - // Register REST routes. add_action( 'rest_api_init', 'nodeinfo_register_routes' ); @@ -52,6 +49,15 @@ function nodeinfo_init() { } 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. */ From 86be1c2079ac7516bf6617aca41b4761ad405c2f Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 8 Dec 2025 11:43:40 +0100 Subject: [PATCH 17/21] Add backwards compatibility and update package.json - Add deprecated Nodeinfo_Endpoint class for backwards compatibility - Update package.json with wp-env scripts and remove grunt - Change wp-env ports to avoid conflicts --- .wp-env.json | 4 ++-- includes/class-nodeinfo-endpoint.php | 25 +++++++++++++++++++++++++ nodeinfo.php | 3 +++ package.json | 26 +++++++++++++++++--------- 4 files changed, 47 insertions(+), 11 deletions(-) create mode 100644 includes/class-nodeinfo-endpoint.php diff --git a/.wp-env.json b/.wp-env.json index c0d9d4b..67ba47b 100644 --- a/.wp-env.json +++ b/.wp-env.json @@ -1,8 +1,8 @@ { "core": null, "plugins": [ "." ], - "port": 8889, - "testsPort": 8890, + "port": 8891, + "testsPort": 8892, "env": { "tests": { "config": { diff --git a/includes/class-nodeinfo-endpoint.php b/includes/class-nodeinfo-endpoint.php new file mode 100644 index 0000000..b8bf6ef --- /dev/null +++ b/includes/class-nodeinfo-endpoint.php @@ -0,0 +1,25 @@ +=18" + }, "devDependencies": { - "grunt": "^1.6.1", - "grunt-wp-i18n": "^1.0.3", - "grunt-wp-readme-to-markdown": "^2.1.0" + "@wordpress/env": "^10.0.0" + }, + "scripts": { + "wp-env": "wp-env", + "start": "wp-env start", + "stop": "wp-env stop", + "test:php": "wp-env run tests-cli --env-cwd=wp-content/plugins/nodeinfo vendor/bin/phpunit", + "lint:php": "composer lint", + "lint:php:fix": "composer lint:fix" } } From cd88863c224314d9f6ceed81c8da6e4cd0ed0db6 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 8 Dec 2025 11:45:56 +0100 Subject: [PATCH 18/21] Update .gitattributes with new config files --- .gitattributes | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/.gitattributes b/.gitattributes index 7d4e892..1e92b90 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,30 +2,30 @@ .editorconfig export-ignore .git export-ignore .gitignore export-ignore +.gitattributes export-ignore +.github export-ignore +.wordpress-org export-ignore +.wp-env.json export-ignore .travis.yml export-ignore .codeclimate.yml export-ignore .data export-ignore -.github export-ignore -.gitattributes export-ignore -.wordpress-org export-ignore -Gruntfile.js export-ignore -LINGUAS export-ignore -Makefile export-ignore -CODE_OF_CONDUCT.md export-ignore -LICENSE.md export-ignore _site export-ignore bin export-ignore +CODE_OF_CONDUCT.md export-ignore composer.json export-ignore composer.lock export-ignore docker-compose.yml export-ignore +Gruntfile.js export-ignore gulpfile.js export-ignore -package.json export-ignore +LICENSE.md export-ignore +LINGUAS export-ignore +Makefile export-ignore node_modules export-ignore npm-debug.log export-ignore -phpcs.xml export-ignore package.json export-ignore +package-lock.json export-ignore +phpcs.xml export-ignore phpunit.xml export-ignore phpunit.xml.dist export-ignore tests export-ignore -node_modules export-ignore vendor export-ignore From 99d31f2892933ee2e34b125475c96201d8b04770 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 8 Dec 2025 12:31:12 +0100 Subject: [PATCH 19/21] Update includes/controller/class-nodeinfo2.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- includes/controller/class-nodeinfo2.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/controller/class-nodeinfo2.php b/includes/controller/class-nodeinfo2.php index de49a28..4387d50 100644 --- a/includes/controller/class-nodeinfo2.php +++ b/includes/controller/class-nodeinfo2.php @@ -55,7 +55,7 @@ public function register_routes() { * Retrieves NodeInfo2 data. * * @param \WP_REST_Request $request The request object. - * @return WP_REST_Response The response object. + * @return \WP_REST_Response The response object. */ public function get_item( $request ) { $version = $request->get_param( 'version' ); From fd8d38191ba8e1c274db102a75517d902d9ab357 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 8 Dec 2025 12:31:26 +0100 Subject: [PATCH 20/21] Update phpcs.xml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- phpcs.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpcs.xml b/phpcs.xml index 3001e85..d3575a8 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -15,7 +15,7 @@ - + From f49831f7d0c2a6949c0171fe90ea90e8966b1d0c Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 8 Dec 2025 12:56:53 +0100 Subject: [PATCH 21/21] Fix issues from code review - Add backslash to @return WP_REST_Response types - Add fields parameter to get_users for performance - Add version 2.2 to test coverage --- includes/controller/class-nodeinfo.php | 4 ++-- includes/controller/class-nodeinfo2.php | 1 + tests/phpunit/tests/class-test-nodeinfo-endpoint.php | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/includes/controller/class-nodeinfo.php b/includes/controller/class-nodeinfo.php index a106481..54eb5dd 100644 --- a/includes/controller/class-nodeinfo.php +++ b/includes/controller/class-nodeinfo.php @@ -83,7 +83,7 @@ protected function get_versions() { /** * Retrieves the discovery document. * - * @return WP_REST_Response The response object. + * @return \WP_REST_Response The response object. */ public function get_discovery() { $links = array(); @@ -114,7 +114,7 @@ public function get_discovery() { * Retrieves NodeInfo for a specific version. * * @param \WP_REST_Request $request The request object. - * @return WP_REST_Response The response object. + * @return \WP_REST_Response The response object. */ public function get_item( $request ) { $version = $request->get_param( 'version' ); diff --git a/includes/controller/class-nodeinfo2.php b/includes/controller/class-nodeinfo2.php index 4387d50..53f61c1 100644 --- a/includes/controller/class-nodeinfo2.php +++ b/includes/controller/class-nodeinfo2.php @@ -63,6 +63,7 @@ public function get_item( $request ) { $users = \get_users( array( 'capability__in' => array( 'publish_posts' ), + 'fields' => 'ID', ) ); diff --git a/tests/phpunit/tests/class-test-nodeinfo-endpoint.php b/tests/phpunit/tests/class-test-nodeinfo-endpoint.php index aa9154b..cc9861a 100644 --- a/tests/phpunit/tests/class-test-nodeinfo-endpoint.php +++ b/tests/phpunit/tests/class-test-nodeinfo-endpoint.php @@ -92,7 +92,7 @@ public function test_discovery_contains_all_versions() { $response = $this->server->dispatch( $request ); $data = $response->get_data(); - $versions = array( '1.0', '1.1', '2.0', '2.1' ); + $versions = array( '1.0', '1.1', '2.0', '2.1', '2.2' ); $links = $data['links']; foreach ( $versions as $version ) {