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
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..f82b764
--- /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_ALLOW_EMPTY_ROOT_PASSWORD: true
+ 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 }}
+
+ - name: Run PHPUnit
+ run: vendor/bin/phpunit
diff --git a/.wp-env.json b/.wp-env.json
new file mode 100644
index 0000000..67ba47b
--- /dev/null
+++ b/.wp-env.json
@@ -0,0 +1,19 @@
+{
+ "core": null,
+ "plugins": [ "." ],
+ "port": 8891,
+ "testsPort": 8892,
+ "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/README.md b/README.md
index 73cbb32..eb5bdf3 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
@@ -18,12 +18,80 @@ 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.
+
+### 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).
+### 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
+* Added Site Health checks to verify endpoints are accessible
+
### 2.3.1
* mask version number
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/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-health-check.php b/includes/class-health-check.php
new file mode 100644
index 0000000..643c2f3
--- /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/includes/class-nodeinfo-endpoint.php b/includes/class-nodeinfo-endpoint.php
index adb5506..b8bf6ef 100644
--- a/includes/class-nodeinfo-endpoint.php
+++ b/includes/class-nodeinfo-endpoint.php
@@ -1,166 +1,25 @@
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() );
- }
+/**
+ * Nodeinfo_Endpoint class.
+ *
+ * @deprecated 3.0.0 Use Nodeinfo\Controller\Nodeinfo instead.
+ */
+class Nodeinfo_Endpoint extends Nodeinfo\Controller\Nodeinfo {
/**
- * 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
+ * Constructor.
*/
- 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' ),
+ public function __construct() {
+ \_doing_it_wrong(
+ __CLASS__,
+ \esc_html__( 'Nodeinfo_Endpoint is deprecated. Use Nodeinfo\Controller\Nodeinfo instead.', 'nodeinfo' ),
+ '3.0.0'
);
-
- $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..54eb5dd
--- /dev/null
+++ b/includes/controller/class-nodeinfo.php
@@ -0,0 +1,196 @@
+namespace,
+ '/discovery',
+ array(
+ array(
+ 'methods' => \WP_REST_Server::READABLE,
+ 'callback' => array( $this, 'get_discovery' ),
+ 'permission_callback' => '__return_true',
+ ),
+ )
+ );
+
+ $versions = $this->get_versions();
+
+ if ( empty( $versions ) ) {
+ return;
+ }
+
+ \register_rest_route(
+ $this->namespace,
+ '/(?P\d\.\d)',
+ array(
+ 'args' => array(
+ 'version' => array(
+ 'description' => 'The NodeInfo schema version.',
+ 'type' => 'string',
+ 'enum' => $versions,
+ 'required' => true,
+ ),
+ ),
+ array(
+ 'methods' => \WP_REST_Server::READABLE,
+ 'callback' => array( $this, 'get_item' ),
+ 'permission_callback' => '__return_true',
+ ),
+ 'schema' => array( $this, 'get_item_schema' ),
+ )
+ );
+ }
+
+ /**
+ * 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.
+ *
+ * @return \WP_REST_Response The response object.
+ */
+ public function get_discovery() {
+ $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.
+ *
+ * @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 );
+ }
+
+ /**
+ * Retrieves the NodeInfo schema.
+ *
+ * @return array The schema data.
+ */
+ public function get_item_schema() {
+ $schema = array(
+ '$schema' => 'http://json-schema.org/draft-04/schema#',
+ 'title' => 'nodeinfo',
+ 'type' => 'object',
+ 'properties' => array(),
+ );
+
+ /**
+ * Filters the NodeInfo schema.
+ *
+ * @param array $schema The schema data.
+ */
+ 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/controller/class-nodeinfo2.php b/includes/controller/class-nodeinfo2.php
new file mode 100644
index 0000000..53f61c1
--- /dev/null
+++ b/includes/controller/class-nodeinfo2.php
@@ -0,0 +1,229 @@
+namespace,
+ '/(?P\d\.\d)',
+ array(
+ 'args' => array(
+ 'version' => array(
+ 'description' => 'The NodeInfo2 schema version.',
+ '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' ),
+ 'fields' => 'ID',
+ )
+ );
+
+ $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.',
+ 'type' => 'string',
+ 'enum' => array( '1.0' ),
+ ),
+ 'server' => array(
+ 'description' => 'Metadata about the server.',
+ '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.',
+ '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',
+ ),
+ ),
+ );
+ }
+}
diff --git a/includes/functions.php b/includes/functions.php
index 2252d83..e17a1eb 100644
--- a/includes/functions.php
+++ b/includes/functions.php
@@ -1,7 +1,20 @@
'post',
'post_status' => 'publish',
@@ -20,10 +33,9 @@ function nodeinfo_get_active_users( $duration = '1 month ago' ) {
return 0;
}
- // get all distinct ID from $posts
- return count(
- array_unique(
- wp_list_pluck(
+ return \count(
+ \array_unique(
+ \wp_list_pluck(
$posts,
'post_author'
)
@@ -32,17 +44,16 @@ 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
- $version = get_bloginfo( 'version' );
- // strip the RC or beta part
- $version = preg_replace( '/-.*$/', '', $version );
- $version = explode( '.', $version );
- $version = array_slice( $version, 0, 2 );
+function get_masked_version() {
+ $version = \get_bloginfo( 'version' );
+ // Strip RC/beta suffixes.
+ $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
new file mode 100644
index 0000000..bb44d30
--- /dev/null
+++ b/includes/integration/class-nodeinfo10.php
@@ -0,0 +1,285 @@
+ 'http://nodeinfo.diaspora.software/ns/schema/' . self::VERSION,
+ 'href' => \get_rest_url( null, '/nodeinfo/' . self::VERSION ),
+ );
+ return $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.
+ */
+ public static function schema( $schema ) {
+ $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',
+ 'enum' => array( 'diaspora', 'friendica', 'redmatrix' ),
+ ),
+ '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',
+ 'enum' => array( 'buddycloud', 'diaspora', 'friendica', 'gnusocial', 'libertree', 'mediagoblin', 'pumpio', 'redmatrix', 'smtp', 'tent' ),
+ ),
+ ),
+ 'outbound' => array(
+ 'type' => 'array',
+ 'items' => array(
+ 'type' => 'string',
+ 'enum' => array( 'buddycloud', 'diaspora', 'friendica', 'gnusocial', 'libertree', 'mediagoblin', 'pumpio', 'redmatrix', 'smtp', 'tent' ),
+ ),
+ ),
+ ),
+ ),
+ 'services' => array(
+ 'description' => 'Third party sites this server can connect to.',
+ 'type' => 'object',
+ 'properties' => array(
+ 'inbound' => array(
+ 'type' => 'array',
+ 'items' => array(
+ 'type' => 'string',
+ 'enum' => array( 'appnet', 'gnusocial', 'pumpio' ),
+ ),
+ ),
+ 'outbound' => array(
+ 'type' => 'array',
+ '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' ),
+ ),
+ ),
+ ),
+ ),
+ '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',
+ 'minimum' => 0,
+ ),
+ 'activeMonth' => array(
+ 'type' => 'integer',
+ 'minimum' => 0,
+ ),
+ 'activeHalfyear' => array(
+ 'type' => 'integer',
+ 'minimum' => 0,
+ ),
+ ),
+ ),
+ 'localPosts' => array(
+ 'type' => 'integer',
+ 'minimum' => 0,
+ ),
+ 'localComments' => array(
+ 'type' => 'integer',
+ 'minimum' => 0,
+ ),
+ ),
+ ),
+ '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();
+
+ 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-nodeinfo11.php b/includes/integration/class-nodeinfo11.php
new file mode 100644
index 0000000..aa7b3b1
--- /dev/null
+++ b/includes/integration/class-nodeinfo11.php
@@ -0,0 +1,286 @@
+ 'http://nodeinfo.diaspora.software/ns/schema/' . self::VERSION,
+ 'href' => \get_rest_url( null, '/nodeinfo/' . self::VERSION ),
+ );
+ return $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 - adds hubzilla to software enum and zot to protocols.
+ $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',
+ 'enum' => array( 'diaspora', 'friendica', 'hubzilla', 'redmatrix' ),
+ ),
+ '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',
+ 'enum' => array( 'buddycloud', 'diaspora', 'friendica', 'gnusocial', 'libertree', 'mediagoblin', 'pumpio', 'redmatrix', 'smtp', 'tent', 'zot' ),
+ ),
+ ),
+ 'outbound' => array(
+ 'type' => 'array',
+ 'items' => array(
+ 'type' => 'string',
+ 'enum' => array( 'buddycloud', 'diaspora', 'friendica', 'gnusocial', 'libertree', 'mediagoblin', 'pumpio', 'redmatrix', 'smtp', 'tent', 'zot' ),
+ ),
+ ),
+ ),
+ ),
+ 'services' => array(
+ 'description' => 'Third party sites this server can connect to.',
+ 'type' => 'object',
+ 'properties' => array(
+ 'inbound' => array(
+ 'type' => 'array',
+ 'items' => array(
+ 'type' => 'string',
+ 'enum' => array( 'appnet', 'gnusocial', 'pumpio' ),
+ ),
+ ),
+ 'outbound' => array(
+ 'type' => 'array',
+ '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' ),
+ ),
+ ),
+ ),
+ ),
+ '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',
+ 'minimum' => 0,
+ ),
+ 'activeMonth' => array(
+ 'type' => 'integer',
+ 'minimum' => 0,
+ ),
+ 'activeHalfyear' => array(
+ 'type' => 'integer',
+ 'minimum' => 0,
+ ),
+ ),
+ ),
+ 'localPosts' => array(
+ 'type' => 'integer',
+ 'minimum' => 0,
+ ),
+ 'localComments' => array(
+ 'type' => 'integer',
+ 'minimum' => 0,
+ ),
+ ),
+ ),
+ '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();
+
+ 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.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;
+ }
+
+ /**
+ * 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-nodeinfo20.php b/includes/integration/class-nodeinfo20.php
new file mode 100644
index 0000000..22fd47a
--- /dev/null
+++ b/includes/integration/class-nodeinfo20.php
@@ -0,0 +1,274 @@
+ '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.
+ *
+ * @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, software name is pattern-based.
+ $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',
+ 'pattern' => '^[a-z0-9-]+$',
+ ),
+ 'version' => array( 'type' => 'string' ),
+ ),
+ ),
+ 'protocols' => array(
+ 'description' => 'The protocols supported on this server.',
+ 'type' => 'array',
+ '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.',
+ 'type' => 'object',
+ 'properties' => array(
+ 'inbound' => array(
+ 'type' => 'array',
+ 'items' => array(
+ 'type' => 'string',
+ 'enum' => array( 'atom1.0', 'gnusocial', 'imap', 'pnut', 'pop3', 'pumpio', 'rss2.0', 'twitter' ),
+ ),
+ ),
+ 'outbound' => array(
+ 'type' => 'array',
+ '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' ),
+ ),
+ ),
+ ),
+ ),
+ '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',
+ 'minimum' => 0,
+ ),
+ 'activeMonth' => array(
+ 'type' => 'integer',
+ 'minimum' => 0,
+ ),
+ 'activeHalfyear' => array(
+ 'type' => 'integer',
+ 'minimum' => 0,
+ ),
+ ),
+ ),
+ 'localPosts' => array(
+ 'type' => 'integer',
+ 'minimum' => 0,
+ ),
+ 'localComments' => array(
+ 'type' => 'integer',
+ 'minimum' => 0,
+ ),
+ ),
+ ),
+ '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();
+
+ 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.
+ *
+ * @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..c108471
--- /dev/null
+++ b/includes/integration/class-nodeinfo21.php
@@ -0,0 +1,284 @@
+ '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.
+ *
+ * @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 and homepage to software.
+ $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',
+ 'pattern' => '^[a-z0-9-]+$',
+ ),
+ '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',
+ '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.',
+ 'type' => 'object',
+ 'properties' => array(
+ 'inbound' => array(
+ 'type' => 'array',
+ 'items' => array(
+ 'type' => 'string',
+ 'enum' => array( 'atom1.0', 'gnusocial', 'imap', 'pnut', 'pop3', 'pumpio', 'rss2.0', 'twitter' ),
+ ),
+ ),
+ 'outbound' => array(
+ 'type' => 'array',
+ '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' ),
+ ),
+ ),
+ ),
+ ),
+ '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',
+ 'minimum' => 0,
+ ),
+ 'activeMonth' => array(
+ 'type' => 'integer',
+ 'minimum' => 0,
+ ),
+ 'activeHalfyear' => array(
+ 'type' => 'integer',
+ 'minimum' => 0,
+ ),
+ ),
+ ),
+ 'localPosts' => array(
+ 'type' => 'integer',
+ 'minimum' => 0,
+ ),
+ 'localComments' => array(
+ 'type' => 'integer',
+ 'minimum' => 0,
+ ),
+ ),
+ ),
+ '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';
+ $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.
+ *
+ * @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-nodeinfo22.php b/includes/integration/class-nodeinfo22.php
new file mode 100644
index 0000000..6ad699e
--- /dev/null
+++ b/includes/integration/class-nodeinfo22.php
@@ -0,0 +1,324 @@
+ '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.
+ *
+ * @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, activeWeek, and nostr protocol.
+ $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',
+ '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',
+ 'pattern' => '^[a-z0-9-]+$',
+ ),
+ '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',
+ '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.',
+ 'type' => 'object',
+ 'properties' => array(
+ 'inbound' => array(
+ 'type' => 'array',
+ 'items' => array(
+ 'type' => 'string',
+ 'enum' => array( 'atom1.0', 'gnusocial', 'imap', 'pnut', 'pop3', 'pumpio', 'rss2.0', 'twitter' ),
+ ),
+ ),
+ 'outbound' => array(
+ 'type' => 'array',
+ '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' ),
+ ),
+ ),
+ ),
+ ),
+ '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',
+ 'minimum' => 0,
+ ),
+ 'activeMonth' => array(
+ 'type' => 'integer',
+ 'minimum' => 0,
+ ),
+ 'activeHalfyear' => array(
+ 'type' => 'integer',
+ 'minimum' => 0,
+ ),
+ 'activeWeek' => array(
+ 'type' => 'integer',
+ 'minimum' => 0,
+ ),
+ ),
+ ),
+ 'localPosts' => array(
+ 'type' => 'integer',
+ 'minimum' => 0,
+ ),
+ 'localComments' => array(
+ 'type' => 'integer',
+ 'minimum' => 0,
+ ),
+ ),
+ ),
+ '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';
+ $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.
+ *
+ * @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 e59c1fc..a7f264a 100644
--- a/nodeinfo.php
+++ b/nodeinfo.php
@@ -3,43 +3,77 @@
* 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
* License URI: http://opensource.org/licenses/MIT
* Text Domain: nodeinfo
* Domain Path: /languages
+ *
+ * @package Nodeinfo
*/
+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';
+
+// Require deprecated classes for backwards compatibility.
+require_once NODEINFO_PLUGIN_DIR . 'includes/class-nodeinfo-endpoint.php';
+
/**
- * Initialize plugin
+ * Initialize the plugin.
*/
function nodeinfo_init() {
- require_once __DIR__ . '/includes/class-nodeinfo-endpoint.php';
- require_once __DIR__ . '/includes/functions.php';
+ // Initialize NodeInfo version integrations.
+ Nodeinfo\Integration\Nodeinfo10::init();
+ Nodeinfo\Integration\Nodeinfo11::init();
+ Nodeinfo\Integration\Nodeinfo20::init();
+ Nodeinfo\Integration\Nodeinfo21::init();
+ Nodeinfo\Integration\Nodeinfo22::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.
+ * Initialize admin-only features.
+ */
+function nodeinfo_admin_init() {
+ // Initialize Site Health checks.
+ Nodeinfo\Health_Check::init();
+}
+add_action( 'admin_init', 'nodeinfo_admin_init' );
+
+/**
+ * Register REST API routes.
*/
-function nodeinfo_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 +82,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' );
diff --git a/package.json b/package.json
index e7497d2..358782c 100644
--- a/package.json
+++ b/package.json
@@ -1,17 +1,16 @@
{
- "name": "nodeinfo",
- "version": "1.0.0",
- "description": "NodeInfo is an effort to create a standardized way of exposing metadata about a server running one of the distributed social networks. ",
+ "name": "wordpress-nodeinfo",
+ "description": "NodeInfo and NodeInfo2 for WordPress!",
"repository": {
"type": "git",
"url": "git+https://github.com/pfefferle/wordpress-nodeinfo.git"
},
"keywords": [
+ "wordpress",
"nodeinfo",
"fediverse",
- "ostatus",
- "diaspora",
- "activitypub"
+ "activitypub",
+ "diaspora"
],
"author": "Matthias Pfefferle",
"license": "MIT",
@@ -19,9 +18,18 @@
"url": "https://github.com/pfefferle/wordpress-nodeinfo/issues"
},
"homepage": "https://github.com/pfefferle/wordpress-nodeinfo#readme",
+ "engines": {
+ "node": ">=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"
}
}
diff --git a/phpcs.xml b/phpcs.xml
index 667ff7f..d3575a8 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..2ee884b 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -1,5 +1,5 @@
-
- ./tests/
+
+ ./tests/phpunit/tests
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 @@
-assertIsString( $version );
+ $this->assertNotEmpty( $version );
+ }
+
+ /**
+ * Test get_masked_version returns major.minor format.
+ *
+ * @covers \Nodeinfo\get_masked_version
+ */
+ public function test_get_masked_version_format() {
+ $version = get_masked_version();
+
+ // Should match major.minor format (e.g., "6.5").
+ $this->assertMatchesRegularExpression( '/^\d+\.\d+$/', $version );
+ }
+
+ /**
+ * 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..cc9861a
--- /dev/null
+++ b/tests/phpunit/tests/class-test-nodeinfo-endpoint.php
@@ -0,0 +1,262 @@
+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', '2.2' );
+ $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 );
+
+ // Returns 400 (bad request) because enum validation fails.
+ $this->assertEquals( 400, $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..ef1766a
--- /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/(?P\\d\\.\\d)', $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'] );
+ }
+}