diff --git a/.distignore b/.distignore index 8715c0240..4ba7015b6 100644 --- a/.distignore +++ b/.distignore @@ -41,3 +41,4 @@ tests node_modules vendor src +docs diff --git a/.editorconfig b/.editorconfig index ceb407da4..a541e47e7 100644 --- a/.editorconfig +++ b/.editorconfig @@ -12,11 +12,10 @@ end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true indent_style = tab -indent_size = 4 -[{.jshintrc,*.json,*.yml}] +[*.yml] indent_style = space indent_size = 2 -[{*.txt,wp-config-sample.php}] -end_of_line = lf +[*.md] +trim_trailing_whitespace = false diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 3c72a8601..c5c3d27ce 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,6 +1,7 @@ name: Bug Report description: Helps us improve our product! -labels: "Needs triage, [Type] Bug" +labels: [ 'Needs triage', '[Type] Bug' ] +type: 'Bug' body: - type: markdown attributes: diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 2fc3601d6..a4017ae9f 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -2,6 +2,7 @@ name: Feature Request description: Suggest an idea for the ActivityPub plugin! title: "Feature Request:" labels: ["[Type] Feature Request"] +type: 'Enhancement' body: - type: markdown attributes: diff --git a/.github/workflows/phpcs.yml b/.github/workflows/phpcs.yml index b7392e165..9707c9124 100644 --- a/.github/workflows/phpcs.yml +++ b/.github/workflows/phpcs.yml @@ -28,4 +28,4 @@ jobs: - name: Install dependencies run: composer install --prefer-dist --no-progress - 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 index 9d0561647..dc1f257c7 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -23,6 +23,10 @@ jobs: - wp-version: '6.5' php-versions: '7.1' steps: + - name: Install svn + run: | + sudo apt-get update + sudo apt-get install subversion - name: Checkout uses: actions/checkout@v2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 67a493d9f..87baccb2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,28 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [3.3.3] - 2024-10-09 + +### Fixed + +* Sanitization callback + +### Improved + +* A lot of PHPCS cleanups +* Prepare multi-lang support + +## [3.3.2] - 2024-10-02 + +### Fixed + +* Keep priority of Icons +* Fatal error if remote-object is `WP_Error` + +### Improved + +* Adopt WordPress PHP Coding Standards + ## [3.3.1] - 2024-09-26 ### Fixed @@ -947,6 +969,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * initial +[3.3.3]: https://github.com/Automattic/wordpress-activitypub/compare/3.3.2...3.3.3 +[3.3.2]: https://github.com/Automattic/wordpress-activitypub/compare/3.3.1...3.3.2 [3.3.1]: https://github.com/Automattic/wordpress-activitypub/compare/3.3.0...3.3.1 [3.3.0]: https://github.com/Automattic/wordpress-activitypub/compare/3.2.5...3.3.0 [3.2.5]: https://github.com/Automattic/wordpress-activitypub/compare/3.2.4...3.2.5 diff --git a/FEDERATION.md b/FEDERATION.md index b1dabe415..eb6a047c9 100644 --- a/FEDERATION.md +++ b/FEDERATION.md @@ -5,8 +5,8 @@ The WordPress plugin largely follows ActivityPub's server-to-server specificatio ## Supported federation protocols and standards - [ActivityPub](https://www.w3.org/TR/activitypub/) (Server-to-Server) -- [WebFinger](https://webfinger.net/) -- [HTTP Signatures](https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures) +- [WebFinger](https://swicg.github.io/activitypub-http-signature/) +- [HTTP Signatures](https://www.w3.org/community/reports/socialcg/CG-FINAL-apwf-20240608/) - [NodeInfo](https://nodeinfo.diaspora.software/) ## Supported FEPs diff --git a/README.md b/README.md index 5a65593ba..b67eaab95 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ **Tags:** OStatus, fediverse, activitypub, activitystream **Requires at least:** 5.5 **Tested up to:** 6.6 -**Stable tag:** 3.3.1 +**Stable tag:** 3.3.3 **Requires PHP:** 7.0 **License:** MIT **License URI:** http://opensource.org/licenses/MIT @@ -150,6 +150,18 @@ For reasons of data protection, it is not possible to see the followers of other ## Changelog ## +### 3.3.3 ### + +* Fixed: Sanitization callback +* Improved: A lot of PHPCS cleanups +* Improved: Prepare multi-lang support + +### 3.3.2 ### + +* Fixed: Keep priority of Icons +* Fixed: Fatal error if remote-object is `WP_Error` +* Improved: Adopt WordPress PHP Coding Standards + ### 3.3.1 ### * Fixed: PHP Warnings diff --git a/_config.yml b/_config.yml deleted file mode 100644 index cc35c1df2..000000000 --- a/_config.yml +++ /dev/null @@ -1 +0,0 @@ -theme: jekyll-theme-modernist \ No newline at end of file diff --git a/activitypub.php b/activitypub.php index d2381957e..0e620ca41 100644 --- a/activitypub.php +++ b/activitypub.php @@ -3,7 +3,7 @@ * Plugin Name: ActivityPub * Plugin URI: https://github.com/pfefferle/wordpress-activitypub/ * Description: The ActivityPub protocol is a decentralized social networking protocol based upon the ActivityStreams 2.0 data format. - * Version: 3.3.1 + * Version: 3.3.3 * Author: Matthias Pfefferle & Automattic * Author URI: https://automattic.com/ * License: MIT @@ -11,41 +11,52 @@ * Requires PHP: 7.0 * Text Domain: activitypub * Domain Path: /languages + * + * @package Activitypub */ namespace Activitypub; use WP_CLI; -use function Activitypub\is_blog_public; -use function Activitypub\site_supports_blocks; - require_once __DIR__ . '/includes/compat.php'; require_once __DIR__ . '/includes/functions.php'; -\define( 'ACTIVITYPUB_PLUGIN_VERSION', '3.3.1' ); +\define( 'ACTIVITYPUB_PLUGIN_VERSION', '4.0.0' ); /** * Initialize the plugin constants. */ \defined( 'ACTIVITYPUB_REST_NAMESPACE' ) || \define( 'ACTIVITYPUB_REST_NAMESPACE', 'activitypub/1.0' ); \defined( 'ACTIVITYPUB_EXCERPT_LENGTH' ) || \define( 'ACTIVITYPUB_EXCERPT_LENGTH', 400 ); +\defined( 'ACTIVITYPUB_NOTE_LENGTH' ) || \define( 'ACTIVITYPUB_NOTE_LENGTH', 400 ); \defined( 'ACTIVITYPUB_SHOW_PLUGIN_RECOMMENDATIONS' ) || \define( 'ACTIVITYPUB_SHOW_PLUGIN_RECOMMENDATIONS', true ); \defined( 'ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS' ) || \define( 'ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS', 3 ); \defined( 'ACTIVITYPUB_HASHTAGS_REGEXP' ) || \define( 'ACTIVITYPUB_HASHTAGS_REGEXP', '(?:(?<=\s)|(?<=
)|(?<=
)|^)#([A-Za-z0-9_]+)(?:(?=\s|[[:punct:]]|$))' );
\defined( 'ACTIVITYPUB_USERNAME_REGEXP' ) || \define( 'ACTIVITYPUB_USERNAME_REGEXP', '(?:([A-Za-z0-9\._-]+)@((?:[A-Za-z0-9_-]+\.)+[A-Za-z]+))' );
-\defined( 'ACTIVITYPUB_URL_REGEXP' ) || \define( 'ACTIVITYPUB_URL_REGEXP', '(www.|http:|https:)+[^\s]+[\w\/]' );
+\defined( 'ACTIVITYPUB_URL_REGEXP' ) || \define( 'ACTIVITYPUB_URL_REGEXP', '(https?:|www\.)\S+[\w\/]' );
\defined( 'ACTIVITYPUB_CUSTOM_POST_CONTENT' ) || \define( 'ACTIVITYPUB_CUSTOM_POST_CONTENT', "
%s
was replaced, this is often done by plugins.',
'activitypub'
@@ -186,11 +192,11 @@ public static function is_author_url_accessible() {
);
}
- // try to access author URL
+ // Try to access author URL.
$response = \wp_remote_get(
$author_url,
array(
- 'headers' => array( 'Accept' => 'application/activity+json' ),
+ 'headers' => array( 'Accept' => 'application/activity+json' ),
'redirection' => 0,
)
);
@@ -199,7 +205,7 @@ public static function is_author_url_accessible() {
return new WP_Error(
'author_url_not_accessible',
\sprintf(
- // translators: %s: Author URL
+ // translators: %s: Author URL.
\__(
'Your author URL %s
is not accessible. Please check your WordPress setup or permalink structure. If the setup seems fine, maybe check if a plugin might restrict the access.',
'activitypub'
@@ -211,12 +217,12 @@ public static function is_author_url_accessible() {
$response_code = \wp_remote_retrieve_response_code( $response );
- // check for redirects
+ // Check for redirects.
if ( \in_array( $response_code, array( 301, 302, 307, 308 ), true ) ) {
return new WP_Error(
'author_url_not_accessible',
\sprintf(
- // translators: %s: Author URL
+ // translators: %s: Author URL.
\__(
'Your author URL %s
is redirecting to another page, this is often done by SEO plugins like "Yoast SEO".',
'activitypub'
@@ -226,14 +232,14 @@ public static function is_author_url_accessible() {
);
}
- // check if response is JSON
+ // Check if response is JSON.
$body = \wp_remote_retrieve_body( $response );
if ( ! \is_string( $body ) || ! \is_array( \json_decode( $body, true ) ) ) {
return new WP_Error(
'author_url_not_accessible',
\sprintf(
- // translators: %s: Author URL
+ // translators: %s: Author URL.
\__(
'Your author URL %s
does not return valid JSON for application/activity+json
. Please check if your hosting supports alternate Accept
headers.',
'activitypub'
@@ -252,7 +258,7 @@ public static function is_author_url_accessible() {
* @return boolean|WP_Error
*/
public static function is_webfinger_endpoint_accessible() {
- $user = Users::get_by_id( Users::APPLICATION_USER_ID );
+ $user = Users::get_by_id( Users::APPLICATION_USER_ID );
$resource = $user->get_webfinger();
$url = Webfinger::resolve( $resource );
@@ -260,7 +266,7 @@ public static function is_webfinger_endpoint_accessible() {
$allowed = array( 'code' => array() );
$not_accessible = wp_kses(
- // translators: %s: Author URL
+ // translators: %s: Author URL.
\__(
'Your WebFinger endpoint %s
is not accessible. Please check your WordPress setup or permalink structure.',
'activitypub'
@@ -268,7 +274,7 @@ public static function is_webfinger_endpoint_accessible() {
$allowed
);
$invalid_response = wp_kses(
- // translators: %s: Author URL
+ // translators: %s: Author URL.
\__(
'Your WebFinger endpoint %s
does not return valid JSON for application/jrd+json
.',
'activitypub'
@@ -277,20 +283,21 @@ public static function is_webfinger_endpoint_accessible() {
);
$health_messages = array(
- 'webfinger_url_not_accessible' => \sprintf(
+ 'webfinger_url_not_accessible' => \sprintf(
$not_accessible,
$url->get_error_data()['data']
),
'webfinger_url_invalid_response' => \sprintf(
- // translators: %s: Author URL
+ // translators: %s: Author URL.
$invalid_response,
$url->get_error_data()['data']
),
);
- $message = null;
+ $message = null;
if ( isset( $health_messages[ $url->get_error_code() ] ) ) {
$message = $health_messages[ $url->get_error_code() ];
}
+
return new WP_Error(
$url->get_error_code(),
$message,
@@ -304,7 +311,7 @@ public static function is_webfinger_endpoint_accessible() {
/**
* Retrieve the URL to the author page for the user with the ID provided.
*
- * @global WP_Rewrite $wp_rewrite WordPress rewrite component.
+ * @global \WP_Rewrite $wp_rewrite WordPress rewrite component.
*
* @param int $author_id Author ID.
* @param string $author_nicename Optional. The author's nicename (slug). Default empty.
@@ -313,8 +320,9 @@ public static function is_webfinger_endpoint_accessible() {
*/
public static function get_author_posts_url( $author_id, $author_nicename = '' ) {
global $wp_rewrite;
+
$auth_id = (int) $author_id;
- $link = $wp_rewrite->get_author_permastruct();
+ $link = $wp_rewrite->get_author_permastruct();
if ( empty( $link ) ) {
$file = home_url( '/' );
@@ -343,12 +351,12 @@ public static function debug_information( $info ) {
$info['activitypub'] = array(
'label' => __( 'ActivityPub', 'activitypub' ),
'fields' => array(
- 'webfinger' => array(
+ 'webfinger' => array(
'label' => __( 'WebFinger Resource', 'activitypub' ),
'value' => Webfinger::get_user_resource( wp_get_current_user()->ID ),
'private' => true,
),
- 'author_url' => array(
+ 'author_url' => array(
'label' => __( 'Author URL', 'activitypub' ),
'value' => get_author_posts_url( wp_get_current_user()->ID ),
'private' => true,
diff --git a/includes/class-http.php b/includes/class-http.php
index 2a8ce7d0d..133a34691 100644
--- a/includes/class-http.php
+++ b/includes/class-http.php
@@ -1,11 +1,15 @@
100,
+ $args = array(
+ 'timeout' => 100,
'limit_response_size' => 1048576,
- 'redirection' => 3,
- 'user-agent' => "$user_agent; ActivityPub",
- 'headers' => array(
- 'Accept' => 'application/activity+json',
+ 'redirection' => 3,
+ 'user-agent' => "$user_agent; ActivityPub",
+ 'headers' => array(
+ 'Accept' => 'application/activity+json',
'Content-Type' => 'application/activity+json',
- 'Digest' => $digest,
- 'Signature' => $signature,
- 'Date' => $date,
+ 'Digest' => $digest,
+ 'Signature' => $signature,
+ 'Date' => $date,
),
- 'body' => $body,
+ 'body' => $body,
);
$response = \wp_safe_remote_post( $url, $args );
@@ -58,18 +62,26 @@ public static function post( $url, $body, $user_id ) {
$response = new WP_Error( $code, __( 'Failed HTTP Request', 'activitypub' ), array( 'status' => $code ) );
}
+ /**
+ * Action to save the response of the remote POST request.
+ *
+ * @param array|WP_Error $response The response of the remote POST request.
+ * @param string $url The URL endpoint.
+ * @param string $body The Post Body.
+ * @param int $user_id The WordPress User-ID.
+ */
\do_action( 'activitypub_safe_remote_post_response', $response, $url, $body, $user_id );
return $response;
}
/**
- * Send a GET Request with the needed HTTP Headers
+ * Send a GET Request with the needed HTTP Headers.
*
- * @param string $url The URL endpoint
- * @param bool|int $cached If the result should be cached, or its duration. Default: 1hr.
+ * @param string $url The URL endpoint.
+ * @param bool|int $cached Optional. Whether the result should be cached, or its duration. Default false.
*
- * @return array|WP_Error The GET Response or an WP_ERROR
+ * @return array|WP_Error The GET Response or a WP_Error.
*/
public static function get( $url, $cached = false ) {
\do_action( 'activitypub_pre_http_get', $url );
@@ -80,13 +92,19 @@ public static function get( $url, $cached = false ) {
$response = \get_transient( $transient_key );
if ( $response ) {
+ /**
+ * Action to save the response of the remote GET request.
+ *
+ * @param array|WP_Error $response The response of the remote GET request.
+ * @param string $url The URL endpoint.
+ */
\do_action( 'activitypub_safe_remote_get_response', $response, $url );
return $response;
}
}
- $date = \gmdate( 'D, d M Y H:i:s T' );
+ $date = \gmdate( 'D, d M Y H:i:s T' );
$signature = Signature::generate_signature( Users::APPLICATION_USER_ID, 'get', $url, $date );
$wp_version = get_masked_wp_version();
@@ -99,15 +117,15 @@ public static function get( $url, $cached = false ) {
$user_agent = \apply_filters( 'http_headers_useragent', 'WordPress/' . $wp_version . '; ' . \get_bloginfo( 'url' ) );
$args = array(
- 'timeout' => apply_filters( 'activitypub_remote_get_timeout', 100 ),
+ 'timeout' => apply_filters( 'activitypub_remote_get_timeout', 100 ),
'limit_response_size' => 1048576,
- 'redirection' => 3,
- 'user-agent' => "$user_agent; ActivityPub",
- 'headers' => array(
- 'Accept' => 'application/activity+json',
+ 'redirection' => 3,
+ 'user-agent' => "$user_agent; ActivityPub",
+ 'headers' => array(
+ 'Accept' => 'application/activity+json',
'Content-Type' => 'application/activity+json',
- 'Signature' => $signature,
- 'Date' => $date,
+ 'Signature' => $signature,
+ 'Date' => $date,
),
);
@@ -118,6 +136,12 @@ public static function get( $url, $cached = false ) {
$response = new WP_Error( $code, __( 'Failed HTTP Request', 'activitypub' ), array( 'status' => $code ) );
}
+ /**
+ * Action to save the response of the remote GET request.
+ *
+ * @param array|WP_Error $response The response of the remote GET request.
+ * @param string $url The URL endpoint.
+ */
\do_action( 'activitypub_safe_remote_get_response', $response, $url );
if ( $cached ) {
@@ -139,6 +163,11 @@ public static function get( $url, $cached = false ) {
* @return bool True if the URL is a tombstone.
*/
public static function is_tombstone( $url ) {
+ /**
+ * Action before checking if the URL is a tombstone.
+ *
+ * @param string $url The URL to check.
+ */
\do_action( 'activitypub_pre_http_is_tombstone', $url );
$response = \wp_safe_remote_get( $url );
@@ -151,15 +180,22 @@ public static function is_tombstone( $url ) {
return false;
}
+ /**
+ * Generate a cache key for the URL.
+ *
+ * @param string $url The URL to generate the cache key for.
+ *
+ * @return string The cache key.
+ */
public static function generate_cache_key( $url ) {
return 'activitypub_http_' . \md5( $url );
}
/**
- * Requests the Data from the Object-URL or Object-Array
+ * Requests the Data from the Object-URL or Object-Array.
*
* @param array|string $url_or_object The Object or the Object URL.
- * @param bool $cached If the result should be cached.
+ * @param bool $cached Optional. Whether the result should be cached. Default true.
*
* @return array|WP_Error The Object data as array or WP_Error on failure.
*/
@@ -204,7 +240,7 @@ public static function get_remote_object( $url_or_object, $cached = true ) {
$transient_key = self::generate_cache_key( $url );
- // only check the cache if needed.
+ // Only check the cache if needed.
if ( $cached ) {
$data = \get_transient( $transient_key );
diff --git a/includes/class-link.php b/includes/class-link.php
index dafea7ae1..6da1233c4 100644
--- a/includes/class-link.php
+++ b/includes/class-link.php
@@ -1,67 +1,73 @@
'ID' ) ) as $user_id ) {
$followers = get_user_meta( $user_id, 'activitypub_followers', true );
@@ -210,9 +220,7 @@ public static function migrate_from_0_17() {
}
/**
- * Clear the cache after updating to 1.3.0
- *
- * @return void
+ * Clear the cache after updating to 1.3.0.
*/
private static function migrate_from_1_2_0() {
$user_ids = \get_users(
@@ -228,9 +236,7 @@ private static function migrate_from_1_2_0() {
}
/**
- * Unschedule Hooks after updating to 2.0.0
- *
- * @return void
+ * Unschedule Hooks after updating to 2.0.0.
*/
private static function migrate_from_2_0_0() {
wp_clear_scheduled_hook( 'activitypub_send_post_activity' );
@@ -249,19 +255,15 @@ private static function migrate_from_2_0_0() {
/**
* Add the ActivityPub capability to all users that can publish posts
- * Delete old meta to store followers
- *
- * @return void
+ * Delete old meta to store followers.
*/
private static function migrate_from_2_2_0() {
- // add the ActivityPub capability to all users that can publish posts
+ // Add the ActivityPub capability to all users that can publish posts.
self::add_activitypub_capability();
}
/**
- * Rename DB fields
- *
- * @return void
+ * Rename DB fields.
*/
private static function migrate_from_2_6_0() {
wp_cache_flush();
@@ -273,30 +275,73 @@ private static function migrate_from_2_6_0() {
}
/**
- * Set the defaults needed for the plugin to work
- *
- * * Add the ActivityPub capability to all users that can publish posts
+ * * Update actor-mode settings.
+ * * Get the ID of the latest blog post and save it to the options table.
+ */
+ private static function migrate_to_4_0_0() {
+ $latest_post_id = 0;
+
+ // Get the ID of the latest blog post and save it to the options table.
+ $latest_post = get_posts(
+ array(
+ 'numberposts' => 1,
+ 'orderby' => 'date',
+ 'order' => 'DESC',
+ 'post_type' => 'any',
+ 'post_status' => 'publish',
+ )
+ );
+
+ if ( $latest_post ) {
+ $latest_post_id = $latest_post[0]->ID;
+ }
+
+ \update_option( 'activitypub_last_post_with_permalink_as_id', $latest_post_id );
+
+ $users = \get_users(
+ array(
+ 'capability__in' => array( 'activitypub' ),
+ )
+ );
+
+ foreach ( $users as $user ) {
+ $followers = Followers::get_followers( $user->ID );
+
+ if ( $followers ) {
+ \update_user_option( $user->ID, 'activitypub_use_permalink_as_id', '1' );
+ }
+ }
+
+ $followers = Followers::get_followers( Users::BLOG_USER_ID );
+
+ if ( $followers ) {
+ \update_option( 'activitypub_use_permalink_as_id_for_blog', '1' );
+ }
+
+ self::migrate_actor_mode();
+ }
+
+ /**
+ * Set the defaults needed for the plugin to work.
*
- * @return void
+ * Add the ActivityPub capability to all users that can publish posts.
*/
public static function add_default_settings() {
self::add_activitypub_capability();
}
/**
- * Add the ActivityPub capability to all users that can publish posts
- *
- * @return void
+ * Add the ActivityPub capability to all users that can publish posts.
*/
private static function add_activitypub_capability() {
- // get all WP_User objects that can publish posts
+ // Get all WP_User objects that can publish posts.
$users = \get_users(
array(
'capability__in' => array( 'publish_posts' ),
)
);
- // add ActivityPub capability to all users that can publish posts
+ // Add ActivityPub capability to all users that can publish posts.
foreach ( $users as $user ) {
$user->add_cap( 'activitypub' );
}
@@ -305,16 +350,16 @@ private static function add_activitypub_capability() {
/**
* Rename meta keys.
*
- * @param string $old The old commentmeta key
- * @param string $new The new commentmeta key
+ * @param string $old_key The old comment meta key.
+ * @param string $new_key The new comment meta key.
*/
- private static function update_usermeta_key( $old, $new ) { // phpcs:ignore
+ private static function update_usermeta_key( $old_key, $new_key ) {
global $wpdb;
- $wpdb->update( // phpcs:ignore
+ $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
$wpdb->usermeta,
- array( 'meta_key' => $new ), // phpcs:ignore
- array( 'meta_key' => $old ), // phpcs:ignore
+ array( 'meta_key' => $new_key ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
+ array( 'meta_key' => $old_key ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
array( '%s' ),
array( '%s' )
);
@@ -323,18 +368,43 @@ private static function update_usermeta_key( $old, $new ) { // phpcs:ignore
/**
* Rename option keys.
*
- * @param string $old The old option key
- * @param string $new The new option key
+ * @param string $old_key The old option key.
+ * @param string $new_key The new option key.
*/
- private static function update_options_key( $old, $new ) { // phpcs:ignore
+ private static function update_options_key( $old_key, $new_key ) {
global $wpdb;
- $wpdb->update( // phpcs:ignore
+ $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
$wpdb->options,
- array( 'option_name' => $new ), // phpcs:ignore
- array( 'option_name' => $old ), // phpcs:ignore
+ array( 'option_name' => $new_key ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
+ array( 'option_name' => $old_key ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
array( '%s' ),
array( '%s' )
);
}
+
+ /**
+ * Migrate the actor mode settings.
+ */
+ public static function migrate_actor_mode() {
+ $blog_profile = \get_option( 'activitypub_enable_blog_user', '0' );
+ $author_profiles = \get_option( 'activitypub_enable_users', '1' );
+
+ if (
+ '1' === $blog_profile &&
+ '1' === $author_profiles
+ ) {
+ \update_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_AND_BLOG_MODE );
+ } elseif (
+ '1' === $blog_profile &&
+ '1' !== $author_profiles
+ ) {
+ \update_option( 'activitypub_actor_mode', ACTIVITYPUB_BLOG_MODE );
+ } elseif (
+ '1' !== $blog_profile &&
+ '1' === $author_profiles
+ ) {
+ \update_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_MODE );
+ }
+ }
}
diff --git a/includes/class-notification.php b/includes/class-notification.php
index 002d52d35..1dd657325 100644
--- a/includes/class-notification.php
+++ b/includes/class-notification.php
@@ -1,4 +1,9 @@
type = $type;
- $this->actor = $actor;
- $this->object = $object;
+ public function __construct( $type, $actor, $activity, $target ) {
+ $this->type = $type;
+ $this->actor = $actor;
+ $this->object = $activity;
$this->target = $target;
}
diff --git a/includes/class-scheduler.php b/includes/class-scheduler.php
index 13e330451..8757f8711 100644
--- a/includes/class-scheduler.php
+++ b/includes/class-scheduler.php
@@ -1,28 +1,26 @@
user_id ) {
return;
}
@@ -210,7 +206,7 @@ public static function schedule_comment_activity( $new_status, $old_status, $com
return;
}
- // check if comment should be federated or not
+ // Check if comment should be federated or not.
if ( ! should_comment_be_federated( $comment ) ) {
return;
}
@@ -225,9 +221,7 @@ public static function schedule_comment_activity( $new_status, $old_status, $com
}
/**
- * Update followers
- *
- * @return void
+ * Update followers.
*/
public static function update_followers() {
$number = 5;
@@ -236,6 +230,11 @@ public static function update_followers() {
$number = 50;
}
+ /**
+ * Filter the number of followers to update.
+ *
+ * @param int $number The number of followers to update.
+ */
$number = apply_filters( 'activitypub_update_followers_number', $number );
$followers = Followers::get_outdated_followers( $number );
@@ -252,9 +251,7 @@ public static function update_followers() {
}
/**
- * Cleanup followers
- *
- * @return void
+ * Cleanup followers.
*/
public static function cleanup_followers() {
$number = 5;
@@ -263,6 +260,11 @@ public static function cleanup_followers() {
$number = 50;
}
+ /**
+ * Filter the number of followers to clean up.
+ *
+ * @param int $number The number of followers to clean up.
+ */
$number = apply_filters( 'activitypub_update_followers_number', $number );
$followers = Followers::get_faulty_followers( $number );
@@ -291,18 +293,17 @@ public static function cleanup_followers() {
/**
* Send a profile update when relevant user meta is updated.
*
- * @param int $meta_id Meta ID being updated.
- * @param int $user_id User ID being updated.
+ * @param int $meta_id Meta ID being updated.
+ * @param int $user_id User ID being updated.
* @param string $meta_key Meta key being updated.
- *
- * @return void
*/
public static function user_meta_update( $meta_id, $user_id, $meta_key ) {
- // don't bother if the user can't publish
+ // Don't bother if the user can't publish.
if ( ! \user_can( $user_id, 'activitypub' ) ) {
return;
}
- // the user meta fields that affect a profile.
+
+ // The user meta fields that affect a profile.
$fields = array(
'activitypub_description',
'activitypub_header_image',
@@ -318,12 +319,10 @@ public static function user_meta_update( $meta_id, $user_id, $meta_key ) {
/**
* Send a profile update when a user is updated.
*
- * @param int $user_id User ID being updated.
- *
- * @return void
+ * @param int $user_id User ID being updated.
*/
public static function user_update( $user_id ) {
- // don't bother if the user can't publish
+ // Don't bother if the user can't publish.
if ( ! \user_can( $user_id, 'activitypub' ) ) {
return;
}
@@ -334,7 +333,7 @@ public static function user_update( $user_id ) {
/**
* Theme mods only have a dynamic filter so we fudge it like this.
*
- * @param mixed $value
+ * @param mixed $value Optional. The value to be updated. Default null.
*
* @return mixed
*/
diff --git a/includes/class-shortcodes.php b/includes/class-shortcodes.php
index 16be96ab9..eb9c5135b 100644
--- a/includes/class-shortcodes.php
+++ b/includes/class-shortcodes.php
@@ -1,12 +1,18 @@
post_type ) {
- // get title of attachment with fallback to alt text.
+ // Get title of attachment with fallback to alt text.
$content = wp_get_attachment_caption( $item->ID );
if ( empty( $content ) ) {
$content = get_post_meta( $item->ID, '_wp_attachment_image_alt', true );
@@ -158,7 +156,7 @@ public static function content( $atts, $content, $tag ) {
$content = wp_filter_content_tags( $content );
}
- // replace script and style elements
+ // Replace script and style elements.
$content = \preg_replace( '@<(script|style)[^>]*?>.*?\\1>@si', '', $content );
$content = strip_shortcodes( $content );
$content = \trim( \preg_replace( '/[\n\r\t]/', '', $content ) );
@@ -170,7 +168,7 @@ public static function content( $atts, $content, $tag ) {
}
/**
- * Generates output for the 'ap_permalink' Shortcode
+ * Generates output for the 'ap_permalink' Shortcode.
*
* @param array $atts The Shortcode attributes.
* @param string $content The ActivityPub post-content.
@@ -204,7 +202,7 @@ public static function permalink( $atts, $content, $tag ) {
}
/**
- * Generates output for the 'ap_shortlink' Shortcode
+ * Generates output for the 'ap_shortlink' Shortcode.
*
* @param array $atts The Shortcode attributes.
* @param string $content The ActivityPub post-content.
@@ -238,7 +236,7 @@ public static function shortlink( $atts, $content, $tag ) {
}
/**
- * Generates output for the 'ap_image' Shortcode
+ * Generates output for the 'ap_image' Shortcode.
*
* @param array $atts The Shortcode attributes.
* @param string $content The ActivityPub post-content.
@@ -281,15 +279,11 @@ public static function image( $atts, $content, $tag ) {
}
/**
- * Generates output for the 'ap_hashcats' Shortcode
- *
- * @param array $atts The Shortcode attributes.
- * @param string $content The ActivityPub post-content.
- * @param string $tag The tag/name of the Shortcode.
+ * Generates output for the 'ap_hashcats' Shortcode.
*
* @return string The post categories as hashtags.
*/
- public static function hashcats( $atts, $content, $tag ) {
+ public static function hashcats() {
$item = self::get_item();
if ( ! $item ) {
@@ -316,15 +310,11 @@ public static function hashcats( $atts, $content, $tag ) {
}
/**
- * Generates output for the 'ap_author' Shortcode
- *
- * @param array $atts The Shortcode attributes.
- * @param string $content The ActivityPub post-content.
- * @param string $tag The tag/name of the Shortcode.
+ * Generates output for the 'ap_author' Shortcode.
*
* @return string The author name.
*/
- public static function author( $atts, $content, $tag ) {
+ public static function author() {
$item = self::get_item();
if ( ! $item ) {
@@ -332,7 +322,7 @@ public static function author( $atts, $content, $tag ) {
}
$author_id = \get_post_field( 'post_author', $item->ID );
- $name = \get_the_author_meta( 'display_name', $author_id );
+ $name = \get_the_author_meta( 'display_name', $author_id );
if ( ! $name ) {
return '';
@@ -342,15 +332,11 @@ public static function author( $atts, $content, $tag ) {
}
/**
- * Generates output for the 'ap_authorurl' Shortcode
- *
- * @param array $atts The Shortcode attributes.
- * @param string $content The ActivityPub post-content.
- * @param string $tag The tag/name of the Shortcode.
+ * Generates output for the 'ap_authorurl' Shortcode.
*
* @return string The author URL.
*/
- public static function authorurl( $atts, $content, $tag ) {
+ public static function authorurl() {
$item = self::get_item();
if ( ! $item ) {
@@ -358,7 +344,7 @@ public static function authorurl( $atts, $content, $tag ) {
}
$author_id = \get_post_field( 'post_author', $item->ID );
- $url = \get_the_author_meta( 'user_url', $author_id );
+ $url = \get_the_author_meta( 'user_url', $author_id );
if ( ! $url ) {
return '';
@@ -368,63 +354,46 @@ public static function authorurl( $atts, $content, $tag ) {
}
/**
- * Generates output for the 'ap_blogurl' Shortcode
- *
- * @param array $atts The Shortcode attributes.
- * @param string $content The ActivityPub post-content.
- * @param string $tag The tag/name of the Shortcode.
+ * Generates output for the 'ap_blogurl' Shortcode.
*
* @return string The site URL.
*/
- public static function blogurl( $atts, $content, $tag ) {
+ public static function blogurl() {
return \esc_url( \get_bloginfo( 'url' ) );
}
/**
- * Generates output for the 'ap_blogname' Shortcode
- *
- * @param array $atts The Shortcode attributes.
- * @param string $content The ActivityPub post-content.
- * @param string $tag The tag/name of the Shortcode.
+ * Generates output for the 'ap_blogname' Shortcode.
*
* @return string
*/
- public static function blogname( $atts, $content, $tag ) {
+ public static function blogname() {
return \wp_strip_all_tags( \get_bloginfo( 'name' ) );
}
/**
- * Generates output for the 'ap_blogdesc' Shortcode
- *
- * @param array $atts The Shortcode attributes.
- * @param string $content The ActivityPub post-content.
- * @param string $tag The tag/name of the Shortcode.
+ * Generates output for the 'ap_blogdesc' Shortcode.
*
* @return string The site description.
*/
- public static function blogdesc( $atts, $content, $tag ) {
+ public static function blogdesc() {
return \wp_strip_all_tags( \get_bloginfo( 'description' ) );
}
/**
- * Generates output for the 'ap_date' Shortcode
- *
- * @param array $atts The Shortcode attributes.
- * @param string $content The ActivityPub post-content.
- * @param string $tag The tag/name of the Shortcode.
+ * Generates output for the 'ap_date' Shortcode.
*
* @return string The post date.
*/
- public static function date( $atts, $content, $tag ) {
+ public static function date() {
$item = self::get_item();
if ( ! $item ) {
return '';
}
- $datetime = \get_post_datetime( $item );
+ $datetime = \get_post_datetime( $item );
$dateformat = \get_option( 'date_format' );
- $timeformat = \get_option( 'time_format' );
$date = $datetime->format( $dateformat );
@@ -436,23 +405,18 @@ public static function date( $atts, $content, $tag ) {
}
/**
- * Generates output for the 'ap_time' Shortcode
- *
- * @param array $atts The Shortcode attributes.
- * @param string $content The ActivityPub post-content.
- * @param string $tag The tag/name of the Shortcode.
+ * Generates output for the 'ap_time' Shortcode.
*
* @return string The post time.
*/
- public static function time( $atts, $content, $tag ) {
+ public static function time() {
$item = self::get_item();
if ( ! $item ) {
return '';
}
- $datetime = \get_post_datetime( $item );
- $dateformat = \get_option( 'date_format' );
+ $datetime = \get_post_datetime( $item );
$timeformat = \get_option( 'time_format' );
$date = $datetime->format( $timeformat );
@@ -465,22 +429,18 @@ public static function time( $atts, $content, $tag ) {
}
/**
- * Generates output for the 'ap_datetime' Shortcode
- *
- * @param array $atts The Shortcode attributes.
- * @param string $content The ActivityPub post-content.
- * @param string $tag The tag/name of the Shortcode.
+ * Generates output for the 'ap_datetime' Shortcode.
*
* @return string The post date/time.
*/
- public static function datetime( $atts, $content, $tag ) {
+ public static function datetime() {
$item = self::get_item();
if ( ! $item ) {
return '';
}
- $datetime = \get_post_datetime( $item );
+ $datetime = \get_post_datetime( $item );
$dateformat = \get_option( 'date_format' );
$timeformat = \get_option( 'time_format' );
@@ -499,7 +459,7 @@ public static function datetime( $atts, $content, $tag ) {
* Checks if item (WP_Post) is "public", a supported post type
* and not password protected.
*
- * @return null|WP_Post The WordPress item.
+ * @return null|\WP_Post The WordPress item.
*/
protected static function get_item() {
$post = \get_post();
diff --git a/includes/class-signature.php b/includes/class-signature.php
index 6f8880a15..5d7ecd626 100644
--- a/includes/class-signature.php
+++ b/includes/class-signature.php
@@ -1,4 +1,10 @@
'sha512',
+ 'digest_alg' => 'sha512',
'private_key_bits' => 2048,
'private_key_type' => \OPENSSL_KEYTYPE_RSA,
);
- $key = \openssl_pkey_new( $config );
+ $key = \openssl_pkey_new( $config );
$priv_key = null;
- $detail = array();
+ $detail = array();
if ( $key ) {
\openssl_pkey_export( $key, $priv_key );
$detail = \openssl_pkey_get_details( $key );
}
- // check if keys are valid
+ // Check if keys are valid.
if (
empty( $priv_key ) || ! is_string( $priv_key ) ||
! isset( $detail['key'] ) || ! is_string( $detail['key'] )
@@ -117,7 +123,7 @@ protected static function generate_key_pair_for( $user_id ) {
'public_key' => $detail['key'],
);
- // persist keys
+ // Persist keys.
\add_option( $option_key, $key_pair );
return $key_pair;
@@ -135,7 +141,7 @@ protected static function get_signature_options_key_for( $user_id ) {
if ( $user_id > 0 ) {
$user = \get_userdata( $user_id );
- // sanatize username because it could include spaces and special chars
+ // Sanitize username because it could include spaces and special chars.
$id = sanitize_title( $user->user_login );
}
@@ -152,15 +158,15 @@ protected static function get_signature_options_key_for( $user_id ) {
protected static function check_legacy_key_pair_for( $user_id ) {
switch ( $user_id ) {
case 0:
- $public_key = \get_option( 'activitypub_blog_user_public_key' );
+ $public_key = \get_option( 'activitypub_blog_user_public_key' );
$private_key = \get_option( 'activitypub_blog_user_private_key' );
break;
case -1:
- $public_key = \get_option( 'activitypub_application_user_public_key' );
+ $public_key = \get_option( 'activitypub_application_user_public_key' );
$private_key = \get_option( 'activitypub_application_user_private_key' );
break;
default:
- $public_key = \get_user_meta( $user_id, 'magic_sig_public_key', true );
+ $public_key = \get_user_meta( $user_id, 'magic_sig_public_key', true );
$private_key = \get_user_meta( $user_id, 'magic_sig_private_key', true );
break;
}
@@ -176,13 +182,13 @@ protected static function check_legacy_key_pair_for( $user_id ) {
}
/**
- * Generates the Signature for a HTTP Request
+ * Generates the Signature for an HTTP Request.
*
* @param int $user_id The WordPress User ID.
* @param string $http_method The HTTP method.
* @param string $url The URL to send the request to.
* @param string $date The date the request is sent.
- * @param string $digest The digest of the request body.
+ * @param string $digest Optional. The digest of the request body. Default null.
*
* @return string The signature.
*/
@@ -195,12 +201,12 @@ public static function generate_signature( $user_id, $http_method, $url, $date,
$host = $url_parts['host'];
$path = '/';
- // add path
+ // Add path.
if ( ! empty( $url_parts['path'] ) ) {
$path = $url_parts['path'];
}
- // add query
+ // Add query.
if ( ! empty( $url_parts['query'] ) ) {
$path .= '?' . $url_parts['query'];
}
@@ -215,9 +221,9 @@ public static function generate_signature( $user_id, $http_method, $url, $date,
$signature = null;
\openssl_sign( $signed_string, $signature, $key, \OPENSSL_ALGO_SHA256 );
- $signature = \base64_encode( $signature ); // phpcs:ignore
+ $signature = \base64_encode( $signature ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
- $key_id = $user->get_url() . '#main-key';
+ $key_id = $user->get_id() . '#main-key';
if ( ! empty( $digest ) ) {
return \sprintf( 'keyId="%s",algorithm="rsa-sha256",headers="(request-target) host date digest",signature="%s"', $key_id, $signature );
@@ -231,18 +237,18 @@ public static function generate_signature( $user_id, $http_method, $url, $date,
*
* @param WP_REST_Request|array $request The request object or $_SERVER array.
*
- * @return mixed A boolean or WP_Error.
+ * @return bool|WP_Error A boolean or WP_Error.
*/
public static function verify_http_signature( $request ) {
- if ( is_object( $request ) ) { // REST Request object
- // check if route starts with "index.php"
+ if ( is_object( $request ) ) { // REST Request object.
+ // Check if route starts with "index.php".
if ( str_starts_with( $request->get_route(), '/index.php' ) || ! rest_get_url_prefix() ) {
$route = $request->get_route();
} else {
$route = '/' . rest_get_url_prefix() . '/' . ltrim( $request->get_route(), '/' );
}
- // fix route for subdirectory installs
+ // Fix route for subdirectory installs.
$path = \wp_parse_url( \get_home_url(), PHP_URL_PATH );
if ( \is_string( $path ) ) {
@@ -253,11 +259,11 @@ public static function verify_http_signature( $request ) {
$route = '/' . $path . $route;
}
- $headers = $request->get_headers();
+ $headers = $request->get_headers();
$headers['(request-target)'][0] = strtolower( $request->get_method() ) . ' ' . $route;
} else {
- $request = self::format_server_request( $request );
- $headers = $request['headers']; // $_SERVER array
+ $request = self::format_server_request( $request );
+ $headers = $request['headers']; // $_SERVER array
$headers['(request-target)'][0] = strtolower( $headers['request_method'][0] ) . ' ' . $headers['request_uri'][0];
}
@@ -323,14 +329,14 @@ public static function verify_http_signature( $request ) {
}
/**
- * Get public key from key_id
+ * Get public key from key_id.
*
* @param string $key_id The URL to the public key.
*
* @return WP_Error|string The public key or WP_Error.
*/
- public static function get_remote_key( $key_id ) { // phpcs:ignore
- $actor = get_remote_metadata_by_actor( strip_fragment_from_url( $key_id ) ); // phpcs:ignore
+ public static function get_remote_key( $key_id ) {
+ $actor = get_remote_metadata_by_actor( strip_fragment_from_url( $key_id ) );
if ( \is_wp_error( $actor ) ) {
return new WP_Error(
'activitypub_no_remote_profile_found',
@@ -339,7 +345,7 @@ public static function get_remote_key( $key_id ) { // phpcs:ignore
);
}
if ( isset( $actor['publicKey']['publicKeyPem'] ) ) {
- return \rtrim( $actor['publicKey']['publicKeyPem'] ); // phpcs:ignore
+ return \rtrim( $actor['publicKey']['publicKeyPem'] );
}
return new WP_Error(
'activitypub_no_remote_key_found',
@@ -349,9 +355,9 @@ public static function get_remote_key( $key_id ) { // phpcs:ignore
}
/**
- * Gets the signature algorithm from the signature header
+ * Gets the signature algorithm from the signature header.
*
- * @param array $signature_block
+ * @param array $signature_block The signature block.
*
* @return string The signature algorithm.
*/
@@ -359,7 +365,7 @@ public static function get_signature_algorithm( $signature_block ) {
if ( $signature_block['algorithm'] ) {
switch ( $signature_block['algorithm'] ) {
case 'rsa-sha-512':
- return 'sha512'; //hs2019 https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12
+ return 'sha512'; // hs2019 https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12.
default:
return 'sha256';
}
@@ -368,15 +374,15 @@ public static function get_signature_algorithm( $signature_block ) {
}
/**
- * Parses the Signature header
+ * Parses the Signature header.
*
* @param string $signature The signature header.
*
- * @return array signature parts
+ * @return array Signature parts.
*/
public static function parse_signature_header( $signature ) {
- $parsed_header = array();
- $matches = array();
+ $parsed_header = array();
+ $matches = array();
if ( \preg_match( '/keyId="(.*?)"/ism', $signature, $matches ) ) {
$parsed_header['keyId'] = trim( $matches[1] );
@@ -394,7 +400,7 @@ public static function parse_signature_header( $signature ) {
$parsed_header['headers'] = \explode( ' ', trim( $matches[1] ) );
}
if ( \preg_match( '/signature="(.*?)"/ism', $signature, $matches ) ) {
- $parsed_header['signature'] = \base64_decode( preg_replace( '/\s+/', '', trim( $matches[1] ) ) ); // phpcs:ignore
+ $parsed_header['signature'] = \base64_decode( preg_replace( '/\s+/', '', trim( $matches[1] ) ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
}
if ( ( $parsed_header['signature'] ) && ( $parsed_header['algorithm'] ) && ( ! $parsed_header['headers'] ) ) {
@@ -405,16 +411,17 @@ public static function parse_signature_header( $signature ) {
}
/**
- * Gets the header data from the included pseudo headers
+ * Gets the header data from the included pseudo headers.
*
* @param array $signed_headers The signed headers.
- * @param array $signature_block (pseudo-headers)
- * @param array $headers (http headers)
+ * @param array $signature_block The signature block.
+ * @param array $headers The HTTP headers.
*
* @return string signed headers for comparison
*/
public static function get_signed_data( $signed_headers, $signature_block, $headers ) {
$signed_data = '';
+
// This also verifies time-based values by returning false if any of these are out of range.
foreach ( $signed_headers as $header ) {
if ( 'host' === $header ) {
@@ -433,7 +440,7 @@ public static function get_signed_data( $signed_headers, $signature_block, $head
}
if ( '(created)' === $header ) {
if ( ! empty( $signature_block['(created)'] ) && \intval( $signature_block['(created)'] ) > \time() ) {
- // created in future
+ // Created in the future.
return false;
}
@@ -444,7 +451,7 @@ public static function get_signed_data( $signed_headers, $signature_block, $head
}
if ( '(expires)' === $header ) {
if ( ! empty( $signature_block['(expires)'] ) && \intval( $signature_block['(expires)'] ) < \time() ) {
- // expired in past
+ // Expired in the past.
return false;
}
@@ -454,16 +461,16 @@ public static function get_signed_data( $signed_headers, $signature_block, $head
}
}
if ( 'date' === $header ) {
- // allow a bit of leeway for misconfigured clocks.
+ // Allow a bit of leeway for misconfigured clocks.
$d = new DateTime( $headers[ $header ][0] );
$d->setTimeZone( new DateTimeZone( 'UTC' ) );
$c = $d->format( 'U' );
- $dplus = time() + ( 3 * HOUR_IN_SECONDS );
+ $dplus = time() + ( 3 * HOUR_IN_SECONDS );
$dminus = time() - ( 3 * HOUR_IN_SECONDS );
if ( $c > $dplus || $c < $dminus ) {
- // time out of range
+ // Time out of range.
return false;
}
}
@@ -473,22 +480,22 @@ public static function get_signed_data( $signed_headers, $signature_block, $head
}
/**
- * Generates the digest for a HTTP Request
+ * Generates the digest for an HTTP Request.
*
* @param string $body The body of the request.
*
* @return string The digest.
*/
public static function generate_digest( $body ) {
- $digest = \base64_encode( \hash( 'sha256', $body, true ) ); // phpcs:ignore
+ $digest = \base64_encode( \hash( 'sha256', $body, true ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
return "SHA-256=$digest";
}
/**
* Formats the $_SERVER to resemble the WP_REST_REQUEST array,
- * for use with verify_http_signature()
+ * for use with verify_http_signature().
*
- * @param array $_SERVER The $_SERVER array.
+ * @param array $server The $_SERVER array.
*
* @return array $request The formatted request array.
*/
@@ -499,7 +506,7 @@ public static function format_server_request( $server ) {
if ( 'REQUEST_URI' === $req_param ) {
$request['headers']['route'][] = $param_val;
} else {
- $header_key = str_replace(
+ $header_key = str_replace(
'http_',
'',
$req_param
diff --git a/includes/class-webfinger.php b/includes/class-webfinger.php
index f87f7ba21..f6a189af3 100644
--- a/includes/class-webfinger.php
+++ b/includes/class-webfinger.php
@@ -1,11 +1,17 @@
@' . \__( 'The Fediverse is a new word made of two words: "federation" + "universe"', 'activitypub' ) . '
' . '' . \__( 'It is a federated social network running on free open software on a myriad of computers across the globe. Many independent servers are interconnected and allow people to interact with one another. There\'s no one central site: you choose a server to register. This ensures some decentralization and sovereignty of data. Fediverse (also called Fedi) has no built-in advertisements, no tricky algorithms, no one big corporation dictating the rules. Instead we have small cozy communities of like-minded people. Welcome!', 'activitypub' ) . '
' . - '' . \__( 'For more informations please visit fediverse.party', 'activitypub' ) . '
' . + '' . \__( 'For more information please visit fediverse.party', 'activitypub' ) . '
' . '' . \__( 'ActivityPub is a decentralized social networking protocol based on the ActivityStreams 2.0 data format. ActivityPub is an official W3C recommended standard published by the W3C Social Web Working Group. It provides a client to server API for creating, updating and deleting content, as well as a federated server to server API for delivering notifications and subscribing to content.', 'activitypub' ) . '
' . '' . \__( 'WebFinger is used to discover information about people or other entities on the Internet that are identified by a URI using standard Hypertext Transfer Protocol (HTTP) methods over a secure transport. A WebFinger resource returns a JavaScript Object Notation (JSON) object describing the entity that is queried. The JSON object is referred to as the JSON Resource Descriptor (JRD).', 'activitypub' ) . '
' . '' . \__( 'For a person, the type of information that might be discoverable via WebFinger includes a personal profile address, identity service, telephone number, or preferred avatar. For other entities on the Internet, a WebFinger resource might return JRDs containing link relations that enable a client to discover, for example, that a printer can print in color on A4 paper, the physical location of a server, or other static information.', 'activitypub' ) . '
' . '' . \__( 'On Mastodon [and other Plattforms], user profiles can be hosted either locally on the same website as yours, or remotely on a completely different website. The same username may be used on a different domain. Therefore, a Mastodon user\'s full mention consists of both the username and the domain, in the form @username@domain
. In practical terms, @user@example.com
is not the same as @user@example.org
. If the domain is not included, Mastodon will try to find a local user named @username
. However, in order to deliver to someone over ActivityPub, the @username@domain
mention is not enough – mentions must be translated to an HTTPS URI first, so that the remote actor\'s inbox and outbox can be found. (This paragraph is copied from the Mastodon Documentation)', 'activitypub' ) . '
' . \__( 'For more informations please visit webfinger.net', 'activitypub' ) . '
' . + '' . \__( 'For more information please visit webfinger.net', 'activitypub' ) . '
' . '' . \__( 'NodeInfo is an effort to create a standardized way of exposing metadata about a server running one of the distributed social networks. The two key goals are being able to get better insights into the user base of distributed social networking and the ability to build tools that allow users to choose the best fitting software and server for their needs.', 'activitypub' ) . '
' . - '' . \__( 'For more informations please visit nodeinfo.diaspora.software', 'activitypub' ) . '
', + '' . \__( 'For more information please visit nodeinfo.diaspora.software', 'activitypub' ) . '
', ) ); diff --git a/includes/model/class-application.php b/includes/model/class-application.php index 35c57d647..80e7de23e 100644 --- a/includes/model/class-application.php +++ b/includes/model/class-application.php @@ -1,4 +1,10 @@ + * @var string */ protected $webfinger; + /** + * Returns the type of the object. + * + * @return string The type of the object. + */ public function get_type() { return 'Application'; } + /** + * Returns whether the Application manually approves followers. + * + * @return true Whether the Application manually approves followers. + */ public function get_manually_approves_followers() { return true; } + /** + * Returns the ID of the Application. + * + * @return string The ID of the Application. + */ public function get_id() { return get_rest_url_by_path( 'application' ); } @@ -70,27 +94,37 @@ public function get_url() { * @return string The User-URL with @-Prefix for the username. */ public function get_alternate_url() { - return $this->get_url(); + return $this->get_id(); } + /** + * Get the Username. + * + * @return string The Username. + */ public function get_name() { return 'application'; } + /** + * Get the preferred username. + * + * @return string The preferred username. + */ public function get_preferred_username() { return $this->get_name(); } - /** + /** * Get the User-Icon. * * @return array The User-Icon. */ public function get_icon() { - // try site icon first + // Try site icon first. $icon_id = get_option( 'site_icon' ); - // try custom logo second + // Try custom logo second. if ( ! $icon_id ) { $icon_id = get_theme_mod( 'custom_logo' ); } @@ -105,7 +139,7 @@ public function get_icon() { } if ( ! $icon_url ) { - // fallback to default icon + // Fallback to default icon. $icon_url = plugins_url( '/assets/img/wp-logo.png', ACTIVITYPUB_PLUGIN_FILE ); } @@ -131,6 +165,11 @@ public function get_header_image() { return null; } + /** + * Get the first published date. + * + * @return string The published date. + */ public function get_published() { $first_post = new WP_Query( array( @@ -176,18 +215,23 @@ public function get_webfinger() { return $this->get_preferred_username() . '@' . \wp_parse_url( \home_url(), \PHP_URL_HOST ); } + /** + * Returns the public key. + * + * @return array The public key. + */ public function get_public_key() { return array( - 'id' => $this->get_id() . '#main-key', - 'owner' => $this->get_id(), + 'id' => $this->get_id() . '#main-key', + 'owner' => $this->get_id(), 'publicKeyPem' => Signature::get_public_key_for( Users::APPLICATION_USER_ID ), ); } /** - * Get the User-Description. + * Get the User description. * - * @return string The User-Description. + * @return string The User description. */ public function get_summary() { return \wpautop( @@ -198,6 +242,11 @@ public function get_summary() { ); } + /** + * Returns the canonical URL of the object. + * + * @return string|null The canonical URL of the object. + */ public function get_canonical_url() { return \home_url(); } diff --git a/includes/model/class-blog.php b/includes/model/class-blog.php index 65e90452a..21d7b0a6b 100644 --- a/includes/model/class-blog.php +++ b/includes/model/class-blog.php @@ -1,8 +1,13 @@ + * @var string */ protected $webfinger; /** - * If the User is discoverable. + * Whether the User is discoverable. * * @see https://docs.joinmastodon.org/spec/activitypub/#discoverable * @@ -74,7 +82,7 @@ class Blog extends Actor { protected $discoverable; /** - * Restrict posting to mods + * Restrict posting to mods. * * @see https://join-lemmy.org/docs/contributors/05-federation.html * @@ -82,21 +90,37 @@ class Blog extends Actor { */ protected $posting_restricted_to_mods; + /** + * Whether the User manually approves followers. + * + * @return false + */ public function get_manually_approves_followers() { return false; } + /** + * Whether the User is discoverable. + * + * @return boolean + */ public function get_discoverable() { return true; } /** - * Get the User-ID. + * Get the User ID. * - * @return string The User-ID. + * @return string The User ID. */ public function get_id() { - return $this->get_url(); + $permalink = \get_option( 'activitypub_use_permalink_as_id_for_blog', false ); + + if ( $permalink ) { + return $this->get_url(); + } + + return \add_query_arg( 'author', $this->_id, \trailingslashit( \home_url() ) ); } /** @@ -115,9 +139,9 @@ public function get_type() { } /** - * Get the User-Name. + * Get the Username. * - * @return string The User-Name. + * @return string The Username. */ public function get_name() { return \wp_strip_all_tags( @@ -130,9 +154,9 @@ public function get_name() { } /** - * Get the User-Description. + * Get the User description. * - * @return string The User-Description. + * @return string The User description. */ public function get_summary() { $summary = \get_option( 'activitypub_blog_description', null ); @@ -150,9 +174,9 @@ public function get_summary() { } /** - * Get the User-Url. + * Get the User url. * - * @return string The User-Url. + * @return string The User url. */ public function get_url() { return \esc_url( \trailingslashit( get_home_url() ) . '@' . $this->get_preferred_username() ); @@ -173,12 +197,12 @@ public function get_alternate_url() { * @return string The auto-generated Username. */ public static function get_default_username() { - // check if domain host has a subdomain + // Check if domain host has a subdomain. $host = \wp_parse_url( \get_home_url(), \PHP_URL_HOST ); $host = \preg_replace( '/^www\./i', '', $host ); /** - * Filter the default blog username. + * Filters the default blog username. * * @param string $host The default username. */ @@ -186,9 +210,9 @@ public static function get_default_username() { } /** - * Get the preferred User-Name. + * Get the preferred Username. * - * @return string The User-Name. + * @return string The Username. */ public function get_preferred_username() { $username = \get_option( 'activitypub_blog_identifier' ); @@ -201,15 +225,15 @@ public function get_preferred_username() { } /** - * Get the User-Icon. + * Get the User icon. * - * @return array The User-Icon. + * @return array The User icon. */ public function get_icon() { - // try site_logo, falling back to site_icon, first + // Try site_logo, falling back to site_icon, first. $icon_id = get_option( 'site_icon' ); - // try custom logo second + // Try custom logo second. if ( ! $icon_id ) { $icon_id = get_theme_mod( 'custom_logo' ); } @@ -224,7 +248,7 @@ public function get_icon() { } if ( ! $icon_url ) { - // fallback to default icon + // Fallback to default icon. $icon_url = plugins_url( '/assets/img/wp-logo.png', ACTIVITYPUB_PLUGIN_FILE ); } @@ -261,6 +285,11 @@ public function get_image() { return null; } + /** + * Get the published date. + * + * @return string The published date. + */ public function get_published() { $first_post = new WP_Query( array( @@ -279,10 +308,20 @@ public function get_published() { return \gmdate( 'Y-m-d\TH:i:s\Z', $time ); } + /** + * Get the canonical URL. + * + * @return string|null The canonical URL. + */ public function get_canonical_url() { return \home_url(); } + /** + * Get the Moderators endpoint. + * + * @return string|null The Moderators endpoint. + */ public function get_moderators() { if ( is_single_user() || 'Group' !== $this->get_type() ) { return null; @@ -291,6 +330,11 @@ public function get_moderators() { return get_rest_url_by_path( 'collections/moderators' ); } + /** + * Get attributedTo value. + * + * @return string|null The attributedTo value. + */ public function get_attributed_to() { if ( is_single_user() || 'Group' !== $this->get_type() ) { return null; @@ -299,14 +343,24 @@ public function get_attributed_to() { return get_rest_url_by_path( 'collections/moderators' ); } + /** + * Get the public key information. + * + * @return array The public key. + */ public function get_public_key() { return array( - 'id' => $this->get_id() . '#main-key', - 'owner' => $this->get_id(), + 'id' => $this->get_id() . '#main-key', + 'owner' => $this->get_id(), 'publicKeyPem' => Signature::get_public_key_for( $this->get__id() ), ); } + /** + * Returns whether posting is restricted to mods. + * + * @return bool|null True if posting is restricted to mods, null if not applicable. + */ public function get_posting_restricted_to_mods() { if ( 'Group' === $this->get_type() ) { return true; @@ -351,6 +405,11 @@ public function get_following() { return get_rest_url_by_path( sprintf( 'actors/%d/following', $this->get__id() ) ); } + /** + * Returns endpoints. + * + * @return array|null The endpoints. + */ public function get_endpoints() { $endpoints = null; @@ -381,6 +440,11 @@ public function get_featured() { return get_rest_url_by_path( sprintf( 'actors/%d/collections/featured', $this->get__id() ) ); } + /** + * Returns whether the site is indexable. + * + * @return bool Whether the site is indexable. + */ public function get_indexable() { if ( is_blog_public() ) { return true; @@ -390,7 +454,7 @@ public function get_indexable() { } /** - * Update the User-Name. + * Update the Username. * * @param mixed $value The new value. * @return bool True if the attribute was updated, false otherwise. @@ -400,21 +464,21 @@ public function update_name( $value ) { } /** - * Update the User-Description. - * - * @param mixed $value The new value. - * @return bool True if the attribute was updated, false otherwise. - */ + * Update the User description. + * + * @param mixed $value The new value. + * @return bool True if the attribute was updated, false otherwise. + */ public function update_summary( $value ) { return \update_option( 'blogdescription', $value ); } /** - * Update the User-Icon. - * - * @param mixed $value The new value. - * @return bool True if the attribute was updated, false otherwise. - */ + * Update the User icon. + * + * @param mixed $value The new value. + * @return bool True if the attribute was updated, false otherwise. + */ public function update_icon( $value ) { if ( ! wp_attachment_is_image( $value ) ) { return false; @@ -423,11 +487,11 @@ public function update_icon( $value ) { } /** - * Update the User-Header-Image. - * - * @param mixed $value The new value. - * @return bool True if the attribute was updated, false otherwise. - */ + * Update the User-Header-Image. + * + * @param mixed $value The new value. + * @return bool True if the attribute was updated, false otherwise. + */ public function update_header( $value ) { if ( ! wp_attachment_is_image( $value ) ) { return false; @@ -436,11 +500,11 @@ public function update_header( $value ) { } /** - * Get the User - Hashtags . + * Get the User - Hashtags. * * @see https://docs.joinmastodon.org/spec/activitypub/#Hashtag * - * @return array The User - Hashtags . + * @return array The User - Hashtags. */ public function get_tag() { $hashtags = array(); @@ -473,4 +537,13 @@ public function get_attachment() { $extra_fields = Extra_Fields::get_actor_fields( $this->_id ); return Extra_Fields::fields_to_attachments( $extra_fields ); } + + /** + * Returns the website hosts allowed to credit this blog. + * + * @return array|null The attribution domains or null if not found. + */ + public function get_attribution_domains() { + return get_attribution_domains(); + } } diff --git a/includes/model/class-follower.php b/includes/model/class-follower.php index 4590ea49f..45ef6157b 100644 --- a/includes/model/class-follower.php +++ b/includes/model/class-follower.php @@ -1,13 +1,18 @@ _id, 'activitypub_errors' ); + return get_post_meta( $this->_id, 'activitypub_errors', false ); } /** * Get the Summary. * - * @return int The Summary. + * @return string The Summary. */ public function get_summary() { if ( isset( $this->summary ) ) { @@ -51,7 +56,7 @@ public function get_summary() { * Getter for URL attribute. * * Falls back to ID, if no URL is set. This is relevant for - * Plattforms like Lemmy, where the ID is the URL. + * Platforms like Lemmy, where the ID is the URL. * * @return string The URL. */ @@ -65,8 +70,6 @@ public function get_url() { /** * Reset (delete) all errors. - * - * @return void */ public function reset_errors() { delete_post_meta( $this->_id, 'activitypub_errors' ); @@ -103,21 +106,19 @@ public function get_latest_error_message() { } /** - * Update the current Follower-Object. - * - * @return void + * Update the current Follower object. */ public function update() { $this->save(); } /** - * Validate the current Follower-Object. + * Validate the current Follower object. * * @return boolean True if the verification was successful. */ public function is_valid() { - // the minimum required attributes + // The minimum required attributes. $required_attributes = array( 'id', 'preferredUsername', @@ -136,9 +137,9 @@ public function is_valid() { } /** - * Save the current Follower-Object. + * Save the current Follower object. * - * @return int|WP_Error The Post-ID or an WP_Error. + * @return int|WP_Error The post ID or an WP_Error. */ public function save() { if ( ! $this->is_valid() ) { @@ -148,7 +149,7 @@ public function save() { if ( ! $this->get__id() ) { global $wpdb; - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching + // phpcs:ignore WordPress.DB.DirectDatabaseQuery $post_id = $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM $wpdb->posts WHERE guid=%s", @@ -177,37 +178,35 @@ public function save() { ); if ( ! empty( $post_id ) ) { - // If this is an update, prevent the "followed" date from being - // overwritten by the current date. + // If this is an update, prevent the "followed" date from being overwritten by the current date. $post = get_post( $post_id ); $args['post_date'] = $post->post_date; $args['post_date_gmt'] = $post->post_date_gmt; } - $post_id = wp_insert_post( $args ); + $post_id = wp_insert_post( $args ); $this->_id = $post_id; return $post_id; } /** - * Upsert the current Follower-Object. + * Upsert the current Follower object. * - * @return int|WP_Error The Post-ID or an WP_Error. + * @return int|WP_Error The post ID or an WP_Error. */ public function upsert() { return $this->save(); } /** - * Delete the current Follower-Object. + * Delete the current Follower object. * * Beware that this os deleting a Follower for ALL users!!! * * To delete only the User connection (unfollow) - * @see \Activitypub\Rest\Followers::remove_follower() * - * @return void + * @see \Activitypub\Rest\Followers::remove_follower() */ public function delete() { wp_delete_post( $this->_id ); @@ -215,12 +214,10 @@ public function delete() { /** * Update the post meta. - * - * @return void */ protected function get_post_meta_input() { - $meta_input = array(); - $meta_input['activitypub_inbox'] = $this->get_shared_inbox(); + $meta_input = array(); + $meta_input['activitypub_inbox'] = $this->get_shared_inbox(); $meta_input['activitypub_actor_json'] = $this->to_json(); return $meta_input; @@ -239,9 +236,9 @@ public function get_icon() { } return array( - 'type' => 'Image', + 'type' => 'Image', 'mediaType' => 'image/jpeg', - 'url' => ACTIVITYPUB_PLUGIN_URL . 'assets/img/mp.jpg', + 'url' => ACTIVITYPUB_PLUGIN_URL . 'assets/img/mp.jpg', ); } @@ -278,7 +275,7 @@ public function get_preferred_username() { } /** - * Get the Icon URL (Avatar) + * Get the Icon URL (Avatar). * * @return string The URL to the Avatar. */ @@ -297,7 +294,7 @@ public function get_icon_url() { } /** - * Get the Icon URL (Avatar) + * Get the Icon URL (Avatar). * * @return string The URL to the Avatar. */ @@ -333,13 +330,12 @@ public function get_shared_inbox() { /** * Convert a Custom-Post-Type input to an Activitypub\Model\Follower. * - * @return string The JSON string. - * - * @return array Activitypub\Model\Follower + * @param \WP_Post $post The post object. + * @return \Activitypub\Activity\Base_Object|WP_Error */ public static function init_from_cpt( $post ) { $actor_json = get_post_meta( $post->ID, 'activitypub_actor_json', true ); - $object = self::init_from_json( $actor_json ); + $object = self::init_from_json( $actor_json ); $object->set__id( $post->ID ); $object->set_id( $post->guid ); $object->set_name( $post->post_title ); @@ -370,10 +366,10 @@ protected function extract_name_from_uri() { if ( $path ) { if ( \strpos( $name, '@' ) !== false ) { - // expected: https://example.com/@user (default URL pattern) + // Expected: https://example.com/@user (default URL pattern). $name = \preg_replace( '|^/@?|', '', $path ); } else { - // expected: https://example.com/users/user (default ID pattern) + // Expected: https://example.com/users/user (default ID pattern). $parts = \explode( '/', $path ); $name = \array_pop( $parts ); } @@ -383,7 +379,7 @@ protected function extract_name_from_uri() { \strpos( $name, 'acct' ) === 0 || \strpos( $name, '@' ) === 0 ) { - // expected: user@example.com or acct:user@example (WebFinger) + // Expected: user@example.com or acct:user@example (WebFinger). $name = \ltrim( $name, '@' ); $name = \ltrim( $name, 'acct:' ); $parts = \explode( '@', $name ); diff --git a/includes/model/class-user.php b/includes/model/class-user.php index c90e1b349..a5c938b47 100644 --- a/includes/model/class-user.php +++ b/includes/model/class-user.php @@ -1,19 +1,25 @@ + * @var string */ protected $webfinger; + /** + * The type of the object. + * + * @return string The type of the object. + */ public function get_type() { return 'Person'; } + /** + * Generate a User object from a WP_User. + * + * @param int $user_id The user ID. + * + * @return WP_Error|User The User object or WP_Error if user not found. + */ public static function from_wp_user( $user_id ) { if ( is_user_disabled( $user_id ) ) { return new WP_Error( @@ -76,34 +94,40 @@ public static function from_wp_user( $user_id ) { ); } - $object = new static(); + $object = new static(); $object->_id = $user_id; return $object; } /** - * Get the User-ID. + * Get the user ID. * - * @return string The User-ID. + * @return string The user ID. */ public function get_id() { - return $this->get_url(); + $permalink = \get_user_option( 'activitypub_use_permalink_as_id', $this->_id ); + + if ( '1' === $permalink ) { + return $this->get_url(); + } + + return \add_query_arg( 'author', $this->_id, \trailingslashit( \home_url() ) ); } /** - * Get the User-Name. + * Get the Username. * - * @return string The User-Name. + * @return string The Username. */ public function get_name() { return \esc_attr( \get_the_author_meta( 'display_name', $this->_id ) ); } /** - * Get the User-Description. + * Get the User description. * - * @return string The User-Description. + * @return string The User description. */ public function get_summary() { $description = get_user_option( 'activitypub_description', $this->_id ); @@ -114,27 +138,37 @@ public function get_summary() { } /** - * Get the User-Url. + * Get the User url. * - * @return string The User-Url. + * @return string The User url. */ public function get_url() { return \esc_url( \get_author_posts_url( $this->_id ) ); } /** - * Returns the User-URL with @-Prefix for the username. + * Returns the User URL with @-Prefix for the username. * - * @return string The User-URL with @-Prefix for the username. + * @return string The User URL with @-Prefix for the username. */ public function get_alternate_url() { return \esc_url( \trailingslashit( get_home_url() ) . '@' . $this->get_preferred_username() ); } + /** + * Get the preferred username. + * + * @return string The preferred username. + */ public function get_preferred_username() { return \esc_attr( \get_the_author_meta( 'login', $this->_id ) ); } + /** + * Get the User icon. + * + * @return array The User icon. + */ public function get_icon() { $icon = \get_user_option( 'activitypub_icon', $this->_id ); if ( wp_attachment_is_image( $icon ) ) { @@ -157,6 +191,11 @@ public function get_icon() { ); } + /** + * Returns the header image. + * + * @return array|null The header image. + */ public function get_image() { $header_image = get_user_option( 'activitypub_header_image', $this->_id ); $image_url = null; @@ -179,14 +218,24 @@ public function get_image() { return null; } + /** + * Returns the date the user was created. + * + * @return false|string The date the user was created. + */ public function get_published() { return \gmdate( 'Y-m-d\TH:i:s\Z', \strtotime( \get_the_author_meta( 'registered', $this->_id ) ) ); } + /** + * Returns the public key. + * + * @return array The public key. + */ public function get_public_key() { return array( - 'id' => $this->get_id() . '#main-key', - 'owner' => $this->get_id(), + 'id' => $this->get_id() . '#main-key', + 'owner' => $this->get_id(), 'publicKeyPem' => Signature::get_public_key_for( $this->get__id() ), ); } @@ -236,6 +285,11 @@ public function get_featured() { return get_rest_url_by_path( sprintf( 'actors/%d/collections/featured', $this->get__id() ) ); } + /** + * Returns the endpoints. + * + * @return array|null The endpoints. + */ public function get_endpoints() { $endpoints = null; @@ -267,18 +321,38 @@ public function get_webfinger() { return $this->get_preferred_username() . '@' . \wp_parse_url( \home_url(), \PHP_URL_HOST ); } + /** + * Returns the canonical URL. + * + * @return string The canonical URL. + */ public function get_canonical_url() { return $this->get_url(); } + /** + * Returns the streams. + * + * @return null The streams. + */ public function get_streams() { return null; } + /** + * Returns the tag. + * + * @return array The tag. + */ public function get_tag() { return array(); } + /** + * Returns the indexable state. + * + * @return bool Whether the user is indexable. + */ public function get_indexable() { if ( is_blog_public() ) { return true; @@ -287,22 +361,24 @@ public function get_indexable() { } } - /** - * Update the User-Name. + * Update the username. * - * @param mixed $value The new value. - * @return bool True if the attribute was updated, false otherwise. + * @param string $value The new value. + * @return int|WP_Error The updated user ID or WP_Error on failure. */ public function update_name( $value ) { - $userdata = [ 'ID' => $this->_id, 'display_name' => $value ]; + $userdata = array( + 'ID' => $this->_id, + 'display_name' => $value, + ); return \wp_update_user( $userdata ); } /** - * Update the User-Description. + * Update the User description. * - * @param mixed $value The new value. + * @param string $value The new value. * @return bool True if the attribute was updated, false otherwise. */ public function update_summary( $value ) { @@ -310,9 +386,9 @@ public function update_summary( $value ) { } /** - * Update the User-Icon. + * Update the User icon. * - * @param mixed $value The new value. Should be an attachment ID. + * @param int $value The new value. Should be an attachment ID. * @return bool True if the attribute was updated, false otherwise. */ public function update_icon( $value ) { @@ -325,7 +401,7 @@ public function update_icon( $value ) { /** * Update the User-Header-Image. * - * @param mixed $value The new value. Should be an attachment ID. + * @param int $value The new value. Should be an attachment ID. * @return bool True if the attribute was updated, false otherwise. */ public function update_header( $value ) { @@ -334,4 +410,13 @@ public function update_header( $value ) { } return \update_user_option( $this->_id, 'activitypub_header_image', $value ); } + + /** + * Returns the website hosts allowed to credit this blog. + * + * @return array|null The attribution domains or null if not found. + */ + public function get_attribution_domains() { + return get_attribution_domains(); + } } diff --git a/includes/rest/class-actors.php b/includes/rest/class-actors.php index a21a7e5de..60f03d298 100644 --- a/includes/rest/class-actors.php +++ b/includes/rest/class-actors.php @@ -1,18 +1,22 @@ get_param( 'user_id' ); @@ -79,15 +83,15 @@ public static function get( $request ) { $link_header = sprintf( '<%1$s>; rel="alternate"; type="application/activity+json"', $user->get_id() ); - // redirect to canonical URL if it is not an ActivityPub request + // Redirect to canonical URL if it is not an ActivityPub request. if ( ! is_activitypub_request() ) { header( 'Link: ' . $link_header ); header( 'Location: ' . $user->get_canonical_url(), true, 301 ); exit; } - /* - * Action triggerd prior to the ActivityPub profile being created and sent to the client + /** + * Action triggered prior to the ActivityPub profile being created and sent to the client. */ \do_action( 'activitypub_rest_users_pre' ); @@ -102,11 +106,11 @@ public static function get( $request ) { /** - * Endpoint for remote follow UI/Block + * Endpoint for remote follow UI/Block. * * @param WP_REST_Request $request The request object. * - * @return void|string The URL to the remote follow page + * @return WP_REST_Response|\WP_Error The response object or WP_Error. */ public static function remote_follow_get( WP_REST_Request $request ) { $resource = $request->get_param( 'resource' ); @@ -127,15 +131,18 @@ public static function remote_follow_get( WP_REST_Request $request ) { $url = str_replace( '{uri}', $resource, $template ); return new WP_REST_Response( - array( 'url' => $url, 'template' => $template ), + array( + 'url' => $url, + 'template' => $template, + ), 200 ); } /** - * The supported parameters + * The supported parameters. * - * @return array list of parameters + * @return array List of parameters, */ public static function request_parameters() { $params = array(); diff --git a/includes/rest/class-collection.php b/includes/rest/class-collection.php index aa7a3bbf6..06f2203b8 100644 --- a/includes/rest/class-collection.php +++ b/includes/rest/class-collection.php @@ -1,4 +1,10 @@ get_param( 'type' ); @@ -144,9 +150,9 @@ public static function replies_get( $request ) { /** * The Featured Tags endpoint * - * @param WP_REST_Request $request The request object. + * @param \WP_REST_Request $request The request object. * - * @return WP_REST_Response The response object. + * @return WP_REST_Response|\WP_Error The response object or WP_Error. */ public static function tags_get( $request ) { $user_id = $request->get_param( 'user_id' ); @@ -196,9 +202,9 @@ public static function tags_get( $request ) { /** * Featured posts endpoint * - * @param WP_REST_Request $request The request object. + * @param \WP_REST_Request $request The request object. * - * @return WP_REST_Response The response object. + * @return WP_REST_Response|\WP_Error The response object or WP_Error. */ public static function featured_get( $request ) { $user_id = $request->get_param( 'user_id' ); @@ -212,7 +218,7 @@ public static function featured_get( $request ) { if ( ! is_single_user() && User_Collection::BLOG_USER_ID === $user->get__id() ) { $posts = array(); - } elseif ( $sticky_posts ) { + } elseif ( is_array( $sticky_posts ) ) { $args = array( 'post__in' => $sticky_posts, 'ignore_sticky_posts' => 1, @@ -254,13 +260,11 @@ public static function featured_get( $request ) { } /** - * Moderators endpoint - * - * @param WP_REST_Request $request The request object. + * Moderators endpoint. * * @return WP_REST_Response The response object. */ - public static function moderators_get( $request ) { + public static function moderators_get() { $response = array( '@context' => Actor::JSON_LD_CONTEXT, 'id' => get_rest_url_by_path( 'collections/moderators' ), @@ -271,7 +275,7 @@ public static function moderators_get( $request ) { $users = User_Collection::get_collection(); foreach ( $users as $user ) { - $response['orderedItems'][] = $user->get_url(); + $response['orderedItems'][] = $user->get_id(); } $rest_response = new WP_REST_Response( $response, 200 ); @@ -281,9 +285,9 @@ public static function moderators_get( $request ) { } /** - * The supported parameters + * The supported parameters. * - * @return array list of parameters + * @return array List of parameters. */ public static function request_parameters() { $params = array(); @@ -297,9 +301,9 @@ public static function request_parameters() { } /** - * The supported parameters + * The supported parameters. * - * @return array list of parameters + * @return array list of parameters. */ public static function request_parameters_for_replies() { $params = array(); diff --git a/includes/rest/class-comment.php b/includes/rest/class-comment.php index c9f911b46..095c85892 100644 --- a/includes/rest/class-comment.php +++ b/includes/rest/class-comment.php @@ -1,4 +1,10 @@ get_param( 'resource' ); @@ -84,7 +90,10 @@ public static function remote_reply_get( WP_REST_Request $request ) { $url = str_replace( '{uri}', $resource, $template ); return new WP_REST_Response( - array( 'url' => $url, 'template' => $template ), + array( + 'url' => $url, + 'template' => $template, + ), 200 ); } diff --git a/includes/rest/class-followers.php b/includes/rest/class-followers.php index ca882cf3e..1b38187f3 100644 --- a/includes/rest/class-followers.php +++ b/includes/rest/class-followers.php @@ -1,7 +1,12 @@ get_param( 'user_id' ); @@ -64,45 +69,44 @@ public static function get( $request ) { $page = (int) $request->get_param( 'page' ); $context = $request->get_param( 'context' ); - /* - * Action triggerd prior to the ActivityPub profile being created and sent to the client + /** + * Action triggered prior to the ActivityPub profile being created and sent to the client */ \do_action( 'activitypub_rest_followers_pre' ); $data = Follower_Collection::get_followers_with_count( $user_id, $per_page, $page, array( 'order' => ucwords( $order ) ) ); $json = new stdClass(); + // phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase $json->{'@context'} = \Activitypub\get_context(); - - $json->id = get_rest_url_by_path( sprintf( 'actors/%d/followers', $user->get__id() ) ); - $json->generator = 'http://wordpress.org/?v=' . get_masked_wp_version(); - $json->actor = $user->get_id(); - $json->type = 'OrderedCollectionPage'; - - $json->totalItems = $data['total']; // phpcs:ignore - $json->partOf = get_rest_url_by_path( sprintf( 'actors/%d/followers', $user->get__id() ) ); // phpcs:ignore - - $json->first = \add_query_arg( 'page', 1, $json->partOf ); // phpcs:ignore - $json->last = \add_query_arg( 'page', \ceil ( $json->totalItems / $per_page ), $json->partOf ); // phpcs:ignore - - if ( $page && ( ( \ceil ( $json->totalItems / $per_page ) ) > $page ) ) { // phpcs:ignore - $json->next = \add_query_arg( 'page', $page + 1, $json->partOf ); // phpcs:ignore + $json->id = get_rest_url_by_path( sprintf( 'actors/%d/followers', $user->get__id() ) ); + $json->generator = 'http://wordpress.org/?v=' . get_masked_wp_version(); + $json->actor = $user->get_id(); + $json->type = 'OrderedCollectionPage'; + $json->totalItems = $data['total']; + $json->partOf = get_rest_url_by_path( sprintf( 'actors/%d/followers', $user->get__id() ) ); + + $json->first = \add_query_arg( 'page', 1, $json->partOf ); + $json->last = \add_query_arg( 'page', \ceil( $json->totalItems / $per_page ), $json->partOf ); + + if ( $page && ( ( \ceil( $json->totalItems / $per_page ) ) > $page ) ) { + $json->next = \add_query_arg( 'page', $page + 1, $json->partOf ); } - if ( $page && ( $page > 1 ) ) { // phpcs:ignore - $json->prev = \add_query_arg( 'page', $page - 1, $json->partOf ); // phpcs:ignore + if ( $page && ( $page > 1 ) ) { + $json->prev = \add_query_arg( 'page', $page - 1, $json->partOf ); } - // phpcs:ignore $json->orderedItems = array_map( function ( $item ) use ( $context ) { if ( 'full' === $context ) { return $item->to_array( false ); } - return $item->get_url(); + return $item->get_id(); }, $data['followers'] ); + // phpcs:enable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase $rest_response = new WP_REST_Response( $json, 200 ); $rest_response->header( 'Content-Type', 'application/activity+json; charset=' . get_option( 'blog_charset' ) ); @@ -111,20 +115,20 @@ function ( $item ) use ( $context ) { } /** - * The supported parameters + * The supported parameters. * - * @return array list of parameters + * @return array List of parameters. */ public static function request_parameters() { $params = array(); $params['page'] = array( - 'type' => 'integer', + 'type' => 'integer', 'default' => 1, ); $params['per_page'] = array( - 'type' => 'integer', + 'type' => 'integer', 'default' => 20, ); @@ -136,13 +140,13 @@ public static function request_parameters() { $params['user_id'] = array( 'required' => true, - 'type' => 'string', + 'type' => 'string', ); $params['context'] = array( - 'type' => 'string', + 'type' => 'string', 'default' => 'simple', - 'enum' => array( 'simple', 'full' ), + 'enum' => array( 'simple', 'full' ), ); return $params; diff --git a/includes/rest/class-following.php b/includes/rest/class-following.php index 4e077279c..cda058962 100644 --- a/includes/rest/class-following.php +++ b/includes/rest/class-following.php @@ -1,4 +1,10 @@ get_param( 'user_id' ); @@ -58,8 +64,8 @@ public static function get( $request ) { return $user; } - /* - * Action triggerd prior to the ActivityPub profile being created and sent to the client + /** + * Action triggered prior to the ActivityPub profile being created and sent to the client. */ \do_action( 'activitypub_rest_following_pre' ); @@ -67,19 +73,25 @@ public static function get( $request ) { $json->{'@context'} = \Activitypub\get_context(); - $json->id = get_rest_url_by_path( sprintf( 'actors/%d/following', $user->get__id() ) ); + // phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + $json->id = get_rest_url_by_path( sprintf( 'actors/%d/following', $user->get__id() ) ); $json->generator = 'http://wordpress.org/?v=' . get_masked_wp_version(); - $json->actor = $user->get_id(); - $json->type = 'OrderedCollectionPage'; - - $json->partOf = get_rest_url_by_path( sprintf( 'actors/%d/following', $user->get__id() ) ); // phpcs:ignore - - $items = apply_filters( 'activitypub_rest_following', array(), $user ); // phpcs:ignore - - $json->totalItems = is_countable( $items ) ? count( $items ) : 0; // phpcs:ignore - $json->orderedItems = $items; // phpcs:ignore + $json->actor = $user->get_id(); + $json->type = 'OrderedCollectionPage'; + $json->partOf = get_rest_url_by_path( sprintf( 'actors/%d/following', $user->get__id() ) ); + + /** + * Filter the list of following urls. + * + * @param array $items The array of following urls. + * @param \Activitypub\Model\User $user The user object. + */ + $items = apply_filters( 'activitypub_rest_following', array(), $user ); - $json->first = $json->partOf; // phpcs:ignore + $json->totalItems = is_countable( $items ) ? count( $items ) : 0; + $json->orderedItems = $items; + $json->first = $json->partOf; + // phpcs:enable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase $rest_response = new WP_REST_Response( $json, 200 ); $rest_response->header( 'Content-Type', 'application/activity+json; charset=' . get_option( 'blog_charset' ) ); @@ -88,9 +100,9 @@ public static function get( $request ) { } /** - * The supported parameters + * The supported parameters. * - * @return array list of parameters + * @return array List of parameters. */ public static function request_parameters() { $params = array(); @@ -101,7 +113,7 @@ public static function request_parameters() { $params['user_id'] = array( 'required' => true, - 'type' => 'string', + 'type' => 'string', ); return $params; @@ -111,22 +123,22 @@ public static function request_parameters() { * Add the Blog Authors to the following list of the Blog Actor * if Blog not in single mode. * - * @param array $array The array of following urls. - * @param User $user The user object. + * @param array $follow_list The array of following urls. + * @param \Activitypub\Model\User $user The user object. * * @return array The array of following urls. */ - public static function default_following( $array, $user ) { + public static function default_following( $follow_list, $user ) { if ( 0 !== $user->get__id() || is_single_user() ) { - return $array; + return $follow_list; } $users = User_Collection::get_collection(); foreach ( $users as $user ) { - $array[] = $user->get_url(); + $follow_list[] = $user->get_id(); } - return $array; + return $follow_list; } } diff --git a/includes/rest/class-inbox.php b/includes/rest/class-inbox.php index a5a7e03c5..b5f5fa9b1 100644 --- a/includes/rest/class-inbox.php +++ b/includes/rest/class-inbox.php @@ -1,21 +1,25 @@ get_param( 'user_id' ); @@ -80,29 +84,33 @@ public static function user_inbox_get( $request ) { return $user; } - $page = $request->get_param( 'page', 0 ); - - /* - * Action triggerd prior to the ActivityPub profile being created and sent to the client + /** + * Action triggered prior to the ActivityPub profile being created and sent to the client. */ \do_action( 'activitypub_rest_inbox_pre' ); $json = new \stdClass(); + // phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase $json->{'@context'} = get_context(); - $json->id = get_rest_url_by_path( sprintf( 'actors/%d/inbox', $user->get__id() ) ); - $json->generator = 'http://wordpress.org/?v=' . get_masked_wp_version(); - $json->type = 'OrderedCollectionPage'; - $json->partOf = get_rest_url_by_path( sprintf( 'actors/%d/inbox', $user->get__id() ) ); // phpcs:ignore - $json->totalItems = 0; // phpcs:ignore - $json->orderedItems = array(); // phpcs:ignore - $json->first = $json->partOf; // phpcs:ignore - - // filter output + $json->id = get_rest_url_by_path( sprintf( 'actors/%d/inbox', $user->get__id() ) ); + $json->generator = 'http://wordpress.org/?v=' . get_masked_wp_version(); + $json->type = 'OrderedCollectionPage'; + $json->partOf = get_rest_url_by_path( sprintf( 'actors/%d/inbox', $user->get__id() ) ); + $json->totalItems = 0; + $json->orderedItems = array(); + $json->first = $json->partOf; + // phpcs:enable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + + /** + * Filter the ActivityPub inbox array. + * + * @param array $json The ActivityPub inbox array. + */ $json = \apply_filters( 'activitypub_rest_inbox_array', $json ); - /* - * Action triggerd after the ActivityPub profile has been created and sent to the client + /** + * Action triggered after the ActivityPub profile has been created and sent to the client. */ \do_action( 'activitypub_inbox_post' ); @@ -113,11 +121,11 @@ public static function user_inbox_get( $request ) { } /** - * Handles user-inbox requests + * Handles user-inbox requests. * - * @param WP_REST_Request $request + * @param \WP_REST_Request $request The request object. * - * @return WP_REST_Response + * @return WP_REST_Response|\WP_Error The response object or WP_Error. */ public static function user_inbox_post( $request ) { $user_id = $request->get_param( 'user_id' ); @@ -132,7 +140,23 @@ public static function user_inbox_post( $request ) { $type = $request->get_param( 'type' ); $type = \strtolower( $type ); + /** + * ActivityPub inbox action. + * + * @param array $data The data array. + * @param int|null $user_id The user ID. + * @param string $type The type of the activity. + * @param Activity $activity The Activity object. + */ \do_action( 'activitypub_inbox', $data, $user->get__id(), $type, $activity ); + + /** + * ActivityPub inbox action for specific activity types. + * + * @param array $data The data array. + * @param int|null $user_id The user ID. + * @param Activity $activity The Activity object. + */ \do_action( "activitypub_inbox_{$type}", $data, $user->get__id(), $activity ); $rest_response = new WP_REST_Response( array(), 202 ); @@ -142,9 +166,9 @@ public static function user_inbox_post( $request ) { } /** - * The shared inbox + * The shared inbox. * - * @param WP_REST_Request $request + * @param \WP_REST_Request $request The request object. * * @return WP_REST_Response */ @@ -154,7 +178,23 @@ public static function shared_inbox_post( $request ) { $type = $request->get_param( 'type' ); $type = \strtolower( $type ); + /** + * ActivityPub inbox action. + * + * @param array $data The data array. + * @param int|null $user_id The user ID. + * @param string $type The type of the activity. + * @param Activity $activity The Activity object. + */ \do_action( 'activitypub_inbox', $data, null, $type, $activity ); + + /** + * ActivityPub inbox action for specific activity types. + * + * @param array $data The data array. + * @param int|null $user_id The user ID. + * @param Activity $activity The Activity object. + */ \do_action( "activitypub_inbox_{$type}", $data, null, $activity ); $rest_response = new WP_REST_Response( array(), 202 ); @@ -164,9 +204,9 @@ public static function shared_inbox_post( $request ) { } /** - * The supported parameters + * The supported parameters. * - * @return array list of parameters + * @return array List of parameters. */ public static function user_inbox_get_parameters() { $params = array(); @@ -177,36 +217,33 @@ public static function user_inbox_get_parameters() { $params['user_id'] = array( 'required' => true, - 'type' => 'string', + 'type' => 'string', ); return $params; } /** - * The supported parameters + * The supported parameters. * - * @return array list of parameters + * @return array List of parameters. */ public static function user_inbox_post_parameters() { $params = array(); $params['user_id'] = array( 'required' => true, - 'type' => 'string', + 'type' => 'string', ); $params['id'] = array( - 'required' => true, + 'required' => true, 'sanitize_callback' => 'esc_url_raw', ); $params['actor'] = array( - 'required' => true, - // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed, VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable - 'sanitize_callback' => function ( $param, $request, $key ) { - return object_to_uri( $param ); - }, + 'required' => true, + 'sanitize_callback' => '\Activitypub\object_to_uri', ); $params['type'] = array( @@ -214,8 +251,16 @@ public static function user_inbox_post_parameters() { ); $params['object'] = array( - 'required' => true, + 'required' => true, 'validate_callback' => function ( $param, $request, $key ) { + /** + * Filter the ActivityPub object validation. + * + * @param bool $validate The validation result. + * @param array $param The object data. + * @param object $request The request object. + * @param string $key The key. + */ return apply_filters( 'activitypub_validate_object', true, $param, $request, $key ); }, ); @@ -224,17 +269,19 @@ public static function user_inbox_post_parameters() { } /** - * The supported parameters + * The supported parameters. * - * @return array list of parameters + * @return array List of parameters. */ public static function shared_inbox_post_parameters() { $params = self::user_inbox_post_parameters(); + // A shared Inbox does not need a User-ID. + unset( $params['user_id'] ); + $params['to'] = array( - 'required' => false, - // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed, VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable - 'sanitize_callback' => function ( $param, $request, $key ) { + 'required' => false, + 'sanitize_callback' => function ( $param ) { if ( \is_string( $param ) ) { $param = array( $param ); } @@ -244,8 +291,7 @@ public static function shared_inbox_post_parameters() { ); $params['cc'] = array( - // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed, VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable - 'sanitize_callback' => function ( $param, $request, $key ) { + 'sanitize_callback' => function ( $param ) { if ( \is_string( $param ) ) { $param = array( $param ); } @@ -255,8 +301,7 @@ public static function shared_inbox_post_parameters() { ); $params['bcc'] = array( - // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed, VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable - 'sanitize_callback' => function ( $param, $request, $key ) { + 'sanitize_callback' => function ( $param ) { if ( \is_string( $param ) ) { $param = array( $param ); } @@ -269,15 +314,15 @@ public static function shared_inbox_post_parameters() { } /** - * Get local user recipients + * Get local user recipients. * - * @param array $data + * @param array $data The data array. * - * @return array The list of local users + * @return array The list of local users. */ public static function get_recipients( $data ) { $recipients = extract_recipients_from_activity( $data ); - $users = array(); + $users = array(); foreach ( $recipients as $recipient ) { $user_id = url_to_authorid( $recipient ); diff --git a/includes/rest/class-interaction.php b/includes/rest/class-interaction.php index 011f13030..8bf78267e 100644 --- a/includes/rest/class-interaction.php +++ b/includes/rest/class-interaction.php @@ -1,12 +1,21 @@ get_param( 'uri' ); @@ -78,10 +87,16 @@ public static function get( $request ) { $redirect_url = \apply_filters( 'activitypub_interactions_reply_url', $redirect_url, $uri, $object ); } - // generic Interaction hook + /** + * Filter the redirect URL. + * + * @param string $redirect_url The URL to redirect to. + * @param string $uri The URI of the object. + * @param array $object The object. + */ $redirect_url = \apply_filters( 'activitypub_interactions_url', $redirect_url, $uri, $object ); - // check if hook is implemented + // Check if hook is implemented. if ( ! $redirect_url ) { \wp_die( esc_html__( diff --git a/includes/rest/class-nodeinfo.php b/includes/rest/class-nodeinfo.php index 02b89b6c6..3eb5fc007 100644 --- a/includes/rest/class-nodeinfo.php +++ b/includes/rest/class-nodeinfo.php @@ -1,4 +1,10 @@ 'wordpress', + 'name' => 'wordpress', 'version' => get_masked_wp_version(), ); - $posts = \wp_count_posts(); + $posts = \wp_count_posts(); $comments = \wp_count_comments(); $nodeinfo['usage'] = array( - 'users' => array( + 'users' => array( 'total' => get_total_users(), 'activeMonth' => get_active_users( '1 month ago' ), 'activeHalfyear' => get_active_users( '6 month ago' ), ), - 'localPosts' => (int) $posts->publish, + 'localPosts' => (int) $posts->publish, 'localComments' => (int) $comments->approved, ); $nodeinfo['openRegistrations'] = false; - $nodeinfo['protocols'] = array( 'activitypub' ); + $nodeinfo['protocols'] = array( 'activitypub' ); $nodeinfo['services'] = array( - 'inbound' => array(), + 'inbound' => array(), 'outbound' => array(), ); $nodeinfo['metadata'] = array( - 'nodeName' => \get_bloginfo( 'name' ), + 'nodeName' => \get_bloginfo( 'name' ), 'nodeDescription' => \get_bloginfo( 'description' ), - 'nodeIcon' => \get_site_icon_url(), + 'nodeIcon' => \get_site_icon_url(), ); return new WP_REST_Response( $nodeinfo, 200 ); } /** - * Render NodeInfo file - * - * @param WP_REST_Request $request + * Render NodeInfo file. * - * @return WP_REST_Response + * @return WP_REST_Response The JSON profile of the NodeInfo. */ - public static function nodeinfo2( $request ) { - /* - * Action triggerd prior to the ActivityPub profile being created and sent to the client + public static function nodeinfo2() { + /** + * Action triggered prior to the ActivityPub profile being created and sent to the client. */ \do_action( 'activitypub_rest_nodeinfo2_pre' ); $nodeinfo = array(); $nodeinfo['version'] = '2.0'; - $nodeinfo['server'] = array( - 'baseUrl' => \home_url( '/' ), - 'name' => \get_bloginfo( 'name' ), + $nodeinfo['server'] = array( + 'baseUrl' => \home_url( '/' ), + 'name' => \get_bloginfo( 'name' ), 'software' => 'wordpress', - 'version' => get_masked_wp_version(), + 'version' => get_masked_wp_version(), ); - $posts = \wp_count_posts(); + $posts = \wp_count_posts(); $comments = \wp_count_comments(); $nodeinfo['usage'] = array( - 'users' => array( + 'users' => array( 'total' => get_total_users(), 'activeMonth' => get_active_users( 1 ), 'activeHalfyear' => get_active_users( 6 ), ), - 'localPosts' => (int) $posts->publish, + 'localPosts' => (int) $posts->publish, 'localComments' => (int) $comments->approved, ); $nodeinfo['openRegistrations'] = false; - $nodeinfo['protocols'] = array( 'activitypub' ); + $nodeinfo['protocols'] = array( 'activitypub' ); $nodeinfo['services'] = array( - 'inbound' => array(), + 'inbound' => array(), 'outbound' => array(), ); @@ -163,21 +165,19 @@ public static function nodeinfo2( $request ) { } /** - * Render NodeInfo discovery file - * - * @param WP_REST_Request $request + * Render NodeInfo discovery file. * * @return WP_REST_Response */ - public static function discovery( $request ) { - $discovery = array(); + public static function discovery() { + $discovery = array(); $discovery['links'] = array( array( - 'rel' => 'http://nodeinfo.diaspora.software/ns/schema/2.0', + 'rel' => 'http://nodeinfo.diaspora.software/ns/schema/2.0', 'href' => get_rest_url_by_path( 'nodeinfo' ), ), array( - 'rel' => 'https://www.w3.org/ns/activitystreams#Application', + 'rel' => 'https://www.w3.org/ns/activitystreams#Application', 'href' => get_rest_url_by_path( 'application' ), ), ); diff --git a/includes/rest/class-outbox.php b/includes/rest/class-outbox.php index e0670301c..461d42861 100644 --- a/includes/rest/class-outbox.php +++ b/includes/rest/class-outbox.php @@ -1,8 +1,13 @@ get_param( 'user_id' ); @@ -64,41 +69,43 @@ public static function user_outbox_get( $request ) { $page = $request->get_param( 'page', 1 ); - /* - * Action triggerd prior to the ActivityPub profile being created and sent to the client + /** + * Action triggered prior to the ActivityPub profile being created and sent to the client. */ \do_action( 'activitypub_rest_outbox_pre' ); $json = new stdClass(); + // phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase $json->{'@context'} = get_context(); - $json->id = get_rest_url_by_path( sprintf( 'actors/%d/outbox', $user_id ) ); - $json->generator = 'http://wordpress.org/?v=' . get_masked_wp_version(); - $json->actor = $user->get_id(); - $json->type = 'OrderedCollectionPage'; - $json->partOf = get_rest_url_by_path( sprintf( 'actors/%d/outbox', $user_id ) ); // phpcs:ignore - $json->totalItems = 0; // phpcs:ignore + $json->id = get_rest_url_by_path( sprintf( 'actors/%d/outbox', $user_id ) ); + $json->generator = 'http://wordpress.org/?v=' . get_masked_wp_version(); + $json->actor = $user->get_id(); + $json->type = 'OrderedCollectionPage'; + $json->partOf = get_rest_url_by_path( sprintf( 'actors/%d/outbox', $user_id ) ); + $json->totalItems = 0; if ( $user_id > 0 ) { - $count_posts = \count_user_posts( $user_id, $post_types, true ); - $json->totalItems = \intval( $count_posts ); // phpcs:ignore + $count_posts = \count_user_posts( $user_id, $post_types, true ); + $json->totalItems = \intval( $count_posts ); } else { foreach ( $post_types as $post_type ) { - $count_posts = \wp_count_posts( $post_type ); - $json->totalItems += \intval( $count_posts->publish ); // phpcs:ignore + $count_posts = \wp_count_posts( $post_type ); + $json->totalItems += \intval( $count_posts->publish ); } } - $json->first = \add_query_arg( 'page', 1, $json->partOf ); // phpcs:ignore - $json->last = \add_query_arg( 'page', \ceil ( $json->totalItems / 10 ), $json->partOf ); // phpcs:ignore + $json->first = \add_query_arg( 'page', 1, $json->partOf ); + $json->last = \add_query_arg( 'page', \ceil( $json->totalItems / 10 ), $json->partOf ); - if ( $page && ( ( \ceil ( $json->totalItems / 10 ) ) > $page ) ) { // phpcs:ignore - $json->next = \add_query_arg( 'page', $page + 1, $json->partOf ); // phpcs:ignore + if ( $page && ( ( \ceil( $json->totalItems / 10 ) ) > $page ) ) { + $json->next = \add_query_arg( 'page', $page + 1, $json->partOf ); } - if ( $page && ( $page > 1 ) ) { // phpcs:ignore - $json->prev = \add_query_arg( 'page', $page - 1, $json->partOf ); // phpcs:ignore + if ( $page && ( $page > 1 ) ) { + $json->prev = \add_query_arg( 'page', $page - 1, $json->partOf ); } + // phpcs:enable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase if ( $page ) { $posts = \get_posts( @@ -121,15 +128,19 @@ public static function user_outbox_get( $request ) { $activity = new Activity(); $activity->set_type( 'Create' ); $activity->set_object( $post ); - $json->orderedItems[] = $activity->to_array( false ); // phpcs:ignore + $json->orderedItems[] = $activity->to_array( false ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase } } - // filter output + /** + * Filter the ActivityPub outbox array. + * + * @param array $json The ActivityPub outbox array. + */ $json = \apply_filters( 'activitypub_rest_outbox_array', $json ); - /* - * Action triggerd after the ActivityPub profile has been created and sent to the client + /** + * Action triggered after the ActivityPub profile has been created and sent to the client */ \do_action( 'activitypub_outbox_post' ); @@ -140,21 +151,21 @@ public static function user_outbox_get( $request ) { } /** - * The supported parameters + * The supported parameters. * - * @return array list of parameters + * @return array List of parameters. */ public static function request_parameters() { $params = array(); $params['page'] = array( - 'type' => 'integer', + 'type' => 'integer', 'default' => 1, ); $params['user_id'] = array( 'required' => true, - 'type' => 'string', + 'type' => 'string', ); return $params; diff --git a/includes/rest/class-server.php b/includes/rest/class-server.php index a910e367f..e7cf57b21 100644 --- a/includes/rest/class-server.php +++ b/includes/rest/class-server.php @@ -1,14 +1,20 @@ get_route(); - // check if it is an activitypub request and exclude webfinger and nodeinfo endpoints + // Check if it is an activitypub request and exclude webfinger and nodeinfo endpoints. if ( ! \str_starts_with( $route, '/' . ACTIVITYPUB_REST_NAMESPACE ) || \str_starts_with( $route, '/' . \trailingslashit( ACTIVITYPUB_REST_NAMESPACE ) . 'webfinger' ) || @@ -95,13 +102,13 @@ public static function authorize_activitypub_requests( $response, $handler, $req } /** - * Filter to defer signature verification + * Filter to defer signature verification. * * Skip signature verification for debugging purposes or to reduce load for * certain Activity-Types, like "Delete". * - * @param bool $defer Whether to defer signature verification. - * @param WP_REST_Request $request The request used to generate the response. + * @param bool $defer Whether to defer signature verification. + * @param \WP_REST_Request $request The request used to generate the response. * * @return bool Whether to defer signature verification. */ @@ -112,9 +119,9 @@ public static function authorize_activitypub_requests( $response, $handler, $req } if ( - // POST-Requests are always signed + // POST-Requests are always signed. 'GET' !== $request->get_method() || - // GET-Requests only require a signature in secure mode + // GET-Requests only require a signature in secure mode. ( 'GET' === $request->get_method() && ACTIVITYPUB_AUTHORIZED_FETCH ) ) { $verified_request = Signature::verify_http_signature( $request ); @@ -133,10 +140,10 @@ public static function authorize_activitypub_requests( $response, $handler, $req /** * Callback function to validate incoming ActivityPub requests * - * @param WP_REST_Response|WP_HTTP_Response|WP_Error|mixed $response Result to send to the client. - * Usually a WP_REST_Response or WP_Error. - * @param array $handler Route handler used for the request. - * @param WP_REST_Request $request Request used to generate the response. + * @param WP_REST_Response|\WP_HTTP_Response|WP_Error|mixed $response Result to send to the client. + * Usually a WP_REST_Response or WP_Error. + * @param array $handler Route handler used for the request. + * @param \WP_REST_Request $request Request used to generate the response. * * @return mixed|WP_Error The response, error, or modified response. */ @@ -156,7 +163,7 @@ public static function validate_activitypub_requests( $response, $handler, $requ $params = $request->get_json_params(); - // Type is required for ActivityPub requests, so it fail later in the process + // Type is required for ActivityPub requests, so it fail later in the process. if ( ! isset( $params['type'] ) ) { return $response; } @@ -176,4 +183,32 @@ public static function validate_activitypub_requests( $response, $handler, $requ return $response; } + + /** + * Modify the parameter priority order for a REST API request. + * + * @param string[] $order Array of types to check, in order of priority. + * @param WP_REST_Request $request The request object. + * + * @return string[] The modified order of types to check. + */ + public static function request_parameter_order( $order, $request ) { + $route = $request->get_route(); + + // Check if it is an activitypub request and exclude webfinger and nodeinfo endpoints. + if ( ! \str_starts_with( $route, '/' . ACTIVITYPUB_REST_NAMESPACE ) ) { + return $order; + } + + $type = $request->get_method(); + + if ( WP_REST_Server::CREATABLE !== $type ) { + return $order; + } + + return array( + 'POST', + 'defaults', + ); + } } diff --git a/includes/rest/class-webfinger.php b/includes/rest/class-webfinger.php index 33aed4651..868ed50d2 100644 --- a/includes/rest/class-webfinger.php +++ b/includes/rest/class-webfinger.php @@ -1,11 +1,16 @@ '*', - 'Content-Type' => 'application/jrd+json; charset=' . get_option( 'blog_charset' ), + 'Content-Type' => 'application/jrd+json; charset=' . get_option( 'blog_charset' ), ) ); } /** - * The supported parameters + * The supported parameters. * * @return array list of parameters */ @@ -88,8 +89,8 @@ public static function request_parameters() { $params['resource'] = array( 'required' => true, - 'type' => 'string', - 'pattern' => '^(acct:)|^(https?://)(.+)$', + 'type' => 'string', + 'pattern' => '^(acct:)|^(https?://)(.+)$', ); return $params; @@ -98,11 +99,17 @@ public static function request_parameters() { /** * Get the WebFinger profile. * - * @param string $resource the WebFinger resource. + * @param string $webfinger the WebFinger resource. * - * @return array the WebFinger profile. + * @return array|\WP_Error The WebFinger profile or WP_Error if not found. */ - public static function get_profile( $resource ) { // phpcs:ignore - return apply_filters( 'webfinger_data', array(), $resource ); + public static function get_profile( $webfinger ) { + /** + * Filter the WebFinger data. + * + * @param array $data The WebFinger data. + * @param string $webfinger The WebFinger resource. + */ + return apply_filters( 'webfinger_data', array(), $webfinger ); } } diff --git a/includes/table/class-followers.php b/includes/table/class-followers.php index df9747bd2..991f97c1d 100644 --- a/includes/table/class-followers.php +++ b/includes/table/class-followers.php @@ -1,4 +1,10 @@ id === 'settings_page_activitypub' ) { $this->user_id = Users::BLOG_USER_ID; @@ -30,6 +47,11 @@ public function __construct() { ); } + /** + * Get columns. + * + * @return array + */ public function get_columns() { return array( 'cb' => '', @@ -42,16 +64,22 @@ public function get_columns() { ); } + /** + * Returns sortable columns. + * + * @return array + */ public function get_sortable_columns() { - $sortable_columns = array( + return array( 'post_title' => array( 'post_title', true ), 'modified' => array( 'modified', false ), 'published' => array( 'published', false ), ); - - return $sortable_columns; } + /** + * Prepare items. + */ public function prepare_items() { $columns = $this->get_columns(); $hidden = array(); @@ -64,26 +92,22 @@ public function prepare_items() { $args = array(); - // phpcs:ignore WordPress.Security.NonceVerification.Recommended + // phpcs:disable WordPress.Security.NonceVerification.Recommended if ( isset( $_GET['orderby'] ) ) { - // phpcs:ignore WordPress.Security.NonceVerification.Recommended $args['orderby'] = sanitize_text_field( wp_unslash( $_GET['orderby'] ) ); } - // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( isset( $_GET['order'] ) ) { - // phpcs:ignore WordPress.Security.NonceVerification.Recommended $args['order'] = sanitize_text_field( wp_unslash( $_GET['order'] ) ); } - // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( isset( $_GET['s'] ) && isset( $_REQUEST['_wpnonce'] ) ) { $nonce = sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) ); if ( wp_verify_nonce( $nonce, 'bulk-' . $this->_args['plural'] ) ) { - // phpcs:ignore WordPress.Security.NonceVerification.Recommended $args['s'] = sanitize_text_field( wp_unslash( $_GET['s'] ) ); } } + // phpcs:enable WordPress.Security.NonceVerification.Recommended $followers_with_count = FollowerCollection::get_followers_with_count( $this->user_id, $per_page, $page_num, $args ); $followers = $followers_with_count['followers']; @@ -113,12 +137,24 @@ public function prepare_items() { } } + /** + * Returns bulk actions. + * + * @return array + */ public function get_bulk_actions() { return array( 'delete' => __( 'Delete', 'activitypub' ), ); } + /** + * Column default. + * + * @param array $item Item. + * @param string $column_name Column name. + * @return string + */ public function column_default( $item, $column_name ) { if ( ! array_key_exists( $column_name, $item ) ) { return __( 'None', 'activitypub' ); @@ -126,6 +162,12 @@ public function column_default( $item, $column_name ) { return $item[ $column_name ]; } + /** + * Column avatar. + * + * @param array $item Item. + * @return string + */ public function column_avatar( $item ) { return sprintf( '